feat: cash flow statement PDF, pdf export format, cash flow in CPA package
The cash flow statement was computed and exportable as csv/json/xlsx but missing from every PDF path, so the CPA package carried two of the three financial statements. Now: - ledger-import: cash_flow_pdf renders the indirect-method statement (operating with net income, depreciation add-back, working-capital lines; investing; financing; net change reconciled to beginning and ending cash) from the computed CashFlowStatement, so the period and cash accounts are the caller's. - books.export gains format "pdf" for profit_and_loss, balance_sheet, and cash_flow (base64, like xlsx), plus financing_accounts for cash_flow. - books.export_cpa_package adds cash-flow.pdf (fiscal year to date, like the P&L) and requires cash_accounts: without them the statement has nothing to reconcile to. - One cash_flow_statement helper resolves codes and dates for xlsx, pdf, and the package. Test: MCP stdio test exports all three statement PDFs, checks the cash_accounts and bad-report refusals, and unzips the CPA package to find all five files. Known report gaps are logged in plan/STATUS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
1 parent
e59e8001c6
commit
5548902f27
7 files changed
+304
-54
No files matched your search
@@ -17,5 +17,5 @@ pub mod xlsx;
|
||||
pub use bank_csv::{BankTransaction, parse_bank_csv};
|
||||
pub use chart_csv::{ChartRow, parse_chart_csv};
|
||||
pub use iif::{IifEntry, journal_to_iif};
|
||||
pub use pdf::{PdfError, balance_sheet_pdf, profit_and_loss_pdf};
|
||||
pub use pdf::{PdfError, balance_sheet_pdf, cash_flow_pdf, profit_and_loss_pdf};
|
||||
pub use xlsx::{XlsxReport, report_to_xlsx};
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Simple, correct, readable — not pretty. Uses printpdf (MIT) with built-in
|
||||
//! Helvetica. Amounts are exact decimals rendered as strings; no floats.
|
||||
|
||||
use ledger_core::reports::CashFlowStatement;
|
||||
use ledger_core::{AccountType, Ledger};
|
||||
use printpdf::{
|
||||
BuiltinFont, Color, Mm, Op, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, Point, Pt,
|
||||
@@ -203,3 +204,59 @@ pub fn balance_sheet_pdf(ledger: &Ledger, as_of: &str) -> Result<Vec<u8>, PdfErr
|
||||
let ops = text_ops("Balance Sheet", &format!("As of {}", as_of), &rows, &totals);
|
||||
save_pdf("Balance Sheet", ops)
|
||||
}
|
||||
|
||||
/// Render a cash flow statement PDF (indirect method): operating (net
|
||||
/// income, depreciation add-back, working-capital changes), investing, and
|
||||
/// financing sections, then the net change reconciled to beginning and
|
||||
/// ending cash. Takes the computed statement, so the period and cash
|
||||
/// accounts are whatever the caller chose.
|
||||
pub fn cash_flow_pdf(cf: &CashFlowStatement) -> Result<Vec<u8>, PdfError> {
|
||||
let line = |l: &ledger_core::reports::CashFlowLine| (format!(" {}", l.label), money(l.amount));
|
||||
let mut rows = vec![("OPERATING ACTIVITIES".to_string(), "".to_string())];
|
||||
rows.push((" Net income".to_string(), money(cf.net_income)));
|
||||
if !cf.depreciation.is_zero() {
|
||||
rows.push((
|
||||
" Depreciation (non-cash)".to_string(),
|
||||
money(cf.depreciation),
|
||||
));
|
||||
}
|
||||
rows.extend(cf.working_capital.iter().map(line));
|
||||
rows.push((
|
||||
"Net cash from operating activities".to_string(),
|
||||
money(cf.cash_from_operations),
|
||||
));
|
||||
rows.push(("".to_string(), "".to_string()));
|
||||
rows.push(("INVESTING ACTIVITIES".to_string(), "".to_string()));
|
||||
rows.extend(cf.investing.iter().map(line));
|
||||
rows.push((
|
||||
"Net cash from investing activities".to_string(),
|
||||
money(cf.cash_from_investing),
|
||||
));
|
||||
rows.push(("".to_string(), "".to_string()));
|
||||
rows.push(("FINANCING ACTIVITIES".to_string(), "".to_string()));
|
||||
rows.extend(cf.financing.iter().map(line));
|
||||
rows.push((
|
||||
"Net cash from financing activities".to_string(),
|
||||
money(cf.cash_from_financing),
|
||||
));
|
||||
|
||||
let totals = vec![
|
||||
(
|
||||
"Net change in cash".to_string(),
|
||||
money(cf.net_change_in_cash),
|
||||
),
|
||||
(
|
||||
"Cash at beginning of period".to_string(),
|
||||
money(cf.cash_beginning),
|
||||
),
|
||||
("Cash at end of period".to_string(), money(cf.cash_ending)),
|
||||
];
|
||||
|
||||
let ops = text_ops(
|
||||
"Statement of Cash Flows",
|
||||
&format!("{} to {}", cf.start, cf.end),
|
||||
&rows,
|
||||
&totals,
|
||||
);
|
||||
save_pdf("Statement of Cash Flows", ops)
|
||||
}
|
||||
Reference in new issue
Block a user