feat: the invoice PDF, with stamps and each payment (P4b S3)
check / check (push) Canceled after 0s

ledger-import invoice_pdf (templates/invoice.typ): seller, bill-to,
number, dates, terms, lines, total, each settled line and the balance
due; money formatted in Rust (the template does no arithmetic), text as
data, deterministic bytes. Stamps as a rubber stamp would mark it, in
red: PAID (with the day it was paid in full: the invoice is then the
customer's receipt), PARTIALLY PAID, PAST DUE, VOID; DRAFT in gray for
S4's preview. ledger-package invoice_document builds it from the books
as of a day: the derived figures (B-38), each payment that stands on its
own line with its date, payment method and reference; re-created copies
say so (the issued copy is kept from S4 on and never changes).
books.document pdf hands it over as a download; the viewer's invoice
page saves or opens it. The owner asked for the paid stamp with the
payment's date and details (2026-10-24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-24 18:20:55 -06:00
1 parent b7b7f17163
commit c1f9001cfe
12 files changed
+843 -4

No files matched your search

+29 -1
View File
@@ -8826,7 +8826,7 @@ impl BooksServer {
name = "books.document",
title = "An invoice or bill",
description = described(
"One invoice or bill (ref \"invoice:7\" or \"bill:12\", from books.documents): party, dates, lines with their projects, total, what was paid, discounted and written off, what it still owes and its status, all as of a day; and its timeline: every ledger line tagged with it in posting order (its own entry, each payment, each reversal), each saying whether its entry still stands. A reversed payment shows twice: the payment (no longer standing) and its reversal (standing), which net to nothing.",
"One invoice or bill (ref \"invoice:7\" or \"bill:12\", from books.documents) as of a day: party, dates, lines with projects, total, paid, discounted, written off, still owed, status; and its timeline, every ledger line tagged with it in posting order, each saying whether its entry still stands (a reversed payment shows as the payment, not standing, and its reversal, standing: they net to nothing). pdf: true also hands over an invoice's PDF to download, re-created from the books.",
ledger_core::calendar::PeriodKind::AsOf
),
annotations(read_only_hint = true, open_world_hint = false)
@@ -8866,6 +8866,33 @@ impl BooksServer {
.doc_state(doc, as_of)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("{} is dated after {as_of}", doc_ref_text(doc)))?;
let pdf = match (p.pdf, doc) {
(false, _) => None,
(true, ledger_core::DocRef::Bill(_)) => {
return Err(
"a bill is the vendor's document: its PDF is the file filed with it (books.list_receipts)"
.into(),
);
}
(true, ledger_core::DocRef::Invoice(id)) => {
let settings = self.settings();
let printed = ledger_package::invoice_document(
ledger,
&settings.company,
id,
as_of,
today(),
)?;
let bytes =
ledger_import::invoice_pdf::invoice_pdf(&printed).map_err(|e| e.to_string())?;
Some(self.hand_over(
&book_name(&store),
&format!("invoice-{id}.pdf"),
"application/pdf",
bytes,
)?)
}
};
let timeline = ledger
.doc_lines(doc)
.into_iter()
@@ -8904,6 +8931,7 @@ impl BooksServer {
status: st.status.key().to_string(),
overdue_days: st.overdue_days,
timeline,
pdf,
}))
}
+7
View File
@@ -3195,6 +3195,10 @@ pub struct DocumentParams {
#[serde(default)]
#[schemars(description = AS_OF_DOC)]
pub as_of: Option<String>,
/// For an invoice: also hand over its PDF (as of the day, re-created
/// from the books) to download.
#[serde(default)]
pub pdf: bool,
}
/// One line of a document.
@@ -3259,6 +3263,9 @@ pub struct DocumentOut {
pub overdue_days: i64,
/// Every ledger line tagged with it, in posting order.
pub timeline: Vec<DocumentEventOut>,
/// The invoice's PDF, handed over (with `pdf`): run its download.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pdf: Option<HandedFile>,
}
/// Parameters for `books.void_document`.