use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::aggregate::pre_elim::AggregatedTb;
use crate::errors::GroupResult;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ConsolidatedCashFlow {
pub group_id: String,
pub period_start: NaiveDate,
pub period_end: NaiveDate,
pub currency: String,
pub operating: CfSection,
pub investing: CfSection,
pub financing: CfSection,
pub opening_cash: Decimal,
pub closing_cash: Decimal,
pub net_change_in_cash: Decimal,
pub fx_effect_on_cash: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CfSection {
pub lines: Vec<CfLine>,
pub subtotal: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CfLine {
pub label: String,
pub amount: Decimal,
}
pub struct CashFlowInputs<'a> {
pub post_elim_tb_current: &'a AggregatedTb,
pub post_elim_tb_prior: Option<&'a AggregatedTb>,
pub net_income: Decimal,
pub depreciation_amortization: Decimal,
pub impairment: Decimal,
pub capex: Decimal,
pub debt_issuance: Decimal,
pub debt_repayment: Decimal,
pub dividends_paid_to_owners: Decimal,
pub dividends_paid_to_nci: Decimal,
pub equity_issuance: Decimal,
}
pub fn build_consolidated_cash_flow(
inputs: &CashFlowInputs,
group_id: &str,
period_start: NaiveDate,
period_end: NaiveDate,
) -> GroupResult<ConsolidatedCashFlow> {
let mut operating_lines: Vec<CfLine> = Vec::new();
operating_lines.push(CfLine {
label: "Net income".to_string(),
amount: inputs.net_income,
});
if inputs.depreciation_amortization != Decimal::ZERO {
operating_lines.push(CfLine {
label: "Depreciation and amortization".to_string(),
amount: inputs.depreciation_amortization,
});
}
if inputs.impairment != Decimal::ZERO {
operating_lines.push(CfLine {
label: "Impairment".to_string(),
amount: inputs.impairment,
});
}
let (delta_ar, delta_ap, delta_inventory) =
working_capital_changes(inputs.post_elim_tb_current, inputs.post_elim_tb_prior);
if delta_ar != Decimal::ZERO {
operating_lines.push(CfLine {
label: "Change in trade receivables".to_string(),
amount: -delta_ar,
});
}
if delta_inventory != Decimal::ZERO {
operating_lines.push(CfLine {
label: "Change in inventory".to_string(),
amount: -delta_inventory,
});
}
if delta_ap != Decimal::ZERO {
operating_lines.push(CfLine {
label: "Change in trade payables".to_string(),
amount: delta_ap,
});
}
let operating_subtotal = sum_lines(&operating_lines);
let mut investing_lines: Vec<CfLine> = Vec::new();
if inputs.capex != Decimal::ZERO {
investing_lines.push(CfLine {
label: "Capital expenditure".to_string(),
amount: -inputs.capex,
});
}
let investing_subtotal = sum_lines(&investing_lines);
let mut financing_lines: Vec<CfLine> = Vec::new();
if inputs.debt_issuance != Decimal::ZERO {
financing_lines.push(CfLine {
label: "Debt issuance".to_string(),
amount: inputs.debt_issuance,
});
}
if inputs.debt_repayment != Decimal::ZERO {
financing_lines.push(CfLine {
label: "Debt repayment".to_string(),
amount: -inputs.debt_repayment,
});
}
if inputs.equity_issuance != Decimal::ZERO {
financing_lines.push(CfLine {
label: "Equity issuance".to_string(),
amount: inputs.equity_issuance,
});
}
if inputs.dividends_paid_to_owners != Decimal::ZERO {
financing_lines.push(CfLine {
label: "Dividends paid to owners".to_string(),
amount: -inputs.dividends_paid_to_owners,
});
}
if inputs.dividends_paid_to_nci != Decimal::ZERO {
financing_lines.push(CfLine {
label: "Dividends paid to non-controlling interest".to_string(),
amount: -inputs.dividends_paid_to_nci,
});
}
let financing_subtotal = sum_lines(&financing_lines);
let opening_cash = match inputs.post_elim_tb_prior {
Some(prior) => sum_cash(prior),
None => Decimal::ZERO,
};
let closing_cash = sum_cash(inputs.post_elim_tb_current);
let net_change_in_cash = operating_subtotal + investing_subtotal + financing_subtotal;
let fx_effect_on_cash = closing_cash - opening_cash - net_change_in_cash;
Ok(ConsolidatedCashFlow {
group_id: group_id.to_string(),
period_start,
period_end,
currency: inputs.post_elim_tb_current.currency.clone(),
operating: CfSection {
lines: operating_lines,
subtotal: operating_subtotal,
},
investing: CfSection {
lines: investing_lines,
subtotal: investing_subtotal,
},
financing: CfSection {
lines: financing_lines,
subtotal: financing_subtotal,
},
opening_cash,
closing_cash,
net_change_in_cash,
fx_effect_on_cash,
})
}
fn sum_lines(lines: &[CfLine]) -> Decimal {
lines
.iter()
.map(|l| l.amount)
.fold(Decimal::ZERO, |acc, v| acc + v)
}
fn sum_cash(tb: &AggregatedTb) -> Decimal {
tb.account_totals
.iter()
.filter(|(code, _)| is_cash(code))
.map(|(_, a)| a.debit_total - a.credit_total)
.fold(Decimal::ZERO, |acc, v| acc + v)
}
fn is_cash(code: &str) -> bool {
let n: u32 = match code.parse() {
Ok(n) => n,
Err(_) => return false,
};
(1000..=1099).contains(&n)
}
fn working_capital_changes(
current: &AggregatedTb,
prior: Option<&AggregatedTb>,
) -> (Decimal, Decimal, Decimal) {
let prior = match prior {
Some(p) => p,
None => return (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
};
let cur_ar = sum_natural_balance(current, "1100");
let prior_ar = sum_natural_balance(prior, "1100");
let cur_ap = sum_natural_balance_credit(current, "2000");
let prior_ap = sum_natural_balance_credit(prior, "2000");
let cur_inv = sum_natural_balance(current, "1200");
let prior_inv = sum_natural_balance(prior, "1200");
(cur_ar - prior_ar, cur_ap - prior_ap, cur_inv - prior_inv)
}
fn sum_natural_balance(tb: &AggregatedTb, code: &str) -> Decimal {
tb.account_totals
.get(code)
.map(|a| a.debit_total - a.credit_total)
.unwrap_or(Decimal::ZERO)
}
fn sum_natural_balance_credit(tb: &AggregatedTb, code: &str) -> Decimal {
tb.account_totals
.get(code)
.map(|a| a.credit_total - a.debit_total)
.unwrap_or(Decimal::ZERO)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn is_cash_classifier() {
assert!(is_cash("1000"));
assert!(is_cash("1099"));
assert!(!is_cash("1100"));
assert!(!is_cash("999"));
assert!(!is_cash("abc"));
assert!(!is_cash(""));
}
#[test]
fn sum_lines_zero_when_empty() {
assert_eq!(sum_lines(&[]), Decimal::ZERO);
}
#[test]
fn sum_lines_signs_preserved() {
let lines = vec![
CfLine {
label: "a".to_string(),
amount: dec!(100),
},
CfLine {
label: "b".to_string(),
amount: dec!(-30),
},
];
assert_eq!(sum_lines(&lines), dec!(70));
}
}