diff --git a/README.md b/README.md index bcb4d65..5f3537c 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ operating = "0.00" ``` Events count from 1: the first `[[events]]` is event 1. An event is an -entry (`lines`), a reversal (`reverses`), a close (`close`, below) or a -bill (`bill`, below), each posting one entry; or one of the books' -commands that post what is due (`run_depreciation`, `dispose_asset`, +entry (`lines`), a reversal (`reverses`), a close (`close`, below), a +bill or an invoice (`bill`, `invoice`, below), each posting one entry; +or one of the books' commands that post what is due (`run_depreciation`, `dispose_asset`, `run_schedules`, below), which post as many entries as are due, none included; or a schedule's registration (`register_schedule`), which posts none: exactly one of them. A key that names an event (`reverses = @@ -77,10 +77,11 @@ posts one entry, event N's is entry #N. A change that posts no entry (a rename, a policy) is a `[[changes]]` entry instead (below). A key the shape doesn't have is refused too, never ignored. -An entry, a reversal, a close, a bill and a disposal are dated (`date`); -the other commands date their own entries (a run its month's end) and -take no date. An entry, a reversal, a close and a bill are described -(`description`); a command describes its own entries and takes none. +An entry, a reversal, a close, a bill, an invoice and a disposal are +dated (`date`); the other commands date their own entries (a run its +month's end) and take no date. An entry, a reversal, a close, a bill and +an invoice are described (`description`); a command describes its own +entries and takes none. The books are the engine's own (`ledger_store::Store`, in a directory of their own): each event is the command the books take it by, journaled as @@ -238,7 +239,8 @@ the side gives the direction. original's tags. - `payee` on an event: the payee (by name, one of `setup.payees`) the entry paid. Only on an entry (`lines`): a reversal takes its - original's, a bill names its vendor, a close and a command have none. + original's, a bill names its vendor, an invoice its customer, a close + and a command have none. - `parties = [{ party, amount? }, { party, percent? }, ...]` on an event: the parties (by name, in `setup.parties`) the entry is with, each with its share: an `amount` (an exact decimal string), a `percent`, or @@ -258,12 +260,24 @@ the side gives the direction. splits that close's net on its own instead (books from an engine that rounded each close by itself). A close must have something to close: it posts one entry like any other event. -- `bill = { vendor, amount }` as an event: a bill from `vendor` (a name; - a payee's record if one has it) for `amount`, dated and due on the - event's date. It posts one entry: General Expenses (6999) debited and - Accounts Payable (2000) credited, each account created with that fixed - name when first needed, as the books create a control role's account - (B-32). +- `bill = { vendor, amount }` or `bill = { vendor, lines = [{ amount, + description?, project? }, ...] }` as an event: a bill from `vendor` (a + name; a payee's record if one has it) for `amount` (one line, described + as the event) or its `lines` (exactly one of them; a line's description + is the event's when left out), dated and due on the event's date. It + posts one entry: General Expenses (6999) debited and Accounts Payable + (2000) credited, each account created with that fixed name when first + needed, as the books create a control role's account (B-32). A line's + `project` (a `setup.projects` name) puts its cost on that project: the + entry's expense side is one line per project named, the untagged lines' + as one, the payable line never tagged (B-37 item 8). +- `invoice = { customer, amount }` or `invoice = { customer, lines = [...] + }` as an event: an invoice to `customer` (a name, its record added when + first named), as a bill is (the same `lines`), dated and due on the + event's date. It posts one entry: Accounts Receivable (1200) debited and + Revenue (4000) credited, each created with that fixed name when first + needed; a line's `project` puts its revenue on that project, the + receivable line never tagged (B-37 item 8). - `run_depreciation = { asset, period }` as an event: the asset's run for `period` (`"YYYY-MM"`), which posts what the schedule says is due through the month's end and the books don't carry, dated then: a diff --git a/examples/projects-invoice-and-bill-lines-count-by-project.toml b/examples/projects-invoice-and-bill-lines-count-by-project.toml new file mode 100644 index 0000000..2cc9b85 --- /dev/null +++ b/examples/projects-invoice-and-bill-lines-count-by-project.toml @@ -0,0 +1,182 @@ +# An invoice's or bill's lines count toward the projects they name. +# +# wanderbooks: rules.md B-37 item 8 (journal feature +# `document_line_projects`); regression tests in ledger-core +# `tests/spec_document_projects.rs`, ledger-store +# `tests/document_projects.rs`, ledger-mcp `tests/spec_projects.rs` +# (`invoice_and_bill_lines_count_toward_their_projects`). +# +# Expected values worked by hand. Invoice 1 bills 3,000.00 + 500.00 of +# Zeta's design (3,500.00), 200.00 of Harbor's hosting and an untagged +# 100.00 rush fee: 3,800.00, all Revenue (4000), the receivable untagged. +# Bill 1 is 1,200.00 of Zeta's illustration and 50.00 of untagged +# postage: 1,250.00 of General Expenses (6999). Bill 2, 300.00, names no +# project. The customer's payment and the bill's payment move cash and +# the receivable or payable only: no project figure. So Zeta nets +# 3,500.00 - 1,200.00 = 2,300.00; Harbor 200.00 (both are client work, +# so neither has an unrecovered cost: on spec, a hoped-for payment is no +# receivable, B-37 item 4); the untagged lines 100.00 - 50.00 - 300.00 +# - 1,500.00 (rent) = -1,750.00; together 750.00, the P&L's net +# (3,800.00 - 1,550.00 - 1,500.00). Dana was paid 1,250.00 from the bank, +# whatever the bill's projects; the second bill is still owed. + +id = "projects-invoice-and-bill-lines-count-by-project" +title = "An invoice's or bill's lines count toward the projects they name" +rule = "Each line of an invoice or bill MAY name a project; its revenue (an invoice's) or cost (a bill's) MUST then count toward that project's figures exactly as a tagged line of a hand-built entry would, the receivable, the payable and their payments MUST count toward none, and every figure that is not a project's (the P&L, a payee's 1099 payments) MUST be what it would be with no project named." +status = "CONVENTION" +sources = [ + "Job-order costing: a job cost sheet accumulates the revenue and the direct costs identified with one job, a supplier's invoice and the job's billing among them (management-accounting convention; wanderbooks docs/spec/rules.md B-37 items 3 and 8)", + "wanderbooks docs/spec/rules.md B-11 and B-14: a period's net income is its revenue less its expenses", + "Treas. Reg. ยง1.6041-1(a)(1)(iv) (docs/spec/sources/irs-1099-payment-method.md): a 1099 counts payments made, not how the books classify them", +] +tags = ["projects", "classes", "job-costing", "accrual", "invoices", "bills"] + +[setup] +currency = "USD" +fiscal_year_start = "01-01" +basis = "accrual" +rounding = "half-even" +cash_accounts = ["1000"] + +[[setup.accounts]] +code = "1000" +name = "Bank" +type = "asset" +[[setup.accounts]] +code = "1200" +name = "Accounts Receivable" +type = "asset" +[[setup.accounts]] +code = "2000" +name = "Accounts Payable" +type = "liability" +[[setup.accounts]] +code = "4000" +name = "Revenue" +type = "revenue" +[[setup.accounts]] +code = "6200" +name = "Rent" +type = "expense" +[[setup.accounts]] +code = "6999" +name = "General Expenses" +type = "expense" + +[[setup.projects]] +name = "Zeta" +kind = "client" +[[setup.projects]] +name = "Harbor" +kind = "client" + +[[setup.payees]] +name = "Dana Illustration" +form = "1099-NEC" + +[[events]] +date = "2026-03-02" +description = "Invoice to Cedar" +invoice = { customer = "Cedar Client", lines = [ + { description = "Design", amount = "3000.00", project = "Zeta" }, + { description = "Hosting", amount = "200.00", project = "Harbor" }, + { description = "Rush fee", amount = "100.00" }, + { description = "Revisions", amount = "500.00", project = "Zeta" }, +] } + +[[events]] +date = "2026-03-09" +description = "Dana's bill" +bill = { vendor = "Dana Illustration", lines = [ + { description = "Illustration", amount = "1200.00", project = "Zeta" }, + { description = "Postage", amount = "50.00" }, +] } + +[[events]] +date = "2026-03-20" +description = "Cedar pays invoice 1" +lines = [ + { account = "1000", side = "debit", amount = "3800.00" }, + { account = "1200", side = "credit", amount = "3800.00" }, +] + +[[events]] +date = "2026-03-30" +description = "Pay Dana's bill" +payee = "Dana Illustration" +lines = [ + { account = "2000", side = "debit", amount = "1250.00" }, + { account = "1000", side = "credit", amount = "1250.00" }, +] + +[[events]] +date = "2026-03-31" +description = "Studio rent" +lines = [ + { account = "6200", side = "debit", amount = "1500.00" }, + { account = "1000", side = "credit", amount = "1500.00" }, +] + +[[events]] +date = "2026-04-02" +description = "Dana's second bill" +bill = { vendor = "Dana Illustration", amount = "300.00" } + +[[expect]] +query = "entry" +entry = 1 +[expect.values] +"1200.debit" = "3800.00" +"4000.credit" = "3800.00" + +[[expect]] +query = "entry" +entry = 2 +[expect.values] +"6999.debit" = "1250.00" +"2000.credit" = "1250.00" + +[[expect]] +query = "project" +project = "Zeta" +from = "2026-01-01" +to = "2026-12-31" +[expect.values] +revenue = "3500.00" +expenses = "1200.00" +net = "2300.00" +"to_date.revenue" = "3500.00" +"to_date.expenses" = "1200.00" +"to_date.net" = "2300.00" +unrecovered_cost = "none" + +[[expect]] +query = "project" +project = "Harbor" +from = "2026-01-01" +to = "2026-12-31" +[expect.values] +revenue = "200.00" +expenses = "0.00" +net = "200.00" +"to_date.revenue" = "200.00" +"to_date.expenses" = "0.00" +"to_date.net" = "200.00" +unrecovered_cost = "none" + +[[expect]] +query = "profit_and_loss" +from = "2026-01-01" +to = "2026-12-31" +[expect.values] +"4000" = "3800.00" +"6200" = "1500.00" +"6999" = "1550.00" +net_income = "750.00" + +[[expect]] +query = "vendor_1099_paid" +from = "2026-01-01" +to = "2026-12-31" +[expect.values] +"Dana Illustration" = "1250.00"