CluPilotCloud/resources/views/pdf/invoice.blade.php

157 lines
8.6 KiB
PHP

{{--
The body of an invoice, for TCPDF's writeHTML().
Tables and inline styles, and a narrower dialect than even an email: TCPDF
understands a small subset of HTML and almost no CSS. No flexbox, no grid,
no shorthand borders on table cells, no percentage padding. What works is a
table with widths, and text-align.
Every value comes from the frozen snapshot ($s). Nothing here reads a
setting or a model — that is the whole reason no PDF has to be stored.
--}}
@php
use App\Services\Billing\InvoiceMath;
$issuer = $s['issuer'] ?? [];
$customer = $s['customer'] ?? [];
$meta = $s['meta'] ?? [];
$lines = $s['lines'] ?? [];
$totals = $s['totals'] ?? [];
@endphp
{{-- Sender line above the address, small — the line that shows through a
window envelope and tells the postman where to return it. --}}
<table cellpadding="0" cellspacing="0" style="width:100%;">
<tr>
<td style="width:58%; font-size:6.5pt; color:#6e6e7a; border-bottom:0.2mm solid #b4b4be;">
{{ collect([$issuer['name'] ?? null, $issuer['address'] ?? null, trim(($issuer['postcode'] ?? '').' '.($issuer['city'] ?? '')), $issuer['country'] ?? null])->filter()->join(' | ') }}
</td>
<td style="width:42%;"></td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" style="width:100%; margin-top:2mm;">
<tr>
{{-- No leading whitespace inside these cells. TCPDF prints it: every indented
line came out indented, and only the last one — which had none — sat flush
against the margin. --}}
{{-- Address lines where the record keeps one free-text field, structured
fields where it keeps them apart. Guessing which free-text line is the
postcode would put it where the street belongs, on a document nobody can
correct afterwards. --}}
<td style="width:58%; font-size:10pt; line-height:145%;"><b>{{ $customer['name'] ?? '' }}</b>@if (! empty($customer['address_lines']))@foreach ($customer['address_lines'] as $addressLine)<br>{{ $addressLine }}@endforeach@else<br>{{ $customer['address'] ?? '' }}<br>{{ trim(($customer['postcode'] ?? '').' '.($customer['city'] ?? '')) }}<br>{{ $customer['country'] ?? '' }}@endif@if (! empty($customer['vat_id']))<br>{{ __('invoice.customer_vat') }}: {{ $customer['vat_id'] }}@endif</td>
<td style="width:42%;"></td>
</tr>
</table>
{{-- The data block, right, level with the address. Its own table because
TCPDF has no way to float one beside the other. --}}
<table cellpadding="1" cellspacing="0" style="width:100%; margin-top:14mm; font-size:9pt;">
<tr><td style="width:55%;"></td>
<td style="width:22%; color:#43434e;">{{ __('invoice.date') }}</td>
<td style="width:23%; text-align:right;">{{ $meta['issued_on'] ?? '' }}</td></tr>
@if (! empty($meta['customer_number']))
<tr><td></td>
<td style="color:#43434e;">{{ __('invoice.customer_number') }}</td>
<td style="text-align:right;">{{ $meta['customer_number'] }}</td></tr>
@endif
<tr><td></td>
<td style="color:#43434e;">{{ __('invoice.number') }}</td>
<td style="text-align:right;"><b>{{ $meta['number'] ?? '' }}</b></td></tr>
@if (! empty($meta['due_on']))
<tr><td></td>
<td style="color:#43434e;">{{ __('invoice.due') }}</td>
<td style="text-align:right;">{{ $meta['due_on'] }}</td></tr>
@endif
</table>
{{-- One block, not a heading with a paragraph beneath it: TCPDF gives an h1 a
margin of its own and the two drifted a centimetre apart. --}}
<table cellpadding="0" cellspacing="0" style="width:100%; margin-top:9mm;">
<tr><td style="font-size:16pt; font-weight:bold; line-height:125%;">{{ $meta['title'] ?? __('invoice.title') }}<br>{{ $meta['number'] ?? '' }}</td></tr>
</table>
@if (! empty($meta['salutation']))
<p style="font-size:9.5pt; margin-top:6mm;">{{ $meta['salutation'] }}</p>
@endif
@if (! empty($meta['intro']))
<p style="font-size:9.5pt; line-height:150%;">{{ $meta['intro'] }}</p>
@endif
{{-- Quantity, net unit price, net total AND gross total per line: the invoice
goes to businesses and to consumers, and each reads a different column
first. --}}
<table cellpadding="3" cellspacing="0" style="width:100%; margin-top:6mm; font-size:8.5pt;">
<tr style="font-weight:bold; background-color:#fafafb;">
<td style="width:7%; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_pos') }}</td>
<td style="width:41%; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_description') }}</td>
<td style="width:12%; text-align:right; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_quantity') }}</td>
<td style="width:14%; text-align:right; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_unit_net') }}</td>
{{-- A unit price, not a line total. Nobody adds up unit prices, so this
column cannot contradict the sum — which a full-price gross TOTAL
column would, by 315,60 against 288,60. --}}
<td style="width:13%; text-align:right; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_unit_gross') }}</td>
<td style="width:13%; text-align:right; border-bottom:0.2mm solid #b4b4be;">{{ __('invoice.col_total_net') }}</td>
</tr>
@foreach ($lines as $index => $line)
@php
// Full price throughout. The discount is collected and shown once,
// in euros, beneath the subtotal — a line printed already-reduced
// leaves the reader guessing whether it is reduced.
$rate = (int) ($totals['rate_basis_points'] ?? 2000);
$net = InvoiceMath::lineNet((int) $line['unit_net_cents'], (int) $line['quantity_milli']);
$unitGross = (int) $line['unit_net_cents'] + InvoiceMath::tax((int) $line['unit_net_cents'], $rate);
@endphp
<tr>
<td style="border-bottom:0.1mm solid #e9e9ee;">{{ $index + 1 }}</td>
<td style="border-bottom:0.1mm solid #e9e9ee;">{{ $line['description'] ?? '' }}@foreach (($line['details'] ?? []) as $detail)<br><span style="color:#6e6e7a;">{{ $detail }}</span>@endforeach</td>
<td style="text-align:right; border-bottom:0.1mm solid #e9e9ee;">{{ InvoiceMath::quantity((int) $line['quantity_milli']) }}@if (! empty($line['unit'])) {{ $line['unit'] }}@endif</td>
<td style="text-align:right; border-bottom:0.1mm solid #e9e9ee;">{{ InvoiceMath::money((int) $line['unit_net_cents']) }}</td>
<td style="text-align:right; border-bottom:0.1mm solid #e9e9ee;">{{ InvoiceMath::money($unitGross) }}</td>
<td style="text-align:right; border-bottom:0.1mm solid #e9e9ee;">{{ InvoiceMath::money($net) }}</td>
</tr>
@endforeach
</table>
<table cellpadding="3" cellspacing="0" style="width:100%; margin-top:4mm; font-size:9pt;">
{{-- The subtotal is what the "Gesamt netto" column adds up to. Without it
the discount below has nothing to be a discount OF, which is exactly
what made the previous version unreadable. --}}
@if (! empty($totals['adjustment']))
<tr>
<td style="width:60%;"></td>
<td style="width:25%;">{{ __('invoice.subtotal') }}</td>
<td style="width:15%; text-align:right;">{{ InvoiceMath::money((int) ($totals['subtotal'] ?? 0)) }}</td>
</tr>
<tr>
<td></td>
<td>{{ $meta['adjustment_label'] ?? __('invoice.adjustment') }}</td>
<td style="text-align:right;">{{ InvoiceMath::money((int) $totals['adjustment']) }}</td>
</tr>
@endif
<tr>
<td style="width:60%;"></td>
<td style="width:25%;">{{ __('invoice.net') }}</td>
<td style="width:15%; text-align:right;">{{ InvoiceMath::money((int) ($totals['net'] ?? 0)) }}</td>
</tr>
<tr>
<td></td>
<td>{{ __('invoice.tax', ['rate' => rtrim(rtrim(number_format(($totals['rate_basis_points'] ?? 2000) / 100, 2, ',', '.'), '0'), ',')]) }}</td>
<td style="text-align:right;">{{ InvoiceMath::money((int) ($totals['tax'] ?? 0)) }}</td>
</tr>
<tr style="font-weight:bold;">
<td></td>
<td style="border-top:0.2mm solid #b4b4be;">{{ __('invoice.gross') }}</td>
<td style="text-align:right; border-top:0.2mm solid #b4b4be;">{{ InvoiceMath::money((int) ($totals['gross'] ?? 0)) }} {{ $meta['currency'] ?? 'EUR' }}</td>
</tr>
</table>
@if (! empty($meta['payment_terms']))
<p style="font-size:9pt; margin-top:10mm;"><b>{{ __('invoice.payment_title') }}</b></p>
<p style="font-size:9pt; line-height:150%; margin-top:0;">{{ $meta['payment_terms'] }}</p>
@endif
@if (! empty($meta['closing']))
<p style="font-size:9pt; line-height:150%; margin-top:6mm;">{{ $meta['closing'] }}</p>
@endif