feat(arap): a write-off on the timeline, and the CPA's note (P4b S7)

The viewer's invoice page already drew a WriteOff movement; now the
fixture proves it: a written-off invoice reads Written off, owes nothing,
and its timeline's "Written off" row opens the entry (the screenshot
invoice-written-off).

Needs attention gains the one item the tax rule asks for: a write-off
made this calendar year is listed, one per invoice, as "Write-off
awaiting the CPA's note", with what was written off, the day, and the
prompt that opens the document. Whether a debt is worthless, and in
which year, is a tax judgment that stays with the CPA (plan 8.3.16); the
books flag it and keep the evidence beside it until the year ends. The
item is computed in ledger-package's one attention function, so the
viewer and books.start see it alike; view_model.rs compares the two reads
on the same books.

The handbook's invoices-and-bills section starts here (S4.5 adds the
issue, terms and payment steps around it): step 4 says when to void, to
write off, or to reverse, and the recovery procedures D-49 records — the
same year, reverse the write-off and record the payment; a later year, a
plain entry to a bad-debt-recoveries revenue account, the invoice staying
written off (IRC §111).

Tests: a_write_off_shows_on_the_timeline,
a_write_off_is_flagged_for_the_cpa.
This commit is contained in:
swaits committed 2026-10-27 12:44:45 -06:00
1 parent 0f1c7786ab
commit b731d7ed04
5 files changed
+294 -1

No files matched your search

+61 -1
View File
@@ -37,7 +37,10 @@
//! books.set_entry_instrument; a year whose worksheet can't be made
//! for want of named cash accounts while vendors were paid in it
//! (Todo); overdue bills, and overdue
//! invoices (Problem, one each, with the total); money owed to
//! invoices (Problem, one each, with the total); a written-off
//! invoice whose write-off is dated this calendar year and stands
//! (Todo, one per invoice, "Write-off awaiting the CPA's note",
//! its prompt naming books.document); money owed to
//! owners (Info, per account with a balance); the database differing
//! from the journal or unreadable, and damaged or missing stored
//! files (Problem); depreciation due through the last finished month
@@ -717,6 +720,63 @@ pub fn find_with(
});
}
// A write-off this year waits for the CPA's note: the deduction rests
// on evidence of worthlessness a human signs off, and §166's judgments
// stay with the CPA (plan P4b S7, 8.3.16; IRC §166(a); Treas. Reg.
// §1.166-2(a)). Listed until the year ends.
let mut written_off: Vec<(u64, Option<String>, NaiveDate, Decimal)> = Vec::new();
for inv in ledger.invoices() {
let doc = ledger_core::DocRef::Invoice(inv.id);
let state = ledger.invoice_state(inv.id, today).ok().flatten();
if state.map(|s| s.status) != Some(ledger_core::DocStatus::WrittenOff) {
continue;
}
let lines: Vec<ledger_core::DocLine> = ledger
.doc_lines(doc)
.into_iter()
.filter(|l| {
l.movement == ledger_core::Movement::WriteOff
&& l.date.year() == today.year()
&& ledger.stands_on(l.entry, today)
})
.collect();
let Some(last) = lines.last() else {
continue;
};
match lines
.iter()
.try_fold(Decimal::ZERO, |t, l| t.checked_sub(l.amount))
{
Some(amount) => written_off.push((inv.id, inv.number.clone(), last.date, amount)),
None => out.push(cant_compute(
format!("Invoice {}", inv.id),
&LedgerError::TooLargeToCompute("what was written off".into()),
Target::Documents,
)),
}
}
for (id, number, on, amount) in written_off {
let name = number.unwrap_or_else(|| format!("Invoice {id}"));
out.push(Attention {
severity: Severity::Todo,
kind: "write_off_note",
title: format!("Write-off awaiting the CPA's note: {name}"),
detail: format!(
"{} written off on {} (IRC §166; Treas. Reg. §1.166-2: the evidence of worthlessness is filed with the write-off).",
dollars(amount),
date(on)
),
target: Target::Documents,
prompt: format!(
"In wanderbooks, show me invoice {id}'s write-off (books.document), with its reason and the evidence filed with it, and note it for the CPA at year end."
),
tool: Some((
"books.document".into(),
serde_json::json!({ "ref": format!("invoice:{id}") }),
)),
});
}
for a in ledger.chart().all().filter(|a| {
a.account_type == AccountType::Liability && {
let n = a.name.to_lowercase();