1use datasynth_core::models::*;
4use datasynth_core::traits::Generator;
5use rand::prelude::*;
6use rand_chacha::ChaCha8Rng;
7
8pub struct ChartOfAccountsGenerator {
10 rng: ChaCha8Rng,
11 seed: u64,
12 complexity: CoAComplexity,
13 industry: IndustrySector,
14 count: u64,
15}
16
17impl ChartOfAccountsGenerator {
18 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 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 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}