All examples
Invoice · Free-form form with payment slip
One complete invoice per record, built from page blocks with declared heights. The Swiss QR payment slip stays on page 1 when it fits and moves to page 2 when the invoice positions need the space.
Strict
Free-form
Pagination
/reports/invoice_example/run
What is typical here
- freeform() renders one complete form per detail record
- Every section is an <x-reports::page-block> with a declared height
- The payment slip is anchored with anchor="bottom": 105 mm from the paper edge, overlaying the footer
- pageNumbering(perForm) restarts the page counter for every invoice
- The page header with invoice number and customer is hidden on page 1
- A block taller than the page body throws a BandOverflowException
Module structure
app/Reports/InvoiceExample/
├── Report.php # freeform(), strict, pageNumbering(perForm)
├── Source.php # two demo invoices
└── views/
├── invoice.blade.php # detail: page-block per invoice section
├── header.blade.php # page 2+ header
└── footer.blade.php
Files
<?php
declare(strict_types=1);
namespace App\Reports\InvoiceExample;
use Guggach\Reports\Definition\PageSetup;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Definition\ReportMode;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;
/**
* Free-form demo: a complete invoice in one detail view. The payment slip is a
* page block; it stays on page 1 when it fits and moves to page 2 otherwise.
*/
final class Report extends BaseReport
{
public function key(): string
{
return 'invoice_example';
}
public function name(): string
{
return 'Rechnung (Beispiel)';
}
public function layout(): string
{
return 'default';
}
public function source(): ReportSource
{
return new Source;
}
public function define(ReportBuilder $r): void
{
$r->mode(ReportMode::Strict);
$r->pageSetup(PageSetup::a4()->portrait()->marginsMm(18, 16, 18, 16));
$r->pageNumbering('perForm');
// One complete invoice per detail record. The page blocks in
// views/invoice.blade.php declare their heights (spec 6.8).
$r->freeform(view: 'invoice');
// On page 1 the invoice starts with the brand/address block; the page
// header with the invoice number is only needed from page 2 onwards.
$r->pageHeader(view: 'header')->height(22)->hideOnFirstPage();
$r->pageFooter(view: 'footer')->height(10);
}
}
<?php
declare(strict_types=1);
namespace App\Reports\InvoiceExample;
use Guggach\Reports\Sources\ReportSource;
/**
* Two demo invoices.
*
* Invoice 1 has two positions, so the payment slip still fits on page 1.
* Invoice 2 has eight positions, which pushes the payment slip to page 2.
*/
final class Source extends ReportSource
{
public function records(): iterable
{
return [
[
'invoice_number' => 'R-265040063001',
'invoice_date' => '18.09.2026',
'customer' => 'Example GmbH',
'address' => ['Pfingstweidstrasse 1', '8031 Zürich'],
'vat_number' => 'CHE-106.833.447 MWST',
'due' => 'Fällig bei Erhalt der Rechnung netto',
'positions' => [
[
'description' => 'Miete Apartment Kategorie City / Gast Peter Müller',
'period' => '01.11.2026 – 21.11.2026',
'quantity' => '20 Nächte',
'unit_price' => 106.67,
'total' => 2133.35,
'vat' => 3.8,
],
[
'description' => 'Service und Reinigung',
'period' => '01.11.2026 – 21.11.2026',
'quantity' => '20 Nächte',
'unit_price' => 13.33,
'total' => 266.65,
'vat' => 3.8,
],
],
'invoice_total' => 2400.00,
'invoice_vat_total' => 87.85,
],
[
'invoice_number' => 'R-265040063002',
'invoice_date' => '18.09.2026',
'customer' => 'Alpine Studio GmbH',
'address' => ['Seefeldstrasse 999', '8008 Zürich'],
'vat_number' => 'CHE-106.833.447 MWST',
'due' => 'Fällig bei Erhalt der Rechnung netto',
'positions' => [
['description' => 'Miete Apartment Kategorie Studio', 'period' => '01.12.2026 – 21.12.2026', 'quantity' => '20 Nächte', 'unit_price' => 120.00, 'total' => 2400.00, 'vat' => 3.8],
['description' => 'Service und Reinigung', 'period' => '01.12.2026 – 21.12.2026', 'quantity' => '20 Nächte', 'unit_price' => 15.00, 'total' => 300.00, 'vat' => 3.8],
['description' => 'Zusätzliche Bettwäsche', 'period' => '01.12.2026', 'quantity' => '2 Sets', 'unit_price' => 25.00, 'total' => 50.00, 'vat' => 3.8],
['description' => 'Parkplatz', 'period' => '01.12.2026 – 21.12.2026', 'quantity' => '20 Tage', 'unit_price' => 12.00, 'total' => 240.00, 'vat' => 3.8],
['description' => 'Endreinigung', 'period' => '21.12.2026', 'quantity' => '1', 'unit_price' => 90.00, 'total' => 90.00, 'vat' => 3.8],
['description' => 'Kurtaxe', 'period' => '01.12.2026 – 21.12.2026', 'quantity' => '40', 'unit_price' => 3.50, 'total' => 140.00, 'vat' => 3.8],
['description' => 'Frühstückskorb', 'period' => '01.12.2026', 'quantity' => '1', 'unit_price' => 45.00, 'total' => 45.00, 'vat' => 3.8],
['description' => 'Late Check-out', 'period' => '21.12.2026', 'quantity' => '1', 'unit_price' => 35.00, 'total' => 35.00, 'vat' => 3.8],
],
'invoice_total' => 3300.00,
'invoice_vat_total' => 120.81,
],
];
}
}
<x-reports::page-block :page="$page" :height="0">
<style>
.invoice-brand {
display: flex;
justify-content: space-between;
align-items: flex-end;
border-bottom: 3px solid #0f766e;
padding-bottom: 4mm;
height: 100%;
box-sizing: border-box;
}
.invoice-brand__name { font-size: 24px; font-weight: 800; letter-spacing: .02em; color: #0f766e; }
.invoice-brand__meta { text-align: right; font-size: 9px; line-height: 1.5; color: #475569; }
.invoice-address { font-size: 10px; line-height: 1.55; color: #1f2937; padding-top: 6mm; box-sizing: border-box; }
.invoice-address__label { margin-bottom: 3mm; font-size: 9px; color: #64748b; text-transform: uppercase; letter-spacing: .08em; }
.invoice-title { display: flex; justify-content: space-between; align-items: flex-end; border-bottom: 1px solid #cbd5e1; padding-bottom: 3mm; box-sizing: border-box; }
.invoice-title h1 { margin: 0; font-size: 22px; line-height: 1; color: #0f172a; }
.invoice-title__meta { text-align: right; font-size: 10px; line-height: 1.5; color: #475569; }
.invoice-table-head,
.invoice-table-row,
.invoice-total {
display: grid;
grid-template-columns: minmax(0, 1fr) 34mm 24mm 28mm 26mm 16mm;
align-items: center;
column-gap: 2mm;
box-sizing: border-box;
}
.invoice-table-head {
border-bottom: 2px solid #0f766e;
font-size: 8px;
font-weight: 700;
line-height: 1.25;
color: #0f172a;
}
.invoice-table-row {
border-bottom: 1px solid #e2e8f0;
font-size: 8px;
line-height: 1.3;
color: #334155;
overflow: hidden;
}
.invoice-table-row__description { font-weight: 600; color: #0f172a; }
.invoice-table-row__period { color: #64748b; font-size: 7px; }
.invoice-table-row .right,
.invoice-table-head .right { text-align: right; }
.invoice-total {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 2px solid #0f172a;
border-bottom: 1px solid #cbd5e1;
font-size: 11px;
font-weight: 700;
}
.invoice-total__value { text-align: right; }
.invoice-note { padding-top: 3mm; font-size: 8px; line-height: 1.5; color: #64748b; }
.payment-slip {
box-sizing: border-box;
height: 100%;
overflow: hidden;
background: #fff;
}
.payment-slip img { display: block; width: 100%; height: 100%; object-fit: fill; }
</style>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="24">
<div class="invoice-brand">
<div>
<div class="invoice-brand__name">Acme AG</div>
<div class="invoice-brand__meta" style="text-align:left">Apartments · Immobilien · IfN</div>
</div>
<div class="invoice-brand__meta">
Beispielstrasse 1<br>
8000 Zürich<br>
info@acme.example
</div>
</div>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="36">
<div class="invoice-address">
<div class="invoice-address__label">Rechnung für</div>
<strong>{{ $ctx->customer }}</strong><br>
@foreach ($ctx->address as $line)
{{ $line }}<br>
@endforeach
<span style="color:#64748b">MWST Nr.: {{ $ctx->vat_number }}</span>
</div>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="16">
<div class="invoice-title">
<h1>Rechnung</h1>
<div class="invoice-title__meta">
{{ $ctx->invoice_number }}<br>
{{ $ctx->invoice_date }}
</div>
</div>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="8">
<div class="invoice-table-head">
<span>Produkt / Beschreibung</span>
<span>von – bis</span>
<span>Menge</span>
<span>Preis</span>
<span>Betrag</span>
<span class="right">MWST</span>
</div>
</x-reports::page-block>
@foreach ($ctx->positions as $position)
<x-reports::page-block :page="$page" :height="10">
<div class="invoice-table-row">
<div>
<div class="invoice-table-row__description">{{ $position['description'] }}</div>
<div class="invoice-table-row__period">{{ $position['period'] }}</div>
</div>
<span>{{ $position['period'] }}</span>
<span>{{ $position['quantity'] }}</span>
<span>{{ number_format($position['unit_price'], 2, '.', "'") }} / Einheit</span>
<span>{{ number_format($position['total'], 2, '.', "'") }}</span>
<span class="right">{{ $position['vat'] }}%</span>
</div>
</x-reports::page-block>
@endforeach
<x-reports::page-block :page="$page" :height="14">
<div class="invoice-total">
<span class="invoice-total__label">Rechnungstotal</span>
<span class="invoice-total__value">CHF {{ number_format($ctx->invoice_total, 2, '.', "'") }}</span>
</div>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="10">
<div class="invoice-note">
MWST Betrag CHF {{ number_format($ctx->invoice_vat_total, 2, '.', "'") }} · {{ $ctx->due }}
</div>
</x-reports::page-block>
<x-reports::page-block :page="$page" :height="105" anchor="bottom">
<div class="payment-slip">
<img src="{{ asset('images/docs/swissqr-payment-slip.png') }}" alt="Swiss QR Einzahlungsschein">
</div>
</x-reports::page-block>
<style>
.invoice-page-head {
display: grid;
grid-template-columns: 1fr 1fr auto;
align-items: end;
column-gap: 6mm;
height: 100%;
box-sizing: border-box;
border-bottom: 1px solid #cbd5e1;
padding-bottom: 2mm;
font-size: 9px;
line-height: 1.5;
color: #334155;
}
.invoice-page-head strong { color: #0f172a; }
.invoice-page-head__label { color: #64748b; font-size: 8px; text-transform: uppercase; letter-spacing: .08em; }
</style>
<div class="invoice-page-head">
<div>
<div class="invoice-page-head__label">Rechnung Nr.</div>
<strong>{{ $ctx->invoice_number }}</strong>
</div>
<div>
<div class="invoice-page-head__label">Rechnung für</div>
<strong>{{ $ctx->customer }}</strong>
</div>
<div style="text-align:right">
<div class="invoice-page-head__label">Seite</div>
<strong>{{ $ctx->page['number'] }} / {{ $ctx->page['total'] }}</strong>
</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:flex-end; height:100%; box-sizing:border-box; border-top:1px solid #cbd5e1; padding-top:2mm; font-size:8px; color:#64748b;">
<span>Acme AG · Beispielstrasse 1 · 8000 Zürich · CHE-106.833.447 MWST</span>
<span>Seite {{ $ctx->page['number'] }} / {{ $ctx->page['total'] }}</span>
</div>