refactor(bank-integration): registries only via "Load Data"; resolve party at reconcile time
The Excel import dialog no longer has the "Also load counterparties & purposes" checkbox — BT import now only creates Bank Transactions. Registries (Bank Integration Customer/Supplier/Purpose) are populated exclusively through the "Load Data" button on the Bank Integration form. Since BTs are now always imported without a party, Create & Reconcile resolves it: a BT's bank_party_name (the counterparty text from the file) is looked up in the customer/supplier mappings, and if that counterparty is mapped to an ERPNext party, the txn gets that party — so counterparty-based transaction-mapping rules match and the created PE/JE carries the party. A safety guard: a party is only attached to a Payment Entry when one of the GL accounts is Receivable/Payable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42cac5a006
commit
fcdfffbcf0
|
|
@ -48,6 +48,40 @@ def _extract_messages(message_log):
|
|||
return " | ".join(p for p in parts if p)
|
||||
|
||||
|
||||
def _build_name_to_party(settings):
|
||||
"""Counterparty text -> {"Customer": erp_customer} / {"Supplier": erp_supplier},
|
||||
built from the mapped rows of customer_mappings / supplier_mappings."""
|
||||
idx = {}
|
||||
for row in settings.customer_mappings:
|
||||
if not row.erp_customer or not row.bi_customer_name:
|
||||
continue
|
||||
cust_name = frappe.db.get_value("Bank Integration Customer", row.bi_customer_name, "customer_name")
|
||||
if cust_name:
|
||||
idx.setdefault(cust_name.strip(), {})["Customer"] = row.erp_customer
|
||||
for row in settings.supplier_mappings:
|
||||
if not row.erp_supplier or not row.bi_supplier_name:
|
||||
continue
|
||||
supp_name = frappe.db.get_value("Bank Integration Supplier", row.bi_supplier_name, "supplier_name")
|
||||
if supp_name:
|
||||
idx.setdefault(supp_name.strip(), {})["Supplier"] = row.erp_supplier
|
||||
return idx
|
||||
|
||||
|
||||
def _resolve_party_from_name(name, name_to_party, payment_type):
|
||||
"""(party_type, erp_party) for `name`, or (None, None). Prefers Customer for
|
||||
incoming (Receive) and Supplier for outgoing (Pay), falling back to the other."""
|
||||
cands = name_to_party.get((name or "").strip())
|
||||
if not cands:
|
||||
return None, None
|
||||
preferred = "Customer" if payment_type == "Receive" else "Supplier"
|
||||
other = "Supplier" if preferred == "Customer" else "Customer"
|
||||
if preferred in cands:
|
||||
return preferred, cands[preferred]
|
||||
if other in cands:
|
||||
return other, cands[other]
|
||||
return None, None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_purpose_mappings(transactions, bank_integration, paid_from=None, paid_to=None,
|
||||
document_type="Payment Entry", mode="Both"):
|
||||
|
|
@ -67,6 +101,20 @@ def create_purpose_mappings(transactions, bank_integration, paid_from=None, paid
|
|||
|
||||
settings = frappe.get_doc("Bank Integration", bank_integration)
|
||||
|
||||
# Bank Transactions are imported without a party (the mapping is what tells
|
||||
# us who they are). Resolve it now, from the customer/supplier mappings, so
|
||||
# both counterparty-based rule matching and the created PE/JE pick it up.
|
||||
name_to_party = _build_name_to_party(settings)
|
||||
for txn in txn_list:
|
||||
if txn.get("party") or not txn.get("bank_transaction_name"):
|
||||
continue
|
||||
bp_name = frappe.db.get_value("Bank Transaction", txn["bank_transaction_name"], "bank_party_name")
|
||||
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||
pt, ep = _resolve_party_from_name(bp_name, name_to_party, payment_type)
|
||||
if pt and ep:
|
||||
txn["party_type"] = pt
|
||||
txn["party"] = ep
|
||||
|
||||
if mode in ("Both", "Mappings Only"):
|
||||
existing_purpose = {
|
||||
(row.purpose_keyword, row.payment_type, (row.currency or "").upper())
|
||||
|
|
@ -457,9 +505,14 @@ def _create_payment_entry_for_brt(txn, paid_from, paid_to, bank_txn):
|
|||
pe.reference_no = bank_txn.reference_number or txn.get("reference_number") or ""
|
||||
pe.reference_date = posting_date
|
||||
pe.remarks = bank_txn.description or txn.get("purpose") or ""
|
||||
# Only attach a party if one of the GL accounts is a party account —
|
||||
# otherwise ERPNext rejects the PE ("Party Account ... is required").
|
||||
if party_type and party:
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pf_type = frappe.get_cached_value("Account", paid_from, "account_type") or ""
|
||||
pt_type = frappe.get_cached_value("Account", paid_to, "account_type") or ""
|
||||
if pf_type in ("Receivable", "Payable") or pt_type in ("Receivable", "Payable"):
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
|
||||
frappe.local.message_log = []
|
||||
pe.insert(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1):
|
||||
def import_bulk_bt(txn_list, bank_integration, bank_account):
|
||||
"""Enqueue bulk Bank Transaction creation as a background job.
|
||||
|
||||
load_registries: when truthy, also upsert counterparties (→ Bank Integration
|
||||
Customer/Supplier by direction) and purpose keywords (→ Bank Integration
|
||||
Purpose) with status=New, exactly as before. When falsy, only Bank
|
||||
Transactions are created.
|
||||
Creates Bank Transactions only — registries (Bank Integration Customer /
|
||||
Supplier / Purpose) are populated separately via the "Load Data" button on
|
||||
the Bank Integration form. Party is resolved later, at "Create & Reconcile"
|
||||
time, from the mappings.
|
||||
"""
|
||||
if isinstance(txn_list, str):
|
||||
txn_list = json.loads(txn_list)
|
||||
|
|
@ -67,7 +67,6 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1):
|
|||
bank_integration=bank_integration,
|
||||
bank_account=bank_account,
|
||||
user=user,
|
||||
load_registries=cint(load_registries),
|
||||
queue="default",
|
||||
timeout=600,
|
||||
)
|
||||
|
|
@ -75,8 +74,8 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1):
|
|||
return {"success": True, "enqueued": True, "total": len(txn_list)}
|
||||
|
||||
|
||||
def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load_registries=1):
|
||||
"""Background job: create BT (+ optionally upsert registry rows) for each transaction."""
|
||||
def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user):
|
||||
"""Background job: create one Bank Transaction per row."""
|
||||
frappe.set_user(user)
|
||||
|
||||
bi = frappe.get_doc("Bank Integration", bank_integration)
|
||||
|
|
@ -91,9 +90,6 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load
|
|||
total = len(txn_list)
|
||||
imported_count = 0
|
||||
errors = []
|
||||
new_customers = 0
|
||||
new_suppliers = 0
|
||||
new_purposes = 0
|
||||
|
||||
for idx, txn in enumerate(txn_list):
|
||||
frappe.publish_realtime(
|
||||
|
|
@ -104,24 +100,8 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load
|
|||
|
||||
try:
|
||||
counterparty = (txn.get("counterparty") or "").strip()
|
||||
contr_voen = (txn.get("contr_voen") or "").strip()
|
||||
contr_iban = (txn.get("contr_iban") or "").strip()
|
||||
drcr = (txn.get("drcr") or "D").upper()
|
||||
purpose = (txn.get("purpose") or "").strip()
|
||||
|
||||
if load_registries:
|
||||
if counterparty:
|
||||
if drcr == "C":
|
||||
if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration):
|
||||
new_customers += 1
|
||||
else:
|
||||
if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration):
|
||||
new_suppliers += 1
|
||||
|
||||
if purpose:
|
||||
if _upsert_purpose(purpose, drcr, bank_integration):
|
||||
new_purposes += 1
|
||||
|
||||
ref_no = (txn.get("ref_no") or "").strip()
|
||||
amount = abs(flt(txn.get("amount", 0)))
|
||||
|
||||
|
|
@ -163,21 +143,10 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load
|
|||
|
||||
frappe.publish_realtime(
|
||||
"bi_bt_import_complete",
|
||||
{
|
||||
"total": total,
|
||||
"imported": imported_count,
|
||||
"errors": errors,
|
||||
"new_customers": new_customers,
|
||||
"new_suppliers": new_suppliers,
|
||||
"new_purposes": new_purposes,
|
||||
},
|
||||
{"total": total, "imported": imported_count, "errors": errors},
|
||||
user=user,
|
||||
)
|
||||
frappe.log_error(
|
||||
f"BI BT import done: {imported_count}/{total}, "
|
||||
f"+{new_customers} customers / +{new_suppliers} suppliers / +{new_purposes} purposes",
|
||||
"BI BT Import Complete",
|
||||
)
|
||||
frappe.log_error(f"BI BT import done: {imported_count}/{total}", "BI BT Import Complete")
|
||||
|
||||
|
||||
def _upsert_customer(name, voen, iban, bank_integration):
|
||||
|
|
|
|||
|
|
@ -84,13 +84,6 @@ const BIExcelImport = {
|
|||
label: __('Excel File'),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'load_registries',
|
||||
fieldtype: 'Check',
|
||||
label: __('Also load counterparties & purposes'),
|
||||
default: 1,
|
||||
description: __('Extract unique counterparties and purpose keywords from the file into the Bank Integration registries (status: New).'),
|
||||
},
|
||||
],
|
||||
primary_action_label: __('Parse'),
|
||||
primary_action(values) {
|
||||
|
|
@ -221,13 +214,9 @@ const BIExcelImport = {
|
|||
|
||||
const ok = d.imported || 0;
|
||||
const errCount = (d.errors || []).length;
|
||||
const newC = d.new_customers || 0;
|
||||
const newS = d.new_suppliers || 0;
|
||||
const newP = d.new_purposes || 0;
|
||||
|
||||
let msg = '<div>' + __('Imported: <b>{0}</b>', [ok]) + '</div>';
|
||||
msg += '<div>' + __('Errors: <b>{0}</b>', [errCount]) + '</div>';
|
||||
msg += '<div>' + __('New unmapped — customers: {0}, suppliers: {1}, purposes: {2}', [newC, newS, newP]) + '</div>';
|
||||
|
||||
const indicator = errCount === 0 ? 'green' : (ok > 0 ? 'orange' : 'red');
|
||||
const title = errCount === 0
|
||||
|
|
@ -258,7 +247,6 @@ const BIExcelImport = {
|
|||
txn_list: JSON.stringify(selected),
|
||||
bank_integration: values.bank_integration,
|
||||
bank_account: values.bank_account,
|
||||
load_registries: values.load_registries ? 1 : 0,
|
||||
},
|
||||
error() {
|
||||
frappe.realtime.off('bi_bt_import_progress');
|
||||
|
|
|
|||
Loading…
Reference in New Issue