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

File diff suppressed because it is too large. Load diff
+23
View File
@@ -397,6 +397,29 @@ pub enum LedgerError {
end: String,
},
/// The statement of cash flows would not foot (rules.md B-14 rule 9):
/// an engine error, never output. It cannot happen on balanced books
/// (Lean `cashflow_sections_sum_to_cash_change`); if it does, the
/// engine is wrong, and nothing is shown rather than a wrong figure.
#[error(
"engine error: the cash flow statement does not foot ({what}: {found} against {expected}); nothing was produced, please report it"
)]
CashFlowDoesNotFoot {
/// Which check failed: "sections", "investing lines", "operating
/// reconciliation", "entry #N", ...
what: String,
/// The figure it should equal.
expected: Decimal,
/// The figure it came to.
found: Decimal,
},
/// The cash flow statement's account roles (cash, financing, investing)
/// contradict each other or the chart (rules.md B-14 rule 2). The text
/// names the setting and says what to change.
#[error("cash flow account roles: {0}")]
CashFlowRoles(String),
/// The referenced accounting period does not exist.
#[error("unknown period id {0}")]
UnknownPeriod(u64),
+348 -1
View File
@@ -29,7 +29,14 @@
//! remainder (B-13);
//! * a cumulative allocation hands out exactly the new amount, and leaves
//! each running total one allocation of everything allocated, so it never
//! drifts a unit from its exact share (B-13).
//! drifts a unit from its exact share (B-13);
//! * an entry's cash is applied to the sections of the statement of cash
//! flows moving against it, in a fixed order, each up to its own
//! movement: the parts sum to the entry's cash, what no cash settled sums
//! to zero, and a reversal's parts are the original's negated (B-14);
//! * what a member or lender paid or received for the company, with no
//! cash account moving, is split among the kinds it settled, netting to
//! zero, and mirrored by a reversal (B-14 rule 11).
use crate::{AccountType, Side};
use rust_decimal::Decimal;
@@ -409,10 +416,350 @@ pub fn apportion_cumulative<T: Amount>(
Some(())
}
/// Where a line falls on the statement of cash flows (rules.md B-14 rule
/// 2): a cash account, or one of the three sections.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Flow {
/// A cash account: what the statement explains the change in.
Cash,
/// Operating activities.
Operating,
/// Investing activities.
Investing,
/// Financing activities.
Financing,
}
impl Flow {
/// The three sections, in the order cash is applied to them (rules.md
/// B-14 rule 3): operating, investing, financing.
pub const SECTIONS: [Flow; 3] = [Flow::Operating, Flow::Investing, Flow::Financing];
}
/// One entry's effect on the statement of cash flows.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EntryFlows<T> {
/// Net movement of the entry's cash lines, debit-positive: + is cash
/// in.
pub cash: T,
/// Cash per section, `[operating, investing, financing]`, + is in.
/// Sums to `cash`.
pub flows: [T; 3],
/// What no cash settled, per section, debit-positive: the section's
/// movement plus its cash. Sums to zero.
pub noncash: [T; 3],
}
/// The cash one section takes from what is left of an entry's cash
/// (`remaining`), given the section's net movement (debit-positive): only
/// a section moving against the cash takes any, and never more than its
/// own movement. Cash in (`remaining > 0`) settles credits (`movement <
/// 0`); cash out settles debits.
pub fn take<T: Amount>(remaining: T, movement: T) -> Option<T> {
let against = movement.checked_neg()?;
Some(if remaining > T::ZERO && movement < T::ZERO {
remaining.min(against)
} else if remaining < T::ZERO && movement > T::ZERO {
remaining.max(against)
} else {
T::ZERO
})
}
/// Apply `cash` to `movements` in order, each taking [`take`] of what is
/// left: `applied[k]` is the cash of part `k`. `None` when something is
/// left over (the movements against the cash are smaller than it), the
/// slices differ in length, or on overflow.
///
/// ```text
/// GOAL: split an entry's cash among the parts it settled, without
/// inventing or losing any.
/// SOURCE: rules.md B-14 rule 3 (ASC 230-10-50-5: only the cash portion
/// of a part-cash transaction is a cash flow; the order is a
/// CONVENTION, the ASC is silent).
/// INVARIANTS:
/// 1. On success the parts sum to `cash` exactly.
/// 2. Each part has the sign of `cash` (or is zero) and is no larger
/// than its own movement, which runs against the cash.
/// 3. Applying `-cash` to the negated movements gives the negated parts
/// (a reversal mirrors its original).
/// SCENARIOS:
/// 1. IF the movements against the cash sum to at least the cash:
/// - SHOULD succeed (a balanced entry always does)
/// 2. IF they don't: SHOULD return None, never a part out of thin air.
/// ```
pub fn apply_cash<T: Amount>(cash: T, movements: &[T], applied: &mut [T]) -> Option<()> {
if movements.len() != applied.len() {
return None;
}
let mut remaining = cash;
for (out, &movement) in applied.iter_mut().zip(movements) {
let part = take(remaining, movement)?;
*out = part;
remaining = remaining.checked_sub(part)?;
}
(remaining == T::ZERO).then_some(())
}
/// An entry's cash flows (rules.md B-14 rule 3): the net movement of its
/// cash lines, applied to the sections moving against it in the order
/// operating, investing, financing, each up to its own movement; what is
/// left of a section's movement is the part no cash settled. `None` when
/// the lines don't net to zero (never for a posted entry) or on overflow.
pub fn entry_flows<T: Amount>(lines: &[(Flow, Side, T)]) -> Option<EntryFlows<T>> {
let mut cash = T::ZERO;
let mut moved = [T::ZERO; 3];
for &(flow, side, amount) in lines {
let delta = signed(side, amount)?;
let slot = match flow {
Flow::Cash => &mut cash,
Flow::Operating => &mut moved[0],
Flow::Investing => &mut moved[1],
Flow::Financing => &mut moved[2],
};
*slot = slot.checked_add(delta)?;
}
// Balanced lines only: every part below rests on it.
let mut total = cash;
for &m in &moved {
total = total.checked_add(m)?;
}
if total != T::ZERO {
return None;
}
let mut flows = [T::ZERO; 3];
apply_cash(cash, &moved, &mut flows)?;
let mut noncash = [T::ZERO; 3];
for ((n, m), f) in noncash.iter_mut().zip(moved).zip(flows) {
*n = m.checked_add(f)?;
}
Some(EntryFlows {
cash,
flows,
noncash,
})
}
/// The credits and the debits among some movements (debit-positive):
/// `(Σ max(-m, 0), Σ max(m, 0))`.
fn credits_and_debits<T: Amount>(movements: &[T]) -> Option<(T, T)> {
let (mut credits, mut debits) = (T::ZERO, T::ZERO);
for &m in movements {
if m < T::ZERO {
credits = credits.checked_sub(m)?;
} else {
debits = debits.checked_add(m)?;
}
}
Some((credits, debits))
}
/// Which way money a member or a lender handled for the company went
/// (rules.md B-14 rule 11, D-42).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OnBehalf {
/// Paid for the company: sources credited (borrowed, contributed)
/// against uses debited (spent).
Paid,
/// Received for the company: sources debited (repaid, distributed)
/// against uses credited (collected).
Received,
}
impl OnBehalf {
/// The other way: what an entry's reversal does.
pub fn mirror(self) -> Self {
match self {
OnBehalf::Paid => OnBehalf::Received,
OnBehalf::Received => OnBehalf::Paid,
}
}
}
/// What a member or a lender paid, or received, for the company with no
/// cash account moving: a constructive receipt and disbursement (rules.md
/// B-14 rule 11, D-42). `sources` are what is left unsettled of the
/// entry's kinds that can pay or receive for the company (a financing
/// liability, a member's equity), `uses` of the kinds they can pay or
/// receive for, each debit-positive. [`OnBehalf::Paid`]: `amount =
/// min(source credits, use debits)`, cash in to the sources' credits and
/// out of the uses' debits (borrowed or contributed, then spent);
/// [`OnBehalf::Received`]: `amount = min(source debits, use credits)`,
/// cash in to the uses' credits and out of the sources' debits (collected,
/// then repaid or distributed). `from[i]` is source `i`'s cash (+ in) and
/// `to[j]` use `j`'s, each applied in order by [`apply_cash`]. `None` when
/// the slices differ in length, or on overflow.
///
/// ```text
/// GOAL: show money a member or lender handled for the company as the
/// company's own, borrowed (or contributed) and spent, without
/// inventing or losing a cent.
/// SOURCE: rules.md B-14 rule 11 (D-42; ASC 230 is silent, PwC Viewpoint
/// FSP 6.9.3.1, Deloitte DART Roadmap 7.2, BDO Blueprint 3.4).
/// INVARIANTS:
/// 1. The parts net to zero: Σ from + Σ to = 0, so the entry's cash
/// flows still sum to its cash.
/// 2. Each part settles its own movement, toward zero and never past it
/// (zero, or the opposite sign and no larger).
/// 3. The other way, minus the sources and minus the uses give minus
/// the parts (a reversal mirrors its original).
/// 4. As much is settled as can be: afterwards, paid, no source credit
/// is left beside a use debit; received, no source debit beside a use
/// credit.
/// ```
pub fn constructive<T: Amount>(
way: OnBehalf,
sources: &[T],
uses: &[T],
from: &mut [T],
to: &mut [T],
) -> Option<()> {
let (source_credits, source_debits) = credits_and_debits(sources)?;
let (use_credits, use_debits) = credits_and_debits(uses)?;
// The cash the sources take (+ in): what was paid, or minus what was
// received; the uses take the opposite.
let cash = match way {
OnBehalf::Paid => source_credits.min(use_debits),
OnBehalf::Received => source_debits.min(use_credits).checked_neg()?,
};
apply_cash(cash, sources, from)?;
apply_cash(cash.checked_neg()?, uses, to)
}
/// How much of an entry is an exchange inside investing and financing
/// that no cash settled (rules.md B-14 rule 7, ASC 230-10-50-4: debt
/// converted to equity, one asset traded for another). `debits` and
/// `credits` are the net debits and net credits of the entry's investing
/// and financing balance-sheet accounts (each account netted first; both
/// non-negative); `inflow` and `outflow` are the cash those two sections
/// took in and paid out (both non-negative; constructive cash included,
/// rule 11). Cash in settles credits and cash out settles debits; what's
/// left of the smaller side was exchanged:
/// `max(0, min(debits - outflow, credits - inflow))`.
pub fn exchanged<T: Amount>(debits: T, credits: T, inflow: T, outflow: T) -> Option<T> {
let d = debits.checked_sub(outflow)?;
let c = credits.checked_sub(inflow)?;
Some(d.min(c).max(T::ZERO))
}
#[cfg(test)]
mod tests {
use super::*;
/// rules.md B-14 rule 3, by hand. Equipment 1,000 for 300 cash and a
/// 700 note: investing takes the 300, the note's 700 and the rest of
/// the equipment's are the noncash part.
#[test]
fn part_cash_part_noncash_takes_only_the_cash() {
use Flow::*;
let f = entry_flows(&[
(Investing, Side::Debit, 1000i64),
(Cash, Side::Credit, 300),
(Financing, Side::Credit, 700),
])
.unwrap();
assert_eq!(f.cash, -300);
assert_eq!(f.flows, [0, -300, 0]);
assert_eq!(f.noncash, [0, 700, -700]);
// A fee withheld from a loan: Dr cash 9,900, Dr fee 100 / Cr loan
// 10,000. Operating moves with the cash (a debit), so takes none.
let f = entry_flows(&[
(Cash, Side::Debit, 9900i64),
(Operating, Side::Debit, 100),
(Financing, Side::Credit, 10000),
])
.unwrap();
assert_eq!(f.flows, [0, 0, 9900]);
assert_eq!(f.noncash, [100, 0, -100]);
}
/// Lines that don't net to zero are refused (review item 12): no part
/// is made up for them.
#[test]
fn unbalanced_lines_have_no_flows() {
use Flow::*;
assert_eq!(
entry_flows(&[(Operating, Side::Debit, 5i64), (Investing, Side::Debit, 5)]),
None
);
assert_eq!(
entry_flows(&[(Cash, Side::Debit, 5i64), (Investing, Side::Credit, 4)]),
None
);
// Too little against the cash: nothing is invented.
let mut out = [0i64; 2];
assert_eq!(apply_cash(10, &[-3, 4], &mut out), None);
assert_eq!(apply_cash(10, &[-3], &mut out), None);
}
/// The exchange measure (rules.md B-14 rule 7) on the review's
/// hand-checked cases.
#[test]
fn exchanged_matches_the_hand_checked_cases() {
// Debt to equity, 500.
assert_eq!(exchanged(500i64, 500, 0, 0), Some(500));
// Part-cash purchase with the seller's note: D 1,000, C 700, cash
// out 300.
assert_eq!(exchanged(1000i64, 700, 0, 300), Some(700));
// Note received for equipment: D 700, C 1,000, cash in 300.
assert_eq!(exchanged(700i64, 1000, 300, 0), Some(700));
// Trade-in with 1,500 boot: D 2,000, C 1,000, cash out 1,500.
assert_eq!(exchanged(2000i64, 1000, 0, 1500), Some(500));
// A cash sale: nothing debited, cash in 500.
assert_eq!(exchanged(0i64, 1000, 500, 0), Some(0));
// A cash purchase: cash out covers every debit.
assert_eq!(exchanged(1000i64, 0, 0, 1000), Some(0));
// Equipment a lender paid for (rule 11): 1,000 borrowed in, 1,000
// spent out, nothing exchanged.
assert_eq!(exchanged(1000i64, 1000, 1000, 1000), Some(0));
}
/// rules.md B-14 rule 11 (D-42), by hand.
#[test]
fn constructive_pays_and_receives_for_the_company() {
use OnBehalf::{Paid, Received};
// A member pays a 59 expense (Dr expense / Cr due to members): the
// source's 59 credit pays the use's 59 debit.
let (mut from, mut to) = ([0i64; 1], [0i64; 1]);
constructive(Paid, &[-59], &[59], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([59], [-59]));
// Its reversal, the other way, mirrors it.
constructive(Received, &[59], &[-59], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([-59], [59]));
// The reversal's shape read as a payment settles nothing.
constructive(Paid, &[59], &[-59], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([0], [0]));
// A 100 credit from the source against 60 of uses (the other 40
// against something else): only 60 is paid; uses take in order.
let (mut from, mut to) = ([0i64; 2], [0i64; 2]);
constructive(Paid, &[-100, 0], &[25, 35], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([60, 0], [-25, -35]));
// Two sources, one use smaller than both: the first source pays
// first.
constructive(Paid, &[-30, -30], &[40, 0], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([30, 10], [-40, 0]));
// Each way settles only its own side.
constructive(Paid, &[-50, 20], &[50, -20], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([50, 0], [-50, 0]));
constructive(Received, &[-50, 20], &[50, -20], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([0, -20], [0, 20]));
// Nothing against: nothing moves.
constructive(Paid, &[-50, 0], &[-10, 0], &mut from, &mut to).unwrap();
assert_eq!((from, to), ([0, 0], [0, 0]));
// Mismatched lengths are refused.
let one = [-1i64];
assert_eq!(
constructive(Paid, &one, &[1], &mut [0i64; 2], &mut [0]),
None
);
assert_eq!(
constructive(Paid, &one, &[1], &mut [0i64], &mut [0; 2]),
None
);
assert_eq!(Paid.mirror(), Received);
assert_eq!(Received.mirror(), Paid);
}
#[test]
fn max_line_amount_is_one_quadrillion() {
assert_eq!(MAX_LINE_AMOUNT, Decimal::from(1_000_000_000_000_000u64));
+13 -6
View File
@@ -21,6 +21,7 @@ pub mod analysis;
pub mod arap;
pub mod assets;
pub mod calendar;
pub mod cashflow;
pub mod classes;
pub mod conventions;
pub mod error;
@@ -53,6 +54,12 @@ pub use assets::{
ScheduleLine, TaxBasis, TaxBasisRule, accumulated_depreciation, book_value,
depreciation_schedule, disposal_amounts,
};
pub use cashflow::{
CashFlowFooting, CashFlowKind, CashFlowLine, CashFlowParams, CashFlowSection,
CashFlowStatement, CashFlowWarning, CashFlowWarningKind, NoncashDisclosure, OperatingClass,
OperatingReconciliation, ReconcilingKind, ReconcilingLine, RoleList, RoleProblem, cash_flow,
role_problems,
};
pub use classes::{Class, ClassId};
pub use error::LedgerError;
pub use form1099::{
@@ -60,6 +67,7 @@ pub use form1099::{
vendor_1099_report_with, vendor_1099_report_with_members,
};
pub use journal::{EntryId, EntryStatus, JournalEntry, JournalLine};
pub use kernel::{EntryFlows, Flow};
pub use ledger::{
ControlAccount, GENERAL_EXPENSES, Ledger, PAYABLES, PAYROLL, RECEIVABLES, RETAINED_EARNINGS,
REVENUE, ReconciliationReport, UnclearedLine, control_account, control_accounts,
@@ -73,12 +81,11 @@ pub use periods::{
PeriodId, PeriodStatus, RETAINED_EARNINGS_CODE,
};
pub use reports::{
AssetRegister, AssetRegisterRow, BalanceSheet, BalanceSheetStatement, CashFlowLine,
CashFlowParams, CashFlowStatement, ClassProfitAndLoss, GeneralLedger, LedgerAccount,
LedgerPosting, MemberCapital, MemberEquityColumn, MembersEquityStatement, ProfitAndLoss,
ProfitAndLossStatement, StatementLine, TrialBalance, TrialBalanceLine, balance_sheet_as_of,
cash_flow, fixed_asset_register, general_ledger, members_equity, profit_and_loss_for_period,
trial_balance_as_of,
AssetRegister, AssetRegisterRow, BalanceSheet, BalanceSheetStatement, ClassProfitAndLoss,
GeneralLedger, LedgerAccount, LedgerPosting, MemberCapital, MemberEquityColumn,
MembersEquityStatement, ProfitAndLoss, ProfitAndLossStatement, StatementLine, TrialBalance,
TrialBalanceLine, balance_sheet_as_of, fixed_asset_register, general_ledger, members_equity,
profit_and_loss_for_period, trial_balance_as_of,
};
pub use schedules::{
AmortizationSchedule, Frequency, NewRecurring, NewSchedule, RecurringEntry, RecurringId,
+380 -2
View File
@@ -9,8 +9,10 @@
//! property test checks the two instantiations agree.
//!
//! Bounds: entries of up to [`N`] lines over up to [`K`] accounts, and
//! amounts up to 2^40 in magnitude. The Lean model in `verify/lean`
//! proves the same laws for any number of lines and accounts.
//! amounts up to 2^40 in magnitude; the statement of cash flows' entry
//! harnesses, up to [`NF`] lines, each in any of the four places. The Lean
//! model in `verify/lean` proves the same laws for any number of lines and
//! accounts.
use crate::kernel::{self, Amount, LineError};
use crate::{AccountType, Side};
@@ -713,6 +715,382 @@ fn cumulative_closing_entry_balances() {
kani::cover!(true_up, "a true-up line on a member");
}
// ---------------------------------------------------------------------------
// The statement of cash flows
// ---------------------------------------------------------------------------
use kernel::Flow;
/// Most lines per entry the cash flow harnesses explore: three is the
/// fewest an entry needs to be mixed (cash and two sections), and each
/// line can land in any of the four places. (Four lines ran past half an
/// hour per harness, too long for `just prove`; the Lean model covers any
/// number.)
const NF: usize = 3;
/// Any count up to `NF`.
fn any_flow_len() -> usize {
let n: usize = kani::any();
kani::assume(n <= NF);
n
}
fn any_flow() -> Flow {
let t: u8 = kani::any();
kani::assume(t < 4);
match t {
0 => Flow::Cash,
1 => Flow::Operating,
2 => Flow::Investing,
_ => Flow::Financing,
}
}
/// `NF` lines, each tagged with its place on the statement (use a
/// prefix).
fn any_flow_lines() -> [(Flow, Side, i64); NF] {
core::array::from_fn(|_| (any_flow(), any_side(), any_amount()))
}
/// The spec's net movement of the lines tagged `f`.
fn tagged(lines: &[(Flow, Side, i64)], f: Flow) -> i64 {
let mut total = 0i64;
for &(g, s, a) in lines {
if g == f {
total += wide(s, a);
}
}
total
}
/// The spec's section movements, `[operating, investing, financing]`.
fn sections(lines: &[(Flow, Side, i64)]) -> [i64; 3] {
[
tagged(lines, Flow::Operating),
tagged(lines, Flow::Investing),
tagged(lines, Flow::Financing),
]
}
/// Whether the lines would post: their sides and amounts pass
/// `check_lines`.
fn accepted(lines: &[(Flow, Side, i64)]) -> bool {
let mut s = [(Side::Debit, 0i64); N];
for (o, &(_, side, a)) in s.iter_mut().zip(lines) {
*o = (side, a);
}
kernel::check_lines(&s[..lines.len()]).is_ok()
}
/// The building block of every cash flow harness below, for any two
/// amounts: what a section takes is odd (a reversal takes minus it), has
/// the cash's sign (or is zero), comes only from a movement against the
/// cash, and is never more than the cash left or the movement.
#[kani::proof]
fn take_is_odd_and_bounded() {
let r = any_amount();
let n = any_amount();
let t = kernel::take(r, n).expect("in range");
assert_eq!(kernel::take(-r, -n), Some(-t));
assert!(t == 0 || (t > 0) == (r > 0));
assert!(t == 0 || (t > 0) == (n < 0));
assert!(t.abs() <= r.abs() && t.abs() <= n.abs());
kani::cover!(
t != 0 && t.abs() < r.abs(),
"a movement smaller than the cash"
);
}
/// Applying minus the cash to minus the movements gives minus the parts,
/// part by part, and succeeds exactly when the original does.
#[kani::proof]
#[kani::unwind(5)]
fn apply_cash_mirrors() {
const M: usize = 3;
let cash = any_amount();
let moves: [i64; M] = core::array::from_fn(|_| any_amount());
let neg: [i64; M] = core::array::from_fn(|k| -moves[k]);
let mut a = [0i64; M];
let mut b = [0i64; M];
let ra = kernel::apply_cash(cash, &moves, &mut a);
let rb = kernel::apply_cash(-cash, &neg, &mut b);
assert_eq!(ra.is_some(), rb.is_some());
if ra.is_some() {
for k in 0..M {
assert_eq!(b[k], -a[k]);
}
kani::cover!(a[0] != 0 && a[2] != 0, "two sections settled, mirrored");
}
}
/// An accepted entry always has cash flows, and they explain its cash
/// exactly: the sections' cash sums to the entry's cash, and what no cash
/// settled (each section's movement plus its cash) sums to zero
/// (rules.md B-14 rules 3 and 9: the statement foots, entry by entry).
#[kani::proof]
#[kani::unwind(5)]
fn entry_flows_sum_to_cash() {
let all = any_flow_lines();
let lines = &all[..any_flow_len()];
if !accepted(lines) {
return;
}
let f = kernel::entry_flows(lines).expect("an accepted entry has cash flows");
let n = sections(lines);
assert_eq!(f.cash, tagged(lines, Flow::Cash));
assert_eq!(f.flows[0] + f.flows[1] + f.flows[2], f.cash);
assert_eq!(f.noncash[0] + f.noncash[1] + f.noncash[2], 0);
for k in 0..3 {
assert_eq!(f.noncash[k], n[k] + f.flows[k]);
}
kani::cover!(
f.flows[1] != 0 && f.noncash[1] != 0,
"a part-cash, part-noncash investing entry"
);
}
/// When every section moves against the cash (or not at all), the cash
/// settles every movement: each section's cash is minus its movement and
/// nothing is noncash. (With no cash at all the premise would hold
/// vacuously; review item 7: it needs cash.)
#[kani::proof]
#[kani::unwind(5)]
fn entry_flows_pure_are_movements() {
let all = any_flow_lines();
let lines = &all[..any_flow_len()];
if !accepted(lines) {
return;
}
let c = tagged(lines, Flow::Cash);
let n = sections(lines);
let against = |m: i64| m == 0 || (c > 0 && m < 0) || (c < 0 && m > 0);
if c == 0 || !(against(n[0]) && against(n[1]) && against(n[2])) {
return;
}
let f = kernel::entry_flows(lines).expect("an accepted entry has cash flows");
for k in 0..3 {
assert_eq!(f.flows[k], -n[k]);
assert_eq!(f.noncash[k], 0);
}
kani::cover!(n[0] != 0 && n[2] != 0, "cash settling two sections at once");
}
/// Each section's cash has the cash's sign (or is zero), is never larger
/// than the section's own movement, and comes only from a section moving
/// against the cash.
#[kani::proof]
#[kani::unwind(5)]
fn entry_flows_are_bounded() {
let all = any_flow_lines();
let lines = &all[..any_flow_len()];
let Some(f) = kernel::entry_flows(lines) else {
return;
};
let n = sections(lines);
for k in 0..3 {
let x = f.flows[k];
assert!(x == 0 || (x > 0) == (f.cash > 0));
assert!(x.abs() <= n[k].abs());
assert!(x == 0 || (x > 0) == (n[k] < 0));
}
kani::cover!(
f.flows[0] != 0 && f.flows[0].abs() < n[0].abs(),
"a section only partly settled by cash"
);
}
/// A reversal's cash flows are its original's, negated, part by part:
/// what lets an entry and its reversal in one period drop out of the
/// statement together (rules.md B-14 rule 6).
#[kani::proof]
#[kani::unwind(5)]
fn entry_flows_of_a_reversal_mirror() {
let all = any_flow_lines();
let n = any_flow_len();
let lines = &all[..n];
// Element by element (see `kani_symbolic_copy_canary`).
let mut rev = [(Flow::Cash, Side::Debit, 0i64); NF];
for i in 0..n {
rev[i] = kernel::reverse_line(lines[i]);
}
let a = kernel::entry_flows(lines);
let b = kernel::entry_flows(&rev[..n]);
assert_eq!(a.is_some(), b.is_some());
if let (Some(a), Some(b)) = (a, b) {
assert_eq!(b.cash, -a.cash);
for k in 0..3 {
assert_eq!(b.flows[k], -a.flows[k]);
assert_eq!(b.noncash[k], -a.noncash[k]);
}
kani::cover!(
a.flows[1] != 0 && a.noncash[2] != 0,
"a mixed entry, reversed"
);
}
}
/// An entry with no cash line has no cash flows: every movement is
/// noncash.
#[kani::proof]
#[kani::unwind(5)]
fn no_cash_no_flow() {
let all = any_flow_lines();
let lines = &all[..any_flow_len()];
for &(f, _, _) in lines {
kani::assume(f != Flow::Cash);
}
if !accepted(lines) {
return;
}
let f = kernel::entry_flows(lines).expect("an accepted entry has cash flows");
let n = sections(lines);
assert_eq!(f.cash, 0);
for k in 0..3 {
assert_eq!(f.flows[k], 0);
assert_eq!(f.noncash[k], n[k]);
}
kani::cover!(n[1] != 0 && n[2] != 0, "property contributed for capital");
}
/// How a section's cash splits among its kinds (contributions per member,
/// borrowing per lender): `apply_cash` succeeds exactly when the
/// movements against the cash cover it, and then the parts sum to the
/// cash, each with its sign and within its own movement.
#[kani::proof]
#[kani::unwind(5)]
fn apply_cash_spends_exactly() {
const M: usize = 3;
let cash = any_amount();
let moves: [i64; M] = core::array::from_fn(|_| any_amount());
let n: usize = kani::any();
kani::assume(n <= M);
let mut against = 0i64;
for &m in &moves[..n] {
if (cash > 0 && m < 0) || (cash < 0 && m > 0) {
against += m.abs();
}
}
let mut out = [0i64; M];
let ok = kernel::apply_cash(cash, &moves[..n], &mut out[..n]);
assert_eq!(ok.is_some(), cash.abs() <= against);
if ok.is_some() {
let mut sum = 0i64;
for k in 0..n {
let x = out[k];
sum += x;
assert!(x == 0 || (x > 0) == (cash > 0));
assert!(x.abs() <= moves[k].abs());
}
assert_eq!(sum, cash);
kani::cover!(n == M && out[2] != 0, "the third kind takes the rest");
}
}
/// Any amount a line may carry, not negative.
fn any_magnitude() -> i64 {
let a = any_amount();
kani::assume(a >= 0);
a
}
/// The exchange inside investing and financing is the smaller side left
/// once cash in has settled credits and cash out debits, never negative
/// and never more than either side.
#[kani::proof]
fn exchanged_is_bounded() {
let (d, c) = (any_magnitude(), any_magnitude());
let (inflow, outflow) = (any_magnitude(), any_magnitude());
let x = kernel::exchanged(d, c, inflow, outflow).expect("no overflow");
let spec = (d - outflow).min(c - inflow).max(0);
assert_eq!(x, spec);
assert!(0 <= x && x <= d && x <= c);
kani::cover!(x > 0 && outflow != 0, "a part-cash exchange");
kani::cover!(
x > 0 && inflow != 0 && outflow != 0,
"an exchange beside cash in and out"
);
}
fn any_way() -> kernel::OnBehalf {
if kani::any() {
kernel::OnBehalf::Paid
} else {
kernel::OnBehalf::Received
}
}
/// What a member or a lender paid or received for the company (rules.md
/// B-14 rule 11, D-42) invents and loses nothing: the parts net to zero,
/// so an entry's flows still sum to its cash; each part settles its own
/// movement toward zero, never past it; and as much is settled as can be.
#[kani::proof]
#[kani::unwind(4)]
fn constructive_nets_to_zero() {
const M: usize = 2;
let way = any_way();
let s: [i64; M] = core::array::from_fn(|_| any_amount());
let u: [i64; M] = core::array::from_fn(|_| any_amount());
let (mut from, mut to) = ([0i64; M], [0i64; M]);
kernel::constructive(way, &s, &u, &mut from, &mut to).expect("amounts in range always settle");
assert_eq!(from[0] + from[1] + to[0] + to[1], 0);
let settles = |x: i64, m: i64| (x == 0 || (x > 0) == (m < 0)) && x.abs() <= m.abs();
for k in 0..M {
assert!(settles(from[k], s[k]));
assert!(settles(to[k], u[k]));
}
let ls = [s[0] + from[0], s[1] + from[1]];
let lu = [u[0] + to[0], u[1] + to[1]];
let any = |xs: [i64; M], f: fn(i64) -> bool| f(xs[0]) || f(xs[1]);
match way {
kernel::OnBehalf::Paid => {
assert!(!(any(ls, |x| x < 0) && any(lu, |x| x > 0)));
// Paying never touches a source debit or a use credit.
for k in 0..M {
assert!(s[k] < 0 || from[k] == 0);
assert!(u[k] > 0 || to[k] == 0);
}
}
kernel::OnBehalf::Received => {
assert!(!(any(ls, |x| x > 0) && any(lu, |x| x < 0)));
for k in 0..M {
assert!(s[k] > 0 || from[k] == 0);
assert!(u[k] < 0 || to[k] == 0);
}
}
}
kani::cover!(from[0] > 0 && from[1] > 0, "two sources pay");
kani::cover!(from[0] < 0 && to[1] > 0, "received for the company");
kani::cover!(
ls[0] < 0 && to[0] < 0,
"a source larger than what it paid for"
);
}
/// The other way, minus the sources and minus the uses give minus the
/// parts: a reversal of a payment made on the company's behalf mirrors it
/// (rules.md B-14 rule 6), so the two drop out together.
#[kani::proof]
#[kani::unwind(4)]
fn constructive_mirrors() {
const M: usize = 2;
let way = any_way();
let s: [i64; M] = core::array::from_fn(|_| any_amount());
let u: [i64; M] = core::array::from_fn(|_| any_amount());
let (ns, nu) = ([-s[0], -s[1]], [-u[0], -u[1]]);
let (mut fa, mut ta) = ([0i64; M], [0i64; M]);
let (mut fb, mut tb) = ([0i64; M], [0i64; M]);
let a = kernel::constructive(way, &s, &u, &mut fa, &mut ta);
let b = kernel::constructive(way.mirror(), &ns, &nu, &mut fb, &mut tb);
assert_eq!(a.is_some(), b.is_some());
if a.is_some() {
for k in 0..M {
assert_eq!(fb[k], -fa[k]);
assert_eq!(tb[k], -ta[k]);
}
kani::cover!(fa[0] != 0 && ta[1] != 0, "a payment, mirrored");
}
}
// ---------------------------------------------------------------------------
// The checker itself
// ---------------------------------------------------------------------------
+6 -457
View File
@@ -1146,465 +1146,14 @@ pub fn snapshot(ledger: &Ledger) -> Result<serde_json::Value, LedgerError> {
}
// ---------------------------------------------------------------------------
// Cash flow statement (indirect method, R-FUTURE-3).
// Cash flow statement (direct method, rules.md B-14): see `crate::cashflow`.
// ---------------------------------------------------------------------------
/// One labeled cash flow line. Positive is an inflow, negative an outflow.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CashFlowLine {
/// Human label, e.g. `"Increase in Accounts Receivable"` or account code/name.
pub label: String,
/// Signed amount: positive = cash in, negative = cash out.
pub amount: Decimal,
/// Code of the account the line is about, when it is about one (so a
/// renderer can relabel, e.g. a member's capital account by name).
#[serde(default)]
pub account: Option<String>,
}
/// Parameters for [`cash_flow`].
#[derive(Clone, Debug)]
pub struct CashFlowParams {
/// Period start, inclusive.
pub start: chrono::NaiveDate,
/// Period end, inclusive.
pub end: chrono::NaiveDate,
/// Accounts treated as cash. Their net change is the reconciling total.
pub cash_accounts: Vec<crate::AccountId>,
/// Liability accounts treated as financing (debt, including what the
/// owners advanced: ASC 230-10-45-14(b) "other short- or long-term
/// borrowing"). Default empty: all liabilities are operating working
/// capital. All equity is financing.
pub financing_accounts: Vec<crate::AccountId>,
/// Asset accounts treated as investing: loans the entity makes and
/// investments it buys (ASC 230-10-20; 230-10-45-13). Registered fixed
/// assets are investing without being listed. Default empty: other
/// assets are operating working capital.
pub investing_accounts: Vec<crate::AccountId>,
}
/// Cash flow statement, indirect method.
///
/// Starts from period net income, adds back non-cash depreciation, adjusts
/// for working-capital changes, then reports investing (fixed-asset
/// purchases at cost, disposal proceeds) and financing (equity changes,
/// designated debt accounts) flows. The three sections always sum to the
/// net change in the designated cash accounts.
///
/// Disposals are identified structurally: a posted entry that credits a
/// registered fixed-asset cost account and debits its linked accumulated
/// depreciation account. The gain/loss in that entry is removed from
/// operating and the proceeds are shown in investing.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CashFlowStatement {
/// Period start, inclusive.
pub start: chrono::NaiveDate,
/// Period end, inclusive.
pub end: chrono::NaiveDate,
/// Period net income (revenue minus expenses).
pub net_income: Decimal,
/// Non-cash depreciation added back to net income.
pub depreciation: Decimal,
/// Working-capital adjustments (non-cash balance-sheet changes).
pub working_capital: Vec<CashFlowLine>,
/// Net cash from operating activities.
pub cash_from_operations: Decimal,
/// Investing flows: asset purchases and disposal proceeds.
pub investing: Vec<CashFlowLine>,
/// Net cash from investing activities.
pub cash_from_investing: Decimal,
/// Financing flows: equity changes and designated debt.
pub financing: Vec<CashFlowLine>,
/// Net cash from financing activities.
pub cash_from_financing: Decimal,
/// Net change in cash: the three sections summed.
pub net_change_in_cash: Decimal,
/// Cash balance at period start.
pub cash_beginning: Decimal,
/// Cash balance at period end.
pub cash_ending: Decimal,
}
/// Build the indirect-method cash flow statement for a period.
///
/// Pure function of the ledger: net income plus non-cash depreciation plus
/// working-capital changes gives operating cash flow; fixed-asset purchases
/// and disposal proceeds, and the accounts named as investing, give
/// investing; equity and the debt named as financing give financing. The
/// three sections sum to the net change in `cash_accounts`.
///
/// ```text
/// GOAL: the statement of cash flows by the indirect method: net income
/// adjusted to operating cash, plus investing and financing, for a
/// period.
/// SOURCE: docs/spec/rules.md B-14 (ASC 230-10-45-28 indirect method;
/// 230-10-45-16 operating inflows; 230-10-45-7/8 gross over net;
/// 230-10-45-13/14/15 investing and financing flows; 230-10-20
/// investing activities).
/// INVARIANTS:
/// 1. Operating + investing + financing MUST equal the net change in the
/// cash accounts over the period, exactly.
/// 2. Operating MUST start from the period's net income (closing entries
/// excluded) and adjust for changes in non-cash working-capital
/// accounts.
/// 3. Investing MUST hold fixed-asset purchases and disposal proceeds
/// (a disposal's gain or loss moves out of operating into investing)
/// and the changes in the accounts named as investing.
/// 4. Financing MUST hold changes in equity (other than earnings) and in
/// the liabilities named as financing.
/// 4a. Investing and financing MUST be gross (ASC 230-10-45-7: gross
/// receipts and payments are "more relevant" than net): per account,
/// cash in and cash out are separate lines (contributions apart from
/// distributions, borrowing apart from repayment), netted only within
/// one entry.
/// 5. NEVER count closing entries.
/// 6. The period is `[start, end]`, inclusive: an entry dated `start` is
/// period activity, not part of the beginning cash balance.
/// 7. Every line's amount is its cash effect: positive is cash in. Its
/// label names the direction ("Increase in" / "Decrease in" for
/// working capital; "Proceeds from" / "Repayment of" for financing
/// debt; "Owner contributions" / "Owner distributions" for equity;
/// "Payments for" / "Collections on" for investing accounts).
/// ```
pub fn cash_flow(
ledger: &Ledger,
params: &CashFlowParams,
) -> Result<CashFlowStatement, LedgerError> {
use std::collections::{HashMap, HashSet};
if params.end < params.start {
return Err(LedgerError::InvalidDateRange {
start: params.start.to_string(),
end: params.end.to_string(),
});
}
// Fixed-asset account roles from the register.
let mut cost_accounts: HashSet<crate::AccountId> = HashSet::new();
let mut accum_accounts: HashSet<crate::AccountId> = HashSet::new();
let mut depr_expense_accounts: HashSet<crate::AccountId> = HashSet::new();
// cost_account -> accum_account, for disposal identification.
let mut cost_to_accum: HashMap<crate::AccountId, crate::AccountId> = HashMap::new();
for asset in ledger.registered_assets() {
cost_accounts.insert(asset.cost_account);
accum_accounts.insert(asset.accum_depr_account);
depr_expense_accounts.insert(asset.expense_account);
cost_to_accum.insert(asset.cost_account, asset.accum_depr_account);
}
let cash_set: HashSet<crate::AccountId> = params.cash_accounts.iter().copied().collect();
let financing_set: HashSet<crate::AccountId> =
params.financing_accounts.iter().copied().collect();
let investing_set: HashSet<crate::AccountId> =
params.investing_accounts.iter().copied().collect();
// Movement helpers: debits minus credits over a date predicate. Closing
// entries are skipped: a close moves earnings into retained earnings,
// which is neither net income nor an owner flow.
let closings = ledger.closing_entry_ids();
let movement_where = |account: crate::AccountId, pred: &dyn Fn(chrono::NaiveDate) -> bool| {
let mut total = Decimal::ZERO;
for entry in ledger.posted_entries() {
if !pred(entry.date()) || closings.contains(&entry.id()) {
continue;
}
for line in entry.lines() {
if line.account == account {
match line.side {
crate::Side::Debit => total += line.amount.amount,
crate::Side::Credit => total -= line.amount.amount,
}
}
}
}
total
};
let begin_movement = |a: crate::AccountId| movement_where(a, &|d| d < params.start);
let period_movement =
|a: crate::AccountId| movement_where(a, &|d| d >= params.start && d <= params.end);
// Normal-side balance from a movement.
let normal = |account: crate::AccountId, movement: Decimal| -> Decimal {
let acct = ledger.chart().get(account).expect("chart account exists");
match acct.account_type.normal_side() {
crate::Side::Debit => movement,
crate::Side::Credit => -movement,
}
};
// --- Net income and depreciation (period P&L) ---
let mut net_income = Decimal::ZERO;
let mut depreciation = Decimal::ZERO;
for account in ledger.chart().all() {
if account.placeholder {
continue;
}
let pm = period_movement(account.id);
if pm.is_zero() {
continue;
}
match account.account_type {
AccountType::Revenue => net_income += -pm, // revenue movement is credits
AccountType::Expense => {
net_income -= pm;
if depr_expense_accounts.contains(&account.id) {
depreciation += pm;
}
}
_ => {}
}
}
// --- Classify balance-sheet accounts ---
let mut working_capital: Vec<CashFlowLine> = Vec::new();
let mut investing: Vec<CashFlowLine> = Vec::new();
let mut financing: Vec<CashFlowLine> = Vec::new();
// Account display helper.
let display = |account: crate::AccountId| -> String {
let a = ledger.chart().get(account).expect("chart account exists");
format!("{} {}", a.code, a.name)
};
// By code: the chart iterates in hash order, and a statement's lines
// must come out in the same order every time.
let mut accounts: Vec<_> = ledger.chart().all().collect();
accounts.sort_by(|a, b| a.code.cmp(&b.code));
for account in accounts {
if account.placeholder {
continue;
}
let id = account.id;
if cash_set.contains(&id) || cost_accounts.contains(&id) || accum_accounts.contains(&id) {
continue; // cash reconciles; fixed assets are investing (gross, below)
}
if account.account_type == AccountType::Equity
|| financing_set.contains(&id)
|| investing_set.contains(&id)
{
continue; // financing and investing roles are gross, below
}
let begin = normal(id, begin_movement(id));
let end = normal(id, period_movement(id) + begin_movement(id));
let delta = end - begin;
if delta.is_zero() {
continue;
}
// Cash effect of the balance change: for both assets and
// liabilities, it is minus the movement (debits - credits).
let movement_delta = period_movement(id);
let cash_effect = -movement_delta;
let (label, line) = match account.account_type {
AccountType::Asset => {
let dir = if delta > Decimal::ZERO {
"Increase in"
} else {
"Decrease in"
};
(format!("{dir} {}", display(id)), cash_effect)
}
AccountType::Liability => {
let dir = if delta > Decimal::ZERO {
"Increase in"
} else {
"Decrease in"
};
(format!("{dir} {}", display(id)), cash_effect)
}
_ => continue, // revenue/expense handled via net income
};
working_capital.push(CashFlowLine {
label,
amount: line,
account: Some(account.code.clone()),
});
}
// --- Financing and investing accounts, gross (ASC 230-10-45-7) ---
// Per account, cash in and cash out as separate lines; an entry's own
// debits and credits to the account net first (one transaction, one
// flow).
let mut roles: Vec<&crate::Account> = ledger
.chart()
.all()
.filter(|a| {
!a.placeholder
&& !cash_set.contains(&a.id)
&& !cost_accounts.contains(&a.id)
&& !accum_accounts.contains(&a.id)
&& (a.account_type == AccountType::Equity
|| financing_set.contains(&a.id)
|| investing_set.contains(&a.id))
})
.collect();
roles.sort_by(|a, b| a.code.cmp(&b.code));
for account in roles {
let (mut cash_in, mut cash_out) = (Decimal::ZERO, Decimal::ZERO);
for entry in ledger.posted_entries() {
let d = entry.date();
if d < params.start || d > params.end || closings.contains(&entry.id()) {
continue;
}
// Cash effect of this entry's change to the account: minus its
// movement (debits - credits), for assets and claims alike.
let effect: Decimal = entry
.lines()
.iter()
.filter(|l| l.account == account.id)
.map(|l| match l.side {
crate::Side::Debit => -l.amount.amount,
crate::Side::Credit => l.amount.amount,
})
.sum();
if effect > Decimal::ZERO {
cash_in += effect;
} else {
cash_out += effect;
}
}
let name = display(account.id);
let (in_label, out_label, target) = if investing_set.contains(&account.id) {
(
format!("Collections on {name}"),
format!("Payments for {name}"),
&mut investing,
)
} else if account.account_type == AccountType::Equity {
(
format!("Owner contributions — {name}"),
format!("Owner distributions — {name}"),
&mut financing,
)
} else {
(
format!("Proceeds from {name}"),
format!("Repayment of {name}"),
&mut financing,
)
};
for (label, amount) in [(in_label, cash_in), (out_label, cash_out)] {
if !amount.is_zero() {
target.push(CashFlowLine {
label,
amount,
account: Some(account.code.clone()),
});
}
}
}
// --- Investing, gross: purchases and disposal proceeds ---
// A disposal entry credits a cost account and debits its linked accum.
let mut disposal_gain_loss = Decimal::ZERO; // +gain / -loss, to remove from operating
for entry in ledger.posted_entries() {
let d = entry.date();
if d < params.start || d > params.end {
continue;
}
// Is this a disposal? Credit to a cost account + debit to its accum.
let mut disposed: Option<(crate::AccountId, Decimal, Decimal)> = None;
for line in entry.lines() {
if line.side == crate::Side::Credit
&& let Some(&accum) = cost_to_accum.get(&line.account)
{
// Confirm the linked accum account is debited in this entry.
let accum_debit: Decimal = entry
.lines()
.iter()
.filter(|l| l.account == accum && l.side == crate::Side::Debit)
.map(|l| l.amount.amount)
.sum();
if !accum_debit.is_zero() {
disposed = Some((line.account, line.amount.amount, accum_debit));
break;
}
}
}
if let Some((cost_acct, cost_amount, accum_debit)) = disposed {
let accum_acct = cost_to_accum[&cost_acct];
let mut proceeds = Decimal::ZERO;
let mut proceeds_label = String::new();
let mut gain = Decimal::ZERO;
let mut loss = Decimal::ZERO;
for line in entry.lines() {
if line.account == cost_acct || line.account == accum_acct {
continue;
}
let acct = ledger
.chart()
.get(line.account)
.expect("chart account exists");
match (acct.account_type, line.side) {
(AccountType::Asset, crate::Side::Debit) => {
proceeds += line.amount.amount;
proceeds_label = display(line.account);
}
(AccountType::Revenue, crate::Side::Credit) => gain += line.amount.amount,
(AccountType::Expense, crate::Side::Debit) => loss += line.amount.amount,
_ => {}
}
}
// Sanity: the entry balances, so proceeds + accum debit = cost + gain - loss.
debug_assert!((proceeds + accum_debit - cost_amount - gain + loss).is_zero());
disposal_gain_loss += gain - loss;
investing.push(CashFlowLine {
label: format!("Sale of fixed assets — {proceeds_label}"),
amount: proceeds,
account: ledger.chart().get(cost_acct).map(|a| a.code.clone()),
});
continue;
}
// Not a disposal: debits to cost accounts are purchases.
for line in entry.lines() {
if cost_accounts.contains(&line.account) && line.side == crate::Side::Debit {
investing.push(CashFlowLine {
label: format!("Purchase of fixed assets — {}", display(line.account)),
amount: -line.amount.amount,
account: ledger.chart().get(line.account).map(|a| a.code.clone()),
});
}
}
}
// Remove disposal gains/losses from operating (they live in investing).
if !disposal_gain_loss.is_zero() {
working_capital.push(CashFlowLine {
label: "Less: gain on sale of fixed assets (in investing)".to_string(),
amount: -disposal_gain_loss,
account: None,
});
}
let cash_from_operations: Decimal =
net_income + depreciation + working_capital.iter().map(|l| l.amount).sum::<Decimal>();
let cash_from_investing: Decimal = investing.iter().map(|l| l.amount).sum();
let cash_from_financing: Decimal = financing.iter().map(|l| l.amount).sum();
// --- Cash reconciliation ---
let mut cash_beginning = Decimal::ZERO;
let mut cash_ending = Decimal::ZERO;
for &id in &params.cash_accounts {
cash_beginning += normal(id, begin_movement(id));
cash_ending += normal(id, begin_movement(id) + period_movement(id));
}
let net_change_in_cash = cash_ending - cash_beginning;
Ok(CashFlowStatement {
start: params.start,
end: params.end,
net_income,
depreciation,
working_capital,
cash_from_operations,
investing,
cash_from_investing,
financing,
cash_from_financing,
net_change_in_cash,
cash_beginning,
cash_ending,
})
}
pub use crate::cashflow::{
CashFlowFooting, CashFlowKind, CashFlowLine, CashFlowParams, CashFlowSection,
CashFlowStatement, CashFlowWarning, CashFlowWarningKind, NoncashDisclosure, OperatingClass,
OperatingReconciliation, ReconcilingKind, ReconcilingLine, cash_flow, foot,
};
/// One asset on the fixed asset register.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+3 -3
View File
@@ -294,21 +294,21 @@ pub static KEYS: &[Key] = &[
toml: ("books", "cash_accounts"),
kind: Kind::Codes,
sensitivity: Sensitivity::Public,
doc: "Account codes that hold cash (bank, savings, cash on hand): the cash flow statement reconciles to them.",
doc: "Asset account codes that hold cash (bank, savings, cash on hand): the cash flow statement explains their change.",
},
Key {
name: "books.financing_accounts",
toml: ("books", "financing_accounts"),
kind: Kind::Codes,
sensitivity: Sensitivity::Public,
doc: "Liability account codes the cash flow statement treats as financing (loans, and what owners advanced: ASC 230-10-45-14(b)).",
doc: "Liability account codes the cash flow statement treats as financing (loans, and what owners advanced: ASC 230-10-45-14(b)); equity is always financing. What a member or lender pays for the company through one is shown as borrowed and spent (D-42).",
},
Key {
name: "books.investing_accounts",
toml: ("books", "investing_accounts"),
kind: Kind::Codes,
sensitivity: Sensitivity::Public,
doc: "Asset account codes the cash flow statement treats as investing (loans the company makes, investments it buys: ASC 230-10-20).",
doc: "Asset account codes the cash flow statement treats as investing (loans the company makes, investments it buys: ASC 230-10-20); registered fixed assets are investing unlisted.",
},
Key {
name: "books.close_into",