fix: period close sweeps only through the period end; partnerships close into members' capital
The close zeroed every revenue and expense account's all-time movement, so closing a period after later entries were posted swept those too: close January in March and February's and March's income landed in January's closing entry. The preview had the same bug and never showed where the net went. Now: - close_period_to(id, to) zeroes balances as of the period end; entries dated later stay open for their own period. Periods must close in order (LedgerError::CloseOutOfOrder): a later close has already swept the earlier period. - CloseTo::Members: for a partnership (multi-member LLC) the net goes to each member's capital account by share, to the cent, the last member absorbing rounding, which is what K-1 capital accounts track. CloseTo::RetainedEarnings (3999) stays the default; close_period is shorthand for it. books.close_period picks Members when wanderbooks.toml lists members. - The journal records the close target; replay re-derives the closing entry and now compares every line with the journal, not just the id, so a change to closing math can never silently rewrite history. - The tool's preview is the real close run on a clone of the ledger (Ledger now derives Clone), so preview and post can't disagree. - Members' equity: closing entries into a capital account are income, not contributions. Balance sheet for a partnership: capital accounts (with allocated earnings), then only unallocated earnings — "Undistributed earnings, prior years" if any, and this year's unclosed net income (loss). periods::allocate is shared by the close and the statement. Tests: January closed after February is posted sweeps exactly Dr 4000 2,500 / Cr 6100 800 / Cr 3999 1,700 (mutation-checked: the old code gives 3,500/120/800/2,580); out-of-order close refused; a partnership close splits 666.67 into 333.34/333.33 with no 3999, and the members' equity statement reads correctly before and after; store replay of a members close (reopen, rebuild, verify); partnership and corporate balance sheet layouts; stdio close preview equals the posted lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
15 files changed
+713
-217
No files matched your search
@@ -1022,4 +1022,37 @@ fn mcp_period_statements_over_stdio() {
|
||||
json!({"format": "pdf", "report": "members_equity", "end": "2026-02-28"}),
|
||||
);
|
||||
assert_eq!(pdf["content_encoding"], "base64");
|
||||
|
||||
// Closing January: a one-member LLC closes into the member's capital
|
||||
// account, not Retained Earnings. The preview is exactly what posts.
|
||||
let jan = mcp.call(
|
||||
"books.create_period",
|
||||
json!({"name": "2026-01", "start": "2026-01-01", "end": "2026-01-31"}),
|
||||
);
|
||||
let jan_id = jan["period_id"].as_u64().unwrap();
|
||||
let preview = mcp.call(
|
||||
"books.close_period",
|
||||
json!({"period_id": jan_id, "confirmed": false}),
|
||||
);
|
||||
assert_eq!(
|
||||
preview["lines"],
|
||||
json!(["Debit 4000 4.00", "Credit 3000 4.00"])
|
||||
);
|
||||
let closed = mcp.call(
|
||||
"books.close_period",
|
||||
json!({"period_id": jan_id, "confirmed": true}),
|
||||
);
|
||||
assert_eq!(closed["lines"], preview["lines"]);
|
||||
let jan_bs = mcp.call("books.balance_sheet", json!({"as_of": "2026-01-31"}));
|
||||
assert_eq!(jan_bs["equity_accounts"][0]["amount"], "1004.00");
|
||||
assert_eq!(jan_bs["current_earnings"], "0.00");
|
||||
// The close is income, not a contribution.
|
||||
let me = mcp.call(
|
||||
"books.export",
|
||||
json!({"format": "json", "report": "members_equity", "start": "2026-01-01", "end": "2026-01-31"}),
|
||||
);
|
||||
let me: serde_json::Value = serde_json::from_str(me["content"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(me["total"]["contributions"], "1000.00");
|
||||
assert_eq!(me["total"]["net_income"], "4.00");
|
||||
assert_eq!(me["total"]["ending"], "1004.00");
|
||||
}
|
||||
Reference in new issue
Block a user