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