From 62bc550186e779bf9b80a8fa6c52c3d66ad4da6a Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Mon, 16 Mar 2026 15:10:27 +0400 Subject: [PATCH] fix: correct multi-currency amounts in Journal Entry creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ad-hoc if/elif branches with a unified approach: 1. Read txn_currency from the transaction data 2. Convert amount → company currency via txn_rate 3. Derive each account's amount in its own currency 4. Recompute exact exchange rates from rounded amounts to keep Frappe's debit == credit in company currency Fixes the bug where a $40 debit was recorded as ~$25 debit and 40 AZN credit due to dividing instead of multiplying when the transaction currency matched the foreign account. Applies to both bank_api.py (_import_one_transaction JE path) and mapping.py (_create_journal_entry_for_brt). Co-Authored-By: Claude Sonnet 4.6 --- kapital_bank/bank_api.py | 98 ++++++++++++++++++++++++++-------------- kapital_bank/mapping.py | 66 ++++++++++++++++----------- 2 files changed, 104 insertions(+), 60 deletions(-) diff --git a/kapital_bank/bank_api.py b/kapital_bank/bank_api.py index 7e14261..13c84bd 100644 --- a/kapital_bank/bank_api.py +++ b/kapital_bank/bank_api.py @@ -1634,6 +1634,9 @@ def _import_one_transaction(txn_data, settings, purpose_rules, purpose_threshold } if doc_type == "Journal Entry": + import erpnext + from erpnext.setup.utils import get_exchange_rate + # Create Journal Entry je = frappe.new_doc("Journal Entry") je.voucher_type = "Bank Entry" @@ -1642,13 +1645,53 @@ def _import_one_transaction(txn_data, settings, purpose_rules, purpose_threshold je.cheque_no = ref_no je.cheque_date = parsed_date je.user_remark = purpose - if multi_currency: + + # Compute per-account amounts with proper multi-currency handling + currency_precision = cint(frappe.db.get_single_value("System Settings", "currency_precision") or 2) + company_currency = erpnext.get_company_currency(settings.default_company) + txn_currency = (txn_data.get("currency") or company_currency).strip() + + pf_account = frappe.get_cached_value("Account", paid_from, ["account_type", "account_currency"], as_dict=True) or {} + pt_account = frappe.get_cached_value("Account", paid_to, ["account_type", "account_currency"], as_dict=True) or {} + pf_currency = pf_account.get("account_currency") or company_currency + pt_currency = pt_account.get("account_currency") or company_currency + + is_multi = pf_currency != company_currency or pt_currency != company_currency + if is_multi or multi_currency: je.multi_currency = 1 + pf_rate = 1 if pf_currency == company_currency else get_exchange_rate(pf_currency, company_currency, parsed_date) + pt_rate = 1 if pt_currency == company_currency else get_exchange_rate(pt_currency, company_currency, parsed_date) + + if txn_currency == pf_currency: + txn_rate = pf_rate + elif txn_currency == pt_currency: + txn_rate = pt_rate + elif txn_currency == company_currency: + txn_rate = 1 + else: + txn_rate = get_exchange_rate(txn_currency, company_currency, parsed_date) + + amount_in_company = flt(amount * txn_rate, currency_precision) + + pf_amount = amount if pf_currency == txn_currency else ( + amount_in_company if pf_currency == company_currency else + flt(amount_in_company / pf_rate, currency_precision) if pf_rate else amount_in_company + ) + pt_amount = amount if pt_currency == txn_currency else ( + amount_in_company if pt_currency == company_currency else + flt(amount_in_company / pt_rate, currency_precision) if pt_rate else amount_in_company + ) + + if pf_currency != company_currency and pf_amount: + pf_rate = amount_in_company / pf_amount + if pt_currency != company_currency and pt_amount: + pt_rate = amount_in_company / pt_amount + # Party can only be set on Receivable/Payable accounts party_accounts = {"Receivable", "Payable"} - pf_type = frappe.get_cached_value("Account", paid_from, "account_type") or "" - pt_type = frappe.get_cached_value("Account", paid_to, "account_type") or "" + pf_type = pf_account.get("account_type") or "" + pt_type = pt_account.get("account_type") or "" pf_party = (party_type, erp_party) if pf_type in party_accounts else (None, None) pt_party = (party_type, erp_party) if pt_type in party_accounts else (None, None) @@ -1656,36 +1699,25 @@ def _import_one_transaction(txn_data, settings, purpose_rules, purpose_threshold if cost_center: je_row_extra["cost_center"] = cost_center - if payment_type == "Pay": - je.append("accounts", { - "account": paid_from, - "credit_in_account_currency": amount, - "party_type": pf_party[0], - "party": pf_party[1], - **je_row_extra, - }) - je.append("accounts", { - "account": paid_to, - "debit_in_account_currency": amount, - "party_type": pt_party[0], - "party": pt_party[1], - **je_row_extra, - }) - else: - je.append("accounts", { - "account": paid_from, - "credit_in_account_currency": amount, - "party_type": pf_party[0], - "party": pf_party[1], - **je_row_extra, - }) - je.append("accounts", { - "account": paid_to, - "debit_in_account_currency": amount, - "party_type": pt_party[0], - "party": pt_party[1], - **je_row_extra, - }) + # paid_from is always credited, paid_to is always debited. + je.append("accounts", { + "account": paid_from, + "exchange_rate": pf_rate, + "credit_in_account_currency": pf_amount, + "debit_in_account_currency": 0, + "party_type": pf_party[0], + "party": pf_party[1], + **je_row_extra, + }) + je.append("accounts", { + "account": paid_to, + "exchange_rate": pt_rate, + "debit_in_account_currency": pt_amount, + "credit_in_account_currency": 0, + "party_type": pt_party[0], + "party": pt_party[1], + **je_row_extra, + }) try: je.insert(ignore_permissions=True) diff --git a/kapital_bank/mapping.py b/kapital_bank/mapping.py index 693ddd7..2cc7529 100644 --- a/kapital_bank/mapping.py +++ b/kapital_bank/mapping.py @@ -399,34 +399,45 @@ def _create_journal_entry_for_brt(txn, paid_from, paid_to, bank_txn, multi_curre # Auto-enable multi_currency if any account is in a foreign currency is_multi = from_currency != company_currency or to_currency != company_currency - # Frappe rounds debit_in_account_currency to field precision BEFORE multiplying - # by exchange_rate to get debit (company currency). - # Strategy: keep the original amount on the company-currency row, then compute - # an exact exchange rate for the foreign row so that: - # flt(foreign_amount * exact_rate, P) == flt(amount, P) - # This mirrors what Frappe UI does when the user types the AZN amount first. currency_precision = cint(frappe.db.get_single_value("System Settings", "currency_precision") or 2) + txn_currency = (txn.get("currency") or company_currency).strip() - if from_currency == company_currency and to_currency != company_currency: - to_rate = get_exchange_rate(to_currency, company_currency, posting_date) - to_amount = flt(amount / to_rate, currency_precision) - exact_to_rate = (amount / to_amount) if to_amount else to_rate - from_rate = 1 - from_amount = amount - to_rate = exact_to_rate - elif to_currency == company_currency and from_currency != company_currency: - from_rate = get_exchange_rate(from_currency, company_currency, posting_date) - from_amount = flt(amount / from_rate, currency_precision) - exact_from_rate = (amount / from_amount) if from_amount else from_rate - to_rate = 1 - to_amount = amount - from_rate = exact_from_rate + # Exchange rates: account_currency → company_currency + from_rate = 1 if from_currency == company_currency else get_exchange_rate(from_currency, company_currency, posting_date) + to_rate = 1 if to_currency == company_currency else get_exchange_rate(to_currency, company_currency, posting_date) + + # Rate for the transaction currency itself + if txn_currency == from_currency: + txn_rate = from_rate + elif txn_currency == to_currency: + txn_rate = to_rate + elif txn_currency == company_currency: + txn_rate = 1 else: - # Both same currency OR both foreign (rare) - from_rate = 1 if from_currency == company_currency else get_exchange_rate(from_currency, company_currency, posting_date) - to_rate = 1 if to_currency == company_currency else get_exchange_rate(to_currency, company_currency, posting_date) - from_amount = flt(amount / from_rate, currency_precision) - to_amount = flt(amount / to_rate, currency_precision) + txn_rate = get_exchange_rate(txn_currency, company_currency, posting_date) + + amount_in_company = flt(amount * txn_rate, currency_precision) + + # Amount in each account's own currency + if from_currency == txn_currency: + from_amount = amount + elif from_currency == company_currency: + from_amount = amount_in_company + else: + from_amount = flt(amount_in_company / from_rate, currency_precision) if from_rate else amount_in_company + + if to_currency == txn_currency: + to_amount = amount + elif to_currency == company_currency: + to_amount = amount_in_company + else: + to_amount = flt(amount_in_company / to_rate, currency_precision) if to_rate else amount_in_company + + # Recompute exact rates from rounded amounts so Frappe's debit == credit in company currency + if from_currency != company_currency and from_amount: + from_rate = amount_in_company / from_amount + if to_currency != company_currency and to_amount: + to_rate = amount_in_company / to_amount je = frappe.new_doc("Journal Entry") je.voucher_type = "Bank Entry" @@ -438,8 +449,9 @@ def _create_journal_entry_for_brt(txn, paid_from, paid_to, bank_txn, multi_curre if is_multi: je.multi_currency = 1 - # Pay: paid_from credit, paid_to debit - # Receive: paid_from debit, paid_to credit + # paid_from is always credited, paid_to is always debited. + # For Pay: paid_from = bank/cash account, paid_to = expense/payable account. + # For Receive: paid_from = income/receivable account, paid_to = bank/cash account. # Only set *_in_account_currency + exchange_rate; Frappe computes debit/credit itself. if is_pay: from_row = {