P3: the cash flow statement by ASC 230 (defect 1, D-35, D-42)
check / check (push) Canceled after 0s

Direct method with the operating reconciliation and noncash schedules; it
foots by construction (Lean and Kani). Every non-cash account belongs to
one section; reversals net; a member or lender paying for the company is
cash borrowed and spent (D-42, constructive receipt and disbursement);
property a member contributes stays noncash. The bookspec seed: one loader
checks every example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 09:35:32 -06:00
1 parent 2c29d48ab9
commit eb4329bb2a
64 files changed
+12128 -1567

No files matched your search

+13 -13
View File
@@ -170,19 +170,19 @@ pub fn build_package<B: PackageBooks + ?Sized>(
),
(
"cash-flow",
report::cash_flow(
&cash_flow_for(
ledger,
settings,
Some(&fy_start),
Some(&as_of_s),
req.cash_accounts,
req.financing_accounts,
req.investing_accounts,
today,
)?,
&members,
),
// The package's one list of each (INVARIANT 7); investing
// accounts are the cash flow's alone (the request's, else the
// settings').
report::cash_flow(&cash_flow_for(
ledger,
settings,
Some(&fy_start),
Some(&as_of_s),
cash_codes,
financing_codes,
req.investing_accounts,
today,
)?),
),
];
let mut k1_csv: Option<(String, Vec<u8>)> = None;
+28 -1
View File
@@ -13,7 +13,12 @@
//! refused.
//! 2. The cash flow statement needs at least one cash account, and
//! every code it is given must be in the chart; account lists a call
//! leaves empty default to the settings' roles.
//! leaves empty default to the settings' roles. It foots, or it is
//! refused (an engine error, never a figure: rules.md B-14 rule 9);
//! roles that contradict each other or the chart are refused by the
//! engine, naming the setting, so the viewer and the server refuse
//! alike. Members are named on their contributions and
//! distributions (D-35).
//! 3. The year's earnings split net income year to date from the part
//! not yet closed into equity (closes are cumulative).
//! ```
@@ -98,6 +103,9 @@ pub fn bs_statement(
/// Build the cash flow statement from account codes, as the exports need it:
/// `start` defaults to the beginning of time, `end` to `today`, and at
/// least one cash account is required (the statement reconciles to them).
/// A member whose capital account the chart lacks is left unnamed (equity
/// is then named by account), never a refusal of the statement.
#[allow(clippy::too_many_arguments)]
pub fn cash_flow_statement(
ledger: &Ledger,
start: Option<&str>,
@@ -105,6 +113,7 @@ pub fn cash_flow_statement(
cash_codes: &[String],
financing_codes: &[String],
investing_codes: &[String],
members: &[Member],
today: NaiveDate,
) -> Result<ledger_core::reports::CashFlowStatement, String> {
let resolve = |codes: &[String], what: &str| -> Result<Vec<AccountId>, String> {
@@ -129,6 +138,22 @@ pub fn cash_flow_statement(
let financing_accounts = resolve(financing_codes, "financing")?;
let investing_accounts = resolve(investing_codes, "investing")?;
let (start, end) = statement_range(ledger, start, end, today)?;
let chart = ledger.chart();
let members = members
.iter()
.filter_map(|m| {
Some(ledger_core::MemberCapital {
name: m.name.clone(),
capital_account: chart.find_by_code(&m.capital_account)?.id,
distribution_account: m
.distribution_account
.as_deref()
.and_then(|c| chart.find_by_code(c))
.map(|a| a.id),
share: m.share,
})
})
.collect();
ledger_core::cash_flow(
ledger,
&ledger_core::CashFlowParams {
@@ -137,6 +162,7 @@ pub fn cash_flow_statement(
cash_accounts,
financing_accounts,
investing_accounts,
members,
},
)
.map_err(|e| e.to_string())
@@ -169,6 +195,7 @@ pub fn cash_flow_for(
&or(cash, settings.cash_accounts),
&or(financing, settings.financing_accounts),
&or(investing, settings.investing_accounts),
&settings.company.members,
today,
)
}
+2 -2
View File
@@ -19,7 +19,7 @@ use chrono::NaiveDate;
use rust_decimal::Decimal;
/// A money amount as a decimal string with at least two places: "0.00",
/// "207.68", "1000.00". Padding only, never rounding: an exact amount with
/// "318.42", "1000.00". Padding only, never rounding: an exact amount with
/// more places (an FX-converted line) keeps them. Never "-0".
pub fn money(n: Decimal) -> String {
let mut n = if n.is_zero() { Decimal::ZERO } else { n };
@@ -89,7 +89,7 @@ mod tests {
assert_eq!(m("-0"), "0.00");
assert_eq!(m("1000"), "1000.00");
assert_eq!(m("5.8"), "5.80");
assert_eq!(m("-207.68"), "-207.68");
assert_eq!(m("-318.42"), "-318.42");
assert_eq!(m("12.3456"), "12.3456");
}
+46 -7
View File
@@ -1734,16 +1734,23 @@ fn the_readme_names_the_entries_on_receivables_and_payables() {
assert!(!text(&p, "README.txt").contains("customer or vendor"));
}
/// An XLSX's shared strings, as text.
/// Every text the package's XLSX `name` holds: its XML parts (the shared
/// strings and the sheets), concatenated, `&amp;` read as `&`.
fn xlsx_strings(p: &Package, name: &str) -> String {
let (_, bytes) = p.files.iter().find(|(n, _)| n == name).unwrap();
let (_, bytes) = p
.files
.iter()
.find(|(n, _)| n == name)
.unwrap_or_else(|| panic!("{name} in {:?}", names(p)));
let mut x = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
let mut out = String::new();
x.by_name("xl/sharedStrings.xml")
.unwrap()
.read_to_string(&mut out)
.unwrap();
out
for i in 0..x.len() {
let mut part = x.by_index(i).unwrap();
if part.name().unwrap().ends_with(".xml") {
part.read_to_string(&mut out).unwrap();
}
}
out.replace("&amp;", "&")
}
/// Random books for [`iif_ties_to_the_trial_balance_as_of`]: entries of
@@ -1947,3 +1954,35 @@ fn the_bookspec_iif_example_matches_the_engine() {
}
assert_eq!(checked, 2);
}
/// The package's statement of cash flows is the direct method's (rules.md
/// B-14, D-35): operating by ASC 230-10-45-25 class, financing by kind
/// with each member named, and both schedules: the reconciliation of net
/// income to operating cash (45-30) and the noncash investing and
/// financing activities (50-3; "None" here: every entry moved cash).
#[test]
fn the_cash_flow_carries_both_schedules() {
let dir = tempfile::tempdir().unwrap();
let b = books(dir.path());
let company = company(true);
let cash = cash();
let p = build_package(&b.store, &settings(&company, &cash), &request(true, None)).unwrap();
let text = xlsx_strings(&p, "cash-flow.xlsx");
for m in &company.members {
let want = format!("Contributions from members — {}", m.name);
assert!(text.contains(&want), "{want:?} not in the cash flow");
}
for want in [
"Statement of Cash Flows",
"Cash received from customers",
"Cash paid to suppliers and employees",
"Members' contributions (#",
"Reconciliation of net income (loss) to net cash from operating activities",
"Noncash investing and financing activities",
"None",
] {
assert!(text.contains(want), "{want:?} not in the cash flow");
}
assert!(!text.contains("Member distributions"));
assert!(p.files.iter().any(|(n, _)| n == "cash-flow.pdf"));
}