fix: correct multi-currency amounts in Journal Entry creation

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 <noreply@anthropic.com>
This commit is contained in:
Ali 2026-03-16 15:10:27 +04:00
parent f063c5cd0c
commit 62bc550186
2 changed files with 104 additions and 60 deletions

View File

@ -1634,6 +1634,9 @@ def _import_one_transaction(txn_data, settings, purpose_rules, purpose_threshold
} }
if doc_type == "Journal Entry": if doc_type == "Journal Entry":
import erpnext
from erpnext.setup.utils import get_exchange_rate
# Create Journal Entry # Create Journal Entry
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = "Bank 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_no = ref_no
je.cheque_date = parsed_date je.cheque_date = parsed_date
je.user_remark = purpose 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 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 can only be set on Receivable/Payable accounts
party_accounts = {"Receivable", "Payable"} party_accounts = {"Receivable", "Payable"}
pf_type = frappe.get_cached_value("Account", paid_from, "account_type") or "" pf_type = pf_account.get("account_type") or ""
pt_type = frappe.get_cached_value("Account", paid_to, "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) 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) pt_party = (party_type, erp_party) if pt_type in party_accounts else (None, None)
@ -1656,32 +1699,21 @@ def _import_one_transaction(txn_data, settings, purpose_rules, purpose_threshold
if cost_center: if cost_center:
je_row_extra["cost_center"] = cost_center je_row_extra["cost_center"] = cost_center
if payment_type == "Pay": # paid_from is always credited, paid_to is always debited.
je.append("accounts", { je.append("accounts", {
"account": paid_from, "account": paid_from,
"credit_in_account_currency": amount, "exchange_rate": pf_rate,
"credit_in_account_currency": pf_amount,
"debit_in_account_currency": 0,
"party_type": pf_party[0], "party_type": pf_party[0],
"party": pf_party[1], "party": pf_party[1],
**je_row_extra, **je_row_extra,
}) })
je.append("accounts", { je.append("accounts", {
"account": paid_to, "account": paid_to,
"debit_in_account_currency": amount, "exchange_rate": pt_rate,
"party_type": pt_party[0], "debit_in_account_currency": pt_amount,
"party": pt_party[1], "credit_in_account_currency": 0,
**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_type": pt_party[0],
"party": pt_party[1], "party": pt_party[1],
**je_row_extra, **je_row_extra,

View File

@ -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 # Auto-enable multi_currency if any account is in a foreign currency
is_multi = from_currency != company_currency or to_currency != company_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) 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: # Exchange rates: account_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
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) 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) 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) # 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:
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 = frappe.new_doc("Journal Entry")
je.voucher_type = "Bank 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: if is_multi:
je.multi_currency = 1 je.multi_currency = 1
# Pay: paid_from credit, paid_to debit # paid_from is always credited, paid_to is always debited.
# Receive: paid_from debit, paid_to credit # 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. # Only set *_in_account_currency + exchange_rate; Frappe computes debit/credit itself.
if is_pay: if is_pay:
from_row = { from_row = {