docs(handbook): the invoices-and-bills procedure, whole (P4b S4.5)
Step 1 now covers the issue: preview first (nothing written, the DRAFT PDF,
an abandoned preview uses no number), confirm only after the human has
seen it, the filed issued copy that a later look never replaces,
quantities and prices, the PO, and the snapshot of the customer at issue.
Step 6 is the CPA's list: agings tied to 1200 and 2000, sequential numbers
with voids visible, bills on their real accounts with the vendor's number
and no duplicate, card payments identified for 1099-NEC, deposits as
liabilities, light governance by decision.
A {documents_v2} placeholder (handbook::section_with, the server's
documents_v2 feature test) says "your books have not declared documents_v2
yet" before the owner's upgrade and what is on after it; no other section
changes. Parity, testing (with the For-P12 paragraph) and mcp-interface
notes for S4.5.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
df93d8b951
commit
caad35d75b
7 files changed
+224
-15
No files matched your search
@@ -7,12 +7,34 @@ own figures are read from the ledger lines tagged with it, never kept on
|
||||
the document: a reversed payment, a void or a write-off comes out right
|
||||
without another copy to correct (B-38).
|
||||
|
||||
## 1. Send an invoice
|
||||
{documents_v2}
|
||||
|
||||
- The books never send mail, and never say they did. To send an invoice,
|
||||
create and post it (`books.create_invoice`), hand its PDF to the human
|
||||
(`books.document` with `pdf: true`), and let the owner or a delegate
|
||||
send it.
|
||||
## 1. Issue and send an invoice
|
||||
|
||||
- Preview first: `books.create_invoice` without `confirmed` writes
|
||||
nothing. It returns the posting, the number and due date the invoice
|
||||
will take, and its DRAFT PDF; show the human the PDF and the posting.
|
||||
An abandoned preview uses up no number.
|
||||
- Confirm with `confirmed: true` only after the human has seen it. The
|
||||
invoice posts Dr Accounts Receivable / Cr each line's account, takes the
|
||||
next number (B-40), and the issued PDF, the page the customer gets, is
|
||||
filed with it. That filed copy never changes: a later look at the
|
||||
invoice (`books.document` with `pdf: true`, or the viewer's Save invoice
|
||||
PDF) is a re-created copy as of that day, says so, and shows what has
|
||||
been paid since.
|
||||
- Give a line a quantity and a unit price when the work is billed that way
|
||||
(`quantity` times `unit_price` must equal the line's amount exactly); the
|
||||
page prints "3 × $33.33" under its description. The customer's purchase
|
||||
order goes in `customer_ref`; `memo` prints under the lines. The
|
||||
company's email, website, payment methods (each can show a QR code) and
|
||||
footer come from the settings, not from the call.
|
||||
- The page prints the customer as it was when the invoice was issued (its
|
||||
name, billing address, contact, phone and billing email, and a Ship-to
|
||||
line when it has a shipping address); changing the customer's record
|
||||
later never changes an issued invoice.
|
||||
- The books never send mail, and never say they did. Hand the issued PDF
|
||||
to the human (`books.document` with `pdf: true`; the viewer's Save
|
||||
issued copy), and let the owner or a delegate send it.
|
||||
- Then record the send with `books.mark_sent`: the invoice (`invoice:7`),
|
||||
the day it went out, who sent it (`by`: the owner, or the agent acting
|
||||
for them), how (`via`: `email`, or `mail`, `portal`, `hand`, or another
|
||||
@@ -138,11 +160,26 @@ if it was reversed, that it no longer stands).
|
||||
`deposit_to: "2300"`, dated on or after the bounce, against the
|
||||
invoices it pays. The 2300 balance returns to zero.
|
||||
|
||||
## What the CPA will check
|
||||
## 6. What the CPA will check
|
||||
|
||||
The agings tie to 1200 and 2000 at month and year end (they do by
|
||||
construction: the control check says so). Every write-off carries its
|
||||
reason and its evidence of collection efforts. Write-offs are not used in
|
||||
- The agings tie to 1200 and 2000 at month and year end (they do by
|
||||
construction: `books.control_check` says so; run it at month end and
|
||||
tie the aging to the trial balance before the CPA asks).
|
||||
- Invoice numbers are unique and sequential, voids stay visible with
|
||||
their number, nothing is deleted.
|
||||
- Every bill is coded to its real account (the Form 1065 lines, B-32),
|
||||
never a catch-all, with the vendor's invoice attached and its number
|
||||
recorded, and no bill is paid twice (the same vendor and number twice is
|
||||
refused).
|
||||
- Payments are matched to documents, and card payments are identified
|
||||
(excluded from 1099-NEC: Treas. Reg. §1.6041-1(a)(1)(iv)).
|
||||
- Customer deposits are liabilities (2300), never netted inside
|
||||
receivables; Schedule L lines 2a and 15 come straight from the control
|
||||
accounts.
|
||||
- Approvals and three-way matching are deliberately not adopted for a
|
||||
two-member LLC: light governance, by decision.
|
||||
- Every write-off carries its reason and its evidence of collection
|
||||
efforts. Write-offs are not used in
|
||||
place of a credit memo for a valid invoice, and never to hide a mistake
|
||||
that a void would show. Whether a debt is worthless, and in which year, is
|
||||
a tax judgment that stays with the CPA: the books flag each write-off made
|
||||
|
||||
@@ -132,11 +132,25 @@ pub const ALL: &str = "all";
|
||||
/// The section `id` (or [`ALL`]), rendered for these settings, the
|
||||
/// books' base currency, and whether they declared the journal feature
|
||||
/// `document_line_projects` (None: no such section).
|
||||
#[cfg(test)]
|
||||
pub fn section(
|
||||
id: &str,
|
||||
settings: &Settings,
|
||||
currency: &str,
|
||||
document_line_projects: bool,
|
||||
) -> Option<String> {
|
||||
section_with(id, settings, currency, document_line_projects, true)
|
||||
}
|
||||
|
||||
/// [`section`], also saying whether the books declared the journal
|
||||
/// feature `documents_v2` (the invoices-and-bills section says what
|
||||
/// waits for the owner's upgrade by it).
|
||||
pub fn section_with(
|
||||
id: &str,
|
||||
settings: &Settings,
|
||||
currency: &str,
|
||||
document_line_projects: bool,
|
||||
documents_v2: bool,
|
||||
) -> Option<String> {
|
||||
let render = |md: &str| {
|
||||
md.replace("{evidence_rule}", &evidence_rule(settings, currency))
|
||||
@@ -145,6 +159,7 @@ pub fn section(
|
||||
"{document_projects}",
|
||||
document_projects_rule(document_line_projects),
|
||||
)
|
||||
.replace("{documents_v2}", documents_v2_rule(documents_v2))
|
||||
};
|
||||
if id == ALL {
|
||||
let parts: Vec<String> = SECTIONS.iter().map(|(_, md)| render(md)).collect();
|
||||
@@ -178,6 +193,17 @@ pub fn document_projects_rule(declared: bool) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the books have of the document upgrade (journal feature
|
||||
/// `documents_v2`, plan P4b S4), as the invoices-and-bills section says
|
||||
/// it: on, or waiting for the owner's go-ahead.
|
||||
pub fn documents_v2_rule(declared: bool) -> &'static str {
|
||||
if declared {
|
||||
"These books have declared documents_v2: an invoice takes a number, terms and a purchase-order reference, a bill takes the vendor's number, and every bill line names its account."
|
||||
} else {
|
||||
"Your books have not declared documents_v2 yet: numbers, terms and line accounts wait for the owner's upgrade (books.start's format_upgrade). Until then an invoice is \"Invoice {id}\", a bill's lines post to 6999, and a document carrying a new field is refused with format_upgrade_pending; the issued PDF is still filed with the invoice."
|
||||
}
|
||||
}
|
||||
|
||||
/// The receipt rule in force (INVARIANT 2).
|
||||
pub fn evidence_rule(settings: &Settings, currency: &str) -> String {
|
||||
format!(
|
||||
@@ -487,6 +513,7 @@ mod tests {
|
||||
assert!(!text.contains("{evidence_rule}"), "{id}");
|
||||
assert!(!text.contains("{period_rule}"), "{id}");
|
||||
assert!(!text.contains("{document_projects}"), "{id}");
|
||||
assert!(!text.contains("{documents_v2}"), "{id}");
|
||||
}
|
||||
// The default period, in the descriptions' own words (B-30).
|
||||
assert!(
|
||||
@@ -502,6 +529,51 @@ mod tests {
|
||||
assert!(section("nope", &s, "USD", true).is_none());
|
||||
}
|
||||
|
||||
/// The invoices-and-bills section says what waits for the owner's
|
||||
/// upgrade (`documents_v2`) before the go-ahead and what is on after;
|
||||
/// the procedure around it is the same text, and no other section
|
||||
/// changes (plan P4b S4.5).
|
||||
#[test]
|
||||
fn documents_v2_follows_the_books_feature() {
|
||||
let s = settings("Co", Decimal::ZERO);
|
||||
let with = section_with("invoices-and-bills", &s, "USD", true, true).unwrap();
|
||||
let without = section_with("invoices-and-bills", &s, "USD", true, false).unwrap();
|
||||
assert!(without.contains(
|
||||
"Your books have not declared documents_v2 yet: numbers, terms and line accounts wait for the owner's upgrade"
|
||||
));
|
||||
assert!(!with.contains("have not declared documents_v2"));
|
||||
assert!(with.contains("These books have declared documents_v2"));
|
||||
// Every step of the procedure is there either way, in order.
|
||||
for text in [&with, &without] {
|
||||
let at = |needle: &str| {
|
||||
text.find(needle)
|
||||
.unwrap_or_else(|| panic!("{needle:?} missing from:\n{text}"))
|
||||
};
|
||||
let steps = [
|
||||
"## 1. Issue and send an invoice",
|
||||
"## 2. Record a customer payment",
|
||||
"## 3. Pay a vendor bill",
|
||||
"## 4. Void, write off, or reverse",
|
||||
"## 5. A processor's fee",
|
||||
"## 6. What the CPA will check",
|
||||
];
|
||||
let places: Vec<usize> = steps.iter().map(|h| at(h)).collect();
|
||||
assert!(places.windows(2).all(|w| w[0] < w[1]), "{places:?}");
|
||||
assert!(text.contains("books.mark_sent") && text.contains("books.control_check"));
|
||||
assert!(text.contains("An abandoned preview uses up no number"));
|
||||
}
|
||||
for (id, _) in SECTIONS
|
||||
.iter()
|
||||
.filter(|(id, _)| *id != "invoices-and-bills")
|
||||
{
|
||||
assert_eq!(
|
||||
section_with(id, &s, "USD", true, true),
|
||||
section_with(id, &s, "USD", true, false),
|
||||
"{id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// INVARIANT 6: the projects section tells books that declared
|
||||
/// document_line_projects to tag an invoice's or bill's lines, and the
|
||||
/// rest that it waits for their format upgrade; no other section
|
||||
|
||||
@@ -601,6 +601,15 @@ impl BooksServer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the books declared the journal feature `documents_v2` (the
|
||||
/// handbook's invoices-and-bills section says what waits for it).
|
||||
fn documents_v2(&self) -> bool {
|
||||
self.lock().is_ok_and(|s| {
|
||||
s.journal_format()
|
||||
.has_feature(ledger_store::format::DOCUMENTS_V2)
|
||||
})
|
||||
}
|
||||
|
||||
/// The books' base currency code, e.g. "USD".
|
||||
fn currency(&self) -> &str {
|
||||
self.store.currency.code()
|
||||
@@ -1519,11 +1528,12 @@ impl BooksServer {
|
||||
}
|
||||
_ if path.starts_with(&format!("{}/", handbook::URI)) => {
|
||||
let id = &path[handbook::URI.len() + 1..];
|
||||
handbook::section(
|
||||
handbook::section_with(
|
||||
id,
|
||||
&self.settings(),
|
||||
self.currency(),
|
||||
self.document_line_projects(),
|
||||
self.documents_v2(),
|
||||
)
|
||||
.map(ResourceBody::Text)
|
||||
.ok_or_else(|| format!("unknown handbook section: {id}"))
|
||||
|
||||
@@ -18,11 +18,13 @@ impl BooksServer {
|
||||
) -> Result<Json<HandbookOut>, String> {
|
||||
let settings = self.settings();
|
||||
let declared = self.document_line_projects();
|
||||
let documents_v2 = self.documents_v2();
|
||||
let sections = handbook::SECTIONS
|
||||
.iter()
|
||||
.map(|(id, _)| {
|
||||
let text =
|
||||
handbook::section(id, &settings, self.currency(), declared).unwrap_or_default();
|
||||
handbook::section_with(id, &settings, self.currency(), declared, documents_v2)
|
||||
.unwrap_or_default();
|
||||
HandbookSectionOut {
|
||||
id: id.to_string(),
|
||||
title: handbook::title(&text).to_string(),
|
||||
@@ -34,7 +36,7 @@ impl BooksServer {
|
||||
let section = match params.0.section.as_deref().map(str::trim) {
|
||||
None => None,
|
||||
Some(id) => {
|
||||
let text = handbook::section(id, &settings, self.currency(), declared).ok_or_else(|| {
|
||||
let text = handbook::section_with(id, &settings, self.currency(), declared, documents_v2).ok_or_else(|| {
|
||||
let ids: Vec<&str> = handbook::SECTIONS.iter().map(|(id, _)| *id).collect();
|
||||
format!(
|
||||
"no handbook section {id:?}: the sections are {}, or \"{}\" for all of them",
|
||||
|
||||
Reference in new issue
Block a user