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

+49
View File
@@ -599,6 +599,10 @@ pub fn show_doc(ui: &mut Ui, b: &Books, doc: DocRef, shell: &mut Shell<'_>) {
},
);
ui.add_space(8.0);
if let DocRef::Invoice(id) = doc {
pdf_actions(ui, b, id);
ui.add_space(4.0);
}
if ui
.link(format!(
"{} Open the entry that recorded it (#{entry})",
@@ -614,6 +618,51 @@ pub fn show_doc(ui: &mut Ui, b: &Books, doc: DocRef, shell: &mut Shell<'_>) {
}
}
/// "Save invoice PDF" and "Open": the invoice as of today, re-created
/// from the books (ledger_package::invoice_document, the same data the
/// server prints), saved in Downloads. Writes nothing to the books.
fn pdf_actions(ui: &mut Ui, b: &Books, id: ledger_core::InvoiceId) {
let p = pal(ui.ctx());
let said = ui.id().with(("invoice-pdf-said", id));
ui.horizontal(|ui| {
let save = ui
.button(format!("{} Save invoice PDF", icon::FILE_PDF))
.on_hover_text("As of today, in your Downloads folder");
let open = ui
.button(format!("{} Open", icon::ARROW_SQUARE_OUT))
.on_hover_text("As of today, in your Downloads folder, opened with its app");
if save.clicked() || open.clicked() {
let result = ledger_package::invoice_document(
&b.ledger,
&b.settings.company,
id,
b.today,
b.today,
)
.and_then(|doc| {
ledger_import::invoice_pdf::invoice_pdf(&doc).map_err(|e| e.to_string())
})
.and_then(|bytes| {
let stem = format!("invoice-{id}");
if open.clicked() {
crate::export::save_and_open(&stem, "pdf", &bytes)
} else {
crate::export::save(&stem, "pdf", &bytes)
}
})
.map(|path| path.display().to_string());
ui.data_mut(|d| d.insert_temp(said, result));
}
});
if let Some(result) = ui.data(|d| d.get_temp::<Result<String, String>>(said)) {
let (text, color) = match result {
Ok(path) => (format!("Saved {path}"), p.muted),
Err(why) => (format!("Couldn't save it: {why}"), p.warning),
};
ui.add(egui::Label::new(egui::RichText::new(text).size(12.0).color(color)).wrap());
}
}
fn missing(ui: &mut Ui, title: &str) {
widgets::card(ui, |ui| widgets::empty(ui, icon::RECEIPT, title, ""));
}