Three reports a CPA expects that the engine could not produce: - general_ledger(start, end): the canonical general ledger, organized by account in code order. Each account has its beginning balance, every posting in the period (date, entry, description, debit, credit, running balance on the normal side) and its ending balance, which ties to the trial balance. Each posting names its split: the one account on the opposite side of its entry, or "-Split-" when that side has several, so a compound entry (one debit, several credits) reads correctly from every account it touches. The entry-ordered listing the exports called a "general ledger" is really the general journal. - trial_balance_as_of(as_of): a trial balance is always as of a date. - members_equity(start, end, members): the statement of changes in members' equity for a multi-member LLC taxed as a partnership. Each member's capital roll-forward: beginning (capital plus their share of undistributed earnings), contributions, allocated net income (loss), distributions, ending. Allocation is by ownership share to the cent, the last member absorbing rounding, so totals are exact and the ending total equals balance-sheet equity; any equity change the roll-forward can't explain shows as "other" instead of disappearing. Shares must be positive and sum to exactly 1; capital accounts must be distinct equity accounts (LedgerError::InvalidMembers). - CashFlowLine.account: the account a line is about, so renderers can name members instead of account codes. Tests (every expectation computed by hand): trial balance as of Jan 31; February general ledger with beginning balances, running balances, a card that nets to zero, an inactive account still listed, and every ending balance tied to the trial balance; a compound entry's splits from all three sides; a two-member partnership where 666.67 and 100.01 split unevenly (333.34/333.33, 50.01/50.00), with and without closing January, tying to the balance sheet each month; bad member configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
536 lines
19 KiB
Rust
536 lines
19 KiB
Rust
//! Period-scoped financial statements: profit and loss for a date range and
|
||
//! the balance sheet as of a date, line per account.
|
||
//!
|
||
//! Every expected number below is computed by hand from the entries, not
|
||
//! read off the engine. The fixture is two months of a small company:
|
||
//!
|
||
//! | date | entry | Dr | Cr |
|
||
//! |--------|-------------------------------|---------------|---------------|
|
||
//! | Jan 2 | owner contributes 10,000 | 1000 Cash | 3000 Capital |
|
||
//! | Jan 15 | client pays 2,500 | 1000 Cash | 4000 Revenue |
|
||
//! | Jan 20 | rent 800 | 6100 Rent | 1000 Cash |
|
||
//! | Feb 3 | software 120 on the card | 6000 Software | 2000 Card |
|
||
//! | Feb 10 | client pays 1,000 | 1000 Cash | 4000 Revenue |
|
||
//! | Feb 25 | pay the card 120 | 2000 Card | 1000 Cash |
|
||
|
||
use chrono::NaiveDate;
|
||
use ledger_core::reports::{
|
||
balance_sheet, balance_sheet_as_of, profit_and_loss, profit_and_loss_for_period,
|
||
};
|
||
use ledger_core::{
|
||
AccountId, AccountType, CashFlowParams, ClassId, Currency, Ledger, LedgerError, MemberCapital,
|
||
Money, Side, StatementLine, cash_flow, general_ledger, members_equity, trial_balance_as_of,
|
||
};
|
||
use rust_decimal::Decimal;
|
||
use std::str::FromStr;
|
||
|
||
fn d(m: u32, day: u32) -> NaiveDate {
|
||
NaiveDate::from_ymd_opt(2026, m, day).unwrap()
|
||
}
|
||
|
||
fn usd(n: i64) -> Money {
|
||
Money::new(Decimal::from(n), Currency::USD)
|
||
}
|
||
|
||
fn dec(n: i64) -> Decimal {
|
||
Decimal::from(n)
|
||
}
|
||
|
||
struct Books {
|
||
ledger: Ledger,
|
||
cash: AccountId,
|
||
software_class: ClassId,
|
||
}
|
||
|
||
fn post(ledger: &mut Ledger, date: NaiveDate, desc: &str, dr: AccountId, cr: AccountId, n: i64) {
|
||
let e = ledger.create_entry(date, desc);
|
||
ledger.add_line(e, dr, Side::Debit, usd(n)).unwrap();
|
||
ledger.add_line(e, cr, Side::Credit, usd(n)).unwrap();
|
||
ledger.post_entry(e).unwrap();
|
||
}
|
||
|
||
/// The fixture. With `close_january`, January is closed before any February
|
||
/// entry exists (the close sweeps every revenue and expense balance).
|
||
fn books(close_january: bool) -> Books {
|
||
let mut l = Ledger::new(Currency::USD);
|
||
let cash = l
|
||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||
.unwrap();
|
||
let card = l
|
||
.add_account("2000", "Card", AccountType::Liability, None, false)
|
||
.unwrap();
|
||
let capital = l
|
||
.add_account("3000", "Capital", AccountType::Equity, None, false)
|
||
.unwrap();
|
||
let revenue = l
|
||
.add_account("4000", "Revenue", AccountType::Revenue, None, false)
|
||
.unwrap();
|
||
let software = l
|
||
.add_account("6000", "Software", AccountType::Expense, None, false)
|
||
.unwrap();
|
||
let rent = l
|
||
.add_account("6100", "Rent", AccountType::Expense, None, false)
|
||
.unwrap();
|
||
let software_class = l.add_class("Apps").unwrap();
|
||
|
||
post(&mut l, d(1, 2), "Owner contribution", cash, capital, 10_000);
|
||
post(&mut l, d(1, 15), "Client payment", cash, revenue, 2_500);
|
||
post(&mut l, d(1, 20), "Rent", rent, cash, 800);
|
||
if close_january {
|
||
let jan = l.create_period("2026-01", d(1, 1), d(1, 31)).unwrap();
|
||
l.close_period(jan).unwrap();
|
||
}
|
||
// Software on the card, tagged to the Apps class.
|
||
let e = l.create_entry(d(2, 3), "Software");
|
||
l.add_line(e, software, Side::Debit, usd(120)).unwrap();
|
||
l.add_line(e, card, Side::Credit, usd(120)).unwrap();
|
||
l.set_line_class(e, 0, Some(software_class)).unwrap();
|
||
l.post_entry(e).unwrap();
|
||
post(&mut l, d(2, 10), "Client payment", cash, revenue, 1_000);
|
||
post(&mut l, d(2, 25), "Pay card", card, cash, 120);
|
||
Books {
|
||
ledger: l,
|
||
cash,
|
||
software_class,
|
||
}
|
||
}
|
||
|
||
fn line(code: &str, name: &str, n: i64) -> StatementLine {
|
||
StatementLine {
|
||
code: code.to_string(),
|
||
name: name.to_string(),
|
||
amount: dec(n),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn profit_and_loss_is_scoped_to_the_period() {
|
||
let b = books(false);
|
||
let jan = profit_and_loss_for_period(&b.ledger, d(1, 1), d(1, 31), None).unwrap();
|
||
assert_eq!(jan.revenue, vec![line("4000", "Revenue", 2_500)]);
|
||
assert_eq!(jan.expenses, vec![line("6100", "Rent", 800)]);
|
||
assert_eq!(jan.net_income(), dec(1_700));
|
||
|
||
let feb = profit_and_loss_for_period(&b.ledger, d(2, 1), d(2, 28), None).unwrap();
|
||
assert_eq!(feb.revenue, vec![line("4000", "Revenue", 1_000)]);
|
||
assert_eq!(feb.expenses, vec![line("6000", "Software", 120)]);
|
||
assert_eq!(feb.net_income(), dec(880));
|
||
|
||
// Both months: 3,500 revenue, 920 expenses, lines sorted by code.
|
||
let both = profit_and_loss_for_period(&b.ledger, d(1, 1), d(2, 28), None).unwrap();
|
||
assert_eq!(
|
||
both.expenses,
|
||
vec![line("6000", "Software", 120), line("6100", "Rent", 800)]
|
||
);
|
||
assert_eq!(
|
||
(both.total_revenue, both.total_expenses),
|
||
(dec(3_500), dec(920))
|
||
);
|
||
// ... which is the life-of-ledger P&L, because nothing is outside it.
|
||
let all = profit_and_loss(&b.ledger).unwrap();
|
||
assert_eq!((all.revenue, all.expenses), (dec(3_500), dec(920)));
|
||
|
||
// One class: only the tagged software line.
|
||
let apps =
|
||
profit_and_loss_for_period(&b.ledger, d(1, 1), d(12, 31), Some(b.software_class)).unwrap();
|
||
assert!(apps.revenue.is_empty());
|
||
assert_eq!(apps.expenses, vec![line("6000", "Software", 120)]);
|
||
|
||
// A backwards range is rejected, not silently empty.
|
||
assert!(profit_and_loss_for_period(&b.ledger, d(2, 1), d(1, 1), None).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn balance_sheet_is_as_of_the_date() {
|
||
let b = books(false);
|
||
// Before anything happened: empty and balanced.
|
||
let start = balance_sheet_as_of(&b.ledger, d(1, 1)).unwrap();
|
||
assert!(start.assets.is_empty() && start.equity.is_empty());
|
||
assert!(start.balanced());
|
||
|
||
// Jan 31: cash 10,000 + 2,500 − 800 = 11,700; equity 10,000 + 1,700.
|
||
let jan = balance_sheet_as_of(&b.ledger, d(1, 31)).unwrap();
|
||
assert_eq!(jan.assets, vec![line("1000", "Cash", 11_700)]);
|
||
assert!(jan.liabilities.is_empty());
|
||
assert_eq!(jan.equity, vec![line("3000", "Capital", 10_000)]);
|
||
assert_eq!(jan.current_earnings, dec(1_700));
|
||
assert_eq!(jan.total_equity, dec(11_700));
|
||
assert!(jan.balanced());
|
||
|
||
// Feb 15: cash 12,700; the card owes 120; earnings 1,700 + 880.
|
||
let mid = balance_sheet_as_of(&b.ledger, d(2, 15)).unwrap();
|
||
assert_eq!(mid.total_assets, dec(12_700));
|
||
assert_eq!(mid.liabilities, vec![line("2000", "Card", 120)]);
|
||
assert_eq!(mid.current_earnings, dec(2_580));
|
||
assert_eq!(mid.total_liabilities + mid.total_equity, dec(12_700));
|
||
|
||
// Feb 28: card paid off, so it drops off the statement.
|
||
let end = balance_sheet_as_of(&b.ledger, d(2, 28)).unwrap();
|
||
assert_eq!(end.total_assets, dec(12_580));
|
||
assert!(end.liabilities.is_empty());
|
||
assert!(end.balanced());
|
||
// ... and agrees with the life-of-ledger balance sheet.
|
||
let all = balance_sheet(&b.ledger).unwrap();
|
||
assert_eq!(
|
||
(all.assets, all.liabilities, all.equity),
|
||
(end.total_assets, end.total_liabilities, end.total_equity)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_closed_period_still_reports_its_earnings() {
|
||
let b = books(true);
|
||
// The close posted Dr 4000 2,500 / Cr 6100 800 / Cr 3999 1,700 on Jan
|
||
// 31. The January P&L must not count it: January earned 1,700.
|
||
let jan = profit_and_loss_for_period(&b.ledger, d(1, 1), d(1, 31), None).unwrap();
|
||
assert_eq!(jan.net_income(), dec(1_700));
|
||
assert_eq!(jan.revenue, vec![line("4000", "Revenue", 2_500)]);
|
||
let feb = profit_and_loss_for_period(&b.ledger, d(2, 1), d(2, 28), None).unwrap();
|
||
assert_eq!(feb.net_income(), dec(880));
|
||
|
||
// After the close, January's earnings sit in retained earnings, not in
|
||
// current earnings; February's are current.
|
||
let jan_bs = balance_sheet_as_of(&b.ledger, d(1, 31)).unwrap();
|
||
assert_eq!(
|
||
jan_bs.equity,
|
||
vec![
|
||
line("3000", "Capital", 10_000),
|
||
line("3999", "Retained Earnings", 1_700)
|
||
]
|
||
);
|
||
assert_eq!(jan_bs.current_earnings, Decimal::ZERO);
|
||
assert!(jan_bs.balanced());
|
||
let feb_bs = balance_sheet_as_of(&b.ledger, d(2, 28)).unwrap();
|
||
assert_eq!(feb_bs.current_earnings, dec(880));
|
||
assert_eq!(feb_bs.total_equity, dec(12_580));
|
||
assert!(feb_bs.balanced());
|
||
|
||
// The cash flow for January: net income 1,700, and the only financing
|
||
// flow is the owner's 10,000. The close is not an owner contribution.
|
||
let cf = cash_flow(
|
||
&b.ledger,
|
||
&CashFlowParams {
|
||
start: d(1, 1),
|
||
end: d(1, 31),
|
||
cash_accounts: vec![b.cash],
|
||
financing_accounts: vec![],
|
||
},
|
||
)
|
||
.unwrap();
|
||
assert_eq!(cf.net_income, dec(1_700));
|
||
assert_eq!(cf.financing.len(), 1);
|
||
assert_eq!(cf.cash_from_financing, dec(10_000));
|
||
assert_eq!(cf.net_change_in_cash, dec(11_700));
|
||
}
|
||
|
||
/// Cash flow lines come out in account-code order, every time. (The chart
|
||
/// iterates in hash order; the statement must not.)
|
||
#[test]
|
||
fn cash_flow_lines_are_in_code_order() {
|
||
for _ in 0..20 {
|
||
let mut l = Ledger::new(Currency::USD);
|
||
let cash = l
|
||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||
.unwrap();
|
||
let mut owners = Vec::new();
|
||
for code in ["3005", "3001", "3009", "3000", "3003"] {
|
||
owners.push(
|
||
l.add_account(code, code, AccountType::Equity, None, false)
|
||
.unwrap(),
|
||
);
|
||
}
|
||
for o in &owners {
|
||
post(&mut l, d(3, 1), "Contribution", cash, *o, 100);
|
||
}
|
||
let cf = cash_flow(
|
||
&l,
|
||
&CashFlowParams {
|
||
start: d(1, 1),
|
||
end: d(12, 31),
|
||
cash_accounts: vec![cash],
|
||
financing_accounts: vec![],
|
||
},
|
||
)
|
||
.unwrap();
|
||
// Labels read "Owner contributions — 3000 3000" (code, then name).
|
||
let codes: Vec<&str> = cf
|
||
.financing
|
||
.iter()
|
||
.map(|f| f.label.rsplit(' ').next().unwrap())
|
||
.collect();
|
||
assert_eq!(
|
||
codes,
|
||
["3000", "3001", "3003", "3005", "3009"],
|
||
"{:?}",
|
||
cf.financing
|
||
);
|
||
}
|
||
}
|
||
|
||
fn cents(s: &str) -> Decimal {
|
||
Decimal::from_str(s).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn trial_balance_is_as_of_the_date() {
|
||
let b = books(false);
|
||
// Jan 31: Dr cash 11,700 and rent 800; Cr capital 10,000 and revenue 2,500.
|
||
let tb = trial_balance_as_of(&b.ledger, d(1, 31));
|
||
let rows: Vec<(&str, Decimal, Decimal)> = tb
|
||
.lines
|
||
.iter()
|
||
.map(|l| (l.code.as_str(), l.debit, l.credit))
|
||
.collect();
|
||
assert_eq!(
|
||
rows,
|
||
[
|
||
("1000", dec(11_700), dec(0)),
|
||
("3000", dec(0), dec(10_000)),
|
||
("4000", dec(0), dec(2_500)),
|
||
("6100", dec(800), dec(0)),
|
||
]
|
||
);
|
||
assert_eq!(
|
||
(tb.total_debits, tb.total_credits),
|
||
(dec(12_500), dec(12_500))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn general_ledger_runs_balances_by_account() {
|
||
let b = books(false);
|
||
let gl = general_ledger(&b.ledger, d(2, 1), d(2, 28)).unwrap();
|
||
let codes: Vec<&str> = gl.accounts.iter().map(|a| a.code.as_str()).collect();
|
||
// Every account with a beginning balance or February activity, by code.
|
||
assert_eq!(codes, ["1000", "2000", "3000", "4000", "6000", "6100"]);
|
||
|
||
let cash = &gl.accounts[0];
|
||
assert_eq!(cash.beginning_balance, dec(11_700));
|
||
let p: Vec<(Decimal, Decimal, &str, Decimal)> = cash
|
||
.postings
|
||
.iter()
|
||
.map(|p| (p.debit, p.credit, p.split.as_str(), p.balance))
|
||
.collect();
|
||
assert_eq!(
|
||
p,
|
||
[
|
||
(dec(1_000), dec(0), "4000 Revenue", dec(12_700)),
|
||
(dec(0), dec(120), "2000 Card", dec(12_580)),
|
||
]
|
||
);
|
||
assert_eq!(cash.ending_balance, dec(12_580));
|
||
|
||
// The card: borrowed 120, paid 120, back to zero (credit-normal).
|
||
let card = &gl.accounts[1];
|
||
assert_eq!(card.beginning_balance, dec(0));
|
||
assert_eq!(card.postings[0].balance, dec(120));
|
||
assert_eq!(card.ending_balance, dec(0));
|
||
// Capital: no February activity, still listed with its balance.
|
||
assert!(gl.accounts[2].postings.is_empty());
|
||
assert_eq!(gl.accounts[2].ending_balance, dec(10_000));
|
||
|
||
// Every ending balance ties to the trial balance as of the end.
|
||
let tb = trial_balance_as_of(&b.ledger, d(2, 28));
|
||
for a in &gl.accounts {
|
||
let tb_line = tb.lines.iter().find(|l| l.code == a.code);
|
||
let tb_amount = tb_line.map_or(Decimal::ZERO, |l| l.debit - l.credit);
|
||
let normal = match a.account_type {
|
||
AccountType::Asset | AccountType::Expense => a.ending_balance,
|
||
_ => -a.ending_balance,
|
||
};
|
||
assert_eq!(tb_amount, normal, "account {}", a.code);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn general_ledger_marks_compound_entries_as_split() {
|
||
let mut l = Ledger::new(Currency::USD);
|
||
let cash = l
|
||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||
.unwrap();
|
||
let card = l
|
||
.add_account("2000", "Card", AccountType::Liability, None, false)
|
||
.unwrap();
|
||
let soft = l
|
||
.add_account("6000", "Software", AccountType::Expense, None, false)
|
||
.unwrap();
|
||
// Dr software 100 / Cr cash 60 / Cr card 40: one debit, split credits.
|
||
let e = l.create_entry(d(3, 1), "Annual license, paid two ways");
|
||
l.add_line(e, soft, Side::Debit, usd(100)).unwrap();
|
||
l.add_line(e, cash, Side::Credit, usd(60)).unwrap();
|
||
l.add_line(e, card, Side::Credit, usd(40)).unwrap();
|
||
l.post_entry(e).unwrap();
|
||
let gl = general_ledger(&l, d(1, 1), d(12, 31)).unwrap();
|
||
let split_of = |code: &str| {
|
||
let a = gl.accounts.iter().find(|a| a.code == code).unwrap();
|
||
(a.postings[0].split.clone(), a.postings[0].entry_id)
|
||
};
|
||
// The debit side has two other accounts; each credit has one.
|
||
assert_eq!(split_of("6000"), ("-Split-".to_string(), e));
|
||
assert_eq!(split_of("1000"), ("6000 Software".to_string(), e));
|
||
assert_eq!(split_of("2000"), ("6000 Software".to_string(), e));
|
||
}
|
||
|
||
/// Two members, 50/50: Alice and Bob. January: Alice puts in 600, Bob 400;
|
||
/// revenue 1,000; expense 333.33, so net income 666.67, which doesn't split
|
||
/// evenly. February: Bob takes a 100 distribution; revenue 100.01.
|
||
fn partnership(close_january: bool) -> (Ledger, Vec<MemberCapital>) {
|
||
let mut l = Ledger::new(Currency::USD);
|
||
let cash = l
|
||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||
.unwrap();
|
||
let alice = l
|
||
.add_account("3000", "Capital — Alice", AccountType::Equity, None, false)
|
||
.unwrap();
|
||
let bob = l
|
||
.add_account("3001", "Capital — Bob", AccountType::Equity, None, false)
|
||
.unwrap();
|
||
let rev = l
|
||
.add_account("4000", "Revenue", AccountType::Revenue, None, false)
|
||
.unwrap();
|
||
let exp = l
|
||
.add_account("6000", "Expense", AccountType::Expense, None, false)
|
||
.unwrap();
|
||
let money = |s: &str| Money::new(cents(s), Currency::USD);
|
||
let post = |l: &mut Ledger, date, dr, cr, amt: &str| {
|
||
let e = l.create_entry(date, "x");
|
||
l.add_line(e, dr, Side::Debit, money(amt)).unwrap();
|
||
l.add_line(e, cr, Side::Credit, money(amt)).unwrap();
|
||
l.post_entry(e).unwrap();
|
||
};
|
||
post(&mut l, d(1, 2), cash, alice, "600");
|
||
post(&mut l, d(1, 2), cash, bob, "400");
|
||
post(&mut l, d(1, 10), cash, rev, "1000");
|
||
post(&mut l, d(1, 20), exp, cash, "333.33");
|
||
if close_january {
|
||
let jan = l.create_period("2026-01", d(1, 1), d(1, 31)).unwrap();
|
||
l.close_period(jan).unwrap();
|
||
}
|
||
post(&mut l, d(2, 5), bob, cash, "100");
|
||
post(&mut l, d(2, 9), cash, rev, "100.01");
|
||
let half = cents("0.5");
|
||
let members = vec![
|
||
MemberCapital {
|
||
name: "Alice".into(),
|
||
capital_account: alice,
|
||
share: half,
|
||
},
|
||
MemberCapital {
|
||
name: "Bob".into(),
|
||
capital_account: bob,
|
||
share: half,
|
||
},
|
||
];
|
||
(l, members)
|
||
}
|
||
|
||
#[test]
|
||
fn members_equity_rolls_capital_forward_and_ties_to_the_balance_sheet() {
|
||
for close in [false, true] {
|
||
let (l, members) = partnership(close);
|
||
// January: 666.67 splits 333.34 / 333.33 (half a cent rounds up for
|
||
// Alice; Bob gets the remainder so the total is exact).
|
||
let jan = members_equity(&l, d(1, 1), d(1, 31), &members).unwrap();
|
||
let row = |c: &ledger_core::MemberEquityColumn| {
|
||
(
|
||
c.beginning,
|
||
c.contributions,
|
||
c.net_income,
|
||
c.distributions,
|
||
c.ending,
|
||
)
|
||
};
|
||
assert_eq!(
|
||
row(&jan.members[0]),
|
||
(
|
||
cents("0"),
|
||
cents("600"),
|
||
cents("333.34"),
|
||
cents("0"),
|
||
cents("933.34")
|
||
)
|
||
);
|
||
assert_eq!(
|
||
row(&jan.members[1]),
|
||
(
|
||
cents("0"),
|
||
cents("400"),
|
||
cents("333.33"),
|
||
cents("0"),
|
||
cents("733.33")
|
||
)
|
||
);
|
||
assert_eq!(jan.total.ending, cents("1666.67"));
|
||
assert_eq!(
|
||
jan.total.ending,
|
||
balance_sheet_as_of(&l, d(1, 31)).unwrap().total_equity
|
||
);
|
||
|
||
// February begins where January ended, whether or not January was
|
||
// closed to retained earnings. 100.01 splits 50.01 / 50.00.
|
||
let feb = members_equity(&l, d(2, 1), d(2, 28), &members).unwrap();
|
||
assert_eq!(
|
||
row(&feb.members[0]),
|
||
(
|
||
cents("933.34"),
|
||
cents("0"),
|
||
cents("50.01"),
|
||
cents("0"),
|
||
cents("983.35")
|
||
)
|
||
);
|
||
assert_eq!(
|
||
row(&feb.members[1]),
|
||
(
|
||
cents("733.33"),
|
||
cents("0"),
|
||
cents("50.00"),
|
||
cents("100"),
|
||
cents("683.33")
|
||
)
|
||
);
|
||
assert_eq!(feb.total.ending, cents("1666.68"));
|
||
assert_eq!(
|
||
feb.total.ending,
|
||
balance_sheet_as_of(&l, d(2, 28)).unwrap().total_equity
|
||
);
|
||
assert!(
|
||
feb.members.iter().all(|m| m.other.is_zero()),
|
||
"close={close}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn members_must_be_configured_correctly() {
|
||
let (l, mut members) = partnership(false);
|
||
members[1].share = cents("0.4");
|
||
assert!(matches!(
|
||
members_equity(&l, d(1, 1), d(1, 31), &members),
|
||
Err(LedgerError::InvalidMembers(_))
|
||
));
|
||
members[1].share = cents("0.5");
|
||
members[1].capital_account = members[0].capital_account;
|
||
assert!(matches!(
|
||
members_equity(&l, d(1, 1), d(1, 31), &members),
|
||
Err(LedgerError::InvalidMembers(_))
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn cash_flow_lines_name_their_account() {
|
||
let (l, _) = partnership(false);
|
||
let cf = cash_flow(
|
||
&l,
|
||
&CashFlowParams {
|
||
start: d(1, 1),
|
||
end: d(12, 31),
|
||
cash_accounts: vec![1],
|
||
financing_accounts: vec![],
|
||
},
|
||
)
|
||
.unwrap();
|
||
let accounts: Vec<Option<&str>> = cf.financing.iter().map(|f| f.account.as_deref()).collect();
|
||
assert_eq!(accounts, [Some("3000"), Some("3001")]);
|
||
}
|