-
Notifications
You must be signed in to change notification settings - Fork 15
Templates Invoice
Applies to:
TEMPLATE_INVOICE_PDFTEMPLATE_INVOICE_EMAIL
Both template types share the same render context. The email variant is used by the workflow engine to send invoice-related emails (e.g. payment reminders). PDF-specific elements like <div class="header"> / <div class="footer"> are not supported in email templates.
-
invoice(Invoice) -
vats(Array, key/value) -
brutto,netto,bruttoFormated,nettoFormated -
periods,numbers(Arrays) -
appartmentTotal,miscTotal
vats is an associative array grouped by VAT rate.
- Key: VAT rate (for example
7,19) - Value: object with these fields:
-
value.brutto: gross amount for this VAT rate -
value.netto: VAT amount for this VAT rate -
value.netSum: net base amount (brutto - netto) for this VAT rate -
value.nettoFormated: formatted VAT amount (string,de_DEnumber format)
Example shape:
vats = {
7: { brutto: 107.00, netto: 7.00, netSum: 100.00, nettoFormated: '7,00' },
19: { brutto: 119.00, netto: 19.00, netSum: 100.00, nettoFormated: '19,00' }
}Note on naming:
- top-level
brutto= total gross sum - top-level
netto= total VAT sum - top-level
nettoFormated= formatted net base sum (brutto - netto)
Header information:
<p>Invoice [[ invoice.number ]] from [[ invoice.date|date('d.m.Y') ]]</p>VAT key/value loop:
<tr data-repeat="vats" data-repeat-key="key" data-repeat-as="value">
<td>[[ key ]] %</td>
<td>Net base: [[ value.netSum ]] €</td>
<td>VAT: [[ value.nettoFormated ]] €</td>
<td>Gross: [[ value.brutto ]] €</td>
</tr>Hiding the 0% bucket on display:
The vats array intentionally keeps a 0 bucket when zero-rated positions exist — its brutto is still part of the invoice total and is summed up in brutto / netto. If you want to hide it use filter as shown below:
<tr data-repeat="vats|filter((v, k) => k > 0)" data-repeat-key="key" data-repeat-as="value">
<td>[[ key ]] %</td>
<td>[[ value.nettoFormated ]] €</td>
</tr>Apartment positions:
<tr data-repeat="invoice.appartments" data-repeat-as="appartment">
<td>[[ appartment.description ]]</td>
<td>[[ appartment.totalPrice ]] €</td>
</tr>Every entry of invoice.positions (an InvoicePosition) carries an optional positionGroup marker that allows you to render different kinds of positions in dedicated tables. Possible values:
-
apartment_modifier— surcharges / discounts attached to an apartment (e.g. last-minute discount, pet fee). Rendered in the UI in a separate table above the tourist-tax table. -
tourist_tax— tourist tax / accommodation tax (Kurtaxe / Beherbergungsabgabe). One position per guest/night bucket; rendered in the UI in a dedicated table. -
nullor any other value — ancillary services / "weitere Leistungen" (breakfast, cleaning fee, …).
The total miscTotal is the sum of all entries of invoice.positions, including modifier and tourist-tax positions. If you split them into separate tables, decide whether you want a single grand total at the bottom (recommended, keep using [[ bruttoFormated ]]) or per-group subtotals via Twig {% set %}.
Use Twig's |filter inside data-repeat and data-if:
<!-- Beherbergungsabgabe / Kurtaxe -->
<table data-if="invoice.positions|filter(p => p.positionGroup == 'tourist_tax')|length > 0" style="width: 100%;">
<thead>
<tr><th>Beherbergungsabgabe</th><th>Anzahl</th><th>Einzelpreis</th><th>MwSt.</th><th>Gesamt</th></tr>
</thead>
<tbody>
<tr data-repeat="invoice.positions|filter(p => p.positionGroup == 'tourist_tax')" data-repeat-as="position">
<td>[[ position.description ]]</td>
<td>[[ position.amount ]]</td>
<td>[[ position.priceFormated ]] €</td>
<td>[[ position.vat ]]</td>
<td>[[ position.totalPrice ]] €</td>
</tr>
</tbody>
</table>
<!-- Aufschläge / Ermäßigungen -->
<table data-if="invoice.positions|filter(p => p.positionGroup == 'apartment_modifier')|length > 0" style="width: 100%;">
<thead>
<tr><th>Aufschläge / Ermäßigungen</th><th>Anzahl</th><th>Einzelpreis</th><th>MwSt.</th><th>Gesamt</th></tr>
</thead>
<tbody>
<tr data-repeat="invoice.positions|filter(p => p.positionGroup == 'apartment_modifier')" data-repeat-as="position">
<td>[[ position.description ]]</td>
<td>[[ position.amount ]]</td>
<td>[[ position.priceFormated ]] €</td>
<td>[[ position.vat ]]</td>
<td>[[ position.totalPrice ]] €</td>
</tr>
</tbody>
</table>
<!-- weitere Leistungen (alles außer tourist_tax und apartment_modifier) -->
<tr data-repeat="invoice.positions|filter(p => p.positionGroup != 'tourist_tax' and p.positionGroup != 'apartment_modifier')" data-repeat-as="position">
<td>[[ position.description ]]</td>
<td>[[ position.totalPrice ]] €</td>
</tr>Notes:
-
data-ifevaluates a plain Twig expression, so|filter(...)|length > 0is the idiomatic way to hide an entire table when no positions of that group exist. - Filter expressions used in
data-repeatare written verbatim into the generated{% for %}, so any Twig-compatible expression (|filter,|sort,|slice, …) is allowed.