Skip to main content

datasynth_generators/
coa_generator.rs

1//! Chart of Accounts generator.
2
3use datasynth_core::models::*;
4use datasynth_core::traits::Generator;
5use rand::prelude::*;
6use rand_chacha::ChaCha8Rng;
7
8/// Generator for Chart of Accounts.
9pub struct ChartOfAccountsGenerator {
10    rng: ChaCha8Rng,
11    seed: u64,
12    complexity: CoAComplexity,
13    industry: IndustrySector,
14    count: u64,
15}
16
17impl ChartOfAccountsGenerator {
18    /// Create a new CoA generator.
19    pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
20        Self {
21            rng: ChaCha8Rng::seed_from_u64(seed),
22            seed,
23            complexity,
24            industry,
25            count: 0,
26        }
27    }
28
29    /// Generate a complete chart of accounts.
30    pub fn generate(&mut self) -> ChartOfAccounts {
31        self.count += 1;
32        let target_count = self.complexity.target_count();
33
34        let mut coa = ChartOfAccounts::new(
35            format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
36            format!("{:?} Chart of Accounts", self.industry),
37            "US".to_string(),
38            self.industry,
39            self.complexity,
40        );
41
42        // Generate accounts by type
43        self.generate_asset_accounts(&mut coa, target_count / 5);
44        self.generate_liability_accounts(&mut coa, target_count / 6);
45        self.generate_equity_accounts(&mut coa, target_count / 10);
46        self.generate_revenue_accounts(&mut coa, target_count / 5);
47        self.generate_expense_accounts(&mut coa, target_count / 4);
48        self.generate_suspense_accounts(&mut coa);
49
50        coa
51    }
52
53    fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
54        let sub_types = vec![
55            (AccountSubType::Cash, "Cash", 0.15),
56            (
57                AccountSubType::AccountsReceivable,
58                "Accounts Receivable",
59                0.20,
60            ),
61            (AccountSubType::Inventory, "Inventory", 0.15),
62            (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
63            (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
64            (
65                AccountSubType::AccumulatedDepreciation,
66                "Accumulated Depreciation",
67                0.10,
68            ),
69            (AccountSubType::OtherAssets, "Other Assets", 0.05),
70        ];
71
72        let mut account_num = 100000u32;
73        for (sub_type, name_prefix, weight) in sub_types {
74            let sub_count = ((count as f64) * weight).round() as usize;
75            for i in 0..sub_count.max(1) {
76                let account = GLAccount::new(
77                    format!("{}", account_num),
78                    format!("{} {}", name_prefix, i + 1),
79                    AccountType::Asset,
80                    sub_type,
81                );
82                coa.add_account(account);
83                account_num += 10;
84            }
85        }
86    }
87
88    fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
89        let sub_types = vec![
90            (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
91            (
92                AccountSubType::AccruedLiabilities,
93                "Accrued Liabilities",
94                0.20,
95            ),
96            (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
97            (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
98            (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
99            (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
100        ];
101
102        let mut account_num = 200000u32;
103        for (sub_type, name_prefix, weight) in sub_types {
104            let sub_count = ((count as f64) * weight).round() as usize;
105            for i in 0..sub_count.max(1) {
106                let account = GLAccount::new(
107                    format!("{}", account_num),
108                    format!("{} {}", name_prefix, i + 1),
109                    AccountType::Liability,
110                    sub_type,
111                );
112                coa.add_account(account);
113                account_num += 10;
114            }
115        }
116    }
117
118    fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
119        let sub_types = vec![
120            (AccountSubType::CommonStock, "Common Stock", 0.20),
121            (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
122            (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
123            (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
124        ];
125
126        let mut account_num = 300000u32;
127        for (sub_type, name_prefix, weight) in sub_types {
128            let sub_count = ((count as f64) * weight).round() as usize;
129            for i in 0..sub_count.max(1) {
130                let account = GLAccount::new(
131                    format!("{}", account_num),
132                    format!("{} {}", name_prefix, i + 1),
133                    AccountType::Equity,
134                    sub_type,
135                );
136                coa.add_account(account);
137                account_num += 10;
138            }
139        }
140    }
141
142    fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
143        let sub_types = vec![
144            (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
145            (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
146            (AccountSubType::InterestIncome, "Interest Income", 0.10),
147            (AccountSubType::OtherIncome, "Other Income", 0.20),
148        ];
149
150        let mut account_num = 400000u32;
151        for (sub_type, name_prefix, weight) in sub_types {
152            let sub_count = ((count as f64) * weight).round() as usize;
153            for i in 0..sub_count.max(1) {
154                let account = GLAccount::new(
155                    format!("{}", account_num),
156                    format!("{} {}", name_prefix, i + 1),
157                    AccountType::Revenue,
158                    sub_type,
159                );
160                coa.add_account(account);
161                account_num += 10;
162            }
163        }
164    }
165
166    fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
167        let sub_types = vec![
168            (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
169            (
170                AccountSubType::OperatingExpenses,
171                "Operating Expenses",
172                0.25,
173            ),
174            (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
175            (
176                AccountSubType::AdministrativeExpenses,
177                "Admin Expenses",
178                0.15,
179            ),
180            (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
181            (AccountSubType::InterestExpense, "Interest Expense", 0.05),
182            (AccountSubType::TaxExpense, "Tax Expense", 0.05),
183            (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
184        ];
185
186        let mut account_num = 500000u32;
187        for (sub_type, name_prefix, weight) in sub_types {
188            let sub_count = ((count as f64) * weight).round() as usize;
189            for i in 0..sub_count.max(1) {
190                let mut account = GLAccount::new(
191                    format!("{}", account_num),
192                    format!("{} {}", name_prefix, i + 1),
193                    AccountType::Expense,
194                    sub_type,
195                );
196                account.requires_cost_center = true;
197                coa.add_account(account);
198                account_num += 10;
199            }
200        }
201    }
202
203    fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
204        let suspense_types = vec![
205            (AccountSubType::SuspenseClearing, "Suspense Clearing"),
206            (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
207            (AccountSubType::BankClearing, "Bank Clearing"),
208            (
209                AccountSubType::IntercompanyClearing,
210                "Intercompany Clearing",
211            ),
212        ];
213
214        let mut account_num = 199000u32;
215        for (sub_type, name) in suspense_types {
216            let mut account = GLAccount::new(
217                format!("{}", account_num),
218                name.to_string(),
219                AccountType::Asset,
220                sub_type,
221            );
222            account.is_suspense_account = true;
223            coa.add_account(account);
224            account_num += 100;
225        }
226    }
227}
228
229impl Generator for ChartOfAccountsGenerator {
230    type Item = ChartOfAccounts;
231    type Config = (CoAComplexity, IndustrySector);
232
233    fn new(config: Self::Config, seed: u64) -> Self {
234        Self::new(config.0, config.1, seed)
235    }
236
237    fn generate_one(&mut self) -> Self::Item {
238        self.generate()
239    }
240
241    fn reset(&mut self) {
242        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
243        self.count = 0;
244    }
245
246    fn count(&self) -> u64 {
247        self.count
248    }
249
250    fn seed(&self) -> u64 {
251        self.seed
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_generate_small_coa() {
261        let mut gen =
262            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
263        let coa = gen.generate();
264
265        assert!(coa.account_count() >= 50);
266        assert!(!coa.get_suspense_accounts().is_empty());
267    }
268}