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