1use chrono::NaiveDate;
10use datasynth_config::schema::FinancialReportingConfig;
11use datasynth_core::models::{
12 CashFlowCategory, CashFlowItem, FinancialStatement, FinancialStatementLineItem, StatementBasis,
13 StatementType,
14};
15use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
16use rand::prelude::*;
17use rand_chacha::ChaCha8Rng;
18use rust_decimal::Decimal;
19use std::collections::HashMap;
20
21pub struct FinancialStatementGenerator {
23 rng: ChaCha8Rng,
24 uuid_factory: DeterministicUuidFactory,
25 config: FinancialReportingConfig,
26}
27
28pub struct TrialBalanceEntry {
30 pub account_code: String,
32 pub account_name: String,
34 pub category: String,
36 pub debit_balance: Decimal,
38 pub credit_balance: Decimal,
40}
41
42impl FinancialStatementGenerator {
43 pub fn new(seed: u64) -> Self {
45 Self {
46 rng: ChaCha8Rng::seed_from_u64(seed),
47 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::FinancialStatement),
48 config: FinancialReportingConfig::default(),
49 }
50 }
51
52 pub fn with_config(seed: u64, config: FinancialReportingConfig) -> Self {
54 Self {
55 rng: ChaCha8Rng::seed_from_u64(seed),
56 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::FinancialStatement),
57 config,
58 }
59 }
60
61 pub fn generate(
63 &mut self,
64 company_code: &str,
65 currency: &str,
66 trial_balance: &[TrialBalanceEntry],
67 period_start: NaiveDate,
68 period_end: NaiveDate,
69 fiscal_year: u16,
70 fiscal_period: u8,
71 prior_trial_balance: Option<&[TrialBalanceEntry]>,
72 preparer_id: &str,
73 ) -> Vec<FinancialStatement> {
74 let mut statements = Vec::new();
75
76 if self.config.generate_balance_sheet {
77 statements.push(self.generate_balance_sheet(
78 company_code,
79 currency,
80 trial_balance,
81 period_start,
82 period_end,
83 fiscal_year,
84 fiscal_period,
85 prior_trial_balance,
86 preparer_id,
87 ));
88 }
89
90 if self.config.generate_income_statement {
91 statements.push(self.generate_income_statement(
92 company_code,
93 currency,
94 trial_balance,
95 period_start,
96 period_end,
97 fiscal_year,
98 fiscal_period,
99 prior_trial_balance,
100 preparer_id,
101 ));
102 }
103
104 if self.config.generate_cash_flow {
105 let net_income = self.calculate_net_income(trial_balance);
106 statements.push(self.generate_cash_flow_statement(
107 company_code,
108 currency,
109 trial_balance,
110 period_start,
111 period_end,
112 fiscal_year,
113 fiscal_period,
114 net_income,
115 preparer_id,
116 ));
117 }
118
119 statements
120 }
121
122 fn generate_balance_sheet(
123 &mut self,
124 company_code: &str,
125 currency: &str,
126 tb: &[TrialBalanceEntry],
127 period_start: NaiveDate,
128 period_end: NaiveDate,
129 fiscal_year: u16,
130 fiscal_period: u8,
131 prior_tb: Option<&[TrialBalanceEntry]>,
132 preparer_id: &str,
133 ) -> FinancialStatement {
134 let mut line_items = Vec::new();
135 let mut sort_order = 0u32;
136
137 let aggregated = self.aggregate_by_category(tb);
139 let prior_aggregated = prior_tb.map(|ptb| self.aggregate_by_category(ptb));
140
141 let get_prior = |key: &str| -> Option<Decimal> {
142 prior_aggregated
143 .as_ref()
144 .and_then(|pa| pa.get(key).copied())
145 };
146
147 let cash = *aggregated.get("Cash").unwrap_or(&Decimal::ZERO);
149 let ar = *aggregated.get("Receivables").unwrap_or(&Decimal::ZERO);
150 let inventory = *aggregated.get("Inventory").unwrap_or(&Decimal::ZERO);
151 let current_assets = cash + ar + inventory;
152 let fixed_assets = *aggregated.get("FixedAssets").unwrap_or(&Decimal::ZERO);
153 let total_assets = current_assets + fixed_assets;
154
155 let items_data = [
156 (
157 "BS-CASH",
158 "Cash and Cash Equivalents",
159 "Current Assets",
160 cash,
161 get_prior("Cash"),
162 0,
163 false,
164 ),
165 (
166 "BS-AR",
167 "Accounts Receivable",
168 "Current Assets",
169 ar,
170 get_prior("Receivables"),
171 0,
172 false,
173 ),
174 (
175 "BS-INV",
176 "Inventory",
177 "Current Assets",
178 inventory,
179 get_prior("Inventory"),
180 0,
181 false,
182 ),
183 (
184 "BS-CA",
185 "Total Current Assets",
186 "Current Assets",
187 current_assets,
188 None,
189 0,
190 true,
191 ),
192 (
193 "BS-FA",
194 "Property, Plant & Equipment, net",
195 "Non-Current Assets",
196 fixed_assets,
197 get_prior("FixedAssets"),
198 0,
199 false,
200 ),
201 (
202 "BS-TA",
203 "Total Assets",
204 "Total Assets",
205 total_assets,
206 None,
207 0,
208 true,
209 ),
210 ];
211
212 for (code, label, section, amount, prior, indent, is_total) in &items_data {
213 sort_order += 1;
214 line_items.push(FinancialStatementLineItem {
215 line_code: code.to_string(),
216 label: label.to_string(),
217 section: section.to_string(),
218 sort_order,
219 amount: *amount,
220 amount_prior: *prior,
221 indent_level: *indent,
222 is_total: *is_total,
223 gl_accounts: Vec::new(),
224 });
225 }
226
227 let ap = *aggregated.get("Payables").unwrap_or(&Decimal::ZERO);
229 let accrued = *aggregated
230 .get("AccruedLiabilities")
231 .unwrap_or(&Decimal::ZERO);
232 let current_liabilities = ap + accrued;
233 let lt_debt = *aggregated.get("LongTermDebt").unwrap_or(&Decimal::ZERO);
234 let total_liabilities = current_liabilities + lt_debt;
235
236 let retained_earnings = total_assets - total_liabilities;
237 let total_equity = retained_earnings;
238 let total_le = total_liabilities + total_equity;
239
240 let le_items = [
241 (
242 "BS-AP",
243 "Accounts Payable",
244 "Current Liabilities",
245 ap,
246 get_prior("Payables"),
247 0,
248 false,
249 ),
250 (
251 "BS-ACR",
252 "Accrued Liabilities",
253 "Current Liabilities",
254 accrued,
255 get_prior("AccruedLiabilities"),
256 0,
257 false,
258 ),
259 (
260 "BS-CL",
261 "Total Current Liabilities",
262 "Current Liabilities",
263 current_liabilities,
264 None,
265 0,
266 true,
267 ),
268 (
269 "BS-LTD",
270 "Long-Term Debt",
271 "Non-Current Liabilities",
272 lt_debt,
273 get_prior("LongTermDebt"),
274 0,
275 false,
276 ),
277 (
278 "BS-TL",
279 "Total Liabilities",
280 "Total Liabilities",
281 total_liabilities,
282 None,
283 0,
284 true,
285 ),
286 (
287 "BS-RE",
288 "Retained Earnings",
289 "Equity",
290 retained_earnings,
291 None,
292 0,
293 false,
294 ),
295 (
296 "BS-TE",
297 "Total Equity",
298 "Equity",
299 total_equity,
300 None,
301 0,
302 true,
303 ),
304 (
305 "BS-TLE",
306 "Total Liabilities & Equity",
307 "Total",
308 total_le,
309 None,
310 0,
311 true,
312 ),
313 ];
314
315 for (code, label, section, amount, prior, indent, is_total) in &le_items {
316 sort_order += 1;
317 line_items.push(FinancialStatementLineItem {
318 line_code: code.to_string(),
319 label: label.to_string(),
320 section: section.to_string(),
321 sort_order,
322 amount: *amount,
323 amount_prior: *prior,
324 indent_level: *indent,
325 is_total: *is_total,
326 gl_accounts: Vec::new(),
327 });
328 }
329
330 FinancialStatement {
331 statement_id: self.uuid_factory.next().to_string(),
332 company_code: company_code.to_string(),
333 statement_type: StatementType::BalanceSheet,
334 basis: StatementBasis::UsGaap,
335 period_start,
336 period_end,
337 fiscal_year,
338 fiscal_period,
339 line_items,
340 cash_flow_items: Vec::new(),
341 currency: currency.to_string(),
342 is_consolidated: false,
343 preparer_id: preparer_id.to_string(),
344 }
345 }
346
347 fn generate_income_statement(
348 &mut self,
349 company_code: &str,
350 currency: &str,
351 tb: &[TrialBalanceEntry],
352 period_start: NaiveDate,
353 period_end: NaiveDate,
354 fiscal_year: u16,
355 fiscal_period: u8,
356 prior_tb: Option<&[TrialBalanceEntry]>,
357 preparer_id: &str,
358 ) -> FinancialStatement {
359 let aggregated = self.aggregate_by_category(tb);
360 let prior_aggregated = prior_tb.map(|ptb| self.aggregate_by_category(ptb));
361
362 let get_prior = |key: &str| -> Option<Decimal> {
363 prior_aggregated
364 .as_ref()
365 .and_then(|pa| pa.get(key).copied())
366 };
367
368 let revenue = *aggregated.get("Revenue").unwrap_or(&Decimal::ZERO);
369 let cogs = *aggregated.get("CostOfSales").unwrap_or(&Decimal::ZERO);
370 let gross_profit = revenue - cogs;
371 let operating_expenses = *aggregated
372 .get("OperatingExpenses")
373 .unwrap_or(&Decimal::ZERO);
374 let operating_income = gross_profit - operating_expenses;
375 let tax = operating_income * Decimal::from_f64_retain(0.25).unwrap_or(Decimal::ZERO);
376 let net_income = operating_income - tax;
377
378 let mut line_items = Vec::new();
379 let items_data = [
380 (
381 "IS-REV",
382 "Revenue",
383 "Revenue",
384 revenue,
385 get_prior("Revenue"),
386 false,
387 ),
388 (
389 "IS-COGS",
390 "Cost of Goods Sold",
391 "Cost of Sales",
392 cogs,
393 get_prior("CostOfSales"),
394 false,
395 ),
396 (
397 "IS-GP",
398 "Gross Profit",
399 "Gross Profit",
400 gross_profit,
401 None,
402 true,
403 ),
404 (
405 "IS-OPEX",
406 "Operating Expenses",
407 "Operating Expenses",
408 operating_expenses,
409 get_prior("OperatingExpenses"),
410 false,
411 ),
412 (
413 "IS-OI",
414 "Operating Income",
415 "Operating Income",
416 operating_income,
417 None,
418 true,
419 ),
420 ("IS-TAX", "Income Tax Expense", "Tax", tax, None, false),
421 ("IS-NI", "Net Income", "Net Income", net_income, None, true),
422 ];
423
424 for (i, (code, label, section, amount, prior, is_total)) in items_data.iter().enumerate() {
425 line_items.push(FinancialStatementLineItem {
426 line_code: code.to_string(),
427 label: label.to_string(),
428 section: section.to_string(),
429 sort_order: (i + 1) as u32,
430 amount: *amount,
431 amount_prior: *prior,
432 indent_level: 0,
433 is_total: *is_total,
434 gl_accounts: Vec::new(),
435 });
436 }
437
438 FinancialStatement {
439 statement_id: self.uuid_factory.next().to_string(),
440 company_code: company_code.to_string(),
441 statement_type: StatementType::IncomeStatement,
442 basis: StatementBasis::UsGaap,
443 period_start,
444 period_end,
445 fiscal_year,
446 fiscal_period,
447 line_items,
448 cash_flow_items: Vec::new(),
449 currency: currency.to_string(),
450 is_consolidated: false,
451 preparer_id: preparer_id.to_string(),
452 }
453 }
454
455 fn generate_cash_flow_statement(
456 &mut self,
457 company_code: &str,
458 currency: &str,
459 _tb: &[TrialBalanceEntry],
460 period_start: NaiveDate,
461 period_end: NaiveDate,
462 fiscal_year: u16,
463 fiscal_period: u8,
464 net_income: Decimal,
465 preparer_id: &str,
466 ) -> FinancialStatement {
467 let depreciation = Decimal::from(self.rng.gen_range(5000..=50000));
469 let ar_change = Decimal::from(self.rng.gen_range(-20000i64..=20000));
470 let ap_change = Decimal::from(self.rng.gen_range(-15000i64..=15000));
471 let inventory_change = Decimal::from(self.rng.gen_range(-10000i64..=10000));
472
473 let operating_cf = net_income + depreciation - ar_change + ap_change - inventory_change;
474
475 let capex = Decimal::from(self.rng.gen_range(-100000i64..=-5000));
476 let investing_cf = capex;
477
478 let debt_change = Decimal::from(self.rng.gen_range(-50000i64..=50000));
479 let financing_cf = debt_change;
480
481 let net_change = operating_cf + investing_cf + financing_cf;
482
483 let cash_flow_items = vec![
484 CashFlowItem {
485 item_code: "CF-NI".to_string(),
486 label: "Net Income".to_string(),
487 category: CashFlowCategory::Operating,
488 amount: net_income,
489 amount_prior: None,
490 sort_order: 1,
491 is_total: false,
492 },
493 CashFlowItem {
494 item_code: "CF-DEP".to_string(),
495 label: "Depreciation & Amortization".to_string(),
496 category: CashFlowCategory::Operating,
497 amount: depreciation,
498 amount_prior: None,
499 sort_order: 2,
500 is_total: false,
501 },
502 CashFlowItem {
503 item_code: "CF-AR".to_string(),
504 label: "Change in Accounts Receivable".to_string(),
505 category: CashFlowCategory::Operating,
506 amount: -ar_change,
507 amount_prior: None,
508 sort_order: 3,
509 is_total: false,
510 },
511 CashFlowItem {
512 item_code: "CF-AP".to_string(),
513 label: "Change in Accounts Payable".to_string(),
514 category: CashFlowCategory::Operating,
515 amount: ap_change,
516 amount_prior: None,
517 sort_order: 4,
518 is_total: false,
519 },
520 CashFlowItem {
521 item_code: "CF-INV".to_string(),
522 label: "Change in Inventory".to_string(),
523 category: CashFlowCategory::Operating,
524 amount: -inventory_change,
525 amount_prior: None,
526 sort_order: 5,
527 is_total: false,
528 },
529 CashFlowItem {
530 item_code: "CF-OP".to_string(),
531 label: "Net Cash from Operating Activities".to_string(),
532 category: CashFlowCategory::Operating,
533 amount: operating_cf,
534 amount_prior: None,
535 sort_order: 6,
536 is_total: true,
537 },
538 CashFlowItem {
539 item_code: "CF-CAPEX".to_string(),
540 label: "Capital Expenditures".to_string(),
541 category: CashFlowCategory::Investing,
542 amount: capex,
543 amount_prior: None,
544 sort_order: 7,
545 is_total: false,
546 },
547 CashFlowItem {
548 item_code: "CF-INV-T".to_string(),
549 label: "Net Cash from Investing Activities".to_string(),
550 category: CashFlowCategory::Investing,
551 amount: investing_cf,
552 amount_prior: None,
553 sort_order: 8,
554 is_total: true,
555 },
556 CashFlowItem {
557 item_code: "CF-DEBT".to_string(),
558 label: "Net Borrowings / (Repayments)".to_string(),
559 category: CashFlowCategory::Financing,
560 amount: debt_change,
561 amount_prior: None,
562 sort_order: 9,
563 is_total: false,
564 },
565 CashFlowItem {
566 item_code: "CF-FIN-T".to_string(),
567 label: "Net Cash from Financing Activities".to_string(),
568 category: CashFlowCategory::Financing,
569 amount: financing_cf,
570 amount_prior: None,
571 sort_order: 10,
572 is_total: true,
573 },
574 CashFlowItem {
575 item_code: "CF-NET".to_string(),
576 label: "Net Change in Cash".to_string(),
577 category: CashFlowCategory::Operating,
578 amount: net_change,
579 amount_prior: None,
580 sort_order: 11,
581 is_total: true,
582 },
583 ];
584
585 FinancialStatement {
586 statement_id: self.uuid_factory.next().to_string(),
587 company_code: company_code.to_string(),
588 statement_type: StatementType::CashFlowStatement,
589 basis: StatementBasis::UsGaap,
590 period_start,
591 period_end,
592 fiscal_year,
593 fiscal_period,
594 line_items: Vec::new(),
595 cash_flow_items,
596 currency: currency.to_string(),
597 is_consolidated: false,
598 preparer_id: preparer_id.to_string(),
599 }
600 }
601
602 fn calculate_net_income(&self, tb: &[TrialBalanceEntry]) -> Decimal {
603 let aggregated = self.aggregate_by_category(tb);
604 let revenue = *aggregated.get("Revenue").unwrap_or(&Decimal::ZERO);
605 let cogs = *aggregated.get("CostOfSales").unwrap_or(&Decimal::ZERO);
606 let opex = *aggregated
607 .get("OperatingExpenses")
608 .unwrap_or(&Decimal::ZERO);
609 let operating_income = revenue - cogs - opex;
610 let tax = operating_income * Decimal::from_f64_retain(0.25).unwrap_or(Decimal::ZERO);
611 operating_income - tax
612 }
613
614 fn aggregate_by_category(&self, tb: &[TrialBalanceEntry]) -> HashMap<String, Decimal> {
615 let mut aggregated: HashMap<String, Decimal> = HashMap::new();
616 for entry in tb {
617 let net = entry.debit_balance - entry.credit_balance;
618 *aggregated.entry(entry.category.clone()).or_default() += net;
619 }
620 aggregated
621 }
622}
623
624#[cfg(test)]
625#[allow(clippy::unwrap_used)]
626mod tests {
627 use super::*;
628
629 fn test_trial_balance() -> Vec<TrialBalanceEntry> {
630 vec![
631 TrialBalanceEntry {
632 account_code: "1000".to_string(),
633 account_name: "Cash".to_string(),
634 category: "Cash".to_string(),
635 debit_balance: Decimal::from(500_000),
636 credit_balance: Decimal::ZERO,
637 },
638 TrialBalanceEntry {
639 account_code: "1100".to_string(),
640 account_name: "Accounts Receivable".to_string(),
641 category: "Receivables".to_string(),
642 debit_balance: Decimal::from(200_000),
643 credit_balance: Decimal::ZERO,
644 },
645 TrialBalanceEntry {
646 account_code: "1300".to_string(),
647 account_name: "Inventory".to_string(),
648 category: "Inventory".to_string(),
649 debit_balance: Decimal::from(150_000),
650 credit_balance: Decimal::ZERO,
651 },
652 TrialBalanceEntry {
653 account_code: "1500".to_string(),
654 account_name: "Fixed Assets".to_string(),
655 category: "FixedAssets".to_string(),
656 debit_balance: Decimal::from(800_000),
657 credit_balance: Decimal::ZERO,
658 },
659 TrialBalanceEntry {
660 account_code: "2000".to_string(),
661 account_name: "Accounts Payable".to_string(),
662 category: "Payables".to_string(),
663 debit_balance: Decimal::ZERO,
664 credit_balance: Decimal::from(120_000),
665 },
666 TrialBalanceEntry {
667 account_code: "2100".to_string(),
668 account_name: "Accrued Liabilities".to_string(),
669 category: "AccruedLiabilities".to_string(),
670 debit_balance: Decimal::ZERO,
671 credit_balance: Decimal::from(80_000),
672 },
673 TrialBalanceEntry {
674 account_code: "4000".to_string(),
675 account_name: "Revenue".to_string(),
676 category: "Revenue".to_string(),
677 debit_balance: Decimal::ZERO,
678 credit_balance: Decimal::from(1_000_000),
679 },
680 TrialBalanceEntry {
681 account_code: "5000".to_string(),
682 account_name: "Cost of Goods Sold".to_string(),
683 category: "CostOfSales".to_string(),
684 debit_balance: Decimal::from(600_000),
685 credit_balance: Decimal::ZERO,
686 },
687 TrialBalanceEntry {
688 account_code: "6000".to_string(),
689 account_name: "Operating Expenses".to_string(),
690 category: "OperatingExpenses".to_string(),
691 debit_balance: Decimal::from(250_000),
692 credit_balance: Decimal::ZERO,
693 },
694 ]
695 }
696
697 #[test]
698 fn test_basic_generation() {
699 let mut gen = FinancialStatementGenerator::new(42);
700 let tb = test_trial_balance();
701 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
702 let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
703
704 let statements = gen.generate("C001", "USD", &tb, start, end, 2024, 1, None, "PREP-01");
705
706 assert_eq!(statements.len(), 3);
708
709 let bs = statements
710 .iter()
711 .find(|s| s.statement_type == StatementType::BalanceSheet)
712 .unwrap();
713 let is = statements
714 .iter()
715 .find(|s| s.statement_type == StatementType::IncomeStatement)
716 .unwrap();
717 let cf = statements
718 .iter()
719 .find(|s| s.statement_type == StatementType::CashFlowStatement)
720 .unwrap();
721
722 assert!(!bs.statement_id.is_empty());
724 assert_eq!(bs.company_code, "C001");
725 assert_eq!(bs.currency, "USD");
726 assert!(!bs.line_items.is_empty());
727 assert_eq!(bs.fiscal_year, 2024);
728 assert_eq!(bs.fiscal_period, 1);
729 assert_eq!(bs.preparer_id, "PREP-01");
730
731 assert!(!is.statement_id.is_empty());
733 assert!(!is.line_items.is_empty());
734
735 assert!(!cf.statement_id.is_empty());
737 assert!(!cf.cash_flow_items.is_empty());
738 }
739
740 #[test]
741 fn test_deterministic() {
742 let tb = test_trial_balance();
743 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
744 let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
745
746 let mut gen1 = FinancialStatementGenerator::new(42);
747 let mut gen2 = FinancialStatementGenerator::new(42);
748
749 let r1 = gen1.generate("C001", "USD", &tb, start, end, 2024, 1, None, "PREP-01");
750 let r2 = gen2.generate("C001", "USD", &tb, start, end, 2024, 1, None, "PREP-01");
751
752 assert_eq!(r1.len(), r2.len());
753 for (a, b) in r1.iter().zip(r2.iter()) {
754 assert_eq!(a.statement_id, b.statement_id);
755 assert_eq!(a.statement_type, b.statement_type);
756 assert_eq!(a.line_items.len(), b.line_items.len());
757 assert_eq!(a.cash_flow_items.len(), b.cash_flow_items.len());
758
759 for (li_a, li_b) in a.line_items.iter().zip(b.line_items.iter()) {
760 assert_eq!(li_a.line_code, li_b.line_code);
761 assert_eq!(li_a.amount, li_b.amount);
762 }
763 for (cf_a, cf_b) in a.cash_flow_items.iter().zip(b.cash_flow_items.iter()) {
764 assert_eq!(cf_a.item_code, cf_b.item_code);
765 assert_eq!(cf_a.amount, cf_b.amount);
766 }
767 }
768 }
769
770 #[test]
771 fn test_balance_sheet_balances() {
772 let mut gen = FinancialStatementGenerator::new(42);
773 let tb = test_trial_balance();
774 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
775 let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
776
777 let statements = gen.generate("C001", "USD", &tb, start, end, 2024, 1, None, "PREP-01");
778 let bs = statements
779 .iter()
780 .find(|s| s.statement_type == StatementType::BalanceSheet)
781 .unwrap();
782
783 let total_assets = bs
785 .line_items
786 .iter()
787 .find(|li| li.line_code == "BS-TA")
788 .unwrap();
789 let total_le = bs
790 .line_items
791 .iter()
792 .find(|li| li.line_code == "BS-TLE")
793 .unwrap();
794
795 assert_eq!(
797 total_assets.amount, total_le.amount,
798 "Balance sheet does not balance: Assets={} vs L+E={}",
799 total_assets.amount, total_le.amount
800 );
801 }
802
803 #[test]
804 fn test_income_statement_structure() {
805 let mut gen = FinancialStatementGenerator::new(42);
806 let tb = test_trial_balance();
807 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
808 let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
809
810 let statements = gen.generate("C001", "USD", &tb, start, end, 2024, 1, None, "PREP-01");
811 let is = statements
812 .iter()
813 .find(|s| s.statement_type == StatementType::IncomeStatement)
814 .unwrap();
815
816 let codes: Vec<&str> = is
818 .line_items
819 .iter()
820 .map(|li| li.line_code.as_str())
821 .collect();
822 assert!(codes.contains(&"IS-REV"));
823 assert!(codes.contains(&"IS-COGS"));
824 assert!(codes.contains(&"IS-GP"));
825 assert!(codes.contains(&"IS-OPEX"));
826 assert!(codes.contains(&"IS-OI"));
827 assert!(codes.contains(&"IS-TAX"));
828 assert!(codes.contains(&"IS-NI"));
829
830 let revenue = is
832 .line_items
833 .iter()
834 .find(|li| li.line_code == "IS-REV")
835 .unwrap();
836 assert_eq!(revenue.amount, Decimal::from(-1_000_000));
838 }
839}