merge: P4b S7, bad debts (branch S7, 4b4fecb9)
The fourth of the paused branches: Ledger::write_off and the bad_debts control role, invoice.written_off with its evidence filed in the same store call, books.write_off_invoice, the write-off on the timeline and in Needs attention, Lean writeOff_within_bounds, rules.md B-41 and the bookspec examples. Twenty-eight files conflicted with the S4.4, S5 and S6 merges; resolved as plan 8.0.2 says, keeping both sides' appends with their seams restored, and: the scope table at 125; S7's law numbered 37; B-41 placed before B-42; the handbook's step 4 and recovery procedures folded into the one invoices-and-bills section; S7's wire tests in their own module with the confirm S4.4's contract needs; S7's copy of the payment steps dropped (S6 replaced them with Ledger::settle) and its write-off journaled through S6's journal_auto_lines; in the old-books fixture S7's hand-written write-off moved to a second invoice, since S6's hand-written payment settles invoice 1 in full. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
1e42d8f265
6 files changed
+241
-18
No files matched your search
@@ -264,6 +264,8 @@ the side gives the direction.
|
||||
on a receivables or payables account, where only an invoice, a bill or
|
||||
their payments post now (rules.md B-38, the control guard). Books that
|
||||
hold one must still read right, so the books take it as replay does.
|
||||
The same line written today is a `refused = "untagged_control_line"`
|
||||
event instead. An event cannot be both.
|
||||
- `close = { start, to, rounding? }` as an event: closes the period from
|
||||
`start` through the event's `date` (rules.md B-11, B-13), `to`
|
||||
`"members"` (split by share, in the order listed) or
|
||||
@@ -303,9 +305,18 @@ the side gives the direction.
|
||||
the disposal entry.
|
||||
- `run_schedules = { through }` as an event: every schedule's months due
|
||||
through `through` posted (B-16).
|
||||
- `refused = "<reason>"` on a reversal or a command: the books refuse it,
|
||||
for that reason (below); it changes nothing, posts nothing, and the
|
||||
example goes on without it.
|
||||
- `write_off = { invoice, amount?, reason }` as an event, dated: writes
|
||||
off the invoice event `invoice` (1-based; an `invoice` event before it)
|
||||
posted, as a bad debt (rules.md B-41): Bad Debt Expense (6990) is
|
||||
debited and Accounts Receivable credited, the receivable line tagged
|
||||
with the invoice, and the evidence of worthlessness filed in the same
|
||||
call. `amount` (an exact decimal above zero; left out, all the invoice
|
||||
owes on the day) makes it partial. Refused on cash-basis books and
|
||||
above what is owed.
|
||||
- `refused = "<reason>"` on a reversal, an entry or a command: the books
|
||||
refuse it, for that reason (below); it changes nothing, posts nothing,
|
||||
and the example goes on without it. An entry posts whole or not at all,
|
||||
so a refused one leaves no draft behind.
|
||||
- `[[changes]]` `{ after, rename?, require_tax_category?, correct_asset?
|
||||
}`: a change that posts no entry, exactly one of `rename = { code, name
|
||||
}` (rename the account `code`, B-31), `require_tax_category = "warn"`
|
||||
@@ -405,8 +416,10 @@ stands, or a run's change dated after the disposal's month isn't undone),
|
||||
disposal, a disposed asset's depreciation, a run re-instated twice or past
|
||||
its schedule, or a reversal dated before its run), `already_disposed`,
|
||||
`invalid_depreciation_period` (a month that ends before the asset is in
|
||||
service) and `invalid_schedule` (an equal-months schedule not starting on
|
||||
the 1st, among others).
|
||||
service), `invalid_schedule` (an equal-months schedule not starting on
|
||||
the 1st, among others) and `untagged_control_line` (a hand line on a
|
||||
receivables or payables control account, which only a document may post
|
||||
to: rules.md B-38's control guard).
|
||||
|
||||
`cash_flow` (rules.md B-14): the statement of cash flows for `[from, to]`.
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ pub fn rule_for_refusal(reason: &str) -> Option<&'static Rule> {
|
||||
| "already_disposed"
|
||||
| "invalid_depreciation_period" => 15,
|
||||
"invalid_schedule" => 16,
|
||||
"untagged_control_line" => 38,
|
||||
_ => return None,
|
||||
};
|
||||
rule(b)
|
||||
@@ -529,7 +530,7 @@ const SPEC: Spec = &[
|
||||
Area::Arap,
|
||||
"A document's figures are its ledger lines that stand; a control account is its documents",
|
||||
Some(Status::Choice),
|
||||
&[],
|
||||
&["arap-hand-control-posting-refused"],
|
||||
),
|
||||
(
|
||||
39,
|
||||
@@ -548,9 +549,9 @@ const SPEC: Spec = &[
|
||||
(
|
||||
41,
|
||||
Area::Arap,
|
||||
"Write-offs (allocated, not yet stated)",
|
||||
None,
|
||||
&[],
|
||||
"A debt that will not be collected is written off, and a recovery is income",
|
||||
Some(Status::Choice),
|
||||
&["bad-debt-direct-write-off"],
|
||||
),
|
||||
(
|
||||
42,
|
||||
|
||||
+59
-3
@@ -367,6 +367,9 @@ pub struct Event {
|
||||
/// An invoice.
|
||||
#[serde(default)]
|
||||
pub invoice: Option<InvoiceSpec>,
|
||||
/// A bad-debt write-off of an earlier invoice.
|
||||
#[serde(default)]
|
||||
pub write_off: Option<WriteOffSpec>,
|
||||
/// A depreciation run.
|
||||
#[serde(default)]
|
||||
pub run_depreciation: Option<RunSpec>,
|
||||
@@ -409,6 +412,7 @@ impl Event {
|
||||
(EventKind::Close, self.close.is_some()),
|
||||
(EventKind::Bill, self.bill.is_some()),
|
||||
(EventKind::Invoice, self.invoice.is_some()),
|
||||
(EventKind::WriteOff, self.write_off.is_some()),
|
||||
(EventKind::RunDepreciation, self.run_depreciation.is_some()),
|
||||
(EventKind::DisposeAsset, self.dispose_asset.is_some()),
|
||||
(EventKind::RunSchedules, self.run_schedules.is_some()),
|
||||
@@ -445,6 +449,8 @@ pub enum EventKind {
|
||||
Bill,
|
||||
/// An invoice.
|
||||
Invoice,
|
||||
/// A bad-debt write-off of an earlier invoice (rules.md B-41).
|
||||
WriteOff,
|
||||
/// A depreciation run.
|
||||
RunDepreciation,
|
||||
/// A fixed asset's disposal.
|
||||
@@ -464,6 +470,7 @@ impl EventKind {
|
||||
EventKind::Close => "close",
|
||||
EventKind::Bill => "bill",
|
||||
EventKind::Invoice => "invoice",
|
||||
EventKind::WriteOff => "write_off",
|
||||
EventKind::RunDepreciation => "run_depreciation",
|
||||
EventKind::DisposeAsset => "dispose_asset",
|
||||
EventKind::RunSchedules => "run_schedules",
|
||||
@@ -481,6 +488,7 @@ impl EventKind {
|
||||
| EventKind::Close
|
||||
| EventKind::Bill
|
||||
| EventKind::Invoice
|
||||
| EventKind::WriteOff
|
||||
| EventKind::DisposeAsset
|
||||
)
|
||||
}
|
||||
@@ -499,12 +507,14 @@ impl EventKind {
|
||||
}
|
||||
|
||||
/// True when the books take it whole or not at all, so `refused` may
|
||||
/// be given: a reversal or a command, never an entry, a close, a bill
|
||||
/// or an invoice.
|
||||
/// be given: a reversal, an entry or a command, never a close, a bill
|
||||
/// or an invoice (those are corrected by a reversal). An entry posts
|
||||
/// whole or not at all, so a hand line the control guard refuses is
|
||||
/// one too.
|
||||
pub fn may_refuse(self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
EventKind::Lines | EventKind::Close | EventKind::Bill | EventKind::Invoice
|
||||
EventKind::Close | EventKind::Bill | EventKind::Invoice
|
||||
)
|
||||
}
|
||||
|
||||
@@ -513,6 +523,7 @@ impl EventKind {
|
||||
!matches!(
|
||||
self,
|
||||
EventKind::Reverses
|
||||
| EventKind::WriteOff
|
||||
| EventKind::RunDepreciation
|
||||
| EventKind::DisposeAsset
|
||||
| EventKind::RunSchedules
|
||||
@@ -582,6 +593,23 @@ pub struct InvoiceSpec {
|
||||
pub lines: Vec<DocumentLineSpec>,
|
||||
}
|
||||
|
||||
/// A write-off of the invoice an earlier event posted (rules.md B-41;
|
||||
/// plan P4b S7): the invoice's event, what to write off and why.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WriteOffSpec {
|
||||
/// The event (1-based) whose invoice this writes off: an `invoice`
|
||||
/// event before it.
|
||||
pub invoice: usize,
|
||||
/// How much to write off, an exact decimal string above zero; left
|
||||
/// out, all the invoice owes on the day.
|
||||
#[serde(default)]
|
||||
pub amount: Option<String>,
|
||||
/// Why it will not be collected (collection correspondence, a
|
||||
/// bankruptcy), one line.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// One line of an invoice or a bill.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -973,6 +1001,7 @@ pub fn refusal_reasons() -> impl Iterator<Item = &'static str> {
|
||||
"already_disposed",
|
||||
"invalid_depreciation_period",
|
||||
"invalid_schedule",
|
||||
"untagged_control_line",
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -1105,6 +1134,13 @@ impl Example {
|
||||
"older is an entry's lines, as an earlier version wrote them".into(),
|
||||
));
|
||||
}
|
||||
if ev.older && ev.refused.is_some() {
|
||||
return Err(what(
|
||||
"an entry posted as an earlier version wrote it (older) can't also be \
|
||||
refused"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if ev.payee.is_some() && kind != EventKind::Lines {
|
||||
return Err(what(
|
||||
"a payee is an entry's: a reversal takes its original's, a bill names \
|
||||
@@ -1198,6 +1234,26 @@ impl Example {
|
||||
return Err(what("an invoice names its customer".into()));
|
||||
}
|
||||
}
|
||||
if let Some(w) = &ev.write_off {
|
||||
if w.invoice < 1 || w.invoice >= n {
|
||||
return Err(what(format!(
|
||||
"write_off names event {}: an earlier invoice",
|
||||
w.invoice
|
||||
)));
|
||||
}
|
||||
if self.events[w.invoice - 1].invoice.is_none() {
|
||||
return Err(what(format!(
|
||||
"write_off names event {}, which is no invoice",
|
||||
w.invoice
|
||||
)));
|
||||
}
|
||||
if let Some(a) = &w.amount {
|
||||
check_amount(a, true, &what)?;
|
||||
}
|
||||
if w.reason.trim().is_empty() {
|
||||
return Err(what("a write-off says why it will not be collected".into()));
|
||||
}
|
||||
}
|
||||
if let Some(r) = &ev.run_depreciation {
|
||||
check_month(&r.period, &what)?;
|
||||
if !self.setup.assets.iter().any(|a| a.id == r.asset) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
id = "arap-hand-control-posting-refused"
|
||||
title = "A hand line on receivables is refused; one an earlier version wrote still reads right"
|
||||
rule = "A line on a receivables or payables control account MUST carry its document, so a hand entry MUST be refused; an entry an earlier version wrote is replayed as it stands."
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"plan/served.md 8.3.1 A-6 (the owner: hand entries to 1200 and 2000 are refused)",
|
||||
"plan/served.md D-45",
|
||||
"ASC 310-10 (receivables)",
|
||||
"ASC 405-20-40-1 (a liability is removed only when paid or released)",
|
||||
]
|
||||
tags = ["accrual", "book", "us", "receivables", "legacy"]
|
||||
|
||||
[setup]
|
||||
currency = "USD"
|
||||
fiscal_year_start = "01-01"
|
||||
basis = "accrual"
|
||||
rounding = "half-even"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "1000"
|
||||
name = "Checking"
|
||||
type = "asset"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "1200"
|
||||
name = "Accounts Receivable"
|
||||
type = "asset"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "4000"
|
||||
name = "Revenue"
|
||||
type = "revenue"
|
||||
|
||||
# The invoice makes 1200 a control account: its balance is now its
|
||||
# documents'. The work was billed and the income counted.
|
||||
[[events]]
|
||||
date = "2026-01-10"
|
||||
description = "Survey work"
|
||||
invoice = { customer = "Ridgeway Co", amount = "100.00" }
|
||||
|
||||
# A line an earlier version wrote by hand on 1200: this version's commands
|
||||
# refuse one, but books that hold it must still read right, so it is
|
||||
# posted as replay posts it.
|
||||
[[events]]
|
||||
date = "2026-01-15"
|
||||
description = "Receivable entered by hand"
|
||||
older = true
|
||||
lines = [
|
||||
{ account = "1200", side = "debit", amount = "50.00" },
|
||||
{ account = "4000", side = "credit", amount = "50.00" },
|
||||
]
|
||||
|
||||
# The same line written today is refused: no document carries it.
|
||||
[[events]]
|
||||
date = "2026-01-20"
|
||||
description = "Another hand line"
|
||||
refused = "untagged_control_line"
|
||||
lines = [
|
||||
{ account = "1200", side = "debit", amount = "25.00" },
|
||||
{ account = "4000", side = "credit", amount = "25.00" },
|
||||
]
|
||||
|
||||
[[expect]]
|
||||
query = "balances"
|
||||
as_of = "2026-12-31"
|
||||
[expect.values]
|
||||
"1000" = "0.00"
|
||||
"1200" = "150.00"
|
||||
"4000" = "150.00"
|
||||
|
||||
[[expect]]
|
||||
query = "trial_balance"
|
||||
as_of = "2026-12-31"
|
||||
[expect.values]
|
||||
"1000" = "0.00"
|
||||
"1200" = "150.00"
|
||||
"4000" = "-150.00"
|
||||
@@ -0,0 +1,76 @@
|
||||
id = "bad-debt-direct-write-off"
|
||||
title = "An invoice that will not be collected is written off, in parts"
|
||||
rule = "A debt that becomes worthless MUST be written off in the year it becomes worthless, a partial write-off MUST be deductible to the extent charged off, and no write-off may exceed what is owed."
|
||||
status = "CHOICE"
|
||||
sources = [
|
||||
"IRC §166(a)",
|
||||
"IRC §111",
|
||||
"Treas. Reg. §1.166-1(e)",
|
||||
"Treas. Reg. §1.166-2(a)",
|
||||
"Treas. Reg. §1.166-3(a)",
|
||||
"plan/served.md 8.3.1 A-5: the direct write-off method",
|
||||
"ASC 326-20 (noted: an allowance is the GAAP preference)",
|
||||
]
|
||||
tags = ["accrual", "book", "us", "receivables"]
|
||||
|
||||
[setup]
|
||||
currency = "USD"
|
||||
fiscal_year_start = "01-01"
|
||||
basis = "accrual"
|
||||
rounding = "half-even"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "1000"
|
||||
name = "Checking"
|
||||
type = "asset"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "1200"
|
||||
name = "Accounts Receivable"
|
||||
type = "asset"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "4000"
|
||||
name = "Revenue"
|
||||
type = "revenue"
|
||||
|
||||
[[setup.accounts]]
|
||||
code = "6990"
|
||||
name = "Bad Debt Expense"
|
||||
type = "expense"
|
||||
tax_category = "bad_debts"
|
||||
|
||||
# The work was done and billed: the invoice is income already counted.
|
||||
[[events]]
|
||||
date = "2026-01-10"
|
||||
description = "Survey work"
|
||||
invoice = { customer = "Ridgeway Co", amount = "100.00" }
|
||||
|
||||
# Part of it is charged off when the collection efforts end; the rest
|
||||
# stays owed and aged.
|
||||
[[events]]
|
||||
date = "2026-03-15"
|
||||
write_off = { invoice = 1, amount = "40.00", reason = "No answer; the collection agency closed the file" }
|
||||
|
||||
# The liquidation notice makes the rest worthless, and it is written off.
|
||||
[[events]]
|
||||
date = "2026-04-20"
|
||||
write_off = { invoice = 1, amount = "60.00", reason = "Liquidation notice received" }
|
||||
|
||||
[[expect]]
|
||||
query = "balances"
|
||||
as_of = "2026-12-31"
|
||||
[expect.values]
|
||||
"1000" = "0.00"
|
||||
"1200" = "0.00"
|
||||
"4000" = "100.00"
|
||||
"6990" = "100.00"
|
||||
|
||||
[[expect]]
|
||||
query = "trial_balance"
|
||||
as_of = "2026-12-31"
|
||||
[expect.values]
|
||||
"1000" = "0.00"
|
||||
"1200" = "0.00"
|
||||
"4000" = "-100.00"
|
||||
"6990" = "100.00"
|
||||
+6
-6
@@ -6,10 +6,10 @@ what its statement rests on and the examples that demonstrate it.
|
||||
|
||||
A rule whose only source is a human choice carries rules.md's **CHOICE**, bookspec's status of the same name:
|
||||
the specification names the options, an engine declares its pick
|
||||
(`claims.choices`, `bookspec_checker::Claims`). Rules B-39 to B-45 are
|
||||
allocated in `plan/served.md` 8.0.3; those not yet stated keep only
|
||||
their identifier and area here, so a later unit fills them in without
|
||||
moving anyone else's number.
|
||||
(`claims.choices`, `bookspec_checker::Claims`). Rules B-39, B-40 and B-43
|
||||
to B-45 are allocated in `plan/served.md` 8.0.3 and not yet stated: their
|
||||
identifier and area are fixed here so a later unit fills them in
|
||||
without moving anyone else's number.
|
||||
|
||||
| B | bookspec id | area | status | rule | examples |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -50,10 +50,10 @@ moving anyone else's number.
|
||||
| B-35 | `BS-partner-3` | partner | CONVENTION | A party's part of an entry, and a party's totals | `p3-party-accrual-counts-once`, `p3-party-distribution-still-owed`, `p3-party-gift-card-is-a-promise`, `p3-party-members-equity-and-k1`, `p3-party-own-liability-never-pays`, `p3-party-reversed-card-payment`, `p3-party-signs-and-reversals` |
|
||||
| B-36 | `BS-partner-4` | partner | AUTHORITY | The K-1 worksheet reports tax-basis capital beside book capital | `partnership-k1-tax-basis-capital`, `partnership-k1-tax-basis-odd-cents` |
|
||||
| B-37 | `BS-statement-5` | statement | CHOICE | A project is a class with its facts; its figures are its tagged lines | `projects-invoice-and-bill-lines-count-by-project`, `projects-net-is-its-tagged-lines`, `projects-on-spec-costs-are-expenses` |
|
||||
| B-38 | `BS-arap-2` | arap | CHOICE | A document's figures are its ledger lines that stand; a control account is its documents | — |
|
||||
| B-38 | `BS-arap-2` | arap | CHOICE | A document's figures are its ledger lines that stand; a control account is its documents | `arap-hand-control-posting-refused` |
|
||||
| B-39 | `BS-arap-3` | arap | — | Terms: when a document is due (allocated, not yet stated) | — |
|
||||
| B-40 | `BS-arap-4` | arap | — | Numbers and the issued copy (allocated, not yet stated) | — |
|
||||
| B-41 | `BS-arap-5` | arap | — | Write-offs (allocated, not yet stated) | — |
|
||||
| B-41 | `BS-arap-5` | arap | CHOICE | A debt that will not be collected is written off, and a recovery is income | `bad-debt-direct-write-off` |
|
||||
| B-42 | `BS-arap-6` | arap | CONVENTION | A settlement is exact, one party, and names its lines; the gross discount and the gross fee | — |
|
||||
| B-43 | `BS-entry-5` | entry | — | Drafts and evidence (allocated, not yet stated) | — |
|
||||
| B-44 | `BS-ledger-12` | ledger | — | Extraction and dedup (allocated, not yet stated) | — |
|
||||
|
||||
Reference in new issue
Block a user