1use 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::traits::Generator;
12use datasynth_core::utils::seeded_rng;
13use rand_chacha::ChaCha8Rng;
14
15pub struct ChartOfAccountsGenerator {
17 rng: ChaCha8Rng,
18 seed: u64,
19 complexity: CoAComplexity,
20 industry: IndustrySector,
21 count: u64,
22 use_french_pcg: bool,
24}
25
26impl ChartOfAccountsGenerator {
27 pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
29 Self {
30 rng: seeded_rng(seed, 0),
31 seed,
32 complexity,
33 industry,
34 count: 0,
35 use_french_pcg: false,
36 }
37 }
38
39 pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
41 self.use_french_pcg = use_pcg;
42 self
43 }
44
45 pub fn generate(&mut self) -> ChartOfAccounts {
47 debug!(
48 complexity = ?self.complexity,
49 industry = ?self.industry,
50 seed = self.seed,
51 "Generating chart of accounts"
52 );
53
54 self.count += 1;
55 if self.use_french_pcg {
56 self.generate_pcg()
57 } else {
58 self.generate_default()
59 }
60 }
61
62 fn generate_default(&mut self) -> ChartOfAccounts {
64 let target_count = self.complexity.target_count();
65 let mut coa = ChartOfAccounts::new(
66 format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
67 format!("{:?} Chart of Accounts", self.industry),
68 "US".to_string(),
69 self.industry,
70 self.complexity,
71 );
72
73 Self::seed_canonical_accounts(&mut coa);
75
76 self.generate_asset_accounts(&mut coa, target_count / 5);
78 self.generate_liability_accounts(&mut coa, target_count / 6);
79 self.generate_equity_accounts(&mut coa, target_count / 10);
80 self.generate_revenue_accounts(&mut coa, target_count / 5);
81 self.generate_expense_accounts(&mut coa, target_count / 4);
82 self.generate_suspense_accounts(&mut coa);
83 coa
84 }
85
86 fn generate_pcg(&mut self) -> ChartOfAccounts {
89 match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
90 Ok(coa) => coa,
91 Err(_) => self.generate_pcg_fallback(),
92 }
93 }
94
95 fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
97 let target_count = self.complexity.target_count();
98 let mut coa = ChartOfAccounts::new(
99 format!("COA_PCG_{:?}_{}", self.industry, target_count),
100 format!("Plan Comptable Général – {:?}", self.industry),
101 "FR".to_string(),
102 self.industry,
103 self.complexity,
104 );
105 coa.account_format = "######".to_string();
106
107 self.generate_pcg_class_1(&mut coa, target_count / 10);
108 self.generate_pcg_class_2(&mut coa, target_count / 6);
109 self.generate_pcg_class_3(&mut coa, target_count / 8);
110 self.generate_pcg_class_4(&mut coa, target_count / 5);
111 self.generate_pcg_class_5(&mut coa, target_count / 12);
112 self.generate_pcg_class_6(&mut coa, target_count / 4);
113 self.generate_pcg_class_7(&mut coa, target_count / 5);
114 self.generate_pcg_class_8(&mut coa);
115
116 coa
117 }
118
119 fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
120 let items = [
121 (
122 101,
123 "Capital",
124 AccountType::Equity,
125 AccountSubType::CommonStock,
126 ),
127 (
128 129,
129 "Résultat",
130 AccountType::Equity,
131 AccountSubType::RetainedEarnings,
132 ),
133 (
134 164,
135 "Emprunts",
136 AccountType::Liability,
137 AccountSubType::LongTermDebt,
138 ),
139 (
140 151,
141 "Provisions pour risques",
142 AccountType::Liability,
143 AccountSubType::AccruedLiabilities,
144 ),
145 ];
146 for (base, name, acc_type, sub_type) in items {
147 for i in 0..count.max(1) {
148 let num = base * 1000 + (i as u32 % 100);
149 coa.add_account(GLAccount::new(
150 format!("{:06}", num),
151 format!("{} {}", name, i + 1),
152 acc_type,
153 sub_type,
154 ));
155 }
156 }
157 }
158
159 fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
160 for i in 0..count.max(1) {
161 let num = 215000 + (i as u32 % 100);
162 coa.add_account(GLAccount::new(
163 format!("{:06}", num),
164 format!("Immobilisations {}", i + 1),
165 AccountType::Asset,
166 AccountSubType::FixedAssets,
167 ));
168 }
169 for i in 0..(count / 2).max(1) {
170 let num = 281000 + (i as u32 % 100);
171 coa.add_account(GLAccount::new(
172 format!("{:06}", num),
173 format!("Amortissements {}", i + 1),
174 AccountType::Asset,
175 AccountSubType::AccumulatedDepreciation,
176 ));
177 }
178 }
179
180 fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
181 for i in 0..count.max(1) {
182 let num = 310000 + (i as u32 % 1000);
183 coa.add_account(GLAccount::new(
184 format!("{:06}", num),
185 format!("Stocks {}", i + 1),
186 AccountType::Asset,
187 AccountSubType::Inventory,
188 ));
189 }
190 }
191
192 fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
193 for i in 0..count.max(1) {
194 let num = 411000 + (i as u32 % 1000);
195 coa.add_account(GLAccount::new(
196 format!("{:06}", num),
197 format!("Clients {}", i + 1),
198 AccountType::Asset,
199 AccountSubType::AccountsReceivable,
200 ));
201 }
202 for i in 0..count.max(1) {
203 let num = 401000 + (i as u32 % 1000);
204 coa.add_account(GLAccount::new(
205 format!("{:06}", num),
206 format!("Fournisseurs {}", i + 1),
207 AccountType::Liability,
208 AccountSubType::AccountsPayable,
209 ));
210 }
211 let clearing = GLAccount::new(
212 "408000".to_string(),
213 "Fournisseurs – non encore reçus".to_string(),
214 AccountType::Liability,
215 AccountSubType::GoodsReceivedClearing,
216 );
217 coa.add_account(clearing);
218 }
219
220 fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
221 let bases = [
222 (512, "Banque"),
223 (530, "Caisse"),
224 (511, "Valeurs à l'encaissement"),
225 ];
226 for (base, name) in bases {
227 for i in 0..(count / 3).max(1) {
228 let num = base * 1000 + (i as u32 % 100);
229 coa.add_account(GLAccount::new(
230 format!("{:06}", num),
231 format!("{} {}", name, i + 1),
232 AccountType::Asset,
233 AccountSubType::Cash,
234 ));
235 }
236 }
237 }
238
239 fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
240 let bases = [
241 (603, "Achats"),
242 (641, "Rémunérations"),
243 (681, "DAP"),
244 (613, "Loyers"),
245 (661, "Charges financières"),
246 ];
247 for (base, name) in bases {
248 for i in 0..(count / 5).max(1) {
249 let num = base * 1000 + (i as u32 % 100);
250 let mut account = GLAccount::new(
251 format!("{:06}", num),
252 format!("{} {}", name, i + 1),
253 AccountType::Expense,
254 AccountSubType::OperatingExpenses,
255 );
256 account.requires_cost_center = true;
257 coa.add_account(account);
258 }
259 }
260 }
261
262 fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
263 let bases = [
264 (701, "Ventes"),
265 (706, "Prestations"),
266 (758, "Produits divers"),
267 ];
268 for (base, name) in bases {
269 for i in 0..(count / 3).max(1) {
270 let num = base * 1000 + (i as u32 % 100);
271 coa.add_account(GLAccount::new(
272 format!("{:06}", num),
273 format!("{} {}", name, i + 1),
274 AccountType::Revenue,
275 AccountSubType::ProductRevenue,
276 ));
277 }
278 }
279 }
280
281 fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
282 coa.add_account(GLAccount::new(
283 "808000".to_string(),
284 "Comptes spéciaux".to_string(),
285 AccountType::Asset,
286 AccountSubType::SuspenseClearing,
287 ));
288 }
289
290 fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
296 coa.add_account(GLAccount::new(
298 cash_accounts::OPERATING_CASH.to_string(),
299 "Operating Cash".to_string(),
300 AccountType::Asset,
301 AccountSubType::Cash,
302 ));
303 coa.add_account(GLAccount::new(
304 cash_accounts::BANK_ACCOUNT.to_string(),
305 "Bank Account".to_string(),
306 AccountType::Asset,
307 AccountSubType::Cash,
308 ));
309 coa.add_account(GLAccount::new(
310 cash_accounts::PETTY_CASH.to_string(),
311 "Petty Cash".to_string(),
312 AccountType::Asset,
313 AccountSubType::Cash,
314 ));
315 coa.add_account(GLAccount::new(
316 cash_accounts::WIRE_CLEARING.to_string(),
317 "Wire Transfer Clearing".to_string(),
318 AccountType::Asset,
319 AccountSubType::BankClearing,
320 ));
321
322 {
324 let mut acct = GLAccount::new(
325 control_accounts::AR_CONTROL.to_string(),
326 "Accounts Receivable Control".to_string(),
327 AccountType::Asset,
328 AccountSubType::AccountsReceivable,
329 );
330 acct.is_control_account = true;
331 coa.add_account(acct);
332 }
333 {
334 let mut acct = GLAccount::new(
335 control_accounts::IC_AR_CLEARING.to_string(),
336 "Intercompany AR Clearing".to_string(),
337 AccountType::Asset,
338 AccountSubType::AccountsReceivable,
339 );
340 acct.is_control_account = true;
341 coa.add_account(acct);
342 }
343 coa.add_account(GLAccount::new(
344 control_accounts::INVENTORY.to_string(),
345 "Inventory".to_string(),
346 AccountType::Asset,
347 AccountSubType::Inventory,
348 ));
349 coa.add_account(GLAccount::new(
350 control_accounts::FIXED_ASSETS.to_string(),
351 "Fixed Assets".to_string(),
352 AccountType::Asset,
353 AccountSubType::FixedAssets,
354 ));
355 coa.add_account(GLAccount::new(
356 control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
357 "Accumulated Depreciation".to_string(),
358 AccountType::Asset,
359 AccountSubType::AccumulatedDepreciation,
360 ));
361
362 coa.add_account(GLAccount::new(
364 tax_accounts::INPUT_VAT.to_string(),
365 "Input VAT".to_string(),
366 AccountType::Asset,
367 AccountSubType::OtherReceivables,
368 ));
369 coa.add_account(GLAccount::new(
370 tax_accounts::DEFERRED_TAX_ASSET.to_string(),
371 "Deferred Tax Asset".to_string(),
372 AccountType::Asset,
373 AccountSubType::OtherAssets,
374 ));
375
376 {
378 let mut acct = GLAccount::new(
379 control_accounts::AP_CONTROL.to_string(),
380 "Accounts Payable Control".to_string(),
381 AccountType::Liability,
382 AccountSubType::AccountsPayable,
383 );
384 acct.is_control_account = true;
385 coa.add_account(acct);
386 }
387 {
388 let mut acct = GLAccount::new(
389 control_accounts::IC_AP_CLEARING.to_string(),
390 "Intercompany AP Clearing".to_string(),
391 AccountType::Liability,
392 AccountSubType::AccountsPayable,
393 );
394 acct.is_control_account = true;
395 coa.add_account(acct);
396 }
397 coa.add_account(GLAccount::new(
398 tax_accounts::SALES_TAX_PAYABLE.to_string(),
399 "Sales Tax Payable".to_string(),
400 AccountType::Liability,
401 AccountSubType::TaxLiabilities,
402 ));
403 coa.add_account(GLAccount::new(
404 tax_accounts::VAT_PAYABLE.to_string(),
405 "VAT Payable".to_string(),
406 AccountType::Liability,
407 AccountSubType::TaxLiabilities,
408 ));
409 coa.add_account(GLAccount::new(
410 tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
411 "Withholding Tax Payable".to_string(),
412 AccountType::Liability,
413 AccountSubType::TaxLiabilities,
414 ));
415 coa.add_account(GLAccount::new(
416 liability_accounts::ACCRUED_EXPENSES.to_string(),
417 "Accrued Expenses".to_string(),
418 AccountType::Liability,
419 AccountSubType::AccruedLiabilities,
420 ));
421 coa.add_account(GLAccount::new(
422 liability_accounts::ACCRUED_SALARIES.to_string(),
423 "Accrued Salaries".to_string(),
424 AccountType::Liability,
425 AccountSubType::AccruedLiabilities,
426 ));
427 coa.add_account(GLAccount::new(
428 liability_accounts::ACCRUED_BENEFITS.to_string(),
429 "Accrued Benefits".to_string(),
430 AccountType::Liability,
431 AccountSubType::AccruedLiabilities,
432 ));
433 coa.add_account(GLAccount::new(
434 liability_accounts::UNEARNED_REVENUE.to_string(),
435 "Unearned Revenue".to_string(),
436 AccountType::Liability,
437 AccountSubType::DeferredRevenue,
438 ));
439 coa.add_account(GLAccount::new(
440 liability_accounts::SHORT_TERM_DEBT.to_string(),
441 "Short-Term Debt".to_string(),
442 AccountType::Liability,
443 AccountSubType::ShortTermDebt,
444 ));
445 coa.add_account(GLAccount::new(
446 tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
447 "Deferred Tax Liability".to_string(),
448 AccountType::Liability,
449 AccountSubType::TaxLiabilities,
450 ));
451 coa.add_account(GLAccount::new(
452 liability_accounts::LONG_TERM_DEBT.to_string(),
453 "Long-Term Debt".to_string(),
454 AccountType::Liability,
455 AccountSubType::LongTermDebt,
456 ));
457 coa.add_account(GLAccount::new(
458 liability_accounts::IC_PAYABLE.to_string(),
459 "Intercompany Payable".to_string(),
460 AccountType::Liability,
461 AccountSubType::OtherLiabilities,
462 ));
463 {
464 let mut acct = GLAccount::new(
465 control_accounts::GR_IR_CLEARING.to_string(),
466 "GR/IR Clearing".to_string(),
467 AccountType::Liability,
468 AccountSubType::GoodsReceivedClearing,
469 );
470 acct.is_suspense_account = true;
471 coa.add_account(acct);
472 }
473
474 coa.add_account(GLAccount::new(
476 equity_accounts::COMMON_STOCK.to_string(),
477 "Common Stock".to_string(),
478 AccountType::Equity,
479 AccountSubType::CommonStock,
480 ));
481 coa.add_account(GLAccount::new(
482 equity_accounts::APIC.to_string(),
483 "Additional Paid-In Capital".to_string(),
484 AccountType::Equity,
485 AccountSubType::AdditionalPaidInCapital,
486 ));
487 coa.add_account(GLAccount::new(
488 equity_accounts::RETAINED_EARNINGS.to_string(),
489 "Retained Earnings".to_string(),
490 AccountType::Equity,
491 AccountSubType::RetainedEarnings,
492 ));
493 coa.add_account(GLAccount::new(
494 equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
495 "Current Year Earnings".to_string(),
496 AccountType::Equity,
497 AccountSubType::NetIncome,
498 ));
499 coa.add_account(GLAccount::new(
500 equity_accounts::TREASURY_STOCK.to_string(),
501 "Treasury Stock".to_string(),
502 AccountType::Equity,
503 AccountSubType::TreasuryStock,
504 ));
505 coa.add_account(GLAccount::new(
506 equity_accounts::CTA.to_string(),
507 "Currency Translation Adjustment".to_string(),
508 AccountType::Equity,
509 AccountSubType::OtherComprehensiveIncome,
510 ));
511
512 coa.add_account(GLAccount::new(
514 revenue_accounts::PRODUCT_REVENUE.to_string(),
515 "Product Revenue".to_string(),
516 AccountType::Revenue,
517 AccountSubType::ProductRevenue,
518 ));
519 coa.add_account(GLAccount::new(
520 revenue_accounts::SALES_DISCOUNTS.to_string(),
521 "Sales Discounts".to_string(),
522 AccountType::Revenue,
523 AccountSubType::ProductRevenue,
524 ));
525 coa.add_account(GLAccount::new(
526 revenue_accounts::SALES_RETURNS.to_string(),
527 "Sales Returns and Allowances".to_string(),
528 AccountType::Revenue,
529 AccountSubType::ProductRevenue,
530 ));
531 coa.add_account(GLAccount::new(
532 revenue_accounts::SERVICE_REVENUE.to_string(),
533 "Service Revenue".to_string(),
534 AccountType::Revenue,
535 AccountSubType::ServiceRevenue,
536 ));
537 coa.add_account(GLAccount::new(
538 revenue_accounts::IC_REVENUE.to_string(),
539 "Intercompany Revenue".to_string(),
540 AccountType::Revenue,
541 AccountSubType::OtherIncome,
542 ));
543 coa.add_account(GLAccount::new(
544 revenue_accounts::OTHER_REVENUE.to_string(),
545 "Other Revenue".to_string(),
546 AccountType::Revenue,
547 AccountSubType::OtherIncome,
548 ));
549
550 {
552 let mut acct = GLAccount::new(
553 expense_accounts::COGS.to_string(),
554 "Cost of Goods Sold".to_string(),
555 AccountType::Expense,
556 AccountSubType::CostOfGoodsSold,
557 );
558 acct.requires_cost_center = true;
559 coa.add_account(acct);
560 }
561 {
562 let mut acct = GLAccount::new(
563 expense_accounts::RAW_MATERIALS.to_string(),
564 "Raw Materials".to_string(),
565 AccountType::Expense,
566 AccountSubType::CostOfGoodsSold,
567 );
568 acct.requires_cost_center = true;
569 coa.add_account(acct);
570 }
571 {
572 let mut acct = GLAccount::new(
573 expense_accounts::DIRECT_LABOR.to_string(),
574 "Direct Labor".to_string(),
575 AccountType::Expense,
576 AccountSubType::CostOfGoodsSold,
577 );
578 acct.requires_cost_center = true;
579 coa.add_account(acct);
580 }
581 {
582 let mut acct = GLAccount::new(
583 expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
584 "Manufacturing Overhead".to_string(),
585 AccountType::Expense,
586 AccountSubType::CostOfGoodsSold,
587 );
588 acct.requires_cost_center = true;
589 coa.add_account(acct);
590 }
591 {
592 let mut acct = GLAccount::new(
593 expense_accounts::DEPRECIATION.to_string(),
594 "Depreciation Expense".to_string(),
595 AccountType::Expense,
596 AccountSubType::DepreciationExpense,
597 );
598 acct.requires_cost_center = true;
599 coa.add_account(acct);
600 }
601 {
602 let mut acct = GLAccount::new(
603 expense_accounts::SALARIES_WAGES.to_string(),
604 "Salaries and Wages".to_string(),
605 AccountType::Expense,
606 AccountSubType::OperatingExpenses,
607 );
608 acct.requires_cost_center = true;
609 coa.add_account(acct);
610 }
611 {
612 let mut acct = GLAccount::new(
613 expense_accounts::BENEFITS.to_string(),
614 "Benefits Expense".to_string(),
615 AccountType::Expense,
616 AccountSubType::OperatingExpenses,
617 );
618 acct.requires_cost_center = true;
619 coa.add_account(acct);
620 }
621 {
622 let mut acct = GLAccount::new(
623 expense_accounts::RENT.to_string(),
624 "Rent Expense".to_string(),
625 AccountType::Expense,
626 AccountSubType::OperatingExpenses,
627 );
628 acct.requires_cost_center = true;
629 coa.add_account(acct);
630 }
631 {
632 let mut acct = GLAccount::new(
633 expense_accounts::UTILITIES.to_string(),
634 "Utilities Expense".to_string(),
635 AccountType::Expense,
636 AccountSubType::OperatingExpenses,
637 );
638 acct.requires_cost_center = true;
639 coa.add_account(acct);
640 }
641 {
642 let mut acct = GLAccount::new(
643 expense_accounts::OFFICE_SUPPLIES.to_string(),
644 "Office Supplies".to_string(),
645 AccountType::Expense,
646 AccountSubType::AdministrativeExpenses,
647 );
648 acct.requires_cost_center = true;
649 coa.add_account(acct);
650 }
651 {
652 let mut acct = GLAccount::new(
653 expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
654 "Travel and Entertainment".to_string(),
655 AccountType::Expense,
656 AccountSubType::SellingExpenses,
657 );
658 acct.requires_cost_center = true;
659 coa.add_account(acct);
660 }
661 {
662 let mut acct = GLAccount::new(
663 expense_accounts::PROFESSIONAL_FEES.to_string(),
664 "Professional Fees".to_string(),
665 AccountType::Expense,
666 AccountSubType::AdministrativeExpenses,
667 );
668 acct.requires_cost_center = true;
669 coa.add_account(acct);
670 }
671 {
672 let mut acct = GLAccount::new(
673 expense_accounts::INSURANCE.to_string(),
674 "Insurance Expense".to_string(),
675 AccountType::Expense,
676 AccountSubType::OperatingExpenses,
677 );
678 acct.requires_cost_center = true;
679 coa.add_account(acct);
680 }
681 {
682 let mut acct = GLAccount::new(
683 expense_accounts::BAD_DEBT.to_string(),
684 "Bad Debt Expense".to_string(),
685 AccountType::Expense,
686 AccountSubType::OperatingExpenses,
687 );
688 acct.requires_cost_center = true;
689 coa.add_account(acct);
690 }
691 {
692 let mut acct = GLAccount::new(
693 expense_accounts::INTEREST_EXPENSE.to_string(),
694 "Interest Expense".to_string(),
695 AccountType::Expense,
696 AccountSubType::InterestExpense,
697 );
698 acct.requires_cost_center = true;
699 coa.add_account(acct);
700 }
701 {
702 let mut acct = GLAccount::new(
703 expense_accounts::PURCHASE_DISCOUNTS.to_string(),
704 "Purchase Discounts".to_string(),
705 AccountType::Expense,
706 AccountSubType::OtherExpenses,
707 );
708 acct.requires_cost_center = true;
709 coa.add_account(acct);
710 }
711 {
712 let mut acct = GLAccount::new(
713 expense_accounts::FX_GAIN_LOSS.to_string(),
714 "FX Gain/Loss".to_string(),
715 AccountType::Expense,
716 AccountSubType::ForeignExchangeLoss,
717 );
718 acct.requires_cost_center = true;
719 coa.add_account(acct);
720 }
721
722 {
724 let mut acct = GLAccount::new(
725 tax_accounts::TAX_EXPENSE.to_string(),
726 "Tax Expense".to_string(),
727 AccountType::Expense,
728 AccountSubType::TaxExpense,
729 );
730 acct.requires_cost_center = true;
731 coa.add_account(acct);
732 }
733
734 {
736 let mut acct = GLAccount::new(
737 suspense_accounts::GENERAL_SUSPENSE.to_string(),
738 "General Suspense".to_string(),
739 AccountType::Asset,
740 AccountSubType::SuspenseClearing,
741 );
742 acct.is_suspense_account = true;
743 coa.add_account(acct);
744 }
745 {
746 let mut acct = GLAccount::new(
747 suspense_accounts::PAYROLL_CLEARING.to_string(),
748 "Payroll Clearing".to_string(),
749 AccountType::Asset,
750 AccountSubType::SuspenseClearing,
751 );
752 acct.is_suspense_account = true;
753 coa.add_account(acct);
754 }
755 {
756 let mut acct = GLAccount::new(
757 suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
758 "Bank Reconciliation Suspense".to_string(),
759 AccountType::Asset,
760 AccountSubType::BankClearing,
761 );
762 acct.is_suspense_account = true;
763 coa.add_account(acct);
764 }
765 {
766 let mut acct = GLAccount::new(
767 suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
768 "IC Elimination Suspense".to_string(),
769 AccountType::Asset,
770 AccountSubType::IntercompanyClearing,
771 );
772 acct.is_suspense_account = true;
773 coa.add_account(acct);
774 }
775 }
776
777 fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
778 let sub_types = vec![
779 (AccountSubType::Cash, "Cash", 0.15),
780 (
781 AccountSubType::AccountsReceivable,
782 "Accounts Receivable",
783 0.20,
784 ),
785 (AccountSubType::Inventory, "Inventory", 0.15),
786 (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
787 (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
788 (
789 AccountSubType::AccumulatedDepreciation,
790 "Accumulated Depreciation",
791 0.10,
792 ),
793 (AccountSubType::OtherAssets, "Other Assets", 0.05),
794 ];
795
796 let mut account_num = 100000u32;
797 for (sub_type, name_prefix, weight) in sub_types {
798 let sub_count = ((count as f64) * weight).round() as usize;
799 for i in 0..sub_count.max(1) {
800 let account = GLAccount::new(
801 format!("{}", account_num),
802 format!("{} {}", name_prefix, i + 1),
803 AccountType::Asset,
804 sub_type,
805 );
806 coa.add_account(account);
807 account_num += 10;
808 }
809 }
810 }
811
812 fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
813 let sub_types = vec![
814 (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
815 (
816 AccountSubType::AccruedLiabilities,
817 "Accrued Liabilities",
818 0.20,
819 ),
820 (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
821 (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
822 (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
823 (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
824 ];
825
826 let mut account_num = 200000u32;
827 for (sub_type, name_prefix, weight) in sub_types {
828 let sub_count = ((count as f64) * weight).round() as usize;
829 for i in 0..sub_count.max(1) {
830 let account = GLAccount::new(
831 format!("{}", account_num),
832 format!("{} {}", name_prefix, i + 1),
833 AccountType::Liability,
834 sub_type,
835 );
836 coa.add_account(account);
837 account_num += 10;
838 }
839 }
840 }
841
842 fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
843 let sub_types = vec![
844 (AccountSubType::CommonStock, "Common Stock", 0.20),
845 (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
846 (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
847 (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
848 ];
849
850 let mut account_num = 300000u32;
851 for (sub_type, name_prefix, weight) in sub_types {
852 let sub_count = ((count as f64) * weight).round() as usize;
853 for i in 0..sub_count.max(1) {
854 let account = GLAccount::new(
855 format!("{}", account_num),
856 format!("{} {}", name_prefix, i + 1),
857 AccountType::Equity,
858 sub_type,
859 );
860 coa.add_account(account);
861 account_num += 10;
862 }
863 }
864 }
865
866 fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
867 let sub_types = vec![
868 (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
869 (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
870 (AccountSubType::InterestIncome, "Interest Income", 0.10),
871 (AccountSubType::OtherIncome, "Other Income", 0.20),
872 ];
873
874 let mut account_num = 400000u32;
875 for (sub_type, name_prefix, weight) in sub_types {
876 let sub_count = ((count as f64) * weight).round() as usize;
877 for i in 0..sub_count.max(1) {
878 let account = GLAccount::new(
879 format!("{}", account_num),
880 format!("{} {}", name_prefix, i + 1),
881 AccountType::Revenue,
882 sub_type,
883 );
884 coa.add_account(account);
885 account_num += 10;
886 }
887 }
888 }
889
890 fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
891 let sub_types = vec![
892 (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
893 (
894 AccountSubType::OperatingExpenses,
895 "Operating Expenses",
896 0.25,
897 ),
898 (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
899 (
900 AccountSubType::AdministrativeExpenses,
901 "Admin Expenses",
902 0.15,
903 ),
904 (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
905 (AccountSubType::InterestExpense, "Interest Expense", 0.05),
906 (AccountSubType::TaxExpense, "Tax Expense", 0.05),
907 (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
908 ];
909
910 let mut account_num = 500000u32;
911 for (sub_type, name_prefix, weight) in sub_types {
912 let sub_count = ((count as f64) * weight).round() as usize;
913 for i in 0..sub_count.max(1) {
914 let mut account = GLAccount::new(
915 format!("{}", account_num),
916 format!("{} {}", name_prefix, i + 1),
917 AccountType::Expense,
918 sub_type,
919 );
920 account.requires_cost_center = true;
921 coa.add_account(account);
922 account_num += 10;
923 }
924 }
925 }
926
927 fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
928 let suspense_types = vec![
929 (AccountSubType::SuspenseClearing, "Suspense Clearing"),
930 (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
931 (AccountSubType::BankClearing, "Bank Clearing"),
932 (
933 AccountSubType::IntercompanyClearing,
934 "Intercompany Clearing",
935 ),
936 ];
937
938 let mut account_num = 199000u32;
939 for (sub_type, name) in suspense_types {
940 let mut account = GLAccount::new(
941 format!("{}", account_num),
942 name.to_string(),
943 AccountType::Asset,
944 sub_type,
945 );
946 account.is_suspense_account = true;
947 coa.add_account(account);
948 account_num += 100;
949 }
950 }
951}
952
953impl Generator for ChartOfAccountsGenerator {
954 type Item = ChartOfAccounts;
955 type Config = (CoAComplexity, IndustrySector);
956
957 fn new(config: Self::Config, seed: u64) -> Self {
958 Self::new(config.0, config.1, seed)
959 }
960
961 fn generate_one(&mut self) -> Self::Item {
962 self.generate()
963 }
964
965 fn reset(&mut self) {
966 self.rng = seeded_rng(self.seed, 0);
967 self.count = 0;
968 }
969
970 fn count(&self) -> u64 {
971 self.count
972 }
973
974 fn seed(&self) -> u64 {
975 self.seed
976 }
977}
978
979#[cfg(test)]
980#[allow(clippy::unwrap_used)]
981mod tests {
982 use super::*;
983
984 #[test]
985 fn test_generate_small_coa() {
986 let mut gen =
987 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
988 let coa = gen.generate();
989
990 assert!(coa.account_count() >= 50);
991 assert!(!coa.get_suspense_accounts().is_empty());
992 }
993
994 #[test]
995 fn test_generate_pcg_coa() {
996 let mut gen =
997 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
998 .with_french_pcg(true);
999 let coa = gen.generate();
1000
1001 assert_eq!(coa.country, "FR");
1002 assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
1003 assert!(coa.account_count() >= 20);
1004 let first = coa.accounts.first().expect("has accounts");
1006 assert_eq!(first.account_number.len(), 6);
1007 }
1008
1009 #[test]
1013 fn test_pcg_account_structure() {
1014 let mut gen =
1015 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1016 .with_french_pcg(true);
1017 let coa = gen.generate();
1018
1019 assert_eq!(
1020 coa.account_format, "######",
1021 "PCG uses 6-digit account format"
1022 );
1023 assert!(
1024 coa.account_count() >= 20,
1025 "PCG CoA has minimum account count"
1026 );
1027
1028 let account_numbers: Vec<_> = coa
1029 .accounts
1030 .iter()
1031 .map(|a| a.account_number.as_str())
1032 .collect();
1033 for num in &account_numbers {
1034 assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
1035 assert!(
1036 num.chars().all(|c| c.is_ascii_digit()),
1037 "PCG account is numeric: {}",
1038 num
1039 );
1040 }
1041
1042 let first_digits: std::collections::HashSet<char> = account_numbers
1044 .iter()
1045 .filter_map(|s| s.chars().next())
1046 .collect();
1047 let pcg_classes: std::collections::HashSet<_> =
1048 ['1', '2', '3', '4', '5', '6', '7', '8'].into();
1049 assert!(
1050 !first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
1051 "PCG account numbers must be in classes 1–8, got first digits: {:?}",
1052 first_digits
1053 );
1054 }
1055}