From 34b92d5f97b345e48c3b46040c0682b00a6fc208 Mon Sep 17 00:00:00 2001 From: laurent roro Date: Sat, 26 Apr 2025 17:33:27 +0200 Subject: [PATCH] invoice 2025 init commit --- 2025/invoice_2025.typ | 117 +++++++++++++++++ 2025/invoice_template.typ | 256 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 2025/invoice_2025.typ create mode 100644 2025/invoice_template.typ diff --git a/2025/invoice_2025.typ b/2025/invoice_2025.typ new file mode 100644 index 0000000..e6679a9 --- /dev/null +++ b/2025/invoice_2025.typ @@ -0,0 +1,117 @@ +#import "invoice_template.typ": * + +#let invoice_recip(name,vat,country,town,postal_code,address) = ( + name: name, + adress: address, + postal_code: postal_code, + town: town, + country: country, + vat_number: vat, +) + +// start at invoice number 0 +#let invoice_data = invoice_enumerate(start:0,( + + // festival Allemagne + ( + date: datetime(day: 28, month: 2, year: 2025), + recipient: invoice_recip( + [Hochschule Flensburg], + none, + [Germany],[Flensburg],[D-24943],[Kanzleistraße 91-93] + ), + invoice: ( + invoice_thing(eur(40.00))[poong screening], + ), + bank: bank_wise_eur, + recu:true, //eur(40.00), + tag:("poong","screening",) + ), + + // festival Allemagne + ( + date: datetime(day: 24, month: 4, year: 2025), + recipient: invoice_recip( + [PrevYou], + none, + [Germany],[Berlin],[],[] + ), + invoice: ( + invoice_thing(eur(75.00))[poong screening], + ), + bank:bank_wise_eur, + recu:false, // chf(0), + tag:("poong","screening",) + ), + + // festival Lausanne + ( + date: datetime(day: 24, month: 4, year: 2025), + recipient: invoice_recip( + [BDFIL], + none, + [Switzerland],[Lausanne],[1005],[Place de la Cathédrale 12] + ), + invoice: ( + invoice_thing(chf(75.00))[poong screening], + ), + bank:bank_wise_chf, + recu:false, // chf(0), + tag:("poong","screening",) + ), + + // BaseCourt + ( + date: datetime(day: 22, month: 4, year: 2025), + recipient: invoice_recip( + [Base court], + none, + [Switzerland],[Lausanne],[1006],[Av. de la Rasude 2] + ), + invoice: ( + invoice_thing(chf(60.00))[poong screening cinétransat], + invoice_thing(chf(60.00))[poong screening], + invoice_thing(chf(60.00))[poong screening], + ), + bank:bank_wise_chf, + recu:false, // chf(0), + tag:("poong","screening",), + ), + + // Salades + ( + date: datetime(day: 24, month: 4, year: 2025), + recipient: invoice_recip( + [TODO], + none, + [Switzerland],[TODO],[TODO],[TODO] + ), + invoice: ( + invoice_thing(chf(200.00))[animation kickstarter Salades], + ), + bank:bank_wise_chf, + recu:false, // chf(0), + ), + + // UNIGE + ( + date: datetime(day: 24, month: 4, year: 2025), + recipient: invoice_recip( + [UNIGE], + none, + [Switzerland],[Geneva],[],[] + ), + invoice: ( + invoice_thing(chf(1100.00))[animation], + ), + bank:bank_wise_chf, + recu:false, // chf(0), + ), +)) + +#for i in invoice_data {invoice_render(i)} + +#invoice_render_bilan(invoice_data) + +// #invoice_data.map( i => invoice_render(i)) +// #invoice_render(invoice_data.last()) diff --git a/2025/invoice_template.typ b/2025/invoice_template.typ new file mode 100644 index 0000000..227b1ca --- /dev/null +++ b/2025/invoice_template.typ @@ -0,0 +1,256 @@ +#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 + ) +}