use chrono::NaiveDate;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::account_balance::AccountType;
use super::trial_balance::AccountCategory;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpeningBalanceSpec {
pub company_code: String,
pub as_of_date: NaiveDate,
pub fiscal_year: i32,
pub currency: String,
pub total_assets: Decimal,
pub industry: IndustryType,
pub asset_composition: AssetComposition,
pub capital_structure: CapitalStructure,
pub target_ratios: TargetRatios,
pub account_overrides: HashMap<String, AccountSpec>,
}
impl OpeningBalanceSpec {
pub fn new(
company_code: String,
as_of_date: NaiveDate,
fiscal_year: i32,
currency: String,
total_assets: Decimal,
industry: IndustryType,
) -> Self {
Self {
company_code,
as_of_date,
fiscal_year,
currency,
total_assets,
industry,
asset_composition: AssetComposition::for_industry(industry),
capital_structure: CapitalStructure::default(),
target_ratios: TargetRatios::for_industry(industry),
account_overrides: HashMap::new(),
}
}
pub fn for_industry(total_assets: Decimal, industry: IndustryType) -> Self {
Self {
company_code: String::new(),
as_of_date: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
fiscal_year: 2024,
currency: "USD".to_string(),
total_assets,
industry,
asset_composition: AssetComposition::for_industry(industry),
capital_structure: CapitalStructure::for_industry(industry),
target_ratios: TargetRatios::for_industry(industry),
account_overrides: HashMap::new(),
}
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let asset_total = self.asset_composition.total_percentage();
if (asset_total - dec!(100)).abs() > dec!(0.01) {
errors.push(format!(
"Asset composition should sum to 100%, got {asset_total}%"
));
}
let capital_total =
self.capital_structure.debt_percent + self.capital_structure.equity_percent;
if (capital_total - dec!(100)).abs() > dec!(0.01) {
errors.push(format!(
"Capital structure should sum to 100%, got {capital_total}%"
));
}
if self.target_ratios.current_ratio < dec!(0.5) {
errors.push("Current ratio below 0.5 indicates severe liquidity problems".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn calculate_total_liabilities(&self) -> Decimal {
self.total_assets * self.capital_structure.debt_percent / dec!(100)
}
pub fn calculate_total_equity(&self) -> Decimal {
self.total_assets * self.capital_structure.equity_percent / dec!(100)
}
pub fn calculate_current_assets(&self) -> Decimal {
self.total_assets * self.asset_composition.current_assets_percent / dec!(100)
}
pub fn calculate_non_current_assets(&self) -> Decimal {
self.total_assets * (dec!(100) - self.asset_composition.current_assets_percent) / dec!(100)
}
pub fn calculate_current_liabilities(&self) -> Decimal {
let current_assets = self.calculate_current_assets();
if self.target_ratios.current_ratio > Decimal::ZERO {
current_assets / self.target_ratios.current_ratio
} else {
Decimal::ZERO
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IndustryType {
#[default]
Manufacturing,
Retail,
Services,
Technology,
Financial,
Healthcare,
Utilities,
RealEstate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetComposition {
pub current_assets_percent: Decimal,
pub cash_percent: Decimal,
pub ar_percent: Decimal,
pub inventory_percent: Decimal,
pub prepaid_percent: Decimal,
pub other_current_percent: Decimal,
pub ppe_percent: Decimal,
pub intangibles_percent: Decimal,
pub investments_percent: Decimal,
pub other_noncurrent_percent: Decimal,
}
impl AssetComposition {
pub fn for_industry(industry: IndustryType) -> Self {
match industry {
IndustryType::Manufacturing => Self {
current_assets_percent: dec!(40),
cash_percent: dec!(15),
ar_percent: dec!(30),
inventory_percent: dec!(45),
prepaid_percent: dec!(5),
other_current_percent: dec!(5),
ppe_percent: dec!(70),
intangibles_percent: dec!(10),
investments_percent: dec!(10),
other_noncurrent_percent: dec!(10),
},
IndustryType::Retail => Self {
current_assets_percent: dec!(55),
cash_percent: dec!(10),
ar_percent: dec!(15),
inventory_percent: dec!(65),
prepaid_percent: dec!(5),
other_current_percent: dec!(5),
ppe_percent: dec!(60),
intangibles_percent: dec!(20),
investments_percent: dec!(10),
other_noncurrent_percent: dec!(10),
},
IndustryType::Services => Self {
current_assets_percent: dec!(50),
cash_percent: dec!(25),
ar_percent: dec!(50),
inventory_percent: dec!(5),
prepaid_percent: dec!(10),
other_current_percent: dec!(10),
ppe_percent: dec!(40),
intangibles_percent: dec!(30),
investments_percent: dec!(15),
other_noncurrent_percent: dec!(15),
},
IndustryType::Technology => Self {
current_assets_percent: dec!(60),
cash_percent: dec!(40),
ar_percent: dec!(35),
inventory_percent: dec!(5),
prepaid_percent: dec!(10),
other_current_percent: dec!(10),
ppe_percent: dec!(25),
intangibles_percent: dec!(50),
investments_percent: dec!(15),
other_noncurrent_percent: dec!(10),
},
IndustryType::Financial => Self {
current_assets_percent: dec!(70),
cash_percent: dec!(30),
ar_percent: dec!(40),
inventory_percent: dec!(0),
prepaid_percent: dec!(5),
other_current_percent: dec!(25),
ppe_percent: dec!(20),
intangibles_percent: dec!(30),
investments_percent: dec!(40),
other_noncurrent_percent: dec!(10),
},
IndustryType::Healthcare => Self {
current_assets_percent: dec!(35),
cash_percent: dec!(20),
ar_percent: dec!(50),
inventory_percent: dec!(15),
prepaid_percent: dec!(10),
other_current_percent: dec!(5),
ppe_percent: dec!(60),
intangibles_percent: dec!(20),
investments_percent: dec!(10),
other_noncurrent_percent: dec!(10),
},
IndustryType::Utilities => Self {
current_assets_percent: dec!(15),
cash_percent: dec!(20),
ar_percent: dec!(50),
inventory_percent: dec!(15),
prepaid_percent: dec!(10),
other_current_percent: dec!(5),
ppe_percent: dec!(85),
intangibles_percent: dec!(5),
investments_percent: dec!(5),
other_noncurrent_percent: dec!(5),
},
IndustryType::RealEstate => Self {
current_assets_percent: dec!(10),
cash_percent: dec!(30),
ar_percent: dec!(40),
inventory_percent: dec!(10),
prepaid_percent: dec!(10),
other_current_percent: dec!(10),
ppe_percent: dec!(90),
intangibles_percent: dec!(3),
investments_percent: dec!(5),
other_noncurrent_percent: dec!(2),
},
}
}
pub fn total_percentage(&self) -> Decimal {
let current = self.cash_percent
+ self.ar_percent
+ self.inventory_percent
+ self.prepaid_percent
+ self.other_current_percent;
let noncurrent = self.ppe_percent
+ self.intangibles_percent
+ self.investments_percent
+ self.other_noncurrent_percent;
if (current - dec!(100)).abs() > dec!(1) || (noncurrent - dec!(100)).abs() > dec!(1) {
current
} else {
dec!(100)
}
}
}
impl Default for AssetComposition {
fn default() -> Self {
Self::for_industry(IndustryType::Manufacturing)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapitalStructure {
pub debt_percent: Decimal,
pub equity_percent: Decimal,
pub current_liabilities_percent: Decimal,
pub long_term_debt_percent: Decimal,
pub other_liabilities_percent: Decimal,
pub common_stock_percent: Decimal,
pub apic_percent: Decimal,
pub retained_earnings_percent: Decimal,
pub other_equity_percent: Decimal,
}
impl Default for CapitalStructure {
fn default() -> Self {
Self {
debt_percent: dec!(40),
equity_percent: dec!(60),
current_liabilities_percent: dec!(50),
long_term_debt_percent: dec!(40),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(15),
apic_percent: dec!(25),
retained_earnings_percent: dec!(55),
other_equity_percent: dec!(5),
}
}
}
impl CapitalStructure {
pub fn for_industry(industry: IndustryType) -> Self {
match industry {
IndustryType::Manufacturing => Self {
debt_percent: dec!(40),
equity_percent: dec!(60),
current_liabilities_percent: dec!(50),
long_term_debt_percent: dec!(40),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(15),
apic_percent: dec!(25),
retained_earnings_percent: dec!(55),
other_equity_percent: dec!(5),
},
IndustryType::Retail => Self {
debt_percent: dec!(45),
equity_percent: dec!(55),
current_liabilities_percent: dec!(60),
long_term_debt_percent: dec!(30),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(20),
apic_percent: dec!(20),
retained_earnings_percent: dec!(55),
other_equity_percent: dec!(5),
},
IndustryType::Services => Self {
debt_percent: dec!(30),
equity_percent: dec!(70),
current_liabilities_percent: dec!(55),
long_term_debt_percent: dec!(35),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(15),
apic_percent: dec!(30),
retained_earnings_percent: dec!(50),
other_equity_percent: dec!(5),
},
IndustryType::Technology => Self {
debt_percent: dec!(25),
equity_percent: dec!(75),
current_liabilities_percent: dec!(60),
long_term_debt_percent: dec!(30),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(10),
apic_percent: dec!(40),
retained_earnings_percent: dec!(45),
other_equity_percent: dec!(5),
},
IndustryType::Financial => Self {
debt_percent: dec!(70),
equity_percent: dec!(30),
current_liabilities_percent: dec!(70),
long_term_debt_percent: dec!(20),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(25),
apic_percent: dec!(35),
retained_earnings_percent: dec!(35),
other_equity_percent: dec!(5),
},
IndustryType::Healthcare => Self {
debt_percent: dec!(35),
equity_percent: dec!(65),
current_liabilities_percent: dec!(50),
long_term_debt_percent: dec!(40),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(15),
apic_percent: dec!(30),
retained_earnings_percent: dec!(50),
other_equity_percent: dec!(5),
},
IndustryType::Utilities => Self {
debt_percent: dec!(55),
equity_percent: dec!(45),
current_liabilities_percent: dec!(35),
long_term_debt_percent: dec!(55),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(20),
apic_percent: dec!(25),
retained_earnings_percent: dec!(50),
other_equity_percent: dec!(5),
},
IndustryType::RealEstate => Self {
debt_percent: dec!(60),
equity_percent: dec!(40),
current_liabilities_percent: dec!(30),
long_term_debt_percent: dec!(60),
other_liabilities_percent: dec!(10),
common_stock_percent: dec!(25),
apic_percent: dec!(30),
retained_earnings_percent: dec!(40),
other_equity_percent: dec!(5),
},
}
}
pub fn with_debt_equity_ratio(ratio: Decimal) -> Self {
let equity_percent = dec!(100) / (Decimal::ONE + ratio);
let debt_percent = dec!(100) - equity_percent;
Self {
debt_percent,
equity_percent,
..Default::default()
}
}
pub fn debt_equity_ratio(&self) -> Decimal {
if self.equity_percent > Decimal::ZERO {
self.debt_percent / self.equity_percent
} else {
Decimal::MAX
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetRatios {
pub current_ratio: Decimal,
pub quick_ratio: Decimal,
pub debt_to_equity: Decimal,
pub asset_turnover: Decimal,
pub target_dso_days: u32,
pub target_dpo_days: u32,
pub target_dio_days: u32,
pub gross_margin: Decimal,
pub operating_margin: Decimal,
}
impl TargetRatios {
pub fn for_industry(industry: IndustryType) -> Self {
match industry {
IndustryType::Manufacturing => Self {
current_ratio: dec!(1.5),
quick_ratio: dec!(0.8),
debt_to_equity: dec!(0.6),
asset_turnover: dec!(1.2),
target_dso_days: 45,
target_dpo_days: 35,
target_dio_days: 60,
gross_margin: dec!(0.35),
operating_margin: dec!(0.12),
},
IndustryType::Retail => Self {
current_ratio: dec!(1.2),
quick_ratio: dec!(0.4),
debt_to_equity: dec!(0.8),
asset_turnover: dec!(2.5),
target_dso_days: 15,
target_dpo_days: 30,
target_dio_days: 45,
gross_margin: dec!(0.30),
operating_margin: dec!(0.08),
},
IndustryType::Services => Self {
current_ratio: dec!(1.8),
quick_ratio: dec!(1.6),
debt_to_equity: dec!(0.4),
asset_turnover: dec!(1.5),
target_dso_days: 60,
target_dpo_days: 25,
target_dio_days: 0,
gross_margin: dec!(0.45),
operating_margin: dec!(0.18),
},
IndustryType::Technology => Self {
current_ratio: dec!(2.5),
quick_ratio: dec!(2.3),
debt_to_equity: dec!(0.3),
asset_turnover: dec!(0.8),
target_dso_days: 55,
target_dpo_days: 40,
target_dio_days: 15,
gross_margin: dec!(0.65),
operating_margin: dec!(0.25),
},
IndustryType::Financial => Self {
current_ratio: dec!(1.1),
quick_ratio: dec!(1.1),
debt_to_equity: dec!(2.0),
asset_turnover: dec!(0.3),
target_dso_days: 30,
target_dpo_days: 20,
target_dio_days: 0,
gross_margin: dec!(0.80),
operating_margin: dec!(0.30),
},
IndustryType::Healthcare => Self {
current_ratio: dec!(1.4),
quick_ratio: dec!(1.1),
debt_to_equity: dec!(0.5),
asset_turnover: dec!(1.0),
target_dso_days: 50,
target_dpo_days: 30,
target_dio_days: 30,
gross_margin: dec!(0.40),
operating_margin: dec!(0.15),
},
IndustryType::Utilities => Self {
current_ratio: dec!(0.9),
quick_ratio: dec!(0.7),
debt_to_equity: dec!(1.2),
asset_turnover: dec!(0.4),
target_dso_days: 40,
target_dpo_days: 45,
target_dio_days: 20,
gross_margin: dec!(0.35),
operating_margin: dec!(0.20),
},
IndustryType::RealEstate => Self {
current_ratio: dec!(1.0),
quick_ratio: dec!(0.8),
debt_to_equity: dec!(1.5),
asset_turnover: dec!(0.2),
target_dso_days: 30,
target_dpo_days: 25,
target_dio_days: 0,
gross_margin: dec!(0.50),
operating_margin: dec!(0.35),
},
}
}
pub fn calculate_target_ar(&self, annual_revenue: Decimal) -> Decimal {
annual_revenue * Decimal::from(self.target_dso_days) / dec!(365)
}
pub fn calculate_target_ap(&self, annual_cogs: Decimal) -> Decimal {
annual_cogs * Decimal::from(self.target_dpo_days) / dec!(365)
}
pub fn calculate_target_inventory(&self, annual_cogs: Decimal) -> Decimal {
annual_cogs * Decimal::from(self.target_dio_days) / dec!(365)
}
}
impl Default for TargetRatios {
fn default() -> Self {
Self::for_industry(IndustryType::Manufacturing)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountSpec {
pub account_code: String,
pub description: String,
pub account_type: AccountType,
pub category: AccountCategory,
pub fixed_balance: Option<Decimal>,
pub category_percent: Option<Decimal>,
pub total_assets_percent: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedOpeningBalance {
pub company_code: String,
pub as_of_date: NaiveDate,
pub balances: HashMap<String, Decimal>,
pub total_assets: Decimal,
pub total_liabilities: Decimal,
pub total_equity: Decimal,
pub is_balanced: bool,
pub calculated_ratios: CalculatedRatios,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalculatedRatios {
pub current_ratio: Option<Decimal>,
pub quick_ratio: Option<Decimal>,
pub debt_to_equity: Option<Decimal>,
pub working_capital: Decimal,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_opening_balance_spec_creation() {
let spec = OpeningBalanceSpec::new(
"1000".to_string(),
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
2022,
"USD".to_string(),
dec!(1000000),
IndustryType::Manufacturing,
);
assert!(spec.validate().is_ok());
assert_eq!(spec.calculate_total_liabilities(), dec!(400000)); assert_eq!(spec.calculate_total_equity(), dec!(600000)); }
#[test]
fn test_capital_structure_debt_equity() {
let structure = CapitalStructure::with_debt_equity_ratio(dec!(0.5));
assert!((structure.equity_percent - dec!(66.67)).abs() < dec!(0.01));
assert!((structure.debt_percent - dec!(33.33)).abs() < dec!(0.01));
assert!((structure.debt_equity_ratio() - dec!(0.5)).abs() < dec!(0.01));
}
#[test]
fn test_asset_composition_for_industries() {
let manufacturing = AssetComposition::for_industry(IndustryType::Manufacturing);
assert_eq!(manufacturing.current_assets_percent, dec!(40));
let retail = AssetComposition::for_industry(IndustryType::Retail);
assert_eq!(retail.current_assets_percent, dec!(55));
assert!(retail.inventory_percent > manufacturing.inventory_percent);
let technology = AssetComposition::for_industry(IndustryType::Technology);
assert!(technology.intangibles_percent > manufacturing.intangibles_percent);
}
#[test]
fn test_target_ratios_calculations() {
let ratios = TargetRatios::for_industry(IndustryType::Manufacturing);
let annual_revenue = dec!(1000000);
let annual_cogs = dec!(650000);
let target_ar = ratios.calculate_target_ar(annual_revenue);
assert!(target_ar > dec!(120000) && target_ar < dec!(130000));
let target_inventory = ratios.calculate_target_inventory(annual_cogs);
assert!(target_inventory > dec!(100000) && target_inventory < dec!(115000));
}
#[test]
fn test_opening_balance_validation() {
let mut spec = OpeningBalanceSpec::new(
"1000".to_string(),
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
2022,
"USD".to_string(),
dec!(1000000),
IndustryType::Manufacturing,
);
assert!(spec.validate().is_ok());
spec.capital_structure.debt_percent = dec!(80);
spec.capital_structure.equity_percent = dec!(30); assert!(spec.validate().is_err());
}
}