feat(mcp): books.write_off_invoice (P4b S7)

One tool for the bad debt (rules.md B-41): it writes off an invoice that
will not be collected, Dr 6990 Bad Debt Expense / Cr 1200 Accounts
Receivable, the receivable line tagged with the invoice so the document
reads written_off and its figures stay derived (B-38).

The evidence is the point, and the tool takes exactly one of
upload_id (an upload, filed with the write-off) and
existing_attachment_id (one already filed, linked to the write-off's
entry and the invoice); neither or both is refused. amount defaults to
what the invoice owes and may be partial.

Without confirmed it is a preview: the engine runs on a clone (every
refusal the post would give) and the answer is the figures the write-off
would leave, entry_id 0, nothing journaled.

The tool lives in its own group file, tools/write_offs.rs (plan U0a), so
S4.4's document tools and this one land apart; it is destructive, needs
books:write to preview and books:post to post (scopes.rs), and is
documented under "Tools — A/R and A/P". The destructive-tool count, the
tool tables and the journey test's tool count each move by one.

Tests over the wire: write_off_then_recovery, write_off_needs_evidence.
This commit is contained in:
swaits committed 2026-10-27 12:44:45 -06:00
1 parent 4e71cf6320
commit 0f1c7786ab
8 files changed
+464 -7

No files matched your search

+22
View File
@@ -2029,6 +2029,28 @@ S4 changes `books.create_invoice` and `books.create_bill`; see plan 8.2 S4.
number and reads `void`. Refused while a payment stands on it (the
refusal names the payment entry to reverse first), when void already,
before its date or in a closed period (P4b S1, B-38).
- `books.write_off_invoice { invoice, date, amount?, reason, upload_id |
existing_attachment_id, confirmed? }` → `{ entry_id, document: { ref,
status, paid, written_off, outstanding } }`. Writes off an invoice that
will not be collected (a bad debt, P4b S7; rules.md B-41): posts Dr
`6990` Bad Debt Expense / Cr `1200` Accounts Receivable, the receivable
line tagged with the invoice (`WriteOff`), so the invoice reads
`written_off` and its figures stay derived (B-38). `amount` (a decimal
string, default all it owes) is at most what is owed and may be partial
(§1.166-3(a)). Its evidence of worthlessness (collection
correspondence, a bankruptcy notice: §1.166-2(a)) is filed in the same
call, as `upload_id` (books.begin_upload, purpose `document` or
`receipt`, then `wanderbooks upload`) or as an
`existing_attachment_id` already filed and not linked; it becomes a
receipt on the write-off's entry and a link to the invoice. Refused on
cash-basis books (§1.166-1(e)), above what it owes, before the
invoice's date or in a closed period. Without `confirmed: true` it
answers `entry_id` 0 and the figures the write-off would leave, writing
nothing. A recovery is not a new command (D-49): the same tax year,
reverse the write-off (`books.reverse_entry`, the invoice reopens) and
record the payment; a later year, a plain entry Dr the deposit account /
Cr a "Bad debt recoveries" revenue account, the invoice staying written
off (§111).
- `books.documents { kind, status?, party?, due_by?, as_of? }` → `{ as_of,
kind, documents: [{ ref, party, date, due_date, total, outstanding,
status, overdue_days }], outstanding_total }`. Invoices (`kind`