use super::{OutputFormat, csv_escape, json_escape};
use anyhow::Result;
use rust_decimal::Decimal;
use rustledger_core::{Directive, DisplayContext, Inventory};
use std::collections::BTreeMap;
use std::io::Write;
pub(super) fn report_balsheet<W: Write>(
directives: &[Directive],
account_types: &rustledger_core::AccountTypes,
ctx: &DisplayContext,
format: &OutputFormat,
writer: &mut W,
) -> Result<()> {
let mut assets: BTreeMap<rustledger_core::Account, Inventory> = BTreeMap::new();
let mut liabilities: BTreeMap<rustledger_core::Account, Inventory> = BTreeMap::new();
let mut equity: BTreeMap<rustledger_core::Account, Inventory> = BTreeMap::new();
use rustledger_core::AccountTypeKind as K;
for (account, inv) in super::account_balances(directives) {
let section = match account_types.kind(&account) {
Some(K::Assets) => &mut assets,
Some(K::Liabilities) => &mut liabilities,
Some(K::Equity) => &mut equity,
_ => continue,
};
section.insert(account, inv);
}
fn sum_by_currency(
balances: &BTreeMap<rustledger_core::Account, Inventory>,
) -> BTreeMap<rustledger_core::Currency, Decimal> {
let mut totals: BTreeMap<rustledger_core::Currency, Decimal> = BTreeMap::new();
for inv in balances.values() {
for pos in inv.positions() {
*totals.entry(pos.units.currency.clone()).or_default() += pos.units.number;
}
}
totals
}
fn collect_rows(
section: &str,
balances: &BTreeMap<rustledger_core::Account, Inventory>,
ctx: &DisplayContext,
) -> Vec<(String, String, String, String, String)> {
let mut rows = Vec::new();
for (account, inventory) in balances {
if inventory.is_empty() {
continue;
}
for position in inventory.positions() {
let cost = position
.cost
.as_ref()
.map(|c| format!("{c}"))
.unwrap_or_default();
rows.push((
section.to_string(),
account.to_string(),
ctx.format_amount_number(position.units.number, &position.units.currency),
position.units.currency.to_string(),
cost,
));
}
}
rows
}
let mut all_rows = Vec::new();
all_rows.extend(collect_rows("Assets", &assets, ctx));
all_rows.extend(collect_rows("Liabilities", &liabilities, ctx));
all_rows.extend(collect_rows("Equity", &equity, ctx));
let asset_totals = sum_by_currency(&assets);
let liability_totals = sum_by_currency(&liabilities);
let mut net_worth: BTreeMap<rustledger_core::Currency, Decimal> = asset_totals;
for (currency, amount) in &liability_totals {
*net_worth.entry(currency.clone()).or_default() += amount;
}
match format {
OutputFormat::Csv => {
writeln!(writer, "section,account,amount,currency,cost")?;
for (section, account, amount, currency, cost) in &all_rows {
writeln!(
writer,
"{},{},{},{},{}",
section,
csv_escape(account),
csv_escape(amount),
currency,
csv_escape(cost)
)?;
}
for (currency, total) in &net_worth {
let total = csv_escape(&ctx.format_amount_number(*total, currency));
writeln!(writer, "Net Worth,TOTAL,{total},{currency},")?;
}
}
OutputFormat::Json => {
writeln!(writer, "{{")?;
writeln!(writer, r#" "accounts": ["#)?;
for (i, (section, account, amount, currency, cost)) in all_rows.iter().enumerate() {
let comma = if i < all_rows.len() - 1 { "," } else { "" };
writeln!(
writer,
r#" {{"section": "{}", "account": "{}", "amount": "{}", "currency": "{}", "cost": "{}"}}{}"#,
section,
json_escape(account),
amount,
currency,
json_escape(cost),
comma
)?;
}
writeln!(writer, " ],")?;
writeln!(writer, r#" "net_worth": {{"#)?;
let nw_vec: Vec<_> = net_worth.iter().collect();
for (i, (currency, total)) in nw_vec.iter().enumerate() {
let comma = if i < nw_vec.len() - 1 { "," } else { "" };
let total = ctx.format_amount_number(**total, currency);
writeln!(writer, r#" "{currency}": "{total}"{comma}"#)?;
}
writeln!(writer, " }}")?;
writeln!(writer, "}}")?;
}
OutputFormat::Text => {
fn write_section<W: Write>(
writer: &mut W,
title: &str,
balances: &BTreeMap<rustledger_core::Account, Inventory>,
ctx: &DisplayContext,
) -> Result<BTreeMap<rustledger_core::Currency, Decimal>> {
writeln!(writer, "{title}")?;
writeln!(writer, "{}", "-".repeat(60))?;
for (account, inventory) in balances {
if inventory.is_empty() {
continue;
}
for position in inventory.positions() {
let cost = position
.cost
.as_ref()
.map(|c| format!(" {c}"))
.unwrap_or_default();
writeln!(
writer,
" {:>12} {:>4}{} {}",
ctx.format_amount_number(
position.units.number,
&position.units.currency
),
position.units.currency,
cost,
account
)?;
}
}
let mut totals: BTreeMap<rustledger_core::Currency, Decimal> = BTreeMap::new();
for inv in balances.values() {
for pos in inv.positions() {
*totals.entry(pos.units.currency.clone()).or_default() += pos.units.number;
}
}
writeln!(writer)?;
for (currency, total) in &totals {
let total = ctx.format_amount_number(*total, currency);
writeln!(writer, " {total:>12} {currency:>4} Total {title}")?;
}
writeln!(writer)?;
Ok(totals)
}
writeln!(writer, "Balance Sheet")?;
writeln!(writer, "{}", "=".repeat(60))?;
writeln!(writer)?;
write_section(writer, "Assets", &assets, ctx)?;
write_section(writer, "Liabilities", &liabilities, ctx)?;
write_section(writer, "Equity", &equity, ctx)?;
writeln!(writer, "Net Worth")?;
writeln!(writer, "{}", "-".repeat(60))?;
for (currency, total) in &net_worth {
let total = ctx.format_amount_number(*total, currency);
writeln!(writer, " {total:>12} {currency:>4}")?;
}
}
}
Ok(())
}