merge: P4b S6, paying on terms (branch S6, 6c4480cd)

The third of the paused branches: Ledger::settle with discounts and the
processor's fee (kernel::settle, Kani settle_is_exact, Lean
settle_posts_a_document_step), books.receive_payment and books.pay_bill
with applications, books.bills_due, the agings' detail and tie-out, the
aging exports and the CPA package's aging CSVs, the handlers in
tools/settlements.rs. Twelve files conflicted with the S4.4 and S5
merges, resolved as plan 8.0.2 says: both routers; both test appends
(the shared match arm in arap_standing.rs spliced into both tests; the
shared `.unwrap()` in store.rs); the scope table at 124; one
invoices-and-bills handbook (S5's send step 1 with S6's steps 2, 3, 5
and procedures) registered once; docs/mcp-interface.md with S4.4's
create docs and S6's payment docs; S5's law renumbered 35 and S6's 36
(S1's is 34); the parity rows and sections of both. S6's wire tests
create their bills as S4.4's contract now requires (confirmed, the
vendor's number, a line account, the vendor's invoice uploaded) and live
in their own module beside S4.4's tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-28 21:26:40 -06:00
commit be58059830
45 files changed
+3949 -520

No files matched your search

+59 -38
View File
@@ -55,6 +55,7 @@ struct Item {
party: String,
date: NaiveDate,
due: NaiveDate,
terms: Option<ledger_core::Terms>,
state: Result<Option<DocState>, ledger_core::LedgerError>,
}
@@ -74,6 +75,7 @@ fn items(b: &Books, bills: bool) -> Vec<Item> {
.unwrap_or_default(),
date: d.date,
due: d.due_date,
terms: d.terms.clone(),
state: b.doc_today(DocRef::Bill(d.id)),
})
.collect()
@@ -89,6 +91,7 @@ fn items(b: &Books, bills: bool) -> Vec<Item> {
.unwrap_or_default(),
date: d.date,
due: d.due_date,
terms: d.terms.clone(),
state: b.doc_today(DocRef::Invoice(d.id)),
})
.collect()
@@ -282,26 +285,23 @@ pub fn show_list(ui: &mut Ui, b: &Books, bills: bool, shell: &mut Shell<'_>) {
return;
}
let amt = widgets::AMOUNT_MIN + 16.0;
let fixed = 110.0 + 104.0 * 2.0 + 120.0 + amt * 2.0;
let fixed = 110.0 + 104.0 * 2.0 + 120.0 + amt * 2.0 + if bills { 104.0 } else { 0.0 };
let w = ui.available_width().max(fixed + 160.0);
widgets::min_width(ui, "invoice_list", w, |ui| {
let head = |t: &str, cw: f32, right: bool| (t.to_string(), cw, right, p.muted);
row(
ui,
w,
&[
head("Document", 110.0, false),
head(if bills { "Vendor" } else { "Customer" }, 0.0, false),
head("Date", 104.0, false),
head("Due", 104.0, false),
head("Status", 120.0, false),
head("Total", amt, true),
head("Still owed", amt, true),
],
None,
true,
false,
);
let mut header = vec![
head("Document", 110.0, false),
head(if bills { "Vendor" } else { "Customer" }, 0.0, false),
head("Date", 104.0, false),
head("Due", 104.0, false),
head("Status", 120.0, false),
head("Total", amt, true),
head("Still owed", amt, true),
];
if bills {
header.push(head("Discount by", 104.0, false));
}
row(ui, w, &header, None, true, false);
widgets::rule(ui);
for it in rows {
let (status, total, owed, late) = match &it.state {
@@ -314,28 +314,49 @@ pub fn show_list(ui: &mut Ui, b: &Books, bills: bool, shell: &mut Shell<'_>) {
Ok(None) => ("Unknown".into(), String::new(), String::new(), false),
Err(_) => ("Can't compute".into(), String::new(), String::new(), false),
};
let discount_by = it.terms.as_ref().and_then(|t| t.discount_by(it.date));
let discount_open = bills && discount_by.is_some_and(|d| d >= b.today);
let discount_soon = discount_open
&& discount_by.is_some_and(|d| d <= b.today + chrono::Days::new(3));
let due_soon = bills
&& matches!(&it.state, Ok(Some(s))
if !s.outstanding.is_zero() && s.status != DocStatus::Void)
&& it.due <= b.today + chrono::Days::new(7);
let name = format!("{}, {}, {}, owes {}", it.name, it.party, status, owed);
let resp = row(
ui,
w,
&[
(it.name.clone(), 110.0, false, p.text),
(it.party.clone(), 0.0, false, p.text),
(fmt::date(it.date), 104.0, false, p.text),
(
fmt::date(it.due),
104.0,
false,
if late { p.warning } else { p.text },
),
(status, 120.0, false, if late { p.warning } else { p.muted }),
(total, amt, true, p.text),
(owed, amt, true, p.text),
],
None,
false,
true,
);
let mut cells = vec![
(it.name.clone(), 110.0, false, p.text),
(it.party.clone(), 0.0, false, p.text),
(fmt::date(it.date), 104.0, false, p.text),
(
fmt::date(it.due),
104.0,
false,
if late || due_soon { p.warning } else { p.text },
),
(
status.clone(),
120.0,
false,
if late { p.warning } else { p.muted },
),
(total, amt, true, p.text),
(owed, amt, true, p.text),
];
if bills {
cells.push((
discount_by.map(fmt::date).unwrap_or_default(),
104.0,
false,
if discount_soon {
p.warning
} else if discount_open {
p.text
} else {
p.muted
},
));
}
let resp = row(ui, w, &cells, None, false, true);
// Named in full, for assistive tech (and the tests).
resp.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &name)