diff options
author | T <t@tjp.lol> | 2025-08-04 09:49:52 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-04 15:15:18 -0600 |
commit | 56e0af3b41742876b471332aeb943a5a2ca8dfbf (patch) | |
tree | ef75f4900107ef28977823eabd11ec3014cd40ba /templates/invoice.typ | |
parent | 4c29dfee9be26996ce548e2edf0328422df598d0 (diff) |
Generate invoice PDFs
Diffstat (limited to 'templates/invoice.typ')
-rw-r--r-- | templates/invoice.typ | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/templates/invoice.typ b/templates/invoice.typ new file mode 100644 index 0000000..7dacc59 --- /dev/null +++ b/templates/invoice.typ @@ -0,0 +1,148 @@ +#set page(margin: (top: 0.75in, bottom: 1in, left: 1in, right: 1in)) +#set text(font: ("EB Garamond", "Georgia"), size: 10pt) +#set par(leading: 0.65em) + +// Load invoice data from JSON file +#let data = json("data.json") + +// Helper function to format hours as HH:MM +#let format-hours(hours) = { + let total-minutes = calc.round(hours * 60) + let h = calc.floor(total-minutes / 60) + let m = calc.rem(total-minutes, 60) + str(h) + ":" + if m < 10 { "0" + str(m) } else { str(m) } +} + +// Helper function to format currency with thousands separator and cents +#let format-currency(amount) = { + let dollars = calc.floor(amount) + let cents = calc.round((amount - dollars) * 100) + + // Convert to string and add thousands separators + let dollar-str = str(dollars) + let len = dollar-str.len() + + let formatted = "" + for i in range(len) { + let digit = dollar-str.at(i) + formatted += digit + let remaining = len - i - 1 + if remaining > 0 and calc.rem(remaining, 3) == 0 { + formatted += "," + } + } + + "$" + formatted + "." + if cents < 10 { "0" + str(cents) } else { str(cents) } +} + +// Professional header with company info +#let professional-header() = { + // Company header + align(left)[ + #text(size: 9pt, fill: gray)[ + #text(weight: "bold")[#data.contractor_name] • #data.contractor_label • #data.contractor_email + ] + ] + + v(3em) + + // Invoice title and number + grid( + columns: (1fr, auto), + align(left)[ + #text(size: 28pt, weight: "bold")[Invoice] + ], + align(right)[ + #text(size: 11pt)[ + #text(weight: "bold")[Invoice \##data.invoice_number] \ + #data.generated_date + ] + ] + ) + + v(2.5em) +} + +#let client-info-section() = { + grid( + columns: (1fr, 1fr), + gutter: 3em, + // Bill To section + [ + #text(size: 9pt, fill: gray)[BILL TO] + #v(0.5em) + #text(size: 12pt, weight: "bold")[#data.client_name] + #if data.project_name != "" [ + #v(0.3em) + #text(size: 10pt)[Project: #data.project_name] + ] + ], + // Invoice details + align(right)[ + #text(size: 9pt, fill: gray)[INVOICE PERIOD] + #v(0.5em) + #text(size: 10pt)[#data.date_range_start to #data.date_range_end] + ] + ) + + v(2.5em) +} + +#let professional-table() = { + table( + columns: (1fr, auto, auto, auto), + stroke: (x, y) => if y == 0 or y == 1 { (bottom: 0.8pt + black) } else { none }, + inset: (x: 8pt, y: 12pt), + align: (left, center, center, right), + column-gutter: 12pt, + + // Header + table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[DESCRIPTION]], + table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[HOURS]], + table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[RATE]], + table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[AMOUNT]], + + // Line items + ..data.line_items.map(item => ( + text(size: 10pt)[#item.description], + text(size: 10pt)[#format-hours(item.hours)], + text(size: 10pt)[#format-currency(item.rate)], + text(size: 10pt, weight: "medium")[#format-currency(item.amount)] + )).flatten() + ) +} + +#let invoice-summary() = { + v(1.5em) + + // Subtotal and total section + align(right, + table( + columns: (auto, auto), + stroke: none, + inset: (x: 12pt, y: 6pt), + align: (right, right), + + [#text(size: 10pt)[Total Hours:]], [#text(size: 10pt)[#format-hours(data.total_hours)]], + table.hline(stroke: 0.5pt), + [#text(size: 12pt, weight: "bold")[Total Amount:]], [#text(size: 12pt, weight: "bold")[#format-currency(data.total_amount)]] + ) + ) +} + +#let payment-terms() = { + v(3em) + + [ + #text(size: 9pt, fill: gray)[ + *Payment Terms:* Net 30 days. Please remit payment within 30 days of invoice date. + ] + ] +} + +// Main invoice layout +#professional-header() +#client-info-section() +#professional-table() +#invoice-summary() +//#payment-terms() |