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::traits::Generator;
11use datasynth_core::utils::seeded_rng;
12use rand_chacha::ChaCha8Rng;
13
14/// Generator for Chart of Accounts.
15pub struct ChartOfAccountsGenerator {
16    rng: ChaCha8Rng,
17    seed: u64,
18    complexity: CoAComplexity,
19    industry: IndustrySector,
20    count: u64,
21}
22
23impl ChartOfAccountsGenerator {
24    /// Create a new CoA generator.
25    pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
26        Self {
27            rng: seeded_rng(seed, 0),
28            seed,
29            complexity,
30            industry,
31            count: 0,
32        }
33    }
34
35    /// Generate a complete chart of accounts.
36    pub fn generate(&mut self) -> ChartOfAccounts {
37        debug!(
38            complexity = ?self.complexity,
39            industry = ?self.industry,
40            seed = self.seed,
41            "Generating chart of accounts"
42        );
43
44        self.count += 1;
45        let target_count = self.complexity.target_count();
46
47        let mut coa = ChartOfAccounts::new(
48            format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
49            format!("{:?} Chart of Accounts", self.industry),
50            "US".to_string(),
51            self.industry,
52            self.complexity,
53        );
54
55        // Seed canonical accounts first so other generators can find them
56        Self::seed_canonical_accounts(&mut coa);
57
58        // Generate additional accounts by type
59        self.generate_asset_accounts(&mut coa, target_count / 5);
60        self.generate_liability_accounts(&mut coa, target_count / 6);
61        self.generate_equity_accounts(&mut coa, target_count / 10);
62        self.generate_revenue_accounts(&mut coa, target_count / 5);
63        self.generate_expense_accounts(&mut coa, target_count / 4);
64        self.generate_suspense_accounts(&mut coa);
65
66        coa
67    }
68
69    /// Insert all canonical accounts from `datasynth_core::accounts` into the CoA.
70    ///
71    /// These are the well-known account numbers (4-digit, 1000-9300 range) that
72    /// other generators reference. They are added before auto-generated accounts
73    /// (which start at 100000+) so there are no collisions.
74    fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
75        // --- Cash accounts (1000-series, Asset / Cash) ---
76        coa.add_account(GLAccount::new(
77            cash_accounts::OPERATING_CASH.to_string(),
78            "Operating Cash".to_string(),
79            AccountType::Asset,
80            AccountSubType::Cash,
81        ));
82        coa.add_account(GLAccount::new(
83            cash_accounts::BANK_ACCOUNT.to_string(),
84            "Bank Account".to_string(),
85            AccountType::Asset,
86            AccountSubType::Cash,
87        ));
88        coa.add_account(GLAccount::new(
89            cash_accounts::PETTY_CASH.to_string(),
90            "Petty Cash".to_string(),
91            AccountType::Asset,
92            AccountSubType::Cash,
93        ));
94        coa.add_account(GLAccount::new(
95            cash_accounts::WIRE_CLEARING.to_string(),
96            "Wire Transfer Clearing".to_string(),
97            AccountType::Asset,
98            AccountSubType::BankClearing,
99        ));
100
101        // --- Control accounts (Asset side) ---
102        {
103            let mut acct = GLAccount::new(
104                control_accounts::AR_CONTROL.to_string(),
105                "Accounts Receivable Control".to_string(),
106                AccountType::Asset,
107                AccountSubType::AccountsReceivable,
108            );
109            acct.is_control_account = true;
110            coa.add_account(acct);
111        }
112        {
113            let mut acct = GLAccount::new(
114                control_accounts::IC_AR_CLEARING.to_string(),
115                "Intercompany AR Clearing".to_string(),
116                AccountType::Asset,
117                AccountSubType::AccountsReceivable,
118            );
119            acct.is_control_account = true;
120            coa.add_account(acct);
121        }
122        coa.add_account(GLAccount::new(
123            control_accounts::INVENTORY.to_string(),
124            "Inventory".to_string(),
125            AccountType::Asset,
126            AccountSubType::Inventory,
127        ));
128        coa.add_account(GLAccount::new(
129            control_accounts::FIXED_ASSETS.to_string(),
130            "Fixed Assets".to_string(),
131            AccountType::Asset,
132            AccountSubType::FixedAssets,
133        ));
134        coa.add_account(GLAccount::new(
135            control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
136            "Accumulated Depreciation".to_string(),
137            AccountType::Asset,
138            AccountSubType::AccumulatedDepreciation,
139        ));
140
141        // --- Tax asset accounts ---
142        coa.add_account(GLAccount::new(
143            tax_accounts::INPUT_VAT.to_string(),
144            "Input VAT".to_string(),
145            AccountType::Asset,
146            AccountSubType::OtherReceivables,
147        ));
148        coa.add_account(GLAccount::new(
149            tax_accounts::DEFERRED_TAX_ASSET.to_string(),
150            "Deferred Tax Asset".to_string(),
151            AccountType::Asset,
152            AccountSubType::OtherAssets,
153        ));
154
155        // --- Liability / Control accounts (2000-series) ---
156        {
157            let mut acct = GLAccount::new(
158                control_accounts::AP_CONTROL.to_string(),
159                "Accounts Payable Control".to_string(),
160                AccountType::Liability,
161                AccountSubType::AccountsPayable,
162            );
163            acct.is_control_account = true;
164            coa.add_account(acct);
165        }
166        {
167            let mut acct = GLAccount::new(
168                control_accounts::IC_AP_CLEARING.to_string(),
169                "Intercompany AP Clearing".to_string(),
170                AccountType::Liability,
171                AccountSubType::AccountsPayable,
172            );
173            acct.is_control_account = true;
174            coa.add_account(acct);
175        }
176        coa.add_account(GLAccount::new(
177            tax_accounts::SALES_TAX_PAYABLE.to_string(),
178            "Sales Tax Payable".to_string(),
179            AccountType::Liability,
180            AccountSubType::TaxLiabilities,
181        ));
182        coa.add_account(GLAccount::new(
183            tax_accounts::VAT_PAYABLE.to_string(),
184            "VAT Payable".to_string(),
185            AccountType::Liability,
186            AccountSubType::TaxLiabilities,
187        ));
188        coa.add_account(GLAccount::new(
189            tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
190            "Withholding Tax Payable".to_string(),
191            AccountType::Liability,
192            AccountSubType::TaxLiabilities,
193        ));
194        coa.add_account(GLAccount::new(
195            liability_accounts::ACCRUED_EXPENSES.to_string(),
196            "Accrued Expenses".to_string(),
197            AccountType::Liability,
198            AccountSubType::AccruedLiabilities,
199        ));
200        coa.add_account(GLAccount::new(
201            liability_accounts::ACCRUED_SALARIES.to_string(),
202            "Accrued Salaries".to_string(),
203            AccountType::Liability,
204            AccountSubType::AccruedLiabilities,
205        ));
206        coa.add_account(GLAccount::new(
207            liability_accounts::ACCRUED_BENEFITS.to_string(),
208            "Accrued Benefits".to_string(),
209            AccountType::Liability,
210            AccountSubType::AccruedLiabilities,
211        ));
212        coa.add_account(GLAccount::new(
213            liability_accounts::UNEARNED_REVENUE.to_string(),
214            "Unearned Revenue".to_string(),
215            AccountType::Liability,
216            AccountSubType::DeferredRevenue,
217        ));
218        coa.add_account(GLAccount::new(
219            liability_accounts::SHORT_TERM_DEBT.to_string(),
220            "Short-Term Debt".to_string(),
221            AccountType::Liability,
222            AccountSubType::ShortTermDebt,
223        ));
224        coa.add_account(GLAccount::new(
225            tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
226            "Deferred Tax Liability".to_string(),
227            AccountType::Liability,
228            AccountSubType::TaxLiabilities,
229        ));
230        coa.add_account(GLAccount::new(
231            liability_accounts::LONG_TERM_DEBT.to_string(),
232            "Long-Term Debt".to_string(),
233            AccountType::Liability,
234            AccountSubType::LongTermDebt,
235        ));
236        coa.add_account(GLAccount::new(
237            liability_accounts::IC_PAYABLE.to_string(),
238            "Intercompany Payable".to_string(),
239            AccountType::Liability,
240            AccountSubType::OtherLiabilities,
241        ));
242        {
243            let mut acct = GLAccount::new(
244                control_accounts::GR_IR_CLEARING.to_string(),
245                "GR/IR Clearing".to_string(),
246                AccountType::Liability,
247                AccountSubType::GoodsReceivedClearing,
248            );
249            acct.is_suspense_account = true;
250            coa.add_account(acct);
251        }
252
253        // --- Equity accounts (3000-series) ---
254        coa.add_account(GLAccount::new(
255            equity_accounts::COMMON_STOCK.to_string(),
256            "Common Stock".to_string(),
257            AccountType::Equity,
258            AccountSubType::CommonStock,
259        ));
260        coa.add_account(GLAccount::new(
261            equity_accounts::APIC.to_string(),
262            "Additional Paid-In Capital".to_string(),
263            AccountType::Equity,
264            AccountSubType::AdditionalPaidInCapital,
265        ));
266        coa.add_account(GLAccount::new(
267            equity_accounts::RETAINED_EARNINGS.to_string(),
268            "Retained Earnings".to_string(),
269            AccountType::Equity,
270            AccountSubType::RetainedEarnings,
271        ));
272        coa.add_account(GLAccount::new(
273            equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
274            "Current Year Earnings".to_string(),
275            AccountType::Equity,
276            AccountSubType::NetIncome,
277        ));
278        coa.add_account(GLAccount::new(
279            equity_accounts::TREASURY_STOCK.to_string(),
280            "Treasury Stock".to_string(),
281            AccountType::Equity,
282            AccountSubType::TreasuryStock,
283        ));
284        coa.add_account(GLAccount::new(
285            equity_accounts::CTA.to_string(),
286            "Currency Translation Adjustment".to_string(),
287            AccountType::Equity,
288            AccountSubType::OtherComprehensiveIncome,
289        ));
290
291        // --- Revenue accounts (4000-series) ---
292        coa.add_account(GLAccount::new(
293            revenue_accounts::PRODUCT_REVENUE.to_string(),
294            "Product Revenue".to_string(),
295            AccountType::Revenue,
296            AccountSubType::ProductRevenue,
297        ));
298        coa.add_account(GLAccount::new(
299            revenue_accounts::SALES_DISCOUNTS.to_string(),
300            "Sales Discounts".to_string(),
301            AccountType::Revenue,
302            AccountSubType::ProductRevenue,
303        ));
304        coa.add_account(GLAccount::new(
305            revenue_accounts::SALES_RETURNS.to_string(),
306            "Sales Returns and Allowances".to_string(),
307            AccountType::Revenue,
308            AccountSubType::ProductRevenue,
309        ));
310        coa.add_account(GLAccount::new(
311            revenue_accounts::SERVICE_REVENUE.to_string(),
312            "Service Revenue".to_string(),
313            AccountType::Revenue,
314            AccountSubType::ServiceRevenue,
315        ));
316        coa.add_account(GLAccount::new(
317            revenue_accounts::IC_REVENUE.to_string(),
318            "Intercompany Revenue".to_string(),
319            AccountType::Revenue,
320            AccountSubType::OtherIncome,
321        ));
322        coa.add_account(GLAccount::new(
323            revenue_accounts::OTHER_REVENUE.to_string(),
324            "Other Revenue".to_string(),
325            AccountType::Revenue,
326            AccountSubType::OtherIncome,
327        ));
328
329        // --- Expense accounts (5000-7xxx series) ---
330        {
331            let mut acct = GLAccount::new(
332                expense_accounts::COGS.to_string(),
333                "Cost of Goods Sold".to_string(),
334                AccountType::Expense,
335                AccountSubType::CostOfGoodsSold,
336            );
337            acct.requires_cost_center = true;
338            coa.add_account(acct);
339        }
340        {
341            let mut acct = GLAccount::new(
342                expense_accounts::RAW_MATERIALS.to_string(),
343                "Raw Materials".to_string(),
344                AccountType::Expense,
345                AccountSubType::CostOfGoodsSold,
346            );
347            acct.requires_cost_center = true;
348            coa.add_account(acct);
349        }
350        {
351            let mut acct = GLAccount::new(
352                expense_accounts::DIRECT_LABOR.to_string(),
353                "Direct Labor".to_string(),
354                AccountType::Expense,
355                AccountSubType::CostOfGoodsSold,
356            );
357            acct.requires_cost_center = true;
358            coa.add_account(acct);
359        }
360        {
361            let mut acct = GLAccount::new(
362                expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
363                "Manufacturing Overhead".to_string(),
364                AccountType::Expense,
365                AccountSubType::CostOfGoodsSold,
366            );
367            acct.requires_cost_center = true;
368            coa.add_account(acct);
369        }
370        {
371            let mut acct = GLAccount::new(
372                expense_accounts::DEPRECIATION.to_string(),
373                "Depreciation Expense".to_string(),
374                AccountType::Expense,
375                AccountSubType::DepreciationExpense,
376            );
377            acct.requires_cost_center = true;
378            coa.add_account(acct);
379        }
380        {
381            let mut acct = GLAccount::new(
382                expense_accounts::SALARIES_WAGES.to_string(),
383                "Salaries and Wages".to_string(),
384                AccountType::Expense,
385                AccountSubType::OperatingExpenses,
386            );
387            acct.requires_cost_center = true;
388            coa.add_account(acct);
389        }
390        {
391            let mut acct = GLAccount::new(
392                expense_accounts::BENEFITS.to_string(),
393                "Benefits Expense".to_string(),
394                AccountType::Expense,
395                AccountSubType::OperatingExpenses,
396            );
397            acct.requires_cost_center = true;
398            coa.add_account(acct);
399        }
400        {
401            let mut acct = GLAccount::new(
402                expense_accounts::RENT.to_string(),
403                "Rent Expense".to_string(),
404                AccountType::Expense,
405                AccountSubType::OperatingExpenses,
406            );
407            acct.requires_cost_center = true;
408            coa.add_account(acct);
409        }
410        {
411            let mut acct = GLAccount::new(
412                expense_accounts::UTILITIES.to_string(),
413                "Utilities Expense".to_string(),
414                AccountType::Expense,
415                AccountSubType::OperatingExpenses,
416            );
417            acct.requires_cost_center = true;
418            coa.add_account(acct);
419        }
420        {
421            let mut acct = GLAccount::new(
422                expense_accounts::OFFICE_SUPPLIES.to_string(),
423                "Office Supplies".to_string(),
424                AccountType::Expense,
425                AccountSubType::AdministrativeExpenses,
426            );
427            acct.requires_cost_center = true;
428            coa.add_account(acct);
429        }
430        {
431            let mut acct = GLAccount::new(
432                expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
433                "Travel and Entertainment".to_string(),
434                AccountType::Expense,
435                AccountSubType::SellingExpenses,
436            );
437            acct.requires_cost_center = true;
438            coa.add_account(acct);
439        }
440        {
441            let mut acct = GLAccount::new(
442                expense_accounts::PROFESSIONAL_FEES.to_string(),
443                "Professional Fees".to_string(),
444                AccountType::Expense,
445                AccountSubType::AdministrativeExpenses,
446            );
447            acct.requires_cost_center = true;
448            coa.add_account(acct);
449        }
450        {
451            let mut acct = GLAccount::new(
452                expense_accounts::INSURANCE.to_string(),
453                "Insurance Expense".to_string(),
454                AccountType::Expense,
455                AccountSubType::OperatingExpenses,
456            );
457            acct.requires_cost_center = true;
458            coa.add_account(acct);
459        }
460        {
461            let mut acct = GLAccount::new(
462                expense_accounts::BAD_DEBT.to_string(),
463                "Bad Debt Expense".to_string(),
464                AccountType::Expense,
465                AccountSubType::OperatingExpenses,
466            );
467            acct.requires_cost_center = true;
468            coa.add_account(acct);
469        }
470        {
471            let mut acct = GLAccount::new(
472                expense_accounts::INTEREST_EXPENSE.to_string(),
473                "Interest Expense".to_string(),
474                AccountType::Expense,
475                AccountSubType::InterestExpense,
476            );
477            acct.requires_cost_center = true;
478            coa.add_account(acct);
479        }
480        {
481            let mut acct = GLAccount::new(
482                expense_accounts::PURCHASE_DISCOUNTS.to_string(),
483                "Purchase Discounts".to_string(),
484                AccountType::Expense,
485                AccountSubType::OtherExpenses,
486            );
487            acct.requires_cost_center = true;
488            coa.add_account(acct);
489        }
490        {
491            let mut acct = GLAccount::new(
492                expense_accounts::FX_GAIN_LOSS.to_string(),
493                "FX Gain/Loss".to_string(),
494                AccountType::Expense,
495                AccountSubType::ForeignExchangeLoss,
496            );
497            acct.requires_cost_center = true;
498            coa.add_account(acct);
499        }
500
501        // --- Tax expense (8000-series) ---
502        {
503            let mut acct = GLAccount::new(
504                tax_accounts::TAX_EXPENSE.to_string(),
505                "Tax Expense".to_string(),
506                AccountType::Expense,
507                AccountSubType::TaxExpense,
508            );
509            acct.requires_cost_center = true;
510            coa.add_account(acct);
511        }
512
513        // --- Suspense / Clearing accounts (9000-series) ---
514        {
515            let mut acct = GLAccount::new(
516                suspense_accounts::GENERAL_SUSPENSE.to_string(),
517                "General Suspense".to_string(),
518                AccountType::Asset,
519                AccountSubType::SuspenseClearing,
520            );
521            acct.is_suspense_account = true;
522            coa.add_account(acct);
523        }
524        {
525            let mut acct = GLAccount::new(
526                suspense_accounts::PAYROLL_CLEARING.to_string(),
527                "Payroll Clearing".to_string(),
528                AccountType::Asset,
529                AccountSubType::SuspenseClearing,
530            );
531            acct.is_suspense_account = true;
532            coa.add_account(acct);
533        }
534        {
535            let mut acct = GLAccount::new(
536                suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
537                "Bank Reconciliation Suspense".to_string(),
538                AccountType::Asset,
539                AccountSubType::BankClearing,
540            );
541            acct.is_suspense_account = true;
542            coa.add_account(acct);
543        }
544        {
545            let mut acct = GLAccount::new(
546                suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
547                "IC Elimination Suspense".to_string(),
548                AccountType::Asset,
549                AccountSubType::IntercompanyClearing,
550            );
551            acct.is_suspense_account = true;
552            coa.add_account(acct);
553        }
554    }
555
556    fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
557        let sub_types = vec![
558            (AccountSubType::Cash, "Cash", 0.15),
559            (
560                AccountSubType::AccountsReceivable,
561                "Accounts Receivable",
562                0.20,
563            ),
564            (AccountSubType::Inventory, "Inventory", 0.15),
565            (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
566            (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
567            (
568                AccountSubType::AccumulatedDepreciation,
569                "Accumulated Depreciation",
570                0.10,
571            ),
572            (AccountSubType::OtherAssets, "Other Assets", 0.05),
573        ];
574
575        let mut account_num = 100000u32;
576        for (sub_type, name_prefix, weight) in sub_types {
577            let sub_count = ((count as f64) * weight).round() as usize;
578            for i in 0..sub_count.max(1) {
579                let account = GLAccount::new(
580                    format!("{}", account_num),
581                    format!("{} {}", name_prefix, i + 1),
582                    AccountType::Asset,
583                    sub_type,
584                );
585                coa.add_account(account);
586                account_num += 10;
587            }
588        }
589    }
590
591    fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
592        let sub_types = vec![
593            (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
594            (
595                AccountSubType::AccruedLiabilities,
596                "Accrued Liabilities",
597                0.20,
598            ),
599            (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
600            (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
601            (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
602            (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
603        ];
604
605        let mut account_num = 200000u32;
606        for (sub_type, name_prefix, weight) in sub_types {
607            let sub_count = ((count as f64) * weight).round() as usize;
608            for i in 0..sub_count.max(1) {
609                let account = GLAccount::new(
610                    format!("{}", account_num),
611                    format!("{} {}", name_prefix, i + 1),
612                    AccountType::Liability,
613                    sub_type,
614                );
615                coa.add_account(account);
616                account_num += 10;
617            }
618        }
619    }
620
621    fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
622        let sub_types = vec![
623            (AccountSubType::CommonStock, "Common Stock", 0.20),
624            (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
625            (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
626            (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
627        ];
628
629        let mut account_num = 300000u32;
630        for (sub_type, name_prefix, weight) in sub_types {
631            let sub_count = ((count as f64) * weight).round() as usize;
632            for i in 0..sub_count.max(1) {
633                let account = GLAccount::new(
634                    format!("{}", account_num),
635                    format!("{} {}", name_prefix, i + 1),
636                    AccountType::Equity,
637                    sub_type,
638                );
639                coa.add_account(account);
640                account_num += 10;
641            }
642        }
643    }
644
645    fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
646        let sub_types = vec![
647            (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
648            (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
649            (AccountSubType::InterestIncome, "Interest Income", 0.10),
650            (AccountSubType::OtherIncome, "Other Income", 0.20),
651        ];
652
653        let mut account_num = 400000u32;
654        for (sub_type, name_prefix, weight) in sub_types {
655            let sub_count = ((count as f64) * weight).round() as usize;
656            for i in 0..sub_count.max(1) {
657                let account = GLAccount::new(
658                    format!("{}", account_num),
659                    format!("{} {}", name_prefix, i + 1),
660                    AccountType::Revenue,
661                    sub_type,
662                );
663                coa.add_account(account);
664                account_num += 10;
665            }
666        }
667    }
668
669    fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
670        let sub_types = vec![
671            (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
672            (
673                AccountSubType::OperatingExpenses,
674                "Operating Expenses",
675                0.25,
676            ),
677            (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
678            (
679                AccountSubType::AdministrativeExpenses,
680                "Admin Expenses",
681                0.15,
682            ),
683            (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
684            (AccountSubType::InterestExpense, "Interest Expense", 0.05),
685            (AccountSubType::TaxExpense, "Tax Expense", 0.05),
686            (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
687        ];
688
689        let mut account_num = 500000u32;
690        for (sub_type, name_prefix, weight) in sub_types {
691            let sub_count = ((count as f64) * weight).round() as usize;
692            for i in 0..sub_count.max(1) {
693                let mut account = GLAccount::new(
694                    format!("{}", account_num),
695                    format!("{} {}", name_prefix, i + 1),
696                    AccountType::Expense,
697                    sub_type,
698                );
699                account.requires_cost_center = true;
700                coa.add_account(account);
701                account_num += 10;
702            }
703        }
704    }
705
706    fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
707        let suspense_types = vec![
708            (AccountSubType::SuspenseClearing, "Suspense Clearing"),
709            (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
710            (AccountSubType::BankClearing, "Bank Clearing"),
711            (
712                AccountSubType::IntercompanyClearing,
713                "Intercompany Clearing",
714            ),
715        ];
716
717        let mut account_num = 199000u32;
718        for (sub_type, name) in suspense_types {
719            let mut account = GLAccount::new(
720                format!("{}", account_num),
721                name.to_string(),
722                AccountType::Asset,
723                sub_type,
724            );
725            account.is_suspense_account = true;
726            coa.add_account(account);
727            account_num += 100;
728        }
729    }
730}
731
732impl Generator for ChartOfAccountsGenerator {
733    type Item = ChartOfAccounts;
734    type Config = (CoAComplexity, IndustrySector);
735
736    fn new(config: Self::Config, seed: u64) -> Self {
737        Self::new(config.0, config.1, seed)
738    }
739
740    fn generate_one(&mut self) -> Self::Item {
741        self.generate()
742    }
743
744    fn reset(&mut self) {
745        self.rng = seeded_rng(self.seed, 0);
746        self.count = 0;
747    }
748
749    fn count(&self) -> u64 {
750        self.count
751    }
752
753    fn seed(&self) -> u64 {
754        self.seed
755    }
756}
757
758#[cfg(test)]
759#[allow(clippy::unwrap_used)]
760mod tests {
761    use super::*;
762
763    #[test]
764    fn test_generate_small_coa() {
765        let mut gen =
766            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
767        let coa = gen.generate();
768
769        assert!(coa.account_count() >= 50);
770        assert!(!coa.get_suspense_accounts().is_empty());
771    }
772}