346 lines
11 KiB
Python
346 lines
11 KiB
Python
"""Whitelisted import endpoints + background job for Bank Integration Excel import."""
|
|
|
|
import json
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.utils import cint, flt
|
|
|
|
from jey_erp.bank_integration.excel_parser import parse_excel
|
|
|
|
|
|
@frappe.whitelist()
|
|
def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_account):
|
|
"""Parse Excel and return preview transactions, deduped against existing BTs."""
|
|
transactions = parse_excel(file_url, preset_name)
|
|
|
|
if not transactions:
|
|
return {"success": True, "transactions": [], "skipped_duplicates": 0, "total_parsed": 0}
|
|
|
|
ref_nos = [t["ref_no"] for t in transactions if t.get("ref_no")]
|
|
existing = set()
|
|
if ref_nos:
|
|
rows = frappe.get_all(
|
|
"Bank Transaction",
|
|
filters={
|
|
"reference_number": ["in", ref_nos],
|
|
"bank_account": bank_account,
|
|
},
|
|
fields=["reference_number"],
|
|
limit_page_length=0,
|
|
)
|
|
existing = {r.reference_number for r in rows}
|
|
|
|
fresh = []
|
|
skipped = 0
|
|
for t in transactions:
|
|
if t.get("ref_no") and t["ref_no"] in existing:
|
|
skipped += 1
|
|
else:
|
|
fresh.append(t)
|
|
|
|
return {
|
|
"success": True,
|
|
"transactions": fresh,
|
|
"skipped_duplicates": skipped,
|
|
"total_parsed": len(transactions),
|
|
}
|
|
|
|
|
|
@frappe.whitelist()
|
|
def import_bulk_bt(txn_list, bank_integration, bank_account):
|
|
"""Enqueue bulk Bank Transaction creation as a background job.
|
|
|
|
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)
|
|
|
|
user = frappe.session.user
|
|
|
|
frappe.enqueue(
|
|
_process_bulk_bt_import,
|
|
txn_list=txn_list,
|
|
bank_integration=bank_integration,
|
|
bank_account=bank_account,
|
|
user=user,
|
|
queue="default",
|
|
timeout=600,
|
|
)
|
|
|
|
return {"success": True, "enqueued": True, "total": len(txn_list)}
|
|
|
|
|
|
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)
|
|
company = bi.default_company
|
|
|
|
ba_doc = frappe.get_doc("Bank Account", bank_account)
|
|
ba_account = ba_doc.account
|
|
ba_currency = (
|
|
frappe.db.get_value("Account", ba_account, "account_currency") if ba_account else None
|
|
) or "AZN"
|
|
|
|
total = len(txn_list)
|
|
imported_count = 0
|
|
errors = []
|
|
|
|
for idx, txn in enumerate(txn_list):
|
|
frappe.publish_realtime(
|
|
"bi_bt_import_progress",
|
|
{"current": idx + 1, "total": total},
|
|
user=user,
|
|
)
|
|
|
|
try:
|
|
counterparty = (txn.get("counterparty") or "").strip()
|
|
drcr = (txn.get("drcr") or "D").upper()
|
|
purpose = (txn.get("purpose") or "").strip()
|
|
ref_no = (txn.get("ref_no") or "").strip()
|
|
amount = abs(flt(txn.get("amount", 0)))
|
|
|
|
bt = frappe.new_doc("Bank Transaction")
|
|
bt.date = txn.get("date")
|
|
bt.deposit = amount if drcr == "C" else 0
|
|
bt.withdrawal = amount if drcr == "D" else 0
|
|
bt.currency = ba_currency
|
|
bt.description = txn.get("description") or purpose
|
|
bt.reference_number = ref_no
|
|
bt.transaction_id = ref_no
|
|
bt.company = company
|
|
bt.bank_party_name = counterparty or purpose
|
|
bt.bank_account = bank_account
|
|
|
|
bt.insert(ignore_permissions=True)
|
|
bt.submit()
|
|
|
|
imported_count += 1
|
|
frappe.db.commit()
|
|
|
|
except Exception as e:
|
|
frappe.db.rollback()
|
|
frappe.local.message_log = []
|
|
errors.append({
|
|
"ref_no": txn.get("ref_no"),
|
|
"counterparty": txn.get("counterparty"),
|
|
"amount": txn.get("amount"),
|
|
"date": txn.get("date"),
|
|
"error_type": "import_error",
|
|
"message": str(e)[:500],
|
|
"txn_data": txn,
|
|
})
|
|
frappe.log_error(
|
|
f"BI BT import failed for ref {txn.get('ref_no')}: {e}\n{frappe.get_traceback()}",
|
|
"BI BT Import Error",
|
|
)
|
|
frappe.db.commit()
|
|
|
|
frappe.publish_realtime(
|
|
"bi_bt_import_complete",
|
|
{"total": total, "imported": imported_count, "errors": errors},
|
|
user=user,
|
|
)
|
|
frappe.log_error(f"BI BT import done: {imported_count}/{total}", "BI BT Import Complete")
|
|
|
|
|
|
def _upsert_customer(name, voen, iban, bank_integration):
|
|
"""Returns True if new record was created."""
|
|
if voen and frappe.db.exists("Bank Integration Customer", {
|
|
"tax_id": voen, "parent_bank_integration": bank_integration
|
|
}):
|
|
return False
|
|
if frappe.db.exists("Bank Integration Customer", {
|
|
"customer_name": name, "parent_bank_integration": bank_integration
|
|
}):
|
|
return False
|
|
|
|
doc = frappe.new_doc("Bank Integration Customer")
|
|
doc.customer_name = name
|
|
doc.tax_id = voen or None
|
|
doc.iban = iban or None
|
|
doc.status = "New"
|
|
doc.parent_bank_integration = bank_integration
|
|
try:
|
|
doc.insert(ignore_permissions=True)
|
|
return True
|
|
except frappe.DuplicateEntryError:
|
|
return False
|
|
|
|
|
|
def _upsert_supplier(name, voen, iban, bank_integration):
|
|
if voen and frappe.db.exists("Bank Integration Supplier", {
|
|
"tax_id": voen, "parent_bank_integration": bank_integration
|
|
}):
|
|
return False
|
|
if frappe.db.exists("Bank Integration Supplier", {
|
|
"supplier_name": name, "parent_bank_integration": bank_integration
|
|
}):
|
|
return False
|
|
|
|
doc = frappe.new_doc("Bank Integration Supplier")
|
|
doc.supplier_name = name
|
|
doc.tax_id = voen or None
|
|
doc.iban = iban or None
|
|
doc.status = "New"
|
|
doc.parent_bank_integration = bank_integration
|
|
try:
|
|
doc.insert(ignore_permissions=True)
|
|
return True
|
|
except frappe.DuplicateEntryError:
|
|
return False
|
|
|
|
|
|
def _upsert_purpose(purpose, drcr, bank_integration):
|
|
existing = frappe.db.get_value("Bank Integration Purpose", {
|
|
"purpose_keyword": purpose,
|
|
"parent_bank_integration": bank_integration,
|
|
}, ["name", "direction"], as_dict=True)
|
|
|
|
new_dir = "Receive" if drcr == "C" else "Pay"
|
|
|
|
if existing:
|
|
if existing.direction and existing.direction != new_dir and existing.direction != "Both":
|
|
frappe.db.set_value("Bank Integration Purpose", existing.name, "direction", "Both", update_modified=False)
|
|
return False
|
|
|
|
doc = frappe.new_doc("Bank Integration Purpose")
|
|
doc.purpose_keyword = purpose
|
|
doc.direction = new_dir
|
|
doc.status = "New"
|
|
doc.parent_bank_integration = bank_integration
|
|
try:
|
|
doc.insert(ignore_permissions=True)
|
|
return True
|
|
except frappe.DuplicateEntryError:
|
|
return False
|
|
|
|
|
|
@frappe.whitelist()
|
|
def get_bi_reference_data_list(bank_integration, data_type, limit=100, offset=0):
|
|
"""Paginated rows for the Bank Integration "Data" tab subtabs.
|
|
|
|
data_type ∈ {accounts, customers, suppliers, purposes}.
|
|
"""
|
|
limit = cint(limit)
|
|
offset = cint(offset)
|
|
|
|
try:
|
|
if data_type == "accounts":
|
|
total = frappe.db.count("Bank Integration Account Mapping", filters={
|
|
"parent": bank_integration, "parenttype": "Bank Integration"
|
|
})
|
|
data = frappe.get_all(
|
|
"Bank Integration Account Mapping",
|
|
filters={"parent": bank_integration, "parenttype": "Bank Integration"},
|
|
fields=["name", "iban", "account_label", "currency", "bank_account", "gl_account"],
|
|
limit=limit, start=offset, order_by="idx asc",
|
|
)
|
|
|
|
elif data_type == "customers":
|
|
total = frappe.db.count("Bank Integration Customer", filters={"parent_bank_integration": bank_integration})
|
|
data = frappe.get_all(
|
|
"Bank Integration Customer",
|
|
filters={"parent_bank_integration": bank_integration},
|
|
fields=["name", "customer_name", "tax_id", "iban", "status", "mapped_customer", "creation"],
|
|
limit=limit, start=offset, order_by="customer_name asc",
|
|
)
|
|
|
|
elif data_type == "suppliers":
|
|
total = frappe.db.count("Bank Integration Supplier", filters={"parent_bank_integration": bank_integration})
|
|
data = frappe.get_all(
|
|
"Bank Integration Supplier",
|
|
filters={"parent_bank_integration": bank_integration},
|
|
fields=["name", "supplier_name", "tax_id", "iban", "status", "mapped_supplier", "creation"],
|
|
limit=limit, start=offset, order_by="supplier_name asc",
|
|
)
|
|
|
|
elif data_type == "purposes":
|
|
total = frappe.db.count("Bank Integration Purpose", filters={"parent_bank_integration": bank_integration})
|
|
data = frappe.get_all(
|
|
"Bank Integration Purpose",
|
|
filters={"parent_bank_integration": bank_integration},
|
|
fields=["name", "purpose_keyword", "direction", "status"],
|
|
limit=limit, start=offset, order_by="purpose_keyword asc",
|
|
)
|
|
|
|
else:
|
|
return {"success": False, "message": f"Unknown data_type: {data_type}"}
|
|
|
|
return {
|
|
"success": True,
|
|
"data": data,
|
|
"total_count": total,
|
|
"has_more": (offset + limit) < total,
|
|
}
|
|
|
|
except Exception as e:
|
|
frappe.log_error(f"get_bi_reference_data_list({data_type}): {e}\n{frappe.get_traceback()}", "BI Reference Data")
|
|
return {"success": False, "message": str(e)}
|
|
|
|
|
|
@frappe.whitelist()
|
|
def update_bank_account_bi_default(bank_account, bi_type, bi_name):
|
|
"""Save the user's BRT-time choice to the BA's hidden fields."""
|
|
if not bank_account:
|
|
frappe.throw(_("Bank Account required"))
|
|
if bi_type and bi_type not in ("Bank Integration", "Kapital Bank Settings"):
|
|
frappe.throw(_("Invalid Bank Integration Type"))
|
|
|
|
frappe.db.set_value("Bank Account", bank_account, {
|
|
"bank_integration_type": bi_type or None,
|
|
"bank_integration": bi_name or None,
|
|
}, update_modified=False)
|
|
frappe.db.commit()
|
|
return {"success": True}
|
|
|
|
|
|
@frappe.whitelist()
|
|
def load_registries_from_excel(file_url, preset_name, bank_integration,
|
|
load_customers=1, load_suppliers=1, load_purposes=1):
|
|
"""Parse an Excel statement and upsert ONLY the selected reference registries
|
|
(Bank Integration Customer / Supplier / Purpose) — no Bank Transactions.
|
|
This is the "Load Data" action on the Bank Integration form, the file-based
|
|
counterpart to kapital_bank's API-driven counterparty/purpose loaders."""
|
|
load_customers = cint(load_customers)
|
|
load_suppliers = cint(load_suppliers)
|
|
load_purposes = cint(load_purposes)
|
|
if not (load_customers or load_suppliers or load_purposes):
|
|
return {"success": False, "message": _("Select at least one type to load.")}
|
|
|
|
transactions = parse_excel(file_url, preset_name)
|
|
|
|
new_c = new_s = new_p = 0
|
|
for txn in transactions:
|
|
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 counterparty:
|
|
if drcr == "C" and load_customers:
|
|
if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration):
|
|
new_c += 1
|
|
elif drcr == "D" and load_suppliers:
|
|
if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration):
|
|
new_s += 1
|
|
|
|
if purpose and load_purposes:
|
|
if _upsert_purpose(purpose, drcr, bank_integration):
|
|
new_p += 1
|
|
|
|
frappe.db.commit()
|
|
return {
|
|
"success": True,
|
|
"total_rows": len(transactions),
|
|
"new_customers": new_c,
|
|
"new_suppliers": new_s,
|
|
"new_purposes": new_p,
|
|
}
|