feat(mcp): the create tools preview and confirm, and the party keeps its contact block (P4b S4.4)
An invoice is issued, not edited. Without `confirmed` the create tools now preview: the posting (with the accounts' codes, which the live chart may not hold yet), the number it will take, its due date, terms and early discount, warnings, and for an invoice the DRAFT copy of the PDF, handed over as a file. Nothing is written and no number is used up. With `confirmed` the invoice posts, takes the number the preview showed, and files the issued copy with itself in the same call: `receipt.attached` on its entry plus the link to the invoice, its SHA-256 in the event (A-1, A-7, B-40). The copy the customer received is therefore the copy in the books, and `books.document`'s re-created page can never be mistaken for it. The result also carries a `mailto` link for a human to send it; the books never send (A-4). A bill keeps what the plan says a bill is: the vendor's own invoice number, the vendor's invoice filed on its entry as its evidence, and, in books that have declared `documents_v2`, an account on every line, so no bill can land in General Expenses by default any more (A-9, A-10, A-13). A vendor number entered twice is refused unless the caller says why, and the reason is journaled with the second bill (B-40 item 4). Books that have not declared the feature keep recording bills exactly as before, so the owner's books keep working between units (D-39, 8.0.4). Terms are resolved in one place and in one order: the due date given, else the terms given, else the customer's or vendor's own default, else the books' `invoicing.default_terms`; a document given only a due date stores no terms, so an old-shape invoice is still written byte for byte as before (B-39, A-13). D-55: a party carries the owner's full contact block (phone, contact person, billing and shipping addresses, billing email). It rides the same `party.updated` event behind `documents_v2`, and an invoice's bill-to is snapshotted at issue with the billing address and billing email in place of the general ones when the party has them — `PartySnapshot::from_party` is the one place that rule lives, so a later edit cannot change what was sent, and the next invoice takes today's details. Tests: the wire tests are the new `crates/ledger-cli/tests/documents.rs` (preview writes nothing, the DRAFT copy, the issued copy's SHA-256 equals `books.get_receipt`'s, numbers run, a bill's file and terms, the duplicate refusal, the line-account rules, the terms fallback, the `party_id` alias and the ambiguity it refuses, the contact block's set and clear, and the bill-to at issue); `format_upgrade.rs` gains `documents_v2_refused_until_consent` and the cut-over test now refuses and then makes a document carrying a new field (8.0.6 item 3); the store tests are `the_contact_block_round_trips_and_replays` and `an_invoice_keeps_the_billing_details_it_was_issued_with`, and `books_written_under_older_rules_still_open` gains a hand-written contact-block event (8.0.6 item 1). Merge does not need the owner's go-ahead: the live books refuse a write that needs `documents_v2` until he gives it, and take every other write as before (8.0.6 item 6). No figure moved: S4 moves none.
This commit is contained in:
1 parent
ed637f0e27
commit
d5b9e5cf99
28 files changed
+2604
-190
No files matched your search
+122
-48
@@ -1186,15 +1186,29 @@ texts are at most 500 characters, no control characters.
|
||||
- `books.add_party { name, roles? }` → the party (below). Journaled as
|
||||
`party.added`.
|
||||
- `books.update_party { party, name?, display_name?, website?, address?,
|
||||
notes?, archived?, creditor_of? }` → the party. Give at least one field;
|
||||
`""` clears `display_name`, `website`, `address`, or `notes`;
|
||||
`creditor_of` (account codes or ids) replaces the party's list of the
|
||||
liabilities the company owes it itself (below). Every field is checked
|
||||
(in order, on a rehearsal) before any is written; each is journaled as
|
||||
its own `party.updated`, in the order name, display_name, website,
|
||||
address, notes, archived, then one `party.creditor_set` per account
|
||||
added or dropped. Un-archiving is refused while another party has the
|
||||
name.
|
||||
notes?, email?, default_terms_as_customer?, default_terms_as_vendor?,
|
||||
phone?, contact?, billing_address?, shipping_address?, billing_email?,
|
||||
archived?, creditor_of? }` → the party. Give at least one field; `""`
|
||||
clears `display_name`, `website`, `address`, `notes`, `email`, the terms,
|
||||
`phone`, `contact`, `billing_address`, `shipping_address` or
|
||||
`billing_email`; `creditor_of` (account codes or ids) replaces the
|
||||
party's list of the liabilities the company owes it itself (below).
|
||||
`email` is its general address; `billing_email` is where its bills go
|
||||
(an invoice's bill-to email, when set, else the general one). `address`
|
||||
is its general/business address; `billing_address` where its bills go
|
||||
(the bill-to address when set, else the general one) and
|
||||
`shipping_address` where goods go; `contact` is the person there to
|
||||
write to and `phone` its number (D-55). The terms are trade terms per
|
||||
side (`"Net 30"`, `"2/10 net 30"`): a document's due date falls back to
|
||||
them (below). Every field is checked (in order, on a rehearsal) before
|
||||
any is written; each is journaled as its own `party.updated`, in the
|
||||
order name, display_name, website, address, notes, email, the two terms,
|
||||
phone, contact, billing_address, shipping_address, billing_email,
|
||||
archived, then one `party.creditor_set` per account added or dropped.
|
||||
The email, terms and contact block need the journal feature
|
||||
`documents_v2` (rules.md B-27): books that haven't declared it refuse
|
||||
them with `format_upgrade_pending`, journaling nothing. Un-archiving is
|
||||
refused while another party has the name.
|
||||
- `books.set_party_roles { party, add?, remove? }` → the party. Adds apply
|
||||
before removes (so a contractor's W-9 survives a swap to landlord); roles
|
||||
it already has, or lacks, are skipped; each change is journaled as
|
||||
@@ -1628,10 +1642,12 @@ posting.
|
||||
violation, and the append-only audit log would show it anyway.
|
||||
(`asset_id` instead of `entry_id` for a disposal; `split_group: [ids]`
|
||||
when the split check triggered.)
|
||||
- **Not checked yet:** `books.create_invoice`, `books.create_bill`,
|
||||
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
|
||||
`books.remit_payroll_taxes` post at once with no preview and no receipt
|
||||
check: attach their evidence right after, to the entry they return.
|
||||
- **Not checked yet:** `books.receive_payment`, `books.pay_bill`,
|
||||
`books.run_payroll` and `books.remit_payroll_taxes` post at once with no
|
||||
receipt check: attach their evidence right after, to the entry they
|
||||
return. (`books.create_invoice` and `books.create_bill` preview and
|
||||
confirm, and a bill's vendor invoice is required, in books that have
|
||||
declared `documents_v2`.)
|
||||
- **Anti-splitting.** Posting an entry triggers a split check: posted
|
||||
entries on the same date with the same normalized description and debit
|
||||
accounts are grouped, and if the group total exceeds the threshold while
|
||||
@@ -1969,45 +1985,103 @@ accounting only: the caller supplies every figure), tax filing.
|
||||
|
||||
## Tools — A/R and A/P
|
||||
|
||||
S4 changes `books.create_invoice` and `books.create_bill`; see plan 8.2 S4.
|
||||
S4 gives `books.create_invoice` and `books.create_bill` a preview and a
|
||||
confirm (A-1) and the documents numbers, terms, line accounts and party
|
||||
snapshots; see plan 8.2 S4.
|
||||
|
||||
- `books.add_customer { name }` → `{ id, party_id }`.
|
||||
- `books.add_vendor { name }` → `{ id, party_id }`. Both link the record to
|
||||
a party ("Tools — parties").
|
||||
- `books.create_invoice { party_id, date, due_date, lines: [{ description,
|
||||
amount, class? }] }` → `{ id, total, entry_id, warnings?, notes? }`.
|
||||
Auto-posts Dr `1200` Accounts Receivable / Cr `4000` Revenue. `warnings`
|
||||
names a revenue account it created with no tax category (rules.md
|
||||
B-32). `party_id` is the customer's `id` from `books.add_customer`, not
|
||||
its `party_id`. A line's `class` (a project: name or id, resolved as
|
||||
`books.add_line`'s) puts its revenue on that project (rules.md B-37 item
|
||||
8, the owner's decision D-43): one project or none per line, several
|
||||
projects as several lines each with its own amount, never a percentage
|
||||
(a cost truly shared names none: it reaches the projects only as
|
||||
entries of their own under the company's monthly allocation convention,
|
||||
when `books.conventions` has one, and otherwise stays untagged while the
|
||||
agent asks the human). The entry's revenue side is one line per project named, in the
|
||||
order each is first named, the untagged lines' as one (a document
|
||||
naming none posts the one revenue line it always did); the receivable
|
||||
line is never tagged. Every project is resolved before anything is
|
||||
written (an unknown one is refused, the document whole). `notes`: one
|
||||
per finished or abandoned project named, as `books.add_line`'s `note`,
|
||||
and one per on-spec project named, asking whether there is now an
|
||||
agreement (on spec, a hoped-for payment is no receivable until
|
||||
received: B-37 item 4) (tagged anyway). Books that haven't declared the journal feature
|
||||
`document_line_projects` (B-27; the live books until their owner's next
|
||||
upgrade, D-39) refuse a line naming a project with
|
||||
`format_upgrade_pending`, journaling nothing; a document naming none is
|
||||
taken as before.
|
||||
- `books.create_bill { party_id, date, due_date, lines }` → `{ id, total,
|
||||
entry_id, warnings?, notes? }`. Auto-posts Dr `6999` General Expenses /
|
||||
Cr `2000` Accounts Payable; `warnings` as for invoices. `party_id` is
|
||||
the vendor's `id` from `books.add_vendor`, not its `party_id`. A line's
|
||||
`class` puts its cost on that project, as an invoice's its revenue: one
|
||||
expense line per project, the untagged lines' as one, the payable line
|
||||
never tagged; `notes` as for invoices, but none for an on-spec project
|
||||
(a cost of on-spec work is an expense like any other); the feature as
|
||||
for invoices.
|
||||
- `books.create_invoice { customer | party_id, date, terms? | due_date?,
|
||||
lines: [{ description, amount, quantity?, unit_price?, account?,
|
||||
class? }], customer_ref?, memo?, bill_to?: { name?, address?, email?,
|
||||
phone?, contact?, billing_address?, shipping_address?, billing_email? },
|
||||
confirmed? }` → without `confirmed`: `{ preview: true, number?, total,
|
||||
due_date, terms?, lines: [{ account, name, side, amount, class? }],
|
||||
pdf }`, and nothing is written; with `confirmed: true`: `{ id, number?,
|
||||
total, due_date, terms?, entry_id, pdf: { attachment_id, sha256 },
|
||||
mailto?, warnings?, notes? }`.
|
||||
Confirmed it posts Dr `1200` Accounts Receivable / Cr each line's
|
||||
account (the revenue role, `4000`, when a line names none), the
|
||||
receivable line tagged with the invoice and the receivable side one line
|
||||
per (account, project) part.
|
||||
- `customer` is the customer's id, its party's id or its name (a name
|
||||
two customers answer to is refused); `party_id` is the old spelling of
|
||||
the same thing (a customer id), kept until P5.
|
||||
- The due date: `due_date` as given (and it overrides `terms`); else
|
||||
`terms` ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on receipt",
|
||||
"Due 2026-11-15", rules.md B-39); else the customer's
|
||||
`default_terms_as_customer`; else the books' `invoicing.default_terms`
|
||||
("Net 30" unset); else refused, naming those ways. Terms are kept on
|
||||
the invoice only when given or taken from a default: with only a due
|
||||
date the invoice prints none.
|
||||
- Its number (`INV-0001`) is `invoicing.number_prefix` and
|
||||
`invoicing.first_number` plus the count of numbered invoices before it
|
||||
(A-2, B-40); it is stored at issue and never recomputed, and a preview
|
||||
shows the number the confirm will take (an abandoned preview uses up
|
||||
none). Books that haven't declared `documents_v2` number nothing and
|
||||
show a document as "Invoice {N}".
|
||||
- The bill-to snapshot is the customer's name, address and email at
|
||||
issue, with its `billing_address` and `billing_email` in their place
|
||||
when it has them, and its `contact` person, `phone` and
|
||||
`shipping_address` (D-55); a `bill_to` object overrides any part for
|
||||
this one document. `customer_ref` is the customer's own reference (a
|
||||
purchase order) and `memo` prints on the page. These are new fields:
|
||||
books that haven't declared `documents_v2` refuse a document carrying
|
||||
any of them with `format_upgrade_pending`, journaling nothing, and
|
||||
take one carrying none as before.
|
||||
- A line's `class` (a project: name or id) tags its revenue, as before;
|
||||
a line's `account` sends it to a revenue account or a liability
|
||||
holding no role (deferred revenue). `quantity` and `unit_price` go
|
||||
together, and quantity × unit_price must equal `amount` exactly.
|
||||
- Without `confirmed` the DRAFT copy of the PDF is handed over (`pdf`,
|
||||
run its `download`, D-46); confirming renders the issued copy before
|
||||
anything is journaled and files it with the invoice in the same call
|
||||
(A-7, B-40), returning its `attachment_id` and `sha256` (the same
|
||||
`sha256` `books.get_receipt` reports).
|
||||
- `mailto` is a convenience link for a human to send it from their own
|
||||
mail program: the subject "Invoice INV-0007 from {company}", the body
|
||||
URL-encoded and cut to 1,800 bytes at the last whole word. The books
|
||||
never send anything (A-4): a mailto link carries no attachment, so say
|
||||
which file to attach.
|
||||
- `warnings` names a revenue account it created with no tax category
|
||||
(rules.md B-32); `notes` one per finished or abandoned project a line
|
||||
names, and on confirm one per on-spec project (is there now an
|
||||
agreement?).
|
||||
- `books.create_bill { vendor | party_id, date, received_on?, vendor_ref?,
|
||||
terms? | due_date?, lines: [{ description, amount, account, class? }],
|
||||
memo?, upload_id | existing_attachment_id, duplicate_reason?,
|
||||
confirmed? }` → without `confirmed`: `{ preview: true, total, due_date,
|
||||
terms?, lines: [...], discount_by?, max_discount? }` and nothing is
|
||||
written; with `confirmed: true`: `{ id, total, due_date, terms?,
|
||||
entry_id, lines?, discount_by?, max_discount?, file: { attachment_id,
|
||||
sha256 }, warnings?, notes? }`.
|
||||
Confirmed it posts Dr each line's account / Cr `2000` Accounts Payable,
|
||||
the payable line tagged with the bill and one debit line per (account,
|
||||
project) part; `vendor` is a vendor id, its party's id or its name, and
|
||||
`party_id` the old spelling (a vendor id).
|
||||
- `vendor_ref` is the vendor's own invoice number: required in books
|
||||
that have declared `documents_v2` (B-40). The same vendor and number
|
||||
twice is refused unless `duplicate_reason` says why, which is
|
||||
journaled with the bill; case, spaces and hyphens are ignored.
|
||||
`received_on` is the day it was received, when it differs from its
|
||||
date.
|
||||
- In books that have declared `documents_v2` every line names its
|
||||
`account` (A-10: an expense account, or an asset that is not cash and
|
||||
holds no role: equipment, prepaid), and the vendor's invoice is
|
||||
required: `upload_id` (books.begin_upload and `wanderbooks upload`,
|
||||
D-46) or `existing_attachment_id` (bytes stored already,
|
||||
books.attachments). It is filed on the bill's entry as its evidence,
|
||||
linked to the bill, and returned as `file` with its `sha256`. In books
|
||||
that haven't declared the feature, lines without `account` post to
|
||||
`6999` as before and the file is optional.
|
||||
- The due date and terms fall back as an invoice's, to the vendor's
|
||||
`default_terms_as_vendor` and no further. `discount_by` is the last
|
||||
day an early-payment discount is open and `max_discount` the largest
|
||||
it allows (B-39): the caller gives the exact cents when it takes one.
|
||||
- `warnings` names an expense account it created with no tax category;
|
||||
`notes` one per finished or abandoned project a line names (none for
|
||||
on-spec: a cost of on-spec work is an expense like any other).
|
||||
- `books.receive_payment { document_id, date, amount, cash_account,
|
||||
instrument?, reference? }` → `{ entry_id, remaining, status }`. Posts
|
||||
Dr Cash / Cr A/R, the receivable line tagged with the invoice (B-38),
|
||||
|
||||
+18
-2
@@ -34,8 +34,8 @@ No other statuses.
|
||||
| Bank reconciliation (cleared flags + report) | Reconcile | done | — |
|
||||
| QuickBooks CSV import (bank txns, chart of accounts) | Banking → Upload | done | R-FUTURE-4 |
|
||||
| QuickBooks IIF + XLSX export | Export / reports | done | R-FUTURE-4 |
|
||||
| A/R invoices + payments + aging | Business → Invoices | done | R-FUTURE-5 |
|
||||
| A/P bills + payments + aging | Business → Bills | done | R-FUTURE-5 |
|
||||
| A/R invoices + payments + aging | Business → Invoices | done | R-FUTURE-5; numbers, terms, line accounts, the party snapshots and the issued PDF filed with the invoice, through `books.create_invoice`'s preview and confirm (P4b S4.4) |
|
||||
| A/P bills + payments + aging | Business → Bills | done | R-FUTURE-5; the vendor's own invoice number (a duplicate refused), terms, line accounts and the vendor's invoice filed as the bill's evidence, through `books.create_bill`'s preview and confirm (P4b S4.4) |
|
||||
| Payroll runs (accounting only, no tax tables) + tax remittance | Payroll | done | — |
|
||||
| Tracking classes (divisional P&L) | Class tracking | done | invoice and bill lines take a class too (projects, rules.md B-37 item 8; QBO/Xero class or tracking per line) |
|
||||
| CPA handoff package (TB + GL xlsx, P&L + BS pdf, zip) | Reports → export | done | — |
|
||||
@@ -92,6 +92,22 @@ and the rule index from B-1 to B-45 (`bookspec/spec/rules-index.md`). Every
|
||||
existing bookspec example still runs on every `cargo test`, through both
|
||||
the loader's own checks and the `Engine` trait.
|
||||
|
||||
### P4b S4.4: the create tools preview and confirm (2026-10-27)
|
||||
|
||||
The A/R and A/P rows above gain their note rather than a new row: no
|
||||
parity item changed status, so the floor stands. `books.create_invoice`
|
||||
and `books.create_bill` now preview without `confirmed` (the posting, the
|
||||
number and due date it will take, the DRAFT or the discount, and nothing
|
||||
written) and post with it (A-1), which is what the workflow reference
|
||||
does before a document leaves the company. A bill in books that have
|
||||
declared `documents_v2` names each line's account and keeps the vendor's
|
||||
invoice (A-10, A-13); an invoice keeps the copy it issued (A-7, B-40) and
|
||||
a `mailto` link for a human to send it, since the books never send (A-4).
|
||||
`books.update_party` takes the party's email and terms per side (A-13) and
|
||||
the owner's full contact block (D-55: phone, contact person, billing and
|
||||
shipping addresses, billing email), which an invoice's bill-to copies at
|
||||
issue.
|
||||
|
||||
### P9a self-host deployment (2026-10-26)
|
||||
|
||||
P9a is a deployment, not a semantic feature: the same engine, store, MCP
|
||||
|
||||
+4
-2
@@ -1740,8 +1740,10 @@ that declared `projects` before it existed wait for their owner's next
|
||||
upgrade before a document's line names a project. `documents_v2` (plan
|
||||
P4b S4, one feature for S4 to S7: D-52) is the next: a `settings.set` of
|
||||
the `invoicing.*`, `bills.*`, `company.email` or `company.website` keys,
|
||||
which an earlier version refuses (S4.2); a `party.updated` with an email
|
||||
or a default terms fact (S4.2); an `invoice.created` or `bill.created`
|
||||
which an earlier version refuses (S4.2); a `party.updated` with an email,
|
||||
a default terms fact (S4.2), or a contact-block fact — a phone, a contact
|
||||
person, a billing or shipping address, a billing email (D-55); an
|
||||
`invoice.created` or `bill.created`
|
||||
carrying a number, terms, a bill-to or seller snapshot, a customer
|
||||
reference or memo, a vendor reference, a receipt date or a duplicate's
|
||||
reason, or a line naming its account, quantity or unit price (S4.3); and
|
||||
|
||||
Reference in new issue
Block a user