use rust_decimal::Decimal;
use datasynth_core::models::balance::GeneratedOpeningBalance;
use datasynth_core::models::journal_entry::{
BusinessProcess, JournalEntry, JournalEntryHeader, JournalEntryLine, TransactionSource,
};
use datasynth_core::models::ChartOfAccounts;
pub fn opening_balance_to_jes(
ob: &GeneratedOpeningBalance,
coa: &ChartOfAccounts,
) -> Vec<JournalEntry> {
if ob.balances.is_empty() {
return Vec::new();
}
let mut header = JournalEntryHeader::new(ob.company_code.clone(), ob.as_of_date);
header.document_type = "OPENING_BALANCE".to_string();
header.created_by = "SYSTEM".to_string();
header.source = TransactionSource::Automated;
header.business_process = Some(BusinessProcess::R2R);
header.header_text = Some(format!("Opening balance as of {}", ob.as_of_date));
let document_id = header.document_id;
let mut je = JournalEntry::new(header);
let mut accounts: Vec<(&String, &Decimal)> = ob.balances.iter().collect();
accounts.sort_by_key(|(code, _)| code.as_str());
let mut line_number: u32 = 1;
for (account_code, &amount) in &accounts {
if amount == Decimal::ZERO {
continue;
}
let is_debit_normal = resolve_debit_normal(account_code, coa);
let line = if is_debit_normal {
JournalEntryLine::debit(document_id, line_number, account_code.to_string(), amount)
} else {
JournalEntryLine::credit(document_id, line_number, account_code.to_string(), amount)
};
je.add_line(line);
line_number += 1;
}
if je.lines.is_empty() {
return Vec::new();
}
let diff = je.total_debit() - je.total_credit();
if diff != Decimal::ZERO {
let plug_line = if diff > Decimal::ZERO {
JournalEntryLine::credit(
document_id,
line_number,
"3100".to_string(), diff,
)
} else {
JournalEntryLine::debit(document_id, line_number, "3100".to_string(), diff.abs())
};
je.add_line(plug_line);
}
vec![je]
}
fn resolve_debit_normal(account_code: &str, coa: &ChartOfAccounts) -> bool {
if let Some(gl_account) = coa.get_account(account_code) {
return gl_account.normal_debit_balance;
}
match account_code.chars().next().unwrap_or('1') {
'1' => true, '2' | '3' => false, '4' => false, '5' | '6' | '7' | '8' => true, _ => true, }
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use chrono::NaiveDate;
use datasynth_core::models::balance::CalculatedRatios;
use datasynth_core::models::{
AccountSubType, AccountType, ChartOfAccounts, CoAComplexity, GLAccount, IndustrySector,
};
use rust_decimal_macros::dec;
use std::collections::HashMap;
fn make_coa() -> ChartOfAccounts {
let mut coa = ChartOfAccounts::new(
"TEST".to_string(),
"Test CoA".to_string(),
"US".to_string(),
IndustrySector::Manufacturing,
CoAComplexity::Small,
);
coa.add_account(GLAccount::new(
"100000".to_string(),
"Cash".to_string(),
AccountType::Asset,
AccountSubType::Cash,
));
coa.add_account(GLAccount::new(
"199000".to_string(),
"Accumulated Depreciation".to_string(),
AccountType::Asset,
AccountSubType::AccumulatedDepreciation,
));
coa.add_account(GLAccount::new(
"200000".to_string(),
"Accounts Payable".to_string(),
AccountType::Liability,
AccountSubType::AccountsPayable,
));
coa.add_account(GLAccount::new(
"300000".to_string(),
"Retained Earnings".to_string(),
AccountType::Equity,
AccountSubType::RetainedEarnings,
));
coa
}
fn make_ob(balances: HashMap<String, Decimal>) -> GeneratedOpeningBalance {
GeneratedOpeningBalance {
company_code: "1000".to_string(),
as_of_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
balances,
total_assets: dec!(0),
total_liabilities: dec!(0),
total_equity: dec!(0),
is_balanced: true,
calculated_ratios: CalculatedRatios {
current_ratio: None,
quick_ratio: None,
debt_to_equity: None,
working_capital: dec!(0),
},
}
}
#[test]
fn test_empty_balances_returns_no_jes() {
let coa = make_coa();
let ob = make_ob(HashMap::new());
let jes = opening_balance_to_jes(&ob, &coa);
assert!(jes.is_empty());
}
#[test]
fn test_zero_balance_accounts_are_skipped() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("100000".to_string(), dec!(0));
let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
assert!(jes.is_empty());
}
#[test]
fn test_asset_gets_debit_line() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("100000".to_string(), dec!(500));
balances.insert("300000".to_string(), dec!(500));
let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
assert_eq!(jes.len(), 1);
let je = &jes[0];
let cash_line = je
.lines
.iter()
.find(|l| l.gl_account == "100000")
.expect("cash line missing");
assert_eq!(cash_line.debit_amount, dec!(500));
assert_eq!(cash_line.credit_amount, dec!(0));
}
#[test]
fn test_liability_gets_credit_line() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("100000".to_string(), dec!(600));
balances.insert("200000".to_string(), dec!(100));
balances.insert("300000".to_string(), dec!(500));
let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
assert_eq!(jes.len(), 1);
let je = &jes[0];
let ap_line = je
.lines
.iter()
.find(|l| l.gl_account == "200000")
.expect("AP line missing");
assert_eq!(ap_line.credit_amount, dec!(100));
assert_eq!(ap_line.debit_amount, dec!(0));
}
#[test]
fn test_accumulated_depreciation_gets_credit_line() {
let coa = make_coa();
let acc_dep = coa.get_account("199000").unwrap();
assert!(acc_dep.normal_debit_balance); }
#[test]
fn test_fallback_heuristic_asset() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("150000".to_string(), dec!(1000));
balances.insert("300000".to_string(), dec!(1000)); let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
let je = &jes[0];
let line = je
.lines
.iter()
.find(|l| l.gl_account == "150000")
.expect("150000 line missing");
assert_eq!(line.debit_amount, dec!(1000));
}
#[test]
fn test_fallback_heuristic_liability() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("100000".to_string(), dec!(1000)); balances.insert("250000".to_string(), dec!(1000)); let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
let je = &jes[0];
let line = je
.lines
.iter()
.find(|l| l.gl_account == "250000")
.expect("250000 line missing");
assert_eq!(line.credit_amount, dec!(1000));
}
#[test]
fn test_header_document_type_and_created_by() {
let coa = make_coa();
let mut balances = HashMap::new();
balances.insert("100000".to_string(), dec!(100));
balances.insert("300000".to_string(), dec!(100));
let ob = make_ob(balances);
let jes = opening_balance_to_jes(&ob, &coa);
let header = &jes[0].header;
assert_eq!(header.document_type, "OPENING_BALANCE");
assert_eq!(header.created_by, "SYSTEM");
assert_eq!(header.company_code, "1000");
}
}