Created
February 4, 2026 15:39
-
-
Save felixdorn/b702f7ba7da472d21e50a19c36b9bbdc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Variables injected by the generator | |
| #let invoice_number = "{{.Number}}" | |
| #let date_year = {{.DateYear}} | |
| #let date_month = {{.DateMonth}} | |
| #let date_day = {{.DateDay}} | |
| #let payment_terms = "{{.PaymentTerms}}" | |
| #let due_year = {{.DueYear}} | |
| #let due_month = {{.DueMonth}} | |
| #let due_day = {{.DueDay}} | |
| #let client_name = "{{.ClientName}}" | |
| #let client_address = "{{.ClientAddress}}" | |
| #let client_email = "{{.ClientEmail}}" | |
| #let client_extra = "{{.ClientExtra}}" | |
| #let business_name = "{{.BusinessName}}" | |
| #let business_address = "{{.BusinessAddress}}" | |
| #let business_email = "{{.BusinessEmail}}" | |
| #let business_phone = "{{.BusinessPhone}}" | |
| #let business_siret = "{{.BusinessSIRET}}" | |
| #let total = "{{.Total}}".replace("\\$", "$") | |
| #let currency = "{{.Currency}}" | |
| #let lang = "{{.Language}}" | |
| #let account_holder = "{{.AccountHolder}}" | |
| #let account_bic = "{{.AccountBIC}}" | |
| #let account_iban = "{{.AccountIBAN}}" | |
| #let account_number = "{{.AccountNumber}}" | |
| #let routing_number = "{{.RoutingNumber}}" | |
| #let account_type = "{{.AccountType}}" | |
| #let bank_name = "{{.BankName}}" | |
| #let bank_address = "{{.BankAddress}}" | |
| #let reference = "{{.Reference}}" | |
| #let internal_ref = "{{.InternalRef}}" | |
| // Line items (injected) | |
| {{.LineItemsTypst}} | |
| // Design tokens | |
| #let accent = rgb("#0065d8") | |
| #let gray-50 = rgb("#f9fafb") | |
| #let gray-200 = rgb("#e5e7eb") | |
| #let gray-400 = rgb("#9ca3af") | |
| #let gray-500 = rgb("#6b7280") | |
| #let gray-900 = rgb("#111827") | |
| // Document setup | |
| #set page( | |
| margin: (x: 50pt, y: 50pt), | |
| footer: align(left)[#text(size: 8pt, fill: gray-400)[Internal reference: #internal_ref]] | |
| ) | |
| #set text(font: "Inter", size: 9.5pt, fill: gray-900) | |
| #set par(leading: 0.65em) | |
| // French month names | |
| #let french_months = ( | |
| "janvier", "février", "mars", "avril", "mai", "juin", | |
| "juillet", "août", "septembre", "octobre", "novembre", "décembre" | |
| ) | |
| // Date formatting function | |
| #let format_date(year, month, day, language) = { | |
| if language == "FR" { | |
| [#day #french_months.at(month - 1) #year] | |
| } else { | |
| let date = datetime(year: year, month: month, day: day) | |
| date.display("[day] [month repr:long] [year]") | |
| } | |
| } | |
| // Due date formatting (handles payment terms) | |
| #let format_due_date(terms, year, month, day, language) = { | |
| if terms == "DUE_ON_RECEIPT" { | |
| if language == "FR" { "À réception" } else { "Due on receipt" } | |
| } else { | |
| format_date(year, month, day, language) | |
| } | |
| } | |
| // Translations | |
| #let labels = if lang == "FR" { | |
| ( | |
| invoice: "Facture", | |
| from: "Émetteur", | |
| to: "Destinataire", | |
| date_issued: "Date d'émission", | |
| date_due: "Date d'échéance", | |
| invoice_number: "Numéro", | |
| line_items: "Prestations", | |
| description: "Description", | |
| total: "Total", | |
| siret_prefix: "SIRET", | |
| ) | |
| } else { | |
| ( | |
| invoice: "Invoice", | |
| from: "From", | |
| to: "Bill to", | |
| date_issued: "Issued", | |
| date_due: "Due", | |
| invoice_number: "Number", | |
| line_items: "Line items", | |
| description: "Description", | |
| total: "Total", | |
| siret_prefix: "SIRET", | |
| ) | |
| } | |
| // PDF metadata | |
| #set document( | |
| title: labels.invoice + " #" + invoice_number, | |
| author: business_name, | |
| keywords: (internal_ref,), | |
| ) | |
| // Section label helper (for From/To) | |
| #let section-label(content) = { | |
| text(size: 9.5pt, fill: gray-400, weight: "semibold")[#content] | |
| } | |
| // Section heading helper (for Line items/Payment details) | |
| #let section-heading(content) = { | |
| text(size: 14pt, weight: "semibold")[#content] | |
| } | |
| // ============ HEADER ============ | |
| #grid( | |
| columns: (1fr, auto), | |
| align: (left + horizon, right + horizon), | |
| [ | |
| #text(size: 28pt, weight: "bold", fill: accent)[#labels.invoice]#text(size: 28pt, weight: "light", fill: gray-400)[\##invoice_number] | |
| ], | |
| [ | |
| #set text(size: 9pt) | |
| #table( | |
| columns: (auto, auto), | |
| column-gutter: 10pt, | |
| stroke: none, | |
| inset: (x: 0pt, y: 4pt), | |
| align: (right, left), | |
| text(fill: gray-500)[#labels.date_due], [*#format_due_date(payment_terms, due_year, due_month, due_day, lang)*], | |
| text(fill: gray-500)[#labels.date_issued], [#format_date(date_year, date_month, date_day, lang)], | |
| ) | |
| ] | |
| ) | |
| #v(4pt) | |
| #line(length: 100%, stroke: 2.5pt + accent) | |
| #v(24pt) | |
| // ============ FROM / TO ============ | |
| #grid( | |
| columns: (1fr, 1fr), | |
| gutter: 40pt, | |
| [ | |
| #section-label(labels.from) | |
| #v(6pt) | |
| #text(weight: "semibold")[#business_name.replace(" (EI)", "")] | |
| #v(2pt) | |
| #set text(size: 9pt) | |
| #business_address \ | |
| #business_email \ | |
| #business_phone | |
| #v(6pt) | |
| #labels.siret_prefix: #business_siret | |
| ], | |
| [ | |
| #section-label(labels.to) | |
| #v(6pt) | |
| #text(weight: "semibold")[#client_name] | |
| #v(2pt) | |
| #set text(size: 9pt) | |
| #client_address \ | |
| #client_email | |
| #if client_extra != "" [ | |
| #v(6pt) | |
| RNA: #client_extra.replace("Numéro RNA: ", "").replace("RNA: ", "") | |
| ] | |
| ] | |
| ) | |
| #v(32pt) | |
| // ============ LINE ITEMS ============ | |
| #section-label(labels.line_items) | |
| #table( | |
| columns: (1fr, auto), | |
| stroke: none, | |
| inset: (x: 12pt, y: 10pt), | |
| fill: (_, row) => if row == 0 { gray-50 } else { none }, | |
| table.hline(stroke: 1pt + gray-200), | |
| [*#labels.description*], align(right)[*#labels.total*], | |
| table.hline(stroke: 1pt + gray-200), | |
| ..line_items.map(item => (item.description, align(right, item.total))).flatten(), | |
| table.hline(stroke: 1pt + gray-200), | |
| ) | |
| #v(12pt) | |
| // ============ TOTAL ============ | |
| #align(right)[ | |
| #rect( | |
| fill: rgb("#dbeafe"), | |
| stroke: 1pt + accent, | |
| inset: (x: 20pt, y: 12pt), | |
| )[ | |
| #text(fill: rgb("#1c398e"), size: 10pt)[#labels.total: ] | |
| #text(fill: rgb("#1c398e"), size: 14pt, weight: "bold")[#total] | |
| ] | |
| ] | |
| // TVA notice for French invoices | |
| #if lang == "FR" [ | |
| #v(4pt) | |
| #align(right)[ | |
| #text(size: 8pt, fill: gray-500, style: "italic")[TVA non applicable, art. 293 B du CGI] | |
| ] | |
| ] | |
| #v(28pt) | |
| // ============ PAYMENT DETAILS ============ | |
| #let payment_labels = if lang == "FR" { | |
| ( | |
| title: "Informations de paiement", | |
| holder: "Titulaire", | |
| bic: "BIC", | |
| iban: "IBAN", | |
| account_number: "Numéro de compte", | |
| routing_number: "Routing", | |
| bank: "Banque", | |
| reference: "Référence", | |
| ) | |
| } else { | |
| ( | |
| title: "Payment details", | |
| holder: "Account holder", | |
| bic: "Swift/BIC", | |
| iban: "IBAN", | |
| account_number: "Account number", | |
| routing_number: "Routing number", | |
| bank: "Bank", | |
| reference: "Reference", | |
| ) | |
| } | |
| #section-label(payment_labels.title) | |
| #let payment-rows = if account_type == "US_DOMESTIC" { | |
| ( | |
| ([*#payment_labels.holder*], account_holder), | |
| ([*#payment_labels.account_number*], text(font: "JetBrains Mono")[#account_number]), | |
| ([*#payment_labels.routing_number*], text(font: "JetBrains Mono")[#routing_number]), | |
| ([*#payment_labels.bic*], text(font: "JetBrains Mono")[#account_bic]), | |
| ([*#payment_labels.bank*], [#bank_name, #bank_address]), | |
| ([*#payment_labels.reference*], reference), | |
| ) | |
| } else { | |
| ( | |
| ([*#payment_labels.holder*], account_holder), | |
| ([*#payment_labels.iban*], text(font: "JetBrains Mono")[#account_iban]), | |
| ([*#payment_labels.bic*], text(font: "JetBrains Mono")[#account_bic]), | |
| ([*#payment_labels.bank*], [#bank_name, #bank_address]), | |
| ([*#payment_labels.reference*], reference), | |
| ) | |
| } | |
| #table( | |
| columns: (120pt, 1fr), | |
| stroke: none, | |
| inset: (x: 12pt, y: 10pt), | |
| fill: (_, row) => if calc.odd(row) { gray-50 } else { white }, | |
| table.hline(stroke: 1pt + gray-200), | |
| ..payment-rows.map(row => ( | |
| table.vline(x: 1, stroke: 1pt + gray-200), | |
| row.at(0), | |
| row.at(1), | |
| table.hline(stroke: 1pt + gray-200), | |
| )).flatten(), | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment