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