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)]
665#[allow(clippy::unwrap_used)]
666mod tests {
667 use super::*;
668 use datasynth_core::models::balance::{
669 AccountCategory, AccountType, TrialBalanceLine, TrialBalanceType,
670 };
671 use datasynth_core::models::FxRate;
672
673 fn create_test_trial_balance() -> TrialBalance {
674 let mut tb = TrialBalance::new(
675 "TB-TEST-2024-12".to_string(),
676 "1200".to_string(),
677 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
678 2024,
679 12,
680 "EUR".to_string(),
681 TrialBalanceType::PostClosing,
682 );
683 tb.company_name = Some("Test Subsidiary".to_string());
684
685 tb.add_line(TrialBalanceLine {
686 account_code: "1000".to_string(),
687 account_description: "Cash".to_string(),
688 category: AccountCategory::CurrentAssets,
689 account_type: AccountType::Asset,
690 opening_balance: Decimal::ZERO,
691 period_debits: dec!(100000),
692 period_credits: Decimal::ZERO,
693 closing_balance: dec!(100000),
694 debit_balance: dec!(100000),
695 credit_balance: Decimal::ZERO,
696 cost_center: None,
697 profit_center: None,
698 });
699
700 tb.add_line(TrialBalanceLine {
701 account_code: "2000".to_string(),
702 account_description: "Accounts Payable".to_string(),
703 category: AccountCategory::CurrentLiabilities,
704 account_type: AccountType::Liability,
705 opening_balance: Decimal::ZERO,
706 period_debits: Decimal::ZERO,
707 period_credits: dec!(50000),
708 closing_balance: dec!(50000),
709 debit_balance: Decimal::ZERO,
710 credit_balance: dec!(50000),
711 cost_center: None,
712 profit_center: None,
713 });
714
715 tb.add_line(TrialBalanceLine {
716 account_code: "4000".to_string(),
717 account_description: "Revenue".to_string(),
718 category: AccountCategory::Revenue,
719 account_type: AccountType::Revenue,
720 opening_balance: Decimal::ZERO,
721 period_debits: Decimal::ZERO,
722 period_credits: dec!(150000),
723 closing_balance: dec!(150000),
724 debit_balance: Decimal::ZERO,
725 credit_balance: dec!(150000),
726 cost_center: None,
727 profit_center: None,
728 });
729
730 tb.add_line(TrialBalanceLine {
731 account_code: "5000".to_string(),
732 account_description: "Expenses".to_string(),
733 category: AccountCategory::OperatingExpenses,
734 account_type: AccountType::Expense,
735 opening_balance: Decimal::ZERO,
736 period_debits: dec!(100000),
737 period_credits: Decimal::ZERO,
738 closing_balance: dec!(100000),
739 debit_balance: dec!(100000),
740 credit_balance: Decimal::ZERO,
741 cost_center: None,
742 profit_center: None,
743 });
744
745 tb
746 }
747
748 #[test]
749 fn test_translate_trial_balance() {
750 let translator = CurrencyTranslator::new(CurrencyTranslatorConfig::default());
751 let trial_balance = create_test_trial_balance();
752
753 let mut rate_table = FxRateTable::new("USD");
754 rate_table.add_rate(FxRate::new(
755 "EUR",
756 "USD",
757 RateType::Closing,
758 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
759 dec!(1.10),
760 "TEST",
761 ));
762 rate_table.add_rate(FxRate::new(
763 "EUR",
764 "USD",
765 RateType::Average,
766 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
767 dec!(1.08),
768 "TEST",
769 ));
770
771 let historical_rates = HashMap::new();
772 let translated =
773 translator.translate_trial_balance(&trial_balance, &rate_table, &historical_rates);
774
775 assert!(translated.is_local_balanced());
776 assert_eq!(translated.closing_rate, dec!(1.10));
777 assert_eq!(translated.average_rate, dec!(1.08));
778 }
779
780 #[test]
781 fn test_is_monetary_us_gaap() {
782 let fa = FrameworkAccounts::us_gaap();
783
784 assert!(is_monetary("1000", &fa));
786 assert!(is_monetary("1001", &fa));
787 assert!(is_monetary("1099", &fa));
788
789 assert!(is_monetary("1100", &fa));
791 assert!(is_monetary("1150", &fa));
792
793 assert!(is_monetary("1200", &fa));
795
796 assert!(is_monetary("1300", &fa));
798
799 assert!(!is_monetary("1400", &fa));
801 assert!(!is_monetary("1450", &fa));
802
803 assert!(!is_monetary("1500", &fa));
805 assert!(!is_monetary("1550", &fa));
806
807 assert!(!is_monetary("1600", &fa));
809 assert!(!is_monetary("1650", &fa));
810
811 assert!(!is_monetary("1700", &fa));
813
814 assert!(!is_monetary("1800", &fa));
816
817 assert!(!is_monetary("1900", &fa));
819
820 assert!(is_monetary("2000", &fa));
822 assert!(is_monetary("2100", &fa));
823 assert!(is_monetary("2500", &fa));
824 assert!(is_monetary("2900", &fa));
825
826 assert!(!is_monetary("3000", &fa));
828 assert!(!is_monetary("3100", &fa));
829 assert!(!is_monetary("3200", &fa));
830 assert!(!is_monetary("3900", &fa));
831
832 assert!(is_monetary("4000", &fa));
834 assert!(is_monetary("4500", &fa));
835
836 assert!(is_monetary("5000", &fa));
838 assert!(is_monetary("6000", &fa));
839
840 assert!(is_monetary("1", &fa));
843 }
844
845 #[test]
846 fn test_is_monetary_french_gaap() {
847 let fa = FrameworkAccounts::french_gaap();
848
849 assert!(is_monetary("512000", &fa));
851
852 assert!(is_monetary("411000", &fa));
855
856 assert!(is_monetary("210000", &fa));
859
860 assert!(is_monetary("401000", &fa));
862
863 assert!(!is_monetary("101000", &fa));
865
866 assert!(is_monetary("701000", &fa));
868
869 assert!(is_monetary("603000", &fa));
871 }
872
873 #[test]
874 fn test_historical_equity_rates() {
875 let mut translator = CurrencyTranslator::new(CurrencyTranslatorConfig::default());
876
877 assert!(translator.historical_equity_rates.is_empty());
879
880 let mut rates = HashMap::new();
882 rates.insert("3100".to_string(), dec!(1.05));
883 rates.insert("3200".to_string(), dec!(0.98));
884 translator.set_historical_equity_rates(rates);
885
886 assert_eq!(translator.historical_equity_rates.len(), 2);
887 assert_eq!(
888 translator.historical_equity_rates.get("3100"),
889 Some(&dec!(1.05))
890 );
891 assert_eq!(
892 translator.historical_equity_rates.get("3200"),
893 Some(&dec!(0.98))
894 );
895
896 let rate = translator.determine_rate(
898 "3100",
899 &TranslationAccountType::CommonStock,
900 dec!(1.10),
901 dec!(1.08),
902 &HashMap::new(),
903 );
904 assert_eq!(rate, dec!(1.05));
905 }
906
907 #[test]
908 fn test_temporal_method_monetary_vs_non_monetary() {
909 let config = CurrencyTranslatorConfig {
910 method: TranslationMethod::Temporal,
911 ..CurrencyTranslatorConfig::default()
912 };
913 let translator = CurrencyTranslator::new(config);
914
915 let closing_rate = dec!(1.10);
916 let average_rate = dec!(1.08);
917 let mut historical_rates = HashMap::new();
918 historical_rates.insert("1500".to_string(), dec!(1.02)); let rate = translator.determine_rate(
922 "1000",
923 &TranslationAccountType::Asset,
924 closing_rate,
925 average_rate,
926 &historical_rates,
927 );
928 assert_eq!(rate, closing_rate);
929
930 let rate = translator.determine_rate(
932 "1100",
933 &TranslationAccountType::Asset,
934 closing_rate,
935 average_rate,
936 &historical_rates,
937 );
938 assert_eq!(rate, closing_rate);
939
940 let rate = translator.determine_rate(
942 "1500",
943 &TranslationAccountType::Asset,
944 closing_rate,
945 average_rate,
946 &historical_rates,
947 );
948 assert_eq!(rate, dec!(1.02));
949
950 let rate = translator.determine_rate(
952 "1600",
953 &TranslationAccountType::Asset,
954 closing_rate,
955 average_rate,
956 &historical_rates,
957 );
958 assert_eq!(rate, closing_rate);
959
960 let rate = translator.determine_rate(
962 "2000",
963 &TranslationAccountType::Liability,
964 closing_rate,
965 average_rate,
966 &historical_rates,
967 );
968 assert_eq!(rate, closing_rate);
969
970 let rate = translator.determine_rate(
972 "4000",
973 &TranslationAccountType::Revenue,
974 closing_rate,
975 average_rate,
976 &historical_rates,
977 );
978 assert_eq!(rate, average_rate);
979
980 let rate = translator.determine_rate(
982 "5000",
983 &TranslationAccountType::Expense,
984 closing_rate,
985 average_rate,
986 &historical_rates,
987 );
988 assert_eq!(rate, average_rate);
989
990 let mut translator_with_equity = CurrencyTranslator::new(CurrencyTranslatorConfig {
992 method: TranslationMethod::Temporal,
993 ..CurrencyTranslatorConfig::default()
994 });
995 let mut equity_rates = HashMap::new();
996 equity_rates.insert("3100".to_string(), dec!(0.95));
997 translator_with_equity.set_historical_equity_rates(equity_rates);
998
999 let rate = translator_with_equity.determine_rate(
1000 "3100",
1001 &TranslationAccountType::CommonStock,
1002 closing_rate,
1003 average_rate,
1004 &historical_rates,
1005 );
1006 assert_eq!(rate, dec!(0.95));
1007 }
1008
1009 #[test]
1010 fn test_monetary_non_monetary_method() {
1011 let config = CurrencyTranslatorConfig {
1012 method: TranslationMethod::MonetaryNonMonetary,
1013 ..CurrencyTranslatorConfig::default()
1014 };
1015 let mut translator = CurrencyTranslator::new(config);
1016
1017 let closing_rate = dec!(1.10);
1018 let average_rate = dec!(1.08);
1019 let mut historical_rates = HashMap::new();
1020 historical_rates.insert("1500".to_string(), dec!(1.02));
1021 historical_rates.insert("1600".to_string(), dec!(0.99));
1022
1023 let mut equity_rates = HashMap::new();
1025 equity_rates.insert("3100".to_string(), dec!(1.05));
1026 equity_rates.insert("3300".to_string(), dec!(1.03));
1027 translator.set_historical_equity_rates(equity_rates);
1028
1029 let rate = translator.determine_rate(
1031 "1000",
1032 &TranslationAccountType::Asset,
1033 closing_rate,
1034 average_rate,
1035 &historical_rates,
1036 );
1037 assert_eq!(rate, closing_rate);
1038
1039 let rate = translator.determine_rate(
1041 "1100",
1042 &TranslationAccountType::Asset,
1043 closing_rate,
1044 average_rate,
1045 &historical_rates,
1046 );
1047 assert_eq!(rate, closing_rate);
1048
1049 let rate = translator.determine_rate(
1051 "1500",
1052 &TranslationAccountType::Asset,
1053 closing_rate,
1054 average_rate,
1055 &historical_rates,
1056 );
1057 assert_eq!(rate, dec!(1.02));
1058
1059 let rate = translator.determine_rate(
1061 "1600",
1062 &TranslationAccountType::Asset,
1063 closing_rate,
1064 average_rate,
1065 &historical_rates,
1066 );
1067 assert_eq!(rate, dec!(0.99));
1068
1069 let rate = translator.determine_rate(
1071 "2000",
1072 &TranslationAccountType::Liability,
1073 closing_rate,
1074 average_rate,
1075 &historical_rates,
1076 );
1077 assert_eq!(rate, closing_rate);
1078
1079 let rate = translator.determine_rate(
1081 "3100",
1082 &TranslationAccountType::CommonStock,
1083 closing_rate,
1084 average_rate,
1085 &historical_rates,
1086 );
1087 assert_eq!(rate, dec!(1.05));
1088
1089 let rate = translator.determine_rate(
1091 "3300",
1092 &TranslationAccountType::RetainedEarnings,
1093 closing_rate,
1094 average_rate,
1095 &historical_rates,
1096 );
1097 assert_eq!(rate, dec!(1.03));
1098
1099 let rate = translator.determine_rate(
1101 "4000",
1102 &TranslationAccountType::Revenue,
1103 closing_rate,
1104 average_rate,
1105 &historical_rates,
1106 );
1107 assert_eq!(rate, closing_rate);
1108
1109 let rate = translator.determine_rate(
1111 "5000",
1112 &TranslationAccountType::Expense,
1113 closing_rate,
1114 average_rate,
1115 &historical_rates,
1116 );
1117 assert_eq!(rate, closing_rate);
1118 }
1119}