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
PDF

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>