use std::collections::BTreeMap;
use datasynth_core::models::ChartOfAccounts;
use crate::manifest::ChartOfAccountsMaster;
#[derive(Debug, Clone)]
pub struct AccountNameDictionary {
map: BTreeMap<String, String>,
}
impl AccountNameDictionary {
pub fn get(&self, code: &str) -> String {
self.map
.get(code)
.cloned()
.unwrap_or_else(|| code.to_string())
}
pub fn from_coa_master(master: &ChartOfAccountsMaster, framework: &str) -> Self {
let mut map = builtin_canonical_map();
if let Some(coa) = master.frameworks.get(framework) {
extend_with_chart(&mut map, coa);
}
Self { map }
}
pub fn from_chart(coa: &ChartOfAccounts) -> Self {
let mut map = builtin_canonical_map();
extend_with_chart(&mut map, coa);
Self { map }
}
}
impl Default for AccountNameDictionary {
fn default() -> Self {
Self {
map: builtin_canonical_map(),
}
}
}
fn extend_with_chart(map: &mut BTreeMap<String, String>, coa: &ChartOfAccounts) {
for acct in &coa.accounts {
map.insert(acct.account_number.clone(), acct.short_description.clone());
}
}
fn builtin_canonical_map() -> BTreeMap<String, String> {
let mut m = BTreeMap::new();
m.insert("1000".to_string(), "Cash and cash equivalents".to_string());
m.insert("1010".to_string(), "Bank account".to_string());
m.insert("1020".to_string(), "Petty cash".to_string());
m.insert("1100".to_string(), "Trade receivables".to_string());
m.insert("1150".to_string(), "IC receivables".to_string());
m.insert("1160".to_string(), "Input VAT".to_string());
m.insert("1200".to_string(), "Inventory".to_string());
m.insert("1300".to_string(), "Prepaid expenses".to_string());
m.insert("1410".to_string(), "Finished goods".to_string());
m.insert("1420".to_string(), "Work in process".to_string());
m.insert("1450".to_string(), "Derivative asset".to_string());
m.insert("1460".to_string(), "Tax receivable".to_string());
m.insert(
"1500".to_string(),
"Property, plant & equipment".to_string(),
);
m.insert("1510".to_string(), "Accumulated depreciation".to_string());
m.insert("1600".to_string(), "Deferred tax asset".to_string());
m.insert(
"1850".to_string(),
"Investment in associates / JVs".to_string(),
);
m.insert("1900".to_string(), "Goodwill".to_string());
m.insert("1910".to_string(), "Customer relationships".to_string());
m.insert("1920".to_string(), "Trade name".to_string());
m.insert("1930".to_string(), "Technology".to_string());
m.insert("1950".to_string(), "Accumulated amortization".to_string());
m.insert("2000".to_string(), "Trade payables".to_string());
m.insert("2050".to_string(), "IC payables".to_string());
m.insert("2100".to_string(), "Sales tax payable".to_string());
m.insert("2110".to_string(), "VAT payable".to_string());
m.insert("2130".to_string(), "Income tax payable".to_string());
m.insert("2160".to_string(), "Interest payable".to_string());
m.insert("2200".to_string(), "Accrued expenses".to_string());
m.insert("2210".to_string(), "Accrued salaries".to_string());
m.insert("2300".to_string(), "Unearned revenue".to_string());
m.insert("2400".to_string(), "Short-term debt".to_string());
m.insert("2450".to_string(), "Provision liability".to_string());
m.insert("2460".to_string(), "Derivative liability".to_string());
m.insert("2500".to_string(), "Deferred tax liability".to_string());
m.insert("2600".to_string(), "Long-term debt".to_string());
m.insert("2700".to_string(), "IC payable (long-term)".to_string());
m.insert("3000".to_string(), "Common stock".to_string());
m.insert("3100".to_string(), "Additional paid-in capital".to_string());
m.insert("3200".to_string(), "Retained earnings (legacy)".to_string());
m.insert("3300".to_string(), "Retained earnings".to_string());
m.insert("3500".to_string(), "Non-controlling interest".to_string());
m.insert(
"3510".to_string(),
"OCI — cash flow hedge reserve".to_string(),
);
m.insert("4000".to_string(), "Product revenue".to_string());
m.insert("4010".to_string(), "Sales discounts".to_string());
m.insert(
"4020".to_string(),
"Sales returns and allowances".to_string(),
);
m.insert("4100".to_string(), "Service revenue".to_string());
m.insert("4500".to_string(), "IC revenue".to_string());
m.insert("4800".to_string(), "Purchase discount income".to_string());
m.insert("4850".to_string(), "Bargain purchase gain".to_string());
m.insert(
"4900".to_string(),
"Share of profit of associates".to_string(),
);
m.insert("5000".to_string(), "Cost of goods sold".to_string());
m.insert("5100".to_string(), "Raw materials".to_string());
m.insert("5200".to_string(), "Direct labor".to_string());
m.insert("5300".to_string(), "Manufacturing overhead".to_string());
m.insert("6000".to_string(), "Depreciation expense".to_string());
m.insert("6010".to_string(), "Amortization expense".to_string());
m.insert("6100".to_string(), "Salaries and wages".to_string());
m.insert("6200".to_string(), "Benefits".to_string());
m.insert("6300".to_string(), "Rent".to_string());
m.insert("6400".to_string(), "Utilities".to_string());
m.insert("6500".to_string(), "Office supplies".to_string());
m.insert("6600".to_string(), "Travel and entertainment".to_string());
m.insert("6700".to_string(), "Professional fees".to_string());
m.insert("6800".to_string(), "Insurance".to_string());
m.insert("6850".to_string(), "Provision expense".to_string());
m.insert("6900".to_string(), "Bad debt expense".to_string());
m.insert("7100".to_string(), "Interest expense".to_string());
m.insert("7400".to_string(), "Purchase discounts".to_string());
m.insert("7500".to_string(), "FX gain/loss".to_string());
m.insert("7510".to_string(), "Hedge ineffectiveness".to_string());
m.insert("8000".to_string(), "Tax expense".to_string());
m.insert("8100".to_string(), "Deferred tax expense".to_string());
m
}
#[cfg(test)]
mod tests {
use super::*;
use datasynth_core::models::{
AccountSubType, AccountType, CoAComplexity, GLAccount, IndustrySector,
};
fn coa_with(code_name_pairs: &[(&str, &str)]) -> ChartOfAccounts {
let mut coa = ChartOfAccounts::new(
"TEST_COA".to_string(),
"Test chart".to_string(),
"DE".to_string(),
IndustrySector::Manufacturing,
CoAComplexity::Small,
);
for (code, name) in code_name_pairs {
coa.add_account(GLAccount::new(
code.to_string(),
name.to_string(),
AccountType::Asset,
AccountSubType::AccountsReceivable,
));
}
coa
}
#[test]
fn default_resolves_canonical_codes() {
let dict = AccountNameDictionary::default();
assert_eq!(dict.get("1100"), "Trade receivables");
assert_eq!(dict.get("4000"), "Product revenue");
assert_eq!(dict.get("9999"), "9999");
}
#[test]
fn from_chart_overrides_canonical_labels() {
let coa = coa_with(&[("1100", "Forderungen aus Lieferungen und Leistungen")]);
let dict = AccountNameDictionary::from_chart(&coa);
assert_eq!(
dict.get("1100"),
"Forderungen aus Lieferungen und Leistungen",
"engagement label must win over built-in canonical label"
);
assert_eq!(dict.get("4000"), "Product revenue");
}
#[test]
fn empty_chart_falls_back_to_canonical() {
let coa = coa_with(&[]);
let dict = AccountNameDictionary::from_chart(&coa);
assert_eq!(dict.get("1100"), "Trade receivables");
}
#[test]
fn missing_framework_falls_back_to_canonical() {
let dict = AccountNameDictionary::from_coa_master(
&ChartOfAccountsMaster {
primary_framework: "us_gaap".to_string(),
frameworks: BTreeMap::new(),
coa_id: "TEST".to_string(),
},
"us_gaap",
);
assert_eq!(dict.get("1100"), "Trade receivables");
}
}