Data

Invoice

A printable invoice: parties, number and dates, lines with quantity and unit price, totals, paid and balance, notes and terms.

#document#commerce#price#print#record#no-js

Demo

Preview

Invoicedue

Number
INV-2026-0042
Date
Due

From

Analytical Engines Ltd

1 Difference Street

London N1 9AA

billing@example.com

VAT GB 123 4567 89

Bill to

Babbage & Co

22 Dorset Street

London W1U 6QW

charles@example.com

DescriptionQtyUnit priceAmount
DesignOctober, 12 days at the day rate12€90.00€1,080.00
HostingTwo services, one region1€120.00€120.00
Domain renewal2€14.50€29.00
Subtotal
€1,229.00
Discount
−€29.00
VAT 20%
€240.00
Total
€1,440.00
Paid
€500.00
Balance due
€940.00

Notes

Thank you for your business.\nBank: Example Bank, IBAN GB00 EXMP 0000 0000 0000 00

Payable within 30 days. Late payments carry 2% a month.

Quotepaid

Number
Q-118
Date

Bill to

Ada Lovelace

DescriptionAmount
A short consult$250.00
Total
$250.00

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/invoice.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/invoice.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/invoice.qwe
view InvoiceDemo() =    <div.demo-stack>        <Invoice id="invoice-demo-due" number="INV-2026-0042" date="2026-10-01" due="2026-10-31" status="due"                 logo="https://dummyimage.com/180x44/5b4bdb/ffffff.png&text=ENGINES"                 from={{ name: "Analytical Engines Ltd", lines: ["1 Difference Street", "London N1 9AA"], email: "billing@example.com", tax_id: "VAT GB 123 4567 89" }}                 to={{ name: "Babbage & Co", lines: ["22 Dorset Street", "London W1U 6QW"], email: "charles@example.com" }}                 lines={[                     { description: "Design", detail: "October, 12 days at the day rate", qty: 12, unit: "€90.00", amount: "€1,080.00" },                     { description: "Hosting", detail: "Two services, one region", qty: 1, unit: "€120.00", amount: "€120.00" },                     { description: "Domain renewal", qty: 2, unit: "€14.50", amount: "€29.00" },                 ]}                 subtotal="€1,229.00" discount="−€29.00" tax="€240.00" tax_label="VAT 20%" total="€1,440.00" paid="€500.00" balance="€940.00"                 notes="Thank you for your business.\nBank: Example Bank, IBAN GB00 EXMP 0000 0000 0000 00" terms="Payable within 30 days. Late payments carry 2% a month."/>        <Invoice id="invoice-demo-paid" number="Q-118" title="Quote" date="2026-09-12" status="paid"                 to={{ name: "Ada Lovelace" }} lines={[{ description: "A short consult", amount: "$250.00" }]} total="$250.00"/>    </div>

Source

ui/invoice.qwe
// Invoice: an invoice as it should print: who it is from and to, the number and// dates, a line each with quantity, unit price and amount, the totals, and notes.// Amounts are text already written in their currency (money.format does it), so// they line up whatever the currency and nothing is rounded here.////   <Invoice number="INV-0042" date="2026-10-01" due="2026-10-31" status="due"//            from={{ name: "Analytical Engines Ltd", lines: ["1 Difference St", "London N1"], email: "billing@example.com" }}//            to={{ name: "Babbage & Co", lines: ["22 Dorset St", "London W1"] }}//            lines={[{ description: "Design", detail: "October", qty: 12, unit: "€90.00", amount: "€1,080.00" }]}//            subtotal="€1,080.00" tax="€216.00" tax_label="VAT 20%" total="€1,296.00"/>//// number: the invoice's number; title: "Invoice" (or "Credit note", "Quote").// date, due: ISO dates, shown as "Oct 1, 2026". status: "draft", "due", "paid" or// "overdue", a badge by the title. from, to: { name, lines?, email?, phone?, tax_id? },// lines being the address a line each. logo: an image at the top left.// lines: a list of { description, detail?, qty?, unit?, amount }. subtotal, discount,// tax, total: the totals (each left out, not shown); tax_label: "Tax" by default;// paid: what was paid already, and balance: what is left, under the total.// notes: a paragraph at the bottom; terms: another, smaller ("Payable within 30// days"). It prints on white without its border. id: the element's id.import ui/theme.qwe use ui_clslet invoice_css = css`.ui-invoice { display: grid; gap: 28px; min-width: 0; padding: 36px 40px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); }.ui-invoice-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px 32px; }.ui-invoice-logo { max-height: 44px; max-width: 180px; object-fit: contain; margin-bottom: 10px; }.ui-invoice-title { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 26px; font-weight: 700; letter-spacing: -.01em; line-height: 1.2; }.ui-invoice-status { padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;  background: var(--ui-subtle); color: var(--ui-muted); }.ui-invoice-paid { background: var(--ui-success-soft); color: var(--ui-success); }.ui-invoice-due { background: var(--ui-info-soft); color: var(--ui-info); }.ui-invoice-overdue { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-invoice-meta { display: grid; grid-template-columns: auto auto; gap: 2px 14px; margin: 0; align-content: start; text-align: right; }.ui-invoice-meta dt { color: var(--ui-muted); }.ui-invoice-meta dd { margin: 0; font-weight: 500; font-variant-numeric: tabular-nums; }.ui-invoice-parties { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }.ui-invoice-party { font-style: normal; }.ui-invoice-party h4 { margin: 0 0 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-invoice-party-name { font-weight: 600; }.ui-invoice-party p { margin: 0; }.ui-invoice-party .ui-invoice-dim { color: var(--ui-muted); }.ui-invoice-scroll { overflow-x: auto; }.ui-invoice table { width: 100%; border-collapse: collapse; }.ui-invoice th { padding: 8px 10px; border-bottom: 2px solid var(--ui-fg); color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; text-align: left; }.ui-invoice td { padding: 10px; border-bottom: 1px solid var(--ui-border); vertical-align: top; }.ui-invoice .ui-invoice-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }.ui-invoice-desc { font-weight: 500; }.ui-invoice-detail { display: block; color: var(--ui-muted); font-size: 13px; }.ui-invoice-totals { display: grid; grid-template-columns: auto minmax(120px, auto); gap: 6px 24px; justify-content: end; margin: 0; }.ui-invoice-totals dt { color: var(--ui-muted); }.ui-invoice-totals dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }.ui-invoice-total dt, .ui-invoice-total dd { padding-top: 8px; border-top: 2px solid var(--ui-fg); font-size: 16px; font-weight: 700; color: var(--ui-fg); }.ui-invoice-discount dd { color: var(--ui-success); }.ui-invoice-balance dt, .ui-invoice-balance dd { font-weight: 600; color: var(--ui-fg); }.ui-invoice-notes { display: grid; gap: 8px; padding-top: 16px; border-top: 1px solid var(--ui-border); }.ui-invoice-notes h4 { margin: 0 0 2px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ui-muted); }.ui-invoice-notes p { margin: 0; white-space: pre-line; }.ui-invoice-terms { color: var(--ui-muted); font-size: 13px; }@media (max-width: 560px) { .ui-invoice { padding: 22px 18px; } .ui-invoice-meta { text-align: left; } }@media print { .ui-invoice { padding: 0; border: 0; border-radius: 0; background: #fff; color: #000; } .ui-invoice-scroll { overflow: visible; } }`let invoice_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date as "Oct 1, 2026"; other text as it is.fn invoice_date(d) -> str {    let p = split(slice(str(d), 0, 10), "-")    len(p) == 3 && int(p[1]) != null && int(p[2]) != null ? `${invoice_months[int(p[1]) - 1]} ${int(p[2])}, ${p[0]}` : str(d)}// One party: who, their address a line each, and how to reach them.fn invoice_party(heading: str, p: map) =    <address.ui-invoice-party>        <h4>{heading}</h4>        <p.ui-invoice-party-name>{p.name}</p>        {for l in p.lines ?? [] => <p key={l}>{l}</p>}        {p.email != null && <p.ui-invoice-dim>{p.email}</p>}        {p.phone != null && <p.ui-invoice-dim>{p.phone}</p>}        {p.tax_id != null && <p.ui-invoice-dim>{p.tax_id}</p>}    </address>view Invoice(number: str, title: str = "Invoice", date = null, due = null, status: str? = null, from: map? = null, to: map? = null,             logo: str? = null, lines: list = [], subtotal: str? = null, discount: str? = null, tax: str? = null, tax_label: str = "Tax",             total: str? = null, paid: str? = null, balance: str? = null, notes: str? = null, terms: str? = null,             id: str? = null, class: str? = null) {    style global invoice_css    let has_qty = any(lines, (l) => l.qty != null)    let has_unit = any(lines, (l) => l.unit != null)    <article class={ui_cls(["ui-invoice", class])} id={id} aria-label={`${title} ${number}`}>        <header.ui-invoice-head>            <div>                {logo != null && <img.ui-invoice-logo src={logo} alt=""/>}                <h2.ui-invoice-title>{title}                    {status != null && <span class={`ui-invoice-status ui-invoice-${status}`}>{status}</span>}                </h2>            </div>            <dl.ui-invoice-meta>                <dt>Number</dt><dd>{number}</dd>                {date != null && [<dt>Date</dt>, <dd><time datetime={str(date)}>{invoice_date(date)}</time></dd>]}                {due != null && [<dt>Due</dt>, <dd><time datetime={str(due)}>{invoice_date(due)}</time></dd>]}            </dl>        </header>        {(from != null || to != null) && <div.ui-invoice-parties>            {from != null && invoice_party("From", from)}            {to != null && invoice_party("Bill to", to)}        </div>}        <div.ui-invoice-scroll>            <table>                <thead><tr>                    <th scope="col">Description</th>                    {has_qty && <th.ui-invoice-num scope="col">Qty</th>}                    {has_unit && <th.ui-invoice-num scope="col">Unit price</th>}                    <th.ui-invoice-num scope="col">Amount</th>                </tr></thead>                <tbody>                    {for [i, l] in enumerate(lines) => <tr key={i}>                        <td><span.ui-invoice-desc>{l.description}</span>{l.detail != null && <span.ui-invoice-detail>{l.detail}</span>}</td>                        {has_qty && <td.ui-invoice-num>{l.qty}</td>}                        {has_unit && <td.ui-invoice-num>{l.unit}</td>}                        <td.ui-invoice-num>{l.amount}</td>                    </tr>}                </tbody>            </table>        </div>        {(subtotal != null || total != null) && <dl.ui-invoice-totals>            {subtotal != null && [<dt>Subtotal</dt>, <dd>{subtotal}</dd>]}            {discount != null && <div.ui-invoice-discount style="display: contents"><dt>Discount</dt><dd>{discount}</dd></div>}            {tax != null && [<dt>{tax_label}</dt>, <dd>{tax}</dd>]}            {total != null && <div.ui-invoice-total style="display: contents"><dt>Total</dt><dd>{total}</dd></div>}            {paid != null && [<dt>Paid</dt>, <dd>{paid}</dd>]}            {balance != null && <div.ui-invoice-balance style="display: contents"><dt>Balance due</dt><dd>{balance}</dd></div>}        </dl>}        {(notes != null || terms != null) && <footer.ui-invoice-notes>            {notes != null && <div><h4>Notes</h4><p>{notes}</p></div>}            {terms != null && <p.ui-invoice-terms>{terms}</p>}        </footer>}    </article>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

On Invoice: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.