P3 review: the same figures agree across the pieces
A distribution declared to a payable and then paid is financing when paid, the declaration disclosed (ASC 230-10-45-15(a)); a disposal's depreciation lines count as depreciation in the cash flow and the tax-basis K-1; an as-of asset list says the status of that day. Seam tests tie the members' statement, K-1, party totals, cash flow, 1099s, depreciation and the CPA package together on three dates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
13 files changed
+2258
-24
No files matched your search
@@ -0,0 +1,677 @@
|
||||
//! P3's seams: the same economic fact, computed by two pieces of the
|
||||
//! engine, agrees (or rules.md says exactly why not).
|
||||
//!
|
||||
//! One year of fictional books for a two-member LLC (Ana Example and Ben
|
||||
//! Example, 50/50), built through the real [`Store`]: cash and property
|
||||
//! contributions, member-paid expenses through Due to Members and their
|
||||
//! reimbursement, card purchases and card payments, a prepaid on a daily
|
||||
//! schedule, a 1099 contractor paid by check, by card and by a member's
|
||||
//! check, whose status changes mid-year, a vendor refund, a loan,
|
||||
//! depreciation runs with one reversed and caught up, a disposal, a
|
||||
//! distribution declared then paid, and monthly closes to the members with
|
||||
//! odd cents. Every name and amount is made up.
|
||||
//!
|
||||
//! Then, on several dates, the pieces are compared with each other:
|
||||
//! the members' equity statement, the K-1 worksheet (book and tax basis),
|
||||
//! party totals on both bases, the cash flow statement, the 1099
|
||||
//! worksheet, the P&L, the balance sheet, the fixed asset register, the
|
||||
//! IIF and the CPA package as of each date.
|
||||
|
||||
#[path = "seams/books.rs"]
|
||||
mod books;
|
||||
|
||||
use books::{Books, books, d, dec};
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::parties::Basis;
|
||||
use ledger_core::tax_capital::Open;
|
||||
use ledger_core::{AccountId, CashFlowSection, CashFlowStatement, Ledger, MemberCapital, PartyId};
|
||||
use ledger_package::{PackageRequest, Settings, build_package};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
fn capitals(b: &Books) -> Vec<MemberCapital> {
|
||||
ledger_package::member_capitals(b.store.ledger(), &b.company.members).unwrap()
|
||||
}
|
||||
|
||||
fn settings(b: &Books) -> Settings<'_> {
|
||||
Settings {
|
||||
company: &b.company,
|
||||
cash_accounts: &b.cash,
|
||||
financing_accounts: &b.financing,
|
||||
investing_accounts: &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn ids(l: &Ledger, codes: &[String]) -> Vec<AccountId> {
|
||||
codes
|
||||
.iter()
|
||||
.map(|c| l.chart().find_by_code(c).unwrap().id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The dates every check runs on: mid-year after a close with a
|
||||
/// distribution declared and unpaid, mid-month with nothing closed for
|
||||
/// the month, and the year's end.
|
||||
const DATES: [&str; 3] = ["2026-06-30", "2026-09-15", "2026-12-31"];
|
||||
const START: &str = "2026-01-01";
|
||||
|
||||
fn cash_flow(b: &Books, end: NaiveDate) -> CashFlowStatement {
|
||||
ledger_package::cash_flow_for(
|
||||
b.store.ledger(),
|
||||
&settings(b),
|
||||
Some(START),
|
||||
Some(&end.to_string()),
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
end,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The total of the kinds in `section` whose caption starts with `prefix`.
|
||||
fn kinds(cf: &CashFlowStatement, section: CashFlowSection, prefix: &str) -> Decimal {
|
||||
cf.kinds(section)
|
||||
.iter()
|
||||
.filter(|k| k.kind.starts_with(prefix))
|
||||
.map(|k| k.amount)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn kind(cf: &CashFlowStatement, section: CashFlowSection, caption: &str) -> Decimal {
|
||||
cf.kinds(section)
|
||||
.iter()
|
||||
.filter(|k| k.kind == caption)
|
||||
.map(|k| k.amount)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn activity(b: &Books, party: PartyId, end: NaiveDate) -> ledger_core::parties::Activity {
|
||||
let l = b.store.ledger();
|
||||
let money = ledger_core::parties::money_accounts(l, &ids(l, &b.cash), &ids(l, &b.financing));
|
||||
ledger_core::parties::activity_with(
|
||||
l,
|
||||
party,
|
||||
(d(START), end),
|
||||
&money,
|
||||
&b.company.party_members(l),
|
||||
)
|
||||
}
|
||||
|
||||
/// An account's balance through `end`, credit-positive.
|
||||
fn credit_balance(l: &Ledger, code: &str, end: NaiveDate) -> Decimal {
|
||||
let id = l.chart().find_by_code(code).unwrap().id;
|
||||
l.posted_entries()
|
||||
.filter(|e| e.date() <= end)
|
||||
.flat_map(|e| e.lines().iter())
|
||||
.filter(|x| x.account == id)
|
||||
.map(|x| match x.side {
|
||||
ledger_core::Side::Credit => x.amount.amount,
|
||||
ledger_core::Side::Debit => -x.amount.amount,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// What the P&L says an expense account was from START to `end`.
|
||||
fn pnl_expense(l: &Ledger, code: &str, end: NaiveDate) -> Decimal {
|
||||
ledger_core::profit_and_loss_for_period(l, d(START), end, None)
|
||||
.unwrap()
|
||||
.expenses
|
||||
.iter()
|
||||
.filter(|x| x.code == code)
|
||||
.map(|x| x.amount)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The P&L's depreciation expense (6500) from START to `end`.
|
||||
fn pnl_depreciation(l: &Ledger, end: NaiveDate) -> Decimal {
|
||||
pnl_expense(l, "6500", end)
|
||||
}
|
||||
|
||||
/// The members' figures agree on every date as B-13, B-35 items 4 and 8
|
||||
/// and B-14 rule 11 say: the statement of members' equity and the K-1
|
||||
/// worksheet are one roll-forward; party totals on basis all are the
|
||||
/// statement's contributions, on basis cash the K-1's cash
|
||||
/// contributions; the cash flow statement's contributions are the K-1's
|
||||
/// cash, the property is disclosed, and its distributions are the
|
||||
/// statement's less what is declared and unpaid, which party totals
|
||||
/// count when paid too; what the members advanced is borrowed and repaid
|
||||
/// on the statement and the repayment is paid to them in party totals.
|
||||
#[test]
|
||||
fn the_members_figures_agree_on_every_date() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let l = b.store.ledger();
|
||||
for end in DATES.map(d) {
|
||||
let me = ledger_core::members_equity(l, d(START), end, &capitals(&b)).unwrap();
|
||||
let k1 = ledger_core::k1::k1_worksheet(l, d(START), end, &capitals(&b), &ids(l, &b.cash))
|
||||
.unwrap();
|
||||
let cf = cash_flow(&b, end);
|
||||
let bs = ledger_core::balance_sheet_as_of(l, end).unwrap();
|
||||
assert_eq!(me.total.ending, bs.total_equity, "{end}");
|
||||
let unpaid = credit_balance(l, "2200", end);
|
||||
let mut distributions_paid = Decimal::ZERO;
|
||||
let mut paid_to_members = Decimal::ZERO;
|
||||
for (i, (party, name)) in [(b.ana, "Ana Example"), (b.ben, "Ben Example")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let (m, k) = (&me.members[i], &k1[i]);
|
||||
// One roll-forward (k1 INVARIANT 1).
|
||||
assert_eq!(
|
||||
(
|
||||
m.beginning,
|
||||
m.net_income,
|
||||
m.distributions,
|
||||
m.other,
|
||||
m.ending
|
||||
),
|
||||
(
|
||||
k.beginning,
|
||||
k.net_income,
|
||||
k.distributions,
|
||||
k.other,
|
||||
k.ending
|
||||
),
|
||||
"{name} {end}"
|
||||
);
|
||||
assert_eq!(
|
||||
m.contributions,
|
||||
k.contributions_cash + k.contributions_property
|
||||
);
|
||||
// Party totals (B-35 item 4).
|
||||
let a = activity(&b, party, end);
|
||||
assert!(a.unresolved.is_empty(), "{name} {end}: {:?}", a.unresolved);
|
||||
assert_eq!(a.totals(Basis::All).1, m.contributions, "{name} {end}");
|
||||
assert_eq!(
|
||||
a.totals(Basis::Cash).1,
|
||||
k.contributions_cash,
|
||||
"{name} {end}"
|
||||
);
|
||||
assert_eq!(a.received_in_kind, k.contributions_property, "{name} {end}");
|
||||
// The cash flow (B-14 rule 11, B-35 item 8).
|
||||
assert_eq!(
|
||||
kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
&format!("Contributions from members — {name}")
|
||||
),
|
||||
k.contributions_cash,
|
||||
"{name} {end}"
|
||||
);
|
||||
distributions_paid -= kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
&format!("Distributions to members — {name}"),
|
||||
);
|
||||
paid_to_members += a.totals(Basis::Cash).0;
|
||||
}
|
||||
// A distribution declared to Distributions Payable is a
|
||||
// distribution on the statement of members' equity when declared
|
||||
// and on the cash flow statement and in party totals when paid
|
||||
// (B-14 rule 2, B-35 item 4).
|
||||
distributions_paid -= kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
"Distributions to members — 2200 Distributions Payable",
|
||||
);
|
||||
assert_eq!(
|
||||
distributions_paid,
|
||||
me.total.distributions - unpaid,
|
||||
"{end}: distributions paid"
|
||||
);
|
||||
assert!(
|
||||
cf.noncash_disclosures
|
||||
.iter()
|
||||
.any(|n| n.label.starts_with("Distribution declared") && n.amount == dec("2000.00")),
|
||||
"{end}: the declaration is disclosed"
|
||||
);
|
||||
assert!(
|
||||
cf.noncash_disclosures
|
||||
.iter()
|
||||
.any(|n| n.label.starts_with("Ana contributes her laptop")
|
||||
&& n.amount == dec("1800.00")),
|
||||
"{end}: the property contributed is disclosed"
|
||||
);
|
||||
// No distribution is ever cash paid to suppliers, and no noncash
|
||||
// operating item hides the declaration.
|
||||
assert!(
|
||||
!cf.operating_reconciliation
|
||||
.lines
|
||||
.iter()
|
||||
.any(|r| r.label.contains("2200") || r.label == "Other noncash items"),
|
||||
"{end}: {:?}",
|
||||
cf.operating_reconciliation.lines
|
||||
);
|
||||
// What the members advanced: borrowed when they paid, repaid when
|
||||
// reimbursed; the reimbursements are what party totals paid the
|
||||
// members beyond their distributions.
|
||||
let advanced = kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
"Proceeds from borrowing — 2100 Due to Members",
|
||||
);
|
||||
let repaid = -kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
"Repayments of borrowing — 2100 Due to Members",
|
||||
);
|
||||
let expect = if end < d("2026-05-20") {
|
||||
dec("120.00")
|
||||
} else {
|
||||
dec("1020.00")
|
||||
};
|
||||
assert_eq!(advanced, expect, "{end}");
|
||||
assert_eq!(repaid, expect, "{end}");
|
||||
assert_eq!(paid_to_members, distributions_paid + repaid, "{end}");
|
||||
assert_eq!(credit_balance(l, "2100", end), Decimal::ZERO, "{end}");
|
||||
}
|
||||
// Hand figures at the year's end: Ana 7,500 cash and the 1,800 laptop,
|
||||
// Ben 7,500; each took 1,000 declared and 500 directly.
|
||||
let end = d("2026-12-31");
|
||||
let k1 =
|
||||
ledger_core::k1::k1_worksheet(l, d(START), end, &capitals(&b), &ids(l, &b.cash)).unwrap();
|
||||
assert_eq!(k1[0].contributions_property, dec("1800.00"));
|
||||
assert_eq!(k1[0].distributions, dec("1500.00"));
|
||||
assert_eq!(
|
||||
activity(&b, b.ana, end).totals(Basis::Cash).0,
|
||||
dec("1620.00")
|
||||
);
|
||||
assert_eq!(
|
||||
activity(&b, b.ben, end).totals(Basis::Cash).0,
|
||||
dec("2400.00")
|
||||
);
|
||||
// Mid-year, the declaration is unpaid: the statement counts it, party
|
||||
// totals and the cash flow don't yet (B-35 item 4's second condition).
|
||||
let mid = d("2026-06-30");
|
||||
assert_eq!(
|
||||
activity(&b, b.ana, mid).totals(Basis::Cash).0,
|
||||
dec("120.00")
|
||||
);
|
||||
assert_eq!(
|
||||
ledger_core::members_equity(l, d(START), mid, &capitals(&b))
|
||||
.unwrap()
|
||||
.members[0]
|
||||
.distributions,
|
||||
dec("1000.00")
|
||||
);
|
||||
}
|
||||
|
||||
/// B-14 rule 2, from the seam review: a distribution declared to a
|
||||
/// liability and paid later is a financing outflow when paid (ASC
|
||||
/// 230-10-45-15(a)), its declaration a noncash financing activity; before
|
||||
/// the fix it was "Cash paid to suppliers and employees".
|
||||
#[test]
|
||||
fn a_distribution_declared_then_paid_is_financing_when_paid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let cf = cash_flow(&b, d("2026-07-31"));
|
||||
assert_eq!(
|
||||
kind(
|
||||
&cf,
|
||||
CashFlowSection::Financing,
|
||||
"Distributions to members — 2200 Distributions Payable"
|
||||
),
|
||||
dec("-2000.00")
|
||||
);
|
||||
let paid = cf
|
||||
.lines(CashFlowSection::Operating)
|
||||
.iter()
|
||||
.filter(|x| x.label.starts_with("Distribution paid"))
|
||||
.count();
|
||||
assert_eq!(paid, 0, "{:?}", cf.operating);
|
||||
// A statement of July alone, after the declaration's period, reads
|
||||
// the payment the same way.
|
||||
let july = ledger_package::cash_flow_for(
|
||||
b.store.ledger(),
|
||||
&settings(&b),
|
||||
Some("2026-07-01"),
|
||||
Some("2026-07-31"),
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
d("2026-07-31"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
kind(
|
||||
&july,
|
||||
CashFlowSection::Financing,
|
||||
"Distributions to members — 2200 Distributions Payable"
|
||||
),
|
||||
dec("-2000.00")
|
||||
);
|
||||
// Through June the payment hasn't happened: nothing in the body, the
|
||||
// declaration disclosed.
|
||||
let june = cash_flow(&b, d("2026-06-30"));
|
||||
assert_eq!(
|
||||
kinds(&june, CashFlowSection::Financing, "Distributions"),
|
||||
Decimal::ZERO
|
||||
);
|
||||
}
|
||||
|
||||
/// B-35 item 9 and B-19: for a payee every entry of which names it alone,
|
||||
/// party totals' paid less received (basis cash) is the 1099 worksheet's
|
||||
/// paid + excluded card + undetermined + not reportable, less what the
|
||||
/// worksheet lists as received for something else; and the worksheet
|
||||
/// ties to the ledger by hand.
|
||||
#[test]
|
||||
fn the_1099_worksheet_ties_to_party_totals_and_the_ledger() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let l = b.store.ledger();
|
||||
for end in DATES.map(d) {
|
||||
let w =
|
||||
ledger_package::vendor_1099_report(l, &settings(&b), 2026, None, Some(end)).unwrap();
|
||||
assert!(w.unresolved.is_empty());
|
||||
for v in &w.vendors {
|
||||
let party = l.party_of_vendor(v.vendor_id).unwrap();
|
||||
let a = activity(&b, party, end);
|
||||
let (paid, received) = a.totals(Basis::Cash);
|
||||
let other: Decimal = w
|
||||
.other_receipts
|
||||
.iter()
|
||||
.filter(|p| p.vendor_id == v.vendor_id)
|
||||
.map(|p| p.amount)
|
||||
.sum();
|
||||
assert_eq!(
|
||||
paid - received,
|
||||
v.paid + v.excluded_card + v.undetermined + v.not_reportable - other,
|
||||
"{} {end}",
|
||||
v.name
|
||||
);
|
||||
}
|
||||
let delta = w.vendors.iter().find(|v| v.vendor_id == b.delta).unwrap();
|
||||
// By hand: 1,500 by check and 900 by Ben's check count; 700 on
|
||||
// the card is excluded (Form 1099-K); 1,200 paid after the
|
||||
// election is not reportable (D-40).
|
||||
let expect = if end < d("2026-09-10") {
|
||||
("2400.00", "700.00", "0")
|
||||
} else {
|
||||
("2400.00", "700.00", "1200.00")
|
||||
};
|
||||
assert_eq!(
|
||||
(delta.paid, delta.excluded_card, delta.not_reportable),
|
||||
(dec(expect.0), dec(expect.1), dec(expect.2)),
|
||||
"{end}"
|
||||
);
|
||||
assert_eq!(delta.status, ledger_core::form1099::Status1099::Reportable);
|
||||
// The contract labor expense (6400) is exactly Delta's payments:
|
||||
// the four buckets are its debits in the year.
|
||||
let labor = pnl_expense(l, "6400", end);
|
||||
assert_eq!(
|
||||
delta.paid + delta.excluded_card + delta.undetermined + delta.not_reportable,
|
||||
labor,
|
||||
"{end}"
|
||||
);
|
||||
// The supplies vendor: 300.25 on the card, a 40.00 refund netted
|
||||
// on the worksheet and received in party totals.
|
||||
let paper = w
|
||||
.vendors
|
||||
.iter()
|
||||
.find(|v| l.party_of_vendor(v.vendor_id) == Some(b.paper_party))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(paper.paid, paper.excluded_card),
|
||||
(dec("-40.00"), dec("300.25"))
|
||||
);
|
||||
assert_eq!(
|
||||
activity(&b, b.paper_party, end).totals(Basis::Cash),
|
||||
(dec("300.25"), dec("40.00"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The IIF's amounts on one account, through as_of (B-34: every posted
|
||||
/// entry, closes left out), debit-positive.
|
||||
fn iif_total(iif: &str, account: &str) -> Decimal {
|
||||
let mut total = Decimal::ZERO;
|
||||
for line in iif.split("\r\n") {
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
if (f[0] == "TRNS" || f[0] == "SPL") && f.get(4) == Some(&account) {
|
||||
total += dec(f[6]);
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Every date in the IIF, as written (MM/DD/YYYY).
|
||||
fn iif_dates(iif: &str) -> Vec<NaiveDate> {
|
||||
iif.split("\r\n")
|
||||
.filter_map(|line| {
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
(f[0] == "TRNS" || f[0] == "SPL")
|
||||
.then(|| NaiveDate::parse_from_str(f[3], "%m/%d/%Y").unwrap())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Depreciation is one figure everywhere (B-15 rule 8, B-14 rule 8,
|
||||
/// B-36): the P&L's expense, the cash flow reconciliation's add-back, the
|
||||
/// tax-basis K-1's open item, the IIF's, and the runs plus what the
|
||||
/// disposal posted; the balance sheet's accumulated depreciation is the
|
||||
/// register's posted figure, and posted differs from scheduled by exactly
|
||||
/// what is still to be caught up.
|
||||
#[test]
|
||||
fn depreciation_is_one_figure_everywhere() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let l = b.store.ledger();
|
||||
// (date, scheduled, posted): May's camera run, reversed on June 3, is
|
||||
// caught up by July's run; mid-September the month isn't run yet.
|
||||
let expect = [
|
||||
("2026-06-30", "440.00", "365.00"),
|
||||
("2026-09-15", "752.50", "690.00"),
|
||||
("2026-12-31", "550.00", "550.00"),
|
||||
];
|
||||
for (end, scheduled, posted) in expect {
|
||||
let end = d(end);
|
||||
let pnl = pnl_depreciation(l, end);
|
||||
let cf = cash_flow(&b, end);
|
||||
let addback: Decimal = cf
|
||||
.operating_reconciliation
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|r| r.kind == ledger_core::ReconcilingKind::Depreciation)
|
||||
.map(|r| r.amount)
|
||||
.sum();
|
||||
assert_eq!(
|
||||
addback, pnl,
|
||||
"{end}: the cash flow's depreciation is the P&L's"
|
||||
);
|
||||
let tc = ledger_core::tax_capital::tax_basis_capital(
|
||||
l,
|
||||
d(START),
|
||||
end,
|
||||
&capitals(&b),
|
||||
&ids(l, &b.cash),
|
||||
)
|
||||
.unwrap();
|
||||
let open: Vec<Decimal> = tc
|
||||
.open
|
||||
.iter()
|
||||
.filter_map(|o| match o {
|
||||
Open::TaxDepreciation { book, .. } => Some(*book),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
open,
|
||||
vec![pnl],
|
||||
"{end}: the tax-basis K-1's book depreciation"
|
||||
);
|
||||
let iif = ledger_package::export_iif(l, &b.cash, end).unwrap();
|
||||
assert_eq!(
|
||||
iif_total(&iif.text, "Depreciation"),
|
||||
pnl,
|
||||
"{end}: the IIF's"
|
||||
);
|
||||
assert!(iif_dates(&iif.text).iter().all(|x| *x <= end));
|
||||
let reg = ledger_core::fixed_asset_register(l, end).unwrap();
|
||||
assert_eq!(reg.total_accumulated_scheduled, dec(scheduled), "{end}");
|
||||
assert_eq!(reg.total_accumulated_posted, dec(posted), "{end}");
|
||||
assert_eq!(
|
||||
reg.ledger_accumulated, reg.total_accumulated_posted,
|
||||
"{end}"
|
||||
);
|
||||
let bs_accum = -(credit_balance(l, "1510", end) + credit_balance(l, "1530", end));
|
||||
assert_eq!(
|
||||
-bs_accum, reg.ledger_accumulated,
|
||||
"{end}: the balance sheet's"
|
||||
);
|
||||
for r in ®.rows {
|
||||
if r.disposed.is_none() {
|
||||
assert_eq!(r.book_value, r.cost - r.accumulated_posted);
|
||||
assert_eq!(
|
||||
r.accumulated_posted,
|
||||
l.accumulated_posted(r.asset_id, end).unwrap(),
|
||||
"list_assets and the viewer read Ledger::accumulated_posted"
|
||||
);
|
||||
}
|
||||
}
|
||||
// The runs plus the disposal's own depreciation lines are the
|
||||
// expense.
|
||||
let runs: Decimal = [b.laptop, b.camera]
|
||||
.iter()
|
||||
.map(|&a| l.depreciation_posted_between(a, d(START), end).unwrap())
|
||||
.sum();
|
||||
let disposal = if end >= d("2026-11-20") {
|
||||
dec("-25.00")
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
assert_eq!(runs + disposal, pnl, "{end}");
|
||||
}
|
||||
// By hand: the laptop 50.00 a month, February through December; the
|
||||
// camera 75.00 a month from April 15 through November 20, by days:
|
||||
// 40.00 + 6 × 75.00 + 50.00 = 540.00.
|
||||
assert_eq!(pnl_depreciation(l, d("2026-12-31")), dec("1090.00"));
|
||||
let cf = cash_flow(&b, d("2026-12-31"));
|
||||
let loss: Decimal = cf
|
||||
.operating_reconciliation
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|r| r.kind == ledger_core::ReconcilingKind::InvestingGainLoss)
|
||||
.map(|r| r.amount)
|
||||
.sum();
|
||||
// Book value 3,600.00 − 540.00 = 3,060.00, sold for 2,900.00.
|
||||
assert_eq!(loss, dec("160.00"));
|
||||
}
|
||||
|
||||
/// Footing on every date: the trial balance, the balance sheet, the cash
|
||||
/// flow (sections, change in cash, the reconciliation to operating).
|
||||
#[test]
|
||||
fn every_statement_foots_on_every_date() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let l = b.store.ledger();
|
||||
for end in DATES.map(d) {
|
||||
assert!(
|
||||
ledger_core::reports::trial_balance_as_of(l, end).balanced(),
|
||||
"{end}"
|
||||
);
|
||||
let bs = ledger_core::balance_sheet_as_of(l, end).unwrap();
|
||||
assert!(bs.balanced(), "{end}");
|
||||
let cf = cash_flow(&b, end);
|
||||
assert!(cf.reconciles.difference.is_zero());
|
||||
assert_eq!(
|
||||
cf.cash_from_operations + cf.cash_from_investing + cf.cash_from_financing,
|
||||
cf.net_change_in_cash
|
||||
);
|
||||
assert_eq!(cf.cash_ending - cf.cash_beginning, cf.net_change_in_cash);
|
||||
assert_eq!(cf.cash_ending, -credit_balance(l, "1000", end));
|
||||
assert_eq!(
|
||||
cf.operating_reconciliation.net_cash_from_operating,
|
||||
cf.cash_from_operations
|
||||
);
|
||||
let pnl = ledger_core::profit_and_loss_for_period(l, d(START), end, None).unwrap();
|
||||
assert_eq!(cf.operating_reconciliation.net_income, pnl.net_income());
|
||||
for section in CashFlowSection::ALL {
|
||||
let lines: Decimal = cf.lines(section).iter().map(|x| x.amount).sum();
|
||||
assert_eq!(lines, cf.total(section));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn file<'p>(p: &'p ledger_package::Package, name: &str) -> &'p str {
|
||||
let (_, bytes) = p
|
||||
.files
|
||||
.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
.unwrap_or_else(|| panic!("{name}"));
|
||||
std::str::from_utf8(bytes).unwrap()
|
||||
}
|
||||
|
||||
/// The CPA package as of each date: every file stops at as_of, and its
|
||||
/// figures are the tools' for the same accounts and dates (B-33).
|
||||
#[test]
|
||||
fn the_cpa_package_as_of_each_date_is_the_tools_figures() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let b = books(dir.path());
|
||||
let l = b.store.ledger();
|
||||
for end in DATES.map(d) {
|
||||
let p = build_package(
|
||||
&b.store,
|
||||
&settings(&b),
|
||||
&PackageRequest {
|
||||
as_of: end,
|
||||
prepared_on: d("2027-01-15"),
|
||||
cash_accounts: &[],
|
||||
financing_accounts: &[],
|
||||
investing_accounts: &[],
|
||||
include_receipts: false,
|
||||
tins: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// The K-1 worksheet's rows are k1_worksheet's for the year to date.
|
||||
let k1 = ledger_core::k1::k1_worksheet(l, d(START), end, &capitals(&b), &ids(l, &b.cash))
|
||||
.unwrap();
|
||||
let csv = file(&p, "k1-members.csv");
|
||||
for (row, k) in csv.lines().skip(1).zip(&k1) {
|
||||
let cells: Vec<&str> = row.split(',').collect();
|
||||
let figures: Vec<Decimal> = cells[cells.len() - 7..].iter().map(|c| dec(c)).collect();
|
||||
assert_eq!(
|
||||
figures,
|
||||
vec![
|
||||
k.beginning,
|
||||
k.contributions_cash,
|
||||
k.contributions_property,
|
||||
k.net_income,
|
||||
k.distributions,
|
||||
k.other,
|
||||
k.ending
|
||||
],
|
||||
"{end}"
|
||||
);
|
||||
}
|
||||
// The tax-basis worksheet's depreciation is the P&L's.
|
||||
let tax = file(&p, "k1-tax-basis-capital.csv");
|
||||
let pnl = pnl_depreciation(l, end);
|
||||
assert!(
|
||||
tax.contains(&format!(
|
||||
"the book depreciation posted ({} in the span",
|
||||
ledger_package::money(pnl)
|
||||
)),
|
||||
"{end}: {tax}"
|
||||
);
|
||||
// The 1099 worksheet is books.vendor_1099's CSV through as_of.
|
||||
let w =
|
||||
ledger_package::vendor_1099_worksheet(l, &settings(&b), 2026, None, Some(end), None)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
file(&p, "1099-worksheet-2026.csv"),
|
||||
ledger_package::vendor_1099_csv(&w, false)
|
||||
);
|
||||
// The IIF is books.export's as of the day, and holds nothing after.
|
||||
let iif = ledger_package::export_iif(l, &b.cash, end).unwrap();
|
||||
assert_eq!(file(&p, "general-journal.iif"), iif.text);
|
||||
assert!(iif_dates(&iif.text).iter().all(|x| *x <= end));
|
||||
// The fixed asset register is in it: both assets were bought by
|
||||
// every date.
|
||||
assert!(
|
||||
p.files
|
||||
.iter()
|
||||
.any(|(n, _)| n == "fixed-asset-register.xlsx")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
//! The P3 seam review's books (shared by `ledger-package`'s
|
||||
//! `tests/p3_seams.rs`, `ledger-gui`'s `tests/p3_seams.rs` and
|
||||
//! `ledger-mcp`'s `tests/p3_seams.rs`, which include this file): one year
|
||||
//! of fictional books for a two-member LLC, Ana Example and Ben Example,
|
||||
//! 50/50, built through the real [`Store`], with the settings the server
|
||||
//! and the viewer read written beside them (`wanderbooks.toml`). Every
|
||||
//! name and amount is made up.
|
||||
//!
|
||||
//! What happens, by month (2026):
|
||||
//! - Jan: the members contribute 7,500.00 each in cash.
|
||||
//! - Feb: Ana contributes her personal laptop (value 1,800.00, her basis
|
||||
//! 2,400.00: a personal-use conversion, D-41); Delta Studio LLC, a
|
||||
//! 1099-NEC contractor, is paid 1,500.00 by check.
|
||||
//! - Mar: Delta 700.00 on the business card; a year of insurance,
|
||||
//! 1,095.00, prepaid from March 15 on a daily schedule; supplies
|
||||
//! 300.25 on the card; Ana pays a 120.00 subscription on her own card
|
||||
//! (through Due to Members).
|
||||
//! - Apr: an 8,000.00 bank loan; a 40.00 refund from the supplies
|
||||
//! vendor; the card paid, 1,000.25; a camera bought, 3,600.00, in
|
||||
//! service April 15; Ana reimbursed.
|
||||
//! - May: Ben pays Delta 900.00 with his own check (Due to Members).
|
||||
//! - Jun: a loan payment; Ben reimbursed; May's camera run reversed on
|
||||
//! June 3 (caught up by July's run); a 2,000.00 distribution declared
|
||||
//! to Distributions Payable on June 30.
|
||||
//! - Jul: the distribution paid; a loan payment.
|
||||
//! - Aug 1: Delta elects corporate taxation (not reportable from then).
|
||||
//! - Sep: Delta paid 1,200.00 by check.
|
||||
//! - Nov: November's camera run made, then the camera sold on November 20
|
||||
//! for 2,900.00 (the disposal credits back the run's last ten days).
|
||||
//! - Dec: a 1,000.00 distribution paid directly.
|
||||
//! - Every month: rent 450.00, revenue with odd cents, depreciation, the
|
||||
//! insurance schedule, and a close to the members.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::settings::{Company, Member, Source};
|
||||
use ledger_core::{
|
||||
AccountId, AccountType, AssetBasis, BasisRule, CloseTo, Currency, Dated1099, Decision1099,
|
||||
Form1099, InstrumentKind, MemberClose, Money, NewAsset, NewSchedule, PartyId, PartyShare, Role,
|
||||
ScheduleConvention, ScheduleKind, Sec704cMethod, Side,
|
||||
};
|
||||
use ledger_store::Store;
|
||||
use rust_decimal::Decimal;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn d(s: &str) -> NaiveDate {
|
||||
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
|
||||
}
|
||||
|
||||
pub fn dec(s: &str) -> Decimal {
|
||||
Decimal::from_str(s).unwrap()
|
||||
}
|
||||
|
||||
pub fn usd(s: &str) -> Money {
|
||||
Money::new(dec(s), Currency::USD)
|
||||
}
|
||||
|
||||
fn src(on: &str) -> Source {
|
||||
Source {
|
||||
decided_by: "Ana Example".into(),
|
||||
decided_on: d(on),
|
||||
citation: None,
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const CASH: &str = "1000";
|
||||
pub const FINANCING: [&str; 3] = ["2050", "2100", "2300"];
|
||||
|
||||
/// The scenario's books and the ids the checks need.
|
||||
pub struct Books {
|
||||
pub store: Store,
|
||||
pub company: Company,
|
||||
pub cash: Vec<String>,
|
||||
pub financing: Vec<String>,
|
||||
pub ana: PartyId,
|
||||
pub ben: PartyId,
|
||||
pub delta: u64,
|
||||
pub delta_party: PartyId,
|
||||
pub paper_party: PartyId,
|
||||
pub laptop: u64,
|
||||
pub camera: u64,
|
||||
}
|
||||
|
||||
pub fn id(s: &Store, code: &str) -> AccountId {
|
||||
s.ledger().chart().find_by_code(code).unwrap().id
|
||||
}
|
||||
|
||||
fn post(s: &mut Store, date: &str, desc: &str, lines: &[(&str, Side, &str)]) -> u64 {
|
||||
let e = s.create_entry(d(date), desc).unwrap();
|
||||
for (code, side, a) in lines {
|
||||
let acct = id(s, code);
|
||||
s.add_line(e, acct, *side, usd(a)).unwrap();
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
fn done(s: &mut Store, e: u64) -> u64 {
|
||||
s.post_entry(e).unwrap();
|
||||
e
|
||||
}
|
||||
|
||||
fn with_party(s: &mut Store, e: u64, p: PartyId) -> u64 {
|
||||
s.set_entry_party(e, Some(p)).unwrap();
|
||||
done(s, e)
|
||||
}
|
||||
|
||||
fn with_vendor(s: &mut Store, e: u64, v: u64) -> u64 {
|
||||
s.set_entry_vendor(e, Some(v)).unwrap();
|
||||
done(s, e)
|
||||
}
|
||||
|
||||
fn both(s: &mut Store, e: u64, ana: PartyId, ben: PartyId, each: &str) -> u64 {
|
||||
let share = ledger_core::shares::Share::Amount(dec(each));
|
||||
s.set_entry_parties(
|
||||
e,
|
||||
&[
|
||||
PartyShare { party: ana, share },
|
||||
PartyShare { party: ben, share },
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
done(s, e)
|
||||
}
|
||||
|
||||
/// The settings the server and the viewer read for these books (beside
|
||||
/// `books-data`, as an older books dir keeps them): [`company`] and the
|
||||
/// account roles, the receipt threshold high so no entry needs a receipt.
|
||||
pub const SETTINGS_TOML: &str = "[company]
|
||||
name = \"Example Trails LLC\"
|
||||
basis = \"Cash\"
|
||||
fiscal_year_start = \"01-01\"
|
||||
|
||||
[[company.members]]
|
||||
name = \"Ana Example\"
|
||||
share = \"50%\"
|
||||
capital_account = \"3000\"
|
||||
distribution_account = \"3010\"
|
||||
files_jointly_with = \"Ben Example\"
|
||||
|
||||
[[company.members]]
|
||||
name = \"Ben Example\"
|
||||
share = \"50%\"
|
||||
capital_account = \"3001\"
|
||||
distribution_account = \"3011\"
|
||||
files_jointly_with = \"Ana Example\"
|
||||
|
||||
[books]
|
||||
receipt_threshold = \"1000000.00\"
|
||||
cash_accounts = [\"1000\"]
|
||||
financing_accounts = [\"2050\", \"2100\", \"2300\"]
|
||||
";
|
||||
|
||||
/// The company as [`SETTINGS_TOML`] gives it.
|
||||
pub fn company() -> Company {
|
||||
let member = |name: &str, capital: &str, draws: &str, other: &str| Member {
|
||||
name: name.into(),
|
||||
share: dec("0.5"),
|
||||
capital_account: capital.into(),
|
||||
distribution_account: Some(draws.into()),
|
||||
files_jointly_with: Some(other.into()),
|
||||
};
|
||||
Company {
|
||||
name: "Example Trails LLC".into(),
|
||||
basis: "Cash".into(),
|
||||
members: vec![
|
||||
member("Ana Example", "3000", "3010", "Ben Example"),
|
||||
member("Ben Example", "3001", "3011", "Ana Example"),
|
||||
],
|
||||
..Company::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub const MONTHS: [&str; 12] = [
|
||||
"2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07", "2026-08",
|
||||
"2026-09", "2026-10", "2026-11", "2026-12",
|
||||
];
|
||||
|
||||
pub fn month_end(m: &str) -> NaiveDate {
|
||||
let first = d(&format!("{m}-01"));
|
||||
let next = first.checked_add_months(chrono::Months::new(1)).unwrap();
|
||||
next.pred_opt().unwrap()
|
||||
}
|
||||
|
||||
pub fn books(dir: &std::path::Path) -> Books {
|
||||
let mut s = Store::create(&dir.join("books-data"), Currency::USD).unwrap();
|
||||
for (code, name, ty) in [
|
||||
("1000", "Checking", AccountType::Asset),
|
||||
("1300", "Prepaid Insurance", AccountType::Asset),
|
||||
("1500", "Camera", AccountType::Asset),
|
||||
(
|
||||
"1510",
|
||||
"Accumulated Depreciation - Camera",
|
||||
AccountType::Asset,
|
||||
),
|
||||
("1520", "Laptop", AccountType::Asset),
|
||||
(
|
||||
"1530",
|
||||
"Accumulated Depreciation - Laptop",
|
||||
AccountType::Asset,
|
||||
),
|
||||
("2050", "Business Card", AccountType::Liability),
|
||||
("2100", "Due to Members", AccountType::Liability),
|
||||
("2200", "Distributions Payable", AccountType::Liability),
|
||||
("2300", "Bank Loan", AccountType::Liability),
|
||||
("3000", "Capital - Ana", AccountType::Equity),
|
||||
("3001", "Capital - Ben", AccountType::Equity),
|
||||
("3010", "Distributions - Ana", AccountType::Equity),
|
||||
("3011", "Distributions - Ben", AccountType::Equity),
|
||||
("4000", "Guiding Revenue", AccountType::Revenue),
|
||||
("6000", "Rent", AccountType::Expense),
|
||||
("6150", "Software", AccountType::Expense),
|
||||
("6200", "Supplies", AccountType::Expense),
|
||||
("6300", "Insurance", AccountType::Expense),
|
||||
("6400", "Contract Labor", AccountType::Expense),
|
||||
("6500", "Depreciation", AccountType::Expense),
|
||||
("6900", "Interest Expense", AccountType::Expense),
|
||||
("7000", "Gain or Loss on Disposal", AccountType::Revenue),
|
||||
] {
|
||||
s.add_account(code, name, ty, None, false).unwrap();
|
||||
}
|
||||
let ana = s.add_party("Ana Example", &[Role::Member]).unwrap();
|
||||
let ben = s.add_party("Ben Example", &[Role::Member]).unwrap();
|
||||
for m in [ana, ben] {
|
||||
s.set_party_creditor(m, id(&s, "2100"), true).unwrap();
|
||||
}
|
||||
let client = s.add_customer("Summit Client Co").unwrap();
|
||||
let client = s.ledger().party_of_customer(client).unwrap();
|
||||
let lender = s
|
||||
.add_party("Example Savings Bank", &[Role::FinancialInstitution])
|
||||
.unwrap();
|
||||
s.set_party_creditor(lender, id(&s, "2300"), true).unwrap();
|
||||
let delta = s.add_vendor("Delta Studio LLC").unwrap();
|
||||
let delta_party = s.ledger().party_of_vendor(delta).unwrap();
|
||||
// Delta files a 1099-NEC, then elects corporate taxation effective
|
||||
// August 1 (Form 8832): not reportable from that day (D-40).
|
||||
s.set_vendor_1099(
|
||||
delta,
|
||||
&Dated1099::from_beginning(Decision1099::Files(Form1099::Nec), Some(src("2026-01-02"))),
|
||||
Some(true),
|
||||
)
|
||||
.unwrap();
|
||||
s.set_vendor_1099(
|
||||
delta,
|
||||
&Dated1099 {
|
||||
effective: Some(d("2026-08-01")),
|
||||
decision: Decision1099::not_reportable(
|
||||
ledger_core::ExemptReason::Corporation,
|
||||
"elected corporate taxation, Form 8832 effective 2026-08-01",
|
||||
)
|
||||
.unwrap(),
|
||||
source: Some(src("2026-08-03")),
|
||||
},
|
||||
Some(true),
|
||||
)
|
||||
.unwrap();
|
||||
let paper = s.add_vendor("Paper Goods Co").unwrap();
|
||||
let paper_party = s.ledger().party_of_vendor(paper).unwrap();
|
||||
let cloud = s.add_vendor("Cloud Tools Inc").unwrap();
|
||||
let lens = s.add_vendor("Lens Shop Inc").unwrap();
|
||||
let insurer = s.add_vendor("Safe Insurance Inc").unwrap();
|
||||
let landlord = s.add_vendor("Trailhead Rentals LLC").unwrap();
|
||||
|
||||
let card = s
|
||||
.add_instrument(
|
||||
InstrumentKind::BusinessCard,
|
||||
"Business card",
|
||||
Some("4242"),
|
||||
None,
|
||||
id(&s, "2050"),
|
||||
)
|
||||
.unwrap();
|
||||
let ana_card = s
|
||||
.add_instrument(
|
||||
InstrumentKind::MemberCard,
|
||||
"Ana's personal card",
|
||||
Some("1111"),
|
||||
Some(ana),
|
||||
id(&s, "2100"),
|
||||
)
|
||||
.unwrap();
|
||||
let ben_check = s
|
||||
.add_instrument(
|
||||
InstrumentKind::MemberCashOrCheck,
|
||||
"Ben's personal checks",
|
||||
None,
|
||||
Some(ben),
|
||||
id(&s, "2100"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let members: Vec<MemberClose> = company()
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| MemberClose {
|
||||
capital: id(&s, &m.capital_account),
|
||||
share: m.share,
|
||||
distributions: m.distribution_account.as_deref().map(|c| id(&s, c)),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut laptop = 0;
|
||||
let mut camera = 0;
|
||||
// Odd-cent revenue so the closes split odd cents.
|
||||
let revenue = [
|
||||
"2100.01", "1850.37", "2433.33", "2950.05", "3100.11", "3333.33", "2875.99", "2600.01",
|
||||
"2999.97", "3050.03", "2200.21", "1999.99",
|
||||
];
|
||||
for (i, m) in MONTHS.iter().enumerate() {
|
||||
let end = month_end(m);
|
||||
let day = |n: u32| format!("{m}-{n:02}");
|
||||
match i {
|
||||
0 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(5),
|
||||
"Members' opening contributions",
|
||||
&[
|
||||
("1000", Side::Debit, "15000.00"),
|
||||
("3000", Side::Credit, "7500.00"),
|
||||
("3001", Side::Credit, "7500.00"),
|
||||
],
|
||||
);
|
||||
both(&mut s, e, ana, ben, "7500.00");
|
||||
}
|
||||
1 => {
|
||||
// Ana contributes the laptop she used personally (D-41).
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(1),
|
||||
"Ana contributes her laptop",
|
||||
&[
|
||||
("1520", Side::Debit, "1800.00"),
|
||||
("3000", Side::Credit, "1800.00"),
|
||||
],
|
||||
);
|
||||
let e = with_party(&mut s, e, ana);
|
||||
laptop = s
|
||||
.register_asset(NewAsset {
|
||||
description: "Laptop".into(),
|
||||
in_service_date: d("2026-02-01"),
|
||||
cost: usd("1800.00"),
|
||||
salvage_value: usd("0.00"),
|
||||
useful_life_months: 36,
|
||||
cost_account: id(&s, "1520"),
|
||||
accum_depr_account: id(&s, "1530"),
|
||||
expense_account: id(&s, "6500"),
|
||||
})
|
||||
.unwrap();
|
||||
s.set_asset_acquisition(laptop, e).unwrap();
|
||||
s.set_asset_basis(
|
||||
laptop,
|
||||
AssetBasis {
|
||||
rule: BasisRule::PersonalUseConversion,
|
||||
contributor_adjusted_basis: Some(dec("2400.00")),
|
||||
fmv_at_contribution: Some(dec("1800.00")),
|
||||
sec704c_method: Some(Sec704cMethod::Traditional),
|
||||
source: Some(src("2026-02-01")),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// Delta paid by check from the bank: no method recorded,
|
||||
// presumed a check (D-40).
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(10),
|
||||
"Delta Studio: trail maps",
|
||||
&[
|
||||
("6400", Side::Debit, "1500.00"),
|
||||
("1000", Side::Credit, "1500.00"),
|
||||
],
|
||||
);
|
||||
with_vendor(&mut s, e, delta);
|
||||
}
|
||||
2 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(12),
|
||||
"Delta Studio: logo work, on the card",
|
||||
&[
|
||||
("6400", Side::Debit, "700.00"),
|
||||
("2050", Side::Credit, "700.00"),
|
||||
],
|
||||
);
|
||||
s.set_entry_instrument(e, Some(card)).unwrap();
|
||||
with_vendor(&mut s, e, delta);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(15),
|
||||
"Liability insurance, a year from March 15",
|
||||
&[
|
||||
("1300", Side::Debit, "1095.00"),
|
||||
("1000", Side::Credit, "1095.00"),
|
||||
],
|
||||
);
|
||||
with_vendor(&mut s, e, insurer);
|
||||
s.register_schedule(NewSchedule {
|
||||
description: "Liability insurance".into(),
|
||||
kind: ScheduleKind::PrepaidExpense,
|
||||
total: usd("1095.00"),
|
||||
start: d("2026-03-15"),
|
||||
months: 12,
|
||||
balance_account: id(&s, "1300"),
|
||||
pl_account: id(&s, "6300"),
|
||||
convention: ScheduleConvention::Daily,
|
||||
})
|
||||
.unwrap();
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(20),
|
||||
"Paper Goods: maps paper, on the card",
|
||||
&[
|
||||
("6200", Side::Debit, "300.25"),
|
||||
("2050", Side::Credit, "300.25"),
|
||||
],
|
||||
);
|
||||
s.set_entry_instrument(e, Some(card)).unwrap();
|
||||
with_vendor(&mut s, e, paper);
|
||||
// Ana pays a software subscription on her own card.
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(22),
|
||||
"Cloud Tools: route planner, Ana's card",
|
||||
&[
|
||||
("6150", Side::Debit, "120.00"),
|
||||
("2100", Side::Credit, "120.00"),
|
||||
],
|
||||
);
|
||||
s.set_entry_instrument(e, Some(ana_card)).unwrap();
|
||||
with_vendor(&mut s, e, cloud);
|
||||
}
|
||||
3 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(1),
|
||||
"Equipment loan",
|
||||
&[
|
||||
("1000", Side::Debit, "8000.00"),
|
||||
("2300", Side::Credit, "8000.00"),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, lender);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(5),
|
||||
"Paper Goods refund: damaged paper",
|
||||
&[
|
||||
("1000", Side::Debit, "40.00"),
|
||||
("6200", Side::Credit, "40.00"),
|
||||
],
|
||||
);
|
||||
with_vendor(&mut s, e, paper);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(8),
|
||||
"Card payment",
|
||||
&[
|
||||
("2050", Side::Debit, "1000.25"),
|
||||
("1000", Side::Credit, "1000.25"),
|
||||
],
|
||||
);
|
||||
s.set_entry_no_party(e, "paying the company's own card")
|
||||
.unwrap();
|
||||
done(&mut s, e);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(10),
|
||||
"Lens Shop: trail camera",
|
||||
&[
|
||||
("1500", Side::Debit, "3600.00"),
|
||||
("1000", Side::Credit, "3600.00"),
|
||||
],
|
||||
);
|
||||
let e = with_vendor(&mut s, e, lens);
|
||||
camera = s
|
||||
.register_asset(NewAsset {
|
||||
description: "Trail camera".into(),
|
||||
in_service_date: d("2026-04-15"),
|
||||
cost: usd("3600.00"),
|
||||
salvage_value: usd("0.00"),
|
||||
useful_life_months: 48,
|
||||
cost_account: id(&s, "1500"),
|
||||
accum_depr_account: id(&s, "1510"),
|
||||
expense_account: id(&s, "6500"),
|
||||
})
|
||||
.unwrap();
|
||||
s.set_asset_acquisition(camera, e).unwrap();
|
||||
s.set_asset_basis(
|
||||
camera,
|
||||
AssetBasis {
|
||||
rule: BasisRule::Purchase,
|
||||
contributor_adjusted_basis: None,
|
||||
fmv_at_contribution: None,
|
||||
sec704c_method: None,
|
||||
source: Some(src("2026-04-10")),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(25),
|
||||
"Ana reimbursed for the route planner",
|
||||
&[
|
||||
("2100", Side::Debit, "120.00"),
|
||||
("1000", Side::Credit, "120.00"),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, ana);
|
||||
}
|
||||
4 => {
|
||||
// Ben pays Delta with his own check, at the company's
|
||||
// direction: the company is the payor (B-19).
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(20),
|
||||
"Delta Studio: guidebook layout, Ben's check",
|
||||
&[
|
||||
("6400", Side::Debit, "900.00"),
|
||||
("2100", Side::Credit, "900.00"),
|
||||
],
|
||||
);
|
||||
s.set_entry_instrument(e, Some(ben_check)).unwrap();
|
||||
with_vendor(&mut s, e, delta);
|
||||
}
|
||||
5 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(1),
|
||||
"Loan payment",
|
||||
&[
|
||||
("2300", Side::Debit, "300.00"),
|
||||
("6900", Side::Debit, "40.00"),
|
||||
("1000", Side::Credit, "340.00"),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, lender);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(15),
|
||||
"Ben reimbursed for the guidebook layout",
|
||||
&[
|
||||
("2100", Side::Debit, "900.00"),
|
||||
("1000", Side::Credit, "900.00"),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, ben);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(30),
|
||||
"Distribution declared",
|
||||
&[
|
||||
("3010", Side::Debit, "1000.00"),
|
||||
("3011", Side::Debit, "1000.00"),
|
||||
("2200", Side::Credit, "2000.00"),
|
||||
],
|
||||
);
|
||||
both(&mut s, e, ana, ben, "1000.00");
|
||||
}
|
||||
6 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(10),
|
||||
"Distribution paid",
|
||||
&[
|
||||
("2200", Side::Debit, "2000.00"),
|
||||
("1000", Side::Credit, "2000.00"),
|
||||
],
|
||||
);
|
||||
both(&mut s, e, ana, ben, "1000.00");
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(1),
|
||||
"Loan payment",
|
||||
&[
|
||||
("2300", Side::Debit, "302.00"),
|
||||
("6900", Side::Debit, "38.00"),
|
||||
("1000", Side::Credit, "340.00"),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, lender);
|
||||
}
|
||||
8 => {
|
||||
// After Delta's election: not reportable (D-40).
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(10),
|
||||
"Delta Studio: signage",
|
||||
&[
|
||||
("6400", Side::Debit, "1200.00"),
|
||||
("1000", Side::Credit, "1200.00"),
|
||||
],
|
||||
);
|
||||
with_vendor(&mut s, e, delta);
|
||||
}
|
||||
10 => {
|
||||
// November's run is made before the camera's sale is
|
||||
// recorded: the disposal takes back the days after it.
|
||||
s.run_depreciation(camera, m).unwrap();
|
||||
let e = s
|
||||
.dispose_asset(
|
||||
camera,
|
||||
d("2026-11-20"),
|
||||
usd("2900.00"),
|
||||
id(&s, "1000"),
|
||||
id(&s, "7000"),
|
||||
)
|
||||
.unwrap();
|
||||
let buyer = lens_buyer(&mut s);
|
||||
s.set_entry_party(e, Some(buyer)).unwrap();
|
||||
}
|
||||
11 => {
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(15),
|
||||
"Year-end distribution",
|
||||
&[
|
||||
("3010", Side::Debit, "500.00"),
|
||||
("3011", Side::Debit, "500.00"),
|
||||
("1000", Side::Credit, "1000.00"),
|
||||
],
|
||||
);
|
||||
both(&mut s, e, ana, ben, "500.00");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let e = post(
|
||||
&mut s,
|
||||
&day(3),
|
||||
"Trail office rent",
|
||||
&[
|
||||
("6000", Side::Debit, "450.00"),
|
||||
("1000", Side::Credit, "450.00"),
|
||||
],
|
||||
);
|
||||
with_vendor(&mut s, e, landlord);
|
||||
let e = post(
|
||||
&mut s,
|
||||
&end.to_string(),
|
||||
"Guided hikes",
|
||||
&[
|
||||
("1000", Side::Debit, revenue[i]),
|
||||
("4000", Side::Credit, revenue[i]),
|
||||
],
|
||||
);
|
||||
with_party(&mut s, e, client);
|
||||
// Month end: depreciation, the schedules, the close.
|
||||
for asset in [laptop, camera] {
|
||||
if asset == 0 {
|
||||
continue;
|
||||
}
|
||||
let a = s.ledger().get_asset(asset).unwrap().clone();
|
||||
if matches!(a.status, ledger_core::AssetStatus::Disposed { .. }) {
|
||||
continue;
|
||||
}
|
||||
if s.ledger().live_depreciation_run(asset, m).is_none() {
|
||||
s.run_depreciation(asset, m).unwrap();
|
||||
}
|
||||
}
|
||||
if i == 5 {
|
||||
let may = s
|
||||
.ledger()
|
||||
.live_depreciation_run(camera, "2026-05")
|
||||
.unwrap()
|
||||
.entry;
|
||||
s.reverse_entry(
|
||||
may,
|
||||
d("2026-06-03"),
|
||||
"May's camera run reversed: wrong asset",
|
||||
)
|
||||
.unwrap();
|
||||
// June's run (made above) came first; reversing May leaves
|
||||
// May's depreciation for the next run to catch up.
|
||||
}
|
||||
s.run_schedules(end).unwrap();
|
||||
let p = s.create_period(m, d(&format!("{m}-01")), end).unwrap();
|
||||
s.close_period_to(p, &CloseTo::Members(members.clone()))
|
||||
.unwrap();
|
||||
}
|
||||
std::fs::write(dir.join("wanderbooks.toml"), SETTINGS_TOML).unwrap();
|
||||
Books {
|
||||
store: s,
|
||||
company: company(),
|
||||
cash: vec![CASH.to_string()],
|
||||
financing: FINANCING.iter().map(|c| c.to_string()).collect(),
|
||||
ana,
|
||||
ben,
|
||||
delta,
|
||||
delta_party,
|
||||
paper_party,
|
||||
laptop,
|
||||
camera,
|
||||
}
|
||||
}
|
||||
|
||||
fn lens_buyer(s: &mut Store) -> PartyId {
|
||||
let c = s.add_customer("Ridge Outfitters Inc").unwrap();
|
||||
s.ledger().party_of_customer(c).unwrap()
|
||||
}
|
||||
Reference in new issue
Block a user