Skip to main content

datasynth_generators/
coa_generator.rs

1//! Chart of Accounts generator.
2
3use tracing::debug;
4
5use datasynth_core::accounts::{
6    cash_accounts, control_accounts, equity_accounts, expense_accounts, liability_accounts,
7    revenue_accounts, suspense_accounts, tax_accounts,
8};
9use datasynth_core::models::*;
10use datasynth_core::pcg_loader;
11use datasynth_core::skr_loader;
12use datasynth_core::traits::Generator;
13use datasynth_core::utils::seeded_rng;
14use rand_chacha::ChaCha8Rng;
15
16/// Accounting framework for CoA generation.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CoAFramework {
19    /// US GAAP (4-digit accounts)
20    #[default]
21    UsGaap,
22    /// French GAAP / Plan Comptable Général (6-digit accounts)
23    FrenchPcg,
24    /// German GAAP / SKR04 (4-digit accounts)
25    GermanSkr04,
26}
27
28/// Generator for Chart of Accounts.
29pub struct ChartOfAccountsGenerator {
30    rng: ChaCha8Rng,
31    seed: u64,
32    complexity: CoAComplexity,
33    industry: IndustrySector,
34    count: u64,
35    /// Accounting framework for CoA generation.
36    coa_framework: CoAFramework,
37}
38
39impl ChartOfAccountsGenerator {
40    /// Create a new CoA generator.
41    pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
42        Self {
43            rng: seeded_rng(seed, 0),
44            seed,
45            complexity,
46            industry,
47            count: 0,
48            coa_framework: CoAFramework::UsGaap,
49        }
50    }
51
52    /// Use French GAAP (Plan Comptable Général) account structure.
53    ///
54    /// Deprecated: use `with_coa_framework(CoAFramework::FrenchPcg)` instead.
55    pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
56        if use_pcg {
57            self.coa_framework = CoAFramework::FrenchPcg;
58        }
59        self
60    }
61
62    /// Set the accounting framework for CoA generation.
63    pub fn with_coa_framework(mut self, framework: CoAFramework) -> Self {
64        self.coa_framework = framework;
65        self
66    }
67
68    /// Generate a complete chart of accounts.
69    pub fn generate(&mut self) -> ChartOfAccounts {
70        debug!(
71            complexity = ?self.complexity,
72            industry = ?self.industry,
73            seed = self.seed,
74            framework = ?self.coa_framework,
75            "Generating chart of accounts"
76        );
77
78        self.count += 1;
79        let mut coa = match self.coa_framework {
80            CoAFramework::UsGaap => self.generate_default(),
81            CoAFramework::FrenchPcg => self.generate_pcg(),
82            CoAFramework::GermanSkr04 => self.generate_skr(),
83        };
84
85        // v5.0.1 fix (Gap 5): stamp `accounting_framework` on every emitted
86        // GLAccount so the per-row column matches the sidecar
87        // `coa_meta.accounting_framework`. Until this fix the field
88        // defaulted to `None` even though the generator knew the framework.
89        let framework_label = match self.coa_framework {
90            CoAFramework::UsGaap => "us_gaap",
91            CoAFramework::FrenchPcg => "french_pcg",
92            CoAFramework::GermanSkr04 => "german_skr04",
93        };
94        for account in coa.accounts.iter_mut() {
95            account.accounting_framework = Some(framework_label.to_string());
96        }
97        coa
98    }
99
100    /// Generate default (US-style) chart of accounts.
101    fn generate_default(&mut self) -> ChartOfAccounts {
102        let target_count = self.complexity.target_count();
103        let mut coa = ChartOfAccounts::new(
104            format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
105            format!("{:?} Chart of Accounts", self.industry),
106            "US".to_string(),
107            self.industry,
108            self.complexity,
109        );
110
111        // Seed canonical accounts first so other generators can find them
112        Self::seed_canonical_accounts(&mut coa);
113
114        // Generate additional accounts by type
115        self.generate_asset_accounts(&mut coa, target_count / 5);
116        self.generate_liability_accounts(&mut coa, target_count / 6);
117        self.generate_equity_accounts(&mut coa, target_count / 10);
118        self.generate_revenue_accounts(&mut coa, target_count / 5);
119        self.generate_expense_accounts(&mut coa, target_count / 4);
120        self.generate_suspense_accounts(&mut coa);
121        coa
122    }
123
124    /// Generate Plan Comptable Général (French GAAP) chart of accounts.
125    /// Uses the comprehensive PCG 2024 structure from [arrhes/PCG](https://github.com/arrhes/PCG) when available.
126    fn generate_pcg(&mut self) -> ChartOfAccounts {
127        match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
128            Ok(coa) => coa,
129            Err(_) => self.generate_pcg_fallback(),
130        }
131    }
132
133    /// Generate SKR04 (German GAAP) chart of accounts.
134    /// Uses the embedded SKR04 2024 structure when available.
135    fn generate_skr(&mut self) -> ChartOfAccounts {
136        match skr_loader::build_chart_of_accounts_from_skr04(self.complexity, self.industry) {
137            Ok(coa) => coa,
138            Err(_) => self.generate_skr_fallback(),
139        }
140    }
141
142    /// Fallback simplified SKR04 when the embedded 2024 JSON cannot be loaded.
143    fn generate_skr_fallback(&mut self) -> ChartOfAccounts {
144        use datasynth_core::skr;
145        let target_count = self.complexity.target_count();
146        let mut coa = ChartOfAccounts::new(
147            format!("COA_SKR04_{:?}_{}", self.industry, target_count),
148            format!("Standardkontenrahmen 04 – {:?}", self.industry),
149            "DE".to_string(),
150            self.industry,
151            self.complexity,
152        );
153        coa.account_format = "####".to_string();
154
155        // Seed key SKR04 accounts
156        let key_accounts = [
157            (
158                skr::control_accounts::AR_CONTROL,
159                "Forderungen aus L+L",
160                AccountType::Asset,
161                AccountSubType::AccountsReceivable,
162            ),
163            (
164                skr::control_accounts::AP_CONTROL,
165                "Verbindlichkeiten aus L+L",
166                AccountType::Liability,
167                AccountSubType::AccountsPayable,
168            ),
169            (
170                skr::control_accounts::INVENTORY,
171                "Vorräte",
172                AccountType::Asset,
173                AccountSubType::Inventory,
174            ),
175            (
176                skr::control_accounts::FIXED_ASSETS,
177                "Sachanlagen",
178                AccountType::Asset,
179                AccountSubType::FixedAssets,
180            ),
181            (
182                skr::cash_accounts::OPERATING_CASH,
183                "Bank",
184                AccountType::Asset,
185                AccountSubType::Cash,
186            ),
187            (
188                skr::cash_accounts::PETTY_CASH,
189                "Kasse",
190                AccountType::Asset,
191                AccountSubType::Cash,
192            ),
193            (
194                skr::equity_accounts::COMMON_STOCK,
195                "Gezeichnetes Kapital",
196                AccountType::Equity,
197                AccountSubType::CommonStock,
198            ),
199            (
200                skr::equity_accounts::RETAINED_EARNINGS,
201                "Gewinnvortrag",
202                AccountType::Equity,
203                AccountSubType::RetainedEarnings,
204            ),
205            (
206                skr::revenue_accounts::PRODUCT_REVENUE,
207                "Umsatzerlöse",
208                AccountType::Revenue,
209                AccountSubType::ProductRevenue,
210            ),
211            (
212                skr::revenue_accounts::SERVICE_REVENUE,
213                "Erlöse Leistungen",
214                AccountType::Revenue,
215                AccountSubType::ServiceRevenue,
216            ),
217            (
218                skr::expense_accounts::COGS,
219                "Materialaufwand",
220                AccountType::Expense,
221                AccountSubType::CostOfGoodsSold,
222            ),
223            (
224                skr::expense_accounts::SALARIES_WAGES,
225                "Löhne und Gehälter",
226                AccountType::Expense,
227                AccountSubType::OperatingExpenses,
228            ),
229            (
230                skr::expense_accounts::DEPRECIATION,
231                "Abschreibungen",
232                AccountType::Expense,
233                AccountSubType::DepreciationExpense,
234            ),
235            (
236                skr::expense_accounts::RENT,
237                "Miete",
238                AccountType::Expense,
239                AccountSubType::OperatingExpenses,
240            ),
241        ];
242
243        for (code, name, acc_type, sub_type) in key_accounts {
244            let mut account =
245                GLAccount::new(code.to_string(), name.to_string(), acc_type, sub_type);
246            account.requires_cost_center = acc_type == AccountType::Expense;
247            coa.add_account(account);
248        }
249
250        // Add additional accounts to reach target count
251        let mut num = 4100u32;
252        while coa.account_count() < target_count && num < 9900 {
253            let code = format!("{num:04}");
254            if coa.get_account(&code).is_none() {
255                let class = (num / 1000) as u8;
256                let (acc_type, sub_type) = match class {
257                    0..=1 => (AccountType::Asset, AccountSubType::OtherAssets),
258                    2 => (AccountType::Equity, AccountSubType::RetainedEarnings),
259                    3 => (AccountType::Liability, AccountSubType::OtherLiabilities),
260                    4 => (AccountType::Revenue, AccountSubType::OtherIncome),
261                    5 => (AccountType::Expense, AccountSubType::CostOfGoodsSold),
262                    6 => (AccountType::Expense, AccountSubType::OperatingExpenses),
263                    7 => (AccountType::Expense, AccountSubType::InterestExpense),
264                    _ => (AccountType::Asset, AccountSubType::SuspenseClearing),
265                };
266                coa.add_account(GLAccount::new(
267                    code,
268                    format!("Konto {num}"),
269                    acc_type,
270                    sub_type,
271                ));
272            }
273            num += 10;
274        }
275
276        coa
277    }
278
279    /// Fallback simplified PCG when the embedded 2024 JSON cannot be loaded.
280    fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
281        let target_count = self.complexity.target_count();
282        let mut coa = ChartOfAccounts::new(
283            format!("COA_PCG_{:?}_{}", self.industry, target_count),
284            format!("Plan Comptable Général – {:?}", self.industry),
285            "FR".to_string(),
286            self.industry,
287            self.complexity,
288        );
289        coa.account_format = "######".to_string();
290
291        self.generate_pcg_class_1(&mut coa, target_count / 10);
292        self.generate_pcg_class_2(&mut coa, target_count / 6);
293        self.generate_pcg_class_3(&mut coa, target_count / 8);
294        self.generate_pcg_class_4(&mut coa, target_count / 5);
295        self.generate_pcg_class_5(&mut coa, target_count / 12);
296        self.generate_pcg_class_6(&mut coa, target_count / 4);
297        self.generate_pcg_class_7(&mut coa, target_count / 5);
298        self.generate_pcg_class_8(&mut coa);
299
300        coa
301    }
302
303    fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
304        let items = [
305            (
306                101,
307                "Capital",
308                AccountType::Equity,
309                AccountSubType::CommonStock,
310            ),
311            (
312                129,
313                "Résultat",
314                AccountType::Equity,
315                AccountSubType::RetainedEarnings,
316            ),
317            (
318                164,
319                "Emprunts",
320                AccountType::Liability,
321                AccountSubType::LongTermDebt,
322            ),
323            (
324                151,
325                "Provisions pour risques",
326                AccountType::Liability,
327                AccountSubType::AccruedLiabilities,
328            ),
329        ];
330        for (base, name, acc_type, sub_type) in items {
331            for i in 0..count.max(1) {
332                let num = base * 1000 + (i as u32 % 100);
333                coa.add_account(GLAccount::new(
334                    format!("{num:06}"),
335                    format!("{} {}", name, i + 1),
336                    acc_type,
337                    sub_type,
338                ));
339            }
340        }
341    }
342
343    fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
344        for i in 0..count.max(1) {
345            let num = 215000 + (i as u32 % 100);
346            coa.add_account(GLAccount::new(
347                format!("{num:06}"),
348                format!("Immobilisations {}", i + 1),
349                AccountType::Asset,
350                AccountSubType::FixedAssets,
351            ));
352        }
353        for i in 0..(count / 2).max(1) {
354            let num = 281000 + (i as u32 % 100);
355            coa.add_account(GLAccount::new(
356                format!("{num:06}"),
357                format!("Amortissements {}", i + 1),
358                AccountType::Asset,
359                AccountSubType::AccumulatedDepreciation,
360            ));
361        }
362    }
363
364    fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
365        for i in 0..count.max(1) {
366            let num = 310000 + (i as u32 % 1000);
367            coa.add_account(GLAccount::new(
368                format!("{num:06}"),
369                format!("Stocks {}", i + 1),
370                AccountType::Asset,
371                AccountSubType::Inventory,
372            ));
373        }
374    }
375
376    fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
377        for i in 0..count.max(1) {
378            let num = 411000 + (i as u32 % 1000);
379            coa.add_account(GLAccount::new(
380                format!("{num:06}"),
381                format!("Clients {}", i + 1),
382                AccountType::Asset,
383                AccountSubType::AccountsReceivable,
384            ));
385        }
386        for i in 0..count.max(1) {
387            let num = 401000 + (i as u32 % 1000);
388            coa.add_account(GLAccount::new(
389                format!("{num:06}"),
390                format!("Fournisseurs {}", i + 1),
391                AccountType::Liability,
392                AccountSubType::AccountsPayable,
393            ));
394        }
395        let clearing = GLAccount::new(
396            "408000".to_string(),
397            "Fournisseurs – non encore reçus".to_string(),
398            AccountType::Liability,
399            AccountSubType::GoodsReceivedClearing,
400        );
401        coa.add_account(clearing);
402    }
403
404    fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
405        let bases = [
406            (512, "Banque"),
407            (530, "Caisse"),
408            (511, "Valeurs à l'encaissement"),
409        ];
410        for (base, name) in bases {
411            for i in 0..(count / 3).max(1) {
412                let num = base * 1000 + (i as u32 % 100);
413                coa.add_account(GLAccount::new(
414                    format!("{num:06}"),
415                    format!("{} {}", name, i + 1),
416                    AccountType::Asset,
417                    AccountSubType::Cash,
418                ));
419            }
420        }
421    }
422
423    fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
424        let bases = [
425            (603, "Achats"),
426            (641, "Rémunérations"),
427            (681, "DAP"),
428            (613, "Loyers"),
429            (661, "Charges financières"),
430        ];
431        for (base, name) in bases {
432            for i in 0..(count / 5).max(1) {
433                let num = base * 1000 + (i as u32 % 100);
434                let mut account = GLAccount::new(
435                    format!("{num:06}"),
436                    format!("{} {}", name, i + 1),
437                    AccountType::Expense,
438                    AccountSubType::OperatingExpenses,
439                );
440                account.requires_cost_center = true;
441                coa.add_account(account);
442            }
443        }
444    }
445
446    fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
447        let bases = [
448            (701, "Ventes"),
449            (706, "Prestations"),
450            (758, "Produits divers"),
451        ];
452        for (base, name) in bases {
453            for i in 0..(count / 3).max(1) {
454                let num = base * 1000 + (i as u32 % 100);
455                coa.add_account(GLAccount::new(
456                    format!("{num:06}"),
457                    format!("{} {}", name, i + 1),
458                    AccountType::Revenue,
459                    AccountSubType::ProductRevenue,
460                ));
461            }
462        }
463    }
464
465    fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
466        coa.add_account(GLAccount::new(
467            "808000".to_string(),
468            "Comptes spéciaux".to_string(),
469            AccountType::Asset,
470            AccountSubType::SuspenseClearing,
471        ));
472    }
473
474    /// Insert all canonical accounts from `datasynth_core::accounts` into the CoA.
475    ///
476    /// These are the well-known account numbers (4-digit, 1000-9300 range) that
477    /// other generators reference. They are added before auto-generated accounts
478    /// (which start at 100000+) so there are no collisions.
479    fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
480        // --- Cash accounts (1000-series, Asset / Cash) ---
481        coa.add_account(GLAccount::new(
482            cash_accounts::OPERATING_CASH.to_string(),
483            "Operating Cash".to_string(),
484            AccountType::Asset,
485            AccountSubType::Cash,
486        ));
487        coa.add_account(GLAccount::new(
488            cash_accounts::BANK_ACCOUNT.to_string(),
489            "Bank Account".to_string(),
490            AccountType::Asset,
491            AccountSubType::Cash,
492        ));
493        coa.add_account(GLAccount::new(
494            cash_accounts::PETTY_CASH.to_string(),
495            "Petty Cash".to_string(),
496            AccountType::Asset,
497            AccountSubType::Cash,
498        ));
499        coa.add_account(GLAccount::new(
500            cash_accounts::WIRE_CLEARING.to_string(),
501            "Wire Transfer Clearing".to_string(),
502            AccountType::Asset,
503            AccountSubType::BankClearing,
504        ));
505
506        // --- Control accounts (Asset side) ---
507        {
508            let mut acct = GLAccount::new(
509                control_accounts::AR_CONTROL.to_string(),
510                "Accounts Receivable Control".to_string(),
511                AccountType::Asset,
512                AccountSubType::AccountsReceivable,
513            );
514            acct.is_control_account = true;
515            coa.add_account(acct);
516        }
517        {
518            let mut acct = GLAccount::new(
519                control_accounts::IC_AR_CLEARING.to_string(),
520                "Intercompany AR Clearing".to_string(),
521                AccountType::Asset,
522                AccountSubType::AccountsReceivable,
523            );
524            acct.is_control_account = true;
525            coa.add_account(acct);
526        }
527        coa.add_account(GLAccount::new(
528            control_accounts::INVENTORY.to_string(),
529            "Inventory".to_string(),
530            AccountType::Asset,
531            AccountSubType::Inventory,
532        ));
533        coa.add_account(GLAccount::new(
534            control_accounts::FIXED_ASSETS.to_string(),
535            "Fixed Assets".to_string(),
536            AccountType::Asset,
537            AccountSubType::FixedAssets,
538        ));
539        coa.add_account(GLAccount::new(
540            control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
541            "Accumulated Depreciation".to_string(),
542            AccountType::Asset,
543            AccountSubType::AccumulatedDepreciation,
544        ));
545
546        // --- Tax asset accounts ---
547        coa.add_account(GLAccount::new(
548            tax_accounts::INPUT_VAT.to_string(),
549            "Input VAT".to_string(),
550            AccountType::Asset,
551            AccountSubType::OtherReceivables,
552        ));
553        coa.add_account(GLAccount::new(
554            tax_accounts::DEFERRED_TAX_ASSET.to_string(),
555            "Deferred Tax Asset".to_string(),
556            AccountType::Asset,
557            AccountSubType::OtherAssets,
558        ));
559
560        // --- Liability / Control accounts (2000-series) ---
561        {
562            let mut acct = GLAccount::new(
563                control_accounts::AP_CONTROL.to_string(),
564                "Accounts Payable Control".to_string(),
565                AccountType::Liability,
566                AccountSubType::AccountsPayable,
567            );
568            acct.is_control_account = true;
569            coa.add_account(acct);
570        }
571        {
572            let mut acct = GLAccount::new(
573                control_accounts::IC_AP_CLEARING.to_string(),
574                "Intercompany AP Clearing".to_string(),
575                AccountType::Liability,
576                AccountSubType::AccountsPayable,
577            );
578            acct.is_control_account = true;
579            coa.add_account(acct);
580        }
581        coa.add_account(GLAccount::new(
582            tax_accounts::SALES_TAX_PAYABLE.to_string(),
583            "Sales Tax Payable".to_string(),
584            AccountType::Liability,
585            AccountSubType::TaxLiabilities,
586        ));
587        coa.add_account(GLAccount::new(
588            tax_accounts::VAT_PAYABLE.to_string(),
589            "VAT Payable".to_string(),
590            AccountType::Liability,
591            AccountSubType::TaxLiabilities,
592        ));
593        coa.add_account(GLAccount::new(
594            tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
595            "Withholding Tax Payable".to_string(),
596            AccountType::Liability,
597            AccountSubType::TaxLiabilities,
598        ));
599        coa.add_account(GLAccount::new(
600            liability_accounts::ACCRUED_EXPENSES.to_string(),
601            "Accrued Expenses".to_string(),
602            AccountType::Liability,
603            AccountSubType::AccruedLiabilities,
604        ));
605        coa.add_account(GLAccount::new(
606            liability_accounts::ACCRUED_SALARIES.to_string(),
607            "Accrued Salaries".to_string(),
608            AccountType::Liability,
609            AccountSubType::AccruedLiabilities,
610        ));
611        coa.add_account(GLAccount::new(
612            liability_accounts::ACCRUED_BENEFITS.to_string(),
613            "Accrued Benefits".to_string(),
614            AccountType::Liability,
615            AccountSubType::AccruedLiabilities,
616        ));
617        coa.add_account(GLAccount::new(
618            liability_accounts::UNEARNED_REVENUE.to_string(),
619            "Unearned Revenue".to_string(),
620            AccountType::Liability,
621            AccountSubType::DeferredRevenue,
622        ));
623        coa.add_account(GLAccount::new(
624            liability_accounts::SHORT_TERM_DEBT.to_string(),
625            "Short-Term Debt".to_string(),
626            AccountType::Liability,
627            AccountSubType::ShortTermDebt,
628        ));
629        coa.add_account(GLAccount::new(
630            tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
631            "Deferred Tax Liability".to_string(),
632            AccountType::Liability,
633            AccountSubType::TaxLiabilities,
634        ));
635        coa.add_account(GLAccount::new(
636            liability_accounts::LONG_TERM_DEBT.to_string(),
637            "Long-Term Debt".to_string(),
638            AccountType::Liability,
639            AccountSubType::LongTermDebt,
640        ));
641        coa.add_account(GLAccount::new(
642            liability_accounts::IC_PAYABLE.to_string(),
643            "Intercompany Payable".to_string(),
644            AccountType::Liability,
645            AccountSubType::OtherLiabilities,
646        ));
647        {
648            let mut acct = GLAccount::new(
649                control_accounts::GR_IR_CLEARING.to_string(),
650                "GR/IR Clearing".to_string(),
651                AccountType::Liability,
652                AccountSubType::GoodsReceivedClearing,
653            );
654            acct.is_suspense_account = true;
655            coa.add_account(acct);
656        }
657
658        // --- Equity accounts (3000-series) ---
659        coa.add_account(GLAccount::new(
660            equity_accounts::COMMON_STOCK.to_string(),
661            "Common Stock".to_string(),
662            AccountType::Equity,
663            AccountSubType::CommonStock,
664        ));
665        coa.add_account(GLAccount::new(
666            equity_accounts::APIC.to_string(),
667            "Additional Paid-In Capital".to_string(),
668            AccountType::Equity,
669            AccountSubType::AdditionalPaidInCapital,
670        ));
671        coa.add_account(GLAccount::new(
672            equity_accounts::RETAINED_EARNINGS.to_string(),
673            "Retained Earnings".to_string(),
674            AccountType::Equity,
675            AccountSubType::RetainedEarnings,
676        ));
677        coa.add_account(GLAccount::new(
678            equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
679            "Current Year Earnings".to_string(),
680            AccountType::Equity,
681            AccountSubType::NetIncome,
682        ));
683        coa.add_account(GLAccount::new(
684            equity_accounts::TREASURY_STOCK.to_string(),
685            "Treasury Stock".to_string(),
686            AccountType::Equity,
687            AccountSubType::TreasuryStock,
688        ));
689        coa.add_account(GLAccount::new(
690            equity_accounts::CTA.to_string(),
691            "Currency Translation Adjustment".to_string(),
692            AccountType::Equity,
693            AccountSubType::OtherComprehensiveIncome,
694        ));
695
696        // --- Revenue accounts (4000-series) ---
697        coa.add_account(GLAccount::new(
698            revenue_accounts::PRODUCT_REVENUE.to_string(),
699            "Product Revenue".to_string(),
700            AccountType::Revenue,
701            AccountSubType::ProductRevenue,
702        ));
703        coa.add_account(GLAccount::new(
704            revenue_accounts::SALES_DISCOUNTS.to_string(),
705            "Sales Discounts".to_string(),
706            AccountType::Revenue,
707            AccountSubType::ProductRevenue,
708        ));
709        coa.add_account(GLAccount::new(
710            revenue_accounts::SALES_RETURNS.to_string(),
711            "Sales Returns and Allowances".to_string(),
712            AccountType::Revenue,
713            AccountSubType::ProductRevenue,
714        ));
715        coa.add_account(GLAccount::new(
716            revenue_accounts::SERVICE_REVENUE.to_string(),
717            "Service Revenue".to_string(),
718            AccountType::Revenue,
719            AccountSubType::ServiceRevenue,
720        ));
721        coa.add_account(GLAccount::new(
722            revenue_accounts::IC_REVENUE.to_string(),
723            "Intercompany Revenue".to_string(),
724            AccountType::Revenue,
725            AccountSubType::OtherIncome,
726        ));
727        coa.add_account(GLAccount::new(
728            revenue_accounts::OTHER_REVENUE.to_string(),
729            "Other Revenue".to_string(),
730            AccountType::Revenue,
731            AccountSubType::OtherIncome,
732        ));
733
734        // --- Expense accounts (5000-7xxx series) ---
735        {
736            let mut acct = GLAccount::new(
737                expense_accounts::COGS.to_string(),
738                "Cost of Goods Sold".to_string(),
739                AccountType::Expense,
740                AccountSubType::CostOfGoodsSold,
741            );
742            acct.requires_cost_center = true;
743            coa.add_account(acct);
744        }
745        {
746            let mut acct = GLAccount::new(
747                expense_accounts::RAW_MATERIALS.to_string(),
748                "Raw Materials".to_string(),
749                AccountType::Expense,
750                AccountSubType::CostOfGoodsSold,
751            );
752            acct.requires_cost_center = true;
753            coa.add_account(acct);
754        }
755        {
756            let mut acct = GLAccount::new(
757                expense_accounts::DIRECT_LABOR.to_string(),
758                "Direct Labor".to_string(),
759                AccountType::Expense,
760                AccountSubType::CostOfGoodsSold,
761            );
762            acct.requires_cost_center = true;
763            coa.add_account(acct);
764        }
765        {
766            let mut acct = GLAccount::new(
767                expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
768                "Manufacturing Overhead".to_string(),
769                AccountType::Expense,
770                AccountSubType::CostOfGoodsSold,
771            );
772            acct.requires_cost_center = true;
773            coa.add_account(acct);
774        }
775        {
776            let mut acct = GLAccount::new(
777                expense_accounts::DEPRECIATION.to_string(),
778                "Depreciation Expense".to_string(),
779                AccountType::Expense,
780                AccountSubType::DepreciationExpense,
781            );
782            acct.requires_cost_center = true;
783            coa.add_account(acct);
784        }
785        {
786            let mut acct = GLAccount::new(
787                expense_accounts::SALARIES_WAGES.to_string(),
788                "Salaries and Wages".to_string(),
789                AccountType::Expense,
790                AccountSubType::OperatingExpenses,
791            );
792            acct.requires_cost_center = true;
793            coa.add_account(acct);
794        }
795        {
796            let mut acct = GLAccount::new(
797                expense_accounts::BENEFITS.to_string(),
798                "Benefits Expense".to_string(),
799                AccountType::Expense,
800                AccountSubType::OperatingExpenses,
801            );
802            acct.requires_cost_center = true;
803            coa.add_account(acct);
804        }
805        {
806            let mut acct = GLAccount::new(
807                expense_accounts::RENT.to_string(),
808                "Rent Expense".to_string(),
809                AccountType::Expense,
810                AccountSubType::OperatingExpenses,
811            );
812            acct.requires_cost_center = true;
813            coa.add_account(acct);
814        }
815        {
816            let mut acct = GLAccount::new(
817                expense_accounts::UTILITIES.to_string(),
818                "Utilities Expense".to_string(),
819                AccountType::Expense,
820                AccountSubType::OperatingExpenses,
821            );
822            acct.requires_cost_center = true;
823            coa.add_account(acct);
824        }
825        {
826            let mut acct = GLAccount::new(
827                expense_accounts::OFFICE_SUPPLIES.to_string(),
828                "Office Supplies".to_string(),
829                AccountType::Expense,
830                AccountSubType::AdministrativeExpenses,
831            );
832            acct.requires_cost_center = true;
833            coa.add_account(acct);
834        }
835        {
836            let mut acct = GLAccount::new(
837                expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
838                "Travel and Entertainment".to_string(),
839                AccountType::Expense,
840                AccountSubType::SellingExpenses,
841            );
842            acct.requires_cost_center = true;
843            coa.add_account(acct);
844        }
845        {
846            let mut acct = GLAccount::new(
847                expense_accounts::PROFESSIONAL_FEES.to_string(),
848                "Professional Fees".to_string(),
849                AccountType::Expense,
850                AccountSubType::AdministrativeExpenses,
851            );
852            acct.requires_cost_center = true;
853            coa.add_account(acct);
854        }
855        {
856            let mut acct = GLAccount::new(
857                expense_accounts::INSURANCE.to_string(),
858                "Insurance Expense".to_string(),
859                AccountType::Expense,
860                AccountSubType::OperatingExpenses,
861            );
862            acct.requires_cost_center = true;
863            coa.add_account(acct);
864        }
865        {
866            let mut acct = GLAccount::new(
867                expense_accounts::BAD_DEBT.to_string(),
868                "Bad Debt Expense".to_string(),
869                AccountType::Expense,
870                AccountSubType::OperatingExpenses,
871            );
872            acct.requires_cost_center = true;
873            coa.add_account(acct);
874        }
875        {
876            let mut acct = GLAccount::new(
877                expense_accounts::INTEREST_EXPENSE.to_string(),
878                "Interest Expense".to_string(),
879                AccountType::Expense,
880                AccountSubType::InterestExpense,
881            );
882            acct.requires_cost_center = true;
883            coa.add_account(acct);
884        }
885        {
886            let mut acct = GLAccount::new(
887                expense_accounts::PURCHASE_DISCOUNTS.to_string(),
888                "Purchase Discounts".to_string(),
889                AccountType::Expense,
890                AccountSubType::OtherExpenses,
891            );
892            acct.requires_cost_center = true;
893            coa.add_account(acct);
894        }
895        {
896            let mut acct = GLAccount::new(
897                expense_accounts::FX_GAIN_LOSS.to_string(),
898                "FX Gain/Loss".to_string(),
899                AccountType::Expense,
900                AccountSubType::ForeignExchangeLoss,
901            );
902            acct.requires_cost_center = true;
903            coa.add_account(acct);
904        }
905
906        // --- Tax expense (8000-series) ---
907        {
908            let mut acct = GLAccount::new(
909                tax_accounts::TAX_EXPENSE.to_string(),
910                "Tax Expense".to_string(),
911                AccountType::Expense,
912                AccountSubType::TaxExpense,
913            );
914            acct.requires_cost_center = true;
915            coa.add_account(acct);
916        }
917
918        // --- Suspense / Clearing accounts (9000-series) ---
919        {
920            let mut acct = GLAccount::new(
921                suspense_accounts::GENERAL_SUSPENSE.to_string(),
922                "General Suspense".to_string(),
923                AccountType::Asset,
924                AccountSubType::SuspenseClearing,
925            );
926            acct.is_suspense_account = true;
927            coa.add_account(acct);
928        }
929        {
930            let mut acct = GLAccount::new(
931                suspense_accounts::PAYROLL_CLEARING.to_string(),
932                "Payroll Clearing".to_string(),
933                AccountType::Asset,
934                AccountSubType::SuspenseClearing,
935            );
936            acct.is_suspense_account = true;
937            coa.add_account(acct);
938        }
939        {
940            let mut acct = GLAccount::new(
941                suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
942                "Bank Reconciliation Suspense".to_string(),
943                AccountType::Asset,
944                AccountSubType::BankClearing,
945            );
946            acct.is_suspense_account = true;
947            coa.add_account(acct);
948        }
949        {
950            let mut acct = GLAccount::new(
951                suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
952                "IC Elimination Suspense".to_string(),
953                AccountType::Asset,
954                AccountSubType::IntercompanyClearing,
955            );
956            acct.is_suspense_account = true;
957            coa.add_account(acct);
958        }
959    }
960
961    fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
962        let sub_types = vec![
963            (AccountSubType::Cash, "Cash", 0.15),
964            (
965                AccountSubType::AccountsReceivable,
966                "Accounts Receivable",
967                0.20,
968            ),
969            (AccountSubType::Inventory, "Inventory", 0.15),
970            (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
971            (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
972            (
973                AccountSubType::AccumulatedDepreciation,
974                "Accumulated Depreciation",
975                0.10,
976            ),
977            (AccountSubType::OtherAssets, "Other Assets", 0.05),
978        ];
979
980        let mut account_num = 100000u32;
981        for (sub_type, name_prefix, weight) in sub_types {
982            let sub_count = ((count as f64) * weight).round() as usize;
983            for i in 0..sub_count.max(1) {
984                let account = GLAccount::new(
985                    format!("{account_num}"),
986                    format!("{} {}", name_prefix, i + 1),
987                    AccountType::Asset,
988                    sub_type,
989                );
990                coa.add_account(account);
991                account_num += 10;
992            }
993        }
994    }
995
996    fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
997        let sub_types = vec![
998            (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
999            (
1000                AccountSubType::AccruedLiabilities,
1001                "Accrued Liabilities",
1002                0.20,
1003            ),
1004            (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
1005            (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
1006            (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
1007            (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
1008        ];
1009
1010        let mut account_num = 200000u32;
1011        for (sub_type, name_prefix, weight) in sub_types {
1012            let sub_count = ((count as f64) * weight).round() as usize;
1013            for i in 0..sub_count.max(1) {
1014                let account = GLAccount::new(
1015                    format!("{account_num}"),
1016                    format!("{} {}", name_prefix, i + 1),
1017                    AccountType::Liability,
1018                    sub_type,
1019                );
1020                coa.add_account(account);
1021                account_num += 10;
1022            }
1023        }
1024    }
1025
1026    fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1027        let sub_types = vec![
1028            (AccountSubType::CommonStock, "Common Stock", 0.20),
1029            (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
1030            (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
1031            (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
1032        ];
1033
1034        let mut account_num = 300000u32;
1035        for (sub_type, name_prefix, weight) in sub_types {
1036            let sub_count = ((count as f64) * weight).round() as usize;
1037            for i in 0..sub_count.max(1) {
1038                let account = GLAccount::new(
1039                    format!("{account_num}"),
1040                    format!("{} {}", name_prefix, i + 1),
1041                    AccountType::Equity,
1042                    sub_type,
1043                );
1044                coa.add_account(account);
1045                account_num += 10;
1046            }
1047        }
1048    }
1049
1050    fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1051        let sub_types = vec![
1052            (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
1053            (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
1054            (AccountSubType::InterestIncome, "Interest Income", 0.10),
1055            (AccountSubType::OtherIncome, "Other Income", 0.20),
1056        ];
1057
1058        let mut account_num = 400000u32;
1059        for (sub_type, name_prefix, weight) in sub_types {
1060            let sub_count = ((count as f64) * weight).round() as usize;
1061            for i in 0..sub_count.max(1) {
1062                let account = GLAccount::new(
1063                    format!("{account_num}"),
1064                    format!("{} {}", name_prefix, i + 1),
1065                    AccountType::Revenue,
1066                    sub_type,
1067                );
1068                coa.add_account(account);
1069                account_num += 10;
1070            }
1071        }
1072    }
1073
1074    fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1075        let sub_types = vec![
1076            (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
1077            (
1078                AccountSubType::OperatingExpenses,
1079                "Operating Expenses",
1080                0.25,
1081            ),
1082            (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
1083            (
1084                AccountSubType::AdministrativeExpenses,
1085                "Admin Expenses",
1086                0.15,
1087            ),
1088            (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
1089            (AccountSubType::InterestExpense, "Interest Expense", 0.05),
1090            (AccountSubType::TaxExpense, "Tax Expense", 0.05),
1091            (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
1092        ];
1093
1094        let mut account_num = 500000u32;
1095        for (sub_type, name_prefix, weight) in sub_types {
1096            let sub_count = ((count as f64) * weight).round() as usize;
1097            for i in 0..sub_count.max(1) {
1098                let mut account = GLAccount::new(
1099                    format!("{account_num}"),
1100                    format!("{} {}", name_prefix, i + 1),
1101                    AccountType::Expense,
1102                    sub_type,
1103                );
1104                account.requires_cost_center = true;
1105                coa.add_account(account);
1106                account_num += 10;
1107            }
1108        }
1109    }
1110
1111    fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
1112        let suspense_types = vec![
1113            (AccountSubType::SuspenseClearing, "Suspense Clearing"),
1114            (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
1115            (AccountSubType::BankClearing, "Bank Clearing"),
1116            (
1117                AccountSubType::IntercompanyClearing,
1118                "Intercompany Clearing",
1119            ),
1120        ];
1121
1122        let mut account_num = 199000u32;
1123        for (sub_type, name) in suspense_types {
1124            let mut account = GLAccount::new(
1125                format!("{account_num}"),
1126                name.to_string(),
1127                AccountType::Asset,
1128                sub_type,
1129            );
1130            account.is_suspense_account = true;
1131            coa.add_account(account);
1132            account_num += 100;
1133        }
1134    }
1135}
1136
1137impl Generator for ChartOfAccountsGenerator {
1138    type Item = ChartOfAccounts;
1139    type Config = (CoAComplexity, IndustrySector);
1140
1141    fn new(config: Self::Config, seed: u64) -> Self {
1142        Self::new(config.0, config.1, seed)
1143    }
1144
1145    fn generate_one(&mut self) -> Self::Item {
1146        self.generate()
1147    }
1148
1149    fn reset(&mut self) {
1150        self.rng = seeded_rng(self.seed, 0);
1151        self.count = 0;
1152        self.coa_framework = CoAFramework::UsGaap;
1153    }
1154
1155    fn count(&self) -> u64 {
1156        self.count
1157    }
1158
1159    fn seed(&self) -> u64 {
1160        self.seed
1161    }
1162}
1163
1164#[cfg(test)]
1165#[allow(clippy::unwrap_used)]
1166mod tests {
1167    use super::*;
1168
1169    #[test]
1170    fn test_generate_small_coa() {
1171        let mut gen =
1172            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
1173        let coa = gen.generate();
1174
1175        assert!(coa.account_count() >= 50);
1176        assert!(!coa.get_suspense_accounts().is_empty());
1177    }
1178
1179    #[test]
1180    fn test_generate_pcg_coa() {
1181        let mut gen =
1182            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1183                .with_french_pcg(true);
1184        let coa = gen.generate();
1185
1186        assert_eq!(coa.country, "FR");
1187        assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
1188        assert!(coa.account_count() >= 20);
1189        // PCG accounts are 6-digit (e.g. 411000, 601000)
1190        let first = coa.accounts.first().expect("has accounts");
1191        assert_eq!(first.account_number.len(), 6);
1192    }
1193
1194    /// Verifies PCG (Plan Comptable Général) account structure: 6-digit format,
1195    /// all accounts numeric, and coverage of at least two PCG classes (1–8).
1196    /// Works for both the embedded PCG 2024 loader and the fallback generator.
1197    #[test]
1198    fn test_pcg_account_structure() {
1199        let mut gen =
1200            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1201                .with_french_pcg(true);
1202        let coa = gen.generate();
1203
1204        assert_eq!(
1205            coa.account_format, "######",
1206            "PCG uses 6-digit account format"
1207        );
1208        assert!(
1209            coa.account_count() >= 20,
1210            "PCG CoA has minimum account count"
1211        );
1212
1213        let account_numbers: Vec<_> = coa
1214            .accounts
1215            .iter()
1216            .map(|a| a.account_number.as_str())
1217            .collect();
1218        for num in &account_numbers {
1219            assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
1220            assert!(
1221                num.chars().all(|c| c.is_ascii_digit()),
1222                "PCG account is numeric: {}",
1223                num
1224            );
1225        }
1226
1227        // All account numbers must belong to a PCG class (first digit 1–8)
1228        let first_digits: std::collections::HashSet<char> = account_numbers
1229            .iter()
1230            .filter_map(|s| s.chars().next())
1231            .collect();
1232        let pcg_classes: std::collections::HashSet<_> =
1233            ['1', '2', '3', '4', '5', '6', '7', '8'].into();
1234        assert!(
1235            !first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
1236            "PCG account numbers must be in classes 1–8, got first digits: {:?}",
1237            first_digits
1238        );
1239    }
1240}