1use rand::RngExt;
4use tracing::debug;
5
6use datasynth_core::accounts::{
7 asset_class_accounts, cash_accounts, control_accounts, dividend_accounts, dormant_accounts,
8 equity_accounts, expense_accounts, intangible_accounts, inventory_accounts, liability_accounts,
9 manufacturing_accounts, provision_accounts, revenue_accounts, suspense_accounts, tax_accounts,
10 treasury_accounts,
11};
12use datasynth_core::models::*;
13use datasynth_core::pcg_loader;
14use datasynth_core::skr_loader;
15use datasynth_core::traits::Generator;
16use datasynth_core::utils::seeded_rng;
17use rand_chacha::ChaCha8Rng;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum CoAFramework {
22 #[default]
24 UsGaap,
25 FrenchPcg,
27 GermanSkr04,
29}
30
31pub struct ChartOfAccountsGenerator {
33 rng: ChaCha8Rng,
34 seed: u64,
35 complexity: CoAComplexity,
36 industry: IndustrySector,
37 count: u64,
38 coa_framework: CoAFramework,
40 expand_industry_subaccounts: bool,
44}
45
46pub fn overlay_coa_semantic(
60 coa: &mut ChartOfAccounts,
61 prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
62) -> usize {
63 let mut applied = 0usize;
64 for account in coa.accounts.iter_mut() {
65 if let Some(sem) = prior.accounts.get(&account.account_number) {
66 if !sem.description.is_empty() {
67 account.short_description.clone_from(&sem.description);
68 account.long_description.clone_from(&sem.description);
69 }
70 if let Some(ref cls) = sem.account_class {
71 if !cls.is_empty() {
72 account.account_class.clone_from(cls);
73 }
74 }
75 if let Some(ref cls_name) = sem.account_class_name {
76 if !cls_name.is_empty() {
77 account.account_class_name.clone_from(cls_name);
78 }
79 }
80 if let Some(ref sub) = sem.account_sub_class {
81 if !sub.is_empty() {
82 account.account_sub_class.clone_from(sub);
83 }
84 }
85 if let Some(ref sub_name) = sem.account_sub_class_name {
86 if !sub_name.is_empty() {
87 account.account_sub_class_name.clone_from(sub_name);
88 }
89 }
90 applied += 1;
91 }
92 }
93 applied
94}
95
96pub fn remap_account_numbers_to_prior<R: rand::Rng>(
118 coa: &mut ChartOfAccounts,
119 prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
120 rng: &mut R,
121) -> usize {
122 use std::collections::HashMap;
123
124 fn normalise_type(s: &str) -> &'static str {
127 match s.trim().to_lowercase().as_str() {
128 "asset" | "assets" => "asset",
129 "liability" | "liabilities" => "liability",
130 "equity" => "equity",
131 "revenue" | "revenues" | "income" => "revenue",
132 "expense" | "expenses" | "cost" => "expense",
133 _ => "",
134 }
135 }
136
137 let mut by_type: HashMap<&'static str, Vec<&String>> = HashMap::new();
139 for (account_number, semantic) in &prior.accounts {
140 let type_key = match &semantic.account_type {
141 Some(t) => normalise_type(t),
142 None => "",
143 };
144 if type_key.is_empty() {
145 continue;
146 }
147 by_type.entry(type_key).or_default().push(account_number);
148 }
149
150 let all_prior: Vec<&String> = prior.accounts.keys().collect();
153
154 const REMAP_SHARE: f64 = 0.80;
155 let mut remapped = 0usize;
156
157 for account in coa.accounts.iter_mut() {
158 if rng.random_range(0.0..1.0_f64) >= REMAP_SHARE {
159 continue;
160 }
161
162 let type_key: &'static str = match account.account_type {
164 AccountType::Asset => "asset",
165 AccountType::Liability => "liability",
166 AccountType::Equity => "equity",
167 AccountType::Revenue => "revenue",
168 AccountType::Expense => "expense",
169 AccountType::Statistical => "",
170 };
171
172 let candidates: &[&String] = if !type_key.is_empty() {
174 by_type.get(type_key).map(|v| v.as_slice()).unwrap_or(&[])
175 } else {
176 &[]
177 };
178
179 let chosen = if !candidates.is_empty() {
180 let idx = rng.random_range(0..candidates.len());
181 Some(candidates[idx].clone())
182 } else if !all_prior.is_empty() {
183 let idx = rng.random_range(0..all_prior.len());
185 Some(all_prior[idx].clone())
186 } else {
187 None
188 };
189
190 if let Some(new_number) = chosen {
191 account.account_number = new_number;
192 remapped += 1;
193 }
194 }
195
196 remapped
197}
198
199pub fn overlay_coa_taxonomy<R: rand::Rng>(
210 coa: &mut ChartOfAccounts,
211 taxonomy: &datasynth_core::distributions::text_taxonomy::TextTaxonomyPrior,
212 resolver: &mut dyn datasynth_core::distributions::text_taxonomy::PlaceholderResolver,
213 rng: &mut R,
214) {
215 use datasynth_core::distributions::text_taxonomy::PlaceholderGrammar;
216 for account in coa.accounts.iter_mut() {
217 if let Some(entry) = taxonomy.coa_pools.get(&account.account_number) {
218 let filled = PlaceholderGrammar::fill(&entry.template, resolver, rng);
219 if !filled.is_empty() {
220 account.short_description.clone_from(&filled);
221 account.long_description = filled;
222 }
223 }
224 }
225}
226
227impl ChartOfAccountsGenerator {
228 pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
230 Self {
231 rng: seeded_rng(seed, 0),
232 seed,
233 complexity,
234 industry,
235 count: 0,
236 coa_framework: CoAFramework::UsGaap,
237 expand_industry_subaccounts: false,
238 }
239 }
240
241 pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
245 if use_pcg {
246 self.coa_framework = CoAFramework::FrenchPcg;
247 }
248 self
249 }
250
251 pub fn with_expand_industry_subaccounts(mut self, expand: bool) -> Self {
258 self.expand_industry_subaccounts = expand;
259 self
260 }
261
262 pub fn with_coa_framework(mut self, framework: CoAFramework) -> Self {
264 self.coa_framework = framework;
265 self
266 }
267
268 pub fn generate(&mut self) -> ChartOfAccounts {
270 debug!(
271 complexity = ?self.complexity,
272 industry = ?self.industry,
273 seed = self.seed,
274 framework = ?self.coa_framework,
275 "Generating chart of accounts"
276 );
277
278 self.count += 1;
279 let mut coa = match self.coa_framework {
280 CoAFramework::UsGaap => self.generate_default(),
281 CoAFramework::FrenchPcg => self.generate_pcg(),
282 CoAFramework::GermanSkr04 => self.generate_skr(),
283 };
284
285 let framework_label = match self.coa_framework {
290 CoAFramework::UsGaap => "us_gaap",
291 CoAFramework::FrenchPcg => "french_pcg",
292 CoAFramework::GermanSkr04 => "german_skr04",
293 };
294 for account in coa.accounts.iter_mut() {
295 account.accounting_framework = Some(framework_label.to_string());
296 }
297 coa
298 }
299
300 pub fn apply_coa_semantic_prior(
313 coa: &mut ChartOfAccounts,
314 prior: &datasynth_core::distributions::behavioral_priors::CoaSemanticPrior,
315 ) -> usize {
316 let enriched = overlay_coa_semantic(coa, prior);
317 tracing::debug!(
318 enriched_accounts = enriched,
319 total_accounts = coa.accounts.len(),
320 "SP4.2 CoA semantic prior applied"
321 );
322 enriched
327 }
328
329 fn generate_default(&mut self) -> ChartOfAccounts {
331 let target_count = self.complexity.target_count();
332 let mut coa = ChartOfAccounts::new(
333 format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
334 format!("{:?} Chart of Accounts", self.industry),
335 "US".to_string(),
336 self.industry,
337 self.complexity,
338 );
339
340 Self::seed_canonical_accounts(&mut coa);
342
343 if self.expand_industry_subaccounts {
344 self.generate_suspense_accounts(&mut coa);
352 Self::expand_with_industry_pack(&mut coa, self.industry);
353 } else {
354 self.generate_asset_accounts(&mut coa, target_count / 5);
356 self.generate_liability_accounts(&mut coa, target_count / 6);
357 self.generate_equity_accounts(&mut coa, target_count / 10);
358 self.generate_revenue_accounts(&mut coa, target_count / 5);
359 self.generate_expense_accounts(&mut coa, target_count / 4);
360 self.generate_suspense_accounts(&mut coa);
361 }
362
363 coa
364 }
365
366 fn expand_with_industry_pack(coa: &mut ChartOfAccounts, industry: IndustrySector) {
386 let pack = match datasynth_core::industry_packs::load_pack(industry) {
387 Ok(Some(p)) => p,
388 Ok(None) => return,
389 Err(e) => {
390 tracing::warn!(
391 "industry pack for {:?} failed to load: {} — skipping expansion",
392 industry,
393 e
394 );
395 return;
396 }
397 };
398
399 for expansion in &pack.expansions {
400 let parent_snapshot = match coa.get_account(&expansion.parent_account) {
403 Some(p) => p.clone(),
404 None => continue,
405 };
406
407 if let Some(parent_mut) = coa
409 .accounts
410 .iter_mut()
411 .find(|a| a.account_number == expansion.parent_account)
412 {
413 parent_mut.is_postable = false;
414 parent_mut.is_control_account = true;
415 }
416
417 for sub in &expansion.sub_accounts {
419 let sub_number = datasynth_core::industry_packs::render_sub_account_number(
420 &expansion.parent_account,
421 &sub.suffix,
422 );
423 if coa.get_account(&sub_number).is_some() {
427 continue;
428 }
429 let sub_name = datasynth_core::industry_packs::render_sub_account_name(
430 &expansion.parent_name,
431 &sub.name,
432 );
433 let mut sub_acct = GLAccount::new(
434 sub_number,
435 sub_name,
436 parent_snapshot.account_type,
437 parent_snapshot.sub_type,
438 );
439 sub_acct.account_class = parent_snapshot.account_class.clone();
441 sub_acct.account_class_name = parent_snapshot.account_class_name.clone();
442 sub_acct.account_sub_class = parent_snapshot.account_sub_class.clone();
443 sub_acct.account_sub_class_name = parent_snapshot.account_sub_class_name.clone();
444 sub_acct.account_group = parent_snapshot.account_group.clone();
445 sub_acct.parent_account = Some(parent_snapshot.account_number.clone());
446 sub_acct.hierarchy_level = parent_snapshot.hierarchy_level.saturating_add(1);
447 sub_acct.requires_cost_center = parent_snapshot.requires_cost_center;
448 sub_acct.requires_profit_center = parent_snapshot.requires_profit_center;
449 sub_acct.accounting_framework = parent_snapshot.accounting_framework.clone();
450 sub_acct.is_postable = true;
451 sub_acct.is_control_account = false;
452 coa.add_account(sub_acct);
453 }
454 }
455 }
456
457 fn generate_pcg(&mut self) -> ChartOfAccounts {
460 match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
461 Ok(coa) => coa,
462 Err(_) => self.generate_pcg_fallback(),
463 }
464 }
465
466 fn generate_skr(&mut self) -> ChartOfAccounts {
469 match skr_loader::build_chart_of_accounts_from_skr04(self.complexity, self.industry) {
470 Ok(coa) => coa,
471 Err(_) => self.generate_skr_fallback(),
472 }
473 }
474
475 fn generate_skr_fallback(&mut self) -> ChartOfAccounts {
477 use datasynth_core::skr;
478 let target_count = self.complexity.target_count();
479 let mut coa = ChartOfAccounts::new(
480 format!("COA_SKR04_{:?}_{}", self.industry, target_count),
481 format!("Standardkontenrahmen 04 – {:?}", self.industry),
482 "DE".to_string(),
483 self.industry,
484 self.complexity,
485 );
486 coa.account_format = "####".to_string();
487
488 let key_accounts = [
490 (
491 skr::control_accounts::AR_CONTROL,
492 "Forderungen aus L+L",
493 AccountType::Asset,
494 AccountSubType::AccountsReceivable,
495 ),
496 (
497 skr::control_accounts::AP_CONTROL,
498 "Verbindlichkeiten aus L+L",
499 AccountType::Liability,
500 AccountSubType::AccountsPayable,
501 ),
502 (
503 skr::control_accounts::INVENTORY,
504 "Vorräte",
505 AccountType::Asset,
506 AccountSubType::Inventory,
507 ),
508 (
509 skr::control_accounts::FIXED_ASSETS,
510 "Sachanlagen",
511 AccountType::Asset,
512 AccountSubType::FixedAssets,
513 ),
514 (
515 skr::cash_accounts::OPERATING_CASH,
516 "Bank",
517 AccountType::Asset,
518 AccountSubType::Cash,
519 ),
520 (
521 skr::cash_accounts::PETTY_CASH,
522 "Kasse",
523 AccountType::Asset,
524 AccountSubType::Cash,
525 ),
526 (
527 skr::equity_accounts::COMMON_STOCK,
528 "Gezeichnetes Kapital",
529 AccountType::Equity,
530 AccountSubType::CommonStock,
531 ),
532 (
533 skr::equity_accounts::RETAINED_EARNINGS,
534 "Gewinnvortrag",
535 AccountType::Equity,
536 AccountSubType::RetainedEarnings,
537 ),
538 (
539 skr::revenue_accounts::PRODUCT_REVENUE,
540 "Umsatzerlöse",
541 AccountType::Revenue,
542 AccountSubType::ProductRevenue,
543 ),
544 (
545 skr::revenue_accounts::SERVICE_REVENUE,
546 "Erlöse Leistungen",
547 AccountType::Revenue,
548 AccountSubType::ServiceRevenue,
549 ),
550 (
551 skr::expense_accounts::COGS,
552 "Materialaufwand",
553 AccountType::Expense,
554 AccountSubType::CostOfGoodsSold,
555 ),
556 (
557 skr::expense_accounts::SALARIES_WAGES,
558 "Löhne und Gehälter",
559 AccountType::Expense,
560 AccountSubType::OperatingExpenses,
561 ),
562 (
563 skr::expense_accounts::DEPRECIATION,
564 "Abschreibungen",
565 AccountType::Expense,
566 AccountSubType::DepreciationExpense,
567 ),
568 (
569 skr::expense_accounts::RENT,
570 "Miete",
571 AccountType::Expense,
572 AccountSubType::OperatingExpenses,
573 ),
574 ];
575
576 for (code, name, acc_type, sub_type) in key_accounts {
577 let mut account =
578 GLAccount::new(code.to_string(), name.to_string(), acc_type, sub_type);
579 account.requires_cost_center = acc_type == AccountType::Expense;
580 coa.add_account(account);
581 }
582
583 let mut num = 4100u32;
585 while coa.account_count() < target_count && num < 9900 {
586 let code = format!("{num:04}");
587 if coa.get_account(&code).is_none() {
588 let class = (num / 1000) as u8;
589 let (acc_type, sub_type) = match class {
590 0..=1 => (AccountType::Asset, AccountSubType::OtherAssets),
591 2 => (AccountType::Equity, AccountSubType::RetainedEarnings),
592 3 => (AccountType::Liability, AccountSubType::OtherLiabilities),
593 4 => (AccountType::Revenue, AccountSubType::OtherIncome),
594 5 => (AccountType::Expense, AccountSubType::CostOfGoodsSold),
595 6 => (AccountType::Expense, AccountSubType::OperatingExpenses),
596 7 => (AccountType::Expense, AccountSubType::InterestExpense),
597 _ => (AccountType::Asset, AccountSubType::SuspenseClearing),
598 };
599 coa.add_account(GLAccount::new(
600 code,
601 format!("Konto {num}"),
602 acc_type,
603 sub_type,
604 ));
605 }
606 num += 10;
607 }
608
609 coa
610 }
611
612 fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
614 let target_count = self.complexity.target_count();
615 let mut coa = ChartOfAccounts::new(
616 format!("COA_PCG_{:?}_{}", self.industry, target_count),
617 format!("Plan Comptable Général – {:?}", self.industry),
618 "FR".to_string(),
619 self.industry,
620 self.complexity,
621 );
622 coa.account_format = "######".to_string();
623
624 self.generate_pcg_class_1(&mut coa, target_count / 10);
625 self.generate_pcg_class_2(&mut coa, target_count / 6);
626 self.generate_pcg_class_3(&mut coa, target_count / 8);
627 self.generate_pcg_class_4(&mut coa, target_count / 5);
628 self.generate_pcg_class_5(&mut coa, target_count / 12);
629 self.generate_pcg_class_6(&mut coa, target_count / 4);
630 self.generate_pcg_class_7(&mut coa, target_count / 5);
631 self.generate_pcg_class_8(&mut coa);
632
633 coa
634 }
635
636 fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
637 let items = [
638 (
639 101,
640 "Capital",
641 AccountType::Equity,
642 AccountSubType::CommonStock,
643 ),
644 (
645 129,
646 "Résultat",
647 AccountType::Equity,
648 AccountSubType::RetainedEarnings,
649 ),
650 (
651 164,
652 "Emprunts",
653 AccountType::Liability,
654 AccountSubType::LongTermDebt,
655 ),
656 (
657 151,
658 "Provisions pour risques",
659 AccountType::Liability,
660 AccountSubType::AccruedLiabilities,
661 ),
662 ];
663 for (base, name, acc_type, sub_type) in items {
664 for i in 0..count.max(1) {
665 let num = base * 1000 + (i as u32 % 100);
666 coa.add_account(GLAccount::new(
667 format!("{num:06}"),
668 format!("{} {}", name, i + 1),
669 acc_type,
670 sub_type,
671 ));
672 }
673 }
674 }
675
676 fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
677 for i in 0..count.max(1) {
678 let num = 215000 + (i as u32 % 100);
679 coa.add_account(GLAccount::new(
680 format!("{num:06}"),
681 format!("Immobilisations {}", i + 1),
682 AccountType::Asset,
683 AccountSubType::FixedAssets,
684 ));
685 }
686 for i in 0..(count / 2).max(1) {
687 let num = 281000 + (i as u32 % 100);
688 coa.add_account(GLAccount::new(
689 format!("{num:06}"),
690 format!("Amortissements {}", i + 1),
691 AccountType::Asset,
692 AccountSubType::AccumulatedDepreciation,
693 ));
694 }
695 }
696
697 fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
698 for i in 0..count.max(1) {
699 let num = 310000 + (i as u32 % 1000);
700 coa.add_account(GLAccount::new(
701 format!("{num:06}"),
702 format!("Stocks {}", i + 1),
703 AccountType::Asset,
704 AccountSubType::Inventory,
705 ));
706 }
707 }
708
709 fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
710 for i in 0..count.max(1) {
711 let num = 411000 + (i as u32 % 1000);
712 coa.add_account(GLAccount::new(
713 format!("{num:06}"),
714 format!("Clients {}", i + 1),
715 AccountType::Asset,
716 AccountSubType::AccountsReceivable,
717 ));
718 }
719 for i in 0..count.max(1) {
720 let num = 401000 + (i as u32 % 1000);
721 coa.add_account(GLAccount::new(
722 format!("{num:06}"),
723 format!("Fournisseurs {}", i + 1),
724 AccountType::Liability,
725 AccountSubType::AccountsPayable,
726 ));
727 }
728 let clearing = GLAccount::new(
729 "408000".to_string(),
730 "Fournisseurs – non encore reçus".to_string(),
731 AccountType::Liability,
732 AccountSubType::GoodsReceivedClearing,
733 );
734 coa.add_account(clearing);
735 }
736
737 fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
738 let bases = [
739 (512, "Banque"),
740 (530, "Caisse"),
741 (511, "Valeurs à l'encaissement"),
742 ];
743 for (base, name) in bases {
744 for i in 0..(count / 3).max(1) {
745 let num = base * 1000 + (i as u32 % 100);
746 coa.add_account(GLAccount::new(
747 format!("{num:06}"),
748 format!("{} {}", name, i + 1),
749 AccountType::Asset,
750 AccountSubType::Cash,
751 ));
752 }
753 }
754 }
755
756 fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
757 let bases = [
758 (603, "Achats"),
759 (641, "Rémunérations"),
760 (681, "DAP"),
761 (613, "Loyers"),
762 (661, "Charges financières"),
763 ];
764 for (base, name) in bases {
765 for i in 0..(count / 5).max(1) {
766 let num = base * 1000 + (i as u32 % 100);
767 let mut account = GLAccount::new(
768 format!("{num:06}"),
769 format!("{} {}", name, i + 1),
770 AccountType::Expense,
771 AccountSubType::OperatingExpenses,
772 );
773 account.requires_cost_center = true;
774 coa.add_account(account);
775 }
776 }
777 }
778
779 fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
780 let bases = [
781 (701, "Ventes"),
782 (706, "Prestations"),
783 (758, "Produits divers"),
784 ];
785 for (base, name) in bases {
786 for i in 0..(count / 3).max(1) {
787 let num = base * 1000 + (i as u32 % 100);
788 coa.add_account(GLAccount::new(
789 format!("{num:06}"),
790 format!("{} {}", name, i + 1),
791 AccountType::Revenue,
792 AccountSubType::ProductRevenue,
793 ));
794 }
795 }
796 }
797
798 fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
799 coa.add_account(GLAccount::new(
800 "808000".to_string(),
801 "Comptes spéciaux".to_string(),
802 AccountType::Asset,
803 AccountSubType::SuspenseClearing,
804 ));
805 }
806
807 fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
813 coa.add_account(GLAccount::new(
815 cash_accounts::OPERATING_CASH.to_string(),
816 "Operating Cash".to_string(),
817 AccountType::Asset,
818 AccountSubType::Cash,
819 ));
820 coa.add_account(GLAccount::new(
821 cash_accounts::BANK_ACCOUNT.to_string(),
822 "Bank Account".to_string(),
823 AccountType::Asset,
824 AccountSubType::Cash,
825 ));
826 coa.add_account(GLAccount::new(
827 cash_accounts::PETTY_CASH.to_string(),
828 "Petty Cash".to_string(),
829 AccountType::Asset,
830 AccountSubType::Cash,
831 ));
832 coa.add_account(GLAccount::new(
833 cash_accounts::WIRE_CLEARING.to_string(),
834 "Wire Transfer Clearing".to_string(),
835 AccountType::Asset,
836 AccountSubType::BankClearing,
837 ));
838
839 {
841 let mut acct = GLAccount::new(
842 control_accounts::AR_CONTROL.to_string(),
843 "Accounts Receivable Control".to_string(),
844 AccountType::Asset,
845 AccountSubType::AccountsReceivable,
846 );
847 acct.is_control_account = true;
848 coa.add_account(acct);
849 }
850 {
851 let mut acct = GLAccount::new(
852 control_accounts::IC_AR_CLEARING.to_string(),
853 "Intercompany AR Clearing".to_string(),
854 AccountType::Asset,
855 AccountSubType::AccountsReceivable,
856 );
857 acct.is_control_account = true;
858 coa.add_account(acct);
859 }
860 coa.add_account(GLAccount::new(
861 control_accounts::INVENTORY.to_string(),
862 "Inventory".to_string(),
863 AccountType::Asset,
864 AccountSubType::Inventory,
865 ));
866 coa.add_account(GLAccount::new(
867 control_accounts::FIXED_ASSETS.to_string(),
868 "Fixed Assets".to_string(),
869 AccountType::Asset,
870 AccountSubType::FixedAssets,
871 ));
872 coa.add_account(GLAccount::new(
873 control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
874 "Accumulated Depreciation".to_string(),
875 AccountType::Asset,
876 AccountSubType::AccumulatedDepreciation,
877 ));
878
879 coa.add_account(GLAccount::new(
881 tax_accounts::INPUT_VAT.to_string(),
882 "Input VAT".to_string(),
883 AccountType::Asset,
884 AccountSubType::OtherReceivables,
885 ));
886 coa.add_account(GLAccount::new(
887 tax_accounts::DEFERRED_TAX_ASSET.to_string(),
888 "Deferred Tax Asset".to_string(),
889 AccountType::Asset,
890 AccountSubType::OtherAssets,
891 ));
892
893 {
895 let mut acct = GLAccount::new(
896 control_accounts::AP_CONTROL.to_string(),
897 "Accounts Payable Control".to_string(),
898 AccountType::Liability,
899 AccountSubType::AccountsPayable,
900 );
901 acct.is_control_account = true;
902 coa.add_account(acct);
903 }
904 {
905 let mut acct = GLAccount::new(
906 control_accounts::IC_AP_CLEARING.to_string(),
907 "Intercompany AP Clearing".to_string(),
908 AccountType::Liability,
909 AccountSubType::AccountsPayable,
910 );
911 acct.is_control_account = true;
912 coa.add_account(acct);
913 }
914 coa.add_account(GLAccount::new(
915 tax_accounts::SALES_TAX_PAYABLE.to_string(),
916 "Sales Tax Payable".to_string(),
917 AccountType::Liability,
918 AccountSubType::TaxLiabilities,
919 ));
920 coa.add_account(GLAccount::new(
921 tax_accounts::VAT_PAYABLE.to_string(),
922 "VAT Payable".to_string(),
923 AccountType::Liability,
924 AccountSubType::TaxLiabilities,
925 ));
926 coa.add_account(GLAccount::new(
927 tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
928 "Withholding Tax Payable".to_string(),
929 AccountType::Liability,
930 AccountSubType::TaxLiabilities,
931 ));
932 coa.add_account(GLAccount::new(
933 liability_accounts::ACCRUED_EXPENSES.to_string(),
934 "Accrued Expenses".to_string(),
935 AccountType::Liability,
936 AccountSubType::AccruedLiabilities,
937 ));
938 coa.add_account(GLAccount::new(
939 liability_accounts::ACCRUED_SALARIES.to_string(),
940 "Accrued Salaries".to_string(),
941 AccountType::Liability,
942 AccountSubType::AccruedLiabilities,
943 ));
944 coa.add_account(GLAccount::new(
945 liability_accounts::ACCRUED_BENEFITS.to_string(),
946 "Accrued Benefits".to_string(),
947 AccountType::Liability,
948 AccountSubType::AccruedLiabilities,
949 ));
950 coa.add_account(GLAccount::new(
951 liability_accounts::UNEARNED_REVENUE.to_string(),
952 "Unearned Revenue".to_string(),
953 AccountType::Liability,
954 AccountSubType::DeferredRevenue,
955 ));
956 coa.add_account(GLAccount::new(
957 liability_accounts::SHORT_TERM_DEBT.to_string(),
958 "Short-Term Debt".to_string(),
959 AccountType::Liability,
960 AccountSubType::ShortTermDebt,
961 ));
962 coa.add_account(GLAccount::new(
963 tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
964 "Deferred Tax Liability".to_string(),
965 AccountType::Liability,
966 AccountSubType::TaxLiabilities,
967 ));
968 coa.add_account(GLAccount::new(
969 liability_accounts::LONG_TERM_DEBT.to_string(),
970 "Long-Term Debt".to_string(),
971 AccountType::Liability,
972 AccountSubType::LongTermDebt,
973 ));
974 coa.add_account(GLAccount::new(
975 liability_accounts::IC_PAYABLE.to_string(),
976 "Intercompany Payable".to_string(),
977 AccountType::Liability,
978 AccountSubType::OtherLiabilities,
979 ));
980 {
981 let mut acct = GLAccount::new(
982 control_accounts::GR_IR_CLEARING.to_string(),
983 "GR/IR Clearing".to_string(),
984 AccountType::Liability,
985 AccountSubType::GoodsReceivedClearing,
986 );
987 acct.is_suspense_account = true;
988 coa.add_account(acct);
989 }
990
991 coa.add_account(GLAccount::new(
993 equity_accounts::COMMON_STOCK.to_string(),
994 "Common Stock".to_string(),
995 AccountType::Equity,
996 AccountSubType::CommonStock,
997 ));
998 coa.add_account(GLAccount::new(
999 equity_accounts::APIC.to_string(),
1000 "Additional Paid-In Capital".to_string(),
1001 AccountType::Equity,
1002 AccountSubType::AdditionalPaidInCapital,
1003 ));
1004 coa.add_account(GLAccount::new(
1005 equity_accounts::RETAINED_EARNINGS.to_string(),
1006 "Retained Earnings".to_string(),
1007 AccountType::Equity,
1008 AccountSubType::RetainedEarnings,
1009 ));
1010 coa.add_account(GLAccount::new(
1011 equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
1012 "Current Year Earnings".to_string(),
1013 AccountType::Equity,
1014 AccountSubType::NetIncome,
1015 ));
1016 coa.add_account(GLAccount::new(
1017 equity_accounts::TREASURY_STOCK.to_string(),
1018 "Treasury Stock".to_string(),
1019 AccountType::Equity,
1020 AccountSubType::TreasuryStock,
1021 ));
1022 coa.add_account(GLAccount::new(
1023 equity_accounts::CTA.to_string(),
1024 "Currency Translation Adjustment".to_string(),
1025 AccountType::Equity,
1026 AccountSubType::OtherComprehensiveIncome,
1027 ));
1028
1029 coa.add_account(GLAccount::new(
1031 revenue_accounts::PRODUCT_REVENUE.to_string(),
1032 "Product Revenue".to_string(),
1033 AccountType::Revenue,
1034 AccountSubType::ProductRevenue,
1035 ));
1036 coa.add_account(GLAccount::new(
1037 revenue_accounts::SALES_DISCOUNTS.to_string(),
1038 "Sales Discounts".to_string(),
1039 AccountType::Revenue,
1040 AccountSubType::ProductRevenue,
1041 ));
1042 coa.add_account(GLAccount::new(
1043 revenue_accounts::SALES_RETURNS.to_string(),
1044 "Sales Returns and Allowances".to_string(),
1045 AccountType::Revenue,
1046 AccountSubType::ProductRevenue,
1047 ));
1048 coa.add_account(GLAccount::new(
1049 revenue_accounts::SERVICE_REVENUE.to_string(),
1050 "Service Revenue".to_string(),
1051 AccountType::Revenue,
1052 AccountSubType::ServiceRevenue,
1053 ));
1054 coa.add_account(GLAccount::new(
1055 revenue_accounts::IC_REVENUE.to_string(),
1056 "Intercompany Revenue".to_string(),
1057 AccountType::Revenue,
1058 AccountSubType::OtherIncome,
1059 ));
1060 coa.add_account(GLAccount::new(
1061 revenue_accounts::OTHER_REVENUE.to_string(),
1062 "Other Revenue".to_string(),
1063 AccountType::Revenue,
1064 AccountSubType::OtherIncome,
1065 ));
1066
1067 {
1069 let mut acct = GLAccount::new(
1070 expense_accounts::COGS.to_string(),
1071 "Cost of Goods Sold".to_string(),
1072 AccountType::Expense,
1073 AccountSubType::CostOfGoodsSold,
1074 );
1075 acct.requires_cost_center = true;
1076 coa.add_account(acct);
1077 }
1078 {
1079 let mut acct = GLAccount::new(
1080 expense_accounts::RAW_MATERIALS.to_string(),
1081 "Raw Materials".to_string(),
1082 AccountType::Expense,
1083 AccountSubType::CostOfGoodsSold,
1084 );
1085 acct.requires_cost_center = true;
1086 coa.add_account(acct);
1087 }
1088 {
1089 let mut acct = GLAccount::new(
1090 expense_accounts::DIRECT_LABOR.to_string(),
1091 "Direct Labor".to_string(),
1092 AccountType::Expense,
1093 AccountSubType::CostOfGoodsSold,
1094 );
1095 acct.requires_cost_center = true;
1096 coa.add_account(acct);
1097 }
1098 {
1099 let mut acct = GLAccount::new(
1100 expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
1101 "Manufacturing Overhead".to_string(),
1102 AccountType::Expense,
1103 AccountSubType::CostOfGoodsSold,
1104 );
1105 acct.requires_cost_center = true;
1106 coa.add_account(acct);
1107 }
1108 {
1109 let mut acct = GLAccount::new(
1110 expense_accounts::DEPRECIATION.to_string(),
1111 "Depreciation Expense".to_string(),
1112 AccountType::Expense,
1113 AccountSubType::DepreciationExpense,
1114 );
1115 acct.requires_cost_center = true;
1116 coa.add_account(acct);
1117 }
1118 {
1119 let mut acct = GLAccount::new(
1120 expense_accounts::SALARIES_WAGES.to_string(),
1121 "Salaries and Wages".to_string(),
1122 AccountType::Expense,
1123 AccountSubType::OperatingExpenses,
1124 );
1125 acct.requires_cost_center = true;
1126 coa.add_account(acct);
1127 }
1128 {
1129 let mut acct = GLAccount::new(
1130 expense_accounts::BENEFITS.to_string(),
1131 "Benefits Expense".to_string(),
1132 AccountType::Expense,
1133 AccountSubType::OperatingExpenses,
1134 );
1135 acct.requires_cost_center = true;
1136 coa.add_account(acct);
1137 }
1138 {
1139 let mut acct = GLAccount::new(
1140 expense_accounts::RENT.to_string(),
1141 "Rent Expense".to_string(),
1142 AccountType::Expense,
1143 AccountSubType::OperatingExpenses,
1144 );
1145 acct.requires_cost_center = true;
1146 coa.add_account(acct);
1147 }
1148 {
1149 let mut acct = GLAccount::new(
1150 expense_accounts::UTILITIES.to_string(),
1151 "Utilities Expense".to_string(),
1152 AccountType::Expense,
1153 AccountSubType::OperatingExpenses,
1154 );
1155 acct.requires_cost_center = true;
1156 coa.add_account(acct);
1157 }
1158 {
1159 let mut acct = GLAccount::new(
1160 expense_accounts::OFFICE_SUPPLIES.to_string(),
1161 "Office Supplies".to_string(),
1162 AccountType::Expense,
1163 AccountSubType::AdministrativeExpenses,
1164 );
1165 acct.requires_cost_center = true;
1166 coa.add_account(acct);
1167 }
1168 {
1169 let mut acct = GLAccount::new(
1170 expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
1171 "Travel and Entertainment".to_string(),
1172 AccountType::Expense,
1173 AccountSubType::SellingExpenses,
1174 );
1175 acct.requires_cost_center = true;
1176 coa.add_account(acct);
1177 }
1178 {
1179 let mut acct = GLAccount::new(
1180 expense_accounts::PROFESSIONAL_FEES.to_string(),
1181 "Professional Fees".to_string(),
1182 AccountType::Expense,
1183 AccountSubType::AdministrativeExpenses,
1184 );
1185 acct.requires_cost_center = true;
1186 coa.add_account(acct);
1187 }
1188 {
1189 let mut acct = GLAccount::new(
1190 expense_accounts::INSURANCE.to_string(),
1191 "Insurance Expense".to_string(),
1192 AccountType::Expense,
1193 AccountSubType::OperatingExpenses,
1194 );
1195 acct.requires_cost_center = true;
1196 coa.add_account(acct);
1197 }
1198 {
1199 let mut acct = GLAccount::new(
1200 expense_accounts::BAD_DEBT.to_string(),
1201 "Bad Debt Expense".to_string(),
1202 AccountType::Expense,
1203 AccountSubType::OperatingExpenses,
1204 );
1205 acct.requires_cost_center = true;
1206 coa.add_account(acct);
1207 }
1208 {
1209 let mut acct = GLAccount::new(
1210 expense_accounts::INTEREST_EXPENSE.to_string(),
1211 "Interest Expense".to_string(),
1212 AccountType::Expense,
1213 AccountSubType::InterestExpense,
1214 );
1215 acct.requires_cost_center = true;
1216 coa.add_account(acct);
1217 }
1218 {
1219 let mut acct = GLAccount::new(
1220 expense_accounts::PURCHASE_DISCOUNTS.to_string(),
1221 "Purchase Discounts".to_string(),
1222 AccountType::Expense,
1223 AccountSubType::OtherExpenses,
1224 );
1225 acct.requires_cost_center = true;
1226 coa.add_account(acct);
1227 }
1228 {
1229 let mut acct = GLAccount::new(
1230 expense_accounts::FX_GAIN_LOSS.to_string(),
1231 "FX Gain/Loss".to_string(),
1232 AccountType::Expense,
1233 AccountSubType::ForeignExchangeLoss,
1234 );
1235 acct.requires_cost_center = true;
1236 coa.add_account(acct);
1237 }
1238
1239 {
1241 let mut acct = GLAccount::new(
1242 tax_accounts::TAX_EXPENSE.to_string(),
1243 "Tax Expense".to_string(),
1244 AccountType::Expense,
1245 AccountSubType::TaxExpense,
1246 );
1247 acct.requires_cost_center = true;
1248 coa.add_account(acct);
1249 }
1250
1251 Self::seed_asset_class_accounts(coa);
1253
1254 Self::seed_manufacturing_accounts(coa);
1256
1257 Self::seed_intangible_accounts(coa);
1259
1260 Self::seed_treasury_accounts(coa);
1262
1263 Self::seed_provision_accounts(coa);
1265
1266 Self::seed_dividend_accounts(coa);
1268
1269 Self::seed_compensation_accounts(coa);
1271
1272 Self::seed_inventory_subledger_accounts(coa);
1274
1275 Self::seed_additional_tax_accounts(coa);
1277
1278 Self::seed_additional_equity_accounts(coa);
1280
1281 Self::seed_dormant_accounts(coa);
1283
1284 {
1286 let mut acct = GLAccount::new(
1287 suspense_accounts::GENERAL_SUSPENSE.to_string(),
1288 "General Suspense".to_string(),
1289 AccountType::Asset,
1290 AccountSubType::SuspenseClearing,
1291 );
1292 acct.is_suspense_account = true;
1293 coa.add_account(acct);
1294 }
1295 {
1296 let mut acct = GLAccount::new(
1297 suspense_accounts::PAYROLL_CLEARING.to_string(),
1298 "Payroll Clearing".to_string(),
1299 AccountType::Asset,
1300 AccountSubType::SuspenseClearing,
1301 );
1302 acct.is_suspense_account = true;
1303 coa.add_account(acct);
1304 }
1305 {
1306 let mut acct = GLAccount::new(
1307 suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
1308 "Bank Reconciliation Suspense".to_string(),
1309 AccountType::Asset,
1310 AccountSubType::BankClearing,
1311 );
1312 acct.is_suspense_account = true;
1313 coa.add_account(acct);
1314 }
1315 {
1316 let mut acct = GLAccount::new(
1317 suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
1318 "IC Elimination Suspense".to_string(),
1319 AccountType::Asset,
1320 AccountSubType::IntercompanyClearing,
1321 );
1322 acct.is_suspense_account = true;
1323 coa.add_account(acct);
1324 }
1325 }
1326
1327 fn seed_asset_class_accounts(coa: &mut ChartOfAccounts) {
1331 let acquisitions = [
1332 (asset_class_accounts::LAND, "Land"),
1333 (asset_class_accounts::BUILDINGS, "Buildings"),
1334 (
1335 asset_class_accounts::BUILDING_IMPROVEMENTS,
1336 "Building Improvements",
1337 ),
1338 (
1339 asset_class_accounts::MACHINERY_EQUIPMENT,
1340 "Machinery & Equipment",
1341 ),
1342 (asset_class_accounts::VEHICLES, "Vehicles"),
1343 (asset_class_accounts::OFFICE_EQUIPMENT, "Office Equipment"),
1344 (asset_class_accounts::COMPUTER_HARDWARE, "Computer Hardware"),
1345 (
1346 asset_class_accounts::SOFTWARE_INTANGIBLES,
1347 "Software / Intangibles",
1348 ),
1349 (
1350 asset_class_accounts::FURNITURE_FIXTURES,
1351 "Furniture & Fixtures",
1352 ),
1353 (
1354 asset_class_accounts::LEASEHOLD_IMPROVEMENTS,
1355 "Leasehold Improvements",
1356 ),
1357 (asset_class_accounts::OTHER_ASSETS, "Other Fixed Assets"),
1358 (asset_class_accounts::LOW_VALUE_ASSETS, "Low-Value Assets"),
1359 (
1360 asset_class_accounts::CONSTRUCTION_IN_PROGRESS,
1361 "Construction in Progress",
1362 ),
1363 ];
1364 for (number, name) in acquisitions {
1365 coa.add_account(GLAccount::new(
1366 number.to_string(),
1367 name.to_string(),
1368 AccountType::Asset,
1369 AccountSubType::FixedAssets,
1370 ));
1371 }
1372
1373 let depreciation_contras = [
1374 (asset_class_accounts::ACC_DEP_LAND, "Acc. Dep. — Land"),
1375 (
1376 asset_class_accounts::ACC_DEP_BUILDINGS,
1377 "Acc. Dep. — Buildings",
1378 ),
1379 (
1380 asset_class_accounts::ACC_DEP_MACHINERY,
1381 "Acc. Dep. — Machinery",
1382 ),
1383 (
1384 asset_class_accounts::ACC_DEP_VEHICLES,
1385 "Acc. Dep. — Vehicles",
1386 ),
1387 (
1388 asset_class_accounts::ACC_DEP_OFFICE_EQUIPMENT,
1389 "Acc. Dep. — Office Equipment",
1390 ),
1391 (
1392 asset_class_accounts::ACC_DEP_SOFTWARE,
1393 "Acc. Dep. — Software / Intangibles",
1394 ),
1395 (
1396 asset_class_accounts::ACC_DEP_FURNITURE,
1397 "Acc. Dep. — Furniture",
1398 ),
1399 (
1400 asset_class_accounts::ACC_DEP_LEASEHOLD,
1401 "Acc. Dep. — Leasehold Improvements",
1402 ),
1403 (asset_class_accounts::ACC_DEP_OTHER, "Acc. Dep. — Other"),
1404 (asset_class_accounts::ACC_DEP_CIP, "Acc. Dep. — CIP"),
1405 ];
1406 for (number, name) in depreciation_contras {
1407 coa.add_account(GLAccount::new(
1408 number.to_string(),
1409 name.to_string(),
1410 AccountType::Asset,
1411 AccountSubType::AccumulatedDepreciation,
1412 ));
1413 }
1414
1415 {
1418 let mut acct = GLAccount::new(
1419 asset_class_accounts::DEPRECIATION_EXPENSE.to_string(),
1420 "FA Depreciation Expense".to_string(),
1421 AccountType::Expense,
1422 AccountSubType::DepreciationExpense,
1423 );
1424 acct.requires_cost_center = true;
1425 coa.add_account(acct);
1426 }
1427 coa.add_account(GLAccount::new(
1428 asset_class_accounts::GAIN_ON_DISPOSAL.to_string(),
1429 "Gain on Disposal of Fixed Assets".to_string(),
1430 AccountType::Revenue,
1431 AccountSubType::GainOnSale,
1432 ));
1433 coa.add_account(GLAccount::new(
1434 asset_class_accounts::LOSS_ON_DISPOSAL.to_string(),
1435 "Loss on Disposal of Fixed Assets".to_string(),
1436 AccountType::Expense,
1437 AccountSubType::LossOnSale,
1438 ));
1439 }
1440
1441 fn seed_manufacturing_accounts(coa: &mut ChartOfAccounts) {
1443 coa.add_account(GLAccount::new(
1444 manufacturing_accounts::FINISHED_GOODS.to_string(),
1445 "Finished Goods".to_string(),
1446 AccountType::Asset,
1447 AccountSubType::Inventory,
1448 ));
1449 coa.add_account(GLAccount::new(
1450 manufacturing_accounts::WIP.to_string(),
1451 "Work in Process".to_string(),
1452 AccountType::Asset,
1453 AccountSubType::Inventory,
1454 ));
1455 coa.add_account(GLAccount::new(
1456 manufacturing_accounts::LABOR_ACCRUAL.to_string(),
1457 "Labor Accrual".to_string(),
1458 AccountType::Liability,
1459 AccountSubType::AccruedLiabilities,
1460 ));
1461 coa.add_account(GLAccount::new(
1462 manufacturing_accounts::WARRANTY_PROVISION.to_string(),
1463 "Warranty Provision".to_string(),
1464 AccountType::Liability,
1465 AccountSubType::OtherLiabilities,
1466 ));
1467
1468 let variance_accounts = [
1469 (
1470 manufacturing_accounts::SCRAP_EXPENSE,
1471 "Scrap Expense",
1472 AccountSubType::CostOfGoodsSold,
1473 ),
1474 (
1475 manufacturing_accounts::OVERHEAD_APPLIED,
1476 "Overhead Applied",
1477 AccountSubType::CostOfGoodsSold,
1478 ),
1479 (
1480 manufacturing_accounts::MATERIAL_PRICE_VARIANCE,
1481 "Material Price Variance",
1482 AccountSubType::CostOfGoodsSold,
1483 ),
1484 (
1485 manufacturing_accounts::MATERIAL_USAGE_VARIANCE,
1486 "Material Usage Variance",
1487 AccountSubType::CostOfGoodsSold,
1488 ),
1489 (
1490 manufacturing_accounts::LABOR_RATE_VARIANCE,
1491 "Labor Rate Variance",
1492 AccountSubType::CostOfGoodsSold,
1493 ),
1494 (
1495 manufacturing_accounts::LABOR_EFFICIENCY_VARIANCE,
1496 "Labor Efficiency Variance",
1497 AccountSubType::CostOfGoodsSold,
1498 ),
1499 (
1500 manufacturing_accounts::OVERHEAD_VOLUME_VARIANCE,
1501 "Overhead Volume Variance",
1502 AccountSubType::CostOfGoodsSold,
1503 ),
1504 (
1505 manufacturing_accounts::WARRANTY_EXPENSE,
1506 "Warranty Expense",
1507 AccountSubType::OperatingExpenses,
1508 ),
1509 ];
1510 for (number, name, sub) in variance_accounts {
1511 let mut acct = GLAccount::new(
1512 number.to_string(),
1513 name.to_string(),
1514 AccountType::Expense,
1515 sub,
1516 );
1517 acct.requires_cost_center = true;
1518 coa.add_account(acct);
1519 }
1520 }
1521
1522 fn seed_intangible_accounts(coa: &mut ChartOfAccounts) {
1524 let intangibles = [
1525 (
1526 intangible_accounts::GOODWILL,
1527 "Goodwill",
1528 AccountType::Asset,
1529 AccountSubType::IntangibleAssets,
1530 ),
1531 (
1532 intangible_accounts::CUSTOMER_RELATIONSHIPS,
1533 "Customer Relationships",
1534 AccountType::Asset,
1535 AccountSubType::IntangibleAssets,
1536 ),
1537 (
1538 intangible_accounts::TRADE_NAME,
1539 "Trade Name / Brand",
1540 AccountType::Asset,
1541 AccountSubType::IntangibleAssets,
1542 ),
1543 (
1544 intangible_accounts::TECHNOLOGY,
1545 "Technology / Developed Software",
1546 AccountType::Asset,
1547 AccountSubType::IntangibleAssets,
1548 ),
1549 (
1550 intangible_accounts::ACCUMULATED_AMORTIZATION,
1551 "Accumulated Amortization",
1552 AccountType::Asset,
1553 AccountSubType::AccumulatedDepreciation,
1554 ),
1555 (
1556 intangible_accounts::AMORTIZATION_EXPENSE,
1557 "Amortization Expense — Intangibles",
1558 AccountType::Expense,
1559 AccountSubType::AmortizationExpense,
1560 ),
1561 (
1562 intangible_accounts::BARGAIN_PURCHASE_GAIN,
1563 "Bargain Purchase Gain",
1564 AccountType::Revenue,
1565 AccountSubType::OtherIncome,
1566 ),
1567 ];
1568 for (number, name, ty, sub) in intangibles {
1569 coa.add_account(GLAccount::new(
1570 number.to_string(),
1571 name.to_string(),
1572 ty,
1573 sub,
1574 ));
1575 }
1576 }
1577
1578 fn seed_treasury_accounts(coa: &mut ChartOfAccounts) {
1580 let entries = [
1581 (
1582 treasury_accounts::INTEREST_PAYABLE,
1583 "Interest Payable",
1584 AccountType::Liability,
1585 AccountSubType::AccruedLiabilities,
1586 ),
1587 (
1588 treasury_accounts::DEBT_PREMIUM,
1589 "Debt Premium",
1590 AccountType::Liability,
1591 AccountSubType::LongTermDebt,
1592 ),
1593 (
1594 treasury_accounts::DEBT_DISCOUNT,
1595 "Debt Discount",
1596 AccountType::Liability,
1597 AccountSubType::LongTermDebt,
1598 ),
1599 (
1600 treasury_accounts::DERIVATIVE_ASSET,
1601 "Derivative Asset",
1602 AccountType::Asset,
1603 AccountSubType::OtherAssets,
1604 ),
1605 (
1606 treasury_accounts::DERIVATIVE_LIABILITY,
1607 "Derivative Liability",
1608 AccountType::Liability,
1609 AccountSubType::OtherLiabilities,
1610 ),
1611 (
1612 treasury_accounts::OCI_CASH_FLOW_HEDGE,
1613 "OCI — Cash Flow Hedge Reserve",
1614 AccountType::Equity,
1615 AccountSubType::OtherComprehensiveIncome,
1616 ),
1617 (
1618 treasury_accounts::HEDGE_INEFFECTIVENESS,
1619 "Hedge Ineffectiveness",
1620 AccountType::Expense,
1621 AccountSubType::OtherExpenses,
1622 ),
1623 (
1624 treasury_accounts::CASH_POOL_IC_RECEIVABLE,
1625 "IC Receivable — Cash Pool",
1626 AccountType::Asset,
1627 AccountSubType::AccountsReceivable,
1628 ),
1629 (
1630 treasury_accounts::CASH_POOL_IC_PAYABLE,
1631 "IC Payable — Cash Pool",
1632 AccountType::Liability,
1633 AccountSubType::AccountsPayable,
1634 ),
1635 ];
1636 for (number, name, ty, sub) in entries {
1637 coa.add_account(GLAccount::new(
1638 number.to_string(),
1639 name.to_string(),
1640 ty,
1641 sub,
1642 ));
1643 }
1644 }
1645
1646 fn seed_provision_accounts(coa: &mut ChartOfAccounts) {
1648 coa.add_account(GLAccount::new(
1649 provision_accounts::PROVISION_LIABILITY.to_string(),
1650 "Provision Liability".to_string(),
1651 AccountType::Liability,
1652 AccountSubType::OtherLiabilities,
1653 ));
1654 let mut prov_exp = GLAccount::new(
1655 provision_accounts::PROVISION_EXPENSE.to_string(),
1656 "Provision Expense".to_string(),
1657 AccountType::Expense,
1658 AccountSubType::OperatingExpenses,
1659 );
1660 prov_exp.requires_cost_center = true;
1661 coa.add_account(prov_exp);
1662 }
1663
1664 fn seed_dividend_accounts(coa: &mut ChartOfAccounts) {
1666 coa.add_account(GLAccount::new(
1667 dividend_accounts::DIVIDENDS_PAYABLE.to_string(),
1668 "Dividends Payable".to_string(),
1669 AccountType::Liability,
1670 AccountSubType::OtherLiabilities,
1671 ));
1672 coa.add_account(GLAccount::new(
1673 dividend_accounts::DIVIDENDS_DECLARED.to_string(),
1674 "Dividends Declared".to_string(),
1675 AccountType::Equity,
1676 AccountSubType::RetainedEarnings,
1677 ));
1678 }
1679
1680 fn seed_compensation_accounts(coa: &mut ChartOfAccounts) {
1682 coa.add_account(GLAccount::new(
1683 liability_accounts::NET_PENSION_LIABILITY.to_string(),
1684 "Net Pension Liability".to_string(),
1685 AccountType::Liability,
1686 AccountSubType::PensionLiabilities,
1687 ));
1688 coa.add_account(GLAccount::new(
1689 equity_accounts::OCI_REMEASUREMENTS.to_string(),
1690 "OCI — Pension Remeasurements".to_string(),
1691 AccountType::Equity,
1692 AccountSubType::OtherComprehensiveIncome,
1693 ));
1694 coa.add_account(GLAccount::new(
1695 equity_accounts::APIC_STOCK_COMP.to_string(),
1696 "APIC — Stock Compensation".to_string(),
1697 AccountType::Equity,
1698 AccountSubType::AdditionalPaidInCapital,
1699 ));
1700 let mut pension_exp = GLAccount::new(
1701 expense_accounts::PENSION_EXPENSE.to_string(),
1702 "Pension Expense".to_string(),
1703 AccountType::Expense,
1704 AccountSubType::OperatingExpenses,
1705 );
1706 pension_exp.requires_cost_center = true;
1707 coa.add_account(pension_exp);
1708 let mut stock_comp = GLAccount::new(
1709 expense_accounts::STOCK_COMP_EXPENSE.to_string(),
1710 "Stock-Based Compensation Expense".to_string(),
1711 AccountType::Expense,
1712 AccountSubType::OperatingExpenses,
1713 );
1714 stock_comp.requires_cost_center = true;
1715 coa.add_account(stock_comp);
1716 }
1717
1718 fn seed_inventory_subledger_accounts(coa: &mut ChartOfAccounts) {
1720 coa.add_account(GLAccount::new(
1721 inventory_accounts::WRITEUP_INCOME.to_string(),
1722 "Inventory Write-up Income".to_string(),
1723 AccountType::Revenue,
1724 AccountSubType::OtherIncome,
1725 ));
1726 let mut writedown = GLAccount::new(
1727 inventory_accounts::WRITEDOWN_EXPENSE.to_string(),
1728 "Inventory Write-down Expense".to_string(),
1729 AccountType::Expense,
1730 AccountSubType::CostOfGoodsSold,
1731 );
1732 writedown.requires_cost_center = true;
1733 coa.add_account(writedown);
1734 }
1735
1736 fn seed_additional_tax_accounts(coa: &mut ChartOfAccounts) {
1738 coa.add_account(GLAccount::new(
1739 tax_accounts::INCOME_TAX_PAYABLE.to_string(),
1740 "Income Tax Payable".to_string(),
1741 AccountType::Liability,
1742 AccountSubType::TaxLiabilities,
1743 ));
1744 coa.add_account(GLAccount::new(
1745 tax_accounts::TAX_RECEIVABLE.to_string(),
1746 "Tax Receivable".to_string(),
1747 AccountType::Asset,
1748 AccountSubType::OtherReceivables,
1749 ));
1750 }
1751
1752 fn seed_dormant_accounts(coa: &mut ChartOfAccounts) {
1764 let entries = [
1765 (
1766 dormant_accounts::LEGACY_SUSPENSE,
1767 "Legacy Suspense (migrated)",
1768 AccountType::Asset,
1769 AccountSubType::SuspenseClearing,
1770 ),
1771 (
1772 dormant_accounts::LEGACY_CLEARING,
1773 "Legacy Clearing (predecessor system)",
1774 AccountType::Liability,
1775 AccountSubType::OtherLiabilities,
1776 ),
1777 (
1778 dormant_accounts::OBSOLETE,
1779 "Obsolete Account",
1780 AccountType::Equity,
1781 AccountSubType::OtherComprehensiveIncome,
1782 ),
1783 (
1784 dormant_accounts::TEST_ACCOUNT,
1785 "Test Account (QA residue)",
1786 AccountType::Asset,
1787 AccountSubType::SuspenseClearing,
1788 ),
1789 ];
1790 for (number, name, ty, sub) in entries {
1791 let mut acct = GLAccount::new(number.to_string(), name.to_string(), ty, sub);
1792 acct.is_blocked = true;
1793 acct.is_postable = false;
1794 acct.is_suspense_account = matches!(sub, AccountSubType::SuspenseClearing);
1795 coa.add_account(acct);
1796 }
1797 }
1798
1799 fn seed_additional_equity_accounts(coa: &mut ChartOfAccounts) {
1801 coa.add_account(GLAccount::new(
1802 equity_accounts::INCOME_SUMMARY.to_string(),
1803 "Income Summary".to_string(),
1804 AccountType::Equity,
1805 AccountSubType::NetIncome,
1806 ));
1807 coa.add_account(GLAccount::new(
1808 equity_accounts::DIVIDENDS_PAID.to_string(),
1809 "Dividends Paid".to_string(),
1810 AccountType::Equity,
1811 AccountSubType::RetainedEarnings,
1812 ));
1813 }
1814
1815 fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1816 let sub_types = vec![
1817 (AccountSubType::Cash, "Cash", 0.15),
1818 (
1819 AccountSubType::AccountsReceivable,
1820 "Accounts Receivable",
1821 0.20,
1822 ),
1823 (AccountSubType::Inventory, "Inventory", 0.15),
1824 (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
1825 (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
1826 (
1827 AccountSubType::AccumulatedDepreciation,
1828 "Accumulated Depreciation",
1829 0.10,
1830 ),
1831 (AccountSubType::OtherAssets, "Other Assets", 0.05),
1832 ];
1833
1834 let mut account_num = 100000u32;
1835 for (sub_type, name_prefix, weight) in sub_types {
1836 let sub_count = ((count as f64) * weight).round() as usize;
1837 for i in 0..sub_count.max(1) {
1838 let account = GLAccount::new(
1839 format!("{account_num}"),
1840 format!("{} {}", name_prefix, i + 1),
1841 AccountType::Asset,
1842 sub_type,
1843 );
1844 coa.add_account(account);
1845 account_num += 10;
1846 }
1847 }
1848 }
1849
1850 fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1851 let sub_types = vec![
1852 (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
1853 (
1854 AccountSubType::AccruedLiabilities,
1855 "Accrued Liabilities",
1856 0.20,
1857 ),
1858 (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
1859 (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
1860 (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
1861 (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
1862 ];
1863
1864 let mut account_num = 200000u32;
1865 for (sub_type, name_prefix, weight) in sub_types {
1866 let sub_count = ((count as f64) * weight).round() as usize;
1867 for i in 0..sub_count.max(1) {
1868 let account = GLAccount::new(
1869 format!("{account_num}"),
1870 format!("{} {}", name_prefix, i + 1),
1871 AccountType::Liability,
1872 sub_type,
1873 );
1874 coa.add_account(account);
1875 account_num += 10;
1876 }
1877 }
1878 }
1879
1880 fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1881 let sub_types = vec![
1882 (AccountSubType::CommonStock, "Common Stock", 0.20),
1883 (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
1884 (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
1885 (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
1886 ];
1887
1888 let mut account_num = 300000u32;
1889 for (sub_type, name_prefix, weight) in sub_types {
1890 let sub_count = ((count as f64) * weight).round() as usize;
1891 for i in 0..sub_count.max(1) {
1892 let account = GLAccount::new(
1893 format!("{account_num}"),
1894 format!("{} {}", name_prefix, i + 1),
1895 AccountType::Equity,
1896 sub_type,
1897 );
1898 coa.add_account(account);
1899 account_num += 10;
1900 }
1901 }
1902 }
1903
1904 fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1905 let sub_types = vec![
1906 (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
1907 (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
1908 (AccountSubType::InterestIncome, "Interest Income", 0.10),
1909 (AccountSubType::OtherIncome, "Other Income", 0.20),
1910 ];
1911
1912 let mut account_num = 400000u32;
1913 for (sub_type, name_prefix, weight) in sub_types {
1914 let sub_count = ((count as f64) * weight).round() as usize;
1915 for i in 0..sub_count.max(1) {
1916 let account = GLAccount::new(
1917 format!("{account_num}"),
1918 format!("{} {}", name_prefix, i + 1),
1919 AccountType::Revenue,
1920 sub_type,
1921 );
1922 coa.add_account(account);
1923 account_num += 10;
1924 }
1925 }
1926 }
1927
1928 fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1929 let sub_types = vec![
1930 (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
1931 (
1932 AccountSubType::OperatingExpenses,
1933 "Operating Expenses",
1934 0.25,
1935 ),
1936 (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
1937 (
1938 AccountSubType::AdministrativeExpenses,
1939 "Admin Expenses",
1940 0.15,
1941 ),
1942 (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
1943 (AccountSubType::InterestExpense, "Interest Expense", 0.05),
1944 (AccountSubType::TaxExpense, "Tax Expense", 0.05),
1945 (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
1946 ];
1947
1948 let mut account_num = 500000u32;
1949 for (sub_type, name_prefix, weight) in sub_types {
1950 let sub_count = ((count as f64) * weight).round() as usize;
1951 for i in 0..sub_count.max(1) {
1952 let mut account = GLAccount::new(
1953 format!("{account_num}"),
1954 format!("{} {}", name_prefix, i + 1),
1955 AccountType::Expense,
1956 sub_type,
1957 );
1958 account.requires_cost_center = true;
1959 coa.add_account(account);
1960 account_num += 10;
1961 }
1962 }
1963 }
1964
1965 fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
1966 let suspense_types = vec![
1967 (AccountSubType::SuspenseClearing, "Suspense Clearing"),
1968 (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
1969 (AccountSubType::BankClearing, "Bank Clearing"),
1970 (
1971 AccountSubType::IntercompanyClearing,
1972 "Intercompany Clearing",
1973 ),
1974 ];
1975
1976 let mut account_num = 199000u32;
1977 for (sub_type, name) in suspense_types {
1978 let mut account = GLAccount::new(
1979 format!("{account_num}"),
1980 name.to_string(),
1981 AccountType::Asset,
1982 sub_type,
1983 );
1984 account.is_suspense_account = true;
1985 coa.add_account(account);
1986 account_num += 100;
1987 }
1988 }
1989}
1990
1991impl Generator for ChartOfAccountsGenerator {
1992 type Item = ChartOfAccounts;
1993 type Config = (CoAComplexity, IndustrySector);
1994
1995 fn new(config: Self::Config, seed: u64) -> Self {
1996 Self::new(config.0, config.1, seed)
1997 }
1998
1999 fn generate_one(&mut self) -> Self::Item {
2000 self.generate()
2001 }
2002
2003 fn reset(&mut self) {
2004 self.rng = seeded_rng(self.seed, 0);
2005 self.count = 0;
2006 self.coa_framework = CoAFramework::UsGaap;
2007 }
2008
2009 fn count(&self) -> u64 {
2010 self.count
2011 }
2012
2013 fn seed(&self) -> u64 {
2014 self.seed
2015 }
2016}
2017
2018#[cfg(test)]
2019mod tests {
2020 use super::*;
2021
2022 #[test]
2023 fn test_generate_small_coa() {
2024 let mut gen =
2025 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
2026 let coa = gen.generate();
2027
2028 assert!(coa.account_count() >= 50);
2029 assert!(!coa.get_suspense_accounts().is_empty());
2030 }
2031
2032 #[test]
2033 fn test_generate_pcg_coa() {
2034 let mut gen =
2035 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
2036 .with_french_pcg(true);
2037 let coa = gen.generate();
2038
2039 assert_eq!(coa.country, "FR");
2040 assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
2041 assert!(coa.account_count() >= 20);
2042 let first = coa.accounts.first().expect("has accounts");
2044 assert_eq!(first.account_number.len(), 6);
2045 }
2046
2047 #[test]
2051 fn test_pcg_account_structure() {
2052 let mut gen =
2053 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
2054 .with_french_pcg(true);
2055 let coa = gen.generate();
2056
2057 assert_eq!(
2058 coa.account_format, "######",
2059 "PCG uses 6-digit account format"
2060 );
2061 assert!(
2062 coa.account_count() >= 20,
2063 "PCG CoA has minimum account count"
2064 );
2065
2066 let account_numbers: Vec<_> = coa
2067 .accounts
2068 .iter()
2069 .map(|a| a.account_number.as_str())
2070 .collect();
2071 for num in &account_numbers {
2072 assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
2073 assert!(
2074 num.chars().all(|c| c.is_ascii_digit()),
2075 "PCG account is numeric: {}",
2076 num
2077 );
2078 }
2079
2080 let first_digits: std::collections::HashSet<char> = account_numbers
2082 .iter()
2083 .filter_map(|s| s.chars().next())
2084 .collect();
2085 let pcg_classes: std::collections::HashSet<_> =
2086 ['1', '2', '3', '4', '5', '6', '7', '8'].into();
2087 assert!(
2088 !first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
2089 "PCG account numbers must be in classes 1–8, got first digits: {:?}",
2090 first_digits
2091 );
2092 }
2093
2094 #[test]
2097 fn overlay_coa_taxonomy_fills_template_once_per_account() {
2098 use datasynth_core::distributions::text_taxonomy::{
2099 SyntheticExampleResolver, TemplateEntry, TextTaxonomyPrior,
2100 };
2101 use rand::SeedableRng;
2102
2103 let mut gen =
2106 ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 1);
2107 let mut coa = gen.generate();
2108
2109 assert!(
2111 coa.accounts.iter().any(|a| a.account_number == "2000"),
2112 "canonical AP_CONTROL account '2000' expected in small CoA"
2113 );
2114
2115 let mut tx = TextTaxonomyPrior::default();
2116 tx.coa_pools.insert(
2117 "2000".to_string(),
2118 TemplateEntry {
2119 template: "Kreditoren {company}".to_string(),
2120 probability: 1.0,
2121 synthetic_example: "Kreditoren Example GmbH".to_string(),
2122 },
2123 );
2124 let mut resolver = SyntheticExampleResolver;
2125 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(9);
2126 overlay_coa_taxonomy(&mut coa, &tx, &mut resolver, &mut rng);
2127
2128 let acct = coa
2129 .accounts
2130 .iter()
2131 .find(|a| a.account_number == "2000")
2132 .expect("account '2000' present after overlay");
2133 assert!(
2134 acct.short_description.starts_with("Kreditoren "),
2135 "expected 'Kreditoren …', got '{}'",
2136 acct.short_description
2137 );
2138 assert!(
2139 !acct.short_description.contains('{'),
2140 "template placeholder left unfilled: '{}'",
2141 acct.short_description
2142 );
2143 assert_eq!(
2144 acct.short_description, acct.long_description,
2145 "short and long descriptions should be identical after taxonomy overlay"
2146 );
2147
2148 let first = acct.short_description.clone();
2154 let acct_again = coa
2155 .accounts
2156 .iter()
2157 .find(|a| a.account_number == "2000")
2158 .expect("account '2000' still present");
2159 assert_eq!(acct_again.short_description, first);
2160 assert_eq!(acct_again.long_description, first);
2161 }
2162}