feat(package): aging CSVs in the export and the CPA package (P4b S6)
The CPA's first tie-out is that the aging equals the control account, so books.export and the CPA package now write ar_aging.csv and ap_aging.csv from one builder: one row per open document (number, party, dates, terms, total, paid, discounted, outstanding, bucket, days overdue) and a TOTAL line carrying the outstanding sum, the GL account and balance, and the difference (always 0.00; B-23, B-38 INVARIANT 1). XLSX and PDF lay the same aging out as a statement with the tie-out beneath it. The package README states the tie-out, the gross discount method (A-8) and the direct write-off method (A-5). The chart templates gain 6995 Purchase Discounts so no template puts another account at a control role's code (4100 and 6060 already match). The viewer's Money out list gains the discount-deadline column and colours a due-soon or discount-open date; the screenshot is re-rendered. Test: the_aging_csvs_tie_to_the_control_check in ledger-package; the existing package file-order test and mcp_journeys see the two new files.
This commit is contained in:
1 parent
57f04dbb0c
commit
6c4480cdc8
8 files changed
+390
-39
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user