fix(bank-integration): port kapital_bank's proven BRT dialog into jey_erp
The first jey_erp BRT implementation read row.name from dtm.transactions,
but those rows are arrays (per ERPNext's DataTableManager.format_row), so
the BT name was always undefined ("Could not resolve Bank Transaction
names"). Replaced it with a faithful port of kapital_bank's BRT
extension: the same checkbox column, "Create & Reconcile" toolbar, and
3-mode dialog (Both / Mappings Only / Documents & Reconcile), reading
the BT name from the row array the way kb did.
The dialog gains a "Mapping Source" select (defaulting from the Bank
Account's hidden bank_integration field, written back on submit). For a
Bank Integration source it calls the new
jey_erp.bank_integration.create_reconcile.create_purpose_mappings (a
port of kapital_bank/mapping.py adapted for the multi-record Bank
Integration doctype and Bank Integration Purpose); for a Kapital Bank
Settings source it calls kapital_bank.mapping.create_purpose_mappings
directly, leaving kb's proven backend untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4b1d1b9b6
commit
71e2c06a35
|
|
@ -1,235 +1,411 @@
|
||||||
"""Universal "Create & Reconcile" backend for Bank Reconciliation Tool.
|
"""Universal "Create & Reconcile" backend for Bank Reconciliation Tool.
|
||||||
|
|
||||||
Works for both Bank Integration records and Kapital Bank Settings via mapping_resolver.
|
A faithful port of kapital_bank/mapping.py:create_purpose_mappings, generalised
|
||||||
|
to work with a Bank Integration record. For Bank Accounts mapped to Kapital Bank
|
||||||
|
Settings, the BRT client calls kapital_bank.mapping.create_purpose_mappings
|
||||||
|
directly (kb's proven backend stays untouched); this module handles the new
|
||||||
|
Bank Integration source. Three modes: "Both" / "Mappings Only" /
|
||||||
|
"Documents & Reconcile" — same semantics as kb.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from difflib import SequenceMatcher
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from jey_erp.bank_integration.mapping_resolver import resolve_mappings_for_bank_account
|
PURPOSE_DOCTYPE = "Bank Integration Purpose"
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_messages(message_log):
|
||||||
|
parts = []
|
||||||
|
for m in message_log:
|
||||||
|
if isinstance(m, dict):
|
||||||
|
parts.append(m.get("message") or m.get("msg") or "")
|
||||||
|
else:
|
||||||
|
parts.append(str(m))
|
||||||
|
return " | ".join(p for p in parts if p)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_documents_for_bts(bt_names, bank_account):
|
def create_purpose_mappings(transactions, bank_integration, paid_from=None, paid_to=None,
|
||||||
"""For each Bank Transaction in `bt_names`, find a matching mapping and
|
document_type="Payment Entry", mode="Both"):
|
||||||
create+reconcile a Payment Entry / Journal Entry."""
|
"""Bulk-create transaction mappings and/or documents from selected BRT transactions."""
|
||||||
if isinstance(bt_names, str):
|
try:
|
||||||
bt_names = json.loads(bt_names)
|
txn_list = json.loads(transactions) if isinstance(transactions, str) else transactions
|
||||||
|
|
||||||
if not bt_names:
|
do_mappings = mode == "Mappings Only"
|
||||||
return {"success": True, "created_docs": 0, "reconciled": 0, "errors": []}
|
do_documents = mode in ("Both", "Documents & Reconcile")
|
||||||
|
|
||||||
|
created_mappings = 0
|
||||||
|
already_mapped = 0
|
||||||
|
skipped_no_data = 0
|
||||||
|
created_docs = 0
|
||||||
|
reconciled = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
settings = frappe.get_doc("Bank Integration", bank_integration)
|
||||||
|
|
||||||
|
if mode in ("Both", "Mappings Only"):
|
||||||
|
existing_purpose = {
|
||||||
|
(row.purpose_keyword, row.payment_type, (row.currency or "").upper())
|
||||||
|
for row in settings.transaction_mappings
|
||||||
|
if row.purpose_keyword and row.payment_type
|
||||||
|
}
|
||||||
|
existing_party_only = {
|
||||||
|
(row.counterparty_type or "", row.counterparty or "", row.payment_type, (row.currency or "").upper())
|
||||||
|
for row in settings.transaction_mappings
|
||||||
|
if not row.purpose_keyword and row.counterparty and row.payment_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if do_mappings:
|
||||||
|
unique_purposes = {}
|
||||||
|
unique_parties = {}
|
||||||
|
|
||||||
|
for txn in txn_list:
|
||||||
|
purpose = (txn.get("purpose") or "").strip()
|
||||||
|
party_type = (txn.get("party_type") or "").strip()
|
||||||
|
party = (txn.get("party") or "").strip()
|
||||||
|
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||||
|
|
||||||
|
bt_name = txn.get("bank_transaction_name")
|
||||||
|
txn_currency = (frappe.db.get_value("Bank Transaction", bt_name, "currency") or "").strip().upper() if bt_name else ""
|
||||||
|
|
||||||
|
if not purpose and not party:
|
||||||
|
skipped_no_data += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if purpose:
|
||||||
|
key = (purpose, payment_type, txn_currency)
|
||||||
|
if key not in unique_purposes:
|
||||||
|
unique_purposes[key] = {
|
||||||
|
"count": 0,
|
||||||
|
"counterparty_type": party_type,
|
||||||
|
"counterparty": party,
|
||||||
|
}
|
||||||
|
unique_purposes[key]["count"] += 1
|
||||||
|
else:
|
||||||
|
key = (party_type, party, payment_type, txn_currency)
|
||||||
|
if key not in unique_parties:
|
||||||
|
unique_parties[key] = {
|
||||||
|
"count": 0,
|
||||||
|
"counterparty_type": party_type,
|
||||||
|
"counterparty": party,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
}
|
||||||
|
unique_parties[key]["count"] += 1
|
||||||
|
|
||||||
|
for (purpose_text, payment_type, txn_currency), data in unique_purposes.items():
|
||||||
|
purpose_name = _ensure_purpose(purpose_text, bank_integration)
|
||||||
|
is_foreign = txn_currency and txn_currency != "AZN"
|
||||||
|
|
||||||
|
if (purpose_name, payment_type, txn_currency) in existing_purpose:
|
||||||
|
already_mapped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings.append("transaction_mappings", {
|
||||||
|
"purpose_keyword": purpose_name,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
"paid_from": paid_from,
|
||||||
|
"paid_to": paid_to,
|
||||||
|
"document_type": document_type,
|
||||||
|
"counterparty_type": data["counterparty_type"] or None,
|
||||||
|
"counterparty": data["counterparty"] or None,
|
||||||
|
"currency": txn_currency or None,
|
||||||
|
"multi_currency": 1 if is_foreign else 0,
|
||||||
|
})
|
||||||
|
existing_purpose.add((purpose_name, payment_type, txn_currency))
|
||||||
|
created_mappings += 1
|
||||||
|
|
||||||
|
for (party_type_k, party_k, payment_type, txn_currency), data in unique_parties.items():
|
||||||
|
is_foreign = txn_currency and txn_currency != "AZN"
|
||||||
|
|
||||||
|
if (party_type_k, party_k, payment_type, txn_currency) in existing_party_only:
|
||||||
|
already_mapped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings.append("transaction_mappings", {
|
||||||
|
"purpose_keyword": None,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
"paid_from": paid_from,
|
||||||
|
"paid_to": paid_to,
|
||||||
|
"document_type": document_type,
|
||||||
|
"counterparty_type": data["counterparty_type"] or None,
|
||||||
|
"counterparty": data["counterparty"] or None,
|
||||||
|
"currency": txn_currency or None,
|
||||||
|
"multi_currency": 1 if is_foreign else 0,
|
||||||
|
})
|
||||||
|
existing_party_only.add((party_type_k, party_k, payment_type, txn_currency))
|
||||||
|
created_mappings += 1
|
||||||
|
|
||||||
|
if created_mappings > 0:
|
||||||
|
settings.save(ignore_permissions=True)
|
||||||
|
for (purpose_text, _payment_type, _txn_currency) in unique_purposes:
|
||||||
|
purpose_name = frappe.db.get_value(
|
||||||
|
PURPOSE_DOCTYPE,
|
||||||
|
{"purpose_keyword": purpose_text, "parent_bank_integration": bank_integration},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
if purpose_name:
|
||||||
|
frappe.db.set_value(PURPOSE_DOCTYPE, purpose_name, "status", "Mapped", update_modified=False)
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
if do_documents:
|
||||||
|
if mode == "Documents & Reconcile":
|
||||||
|
purpose_text_cache = {}
|
||||||
|
for row in settings.transaction_mappings:
|
||||||
|
if row.purpose_keyword and row.purpose_keyword not in purpose_text_cache:
|
||||||
|
text = frappe.db.get_value(PURPOSE_DOCTYPE, row.purpose_keyword, "purpose_keyword") or ""
|
||||||
|
purpose_text_cache[row.purpose_keyword] = text.lower()
|
||||||
|
txn_mappings = settings.transaction_mappings
|
||||||
|
else:
|
||||||
|
purpose_text_cache = {}
|
||||||
|
txn_mappings = None
|
||||||
|
|
||||||
|
for txn in txn_list:
|
||||||
|
if not txn.get("bank_transaction_name"):
|
||||||
|
errors.append({
|
||||||
|
"reference_number": txn.get("reference_number") or "",
|
||||||
|
"message": "No bank transaction name",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mode == "Documents & Reconcile":
|
||||||
|
mapping_row = _find_mapping_for_txn(txn, txn_mappings, purpose_text_cache)
|
||||||
|
if not mapping_row:
|
||||||
|
errors.append({
|
||||||
|
"reference_number": txn.get("reference_number") or "",
|
||||||
|
"message": f"No mapping found for purpose: '{txn.get('purpose', '')!s:.60}', party: '{txn.get('party', '')}'",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
txn_paid_from = mapping_row.paid_from
|
||||||
|
txn_paid_to = mapping_row.paid_to
|
||||||
|
txn_doc_type = mapping_row.document_type or "Payment Entry"
|
||||||
|
txn_multi_currency = bool(mapping_row.multi_currency)
|
||||||
|
txn_mapping_currency = (mapping_row.currency or "").strip()
|
||||||
|
else:
|
||||||
|
txn_paid_from = paid_from
|
||||||
|
txn_paid_to = paid_to
|
||||||
|
txn_doc_type = document_type
|
||||||
|
txn_multi_currency = False
|
||||||
|
txn_mapping_currency = ""
|
||||||
|
|
||||||
|
result = _create_and_reconcile_doc(txn, txn_paid_from, txn_paid_to, txn_doc_type, txn_multi_currency, txn_mapping_currency)
|
||||||
|
if result.get("success"):
|
||||||
|
created_docs += 1
|
||||||
|
if result.get("reconciled"):
|
||||||
|
reconciled += 1
|
||||||
|
elif result.get("error"):
|
||||||
|
errors.append({
|
||||||
|
"reference_number": txn.get("reference_number") or "",
|
||||||
|
"message": result.get("error"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if mode == "Both":
|
||||||
|
purpose = (txn.get("purpose") or "").strip()
|
||||||
|
party_type = (txn.get("party_type") or "").strip()
|
||||||
|
party = (txn.get("party") or "").strip()
|
||||||
|
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||||
|
bt_name = txn.get("bank_transaction_name")
|
||||||
|
txn_currency = (frappe.db.get_value("Bank Transaction", bt_name, "currency") or "").strip().upper() if bt_name else ""
|
||||||
|
is_foreign = txn_currency and txn_currency != "AZN"
|
||||||
|
|
||||||
|
if purpose:
|
||||||
|
purpose_name = _ensure_purpose(purpose, bank_integration)
|
||||||
|
key = (purpose_name, payment_type, txn_currency)
|
||||||
|
if key not in existing_purpose:
|
||||||
|
settings.append("transaction_mappings", {
|
||||||
|
"purpose_keyword": purpose_name,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
"paid_from": paid_from,
|
||||||
|
"paid_to": paid_to,
|
||||||
|
"document_type": document_type,
|
||||||
|
"counterparty_type": party_type or None,
|
||||||
|
"counterparty": party or None,
|
||||||
|
"currency": txn_currency or None,
|
||||||
|
"multi_currency": 1 if is_foreign else 0,
|
||||||
|
})
|
||||||
|
existing_purpose.add(key)
|
||||||
|
created_mappings += 1
|
||||||
|
elif party:
|
||||||
|
key = (party_type, party, payment_type, txn_currency)
|
||||||
|
if key not in existing_party_only:
|
||||||
|
settings.append("transaction_mappings", {
|
||||||
|
"purpose_keyword": None,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
"paid_from": paid_from,
|
||||||
|
"paid_to": paid_to,
|
||||||
|
"document_type": document_type,
|
||||||
|
"counterparty_type": party_type or None,
|
||||||
|
"counterparty": party or None,
|
||||||
|
"currency": txn_currency or None,
|
||||||
|
"multi_currency": 1 if is_foreign else 0,
|
||||||
|
})
|
||||||
|
existing_party_only.add(key)
|
||||||
|
created_mappings += 1
|
||||||
|
else:
|
||||||
|
skipped_no_data += 1
|
||||||
|
else:
|
||||||
|
errors.append({
|
||||||
|
"reference_number": txn.get("reference_number") or "",
|
||||||
|
"message": result.get("error") or "Unknown error",
|
||||||
|
})
|
||||||
|
|
||||||
|
if mode == "Both" and created_mappings > 0:
|
||||||
|
settings.save(ignore_permissions=True)
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
mappings = resolve_mappings_for_bank_account(bank_account)
|
|
||||||
if not mappings:
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": True,
|
||||||
"message": _("Bank Account is not linked to any Bank Integration. "
|
"created_mappings": created_mappings,
|
||||||
"Pick one in the Bank Reconciliation Tool first."),
|
"already_mapped": already_mapped,
|
||||||
|
"skipped_no_data": skipped_no_data,
|
||||||
|
"created_docs": created_docs,
|
||||||
|
"reconciled": reconciled,
|
||||||
|
"errors": errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
created_docs = 0
|
except Exception as e:
|
||||||
reconciled = 0
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for bt_name in bt_names:
|
|
||||||
try:
|
|
||||||
result = _process_one(bt_name, mappings)
|
|
||||||
if result.get("success"):
|
|
||||||
created_docs += 1
|
|
||||||
if result.get("reconciled"):
|
|
||||||
reconciled += 1
|
|
||||||
else:
|
|
||||||
errors.append({
|
|
||||||
"bank_transaction": bt_name,
|
|
||||||
"reference_number": result.get("reference_number"),
|
|
||||||
"message": result.get("error") or "Unknown error",
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.local.message_log = []
|
|
||||||
frappe.log_error(
|
|
||||||
f"create_documents_for_bts failed for {bt_name}: {e}\n{frappe.get_traceback()}",
|
|
||||||
"BI Create Reconcile",
|
|
||||||
)
|
|
||||||
errors.append({"bank_transaction": bt_name, "message": str(e)})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"created_docs": created_docs,
|
|
||||||
"reconciled": reconciled,
|
|
||||||
"errors": errors,
|
|
||||||
"total": len(bt_names),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _process_one(bt_name, mappings):
|
|
||||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
|
||||||
|
|
||||||
if (bt.unallocated_amount or 0) == 0:
|
|
||||||
return {"success": False, "reference_number": bt.reference_number,
|
|
||||||
"error": "Bank Transaction already fully reconciled"}
|
|
||||||
|
|
||||||
deposit = flt(bt.deposit or 0)
|
|
||||||
withdrawal = flt(bt.withdrawal or 0)
|
|
||||||
if deposit > 0:
|
|
||||||
drcr = "C"
|
|
||||||
amount = deposit
|
|
||||||
payment_type = "Receive"
|
|
||||||
elif withdrawal > 0:
|
|
||||||
drcr = "D"
|
|
||||||
amount = withdrawal
|
|
||||||
payment_type = "Pay"
|
|
||||||
else:
|
|
||||||
return {"success": False, "reference_number": bt.reference_number,
|
|
||||||
"error": "BT has zero amount"}
|
|
||||||
|
|
||||||
purpose = (bt.description or "").strip()
|
|
||||||
contr_name = (bt.bank_party_name or "").strip()
|
|
||||||
|
|
||||||
# Find party via mappings (voen lookup not used — BT has no VOEN)
|
|
||||||
party_type = None
|
|
||||||
erp_party = None
|
|
||||||
if contr_name and contr_name in mappings.get("name_to_party", {}):
|
|
||||||
entries = mappings["name_to_party"][contr_name]
|
|
||||||
preferred = "Customer" if payment_type == "Receive" else "Supplier"
|
|
||||||
fallback = "Supplier" if preferred == "Customer" else "Customer"
|
|
||||||
entry = entries.get(preferred) or entries.get(fallback)
|
|
||||||
if entry:
|
|
||||||
party_type = entry["party_type"]
|
|
||||||
erp_party = entry["erp_party"]
|
|
||||||
|
|
||||||
# Find a matching transaction mapping
|
|
||||||
rule = _find_rule(mappings.get("purpose_rules", []), purpose, contr_name, payment_type,
|
|
||||||
drcr, mappings.get("similarity_threshold_purpose", 0.7))
|
|
||||||
if not rule:
|
|
||||||
return {"success": False, "reference_number": bt.reference_number,
|
|
||||||
"error": f"No transaction mapping for purpose='{purpose[:60]}', party='{contr_name}'"}
|
|
||||||
|
|
||||||
doc_type = rule.get("document_type") or "Payment Entry"
|
|
||||||
paid_from = rule["paid_from"]
|
|
||||||
paid_to = rule["paid_to"]
|
|
||||||
|
|
||||||
if doc_type == "Journal Entry":
|
|
||||||
create_result = _create_journal_entry(
|
|
||||||
bt, paid_from, paid_to, payment_type, amount,
|
|
||||||
party_type, erp_party, rule.get("cost_center"),
|
|
||||||
rule.get("multi_currency", False),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
create_result = _create_payment_entry(
|
|
||||||
bt, paid_from, paid_to, payment_type, amount,
|
|
||||||
party_type, erp_party, rule.get("cost_center"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not create_result.get("success"):
|
|
||||||
return {"success": False, "reference_number": bt.reference_number,
|
|
||||||
"error": create_result.get("error")}
|
|
||||||
|
|
||||||
doc_name = create_result["doc_name"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
|
||||||
reconcile_vouchers,
|
|
||||||
)
|
|
||||||
reconcile_vouchers(
|
|
||||||
bt_name,
|
|
||||||
json.dumps([{
|
|
||||||
"payment_doctype": doc_type,
|
|
||||||
"payment_name": doc_name,
|
|
||||||
"amount": amount,
|
|
||||||
}]),
|
|
||||||
)
|
|
||||||
return {"success": True, "doc_name": doc_name, "reconciled": True}
|
|
||||||
except Exception as re:
|
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.local.message_log = []
|
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
f"reconcile_vouchers failed for {doc_name}: {re}\n{frappe.get_traceback()}",
|
f"create_purpose_mappings: {e}\n{frappe.get_traceback()}",
|
||||||
"BI Create Reconcile",
|
"BI Create Reconcile",
|
||||||
)
|
)
|
||||||
return {"success": False, "reference_number": bt.reference_number, "error": str(re)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def _find_rule(rules, purpose, party_name, payment_type, drcr, threshold):
|
def _find_mapping_for_txn(txn, transaction_mappings, purpose_text_cache):
|
||||||
"""Return the best matching rule. Priority:
|
"""Find the best matching transaction mapping row for a BRT txn.
|
||||||
purpose+counterparty > purpose-only > counterparty-only > counterparty_type fallback."""
|
|
||||||
purpose_lower = purpose.lower().strip()
|
|
||||||
|
|
||||||
both = []
|
Priority: rows matching both purpose+counterparty > purpose-only > counterparty-only > fallback.
|
||||||
purpose_only = []
|
"""
|
||||||
counterparty_only = []
|
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||||
fallback = []
|
txn_purpose = (txn.get("purpose") or "").lower()
|
||||||
|
txn_party = (txn.get("party") or "").lower()
|
||||||
|
|
||||||
for r in rules:
|
bt_name = txn.get("bank_transaction_name")
|
||||||
if r.get("payment_type") and r["payment_type"] != payment_type:
|
txn_currency = (frappe.db.get_value("Bank Transaction", bt_name, "currency") or "").strip().upper() if bt_name else ""
|
||||||
|
|
||||||
|
both_rules = []
|
||||||
|
purpose_only_rules = []
|
||||||
|
counterparty_only_rules = []
|
||||||
|
fallback_rules = []
|
||||||
|
|
||||||
|
for row in transaction_mappings:
|
||||||
|
if not row.paid_from or not row.paid_to:
|
||||||
|
continue
|
||||||
|
if row.payment_type and row.payment_type != payment_type:
|
||||||
|
continue
|
||||||
|
row_currency = (row.currency or "").strip().upper()
|
||||||
|
if row_currency and row_currency != txn_currency:
|
||||||
continue
|
continue
|
||||||
has_kw = bool(r.get("purpose_keyword"))
|
|
||||||
has_cp = bool(r.get("counterparty_name") or r.get("counterparty_voen"))
|
|
||||||
if has_kw and has_cp:
|
|
||||||
both.append(r)
|
|
||||||
elif has_kw:
|
|
||||||
purpose_only.append(r)
|
|
||||||
elif has_cp:
|
|
||||||
counterparty_only.append(r)
|
|
||||||
elif r.get("counterparty_type"):
|
|
||||||
fallback.append(r)
|
|
||||||
|
|
||||||
def _cp_match(r):
|
has_purpose = bool(row.purpose_keyword)
|
||||||
cn = (r.get("counterparty_name") or "").strip()
|
has_counterparty = bool(row.counterparty)
|
||||||
return cn and cn == party_name
|
|
||||||
|
|
||||||
def _kw_match(r):
|
if has_purpose and has_counterparty:
|
||||||
kw = (r.get("purpose_keyword") or "").strip().lower()
|
both_rules.append(row)
|
||||||
if not kw:
|
elif has_purpose:
|
||||||
return False
|
purpose_only_rules.append(row)
|
||||||
if kw in purpose_lower:
|
elif has_counterparty:
|
||||||
return True
|
counterparty_only_rules.append(row)
|
||||||
# fuzzy
|
elif row.counterparty_type:
|
||||||
score = SequenceMatcher(None, purpose_lower, kw).ratio()
|
fallback_rules.append(row)
|
||||||
return score >= threshold
|
|
||||||
|
|
||||||
for r in both:
|
def _purpose_matches(row):
|
||||||
if _kw_match(r) and _cp_match(r):
|
keyword_text = purpose_text_cache.get(row.purpose_keyword, "")
|
||||||
return r
|
return keyword_text and keyword_text in txn_purpose
|
||||||
|
|
||||||
for r in purpose_only:
|
def _counterparty_matches(row):
|
||||||
if _kw_match(r):
|
return row.counterparty.lower() == txn_party
|
||||||
return r
|
|
||||||
|
|
||||||
for r in counterparty_only:
|
for row in both_rules:
|
||||||
if _cp_match(r):
|
if _purpose_matches(row) and _counterparty_matches(row):
|
||||||
return r
|
return row
|
||||||
|
|
||||||
inferred_type = "Customer" if payment_type == "Receive" else "Supplier"
|
for row in purpose_only_rules:
|
||||||
for r in fallback:
|
if _purpose_matches(row):
|
||||||
if r.get("counterparty_type") == inferred_type:
|
return row
|
||||||
return r
|
|
||||||
|
for row in counterparty_only_rules:
|
||||||
|
if _counterparty_matches(row):
|
||||||
|
return row
|
||||||
|
|
||||||
|
inferred_cp_type = "Customer" if payment_type == "Receive" else "Supplier"
|
||||||
|
for row in fallback_rules:
|
||||||
|
if row.counterparty_type == inferred_cp_type:
|
||||||
|
return row
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _create_payment_entry(bt, paid_from, paid_to, payment_type, amount, party_type, erp_party, cost_center):
|
def _create_and_reconcile_doc(txn, paid_from, paid_to, document_type, multi_currency=False, mapping_currency=""):
|
||||||
|
"""Create a Payment Entry or Journal Entry and reconcile it with the bank transaction."""
|
||||||
|
bank_transaction_name = txn.get("bank_transaction_name")
|
||||||
try:
|
try:
|
||||||
|
bank_txn = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||||
|
|
||||||
|
if document_type == "Journal Entry":
|
||||||
|
result = _create_journal_entry_for_brt(txn, paid_from, paid_to, bank_txn, multi_currency, mapping_currency)
|
||||||
|
else:
|
||||||
|
result = _create_payment_entry_for_brt(txn, paid_from, paid_to, bank_txn)
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
return result
|
||||||
|
|
||||||
|
doc_name = result["doc_name"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||||
|
reconcile_vouchers,
|
||||||
|
)
|
||||||
|
reconcile_vouchers(
|
||||||
|
bank_transaction_name,
|
||||||
|
json.dumps([{
|
||||||
|
"payment_doctype": document_type,
|
||||||
|
"payment_name": doc_name,
|
||||||
|
"amount": abs(float(txn.get("amount") or 0)),
|
||||||
|
}]),
|
||||||
|
)
|
||||||
|
return {"success": True, "doc_name": doc_name, "reconciled": True}
|
||||||
|
except Exception as re:
|
||||||
|
frappe.db.rollback()
|
||||||
|
frappe.local.message_log = []
|
||||||
|
frappe.log_error(
|
||||||
|
f"reconcile_vouchers failed for {doc_name}: {re}\n{frappe.get_traceback()}",
|
||||||
|
"BI Create Reconcile",
|
||||||
|
)
|
||||||
|
return {"success": False, "error": str(re)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.db.rollback()
|
||||||
|
frappe.local.message_log = []
|
||||||
|
frappe.log_error(
|
||||||
|
f"_create_and_reconcile_doc failed for {bank_transaction_name}: {e}\n{frappe.get_traceback()}",
|
||||||
|
"BI Create Reconcile",
|
||||||
|
)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_payment_entry_for_brt(txn, paid_from, paid_to, bank_txn):
|
||||||
|
try:
|
||||||
|
amount = abs(float(txn.get("amount") or 0))
|
||||||
|
party_type = (txn.get("party_type") or "").strip()
|
||||||
|
party = (txn.get("party") or "").strip()
|
||||||
|
posting_date = bank_txn.date or txn.get("date")
|
||||||
|
|
||||||
pe = frappe.new_doc("Payment Entry")
|
pe = frappe.new_doc("Payment Entry")
|
||||||
pe.payment_type = payment_type
|
pe.payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||||
pe.company = bt.company
|
pe.company = bank_txn.company
|
||||||
pe.posting_date = bt.date
|
pe.posting_date = posting_date
|
||||||
pe.paid_from = paid_from
|
pe.paid_from = paid_from
|
||||||
pe.paid_to = paid_to
|
pe.paid_to = paid_to
|
||||||
pe.paid_amount = amount
|
pe.paid_amount = amount
|
||||||
pe.received_amount = amount
|
pe.received_amount = amount
|
||||||
pe.reference_no = bt.reference_number or bt.transaction_id or bt.name
|
pe.reference_no = bank_txn.reference_number or txn.get("reference_number") or ""
|
||||||
pe.reference_date = bt.date
|
pe.reference_date = posting_date
|
||||||
pe.remarks = bt.description or ""
|
pe.remarks = bank_txn.description or txn.get("purpose") or ""
|
||||||
if party_type and erp_party:
|
if party_type and party:
|
||||||
pe.party_type = party_type
|
pe.party_type = party_type
|
||||||
pe.party = erp_party
|
pe.party = party
|
||||||
if cost_center:
|
|
||||||
pe.cost_center = cost_center
|
|
||||||
|
|
||||||
frappe.local.message_log = []
|
frappe.local.message_log = []
|
||||||
pe.insert(ignore_permissions=True)
|
pe.insert(ignore_permissions=True)
|
||||||
|
|
@ -247,42 +423,41 @@ def _create_payment_entry(bt, paid_from, paid_to, payment_type, amount, party_ty
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.local.message_log = []
|
frappe.local.message_log = []
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
f"_create_payment_entry failed: {e}\n{frappe.get_traceback()}",
|
f"_create_payment_entry_for_brt failed: {e}\n{frappe.get_traceback()}",
|
||||||
"BI Create Reconcile",
|
"BI Create Reconcile",
|
||||||
)
|
)
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def _create_journal_entry(bt, paid_from, paid_to, payment_type, amount, party_type, erp_party,
|
def _create_journal_entry_for_brt(txn, paid_from, paid_to, bank_txn, multi_currency=False, mapping_currency=""):
|
||||||
cost_center, force_multi_currency):
|
|
||||||
try:
|
try:
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
is_pay = payment_type == "Pay"
|
amount = abs(float(txn.get("amount") or 0))
|
||||||
posting_date = bt.date
|
party_type = (txn.get("party_type") or "").strip()
|
||||||
ref_no = bt.reference_number or bt.transaction_id or bt.name
|
party = (txn.get("party") or "").strip()
|
||||||
|
is_pay = txn.get("drcr") == "D"
|
||||||
|
posting_date = bank_txn.date or txn.get("date")
|
||||||
|
ref_no = bank_txn.reference_number or txn.get("reference_number") or ""
|
||||||
|
|
||||||
company_currency = erpnext.get_company_currency(bt.company)
|
company_currency = erpnext.get_company_currency(bank_txn.company)
|
||||||
from_account = frappe.get_cached_value(
|
|
||||||
"Account", paid_from, ["account_type", "account_currency"], as_dict=True
|
from_account = frappe.get_cached_value("Account", paid_from, ["account_type", "account_currency"], as_dict=True) or {}
|
||||||
) or {}
|
to_account = frappe.get_cached_value("Account", paid_to, ["account_type", "account_currency"], as_dict=True) or {}
|
||||||
to_account = frappe.get_cached_value(
|
|
||||||
"Account", paid_to, ["account_type", "account_currency"], as_dict=True
|
|
||||||
) or {}
|
|
||||||
paid_from_type = from_account.get("account_type") or ""
|
paid_from_type = from_account.get("account_type") or ""
|
||||||
paid_to_type = to_account.get("account_type") or ""
|
paid_to_type = to_account.get("account_type") or ""
|
||||||
from_currency = from_account.get("account_currency") or company_currency
|
from_currency = from_account.get("account_currency") or company_currency
|
||||||
to_currency = to_account.get("account_currency") or company_currency
|
to_currency = to_account.get("account_currency") or company_currency
|
||||||
|
|
||||||
is_multi = (
|
is_multi = from_currency != company_currency or to_currency != company_currency or bool(multi_currency)
|
||||||
from_currency != company_currency
|
|
||||||
or to_currency != company_currency
|
|
||||||
or bool(force_multi_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 = (bt.currency or company_currency).strip()
|
txn_currency = (
|
||||||
|
txn.get("currency")
|
||||||
|
or (getattr(bank_txn, "currency", None) or "")
|
||||||
|
or company_currency
|
||||||
|
).strip()
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -296,21 +471,21 @@ def _create_journal_entry(bt, paid_from, paid_to, payment_type, amount, party_ty
|
||||||
else:
|
else:
|
||||||
txn_rate = get_exchange_rate(txn_currency, company_currency, posting_date)
|
txn_rate = get_exchange_rate(txn_currency, company_currency, posting_date)
|
||||||
|
|
||||||
amount_in_company = flt(amount * txn_rate, precision)
|
amount_in_company = flt(amount * txn_rate, currency_precision)
|
||||||
|
|
||||||
if from_currency == txn_currency:
|
if from_currency == txn_currency:
|
||||||
from_amount = amount
|
from_amount = amount
|
||||||
elif from_currency == company_currency:
|
elif from_currency == company_currency:
|
||||||
from_amount = amount_in_company
|
from_amount = amount_in_company
|
||||||
else:
|
else:
|
||||||
from_amount = flt(amount_in_company / from_rate, precision) if from_rate else amount_in_company
|
from_amount = flt(amount_in_company / from_rate, currency_precision) if from_rate else amount_in_company
|
||||||
|
|
||||||
if to_currency == txn_currency:
|
if to_currency == txn_currency:
|
||||||
to_amount = amount
|
to_amount = amount
|
||||||
elif to_currency == company_currency:
|
elif to_currency == company_currency:
|
||||||
to_amount = amount_in_company
|
to_amount = amount_in_company
|
||||||
else:
|
else:
|
||||||
to_amount = flt(amount_in_company / to_rate, precision) if to_rate else amount_in_company
|
to_amount = flt(amount_in_company / to_rate, currency_precision) if to_rate else amount_in_company
|
||||||
|
|
||||||
if from_currency != company_currency and from_amount:
|
if from_currency != company_currency and from_amount:
|
||||||
from_rate = amount_in_company / from_amount
|
from_rate = amount_in_company / from_amount
|
||||||
|
|
@ -320,39 +495,48 @@ def _create_journal_entry(bt, paid_from, paid_to, payment_type, amount, party_ty
|
||||||
je = frappe.new_doc("Journal Entry")
|
je = frappe.new_doc("Journal Entry")
|
||||||
je.voucher_type = "Bank Entry"
|
je.voucher_type = "Bank Entry"
|
||||||
je.posting_date = posting_date
|
je.posting_date = posting_date
|
||||||
je.company = bt.company
|
je.company = bank_txn.company
|
||||||
je.cheque_no = ref_no
|
je.cheque_no = ref_no
|
||||||
je.cheque_date = posting_date
|
je.cheque_date = posting_date
|
||||||
je.user_remark = bt.description or ""
|
je.user_remark = bank_txn.description or txn.get("purpose") or ""
|
||||||
je.expense_income_type = "Expense" if is_pay else "Income"
|
je.expense_income_type = "Expense" if is_pay else "Income"
|
||||||
if is_multi:
|
if is_multi:
|
||||||
je.multi_currency = 1
|
je.multi_currency = 1
|
||||||
|
|
||||||
# paid_from is always credited, paid_to is always debited.
|
if is_pay:
|
||||||
from_row = {
|
from_row = {
|
||||||
"account": paid_from,
|
"account": paid_from,
|
||||||
"exchange_rate": from_rate,
|
"exchange_rate": from_rate,
|
||||||
"credit_in_account_currency": from_amount,
|
"credit_in_account_currency": from_amount,
|
||||||
"debit_in_account_currency": 0,
|
"debit_in_account_currency": 0,
|
||||||
}
|
}
|
||||||
to_row = {
|
to_row = {
|
||||||
"account": paid_to,
|
"account": paid_to,
|
||||||
"exchange_rate": to_rate,
|
"exchange_rate": to_rate,
|
||||||
"debit_in_account_currency": to_amount,
|
"debit_in_account_currency": to_amount,
|
||||||
"credit_in_account_currency": 0,
|
"credit_in_account_currency": 0,
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
from_row = {
|
||||||
|
"account": paid_from,
|
||||||
|
"exchange_rate": from_rate,
|
||||||
|
"credit_in_account_currency": from_amount,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
}
|
||||||
|
to_row = {
|
||||||
|
"account": paid_to,
|
||||||
|
"exchange_rate": to_rate,
|
||||||
|
"debit_in_account_currency": to_amount,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
}
|
||||||
|
|
||||||
if party_type and erp_party:
|
if party_type and party:
|
||||||
if paid_from_type in ("Receivable", "Payable"):
|
if paid_from_type in ("Receivable", "Payable"):
|
||||||
from_row["party_type"] = party_type
|
from_row["party_type"] = party_type
|
||||||
from_row["party"] = erp_party
|
from_row["party"] = party
|
||||||
if paid_to_type in ("Receivable", "Payable"):
|
if paid_to_type in ("Receivable", "Payable"):
|
||||||
to_row["party_type"] = party_type
|
to_row["party_type"] = party_type
|
||||||
to_row["party"] = erp_party
|
to_row["party"] = party
|
||||||
|
|
||||||
if cost_center:
|
|
||||||
from_row["cost_center"] = cost_center
|
|
||||||
to_row["cost_center"] = cost_center
|
|
||||||
|
|
||||||
je.append("accounts", from_row)
|
je.append("accounts", from_row)
|
||||||
je.append("accounts", to_row)
|
je.append("accounts", to_row)
|
||||||
|
|
@ -373,17 +557,36 @@ def _create_journal_entry(bt, paid_from, paid_to, payment_type, amount, party_ty
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.local.message_log = []
|
frappe.local.message_log = []
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
f"_create_journal_entry failed: {e}\n{frappe.get_traceback()}",
|
f"_create_journal_entry_for_brt failed: {e}\n{frappe.get_traceback()}",
|
||||||
"BI Create Reconcile",
|
"BI Create Reconcile",
|
||||||
)
|
)
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def _extract_messages(message_log):
|
def _ensure_purpose(purpose_text, bank_integration):
|
||||||
parts = []
|
"""Return the name of an existing or newly created Bank Integration Purpose for this integration."""
|
||||||
for m in message_log:
|
existing = frappe.db.get_value(
|
||||||
if isinstance(m, dict):
|
PURPOSE_DOCTYPE,
|
||||||
parts.append(m.get("message") or m.get("msg") or "")
|
{"purpose_keyword": purpose_text, "parent_bank_integration": bank_integration},
|
||||||
else:
|
"name",
|
||||||
parts.append(str(m))
|
)
|
||||||
return " | ".join(p for p in parts if p)
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = frappe.new_doc(PURPOSE_DOCTYPE)
|
||||||
|
doc.purpose_keyword = purpose_text
|
||||||
|
doc.direction = "Both"
|
||||||
|
doc.status = "New"
|
||||||
|
doc.parent_bank_integration = bank_integration
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
return doc.name
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
return frappe.db.get_value(
|
||||||
|
PURPOSE_DOCTYPE,
|
||||||
|
{"purpose_keyword": purpose_text, "parent_bank_integration": bank_integration},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"Insert purpose failed for {purpose_text}: {e}", "BI Purpose")
|
||||||
|
raise
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
// Bank Reconciliation Tool extensions for jey_erp:
|
// Bank Reconciliation Tool extensions for jey_erp.
|
||||||
// 1. Adds "Bank Transaction" column showing the BT name (with link).
|
//
|
||||||
// 2. Rebuilds DataTable with a checkbox column.
|
// (1) refresh — patches DataTableManager prototype once to add a
|
||||||
// 3. Adds a floating "Create & Reconcile" toolbar above the DataTable
|
// "Bank Transaction" column (BT name with a link).
|
||||||
// that operates on selected rows. The user confirms which Bank Integration
|
// (2) render — fires on every "Get Unreconciled Entries"; rebuilds the
|
||||||
// (or Kapital Bank Settings) provides the mappings; default is taken from
|
// DataTable with a checkbox column and injects a "Create & Reconcile"
|
||||||
// the Bank Account's hidden bank_integration field, and the chosen value
|
// toolbar. Selected rows are mapped to Payment Entry / Journal Entry
|
||||||
// is written back so it pre-fills next time.
|
// using either a Bank Integration record's mappings or Kapital Bank
|
||||||
|
// Settings' mappings (chosen in the dialog; default comes from the
|
||||||
|
// Bank Account's hidden bank_integration field, written back on submit).
|
||||||
|
//
|
||||||
|
// The dialog / mapping logic is a port of kapital_bank's BRT extension,
|
||||||
|
// generalised to pick the mapping source.
|
||||||
|
|
||||||
frappe.ui.form.on("Bank Reconciliation Tool", {
|
frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
// Make BRT fill the page (existing fallback)
|
frm.$wrapper.closest(".page-container").css("--page-max-width", "none");
|
||||||
frm.$wrapper.closest('.page-container').css('--page-max-width', 'none');
|
|
||||||
|
|
||||||
// Patch the DataTableManager prototype once: adds the "Bank Transaction" column.
|
|
||||||
frappe.require("bank-reconciliation-tool.bundle.js", function () {
|
frappe.require("bank-reconciliation-tool.bundle.js", function () {
|
||||||
const DTM = erpnext.accounts.bank_reconciliation.DataTableManager;
|
const DTM = erpnext.accounts.bank_reconciliation.DataTableManager;
|
||||||
if (!DTM || DTM.prototype._jey_erp_patched) return;
|
if (!DTM || DTM.prototype._jey_erp_patched) return;
|
||||||
|
|
||||||
const proto = DTM.prototype;
|
const proto = DTM.prototype;
|
||||||
|
|
||||||
// === 1. Add "Bank Transaction" column before the Actions column ===
|
|
||||||
const orig_get_dt_columns = proto.get_dt_columns;
|
const orig_get_dt_columns = proto.get_dt_columns;
|
||||||
proto.get_dt_columns = function () {
|
proto.get_dt_columns = function () {
|
||||||
orig_get_dt_columns.call(this);
|
orig_get_dt_columns.call(this);
|
||||||
|
|
@ -34,7 +35,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert bank transaction name (row["name"]) before the Actions button
|
|
||||||
const orig_format_row = proto.format_row;
|
const orig_format_row = proto.format_row;
|
||||||
proto.format_row = function (row) {
|
proto.format_row = function (row) {
|
||||||
const result = orig_format_row.call(this, row);
|
const result = orig_format_row.call(this, row);
|
||||||
|
|
@ -46,9 +46,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// `render` fires every time the reconciliation tool is (re)built — i.e. on
|
|
||||||
// every "Get Unreconciled Entries". Each time a brand-new DataTableManager
|
|
||||||
// is created, so we re-poll for it and re-add the checkbox column + toolbar.
|
|
||||||
render(frm) {
|
render(frm) {
|
||||||
if (frm._bi_check_interval) clearInterval(frm._bi_check_interval);
|
if (frm._bi_check_interval) clearInterval(frm._bi_check_interval);
|
||||||
frm._bi_check_interval = setInterval(function () {
|
frm._bi_check_interval = setInterval(function () {
|
||||||
|
|
@ -63,11 +60,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||||
});
|
});
|
||||||
|
|
||||||
const BIBRT = {
|
const BIBRT = {
|
||||||
enhance(frm, dtm) {
|
enhance: function (frm, dtm) {
|
||||||
dtm._bi_enhanced = true;
|
dtm._bi_enhanced = true;
|
||||||
dtm._bi_frm = frm;
|
dtm._bi_frm = frm;
|
||||||
|
|
||||||
// Rebuild DataTable with checkboxColumn: true
|
|
||||||
dtm.datatable.destroy();
|
dtm.datatable.destroy();
|
||||||
dtm.datatable = new frappe.DataTable(dtm.$reconciliation_tool_dt.get(0), {
|
dtm.datatable = new frappe.DataTable(dtm.$reconciliation_tool_dt.get(0), {
|
||||||
columns: dtm.columns,
|
columns: dtm.columns,
|
||||||
|
|
@ -83,13 +79,14 @@ const BIBRT = {
|
||||||
BIBRT.injectToolbar(dtm);
|
BIBRT.injectToolbar(dtm);
|
||||||
},
|
},
|
||||||
|
|
||||||
injectToolbar(dtm) {
|
injectToolbar: function (dtm) {
|
||||||
$("#bi-brt-toolbar").remove();
|
$("#bi-brt-toolbar").remove();
|
||||||
|
|
||||||
const $toolbar = $(`
|
const $toolbar = $(`
|
||||||
<div id="bi-brt-toolbar"
|
<div id="bi-brt-toolbar"
|
||||||
style="display:none; align-items:center; gap:10px; margin-bottom:8px;
|
style="display:none; align-items:center; gap:10px; margin-bottom:8px;
|
||||||
padding:8px 12px; background:var(--fg-color);
|
padding:8px 12px; background:var(--fg-color);
|
||||||
border:1px solid var(--border-color); border-radius:6px;">
|
border:1px solid var(--border-color); border-radius:6px;">
|
||||||
<span class="bi-brt-selected-count text-muted"></span>
|
<span class="bi-brt-selected-count text-muted"></span>
|
||||||
<button class="btn btn-primary btn-sm bi-brt-create-btn">
|
<button class="btn btn-primary btn-sm bi-brt-create-btn">
|
||||||
${__("Create & Reconcile")}
|
${__("Create & Reconcile")}
|
||||||
|
|
@ -98,20 +95,21 @@ const BIBRT = {
|
||||||
`);
|
`);
|
||||||
dtm.$reconciliation_tool_dt.before($toolbar);
|
dtm.$reconciliation_tool_dt.before($toolbar);
|
||||||
|
|
||||||
const scope = dtm.datatable.style.scopeClass;
|
const scopeClass = dtm.datatable.style.scopeClass;
|
||||||
$(document).off("click.bi_brt").on("click.bi_brt",
|
$(document).off("click.bi_brt").on(
|
||||||
`.${scope} input[type="checkbox"]`,
|
"click.bi_brt",
|
||||||
|
`.${scopeClass} input[type="checkbox"]`,
|
||||||
function () {
|
function () {
|
||||||
setTimeout(() => BIBRT.updateToolbar(dtm, $toolbar), 30);
|
setTimeout(() => BIBRT.updateToolbar(dtm, $toolbar), 30);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$toolbar.find(".bi-brt-create-btn").on("click", function () {
|
$toolbar.find(".bi-brt-create-btn").on("click", function () {
|
||||||
BIBRT.openDialog(dtm);
|
BIBRT.showMappingDialog(dtm);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateToolbar(dtm, $toolbar) {
|
updateToolbar: function (dtm, $toolbar) {
|
||||||
const checked = dtm.datatable.rowmanager.getCheckedRows();
|
const checked = dtm.datatable.rowmanager.getCheckedRows();
|
||||||
if (checked && checked.length > 0) {
|
if (checked && checked.length > 0) {
|
||||||
$toolbar.css("display", "flex");
|
$toolbar.css("display", "flex");
|
||||||
|
|
@ -121,29 +119,13 @@ const BIBRT = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openDialog(dtm) {
|
showMappingDialog: function (dtm) {
|
||||||
const checkedIndices = dtm.datatable.rowmanager.getCheckedRows();
|
const checkedIndices = dtm.datatable.rowmanager.getCheckedRows();
|
||||||
if (!checkedIndices || !checkedIndices.length) {
|
if (!checkedIndices || checkedIndices.length === 0) {
|
||||||
frappe.msgprint({ title: __("No Selection"), indicator: "orange",
|
frappe.msgprint({ title: __("Warning"), indicator: "orange", message: __("No rows selected.") });
|
||||||
message: __("No rows selected.") });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect BT names
|
|
||||||
const btNames = [];
|
|
||||||
checkedIndices.forEach(function (idx) {
|
|
||||||
const row = dtm.transactions[parseInt(idx, 10)];
|
|
||||||
if (!row) return;
|
|
||||||
// row[N] = Actions; the "name" is on row.name attribute (per format_row above)
|
|
||||||
if (row.name) btNames.push(row.name);
|
|
||||||
});
|
|
||||||
if (!btNames.length) {
|
|
||||||
frappe.msgprint({ title: __("No Selection"), indicator: "orange",
|
|
||||||
message: __("Could not resolve Bank Transaction names from selection.") });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bank Account from BRT form
|
|
||||||
const frm = dtm._bi_frm;
|
const frm = dtm._bi_frm;
|
||||||
const bankAccount = frm.doc.bank_account;
|
const bankAccount = frm.doc.bank_account;
|
||||||
if (!bankAccount) {
|
if (!bankAccount) {
|
||||||
|
|
@ -152,168 +134,318 @@ const BIBRT = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default Bank Integration from BA's hidden fields
|
// dtm.transactions rows are arrays. With the jey_erp prototype patch active the layout is:
|
||||||
|
// [0]date [1]party_type [2]party(HTML) [3]description [4]deposit [5]withdrawal
|
||||||
|
// [6]unallocated [7]reference_number [8]bank_transaction_name(string) [9]actions(button HTML)
|
||||||
|
const txns = [];
|
||||||
|
let skippedCount = 0;
|
||||||
|
checkedIndices.forEach(function (rowIndex) {
|
||||||
|
const row = dtm.transactions[parseInt(rowIndex, 10)];
|
||||||
|
if (!row) return;
|
||||||
|
const description = (row[3] || "").trim();
|
||||||
|
if (!description) { skippedCount++; return; }
|
||||||
|
const partyType = (row[1] || "").trim();
|
||||||
|
const partyName = $("<div>").html(row[2] || "").text().trim();
|
||||||
|
const deposit = parseFloat(row[4]) || 0;
|
||||||
|
const withdrawal = parseFloat(row[5]) || 0;
|
||||||
|
|
||||||
|
let btName = "";
|
||||||
|
if (typeof row[8] === "string" && row[8] && row[8].indexOf("<") === -1) {
|
||||||
|
btName = row[8].trim();
|
||||||
|
}
|
||||||
|
if (!btName) btName = $(row[9] || row[8] || "").data("name") || "";
|
||||||
|
|
||||||
|
txns.push({
|
||||||
|
purpose: description,
|
||||||
|
drcr: withdrawal > 0 ? "D" : "C",
|
||||||
|
party_type: partyType,
|
||||||
|
party: partyName,
|
||||||
|
amount: deposit || withdrawal,
|
||||||
|
date: (row[0] || "").trim(),
|
||||||
|
reference_number: (row[7] || "").trim(),
|
||||||
|
bank_transaction_name: btName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (txns.length === 0) {
|
||||||
|
frappe.msgprint({ title: __("No Transactions"), indicator: "orange",
|
||||||
|
message: __("All selected rows have no description text.") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (txns.some(t => !t.bank_transaction_name)) {
|
||||||
|
frappe.msgprint({ title: __("Error"), indicator: "red",
|
||||||
|
message: __("Could not resolve Bank Transaction names from selection. Try reloading the page.") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build mapping-source options + load BA default
|
||||||
Promise.all([
|
Promise.all([
|
||||||
frappe.db.get_value("Bank Account", bankAccount, ["bank_integration_type", "bank_integration"]),
|
|
||||||
frappe.db.get_list("Bank Integration", { fields: ["name", "bank_name"], limit: 200 }),
|
frappe.db.get_list("Bank Integration", { fields: ["name", "bank_name"], limit: 200 }),
|
||||||
]).then(([baResp, integrations]) => {
|
frappe.db.get_value("Bank Account", bankAccount, ["bank_integration_type", "bank_integration"]),
|
||||||
|
frappe.db.get_list("DocType", { filters: { name: "Kapital Bank Settings" }, fields: ["name"], limit: 1 }),
|
||||||
|
]).then(([integrations, baResp, kbList]) => {
|
||||||
const baFields = (baResp && baResp.message) || {};
|
const baFields = (baResp && baResp.message) || {};
|
||||||
const defaultType = baFields.bank_integration_type || "";
|
|
||||||
const defaultName = baFields.bank_integration || "";
|
|
||||||
|
|
||||||
BIBRT._showSourceDialog(btNames, bankAccount, defaultType, defaultName, integrations || [], dtm);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_showSourceDialog(btNames, bankAccount, defaultType, defaultName, integrations, dtm) {
|
|
||||||
// Build options: each Bank Integration record + "Kapital Bank" if installed
|
|
||||||
const checkKB = frappe.db.get_list("DocType", {
|
|
||||||
filters: { name: "Kapital Bank Settings" },
|
|
||||||
fields: ["name"],
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
checkKB.then((kbList) => {
|
|
||||||
const kbInstalled = kbList && kbList.length > 0;
|
const kbInstalled = kbList && kbList.length > 0;
|
||||||
const options = [{ label: "", value: "" }];
|
|
||||||
integrations.forEach(i => options.push({
|
const sourceMap = {}; // value -> label
|
||||||
label: __("Bank Integration: {0}", [i.bank_name || i.name]),
|
(integrations || []).forEach(i => {
|
||||||
value: "Bank Integration::" + i.name,
|
sourceMap["Bank Integration::" + i.name] = __("Bank Integration: {0}", [i.bank_name || i.name]);
|
||||||
}));
|
});
|
||||||
if (kbInstalled) {
|
if (kbInstalled) {
|
||||||
options.push({
|
sourceMap["Kapital Bank Settings::Kapital Bank Settings"] = __("Kapital Bank Settings");
|
||||||
label: __("Kapital Bank Settings"),
|
|
||||||
value: "Kapital Bank Settings::Kapital Bank Settings",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValue = defaultType && defaultName
|
const sourceValues = Object.keys(sourceMap);
|
||||||
? defaultType + "::" + defaultName
|
if (sourceValues.length === 0) {
|
||||||
: "";
|
frappe.msgprint({ title: __("No Mapping Source"), indicator: "orange",
|
||||||
|
message: __("Create at least one Bank Integration record first.") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const d = new frappe.ui.Dialog({
|
let defaultSource = "";
|
||||||
title: __("Create & Reconcile") + " (" + btNames.length + ")",
|
if (baFields.bank_integration_type && baFields.bank_integration) {
|
||||||
fields: [
|
const candidate = baFields.bank_integration_type + "::" + baFields.bank_integration;
|
||||||
{
|
if (sourceMap[candidate]) defaultSource = candidate;
|
||||||
fieldname: "bank_account",
|
}
|
||||||
fieldtype: "Data",
|
|
||||||
label: __("Bank Account"),
|
|
||||||
read_only: 1,
|
|
||||||
default: bankAccount,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: "source",
|
|
||||||
fieldtype: "Select",
|
|
||||||
label: __("Bank Integration / Mapping Source"),
|
|
||||||
reqd: 1,
|
|
||||||
options: options.map(o => o.value).join("\n"),
|
|
||||||
default: defaultValue,
|
|
||||||
description: __("Mapping source used to create PE / JE for the selected BTs."),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: "source_label_html",
|
|
||||||
fieldtype: "HTML",
|
|
||||||
options: '<div class="text-muted small" id="bi-source-labels"></div>',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
primary_action_label: __("Create & Reconcile"),
|
|
||||||
primary_action(values) {
|
|
||||||
if (!values.source) {
|
|
||||||
frappe.msgprint({ title: __("Required"), indicator: "orange",
|
|
||||||
message: __("Please pick a mapping source.") });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [biType, biName] = values.source.split("::");
|
|
||||||
d.disable_primary_action();
|
|
||||||
|
|
||||||
// Save selection to BA hidden fields, then run reconciliation
|
BIBRT._renderDialog(dtm, frm, bankAccount, txns, skippedCount, sourceMap, sourceValues, defaultSource);
|
||||||
frappe.call({
|
|
||||||
method: "jey_erp.bank_integration.import_api.update_bank_account_bi_default",
|
|
||||||
args: { bank_account: bankAccount, bi_type: biType, bi_name: biName },
|
|
||||||
callback() {
|
|
||||||
BIBRT._runReconcile(d, btNames, bankAccount, dtm);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render select labels (Frappe Select doesn't natively support label≠value)
|
|
||||||
d.show();
|
|
||||||
setTimeout(() => {
|
|
||||||
const $select = d.$wrapper.find('select[data-fieldname="source"]');
|
|
||||||
$select.find('option').each(function () {
|
|
||||||
const v = $(this).val();
|
|
||||||
const opt = options.find(o => o.value === v);
|
|
||||||
if (opt) $(this).text(opt.label || "—");
|
|
||||||
});
|
|
||||||
}, 80);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_runReconcile(dialog, btNames, bankAccount, dtm) {
|
_renderDialog: function (dtm, frm, bankAccount, txns, skippedCount, sourceMap, sourceValues, defaultSource) {
|
||||||
frappe.call({
|
// Preview table (deduped)
|
||||||
method: "jey_erp.bank_integration.create_reconcile.create_documents_for_bts",
|
const purposeMap = new Map();
|
||||||
args: {
|
txns.forEach(function (txn) {
|
||||||
bt_names: JSON.stringify(btNames),
|
const key = txn.purpose + "|" + txn.drcr;
|
||||||
bank_account: bankAccount,
|
if (purposeMap.has(key)) {
|
||||||
|
purposeMap.get(key).count++;
|
||||||
|
} else {
|
||||||
|
purposeMap.set(key, {
|
||||||
|
purpose: txn.purpose,
|
||||||
|
payment_type: txn.drcr === "D" ? "Pay" : "Receive",
|
||||||
|
party: txn.party,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let previewHtml = '<div style="max-height:240px; overflow-y:auto; margin-bottom:12px;">' +
|
||||||
|
'<table class="table table-bordered" style="width:100%;">' +
|
||||||
|
"<thead><tr>" +
|
||||||
|
"<th>" + __("Description") + "</th>" +
|
||||||
|
'<th style="width:90px;">' + __("Type") + "</th>" +
|
||||||
|
'<th style="width:140px;">' + __("Party") + "</th>" +
|
||||||
|
'<th style="width:60px; text-align:center;">' + __("Rows") + "</th>" +
|
||||||
|
"</tr></thead><tbody>";
|
||||||
|
purposeMap.forEach(function (e) {
|
||||||
|
const badge = e.payment_type === "Pay"
|
||||||
|
? '<span class="label label-danger">Pay</span>'
|
||||||
|
: '<span class="label label-success">Receive</span>';
|
||||||
|
previewHtml += "<tr>" +
|
||||||
|
'<td style="word-break:break-word;font-size:0.9em;">' + frappe.utils.escape_html(e.purpose) + "</td>" +
|
||||||
|
"<td>" + badge + "</td>" +
|
||||||
|
"<td>" + (e.party ? frappe.utils.escape_html(e.party) : '<span class="text-muted">—</span>') + "</td>" +
|
||||||
|
'<td style="text-align:center;">' + e.count + "</td></tr>";
|
||||||
|
});
|
||||||
|
previewHtml += "</tbody></table></div>";
|
||||||
|
if (skippedCount > 0) {
|
||||||
|
previewHtml += '<div class="alert alert-warning">' +
|
||||||
|
__("{0} row(s) skipped — no description text.", [skippedCount]) + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Create & Reconcile") + " (" + purposeMap.size + ")",
|
||||||
|
size: "large",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "source",
|
||||||
|
fieldtype: "Select",
|
||||||
|
label: __("Mapping Source"),
|
||||||
|
options: sourceValues.join("\n"),
|
||||||
|
default: defaultSource || sourceValues[0],
|
||||||
|
reqd: 1,
|
||||||
|
description: __("Which Bank Integration / Kapital Bank Settings provides the mappings."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "mode",
|
||||||
|
fieldtype: "Select",
|
||||||
|
label: __("Action"),
|
||||||
|
options: "Both\nMappings Only\nDocuments & Reconcile",
|
||||||
|
default: "Both",
|
||||||
|
reqd: 1,
|
||||||
|
onchange: function () { BIBRT._applyModeVisibility(d); },
|
||||||
|
},
|
||||||
|
{ fieldname: "preview_html", fieldtype: "HTML", options: previewHtml },
|
||||||
|
{ fieldname: "sec_options", fieldtype: "Section Break", label: __("Options") },
|
||||||
|
{
|
||||||
|
fieldname: "document_type",
|
||||||
|
fieldtype: "Select",
|
||||||
|
label: __("Document Type"),
|
||||||
|
options: "Payment Entry\nJournal Entry",
|
||||||
|
default: "Payment Entry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "use_original_purpose",
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: __("Use original description"),
|
||||||
|
onchange: function () {
|
||||||
|
const on = d.get_value("use_original_purpose");
|
||||||
|
d.set_df_property("custom_purpose", "hidden", on ? 1 : 0);
|
||||||
|
d.get_field("custom_purpose").refresh();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "custom_purpose",
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Custom Purpose Keyword"),
|
||||||
|
hidden: 0,
|
||||||
|
description: __("Leave empty to map by party only. All rows will use this keyword instead of their own description."),
|
||||||
|
},
|
||||||
|
{ fieldname: "sec_accounts", fieldtype: "Section Break", label: __("GL Accounts") },
|
||||||
|
{ fieldname: "paid_from", fieldtype: "Link", options: "Account", label: __("Paid From (Account)") },
|
||||||
|
{ fieldname: "col_break", fieldtype: "Column Break" },
|
||||||
|
{ fieldname: "paid_to", fieldtype: "Link", options: "Account", label: __("Paid To (Account)") },
|
||||||
|
],
|
||||||
|
primary_action_label: __("Create & Reconcile"),
|
||||||
|
primary_action: function () {
|
||||||
|
const values = d.get_values(true);
|
||||||
|
if (!values) return;
|
||||||
|
const isDR = values.mode === "Documents & Reconcile";
|
||||||
|
if (!isDR) {
|
||||||
|
const missing = [];
|
||||||
|
if (!values.paid_from) missing.push(__("Paid From (Account)"));
|
||||||
|
if (!values.paid_to) missing.push(__("Paid To (Account)"));
|
||||||
|
if (missing.length) {
|
||||||
|
frappe.msgprint({ title: __("Missing Values"), indicator: "red",
|
||||||
|
message: __("Following fields are required:") + "<br>" + missing.join("<br>") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalTxns = txns.slice();
|
||||||
|
if (!isDR && !values.use_original_purpose) {
|
||||||
|
const cp = (values.custom_purpose || "").trim();
|
||||||
|
if (cp) {
|
||||||
|
finalTxns = txns.map(t => Object.assign({}, t, { purpose: cp }));
|
||||||
|
} else {
|
||||||
|
finalTxns = txns.map(t => Object.assign({}, t, { purpose: "" }));
|
||||||
|
if (finalTxns.every(t => !t.party)) {
|
||||||
|
frappe.msgprint({ title: __("Validation Error"), indicator: "red",
|
||||||
|
message: __("Either a purpose keyword or a party is required.") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sourceType, sourceName] = values.source.split("::");
|
||||||
|
d.disable_primary_action();
|
||||||
|
|
||||||
|
// Persist the chosen source on the Bank Account, then run the mapping/reconcile
|
||||||
|
frappe.call({
|
||||||
|
method: "jey_erp.bank_integration.import_api.update_bank_account_bi_default",
|
||||||
|
args: { bank_account: bankAccount, bi_type: sourceType, bi_name: sourceName },
|
||||||
|
always: function () {
|
||||||
|
BIBRT._submit(d, finalTxns, sourceType, sourceName,
|
||||||
|
values.paid_from, values.paid_to, values.document_type, values.mode, frm);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
callback(r) {
|
});
|
||||||
|
|
||||||
|
d._bi_scopeClass = dtm._bi_scopeClass;
|
||||||
|
d.show();
|
||||||
|
BIBRT._applyModeVisibility(d);
|
||||||
|
|
||||||
|
// Pre-fill custom purpose from the Description inline filter if active
|
||||||
|
const $fi = $(`.${dtm._bi_scopeClass} .dt-row-filter input.dt-filter[data-name="Description"]`);
|
||||||
|
const filterVal = ($fi.val() || "").trim();
|
||||||
|
if (filterVal) d.set_value("custom_purpose", filterVal);
|
||||||
|
},
|
||||||
|
|
||||||
|
_applyModeVisibility: function (d) {
|
||||||
|
const isDR = d.get_value("mode") === "Documents & Reconcile";
|
||||||
|
const toHide = ["preview_html", "sec_options", "document_type", "use_original_purpose",
|
||||||
|
"custom_purpose", "sec_accounts", "paid_from", "col_break", "paid_to"];
|
||||||
|
toHide.forEach(fn => d.set_df_property(fn, "hidden", isDR ? 1 : 0));
|
||||||
|
d.refresh_fields(toHide);
|
||||||
|
},
|
||||||
|
|
||||||
|
_submit: function (dialog, txns, sourceType, sourceName, paid_from, paid_to, document_type, mode, frm) {
|
||||||
|
const isKB = sourceType === "Kapital Bank Settings";
|
||||||
|
const method = isKB
|
||||||
|
? "kapital_bank.mapping.create_purpose_mappings"
|
||||||
|
: "jey_erp.bank_integration.create_reconcile.create_purpose_mappings";
|
||||||
|
const args = isKB
|
||||||
|
? {
|
||||||
|
transactions: JSON.stringify(txns),
|
||||||
|
paid_from: paid_from,
|
||||||
|
paid_to: paid_to,
|
||||||
|
document_type: document_type || "Payment Entry",
|
||||||
|
mode: mode || "Both",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
transactions: JSON.stringify(txns),
|
||||||
|
bank_integration: sourceName,
|
||||||
|
paid_from: paid_from,
|
||||||
|
paid_to: paid_to,
|
||||||
|
document_type: document_type || "Payment Entry",
|
||||||
|
mode: mode || "Both",
|
||||||
|
};
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: method,
|
||||||
|
args: args,
|
||||||
|
callback: function (r) {
|
||||||
dialog.enable_primary_action();
|
dialog.enable_primary_action();
|
||||||
if (!r.message || r.message.success === false) {
|
if (!r.message || !r.message.success) {
|
||||||
frappe.msgprint({
|
frappe.msgprint({ title: __("Error"), indicator: "red",
|
||||||
title: __("Error"),
|
message: r.message ? r.message.message : __("Unknown error") });
|
||||||
indicator: "red",
|
|
||||||
message: (r.message && r.message.message) || __("Unknown error"),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
const res = r.message;
|
const res = r.message;
|
||||||
const total = res.total || 0;
|
const parts = [];
|
||||||
const docs = res.created_docs || 0;
|
if (res.created_mappings) parts.push(__("Mappings created: {0}", [res.created_mappings]));
|
||||||
const reconciled = res.reconciled || 0;
|
if (res.already_mapped) parts.push(__("Already mapped: {0}", [res.already_mapped]));
|
||||||
const errs = (res.errors || []).length;
|
if (res.skipped_no_data) parts.push(__("Skipped (no purpose): {0}", [res.skipped_no_data]));
|
||||||
|
if (res.created_docs) parts.push(__("Documents created: {0}", [res.created_docs]));
|
||||||
const indicator = errs === 0 ? "green" : (docs > 0 ? "orange" : "red");
|
if (res.reconciled) parts.push(__("Reconciled: {0}", [res.reconciled]));
|
||||||
let msg = __("Created: <b>{0}</b> / {1}", [docs, total]) + "<br>";
|
if (res.errors && res.errors.length) parts.push(__("Errors: {0}", [res.errors.length]));
|
||||||
msg += __("Reconciled: <b>{0}</b>", [reconciled]) + "<br>";
|
frappe.show_alert({
|
||||||
msg += __("Errors: <b>{0}</b>", [errs]);
|
message: parts.join(" • ") || __("No changes"),
|
||||||
|
indicator: (res.created_mappings > 0 || res.created_docs > 0) ? "green" : "blue",
|
||||||
frappe.msgprint({
|
}, 6);
|
||||||
title: __("Create & Reconcile"),
|
if (frm) frm.refresh();
|
||||||
indicator: indicator,
|
if (res.errors && res.errors.length) BIBRT.showErrors(res.errors);
|
||||||
message: msg,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errs > 0) {
|
|
||||||
BIBRT._showErrors(res.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dtm._bi_frm) dtm._bi_frm.refresh();
|
|
||||||
},
|
},
|
||||||
error() {
|
error: function () {
|
||||||
dialog.enable_primary_action();
|
dialog.enable_primary_action();
|
||||||
frappe.msgprint({ title: __("Error"), indicator: "red",
|
frappe.msgprint({ title: __("Error"), indicator: "red",
|
||||||
message: __("Network error during reconciliation") });
|
message: __("Network error creating mappings") });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_showErrors(errors) {
|
showErrors: function (errors) {
|
||||||
let html = '<div style="max-height:500px; overflow-y:auto;">';
|
let html = '<div style="max-height:500px; overflow-y:auto;">';
|
||||||
errors.forEach(e => {
|
errors.forEach(function (err) {
|
||||||
const ref = e.reference_number || e.bank_transaction || __("Unknown");
|
const ref = err.reference_number || err.bank_transaction || __("Unknown");
|
||||||
html += '<div style="margin-bottom:12px; padding:10px; border:1px solid var(--border-color); border-radius:6px;">';
|
html += '<div style="margin-bottom:14px; padding:12px; border:1px solid var(--border-color); border-radius:6px; background:var(--bg-color);">';
|
||||||
html += '<div style="display:flex; justify-content:space-between; margin-bottom:6px;">';
|
html += '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; padding-bottom:6px; border-bottom:1px solid var(--border-color);">';
|
||||||
html += '<strong>' + frappe.utils.escape_html(ref) + '</strong>';
|
html += "<strong>" + frappe.utils.escape_html(ref) + "</strong>";
|
||||||
html += '<span class="label label-danger">' + __("Error") + '</span></div>';
|
html += '<span class="label label-danger">' + __("Error") + "</span>";
|
||||||
html += '<div>' + frappe.utils.escape_html(e.message || __("Unknown")) + '</div></div>';
|
html += "</div>";
|
||||||
|
html += '<div style="color:var(--text-color);">' + frappe.utils.escape_html(err.message || __("Unknown error")) + "</div>";
|
||||||
|
html += "</div>";
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += "</div>";
|
||||||
|
|
||||||
const d = new frappe.ui.Dialog({
|
const d = new frappe.ui.Dialog({
|
||||||
title: __("Errors") + " (" + errors.length + ")",
|
title: __("Errors ({0})", [errors.length]),
|
||||||
size: "large",
|
size: "large",
|
||||||
fields: [{ fieldname: "errors_html", fieldtype: "HTML", options: html }],
|
fields: [{ fieldname: "errors_html", fieldtype: "HTML", options: html }],
|
||||||
|
primary_action_label: __("Close"),
|
||||||
|
primary_action: function () { d.hide(); },
|
||||||
});
|
});
|
||||||
d.show();
|
d.show();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue