use super::funejson::Value; use super::finance_ledger_journal_validate::{journal_line_from_value, validate_journal, JournalLine}; use super::money_add::add_money; use super::money_amount::{money, money_to_value, Money}; const FATAL: [&str; 3] = ["invalid-account", "currency-mismatch", "negative-amount"]; /// A trial balance: each account's balance on its own side, and whether the /// debit column equals the credit column. /// /// # Panics /// Panics, with journal-validate's message, on a line that cannot be totalled: /// another currency, a negative amount or a malformed account code. An /// unbalanced ledger is an answer, not a panic. pub fn trial_balance(lines: &[JournalLine], currency: &str) -> TrialBalance { let check = validate_journal(lines, currency); for problem in &check.problems { if FATAL.contains(&problem.code.as_str()) { panic!("{}", problem.message); } } let zero = money(0, currency); let mut totals: Vec<(String, Money, Money)> = Vec::new(); for line in lines { if line.debit.minor == 0 && line.credit.minor == 0 { continue; } match totals.iter_mut().find(|(account, _, _)| *account == line.account) { Some(entry) => { entry.1 = add_money(&entry.1, &line.debit); entry.2 = add_money(&entry.2, &line.credit); } None => totals.push((line.account.clone(), line.debit.clone(), line.credit.clone())), } } totals.sort_by(|a, b| a.0.cmp(&b.0)); let mut rows: Vec = Vec::with_capacity(totals.len()); let mut total_debit = zero.clone(); let mut total_credit = zero; for (account, debits, credits) in totals { let net = debits.minor - credits.minor; let debit_balance = money(if net > 0 { net } else { 0 }, currency); let credit_balance = money(if net < 0 { -net } else { 0 }, currency); total_debit = add_money(&total_debit, &debit_balance); total_credit = add_money(&total_credit, &credit_balance); rows.push(TrialBalanceRow { account, debits, credits, debit_balance, credit_balance, }); } let balanced = total_debit.minor == total_credit.minor; TrialBalance { rows, total_debit, total_credit, balanced, } } pub fn trial_balance_to_value(tb: &TrialBalance) -> Value { Value::obj(vec![ ( "rows", Value::Arr( tb.rows .iter() .map(|r| { Value::obj(vec![ ("account", Value::str(&r.account)), ("debits", money_to_value(&r.debits)), ("credits", money_to_value(&r.credits)), ("debitBalance", money_to_value(&r.debit_balance)), ("creditBalance", money_to_value(&r.credit_balance)), ]) }) .collect(), ), ), ("totalDebit", money_to_value(&tb.total_debit)), ("totalCredit", money_to_value(&tb.total_credit)), ("balanced", Value::Bool(tb.balanced)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let lines: Vec = args[0].as_arr().iter().map(journal_line_from_value).collect(); trial_balance_to_value(&trial_balance(&lines, args[1].as_str())) }