use rand::RngExt;
use tracing::debug;
use datasynth_core::accounts::{
asset_class_accounts, cash_accounts, control_accounts, dividend_accounts, dormant_accounts,
equity_accounts, expense_accounts, intangible_accounts, inventory_accounts, liability_accounts,
manufacturing_accounts, provision_accounts, revenue_accounts, suspense_accounts, tax_accounts,
treasury_accounts,
};
use datasynth_core::models::*;
use datasynth_core::pcg_loader;
use datasynth_core::skr_loader;
use datasynth_core::traits::Generator;
use datasynth_core::utils::seeded_rng;
use rand_chacha::ChaCha8Rng;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CoAFramework {
#[default]
UsGaap,
FrenchPcg,
GermanSkr04,
}
pub struct ChartOfAccountsGenerator {
rng: ChaCha8Rng,
seed: u64,
complexity: CoAComplexity,
industry: IndustrySector,
count: u64,
coa_framework: CoAFramework,
expand_industry_subaccounts: bool,
}
pub fn overlay_coa_semantic(
coa: &mut ChartOfAccounts,
prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
) -> usize {
let mut applied = 0usize;
for account in coa.accounts.iter_mut() {
if let Some(sem) = prior.accounts.get(&account.account_number) {
if !sem.description.is_empty() {
account.short_description.clone_from(&sem.description);
account.long_description.clone_from(&sem.description);
}
if let Some(ref cls) = sem.account_class {
if !cls.is_empty() {
account.account_class.clone_from(cls);
}
}
if let Some(ref cls_name) = sem.account_class_name {
if !cls_name.is_empty() {
account.account_class_name.clone_from(cls_name);
}
}
if let Some(ref sub) = sem.account_sub_class {
if !sub.is_empty() {
account.account_sub_class.clone_from(sub);
}
}
if let Some(ref sub_name) = sem.account_sub_class_name {
if !sub_name.is_empty() {
account.account_sub_class_name.clone_from(sub_name);
}
}
applied += 1;
}
}
applied
}
pub fn remap_account_numbers_to_prior<R: rand::Rng>(
coa: &mut ChartOfAccounts,
prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
rng: &mut R,
) -> usize {
use std::collections::HashMap;
fn normalise_type(s: &str) -> &'static str {
match s.trim().to_lowercase().as_str() {
"asset" | "assets" => "asset",
"liability" | "liabilities" => "liability",
"equity" => "equity",
"revenue" | "revenues" | "income" => "revenue",
"expense" | "expenses" | "cost" => "expense",
_ => "",
}
}
let mut by_type: HashMap<&'static str, Vec<&String>> = HashMap::new();
for (account_number, semantic) in &prior.accounts {
let type_key = match &semantic.account_type {
Some(t) => normalise_type(t),
None => "",
};
if type_key.is_empty() {
continue;
}
by_type.entry(type_key).or_default().push(account_number);
}
let all_prior: Vec<&String> = prior.accounts.keys().collect();
const REMAP_SHARE: f64 = 0.80;
let mut remapped = 0usize;
for account in coa.accounts.iter_mut() {
if rng.random_range(0.0..1.0_f64) >= REMAP_SHARE {
continue;
}
let type_key: &'static str = match account.account_type {
AccountType::Asset => "asset",
AccountType::Liability => "liability",
AccountType::Equity => "equity",
AccountType::Revenue => "revenue",
AccountType::Expense => "expense",
AccountType::Statistical => "",
};
let candidates: &[&String] = if !type_key.is_empty() {
by_type.get(type_key).map(|v| v.as_slice()).unwrap_or(&[])
} else {
&[]
};
let chosen = if !candidates.is_empty() {
let idx = rng.random_range(0..candidates.len());
Some(candidates[idx].clone())
} else if !all_prior.is_empty() {
let idx = rng.random_range(0..all_prior.len());
Some(all_prior[idx].clone())
} else {
None
};
if let Some(new_number) = chosen {
account.account_number = new_number;
remapped += 1;
}
}
remapped
}
pub fn overlay_coa_taxonomy<R: rand::Rng>(
coa: &mut ChartOfAccounts,
taxonomy: &datasynth_core::distributions::text_taxonomy::TextTaxonomyPrior,
resolver: &mut dyn datasynth_core::distributions::text_taxonomy::PlaceholderResolver,
rng: &mut R,
) {
use datasynth_core::distributions::text_taxonomy::PlaceholderGrammar;
for account in coa.accounts.iter_mut() {
if let Some(entry) = taxonomy.coa_pools.get(&account.account_number) {
let filled = PlaceholderGrammar::fill(&entry.template, resolver, rng);
if !filled.is_empty() {
account.short_description.clone_from(&filled);
account.long_description = filled;
}
}
}
}
impl ChartOfAccountsGenerator {
pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
Self {
rng: seeded_rng(seed, 0),
seed,
complexity,
industry,
count: 0,
coa_framework: CoAFramework::UsGaap,
expand_industry_subaccounts: false,
}
}
pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
if use_pcg {
self.coa_framework = CoAFramework::FrenchPcg;
}
self
}
pub fn with_expand_industry_subaccounts(mut self, expand: bool) -> Self {
self.expand_industry_subaccounts = expand;
self
}
pub fn with_coa_framework(mut self, framework: CoAFramework) -> Self {
self.coa_framework = framework;
self
}
pub fn generate(&mut self) -> ChartOfAccounts {
debug!(
complexity = ?self.complexity,
industry = ?self.industry,
seed = self.seed,
framework = ?self.coa_framework,
"Generating chart of accounts"
);
self.count += 1;
let mut coa = match self.coa_framework {
CoAFramework::UsGaap => self.generate_default(),
CoAFramework::FrenchPcg => self.generate_pcg(),
CoAFramework::GermanSkr04 => self.generate_skr(),
};
let framework_label = match self.coa_framework {
CoAFramework::UsGaap => "us_gaap",
CoAFramework::FrenchPcg => "french_pcg",
CoAFramework::GermanSkr04 => "german_skr04",
};
for account in coa.accounts.iter_mut() {
account.accounting_framework = Some(framework_label.to_string());
}
coa
}
pub fn apply_coa_semantic_prior(
coa: &mut ChartOfAccounts,
prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
) -> usize {
let enriched = overlay_coa_semantic(coa, prior);
tracing::debug!(
enriched_accounts = enriched,
total_accounts = coa.accounts.len(),
"SP4.2 CoA semantic prior applied"
);
enriched
}
fn generate_default(&mut self) -> ChartOfAccounts {
let target_count = self.complexity.target_count();
let mut coa = ChartOfAccounts::new(
format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
format!("{:?} Chart of Accounts", self.industry),
"US".to_string(),
self.industry,
self.complexity,
);
Self::seed_canonical_accounts(&mut coa);
if self.expand_industry_subaccounts {
self.generate_suspense_accounts(&mut coa);
Self::expand_with_industry_pack(&mut coa, self.industry);
} else {
self.generate_asset_accounts(&mut coa, target_count / 5);
self.generate_liability_accounts(&mut coa, target_count / 6);
self.generate_equity_accounts(&mut coa, target_count / 10);
self.generate_revenue_accounts(&mut coa, target_count / 5);
self.generate_expense_accounts(&mut coa, target_count / 4);
self.generate_suspense_accounts(&mut coa);
}
coa
}
fn expand_with_industry_pack(coa: &mut ChartOfAccounts, industry: IndustrySector) {
let pack = match datasynth_core::industry_packs::load_pack(industry) {
Ok(Some(p)) => p,
Ok(None) => return,
Err(e) => {
tracing::warn!(
"industry pack for {:?} failed to load: {} — skipping expansion",
industry,
e
);
return;
}
};
for expansion in &pack.expansions {
let parent_snapshot = match coa.get_account(&expansion.parent_account) {
Some(p) => p.clone(),
None => continue,
};
if let Some(parent_mut) = coa
.accounts
.iter_mut()
.find(|a| a.account_number == expansion.parent_account)
{
parent_mut.is_postable = false;
parent_mut.is_control_account = true;
}
for sub in &expansion.sub_accounts {
let sub_number = datasynth_core::industry_packs::render_sub_account_number(
&expansion.parent_account,
&sub.suffix,
);
if coa.get_account(&sub_number).is_some() {
continue;
}
let sub_name = datasynth_core::industry_packs::render_sub_account_name(
&expansion.parent_name,
&sub.name,
);
let mut sub_acct = GLAccount::new(
sub_number,
sub_name,
parent_snapshot.account_type,
parent_snapshot.sub_type,
);
sub_acct.account_class = parent_snapshot.account_class.clone();
sub_acct.account_class_name = parent_snapshot.account_class_name.clone();
sub_acct.account_sub_class = parent_snapshot.account_sub_class.clone();
sub_acct.account_sub_class_name = parent_snapshot.account_sub_class_name.clone();
sub_acct.account_group = parent_snapshot.account_group.clone();
sub_acct.parent_account = Some(parent_snapshot.account_number.clone());
sub_acct.hierarchy_level = parent_snapshot.hierarchy_level.saturating_add(1);
sub_acct.requires_cost_center = parent_snapshot.requires_cost_center;
sub_acct.requires_profit_center = parent_snapshot.requires_profit_center;
sub_acct.accounting_framework = parent_snapshot.accounting_framework.clone();
sub_acct.is_postable = true;
sub_acct.is_control_account = false;
coa.add_account(sub_acct);
}
}
}
fn generate_pcg(&mut self) -> ChartOfAccounts {
match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
Ok(coa) => coa,
Err(_) => self.generate_pcg_fallback(),
}
}
fn generate_skr(&mut self) -> ChartOfAccounts {
match skr_loader::build_chart_of_accounts_from_skr04(self.complexity, self.industry) {
Ok(coa) => coa,
Err(_) => self.generate_skr_fallback(),
}
}
fn generate_skr_fallback(&mut self) -> ChartOfAccounts {
use datasynth_core::skr;
let target_count = self.complexity.target_count();
let mut coa = ChartOfAccounts::new(
format!("COA_SKR04_{:?}_{}", self.industry, target_count),
format!("Standardkontenrahmen 04 – {:?}", self.industry),
"DE".to_string(),
self.industry,
self.complexity,
);
coa.account_format = "####".to_string();
let key_accounts = [
(
skr::control_accounts::AR_CONTROL,
"Forderungen aus L+L",
AccountType::Asset,
AccountSubType::AccountsReceivable,
),
(
skr::control_accounts::AP_CONTROL,
"Verbindlichkeiten aus L+L",
AccountType::Liability,
AccountSubType::AccountsPayable,
),
(
skr::control_accounts::INVENTORY,
"Vorräte",
AccountType::Asset,
AccountSubType::Inventory,
),
(
skr::control_accounts::FIXED_ASSETS,
"Sachanlagen",
AccountType::Asset,
AccountSubType::FixedAssets,
),
(
skr::cash_accounts::OPERATING_CASH,
"Bank",
AccountType::Asset,
AccountSubType::Cash,
),
(
skr::cash_accounts::PETTY_CASH,
"Kasse",
AccountType::Asset,
AccountSubType::Cash,
),
(
skr::equity_accounts::COMMON_STOCK,
"Gezeichnetes Kapital",
AccountType::Equity,
AccountSubType::CommonStock,
),
(
skr::equity_accounts::RETAINED_EARNINGS,
"Gewinnvortrag",
AccountType::Equity,
AccountSubType::RetainedEarnings,
),
(
skr::revenue_accounts::PRODUCT_REVENUE,
"Umsatzerlöse",
AccountType::Revenue,
AccountSubType::ProductRevenue,
),
(
skr::revenue_accounts::SERVICE_REVENUE,
"Erlöse Leistungen",
AccountType::Revenue,
AccountSubType::ServiceRevenue,
),
(
skr::expense_accounts::COGS,
"Materialaufwand",
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
),
(
skr::expense_accounts::SALARIES_WAGES,
"Löhne und Gehälter",
AccountType::Expense,
AccountSubType::OperatingExpenses,
),
(
skr::expense_accounts::DEPRECIATION,
"Abschreibungen",
AccountType::Expense,
AccountSubType::DepreciationExpense,
),
(
skr::expense_accounts::RENT,
"Miete",
AccountType::Expense,
AccountSubType::OperatingExpenses,
),
];
for (code, name, acc_type, sub_type) in key_accounts {
let mut account =
GLAccount::new(code.to_string(), name.to_string(), acc_type, sub_type);
account.requires_cost_center = acc_type == AccountType::Expense;
coa.add_account(account);
}
let mut num = 4100u32;
while coa.account_count() < target_count && num < 9900 {
let code = format!("{num:04}");
if coa.get_account(&code).is_none() {
let class = (num / 1000) as u8;
let (acc_type, sub_type) = match class {
0..=1 => (AccountType::Asset, AccountSubType::OtherAssets),
2 => (AccountType::Equity, AccountSubType::RetainedEarnings),
3 => (AccountType::Liability, AccountSubType::OtherLiabilities),
4 => (AccountType::Revenue, AccountSubType::OtherIncome),
5 => (AccountType::Expense, AccountSubType::CostOfGoodsSold),
6 => (AccountType::Expense, AccountSubType::OperatingExpenses),
7 => (AccountType::Expense, AccountSubType::InterestExpense),
_ => (AccountType::Asset, AccountSubType::SuspenseClearing),
};
coa.add_account(GLAccount::new(
code,
format!("Konto {num}"),
acc_type,
sub_type,
));
}
num += 10;
}
coa
}
fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
let target_count = self.complexity.target_count();
let mut coa = ChartOfAccounts::new(
format!("COA_PCG_{:?}_{}", self.industry, target_count),
format!("Plan Comptable Général – {:?}", self.industry),
"FR".to_string(),
self.industry,
self.complexity,
);
coa.account_format = "######".to_string();
self.generate_pcg_class_1(&mut coa, target_count / 10);
self.generate_pcg_class_2(&mut coa, target_count / 6);
self.generate_pcg_class_3(&mut coa, target_count / 8);
self.generate_pcg_class_4(&mut coa, target_count / 5);
self.generate_pcg_class_5(&mut coa, target_count / 12);
self.generate_pcg_class_6(&mut coa, target_count / 4);
self.generate_pcg_class_7(&mut coa, target_count / 5);
self.generate_pcg_class_8(&mut coa);
coa
}
fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let items = [
(
101,
"Capital",
AccountType::Equity,
AccountSubType::CommonStock,
),
(
129,
"Résultat",
AccountType::Equity,
AccountSubType::RetainedEarnings,
),
(
164,
"Emprunts",
AccountType::Liability,
AccountSubType::LongTermDebt,
),
(
151,
"Provisions pour risques",
AccountType::Liability,
AccountSubType::AccruedLiabilities,
),
];
for (base, name, acc_type, sub_type) in items {
for i in 0..count.max(1) {
let num = base * 1000 + (i as u32 % 100);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("{} {}", name, i + 1),
acc_type,
sub_type,
));
}
}
}
fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
for i in 0..count.max(1) {
let num = 215000 + (i as u32 % 100);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("Immobilisations {}", i + 1),
AccountType::Asset,
AccountSubType::FixedAssets,
));
}
for i in 0..(count / 2).max(1) {
let num = 281000 + (i as u32 % 100);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("Amortissements {}", i + 1),
AccountType::Asset,
AccountSubType::AccumulatedDepreciation,
));
}
}
fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
for i in 0..count.max(1) {
let num = 310000 + (i as u32 % 1000);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("Stocks {}", i + 1),
AccountType::Asset,
AccountSubType::Inventory,
));
}
}
fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
for i in 0..count.max(1) {
let num = 411000 + (i as u32 % 1000);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("Clients {}", i + 1),
AccountType::Asset,
AccountSubType::AccountsReceivable,
));
}
for i in 0..count.max(1) {
let num = 401000 + (i as u32 % 1000);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("Fournisseurs {}", i + 1),
AccountType::Liability,
AccountSubType::AccountsPayable,
));
}
let clearing = GLAccount::new(
"408000".to_string(),
"Fournisseurs – non encore reçus".to_string(),
AccountType::Liability,
AccountSubType::GoodsReceivedClearing,
);
coa.add_account(clearing);
}
fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let bases = [
(512, "Banque"),
(530, "Caisse"),
(511, "Valeurs à l'encaissement"),
];
for (base, name) in bases {
for i in 0..(count / 3).max(1) {
let num = base * 1000 + (i as u32 % 100);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("{} {}", name, i + 1),
AccountType::Asset,
AccountSubType::Cash,
));
}
}
}
fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let bases = [
(603, "Achats"),
(641, "Rémunérations"),
(681, "DAP"),
(613, "Loyers"),
(661, "Charges financières"),
];
for (base, name) in bases {
for i in 0..(count / 5).max(1) {
let num = base * 1000 + (i as u32 % 100);
let mut account = GLAccount::new(
format!("{num:06}"),
format!("{} {}", name, i + 1),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
account.requires_cost_center = true;
coa.add_account(account);
}
}
}
fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let bases = [
(701, "Ventes"),
(706, "Prestations"),
(758, "Produits divers"),
];
for (base, name) in bases {
for i in 0..(count / 3).max(1) {
let num = base * 1000 + (i as u32 % 100);
coa.add_account(GLAccount::new(
format!("{num:06}"),
format!("{} {}", name, i + 1),
AccountType::Revenue,
AccountSubType::ProductRevenue,
));
}
}
}
fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
"808000".to_string(),
"Comptes spéciaux".to_string(),
AccountType::Asset,
AccountSubType::SuspenseClearing,
));
}
fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
cash_accounts::OPERATING_CASH.to_string(),
"Operating Cash".to_string(),
AccountType::Asset,
AccountSubType::Cash,
));
coa.add_account(GLAccount::new(
cash_accounts::BANK_ACCOUNT.to_string(),
"Bank Account".to_string(),
AccountType::Asset,
AccountSubType::Cash,
));
coa.add_account(GLAccount::new(
cash_accounts::PETTY_CASH.to_string(),
"Petty Cash".to_string(),
AccountType::Asset,
AccountSubType::Cash,
));
coa.add_account(GLAccount::new(
cash_accounts::WIRE_CLEARING.to_string(),
"Wire Transfer Clearing".to_string(),
AccountType::Asset,
AccountSubType::BankClearing,
));
{
let mut acct = GLAccount::new(
control_accounts::AR_CONTROL.to_string(),
"Accounts Receivable Control".to_string(),
AccountType::Asset,
AccountSubType::AccountsReceivable,
);
acct.is_control_account = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
control_accounts::IC_AR_CLEARING.to_string(),
"Intercompany AR Clearing".to_string(),
AccountType::Asset,
AccountSubType::AccountsReceivable,
);
acct.is_control_account = true;
coa.add_account(acct);
}
coa.add_account(GLAccount::new(
control_accounts::INVENTORY.to_string(),
"Inventory".to_string(),
AccountType::Asset,
AccountSubType::Inventory,
));
coa.add_account(GLAccount::new(
control_accounts::FIXED_ASSETS.to_string(),
"Fixed Assets".to_string(),
AccountType::Asset,
AccountSubType::FixedAssets,
));
coa.add_account(GLAccount::new(
control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
"Accumulated Depreciation".to_string(),
AccountType::Asset,
AccountSubType::AccumulatedDepreciation,
));
coa.add_account(GLAccount::new(
tax_accounts::INPUT_VAT.to_string(),
"Input VAT".to_string(),
AccountType::Asset,
AccountSubType::OtherReceivables,
));
coa.add_account(GLAccount::new(
tax_accounts::DEFERRED_TAX_ASSET.to_string(),
"Deferred Tax Asset".to_string(),
AccountType::Asset,
AccountSubType::OtherAssets,
));
{
let mut acct = GLAccount::new(
control_accounts::AP_CONTROL.to_string(),
"Accounts Payable Control".to_string(),
AccountType::Liability,
AccountSubType::AccountsPayable,
);
acct.is_control_account = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
control_accounts::IC_AP_CLEARING.to_string(),
"Intercompany AP Clearing".to_string(),
AccountType::Liability,
AccountSubType::AccountsPayable,
);
acct.is_control_account = true;
coa.add_account(acct);
}
coa.add_account(GLAccount::new(
tax_accounts::SALES_TAX_PAYABLE.to_string(),
"Sales Tax Payable".to_string(),
AccountType::Liability,
AccountSubType::TaxLiabilities,
));
coa.add_account(GLAccount::new(
tax_accounts::VAT_PAYABLE.to_string(),
"VAT Payable".to_string(),
AccountType::Liability,
AccountSubType::TaxLiabilities,
));
coa.add_account(GLAccount::new(
tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
"Withholding Tax Payable".to_string(),
AccountType::Liability,
AccountSubType::TaxLiabilities,
));
coa.add_account(GLAccount::new(
liability_accounts::ACCRUED_EXPENSES.to_string(),
"Accrued Expenses".to_string(),
AccountType::Liability,
AccountSubType::AccruedLiabilities,
));
coa.add_account(GLAccount::new(
liability_accounts::ACCRUED_SALARIES.to_string(),
"Accrued Salaries".to_string(),
AccountType::Liability,
AccountSubType::AccruedLiabilities,
));
coa.add_account(GLAccount::new(
liability_accounts::ACCRUED_BENEFITS.to_string(),
"Accrued Benefits".to_string(),
AccountType::Liability,
AccountSubType::AccruedLiabilities,
));
coa.add_account(GLAccount::new(
liability_accounts::UNEARNED_REVENUE.to_string(),
"Unearned Revenue".to_string(),
AccountType::Liability,
AccountSubType::DeferredRevenue,
));
coa.add_account(GLAccount::new(
liability_accounts::SHORT_TERM_DEBT.to_string(),
"Short-Term Debt".to_string(),
AccountType::Liability,
AccountSubType::ShortTermDebt,
));
coa.add_account(GLAccount::new(
tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
"Deferred Tax Liability".to_string(),
AccountType::Liability,
AccountSubType::TaxLiabilities,
));
coa.add_account(GLAccount::new(
liability_accounts::LONG_TERM_DEBT.to_string(),
"Long-Term Debt".to_string(),
AccountType::Liability,
AccountSubType::LongTermDebt,
));
coa.add_account(GLAccount::new(
liability_accounts::IC_PAYABLE.to_string(),
"Intercompany Payable".to_string(),
AccountType::Liability,
AccountSubType::OtherLiabilities,
));
{
let mut acct = GLAccount::new(
control_accounts::GR_IR_CLEARING.to_string(),
"GR/IR Clearing".to_string(),
AccountType::Liability,
AccountSubType::GoodsReceivedClearing,
);
acct.is_suspense_account = true;
coa.add_account(acct);
}
coa.add_account(GLAccount::new(
equity_accounts::COMMON_STOCK.to_string(),
"Common Stock".to_string(),
AccountType::Equity,
AccountSubType::CommonStock,
));
coa.add_account(GLAccount::new(
equity_accounts::APIC.to_string(),
"Additional Paid-In Capital".to_string(),
AccountType::Equity,
AccountSubType::AdditionalPaidInCapital,
));
coa.add_account(GLAccount::new(
equity_accounts::RETAINED_EARNINGS.to_string(),
"Retained Earnings".to_string(),
AccountType::Equity,
AccountSubType::RetainedEarnings,
));
coa.add_account(GLAccount::new(
equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
"Current Year Earnings".to_string(),
AccountType::Equity,
AccountSubType::NetIncome,
));
coa.add_account(GLAccount::new(
equity_accounts::TREASURY_STOCK.to_string(),
"Treasury Stock".to_string(),
AccountType::Equity,
AccountSubType::TreasuryStock,
));
coa.add_account(GLAccount::new(
equity_accounts::CTA.to_string(),
"Currency Translation Adjustment".to_string(),
AccountType::Equity,
AccountSubType::OtherComprehensiveIncome,
));
coa.add_account(GLAccount::new(
revenue_accounts::PRODUCT_REVENUE.to_string(),
"Product Revenue".to_string(),
AccountType::Revenue,
AccountSubType::ProductRevenue,
));
coa.add_account(GLAccount::new(
revenue_accounts::SALES_DISCOUNTS.to_string(),
"Sales Discounts".to_string(),
AccountType::Revenue,
AccountSubType::ProductRevenue,
));
coa.add_account(GLAccount::new(
revenue_accounts::SALES_RETURNS.to_string(),
"Sales Returns and Allowances".to_string(),
AccountType::Revenue,
AccountSubType::ProductRevenue,
));
coa.add_account(GLAccount::new(
revenue_accounts::SERVICE_REVENUE.to_string(),
"Service Revenue".to_string(),
AccountType::Revenue,
AccountSubType::ServiceRevenue,
));
coa.add_account(GLAccount::new(
revenue_accounts::IC_REVENUE.to_string(),
"Intercompany Revenue".to_string(),
AccountType::Revenue,
AccountSubType::OtherIncome,
));
coa.add_account(GLAccount::new(
revenue_accounts::OTHER_REVENUE.to_string(),
"Other Revenue".to_string(),
AccountType::Revenue,
AccountSubType::OtherIncome,
));
{
let mut acct = GLAccount::new(
expense_accounts::COGS.to_string(),
"Cost of Goods Sold".to_string(),
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::RAW_MATERIALS.to_string(),
"Raw Materials".to_string(),
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::DIRECT_LABOR.to_string(),
"Direct Labor".to_string(),
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
"Manufacturing Overhead".to_string(),
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::DEPRECIATION.to_string(),
"Depreciation Expense".to_string(),
AccountType::Expense,
AccountSubType::DepreciationExpense,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::SALARIES_WAGES.to_string(),
"Salaries and Wages".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::BENEFITS.to_string(),
"Benefits Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::RENT.to_string(),
"Rent Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::UTILITIES.to_string(),
"Utilities Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::OFFICE_SUPPLIES.to_string(),
"Office Supplies".to_string(),
AccountType::Expense,
AccountSubType::AdministrativeExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
"Travel and Entertainment".to_string(),
AccountType::Expense,
AccountSubType::SellingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::PROFESSIONAL_FEES.to_string(),
"Professional Fees".to_string(),
AccountType::Expense,
AccountSubType::AdministrativeExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::INSURANCE.to_string(),
"Insurance Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::BAD_DEBT.to_string(),
"Bad Debt Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::INTEREST_EXPENSE.to_string(),
"Interest Expense".to_string(),
AccountType::Expense,
AccountSubType::InterestExpense,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::PURCHASE_DISCOUNTS.to_string(),
"Purchase Discounts".to_string(),
AccountType::Expense,
AccountSubType::OtherExpenses,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
expense_accounts::FX_GAIN_LOSS.to_string(),
"FX Gain/Loss".to_string(),
AccountType::Expense,
AccountSubType::ForeignExchangeLoss,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
tax_accounts::TAX_EXPENSE.to_string(),
"Tax Expense".to_string(),
AccountType::Expense,
AccountSubType::TaxExpense,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
Self::seed_asset_class_accounts(coa);
Self::seed_manufacturing_accounts(coa);
Self::seed_intangible_accounts(coa);
Self::seed_treasury_accounts(coa);
Self::seed_provision_accounts(coa);
Self::seed_dividend_accounts(coa);
Self::seed_compensation_accounts(coa);
Self::seed_inventory_subledger_accounts(coa);
Self::seed_additional_tax_accounts(coa);
Self::seed_additional_equity_accounts(coa);
Self::seed_dormant_accounts(coa);
{
let mut acct = GLAccount::new(
suspense_accounts::GENERAL_SUSPENSE.to_string(),
"General Suspense".to_string(),
AccountType::Asset,
AccountSubType::SuspenseClearing,
);
acct.is_suspense_account = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
suspense_accounts::PAYROLL_CLEARING.to_string(),
"Payroll Clearing".to_string(),
AccountType::Asset,
AccountSubType::SuspenseClearing,
);
acct.is_suspense_account = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
"Bank Reconciliation Suspense".to_string(),
AccountType::Asset,
AccountSubType::BankClearing,
);
acct.is_suspense_account = true;
coa.add_account(acct);
}
{
let mut acct = GLAccount::new(
suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
"IC Elimination Suspense".to_string(),
AccountType::Asset,
AccountSubType::IntercompanyClearing,
);
acct.is_suspense_account = true;
coa.add_account(acct);
}
}
fn seed_asset_class_accounts(coa: &mut ChartOfAccounts) {
let acquisitions = [
(asset_class_accounts::LAND, "Land"),
(asset_class_accounts::BUILDINGS, "Buildings"),
(
asset_class_accounts::BUILDING_IMPROVEMENTS,
"Building Improvements",
),
(
asset_class_accounts::MACHINERY_EQUIPMENT,
"Machinery & Equipment",
),
(asset_class_accounts::VEHICLES, "Vehicles"),
(asset_class_accounts::OFFICE_EQUIPMENT, "Office Equipment"),
(asset_class_accounts::COMPUTER_HARDWARE, "Computer Hardware"),
(
asset_class_accounts::SOFTWARE_INTANGIBLES,
"Software / Intangibles",
),
(
asset_class_accounts::FURNITURE_FIXTURES,
"Furniture & Fixtures",
),
(
asset_class_accounts::LEASEHOLD_IMPROVEMENTS,
"Leasehold Improvements",
),
(asset_class_accounts::OTHER_ASSETS, "Other Fixed Assets"),
(asset_class_accounts::LOW_VALUE_ASSETS, "Low-Value Assets"),
(
asset_class_accounts::CONSTRUCTION_IN_PROGRESS,
"Construction in Progress",
),
];
for (number, name) in acquisitions {
coa.add_account(GLAccount::new(
number.to_string(),
name.to_string(),
AccountType::Asset,
AccountSubType::FixedAssets,
));
}
let depreciation_contras = [
(asset_class_accounts::ACC_DEP_LAND, "Acc. Dep. — Land"),
(
asset_class_accounts::ACC_DEP_BUILDINGS,
"Acc. Dep. — Buildings",
),
(
asset_class_accounts::ACC_DEP_MACHINERY,
"Acc. Dep. — Machinery",
),
(
asset_class_accounts::ACC_DEP_VEHICLES,
"Acc. Dep. — Vehicles",
),
(
asset_class_accounts::ACC_DEP_OFFICE_EQUIPMENT,
"Acc. Dep. — Office Equipment",
),
(
asset_class_accounts::ACC_DEP_SOFTWARE,
"Acc. Dep. — Software / Intangibles",
),
(
asset_class_accounts::ACC_DEP_FURNITURE,
"Acc. Dep. — Furniture",
),
(
asset_class_accounts::ACC_DEP_LEASEHOLD,
"Acc. Dep. — Leasehold Improvements",
),
(asset_class_accounts::ACC_DEP_OTHER, "Acc. Dep. — Other"),
(asset_class_accounts::ACC_DEP_CIP, "Acc. Dep. — CIP"),
];
for (number, name) in depreciation_contras {
coa.add_account(GLAccount::new(
number.to_string(),
name.to_string(),
AccountType::Asset,
AccountSubType::AccumulatedDepreciation,
));
}
{
let mut acct = GLAccount::new(
asset_class_accounts::DEPRECIATION_EXPENSE.to_string(),
"FA Depreciation Expense".to_string(),
AccountType::Expense,
AccountSubType::DepreciationExpense,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
coa.add_account(GLAccount::new(
asset_class_accounts::GAIN_ON_DISPOSAL.to_string(),
"Gain on Disposal of Fixed Assets".to_string(),
AccountType::Revenue,
AccountSubType::GainOnSale,
));
coa.add_account(GLAccount::new(
asset_class_accounts::LOSS_ON_DISPOSAL.to_string(),
"Loss on Disposal of Fixed Assets".to_string(),
AccountType::Expense,
AccountSubType::LossOnSale,
));
}
fn seed_manufacturing_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
manufacturing_accounts::FINISHED_GOODS.to_string(),
"Finished Goods".to_string(),
AccountType::Asset,
AccountSubType::Inventory,
));
coa.add_account(GLAccount::new(
manufacturing_accounts::WIP.to_string(),
"Work in Process".to_string(),
AccountType::Asset,
AccountSubType::Inventory,
));
coa.add_account(GLAccount::new(
manufacturing_accounts::LABOR_ACCRUAL.to_string(),
"Labor Accrual".to_string(),
AccountType::Liability,
AccountSubType::AccruedLiabilities,
));
coa.add_account(GLAccount::new(
manufacturing_accounts::WARRANTY_PROVISION.to_string(),
"Warranty Provision".to_string(),
AccountType::Liability,
AccountSubType::OtherLiabilities,
));
let variance_accounts = [
(
manufacturing_accounts::SCRAP_EXPENSE,
"Scrap Expense",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::OVERHEAD_APPLIED,
"Overhead Applied",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::MATERIAL_PRICE_VARIANCE,
"Material Price Variance",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::MATERIAL_USAGE_VARIANCE,
"Material Usage Variance",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::LABOR_RATE_VARIANCE,
"Labor Rate Variance",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::LABOR_EFFICIENCY_VARIANCE,
"Labor Efficiency Variance",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::OVERHEAD_VOLUME_VARIANCE,
"Overhead Volume Variance",
AccountSubType::CostOfGoodsSold,
),
(
manufacturing_accounts::WARRANTY_EXPENSE,
"Warranty Expense",
AccountSubType::OperatingExpenses,
),
];
for (number, name, sub) in variance_accounts {
let mut acct = GLAccount::new(
number.to_string(),
name.to_string(),
AccountType::Expense,
sub,
);
acct.requires_cost_center = true;
coa.add_account(acct);
}
}
fn seed_intangible_accounts(coa: &mut ChartOfAccounts) {
let intangibles = [
(
intangible_accounts::GOODWILL,
"Goodwill",
AccountType::Asset,
AccountSubType::IntangibleAssets,
),
(
intangible_accounts::CUSTOMER_RELATIONSHIPS,
"Customer Relationships",
AccountType::Asset,
AccountSubType::IntangibleAssets,
),
(
intangible_accounts::TRADE_NAME,
"Trade Name / Brand",
AccountType::Asset,
AccountSubType::IntangibleAssets,
),
(
intangible_accounts::TECHNOLOGY,
"Technology / Developed Software",
AccountType::Asset,
AccountSubType::IntangibleAssets,
),
(
intangible_accounts::ACCUMULATED_AMORTIZATION,
"Accumulated Amortization",
AccountType::Asset,
AccountSubType::AccumulatedDepreciation,
),
(
intangible_accounts::AMORTIZATION_EXPENSE,
"Amortization Expense — Intangibles",
AccountType::Expense,
AccountSubType::AmortizationExpense,
),
(
intangible_accounts::BARGAIN_PURCHASE_GAIN,
"Bargain Purchase Gain",
AccountType::Revenue,
AccountSubType::OtherIncome,
),
];
for (number, name, ty, sub) in intangibles {
coa.add_account(GLAccount::new(
number.to_string(),
name.to_string(),
ty,
sub,
));
}
}
fn seed_treasury_accounts(coa: &mut ChartOfAccounts) {
let entries = [
(
treasury_accounts::INTEREST_PAYABLE,
"Interest Payable",
AccountType::Liability,
AccountSubType::AccruedLiabilities,
),
(
treasury_accounts::DEBT_PREMIUM,
"Debt Premium",
AccountType::Liability,
AccountSubType::LongTermDebt,
),
(
treasury_accounts::DEBT_DISCOUNT,
"Debt Discount",
AccountType::Liability,
AccountSubType::LongTermDebt,
),
(
treasury_accounts::DERIVATIVE_ASSET,
"Derivative Asset",
AccountType::Asset,
AccountSubType::OtherAssets,
),
(
treasury_accounts::DERIVATIVE_LIABILITY,
"Derivative Liability",
AccountType::Liability,
AccountSubType::OtherLiabilities,
),
(
treasury_accounts::OCI_CASH_FLOW_HEDGE,
"OCI — Cash Flow Hedge Reserve",
AccountType::Equity,
AccountSubType::OtherComprehensiveIncome,
),
(
treasury_accounts::HEDGE_INEFFECTIVENESS,
"Hedge Ineffectiveness",
AccountType::Expense,
AccountSubType::OtherExpenses,
),
(
treasury_accounts::CASH_POOL_IC_RECEIVABLE,
"IC Receivable — Cash Pool",
AccountType::Asset,
AccountSubType::AccountsReceivable,
),
(
treasury_accounts::CASH_POOL_IC_PAYABLE,
"IC Payable — Cash Pool",
AccountType::Liability,
AccountSubType::AccountsPayable,
),
];
for (number, name, ty, sub) in entries {
coa.add_account(GLAccount::new(
number.to_string(),
name.to_string(),
ty,
sub,
));
}
}
fn seed_provision_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
provision_accounts::PROVISION_LIABILITY.to_string(),
"Provision Liability".to_string(),
AccountType::Liability,
AccountSubType::OtherLiabilities,
));
let mut prov_exp = GLAccount::new(
provision_accounts::PROVISION_EXPENSE.to_string(),
"Provision Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
prov_exp.requires_cost_center = true;
coa.add_account(prov_exp);
}
fn seed_dividend_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
dividend_accounts::DIVIDENDS_PAYABLE.to_string(),
"Dividends Payable".to_string(),
AccountType::Liability,
AccountSubType::OtherLiabilities,
));
coa.add_account(GLAccount::new(
dividend_accounts::DIVIDENDS_DECLARED.to_string(),
"Dividends Declared".to_string(),
AccountType::Equity,
AccountSubType::RetainedEarnings,
));
}
fn seed_compensation_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
liability_accounts::NET_PENSION_LIABILITY.to_string(),
"Net Pension Liability".to_string(),
AccountType::Liability,
AccountSubType::PensionLiabilities,
));
coa.add_account(GLAccount::new(
equity_accounts::OCI_REMEASUREMENTS.to_string(),
"OCI — Pension Remeasurements".to_string(),
AccountType::Equity,
AccountSubType::OtherComprehensiveIncome,
));
coa.add_account(GLAccount::new(
equity_accounts::APIC_STOCK_COMP.to_string(),
"APIC — Stock Compensation".to_string(),
AccountType::Equity,
AccountSubType::AdditionalPaidInCapital,
));
let mut pension_exp = GLAccount::new(
expense_accounts::PENSION_EXPENSE.to_string(),
"Pension Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
pension_exp.requires_cost_center = true;
coa.add_account(pension_exp);
let mut stock_comp = GLAccount::new(
expense_accounts::STOCK_COMP_EXPENSE.to_string(),
"Stock-Based Compensation Expense".to_string(),
AccountType::Expense,
AccountSubType::OperatingExpenses,
);
stock_comp.requires_cost_center = true;
coa.add_account(stock_comp);
}
fn seed_inventory_subledger_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
inventory_accounts::WRITEUP_INCOME.to_string(),
"Inventory Write-up Income".to_string(),
AccountType::Revenue,
AccountSubType::OtherIncome,
));
let mut writedown = GLAccount::new(
inventory_accounts::WRITEDOWN_EXPENSE.to_string(),
"Inventory Write-down Expense".to_string(),
AccountType::Expense,
AccountSubType::CostOfGoodsSold,
);
writedown.requires_cost_center = true;
coa.add_account(writedown);
}
fn seed_additional_tax_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
tax_accounts::INCOME_TAX_PAYABLE.to_string(),
"Income Tax Payable".to_string(),
AccountType::Liability,
AccountSubType::TaxLiabilities,
));
coa.add_account(GLAccount::new(
tax_accounts::TAX_RECEIVABLE.to_string(),
"Tax Receivable".to_string(),
AccountType::Asset,
AccountSubType::OtherReceivables,
));
}
fn seed_dormant_accounts(coa: &mut ChartOfAccounts) {
let entries = [
(
dormant_accounts::LEGACY_SUSPENSE,
"Legacy Suspense (migrated)",
AccountType::Asset,
AccountSubType::SuspenseClearing,
),
(
dormant_accounts::LEGACY_CLEARING,
"Legacy Clearing (predecessor system)",
AccountType::Liability,
AccountSubType::OtherLiabilities,
),
(
dormant_accounts::OBSOLETE,
"Obsolete Account",
AccountType::Equity,
AccountSubType::OtherComprehensiveIncome,
),
(
dormant_accounts::TEST_ACCOUNT,
"Test Account (QA residue)",
AccountType::Asset,
AccountSubType::SuspenseClearing,
),
];
for (number, name, ty, sub) in entries {
let mut acct = GLAccount::new(number.to_string(), name.to_string(), ty, sub);
acct.is_blocked = true;
acct.is_postable = false;
acct.is_suspense_account = matches!(sub, AccountSubType::SuspenseClearing);
coa.add_account(acct);
}
}
fn seed_additional_equity_accounts(coa: &mut ChartOfAccounts) {
coa.add_account(GLAccount::new(
equity_accounts::INCOME_SUMMARY.to_string(),
"Income Summary".to_string(),
AccountType::Equity,
AccountSubType::NetIncome,
));
coa.add_account(GLAccount::new(
equity_accounts::DIVIDENDS_PAID.to_string(),
"Dividends Paid".to_string(),
AccountType::Equity,
AccountSubType::RetainedEarnings,
));
}
fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let sub_types = vec![
(AccountSubType::Cash, "Cash", 0.15),
(
AccountSubType::AccountsReceivable,
"Accounts Receivable",
0.20,
),
(AccountSubType::Inventory, "Inventory", 0.15),
(AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
(AccountSubType::FixedAssets, "Fixed Assets", 0.25),
(
AccountSubType::AccumulatedDepreciation,
"Accumulated Depreciation",
0.10,
),
(AccountSubType::OtherAssets, "Other Assets", 0.05),
];
let mut account_num = 100000u32;
for (sub_type, name_prefix, weight) in sub_types {
let sub_count = ((count as f64) * weight).round() as usize;
for i in 0..sub_count.max(1) {
let account = GLAccount::new(
format!("{account_num}"),
format!("{} {}", name_prefix, i + 1),
AccountType::Asset,
sub_type,
);
coa.add_account(account);
account_num += 10;
}
}
}
fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let sub_types = vec![
(AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
(
AccountSubType::AccruedLiabilities,
"Accrued Liabilities",
0.20,
),
(AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
(AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
(AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
(AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
];
let mut account_num = 200000u32;
for (sub_type, name_prefix, weight) in sub_types {
let sub_count = ((count as f64) * weight).round() as usize;
for i in 0..sub_count.max(1) {
let account = GLAccount::new(
format!("{account_num}"),
format!("{} {}", name_prefix, i + 1),
AccountType::Liability,
sub_type,
);
coa.add_account(account);
account_num += 10;
}
}
}
fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let sub_types = vec![
(AccountSubType::CommonStock, "Common Stock", 0.20),
(AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
(AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
(AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
];
let mut account_num = 300000u32;
for (sub_type, name_prefix, weight) in sub_types {
let sub_count = ((count as f64) * weight).round() as usize;
for i in 0..sub_count.max(1) {
let account = GLAccount::new(
format!("{account_num}"),
format!("{} {}", name_prefix, i + 1),
AccountType::Equity,
sub_type,
);
coa.add_account(account);
account_num += 10;
}
}
}
fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let sub_types = vec![
(AccountSubType::ProductRevenue, "Product Revenue", 0.40),
(AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
(AccountSubType::InterestIncome, "Interest Income", 0.10),
(AccountSubType::OtherIncome, "Other Income", 0.20),
];
let mut account_num = 400000u32;
for (sub_type, name_prefix, weight) in sub_types {
let sub_count = ((count as f64) * weight).round() as usize;
for i in 0..sub_count.max(1) {
let account = GLAccount::new(
format!("{account_num}"),
format!("{} {}", name_prefix, i + 1),
AccountType::Revenue,
sub_type,
);
coa.add_account(account);
account_num += 10;
}
}
}
fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
let sub_types = vec![
(AccountSubType::CostOfGoodsSold, "COGS", 0.20),
(
AccountSubType::OperatingExpenses,
"Operating Expenses",
0.25,
),
(AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
(
AccountSubType::AdministrativeExpenses,
"Admin Expenses",
0.15,
),
(AccountSubType::DepreciationExpense, "Depreciation", 0.10),
(AccountSubType::InterestExpense, "Interest Expense", 0.05),
(AccountSubType::TaxExpense, "Tax Expense", 0.05),
(AccountSubType::OtherExpenses, "Other Expenses", 0.05),
];
let mut account_num = 500000u32;
for (sub_type, name_prefix, weight) in sub_types {
let sub_count = ((count as f64) * weight).round() as usize;
for i in 0..sub_count.max(1) {
let mut account = GLAccount::new(
format!("{account_num}"),
format!("{} {}", name_prefix, i + 1),
AccountType::Expense,
sub_type,
);
account.requires_cost_center = true;
coa.add_account(account);
account_num += 10;
}
}
}
fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
let suspense_types = vec![
(AccountSubType::SuspenseClearing, "Suspense Clearing"),
(AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
(AccountSubType::BankClearing, "Bank Clearing"),
(
AccountSubType::IntercompanyClearing,
"Intercompany Clearing",
),
];
let mut account_num = 199000u32;
for (sub_type, name) in suspense_types {
let mut account = GLAccount::new(
format!("{account_num}"),
name.to_string(),
AccountType::Asset,
sub_type,
);
account.is_suspense_account = true;
coa.add_account(account);
account_num += 100;
}
}
}
impl Generator for ChartOfAccountsGenerator {
type Item = ChartOfAccounts;
type Config = (CoAComplexity, IndustrySector);
fn new(config: Self::Config, seed: u64) -> Self {
Self::new(config.0, config.1, seed)
}
fn generate_one(&mut self) -> Self::Item {
self.generate()
}
fn reset(&mut self) {
self.rng = seeded_rng(self.seed, 0);
self.count = 0;
self.coa_framework = CoAFramework::UsGaap;
}
fn count(&self) -> u64 {
self.count
}
fn seed(&self) -> u64 {
self.seed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_small_coa() {
let mut gen =
ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
let coa = gen.generate();
assert!(coa.account_count() >= 50);
assert!(!coa.get_suspense_accounts().is_empty());
}
#[test]
fn test_generate_pcg_coa() {
let mut gen =
ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
.with_french_pcg(true);
let coa = gen.generate();
assert_eq!(coa.country, "FR");
assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
assert!(coa.account_count() >= 20);
let first = coa.accounts.first().expect("has accounts");
assert_eq!(first.account_number.len(), 6);
}
#[test]
fn test_pcg_account_structure() {
let mut gen =
ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
.with_french_pcg(true);
let coa = gen.generate();
assert_eq!(
coa.account_format, "######",
"PCG uses 6-digit account format"
);
assert!(
coa.account_count() >= 20,
"PCG CoA has minimum account count"
);
let account_numbers: Vec<_> = coa
.accounts
.iter()
.map(|a| a.account_number.as_str())
.collect();
for num in &account_numbers {
assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
assert!(
num.chars().all(|c| c.is_ascii_digit()),
"PCG account is numeric: {}",
num
);
}
let first_digits: std::collections::HashSet<char> = account_numbers
.iter()
.filter_map(|s| s.chars().next())
.collect();
let pcg_classes: std::collections::HashSet<_> =
['1', '2', '3', '4', '5', '6', '7', '8'].into();
assert!(
!first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
"PCG account numbers must be in classes 1–8, got first digits: {:?}",
first_digits
);
}
#[test]
fn overlay_coa_taxonomy_fills_template_once_per_account() {
use datasynth_core::distributions::text_taxonomy::{
SyntheticExampleResolver, TemplateEntry, TextTaxonomyPrior,
};
use rand::SeedableRng;
let mut gen =
ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 1);
let mut coa = gen.generate();
assert!(
coa.accounts.iter().any(|a| a.account_number == "2000"),
"canonical AP_CONTROL account '2000' expected in small CoA"
);
let mut tx = TextTaxonomyPrior::default();
tx.coa_pools.insert(
"2000".to_string(),
TemplateEntry {
template: "Kreditoren {company}".to_string(),
probability: 1.0,
synthetic_example: "Kreditoren Example GmbH".to_string(),
},
);
let mut resolver = SyntheticExampleResolver;
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(9);
overlay_coa_taxonomy(&mut coa, &tx, &mut resolver, &mut rng);
let acct = coa
.accounts
.iter()
.find(|a| a.account_number == "2000")
.expect("account '2000' present after overlay");
assert!(
acct.short_description.starts_with("Kreditoren "),
"expected 'Kreditoren …', got '{}'",
acct.short_description
);
assert!(
!acct.short_description.contains('{'),
"template placeholder left unfilled: '{}'",
acct.short_description
);
assert_eq!(
acct.short_description, acct.long_description,
"short and long descriptions should be identical after taxonomy overlay"
);
let first = acct.short_description.clone();
let acct_again = coa
.accounts
.iter()
.find(|a| a.account_number == "2000")
.expect("account '2000' still present");
assert_eq!(acct_again.short_description, first);
assert_eq!(acct_again.long_description, first);
}
}