use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::aggregate::fs::account_names::AccountNameDictionary;
use crate::aggregate::nci::NciRollforward;
use crate::aggregate::pre_elim::AggregatedTb;
use crate::errors::GroupResult;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ConsolidatedIncomeStatement {
pub group_id: String,
pub period_end: NaiveDate,
pub currency: String,
pub revenue: Vec<IsLine>,
pub cost_of_goods_sold: Vec<IsLine>,
pub gross_profit: Decimal,
pub operating_expenses: Vec<IsLine>,
pub operating_income: Decimal,
pub other_income_expense: Vec<IsLine>,
pub net_income_before_tax: Decimal,
pub tax_expense: Decimal,
pub net_income: Decimal,
pub net_income_to_owners: Decimal,
pub net_income_to_nci: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IsLine {
pub account_code: String,
pub account_name: String,
pub amount: Decimal,
}
pub fn build_consolidated_income_statement(
post_elim_tb: &AggregatedTb,
nci_rollforwards: &[NciRollforward],
group_id: &str,
period_end: NaiveDate,
) -> GroupResult<ConsolidatedIncomeStatement> {
build_consolidated_income_statement_with_names(
post_elim_tb,
nci_rollforwards,
group_id,
period_end,
&AccountNameDictionary::default(),
)
}
pub fn build_consolidated_income_statement_with_names(
post_elim_tb: &AggregatedTb,
nci_rollforwards: &[NciRollforward],
group_id: &str,
period_end: NaiveDate,
account_names: &AccountNameDictionary,
) -> GroupResult<ConsolidatedIncomeStatement> {
let mut revenue: Vec<IsLine> = Vec::new();
let mut cogs: Vec<IsLine> = Vec::new();
let mut opex: Vec<IsLine> = Vec::new();
let mut other: Vec<IsLine> = Vec::new();
let mut tax_total: Decimal = Decimal::ZERO;
for (code, account) in &post_elim_tb.account_totals {
let section = classify_is_section(code);
let amount = match section {
IsSection::Revenue => account.credit_total - account.debit_total,
IsSection::Cogs | IsSection::Opex | IsSection::Tax => {
account.debit_total - account.credit_total
}
IsSection::Other => account.credit_total - account.debit_total,
IsSection::Excluded => continue,
};
let line = IsLine {
account_code: code.clone(),
account_name: account_names.get(code),
amount,
};
match section {
IsSection::Revenue => revenue.push(line),
IsSection::Cogs => cogs.push(line),
IsSection::Opex => opex.push(line),
IsSection::Other => other.push(line),
IsSection::Tax => tax_total += line.amount,
IsSection::Excluded => unreachable!(),
}
}
for v in [&mut revenue, &mut cogs, &mut opex, &mut other] {
v.sort_by(|a, b| a.account_code.cmp(&b.account_code));
}
let gross_profit = sum(&revenue) - sum(&cogs);
let operating_income = gross_profit - sum(&opex);
let net_income_before_tax = operating_income + sum(&other);
let net_income = net_income_before_tax - tax_total;
let net_income_to_nci = nci_rollforwards
.iter()
.map(|rf| rf.nci_share_of_profit)
.fold(Decimal::ZERO, |acc, v| acc + v);
let net_income_to_owners = net_income - net_income_to_nci;
Ok(ConsolidatedIncomeStatement {
group_id: group_id.to_string(),
period_end,
currency: post_elim_tb.currency.clone(),
revenue,
cost_of_goods_sold: cogs,
gross_profit,
operating_expenses: opex,
operating_income,
other_income_expense: other,
net_income_before_tax,
tax_expense: tax_total,
net_income,
net_income_to_owners,
net_income_to_nci,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IsSection {
Revenue,
Cogs,
Opex,
Other,
Tax,
Excluded,
}
fn classify_is_section(code: &str) -> IsSection {
let n: u32 = match code.parse() {
Ok(n) => n,
Err(_) => return IsSection::Excluded,
};
match n {
4000..=4499 => IsSection::Revenue,
4500..=4999 => IsSection::Other,
5000..=5099 => IsSection::Cogs,
5100..=6999 => IsSection::Opex,
7000..=7999 => IsSection::Other,
8000..=8499 => IsSection::Tax,
_ => IsSection::Excluded,
}
}
fn sum(lines: &[IsLine]) -> Decimal {
lines
.iter()
.map(|l| l.amount)
.fold(Decimal::ZERO, |acc, v| acc + v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifier_assigns_canonical_ranges() {
assert_eq!(classify_is_section("4000"), IsSection::Revenue);
assert_eq!(classify_is_section("4499"), IsSection::Revenue);
assert_eq!(classify_is_section("4500"), IsSection::Other);
assert_eq!(classify_is_section("4900"), IsSection::Other);
assert_eq!(classify_is_section("5000"), IsSection::Cogs);
assert_eq!(classify_is_section("5099"), IsSection::Cogs);
assert_eq!(classify_is_section("5100"), IsSection::Opex);
assert_eq!(classify_is_section("6999"), IsSection::Opex);
assert_eq!(classify_is_section("7100"), IsSection::Other);
assert_eq!(classify_is_section("8000"), IsSection::Tax);
assert_eq!(classify_is_section("1000"), IsSection::Excluded);
assert_eq!(classify_is_section("3000"), IsSection::Excluded);
assert_eq!(classify_is_section(""), IsSection::Excluded);
}
}