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:
parent
f063c5cd0c
commit
62bc550186
|
|
@ -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,32 +1699,21 @@ 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":
|
||||
# paid_from is always credited, paid_to is always debited.
|
||||
je.append("accounts", {
|
||||
"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": 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,
|
||||
"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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
else:
|
||||
# Both same currency OR both foreign (rare)
|
||||
# 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)
|
||||
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.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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue