1use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use std::collections::HashMap;
10use tracing::warn;
11
12use datasynth_core::accounts::AccountCategory;
13use datasynth_core::models::balance::TrialBalance;
14use datasynth_core::models::{
15 FxRateTable, RateType, TranslatedAmount, TranslationAccountType, TranslationMethod,
16};
17use datasynth_core::FrameworkAccounts;
18
19#[derive(Debug, Clone)]
21pub struct CurrencyTranslatorConfig {
22 pub method: TranslationMethod,
24 pub group_currency: String,
26 pub account_type_map: HashMap<String, TranslationAccountType>,
28 pub historical_rate_accounts: Vec<String>,
30 pub retained_earnings_account: String,
32 pub cta_account: String,
34}
35
36impl Default for CurrencyTranslatorConfig {
37 fn default() -> Self {
38 let mut account_type_map = HashMap::new();
39 account_type_map.insert("1".to_string(), TranslationAccountType::Asset);
41 account_type_map.insert("2".to_string(), TranslationAccountType::Liability);
43 account_type_map.insert("3".to_string(), TranslationAccountType::Equity);
45 account_type_map.insert("4".to_string(), TranslationAccountType::Revenue);
47 account_type_map.insert("5".to_string(), TranslationAccountType::Expense);
49 account_type_map.insert("6".to_string(), TranslationAccountType::Expense);
50
51 Self {
52 method: TranslationMethod::CurrentRate,
53 group_currency: "USD".to_string(),
54 account_type_map,
55 historical_rate_accounts: vec![
56 "3100".to_string(), "3200".to_string(), ],
59 retained_earnings_account: "3300".to_string(),
60 cta_account: "3900".to_string(),
61 }
62 }
63}
64
65pub fn is_monetary(account_code: &str, framework_accounts: &FrameworkAccounts) -> bool {
84 let category = framework_accounts.classify(account_code);
85
86 match category {
87 AccountCategory::Liability => true,
89 AccountCategory::Equity => false,
91 AccountCategory::Revenue
93 | AccountCategory::Cogs
94 | AccountCategory::OperatingExpense
95 | AccountCategory::OtherIncomeExpense
96 | AccountCategory::Tax => true,
97 AccountCategory::Asset => is_monetary_asset(account_code),
99 AccountCategory::Suspense | AccountCategory::Unknown => {
101 warn!(
102 account_code,
103 ?category,
104 "is_monetary: unrecognized account category, defaulting to monetary"
105 );
106 true
107 }
108 }
109}
110
111fn is_monetary_asset(account_code: &str) -> bool {
122 if account_code.len() < 2 {
123 return true;
124 }
125
126 let prefix2 = &account_code[..2];
127 match prefix2 {
128 "10" => true,
130 "11" => true,
132 "12" => true,
134 "13" => true,
136 "14" => false,
138 "15" => false,
140 "16" => false,
142 "17" => false,
144 "18" => false,
146 "19" => false,
148 _ => true,
150 }
151}
152
153pub struct CurrencyTranslator {
155 config: CurrencyTranslatorConfig,
156 historical_equity_rates: HashMap<String, Decimal>,
158 framework_accounts: FrameworkAccounts,
160}
161
162impl CurrencyTranslator {
163 pub fn new_with_framework(config: CurrencyTranslatorConfig, framework: &str) -> Self {
165 Self {
166 config,
167 historical_equity_rates: HashMap::new(),
168 framework_accounts: FrameworkAccounts::for_framework(framework),
169 }
170 }
171
172 pub fn new(config: CurrencyTranslatorConfig) -> Self {
174 Self::new_with_framework(config, "us_gaap")
175 }
176
177 pub fn set_historical_equity_rates(&mut self, rates: HashMap<String, Decimal>) {
183 self.historical_equity_rates = rates;
184 }
185
186 pub fn translate_trial_balance(
188 &self,
189 trial_balance: &TrialBalance,
190 rate_table: &FxRateTable,
191 historical_rates: &HashMap<String, Decimal>,
192 ) -> TranslatedTrialBalance {
193 let local_currency = &trial_balance.currency;
194 let period_end = trial_balance.as_of_date;
195
196 let closing_rate = rate_table
198 .get_closing_rate(local_currency, &self.config.group_currency, period_end)
199 .map(|r| r.rate)
200 .unwrap_or(Decimal::ONE);
201
202 let average_rate = rate_table
203 .get_average_rate(local_currency, &self.config.group_currency, period_end)
204 .map(|r| r.rate)
205 .unwrap_or(closing_rate);
206
207 let mut translated_lines = Vec::new();
208 let mut total_local_debit = Decimal::ZERO;
209 let mut total_local_credit = Decimal::ZERO;
210 let mut total_group_debit = Decimal::ZERO;
211 let mut total_group_credit = Decimal::ZERO;
212
213 for line in &trial_balance.lines {
214 let account_type = self.determine_account_type(&line.account_code);
215 let rate = self.determine_rate(
216 &line.account_code,
217 &account_type,
218 closing_rate,
219 average_rate,
220 historical_rates,
221 );
222
223 let group_debit = (line.debit_balance * rate).round_dp(2);
224 let group_credit = (line.credit_balance * rate).round_dp(2);
225
226 translated_lines.push(TranslatedTrialBalanceLine {
227 account_code: line.account_code.clone(),
228 account_description: Some(line.account_description.clone()),
229 account_type: account_type.clone(),
230 local_debit: line.debit_balance,
231 local_credit: line.credit_balance,
232 rate_used: rate,
233 rate_type: self.rate_type_for_account(&account_type),
234 group_debit,
235 group_credit,
236 });
237
238 total_local_debit += line.debit_balance;
239 total_local_credit += line.credit_balance;
240 total_group_debit += group_debit;
241 total_group_credit += group_credit;
242 }
243
244 let cta_amount = total_group_debit - total_group_credit;
246
247 TranslatedTrialBalance {
248 company_code: trial_balance.company_code.clone(),
249 company_name: trial_balance.company_name.clone().unwrap_or_default(),
250 local_currency: local_currency.clone(),
251 group_currency: self.config.group_currency.clone(),
252 period_end_date: period_end,
253 fiscal_year: trial_balance.fiscal_year,
254 fiscal_period: trial_balance.fiscal_period as u8,
255 lines: translated_lines,
256 closing_rate,
257 average_rate,
258 total_local_debit,
259 total_local_credit,
260 total_group_debit,
261 total_group_credit,
262 cta_amount,
263 translation_method: self.config.method.clone(),
264 }
265 }
266
267 pub fn translate_amount(
273 &self,
274 amount: Decimal,
275 local_currency: &str,
276 account_code: &str,
277 account_type: &TranslationAccountType,
278 rate_table: &FxRateTable,
279 date: NaiveDate,
280 ) -> TranslatedAmount {
281 let closing_rate = rate_table
282 .get_closing_rate(local_currency, &self.config.group_currency, date)
283 .map(|r| r.rate)
284 .unwrap_or(Decimal::ONE);
285
286 let average_rate = rate_table
287 .get_average_rate(local_currency, &self.config.group_currency, date)
288 .map(|r| r.rate)
289 .unwrap_or(closing_rate);
290
291 let (rate, rate_type) = match &self.config.method {
292 TranslationMethod::CurrentRate => match account_type {
293 TranslationAccountType::Asset | TranslationAccountType::Liability => {
294 (closing_rate, RateType::Closing)
295 }
296 TranslationAccountType::Revenue | TranslationAccountType::Expense => {
297 (average_rate, RateType::Average)
298 }
299 TranslationAccountType::Equity
300 | TranslationAccountType::CommonStock
301 | TranslationAccountType::AdditionalPaidInCapital
302 | TranslationAccountType::RetainedEarnings => {
303 let hist_rate = self
304 .historical_equity_rates
305 .get(account_code)
306 .copied()
307 .unwrap_or(Decimal::ONE);
308 (hist_rate, RateType::Historical)
309 }
310 },
311 TranslationMethod::Temporal => match account_type {
312 TranslationAccountType::Revenue | TranslationAccountType::Expense => {
313 (average_rate, RateType::Average)
314 }
315 TranslationAccountType::CommonStock
316 | TranslationAccountType::AdditionalPaidInCapital
317 | TranslationAccountType::RetainedEarnings
318 | TranslationAccountType::Equity => {
319 let hist_rate = self
320 .historical_equity_rates
321 .get(account_code)
322 .copied()
323 .unwrap_or(Decimal::ONE);
324 (hist_rate, RateType::Historical)
325 }
326 TranslationAccountType::Asset | TranslationAccountType::Liability => {
327 if is_monetary(account_code, &self.framework_accounts) {
328 (closing_rate, RateType::Closing)
329 } else {
330 let hist_rate = self
331 .historical_equity_rates
332 .get(account_code)
333 .copied()
334 .unwrap_or(closing_rate);
335 (hist_rate, RateType::Historical)
336 }
337 }
338 },
339 TranslationMethod::MonetaryNonMonetary => match account_type {
340 TranslationAccountType::CommonStock
341 | TranslationAccountType::AdditionalPaidInCapital
342 | TranslationAccountType::RetainedEarnings
343 | TranslationAccountType::Equity => {
344 let hist_rate = self
345 .historical_equity_rates
346 .get(account_code)
347 .copied()
348 .unwrap_or(Decimal::ONE);
349 (hist_rate, RateType::Historical)
350 }
351 _ => {
352 if is_monetary(account_code, &self.framework_accounts) {
353 (closing_rate, RateType::Closing)
354 } else {
355 let hist_rate = self
356 .historical_equity_rates
357 .get(account_code)
358 .copied()
359 .unwrap_or(closing_rate);
360 (hist_rate, RateType::Historical)
361 }
362 }
363 },
364 };
365
366 TranslatedAmount {
367 local_amount: amount,
368 local_currency: local_currency.to_string(),
369 group_amount: (amount * rate).round_dp(2),
370 group_currency: self.config.group_currency.clone(),
371 rate_used: rate,
372 rate_type,
373 translation_date: date,
374 }
375 }
376
377 fn determine_account_type(&self, account_code: &str) -> TranslationAccountType {
379 if self
381 .config
382 .historical_rate_accounts
383 .contains(&account_code.to_string())
384 {
385 if account_code.starts_with("31") {
386 return TranslationAccountType::CommonStock;
387 } else if account_code.starts_with("32") {
388 return TranslationAccountType::AdditionalPaidInCapital;
389 }
390 }
391
392 if account_code == self.config.retained_earnings_account {
393 return TranslationAccountType::RetainedEarnings;
394 }
395
396 for (prefix, account_type) in &self.config.account_type_map {
398 if account_code.starts_with(prefix) {
399 return account_type.clone();
400 }
401 }
402
403 TranslationAccountType::Asset
405 }
406
407 fn lookup_historical_equity_rate(
412 &self,
413 account_code: &str,
414 historical_rates: &HashMap<String, Decimal>,
415 fallback: Decimal,
416 ) -> Decimal {
417 self.historical_equity_rates
418 .get(account_code)
419 .or_else(|| historical_rates.get(account_code))
420 .copied()
421 .unwrap_or(fallback)
422 }
423
424 fn determine_rate(
426 &self,
427 account_code: &str,
428 account_type: &TranslationAccountType,
429 closing_rate: Decimal,
430 average_rate: Decimal,
431 historical_rates: &HashMap<String, Decimal>,
432 ) -> Decimal {
433 match self.config.method {
434 TranslationMethod::CurrentRate => {
435 match account_type {
436 TranslationAccountType::Asset | TranslationAccountType::Liability => {
437 closing_rate
438 }
439 TranslationAccountType::Revenue | TranslationAccountType::Expense => {
440 average_rate
441 }
442 TranslationAccountType::CommonStock
443 | TranslationAccountType::AdditionalPaidInCapital => {
444 self.lookup_historical_equity_rate(
446 account_code,
447 historical_rates,
448 closing_rate,
449 )
450 }
451 TranslationAccountType::Equity | TranslationAccountType::RetainedEarnings => {
452 closing_rate
454 }
455 }
456 }
457 TranslationMethod::Temporal => {
458 match account_type {
461 TranslationAccountType::CommonStock
462 | TranslationAccountType::AdditionalPaidInCapital
463 | TranslationAccountType::RetainedEarnings => self
464 .lookup_historical_equity_rate(
465 account_code,
466 historical_rates,
467 closing_rate,
468 ),
469 TranslationAccountType::Equity => self.lookup_historical_equity_rate(
470 account_code,
471 historical_rates,
472 closing_rate,
473 ),
474 TranslationAccountType::Revenue | TranslationAccountType::Expense => {
475 average_rate
477 }
478 TranslationAccountType::Asset | TranslationAccountType::Liability => {
479 if is_monetary(account_code, &self.framework_accounts) {
481 closing_rate
482 } else {
483 historical_rates
486 .get(account_code)
487 .copied()
488 .unwrap_or(closing_rate)
489 }
490 }
491 }
492 }
493 TranslationMethod::MonetaryNonMonetary => {
494 match account_type {
498 TranslationAccountType::CommonStock
499 | TranslationAccountType::AdditionalPaidInCapital
500 | TranslationAccountType::RetainedEarnings => self
501 .lookup_historical_equity_rate(
502 account_code,
503 historical_rates,
504 closing_rate,
505 ),
506 TranslationAccountType::Equity => self.lookup_historical_equity_rate(
507 account_code,
508 historical_rates,
509 closing_rate,
510 ),
511 _ => {
512 if is_monetary(account_code, &self.framework_accounts) {
514 closing_rate
515 } else {
516 historical_rates
518 .get(account_code)
519 .copied()
520 .unwrap_or(closing_rate)
521 }
522 }
523 }
524 }
525 }
526 }
527
528 fn rate_type_for_account(&self, account_type: &TranslationAccountType) -> RateType {
530 match account_type {
531 TranslationAccountType::Asset | TranslationAccountType::Liability => RateType::Closing,
532 TranslationAccountType::Revenue | TranslationAccountType::Expense => RateType::Average,
533 TranslationAccountType::Equity
534 | TranslationAccountType::CommonStock
535 | TranslationAccountType::AdditionalPaidInCapital
536 | TranslationAccountType::RetainedEarnings => RateType::Historical,
537 }
538 }
539}
540
541#[derive(Debug, Clone)]
543pub struct TranslatedTrialBalance {
544 pub company_code: String,
546 pub company_name: String,
548 pub local_currency: String,
550 pub group_currency: String,
552 pub period_end_date: NaiveDate,
554 pub fiscal_year: i32,
556 pub fiscal_period: u8,
558 pub lines: Vec<TranslatedTrialBalanceLine>,
560 pub closing_rate: Decimal,
562 pub average_rate: Decimal,
564 pub total_local_debit: Decimal,
566 pub total_local_credit: Decimal,
568 pub total_group_debit: Decimal,
570 pub total_group_credit: Decimal,
572 pub cta_amount: Decimal,
574 pub translation_method: TranslationMethod,
576}
577
578impl TranslatedTrialBalance {
579 pub fn is_local_balanced(&self) -> bool {
581 (self.total_local_debit - self.total_local_credit).abs() < dec!(0.01)
582 }
583
584 pub fn is_group_balanced(&self) -> bool {
586 let balance = self.total_group_debit - self.total_group_credit - self.cta_amount;
587 balance.abs() < dec!(0.01)
588 }
589
590 pub fn local_net_assets(&self) -> Decimal {
592 let assets: Decimal = self
593 .lines
594 .iter()
595 .filter(|l| matches!(l.account_type, TranslationAccountType::Asset))
596 .map(|l| l.local_debit - l.local_credit)
597 .sum();
598
599 let liabilities: Decimal = self
600 .lines
601 .iter()
602 .filter(|l| matches!(l.account_type, TranslationAccountType::Liability))
603 .map(|l| l.local_credit - l.local_debit)
604 .sum();
605
606 assets - liabilities
607 }
608
609 pub fn group_net_assets(&self) -> Decimal {
611 let assets: Decimal = self
612 .lines
613 .iter()
614 .filter(|l| matches!(l.account_type, TranslationAccountType::Asset))
615 .map(|l| l.group_debit - l.group_credit)
616 .sum();
617
618 let liabilities: Decimal = self
619 .lines
620 .iter()
621 .filter(|l| matches!(l.account_type, TranslationAccountType::Liability))
622 .map(|l| l.group_credit - l.group_debit)
623 .sum();
624
625 assets - liabilities
626 }
627}
628
629#[derive(Debug, Clone)]
631pub struct TranslatedTrialBalanceLine {
632 pub account_code: String,
634 pub account_description: Option<String>,
636 pub account_type: TranslationAccountType,
638 pub local_debit: Decimal,
640 pub local_credit: Decimal,
642 pub rate_used: Decimal,
644 pub rate_type: RateType,
646 pub group_debit: Decimal,
648 pub group_credit: Decimal,
650}
651
652impl TranslatedTrialBalanceLine {
653 pub fn local_net(&self) -> Decimal {
655 self.local_debit - self.local_credit
656 }
657
658 pub fn group_net(&self) -> Decimal {
660 self.group_debit - self.group_credit
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use datasynth_core::models::balance::{
668 AccountCategory, AccountType, TrialBalanceLine, TrialBalanceType,
669 };
670 use datasynth_core::models::FxRate;
671
672 fn create_test_trial_balance() -> TrialBalance {
673 let mut tb = TrialBalance::new(
674 "TB-TEST-2024-12".to_string(),
675 "1200".to_string(),
676 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
677 2024,
678 12,
679 "EUR".to_string(),
680 TrialBalanceType::PostClosing,
681 );
682 tb.company_name = Some("Test Subsidiary".to_string());
683
684 tb.add_line(TrialBalanceLine {
685 account_code: "1000".to_string(),
686 account_description: "Cash".to_string(),
687 category: AccountCategory::CurrentAssets,
688 account_type: AccountType::Asset,
689 opening_balance: Decimal::ZERO,
690 period_debits: dec!(100000),
691 period_credits: Decimal::ZERO,
692 closing_balance: dec!(100000),
693 debit_balance: dec!(100000),
694 credit_balance: Decimal::ZERO,
695 cost_center: None,
696 profit_center: None,
697 });
698
699 tb.add_line(TrialBalanceLine {
700 account_code: "2000".to_string(),
701 account_description: "Accounts Payable".to_string(),
702 category: AccountCategory::CurrentLiabilities,
703 account_type: AccountType::Liability,
704 opening_balance: Decimal::ZERO,
705 period_debits: Decimal::ZERO,
706 period_credits: dec!(50000),
707 closing_balance: dec!(50000),
708 debit_balance: Decimal::ZERO,
709 credit_balance: dec!(50000),
710 cost_center: None,
711 profit_center: None,
712 });
713
714 tb.add_line(TrialBalanceLine {
715 account_code: "4000".to_string(),
716 account_description: "Revenue".to_string(),
717 category: AccountCategory::Revenue,
718 account_type: AccountType::Revenue,
719 opening_balance: Decimal::ZERO,
720 period_debits: Decimal::ZERO,
721 period_credits: dec!(150000),
722 closing_balance: dec!(150000),
723 debit_balance: Decimal::ZERO,
724 credit_balance: dec!(150000),
725 cost_center: None,
726 profit_center: None,
727 });
728
729 tb.add_line(TrialBalanceLine {
730 account_code: "5000".to_string(),
731 account_description: "Expenses".to_string(),
732 category: AccountCategory::OperatingExpenses,
733 account_type: AccountType::Expense,
734 opening_balance: Decimal::ZERO,
735 period_debits: dec!(100000),
736 period_credits: Decimal::ZERO,
737 closing_balance: dec!(100000),
738 debit_balance: dec!(100000),
739 credit_balance: Decimal::ZERO,
740 cost_center: None,
741 profit_center: None,
742 });
743
744 tb
745 }
746
747 #[test]
748 fn test_translate_trial_balance() {
749 let translator = CurrencyTranslator::new(CurrencyTranslatorConfig::default());
750 let trial_balance = create_test_trial_balance();
751
752 let mut rate_table = FxRateTable::new("USD");
753 rate_table.add_rate(FxRate::new(
754 "EUR",
755 "USD",
756 RateType::Closing,
757 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
758 dec!(1.10),
759 "TEST",
760 ));
761 rate_table.add_rate(FxRate::new(
762 "EUR",
763 "USD",
764 RateType::Average,
765 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
766 dec!(1.08),
767 "TEST",
768 ));
769
770 let historical_rates = HashMap::new();
771 let translated =
772 translator.translate_trial_balance(&trial_balance, &rate_table, &historical_rates);
773
774 assert!(translated.is_local_balanced());
775 assert_eq!(translated.closing_rate, dec!(1.10));
776 assert_eq!(translated.average_rate, dec!(1.08));
777 }
778
779 #[test]
780 fn test_is_monetary_us_gaap() {
781 let fa = FrameworkAccounts::us_gaap();
782
783 assert!(is_monetary("1000", &fa));
785 assert!(is_monetary("1001", &fa));
786 assert!(is_monetary("1099", &fa));
787
788 assert!(is_monetary("1100", &fa));
790 assert!(is_monetary("1150", &fa));
791
792 assert!(is_monetary("1200", &fa));
794
795 assert!(is_monetary("1300", &fa));
797
798 assert!(!is_monetary("1400", &fa));
800 assert!(!is_monetary("1450", &fa));
801
802 assert!(!is_monetary("1500", &fa));
804 assert!(!is_monetary("1550", &fa));
805
806 assert!(!is_monetary("1600", &fa));
808 assert!(!is_monetary("1650", &fa));
809
810 assert!(!is_monetary("1700", &fa));
812
813 assert!(!is_monetary("1800", &fa));
815
816 assert!(!is_monetary("1900", &fa));
818
819 assert!(is_monetary("2000", &fa));
821 assert!(is_monetary("2100", &fa));
822 assert!(is_monetary("2500", &fa));
823 assert!(is_monetary("2900", &fa));
824
825 assert!(!is_monetary("3000", &fa));
827 assert!(!is_monetary("3100", &fa));
828 assert!(!is_monetary("3200", &fa));
829 assert!(!is_monetary("3900", &fa));
830
831 assert!(is_monetary("4000", &fa));
833 assert!(is_monetary("4500", &fa));
834
835 assert!(is_monetary("5000", &fa));
837 assert!(is_monetary("6000", &fa));
838
839 assert!(is_monetary("1", &fa));
842 }
843
844 #[test]
845 fn test_is_monetary_french_gaap() {
846 let fa = FrameworkAccounts::french_gaap();
847
848 assert!(is_monetary("512000", &fa));
850
851 assert!(is_monetary("411000", &fa));
854
855 assert!(is_monetary("210000", &fa));
858
859 assert!(is_monetary("401000", &fa));
861
862 assert!(!is_monetary("101000", &fa));
864
865 assert!(is_monetary("701000", &fa));
867
868 assert!(is_monetary("603000", &fa));
870 }
871
872 #[test]
873 fn test_historical_equity_rates() {
874 let mut translator = CurrencyTranslator::new(CurrencyTranslatorConfig::default());
875
876 assert!(translator.historical_equity_rates.is_empty());
878
879 let mut rates = HashMap::new();
881 rates.insert("3100".to_string(), dec!(1.05));
882 rates.insert("3200".to_string(), dec!(0.98));
883 translator.set_historical_equity_rates(rates);
884
885 assert_eq!(translator.historical_equity_rates.len(), 2);
886 assert_eq!(
887 translator.historical_equity_rates.get("3100"),
888 Some(&dec!(1.05))
889 );
890 assert_eq!(
891 translator.historical_equity_rates.get("3200"),
892 Some(&dec!(0.98))
893 );
894
895 let rate = translator.determine_rate(
897 "3100",
898 &TranslationAccountType::CommonStock,
899 dec!(1.10),
900 dec!(1.08),
901 &HashMap::new(),
902 );
903 assert_eq!(rate, dec!(1.05));
904 }
905
906 #[test]
907 fn test_temporal_method_monetary_vs_non_monetary() {
908 let config = CurrencyTranslatorConfig {
909 method: TranslationMethod::Temporal,
910 ..CurrencyTranslatorConfig::default()
911 };
912 let translator = CurrencyTranslator::new(config);
913
914 let closing_rate = dec!(1.10);
915 let average_rate = dec!(1.08);
916 let mut historical_rates = HashMap::new();
917 historical_rates.insert("1500".to_string(), dec!(1.02)); let rate = translator.determine_rate(
921 "1000",
922 &TranslationAccountType::Asset,
923 closing_rate,
924 average_rate,
925 &historical_rates,
926 );
927 assert_eq!(rate, closing_rate);
928
929 let rate = translator.determine_rate(
931 "1100",
932 &TranslationAccountType::Asset,
933 closing_rate,
934 average_rate,
935 &historical_rates,
936 );
937 assert_eq!(rate, closing_rate);
938
939 let rate = translator.determine_rate(
941 "1500",
942 &TranslationAccountType::Asset,
943 closing_rate,
944 average_rate,
945 &historical_rates,
946 );
947 assert_eq!(rate, dec!(1.02));
948
949 let rate = translator.determine_rate(
951 "1600",
952 &TranslationAccountType::Asset,
953 closing_rate,
954 average_rate,
955 &historical_rates,
956 );
957 assert_eq!(rate, closing_rate);
958
959 let rate = translator.determine_rate(
961 "2000",
962 &TranslationAccountType::Liability,
963 closing_rate,
964 average_rate,
965 &historical_rates,
966 );
967 assert_eq!(rate, closing_rate);
968
969 let rate = translator.determine_rate(
971 "4000",
972 &TranslationAccountType::Revenue,
973 closing_rate,
974 average_rate,
975 &historical_rates,
976 );
977 assert_eq!(rate, average_rate);
978
979 let rate = translator.determine_rate(
981 "5000",
982 &TranslationAccountType::Expense,
983 closing_rate,
984 average_rate,
985 &historical_rates,
986 );
987 assert_eq!(rate, average_rate);
988
989 let mut translator_with_equity = CurrencyTranslator::new(CurrencyTranslatorConfig {
991 method: TranslationMethod::Temporal,
992 ..CurrencyTranslatorConfig::default()
993 });
994 let mut equity_rates = HashMap::new();
995 equity_rates.insert("3100".to_string(), dec!(0.95));
996 translator_with_equity.set_historical_equity_rates(equity_rates);
997
998 let rate = translator_with_equity.determine_rate(
999 "3100",
1000 &TranslationAccountType::CommonStock,
1001 closing_rate,
1002 average_rate,
1003 &historical_rates,
1004 );
1005 assert_eq!(rate, dec!(0.95));
1006 }
1007
1008 #[test]
1009 fn test_monetary_non_monetary_method() {
1010 let config = CurrencyTranslatorConfig {
1011 method: TranslationMethod::MonetaryNonMonetary,
1012 ..CurrencyTranslatorConfig::default()
1013 };
1014 let mut translator = CurrencyTranslator::new(config);
1015
1016 let closing_rate = dec!(1.10);
1017 let average_rate = dec!(1.08);
1018 let mut historical_rates = HashMap::new();
1019 historical_rates.insert("1500".to_string(), dec!(1.02));
1020 historical_rates.insert("1600".to_string(), dec!(0.99));
1021
1022 let mut equity_rates = HashMap::new();
1024 equity_rates.insert("3100".to_string(), dec!(1.05));
1025 equity_rates.insert("3300".to_string(), dec!(1.03));
1026 translator.set_historical_equity_rates(equity_rates);
1027
1028 let rate = translator.determine_rate(
1030 "1000",
1031 &TranslationAccountType::Asset,
1032 closing_rate,
1033 average_rate,
1034 &historical_rates,
1035 );
1036 assert_eq!(rate, closing_rate);
1037
1038 let rate = translator.determine_rate(
1040 "1100",
1041 &TranslationAccountType::Asset,
1042 closing_rate,
1043 average_rate,
1044 &historical_rates,
1045 );
1046 assert_eq!(rate, closing_rate);
1047
1048 let rate = translator.determine_rate(
1050 "1500",
1051 &TranslationAccountType::Asset,
1052 closing_rate,
1053 average_rate,
1054 &historical_rates,
1055 );
1056 assert_eq!(rate, dec!(1.02));
1057
1058 let rate = translator.determine_rate(
1060 "1600",
1061 &TranslationAccountType::Asset,
1062 closing_rate,
1063 average_rate,
1064 &historical_rates,
1065 );
1066 assert_eq!(rate, dec!(0.99));
1067
1068 let rate = translator.determine_rate(
1070 "2000",
1071 &TranslationAccountType::Liability,
1072 closing_rate,
1073 average_rate,
1074 &historical_rates,
1075 );
1076 assert_eq!(rate, closing_rate);
1077
1078 let rate = translator.determine_rate(
1080 "3100",
1081 &TranslationAccountType::CommonStock,
1082 closing_rate,
1083 average_rate,
1084 &historical_rates,
1085 );
1086 assert_eq!(rate, dec!(1.05));
1087
1088 let rate = translator.determine_rate(
1090 "3300",
1091 &TranslationAccountType::RetainedEarnings,
1092 closing_rate,
1093 average_rate,
1094 &historical_rates,
1095 );
1096 assert_eq!(rate, dec!(1.03));
1097
1098 let rate = translator.determine_rate(
1100 "4000",
1101 &TranslationAccountType::Revenue,
1102 closing_rate,
1103 average_rate,
1104 &historical_rates,
1105 );
1106 assert_eq!(rate, closing_rate);
1107
1108 let rate = translator.determine_rate(
1110 "5000",
1111 &TranslationAccountType::Expense,
1112 closing_rate,
1113 average_rate,
1114 &historical_rates,
1115 );
1116 assert_eq!(rate, closing_rate);
1117 }
1118}