cash flow: financing and investing gross; account roles are a setting
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
From ASC 230 (via Deloitte DART and PwC Viewpoint), replacing two formerly assumed behaviors: - Financing and investing are gross (230-10-45-7): per account, cash in and cash out are separate lines (contributions apart from distributions, borrowing apart from repayment), netted only within one entry. Lines come in code order, in before out. - Investing includes making and collecting loans and buying investments (230-10-20, 45-13), not only registered fixed assets: CashFlowParams gains investing_accounts. - The roles are per-company settings: [books] financing_accounts and investing_accounts in wanderbooks.toml (beside cash_accounts). The MCP tools (cash_flow, export, CPA package) default every list to them and share one path; the viewer reads them too, falling back to its name guess for debt. A due-to-owners liability listed as financing gives PwC 6.9.3.1's constructive treatment of expenses an owner paid. rules.md B-14 quotes the sources; new tests cite them; docs/testing.md #8 is resolved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
08d1a9c230
commit
819cd09e0d
13 files changed
+521
-126
No files matched your search
@@ -113,6 +113,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
},
|
||||
(None, None) => ReceiptPolicy::default(),
|
||||
};
|
||||
let roles = server::CashRoles {
|
||||
cash: settings.cash_accounts,
|
||||
financing: settings.financing_accounts,
|
||||
investing: settings.investing_accounts,
|
||||
};
|
||||
let company = settings.company;
|
||||
|
||||
let mut store = if data_dir.join(ledger_store::JOURNAL_FILE).exists() {
|
||||
@@ -138,12 +143,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build()?;
|
||||
if let Some(addr) = http_addr {
|
||||
eprintln!("ledger-mcp: store verified, serving Streamable HTTP on {addr}");
|
||||
rt.block_on(serve_http(addr, store, policy, company))?;
|
||||
rt.block_on(serve_http(addr, store, policy, company, roles))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!("ledger-mcp: store verified, serving over stdio");
|
||||
let server = BooksServer::new(store, policy).with_company(company);
|
||||
let server = BooksServer::new(store, policy)
|
||||
.with_company(company)
|
||||
.with_cash_roles(roles);
|
||||
rt.block_on(async {
|
||||
let running = server.serve(stdio()).await?;
|
||||
let _ = running.waiting().await;
|
||||
@@ -157,6 +164,7 @@ async fn serve_http(
|
||||
store: Store,
|
||||
policy: ReceiptPolicy,
|
||||
company: company::Company,
|
||||
roles: server::CashRoles,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rmcp::transport::streamable_http_server::{
|
||||
StreamableHttpService, session::local::LocalSessionManager,
|
||||
@@ -167,7 +175,8 @@ async fn serve_http(
|
||||
let service = StreamableHttpService::new(
|
||||
move || {
|
||||
let server = server::BooksServer::new_shared(store.clone(), policy.clone())
|
||||
.with_company(company.clone());
|
||||
.with_company(company.clone())
|
||||
.with_cash_roles(roles.clone());
|
||||
Ok(server)
|
||||
},
|
||||
Arc::new(LocalSessionManager::default()),
|
||||
|
||||
@@ -36,6 +36,19 @@ pub struct BooksServer {
|
||||
store: Arc<Mutex<Store>>,
|
||||
policy: ReceiptPolicy,
|
||||
company: Company,
|
||||
roles: CashRoles,
|
||||
}
|
||||
|
||||
/// `[books]` account roles for the cash flow statement, used when a call
|
||||
/// doesn't pass its own: cash, financing, and investing account codes.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CashRoles {
|
||||
/// `[books] cash_accounts`.
|
||||
pub cash: Vec<String>,
|
||||
/// `[books] financing_accounts`.
|
||||
pub financing: Vec<String>,
|
||||
/// `[books] investing_accounts`.
|
||||
pub investing: Vec<String>,
|
||||
}
|
||||
|
||||
impl BooksServer {
|
||||
@@ -45,6 +58,7 @@ impl BooksServer {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
policy,
|
||||
company: Company::default(),
|
||||
roles: CashRoles::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +69,7 @@ impl BooksServer {
|
||||
store,
|
||||
policy,
|
||||
company: Company::default(),
|
||||
roles: CashRoles::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +79,40 @@ impl BooksServer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `[books]` cash flow account roles.
|
||||
pub fn with_cash_roles(mut self, roles: CashRoles) -> Self {
|
||||
self.roles = roles;
|
||||
self
|
||||
}
|
||||
|
||||
/// The cash flow statement from account codes, each list defaulting to
|
||||
/// the `[books]` roles when the call passes none.
|
||||
fn cash_flow_for(
|
||||
&self,
|
||||
ledger: &Ledger,
|
||||
start: Option<&str>,
|
||||
end: Option<&str>,
|
||||
cash: &[String],
|
||||
financing: &[String],
|
||||
investing: &[String],
|
||||
) -> Result<ledger_core::reports::CashFlowStatement, String> {
|
||||
let or = |given: &'_ [String], default: &'_ [String]| -> Vec<String> {
|
||||
if given.is_empty() {
|
||||
default.to_vec()
|
||||
} else {
|
||||
given.to_vec()
|
||||
}
|
||||
};
|
||||
cash_flow_statement(
|
||||
ledger,
|
||||
start,
|
||||
end,
|
||||
&or(cash, &self.roles.cash),
|
||||
&or(financing, &self.roles.financing),
|
||||
&or(investing, &self.roles.investing),
|
||||
)
|
||||
}
|
||||
|
||||
/// Header and provenance for a rendered report.
|
||||
fn report_meta(&self, store: &Store) -> ReportMeta {
|
||||
ReportMeta {
|
||||
@@ -446,6 +495,7 @@ fn cash_flow_statement(
|
||||
end: Option<&str>,
|
||||
cash_codes: &[String],
|
||||
financing_codes: &[String],
|
||||
investing_codes: &[String],
|
||||
) -> Result<ledger_core::reports::CashFlowStatement, String> {
|
||||
let resolve = |codes: &[String], what: &str| -> Result<Vec<AccountId>, String> {
|
||||
codes
|
||||
@@ -461,9 +511,13 @@ fn cash_flow_statement(
|
||||
};
|
||||
let cash_accounts = resolve(cash_codes, "cash")?;
|
||||
if cash_accounts.is_empty() {
|
||||
return Err("cash_accounts is required for the cash flow statement".to_string());
|
||||
return Err(
|
||||
"cash_accounts is required for the cash flow statement (pass it, or set [books] cash_accounts in wanderbooks.toml)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let financing_accounts = resolve(financing_codes, "financing")?;
|
||||
let investing_accounts = resolve(investing_codes, "investing")?;
|
||||
let (start, end) = statement_range(ledger, start, end)?;
|
||||
ledger_core::cash_flow(
|
||||
ledger,
|
||||
@@ -472,6 +526,7 @@ fn cash_flow_statement(
|
||||
end,
|
||||
cash_accounts,
|
||||
financing_accounts,
|
||||
investing_accounts,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -769,46 +824,14 @@ impl BooksServer {
|
||||
let p = params.0;
|
||||
let store = self.lock()?;
|
||||
let ledger = store.ledger();
|
||||
let start = match p.start {
|
||||
Some(s) => parse_date(&s)?,
|
||||
None => NaiveDate::from_ymd_opt(1, 1, 1).unwrap(),
|
||||
};
|
||||
let end = match p.end {
|
||||
Some(s) => parse_date(&s)?,
|
||||
None => today(),
|
||||
};
|
||||
// Resolve cash and financing account codes to ids.
|
||||
let mut cash_accounts = Vec::new();
|
||||
for code in &p.cash_accounts {
|
||||
let id = ledger
|
||||
.chart()
|
||||
.find_by_code(code)
|
||||
.map(|a| a.id)
|
||||
.ok_or_else(|| format!("unknown cash account code: {code}"))?;
|
||||
cash_accounts.push(id);
|
||||
}
|
||||
if cash_accounts.is_empty() {
|
||||
return Err("at least one cash account code is required".to_string());
|
||||
}
|
||||
let mut financing_accounts = Vec::new();
|
||||
for code in &p.financing_accounts {
|
||||
let id = ledger
|
||||
.chart()
|
||||
.find_by_code(code)
|
||||
.map(|a| a.id)
|
||||
.ok_or_else(|| format!("unknown financing account code: {code}"))?;
|
||||
financing_accounts.push(id);
|
||||
}
|
||||
let stmt = ledger_core::cash_flow(
|
||||
let stmt = self.cash_flow_for(
|
||||
ledger,
|
||||
&ledger_core::CashFlowParams {
|
||||
start,
|
||||
end,
|
||||
cash_accounts,
|
||||
financing_accounts,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
p.start.as_deref(),
|
||||
p.end.as_deref(),
|
||||
&p.cash_accounts,
|
||||
&p.financing_accounts,
|
||||
&p.investing_accounts,
|
||||
)?;
|
||||
let line = |l: &ledger_core::CashFlowLine| CashFlowLineOut {
|
||||
label: l.label.clone(),
|
||||
amount: money(l.amount),
|
||||
@@ -2454,12 +2477,13 @@ impl BooksServer {
|
||||
report::balance_sheet(&bs, earnings, &members)
|
||||
}
|
||||
"cash_flow" => report::cash_flow(
|
||||
&cash_flow_statement(
|
||||
&self.cash_flow_for(
|
||||
ledger,
|
||||
p.start.as_deref(),
|
||||
p.end.as_deref(),
|
||||
&p.cash_accounts,
|
||||
&p.financing_accounts,
|
||||
&p.investing_accounts,
|
||||
)?,
|
||||
&members,
|
||||
),
|
||||
@@ -3556,12 +3580,13 @@ impl BooksServer {
|
||||
(
|
||||
"cash-flow",
|
||||
report::cash_flow(
|
||||
&cash_flow_statement(
|
||||
&self.cash_flow_for(
|
||||
ledger,
|
||||
Some(&fy_start),
|
||||
Some(&as_of_s),
|
||||
&p.cash_accounts,
|
||||
&p.financing_accounts,
|
||||
&p.investing_accounts,
|
||||
)?,
|
||||
&members,
|
||||
),
|
||||
@@ -4056,36 +4081,14 @@ impl BooksServer {
|
||||
fn export_cash_flow(&self, format: &str, p: &ExportParams) -> Result<String, String> {
|
||||
let store = self.lock()?;
|
||||
let ledger = store.ledger();
|
||||
let mut cash_accounts = Vec::new();
|
||||
for code in &p.cash_accounts {
|
||||
let id = ledger
|
||||
.chart()
|
||||
.find_by_code(code)
|
||||
.map(|a| a.id)
|
||||
.ok_or_else(|| format!("unknown cash account code: {code}"))?;
|
||||
cash_accounts.push(id);
|
||||
}
|
||||
if cash_accounts.is_empty() {
|
||||
return Err("cash_accounts is required for cash_flow".to_string());
|
||||
}
|
||||
let start = match &p.start {
|
||||
Some(s) => parse_date(s)?,
|
||||
None => NaiveDate::from_ymd_opt(1, 1, 1).unwrap(),
|
||||
};
|
||||
let end = match &p.end {
|
||||
Some(s) => parse_date(s)?,
|
||||
None => today(),
|
||||
};
|
||||
let stmt = ledger_core::cash_flow(
|
||||
let stmt = self.cash_flow_for(
|
||||
ledger,
|
||||
&ledger_core::CashFlowParams {
|
||||
start,
|
||||
end,
|
||||
cash_accounts,
|
||||
financing_accounts: vec![],
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
p.start.as_deref(),
|
||||
p.end.as_deref(),
|
||||
&p.cash_accounts,
|
||||
&p.financing_accounts,
|
||||
&p.investing_accounts,
|
||||
)?;
|
||||
if format == "json" {
|
||||
return serde_json::to_string_pretty(&serde_json::json!({
|
||||
"start": stmt.start.to_string(), "end": stmt.end.to_string(),
|
||||
|
||||
@@ -621,13 +621,21 @@ pub struct CashFlowParams {
|
||||
pub start: Option<String>,
|
||||
/// Period end, YYYY-MM-DD. Defaults to today.
|
||||
pub end: Option<String>,
|
||||
/// Account codes treated as cash (e.g. `["1000", "1010"]`). Required:
|
||||
/// the statement reconciles to the net change in these accounts.
|
||||
/// Account codes treated as cash (e.g. `["1000", "1010"]`): the
|
||||
/// statement reconciles to the net change in these accounts. Defaults
|
||||
/// to `[books] cash_accounts`; one or the other is required.
|
||||
#[serde(default)]
|
||||
pub cash_accounts: Vec<String>,
|
||||
/// Account codes for liabilities treated as financing (debt).
|
||||
/// Defaults to empty: liabilities are operating working capital.
|
||||
/// Account codes for liabilities treated as financing (loans, and what
|
||||
/// owners advanced: ASC 230-10-45-14(2)). Defaults to `[books]
|
||||
/// financing_accounts`, else empty (liabilities are working capital).
|
||||
#[serde(default)]
|
||||
pub financing_accounts: Vec<String>,
|
||||
/// Asset account codes treated as investing for cash_flow: loans the
|
||||
/// company makes, investments it buys (ASC 230-10-20). Defaults to
|
||||
/// `[books] investing_accounts`.
|
||||
#[serde(default)]
|
||||
pub investing_accounts: Vec<String>,
|
||||
}
|
||||
|
||||
/// One cash flow line.
|
||||
@@ -1315,13 +1323,19 @@ pub struct ExportParams {
|
||||
pub end: Option<String>,
|
||||
/// As-of date for balance_sheet, YYYY-MM-DD.
|
||||
pub as_of: Option<String>,
|
||||
/// Cash account codes for cash_flow (required for that report).
|
||||
/// Cash account codes for cash_flow. Defaults to `[books]
|
||||
/// cash_accounts`; one or the other is required for that report.
|
||||
#[serde(default)]
|
||||
pub cash_accounts: Vec<String>,
|
||||
/// Liability account codes treated as financing (debt) for cash_flow.
|
||||
/// Defaults to empty: liabilities are operating working capital.
|
||||
/// Liability account codes treated as financing for cash_flow.
|
||||
/// Defaults to `[books] financing_accounts`, else empty.
|
||||
#[serde(default)]
|
||||
pub financing_accounts: Vec<String>,
|
||||
/// Asset account codes treated as investing for cash_flow: loans the
|
||||
/// company makes, investments it buys (ASC 230-10-20). Defaults to
|
||||
/// `[books] investing_accounts`.
|
||||
#[serde(default)]
|
||||
pub investing_accounts: Vec<String>,
|
||||
}
|
||||
|
||||
/// Result of `books.export`.
|
||||
@@ -1997,14 +2011,20 @@ pub struct ProfitAndLossByClassOut {
|
||||
pub struct ExportCpaPackageParams {
|
||||
/// As-of date, YYYY-MM-DD. Defaults to today.
|
||||
pub as_of: Option<String>,
|
||||
/// Cash account codes the cash flow statement reconciles to (required),
|
||||
/// e.g. `["1000"]`.
|
||||
/// Cash account codes the cash flow statement reconciles to, e.g.
|
||||
/// `["1000"]`. Defaults to `[books] cash_accounts`; one or the other
|
||||
/// is required.
|
||||
#[serde(default)]
|
||||
pub cash_accounts: Vec<String>,
|
||||
/// Liability account codes treated as financing (debt). Defaults to
|
||||
/// empty: liabilities are operating working capital.
|
||||
/// Liability account codes treated as financing. Defaults to `[books]
|
||||
/// financing_accounts`, else empty.
|
||||
#[serde(default)]
|
||||
pub financing_accounts: Vec<String>,
|
||||
/// Asset account codes treated as investing for cash_flow: loans the
|
||||
/// company makes, investments it buys (ASC 230-10-20). Defaults to
|
||||
/// `[books] investing_accounts`.
|
||||
#[serde(default)]
|
||||
pub investing_accounts: Vec<String>,
|
||||
/// Include every receipt for the year's entries and the company's
|
||||
/// documents (default true).
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
Reference in new issue
Block a user