#let format_invoice_num(num, pre:4) = { let a = calc.max(0,pre - str(num).len()) return a*"0" + str(num) } #let format_num(num, decimal: ".", thousands: "'") = { let parts = str(num).split(".") let decimal_part = if parts.len() == 2 { parts.at(1) } let integer_part = parts.at(0).rev().clusters().enumerate() .map((item) => { let (index, value) = item return value + if calc.rem(index, 3) == 0 and index != 0 { thousands } }).rev().join("") // TODO return integer_part + if decimal_part != none { decimal + decimal_part } } #let translations = ( français: ( sender: "ÉMETTEUR", recipient: "DESTINATAIRE", invoice: "FACTURE", invoice_number: "Facture N° :", date: "Date :", due_date: "Échéance :", description: "Description", amount: "Montant", total: "Total :", vat_notice: "Association non soumise à la TVA selon l'Art. 10 LTVA", payment_terms: "Cette facture est payable dans les 30 jours suivant sa réception.", banking_info: "COORDONNÉES BANCAIRES", bank: "Banque :", account_holder: "Titulaire :", account_notice: "Compte au nom d'un membre de l'association", association_notice: "Association à but non lucratif non inscrite au registre du commerce", ), deutsch: ( sender: "ABSENDER", recipient: "EMPFÄNGER", invoice: "RECHNUNG", invoice_number: "Rechnungs-Nr.:", date: "Datum:", due_date: "Fälligkeitsdatum:", description: "Beschreibung", amount: "Betrag", total: "Gesamtbetrag:", vat_notice: "Verein nicht mehrwertsteuerpflichtig gemäß Art. 10 MWSTG", payment_terms: "Diese Rechnung ist innerhalb von 30 Tagen nach Erhalt zu bezahlen.", banking_info: "BANKVERBINDUNG", bank: "Bank:", account_holder: "Kontoinhaber:", account_notice: "Konto auf den Namen eines Vereinsmitglieds", association_notice: "Gemeinnütziger Verein, nicht im Handelsregister eingetragen", ), english: ( sender: "FROM", recipient: "TO", invoice: "INVOICE", invoice_number: "Invoice No.:", date: "Date:", due_date: "Due date:", description: "Description", amount: "Amount", total: "Total:", vat_notice: "Association not subject to VAT according to Art. 10 VAT Act", payment_terms: "This invoice is payable within 30 days of receipt.", banking_info: "BANKING DETAILS", bank: "Bank:", account_holder: "Account holder:", account_notice: "Account in the name of an association member", association_notice: "Non-profit association not registered in the commercial register", ), ) #let bank_wise_eur = [ #let t = translations.at("english", default: translations.français) #grid( columns:(auto, auto), align:(right,left), column-gutter: 1em, row-gutter: .5em, [#t.bank],[ Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium], [IBAN:],[*BE22905094540247*], [BIC/SWIFT:],[ TRWIBEB1XXX], [#t.account_holder],[ Robin Szymczak], [],[#t.account_notice], ) // #t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium\ // IBAN: BE22905094540247\ // BIC/SWIFT: TRWIBEB1XXX\ // #t.account_holder Robin Szymczak\ ] #let bank_wise_chf = [ #let t = translations.at("english", default: translations.français) #grid( columns:(auto, auto), align:(right,left), column-gutter: 1em, row-gutter: .5em, [#t.bank],[ Wise Payments Limited, 1st Floor, Worship Square, 65 Clifton Street, London, EC2A 4JE, United Kingdom ], [IBAN:],[*GB51TRWI23080140876698*], [BIC/SWIFT:],[ TRWIGB2LXXX ], [#t.account_holder],[ Robin Szymczak ], [],[#t.account_notice], ) ] // on stock en centimes, et on arrondit #let chf(value) = (CHF:calc.round(value*100)/100) #let eur(value) = (EUR:calc.round(value*100)/100) #let usd(value) = (USD:calc.round(value*100)/100) // #let money(value,currency) = (value:decimal(value),currency:currency) // #let money_add(m1,m2) = if m1.currency != m2.currency : fail #let money_render(m) = { let cs = m.keys().sorted().map(currency => { let value = m.at(currency) let a = int(value) let b1 = int(100*(value - a)) let b0 = if b1 < 10 [0] else [] let b = [#b0#b1] let c = currency return [#format_num(a)\.#b #c] }) cs.join([ \+ ]) } #let invoice_thing(price,description) = (price:price,description:description) #let invoice_enumerate(start:0, invoice_data) = invoice_data.enumerate(start:start).map(e => { e.at(1).invoice_number=format_invoice_num(e.at(0)) e.at(1) }) #let invoice_render( invoice, language: "english", // Options: "français", "deutsch", "english" ) = [ #set page( footer: align(center)[ #line(length: 100%, stroke: 0.5pt + gray) #text(size:9pt)[Cheap Motion Pictures - 1 rue de l'Ale - 1003 Lausanne - contact\@cheapmo.ch] ], ) #set text(font: "Arial", size: 11pt) // Sélection du dictionnaire en fonction de la langue #let t = translations.at(language, default: translations.français) // Titre et informations de la facture #grid( columns: (auto,1fr, 1fr), gutter:10pt, image("cheapmo-logo.png",width:60pt), align(left)[ #text(weight: "bold", size: 14pt)[Cheap Motion Pictures ]\ #text(style: "italic")[#t.association_notice] ], align(right)[ #t.invoice_number *2025-#(invoice.invoice_number)*\ #t.date #(invoice.date ).display("[day]/[month]/[year]")\ #t.due_date *#(invoice.date + duration(days: 30)).display("[day]/[month]/[year]")*\ ], ) #v(15pt) #grid( columns: (1fr, 1fr), // Informations de l'émetteur align(left)[ #par(text(weight: "bold", size: 14pt)[#t.sender]) Cheap Motion Pictures\ Rue de l'Ale 1\ 1003 Lausanne, Suisse\ Tél: +41 77 471 11 34\ Email: contact\@cheapmo.ch ], // Informations du destinataire align(right)[ #par(text(weight: "bold", size: 14pt)[#t.recipient]) #invoice.recipient.name\ #invoice.recipient.adress\ #invoice.recipient.postal_code #invoice.recipient.town, #invoice.recipient.country #if invoice.recipient.vat_number != none [VAT: #invoice.recipient.vat_number] else [] ], ) #let things = invoice.invoice.fold((),(l,e)=>(..l,e.description,money_render(e.price))) #let total = money_render(invoice.invoice.fold((:),(l,e)=> { for currency in e.price.keys() { l.insert(currency, l.at(currency,default:0) + e.price.at(currency)) } return l })) #v(15pt) #table( columns: (1fr, auto), inset: (x:10pt,y:8pt), align: (left, right), table.header( table.cell(fill:rgb(245, 245, 245))[*#t.description*], table.cell(fill:rgb(245, 245, 245))[*#t.amount* ], ), ..things, table.cell(fill:silver,align:right)[TOTAL], table.cell(fill:silver,align:right)[*#total*] ) #v(15pt) // Informations complémentaires #t.vat_notice \ #t.payment_terms // Coordonnées bancaires avec notification spéciale = #t.banking_info #invoice.bank #if invoice.recu { place(horizon+center,rotate(-60deg,text(fill:red.transparentize(50%),size:300pt,[PAID]),reflow:true)) } ] #let invoice_render_bilan(invoices) = { let content = () for invoice in invoices { let total = money_render(invoice.invoice.fold((:),(l,e)=> { for currency in e.price.keys() { l.insert(currency, l.at(currency,default:0) + e.price.at(currency)) } return l })) content.push(invoice.recipient.name) content.push([2025-#invoice.invoice_number]) content.push([#total]) content.push([]) // #table( // [#i.recipient.name], // [#i.invoice], // ) } table( columns: (1fr, auto, auto, auto), align: (auto, auto, right, right), [*name*],[*invoice*],[*amount*],[*received*], ..content ) }