1use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::account_balance::AccountType;
13use super::trial_balance::AccountCategory;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OpeningBalanceSpec {
18 pub company_code: String,
20 pub as_of_date: NaiveDate,
22 pub fiscal_year: i32,
24 pub currency: String,
26 pub total_assets: Decimal,
28 pub industry: IndustryType,
30 pub asset_composition: AssetComposition,
32 pub capital_structure: CapitalStructure,
34 pub target_ratios: TargetRatios,
36 pub account_overrides: HashMap<String, AccountSpec>,
38}
39
40impl OpeningBalanceSpec {
41 pub fn new(
43 company_code: String,
44 as_of_date: NaiveDate,
45 fiscal_year: i32,
46 currency: String,
47 total_assets: Decimal,
48 industry: IndustryType,
49 ) -> Self {
50 Self {
51 company_code,
52 as_of_date,
53 fiscal_year,
54 currency,
55 total_assets,
56 industry,
57 asset_composition: AssetComposition::for_industry(industry),
58 capital_structure: CapitalStructure::default(),
59 target_ratios: TargetRatios::for_industry(industry),
60 account_overrides: HashMap::new(),
61 }
62 }
63
64 pub fn for_industry(total_assets: Decimal, industry: IndustryType) -> Self {
67 Self {
68 company_code: String::new(),
69 as_of_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
70 fiscal_year: 2024,
71 currency: "USD".to_string(),
72 total_assets,
73 industry,
74 asset_composition: AssetComposition::for_industry(industry),
75 capital_structure: CapitalStructure::for_industry(industry),
76 target_ratios: TargetRatios::for_industry(industry),
77 account_overrides: HashMap::new(),
78 }
79 }
80
81 pub fn validate(&self) -> Result<(), Vec<String>> {
83 let mut errors = Vec::new();
84
85 let asset_total = self.asset_composition.total_percentage();
87 if (asset_total - dec!(100)).abs() > dec!(0.01) {
88 errors.push(format!(
89 "Asset composition should sum to 100%, got {}%",
90 asset_total
91 ));
92 }
93
94 let capital_total =
96 self.capital_structure.debt_percent + self.capital_structure.equity_percent;
97 if (capital_total - dec!(100)).abs() > dec!(0.01) {
98 errors.push(format!(
99 "Capital structure should sum to 100%, got {}%",
100 capital_total
101 ));
102 }
103
104 if self.target_ratios.current_ratio < dec!(0.5) {
106 errors.push("Current ratio below 0.5 indicates severe liquidity problems".to_string());
107 }
108
109 if errors.is_empty() {
110 Ok(())
111 } else {
112 Err(errors)
113 }
114 }
115
116 pub fn calculate_total_liabilities(&self) -> Decimal {
118 self.total_assets * self.capital_structure.debt_percent / dec!(100)
119 }
120
121 pub fn calculate_total_equity(&self) -> Decimal {
123 self.total_assets * self.capital_structure.equity_percent / dec!(100)
124 }
125
126 pub fn calculate_current_assets(&self) -> Decimal {
128 self.total_assets * self.asset_composition.current_assets_percent / dec!(100)
129 }
130
131 pub fn calculate_non_current_assets(&self) -> Decimal {
133 self.total_assets * (dec!(100) - self.asset_composition.current_assets_percent) / dec!(100)
134 }
135
136 pub fn calculate_current_liabilities(&self) -> Decimal {
138 let current_assets = self.calculate_current_assets();
139 if self.target_ratios.current_ratio > Decimal::ZERO {
140 current_assets / self.target_ratios.current_ratio
141 } else {
142 Decimal::ZERO
143 }
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
149#[serde(rename_all = "snake_case")]
150pub enum IndustryType {
151 #[default]
153 Manufacturing,
154 Retail,
156 Services,
158 Technology,
160 Financial,
162 Healthcare,
164 Utilities,
166 RealEstate,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AssetComposition {
173 pub current_assets_percent: Decimal,
175 pub cash_percent: Decimal,
177 pub ar_percent: Decimal,
179 pub inventory_percent: Decimal,
181 pub prepaid_percent: Decimal,
183 pub other_current_percent: Decimal,
185 pub ppe_percent: Decimal,
187 pub intangibles_percent: Decimal,
189 pub investments_percent: Decimal,
191 pub other_noncurrent_percent: Decimal,
193}
194
195impl AssetComposition {
196 pub fn for_industry(industry: IndustryType) -> Self {
198 match industry {
199 IndustryType::Manufacturing => Self {
200 current_assets_percent: dec!(40),
201 cash_percent: dec!(15),
202 ar_percent: dec!(30),
203 inventory_percent: dec!(45),
204 prepaid_percent: dec!(5),
205 other_current_percent: dec!(5),
206 ppe_percent: dec!(70),
207 intangibles_percent: dec!(10),
208 investments_percent: dec!(10),
209 other_noncurrent_percent: dec!(10),
210 },
211 IndustryType::Retail => Self {
212 current_assets_percent: dec!(55),
213 cash_percent: dec!(10),
214 ar_percent: dec!(15),
215 inventory_percent: dec!(65),
216 prepaid_percent: dec!(5),
217 other_current_percent: dec!(5),
218 ppe_percent: dec!(60),
219 intangibles_percent: dec!(20),
220 investments_percent: dec!(10),
221 other_noncurrent_percent: dec!(10),
222 },
223 IndustryType::Services => Self {
224 current_assets_percent: dec!(50),
225 cash_percent: dec!(25),
226 ar_percent: dec!(50),
227 inventory_percent: dec!(5),
228 prepaid_percent: dec!(10),
229 other_current_percent: dec!(10),
230 ppe_percent: dec!(40),
231 intangibles_percent: dec!(30),
232 investments_percent: dec!(15),
233 other_noncurrent_percent: dec!(15),
234 },
235 IndustryType::Technology => Self {
236 current_assets_percent: dec!(60),
237 cash_percent: dec!(40),
238 ar_percent: dec!(35),
239 inventory_percent: dec!(5),
240 prepaid_percent: dec!(10),
241 other_current_percent: dec!(10),
242 ppe_percent: dec!(25),
243 intangibles_percent: dec!(50),
244 investments_percent: dec!(15),
245 other_noncurrent_percent: dec!(10),
246 },
247 IndustryType::Financial => Self {
248 current_assets_percent: dec!(70),
249 cash_percent: dec!(30),
250 ar_percent: dec!(40),
251 inventory_percent: dec!(0),
252 prepaid_percent: dec!(5),
253 other_current_percent: dec!(25),
254 ppe_percent: dec!(20),
255 intangibles_percent: dec!(30),
256 investments_percent: dec!(40),
257 other_noncurrent_percent: dec!(10),
258 },
259 IndustryType::Healthcare => Self {
260 current_assets_percent: dec!(35),
261 cash_percent: dec!(20),
262 ar_percent: dec!(50),
263 inventory_percent: dec!(15),
264 prepaid_percent: dec!(10),
265 other_current_percent: dec!(5),
266 ppe_percent: dec!(60),
267 intangibles_percent: dec!(20),
268 investments_percent: dec!(10),
269 other_noncurrent_percent: dec!(10),
270 },
271 IndustryType::Utilities => Self {
272 current_assets_percent: dec!(15),
273 cash_percent: dec!(20),
274 ar_percent: dec!(50),
275 inventory_percent: dec!(15),
276 prepaid_percent: dec!(10),
277 other_current_percent: dec!(5),
278 ppe_percent: dec!(85),
279 intangibles_percent: dec!(5),
280 investments_percent: dec!(5),
281 other_noncurrent_percent: dec!(5),
282 },
283 IndustryType::RealEstate => Self {
284 current_assets_percent: dec!(10),
285 cash_percent: dec!(30),
286 ar_percent: dec!(40),
287 inventory_percent: dec!(10),
288 prepaid_percent: dec!(10),
289 other_current_percent: dec!(10),
290 ppe_percent: dec!(90),
291 intangibles_percent: dec!(3),
292 investments_percent: dec!(5),
293 other_noncurrent_percent: dec!(2),
294 },
295 }
296 }
297
298 pub fn total_percentage(&self) -> Decimal {
300 let current = self.cash_percent
302 + self.ar_percent
303 + self.inventory_percent
304 + self.prepaid_percent
305 + self.other_current_percent;
306
307 let noncurrent = self.ppe_percent
309 + self.intangibles_percent
310 + self.investments_percent
311 + self.other_noncurrent_percent;
312
313 if (current - dec!(100)).abs() > dec!(1) || (noncurrent - dec!(100)).abs() > dec!(1) {
315 current
317 } else {
318 dec!(100)
319 }
320 }
321}
322
323impl Default for AssetComposition {
324 fn default() -> Self {
325 Self::for_industry(IndustryType::Manufacturing)
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct CapitalStructure {
332 pub debt_percent: Decimal,
334 pub equity_percent: Decimal,
336 pub current_liabilities_percent: Decimal,
338 pub long_term_debt_percent: Decimal,
340 pub other_liabilities_percent: Decimal,
342 pub common_stock_percent: Decimal,
344 pub apic_percent: Decimal,
346 pub retained_earnings_percent: Decimal,
348 pub other_equity_percent: Decimal,
350}
351
352impl Default for CapitalStructure {
353 fn default() -> Self {
354 Self {
355 debt_percent: dec!(40),
356 equity_percent: dec!(60),
357 current_liabilities_percent: dec!(50),
358 long_term_debt_percent: dec!(40),
359 other_liabilities_percent: dec!(10),
360 common_stock_percent: dec!(15),
361 apic_percent: dec!(25),
362 retained_earnings_percent: dec!(55),
363 other_equity_percent: dec!(5),
364 }
365 }
366}
367
368impl CapitalStructure {
369 pub fn for_industry(industry: IndustryType) -> Self {
371 match industry {
372 IndustryType::Manufacturing => Self {
373 debt_percent: dec!(40),
374 equity_percent: dec!(60),
375 current_liabilities_percent: dec!(50),
376 long_term_debt_percent: dec!(40),
377 other_liabilities_percent: dec!(10),
378 common_stock_percent: dec!(15),
379 apic_percent: dec!(25),
380 retained_earnings_percent: dec!(55),
381 other_equity_percent: dec!(5),
382 },
383 IndustryType::Retail => Self {
384 debt_percent: dec!(45),
385 equity_percent: dec!(55),
386 current_liabilities_percent: dec!(60),
387 long_term_debt_percent: dec!(30),
388 other_liabilities_percent: dec!(10),
389 common_stock_percent: dec!(20),
390 apic_percent: dec!(20),
391 retained_earnings_percent: dec!(55),
392 other_equity_percent: dec!(5),
393 },
394 IndustryType::Services => Self {
395 debt_percent: dec!(30),
396 equity_percent: dec!(70),
397 current_liabilities_percent: dec!(55),
398 long_term_debt_percent: dec!(35),
399 other_liabilities_percent: dec!(10),
400 common_stock_percent: dec!(15),
401 apic_percent: dec!(30),
402 retained_earnings_percent: dec!(50),
403 other_equity_percent: dec!(5),
404 },
405 IndustryType::Technology => Self {
406 debt_percent: dec!(25),
407 equity_percent: dec!(75),
408 current_liabilities_percent: dec!(60),
409 long_term_debt_percent: dec!(30),
410 other_liabilities_percent: dec!(10),
411 common_stock_percent: dec!(10),
412 apic_percent: dec!(40),
413 retained_earnings_percent: dec!(45),
414 other_equity_percent: dec!(5),
415 },
416 IndustryType::Financial => Self {
417 debt_percent: dec!(70),
418 equity_percent: dec!(30),
419 current_liabilities_percent: dec!(70),
420 long_term_debt_percent: dec!(20),
421 other_liabilities_percent: dec!(10),
422 common_stock_percent: dec!(25),
423 apic_percent: dec!(35),
424 retained_earnings_percent: dec!(35),
425 other_equity_percent: dec!(5),
426 },
427 IndustryType::Healthcare => Self {
428 debt_percent: dec!(35),
429 equity_percent: dec!(65),
430 current_liabilities_percent: dec!(50),
431 long_term_debt_percent: dec!(40),
432 other_liabilities_percent: dec!(10),
433 common_stock_percent: dec!(15),
434 apic_percent: dec!(30),
435 retained_earnings_percent: dec!(50),
436 other_equity_percent: dec!(5),
437 },
438 IndustryType::Utilities => Self {
439 debt_percent: dec!(55),
440 equity_percent: dec!(45),
441 current_liabilities_percent: dec!(35),
442 long_term_debt_percent: dec!(55),
443 other_liabilities_percent: dec!(10),
444 common_stock_percent: dec!(20),
445 apic_percent: dec!(25),
446 retained_earnings_percent: dec!(50),
447 other_equity_percent: dec!(5),
448 },
449 IndustryType::RealEstate => Self {
450 debt_percent: dec!(60),
451 equity_percent: dec!(40),
452 current_liabilities_percent: dec!(30),
453 long_term_debt_percent: dec!(60),
454 other_liabilities_percent: dec!(10),
455 common_stock_percent: dec!(25),
456 apic_percent: dec!(30),
457 retained_earnings_percent: dec!(40),
458 other_equity_percent: dec!(5),
459 },
460 }
461 }
462
463 pub fn with_debt_equity_ratio(ratio: Decimal) -> Self {
465 let equity_percent = dec!(100) / (Decimal::ONE + ratio);
471 let debt_percent = dec!(100) - equity_percent;
472
473 Self {
474 debt_percent,
475 equity_percent,
476 ..Default::default()
477 }
478 }
479
480 pub fn debt_equity_ratio(&self) -> Decimal {
482 if self.equity_percent > Decimal::ZERO {
483 self.debt_percent / self.equity_percent
484 } else {
485 Decimal::MAX
486 }
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct TargetRatios {
493 pub current_ratio: Decimal,
495 pub quick_ratio: Decimal,
497 pub debt_to_equity: Decimal,
499 pub asset_turnover: Decimal,
501 pub target_dso_days: u32,
503 pub target_dpo_days: u32,
505 pub target_dio_days: u32,
507 pub gross_margin: Decimal,
509 pub operating_margin: Decimal,
511}
512
513impl TargetRatios {
514 pub fn for_industry(industry: IndustryType) -> Self {
516 match industry {
517 IndustryType::Manufacturing => Self {
518 current_ratio: dec!(1.5),
519 quick_ratio: dec!(0.8),
520 debt_to_equity: dec!(0.6),
521 asset_turnover: dec!(1.2),
522 target_dso_days: 45,
523 target_dpo_days: 35,
524 target_dio_days: 60,
525 gross_margin: dec!(0.35),
526 operating_margin: dec!(0.12),
527 },
528 IndustryType::Retail => Self {
529 current_ratio: dec!(1.2),
530 quick_ratio: dec!(0.4),
531 debt_to_equity: dec!(0.8),
532 asset_turnover: dec!(2.5),
533 target_dso_days: 15,
534 target_dpo_days: 30,
535 target_dio_days: 45,
536 gross_margin: dec!(0.30),
537 operating_margin: dec!(0.08),
538 },
539 IndustryType::Services => Self {
540 current_ratio: dec!(1.8),
541 quick_ratio: dec!(1.6),
542 debt_to_equity: dec!(0.4),
543 asset_turnover: dec!(1.5),
544 target_dso_days: 60,
545 target_dpo_days: 25,
546 target_dio_days: 0,
547 gross_margin: dec!(0.45),
548 operating_margin: dec!(0.18),
549 },
550 IndustryType::Technology => Self {
551 current_ratio: dec!(2.5),
552 quick_ratio: dec!(2.3),
553 debt_to_equity: dec!(0.3),
554 asset_turnover: dec!(0.8),
555 target_dso_days: 55,
556 target_dpo_days: 40,
557 target_dio_days: 15,
558 gross_margin: dec!(0.65),
559 operating_margin: dec!(0.25),
560 },
561 IndustryType::Financial => Self {
562 current_ratio: dec!(1.1),
563 quick_ratio: dec!(1.1),
564 debt_to_equity: dec!(2.0),
565 asset_turnover: dec!(0.3),
566 target_dso_days: 30,
567 target_dpo_days: 20,
568 target_dio_days: 0,
569 gross_margin: dec!(0.80),
570 operating_margin: dec!(0.30),
571 },
572 IndustryType::Healthcare => Self {
573 current_ratio: dec!(1.4),
574 quick_ratio: dec!(1.1),
575 debt_to_equity: dec!(0.5),
576 asset_turnover: dec!(1.0),
577 target_dso_days: 50,
578 target_dpo_days: 30,
579 target_dio_days: 30,
580 gross_margin: dec!(0.40),
581 operating_margin: dec!(0.15),
582 },
583 IndustryType::Utilities => Self {
584 current_ratio: dec!(0.9),
585 quick_ratio: dec!(0.7),
586 debt_to_equity: dec!(1.2),
587 asset_turnover: dec!(0.4),
588 target_dso_days: 40,
589 target_dpo_days: 45,
590 target_dio_days: 20,
591 gross_margin: dec!(0.35),
592 operating_margin: dec!(0.20),
593 },
594 IndustryType::RealEstate => Self {
595 current_ratio: dec!(1.0),
596 quick_ratio: dec!(0.8),
597 debt_to_equity: dec!(1.5),
598 asset_turnover: dec!(0.2),
599 target_dso_days: 30,
600 target_dpo_days: 25,
601 target_dio_days: 0,
602 gross_margin: dec!(0.50),
603 operating_margin: dec!(0.35),
604 },
605 }
606 }
607
608 pub fn calculate_target_ar(&self, annual_revenue: Decimal) -> Decimal {
610 annual_revenue * Decimal::from(self.target_dso_days) / dec!(365)
611 }
612
613 pub fn calculate_target_ap(&self, annual_cogs: Decimal) -> Decimal {
615 annual_cogs * Decimal::from(self.target_dpo_days) / dec!(365)
616 }
617
618 pub fn calculate_target_inventory(&self, annual_cogs: Decimal) -> Decimal {
620 annual_cogs * Decimal::from(self.target_dio_days) / dec!(365)
621 }
622}
623
624impl Default for TargetRatios {
625 fn default() -> Self {
626 Self::for_industry(IndustryType::Manufacturing)
627 }
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
632pub struct AccountSpec {
633 pub account_code: String,
635 pub description: String,
637 pub account_type: AccountType,
639 pub category: AccountCategory,
641 pub fixed_balance: Option<Decimal>,
643 pub category_percent: Option<Decimal>,
645 pub total_assets_percent: Option<Decimal>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct GeneratedOpeningBalance {
652 pub company_code: String,
654 pub as_of_date: NaiveDate,
656 pub balances: HashMap<String, Decimal>,
658 pub total_assets: Decimal,
660 pub total_liabilities: Decimal,
662 pub total_equity: Decimal,
664 pub is_balanced: bool,
666 pub calculated_ratios: CalculatedRatios,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct CalculatedRatios {
673 pub current_ratio: Option<Decimal>,
675 pub quick_ratio: Option<Decimal>,
677 pub debt_to_equity: Option<Decimal>,
679 pub working_capital: Decimal,
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_opening_balance_spec_creation() {
689 let spec = OpeningBalanceSpec::new(
690 "1000".to_string(),
691 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
692 2022,
693 "USD".to_string(),
694 dec!(1000000),
695 IndustryType::Manufacturing,
696 );
697
698 assert!(spec.validate().is_ok());
699 assert_eq!(spec.calculate_total_liabilities(), dec!(400000)); assert_eq!(spec.calculate_total_equity(), dec!(600000)); }
702
703 #[test]
704 fn test_capital_structure_debt_equity() {
705 let structure = CapitalStructure::with_debt_equity_ratio(dec!(0.5));
706
707 assert!((structure.equity_percent - dec!(66.67)).abs() < dec!(0.01));
710 assert!((structure.debt_percent - dec!(33.33)).abs() < dec!(0.01));
711 assert!((structure.debt_equity_ratio() - dec!(0.5)).abs() < dec!(0.01));
712 }
713
714 #[test]
715 fn test_asset_composition_for_industries() {
716 let manufacturing = AssetComposition::for_industry(IndustryType::Manufacturing);
717 assert_eq!(manufacturing.current_assets_percent, dec!(40));
718
719 let retail = AssetComposition::for_industry(IndustryType::Retail);
720 assert_eq!(retail.current_assets_percent, dec!(55));
721 assert!(retail.inventory_percent > manufacturing.inventory_percent);
722
723 let technology = AssetComposition::for_industry(IndustryType::Technology);
724 assert!(technology.intangibles_percent > manufacturing.intangibles_percent);
725 }
726
727 #[test]
728 fn test_target_ratios_calculations() {
729 let ratios = TargetRatios::for_industry(IndustryType::Manufacturing);
730
731 let annual_revenue = dec!(1000000);
732 let annual_cogs = dec!(650000); let target_ar = ratios.calculate_target_ar(annual_revenue);
735 assert!(target_ar > dec!(120000) && target_ar < dec!(130000));
737
738 let target_inventory = ratios.calculate_target_inventory(annual_cogs);
739 assert!(target_inventory > dec!(100000) && target_inventory < dec!(115000));
741 }
742
743 #[test]
744 fn test_opening_balance_validation() {
745 let mut spec = OpeningBalanceSpec::new(
746 "1000".to_string(),
747 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
748 2022,
749 "USD".to_string(),
750 dec!(1000000),
751 IndustryType::Manufacturing,
752 );
753
754 assert!(spec.validate().is_ok());
756
757 spec.capital_structure.debt_percent = dec!(80);
759 spec.capital_structure.equity_percent = dec!(30); assert!(spec.validate().is_err());
761 }
762}