payment entry import works
This commit is contained in:
parent
70c0f33d37
commit
4a560dbde5
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
import re
|
||||
import frappe
|
||||
from datetime import datetime
|
||||
from frappe.utils import cint, flt
|
||||
from difflib import SequenceMatcher
|
||||
from kapital_bank.api import BIRBankClient
|
||||
|
|
@ -98,11 +100,11 @@ def load_cards_to_registry(login_name=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_clients_from_statements(from_date, to_date, login_name=None):
|
||||
def load_counterparties_from_statements(from_date, to_date, login_name=None):
|
||||
"""
|
||||
Fetch account statements for all registered accounts and extract counterparties
|
||||
(contrAccount field) into the Kapital Bank Client registry.
|
||||
Analogous to invoice_az loading customers/suppliers from invoice documents.
|
||||
into Kapital Bank Customer / Kapital Bank Supplier registries based on transaction direction.
|
||||
D (debit, money out) → Supplier, C (credit, money in) → Customer.
|
||||
"""
|
||||
try:
|
||||
client = BIRBankClient(login_name)
|
||||
|
|
@ -111,12 +113,11 @@ def load_clients_from_statements(from_date, to_date, login_name=None):
|
|||
if not accounts:
|
||||
return {"success": False, "message": "No accounts in registry. Load accounts first."}
|
||||
|
||||
created = skipped = 0
|
||||
customers_created = suppliers_created = skipped = 0
|
||||
debug_log = []
|
||||
|
||||
for acc in accounts:
|
||||
iban = acc.iban
|
||||
# API expects the numeric account number (custAcNo), not the full IBAN
|
||||
account_number = acc.cust_ac_no or iban
|
||||
debug_log.append(f"Account: {_mask(iban)} custAcNo={_mask(account_number)} ({from_date} → {to_date})")
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ def load_clients_from_statements(from_date, to_date, login_name=None):
|
|||
data = resp.json()
|
||||
except Exception as e:
|
||||
debug_log.append(f" Error: {e}")
|
||||
frappe.log_error(f"Statement fetch failed for {_mask(iban)}: {e}", "KB Load Clients")
|
||||
frappe.log_error(f"Statement fetch failed for {_mask(iban)}: {e}", "KB Load Counterparties")
|
||||
continue
|
||||
|
||||
resp_obj = data.get("response", {})
|
||||
|
|
@ -151,60 +152,76 @@ def load_clients_from_statements(from_date, to_date, login_name=None):
|
|||
)
|
||||
debug_log.append(f" statementList entries: {len(statement_list)}")
|
||||
|
||||
# Log first 3 entries to understand the real contrAccount format
|
||||
if statement_list:
|
||||
debug_log.append(f" First entry keys: {list(statement_list[0].keys())}")
|
||||
for i, e in enumerate(statement_list[:3]):
|
||||
ca = _mask(e.get('contrAccount', 'N/A'))
|
||||
pu = e.get('purpose', '')[:60]
|
||||
debug_log.append(f" Entry[{i}] contrAccount={ca!r} purpose={pu!r}")
|
||||
|
||||
for entry in statement_list:
|
||||
contr_raw = (entry.get("contrAccount") or "").strip()
|
||||
if not contr_raw:
|
||||
continue
|
||||
|
||||
# contrAccount format: "IBAN / COMPANY NAME / VÖEN"
|
||||
# e.g. "AZ12AIIB400...000 / TEST COMPANY LLC / 4800123456"
|
||||
parts = [p.strip() for p in contr_raw.split(" / ")]
|
||||
if len(parts) < 2:
|
||||
continue # unrecognised format, skip
|
||||
continue
|
||||
|
||||
contr_iban = parts[0]
|
||||
contr_name = parts[1]
|
||||
contr_voen = parts[2] if len(parts) >= 3 else ""
|
||||
|
||||
if not contr_name:
|
||||
continue
|
||||
|
||||
# Deduplicate: by VÖEN if present, else by client_name
|
||||
if contr_voen and frappe.db.exists("Kapital Bank Client", {"tax_id": contr_voen}):
|
||||
skipped += 1
|
||||
continue
|
||||
if not contr_voen and frappe.db.exists("Kapital Bank Client", {"client_name": contr_name}):
|
||||
# Determine direction: D = money out = Supplier, C = money in = Customer
|
||||
dr_cr = (entry.get("drcrInd") or "").upper()
|
||||
|
||||
if dr_cr == "D":
|
||||
# Supplier
|
||||
doctype = "Kapital Bank Supplier"
|
||||
name_field = "supplier_name"
|
||||
elif dr_cr == "C":
|
||||
# Customer
|
||||
doctype = "Kapital Bank Customer"
|
||||
name_field = "customer_name"
|
||||
else:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
doc = frappe.new_doc("Kapital Bank Client")
|
||||
doc.client_name = contr_name
|
||||
doc.tax_id = contr_voen or None # NULL doesn't violate unique constraint
|
||||
doc.status = "New"
|
||||
# Deduplicate: by VÖEN if present, else by name
|
||||
if contr_voen and frappe.db.exists(doctype, {"tax_id": contr_voen}):
|
||||
skipped += 1
|
||||
continue
|
||||
if not contr_voen and frappe.db.exists(doctype, {name_field: contr_name}):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
doc = frappe.new_doc(doctype)
|
||||
doc.update({
|
||||
name_field: contr_name,
|
||||
"tax_id": contr_voen or None,
|
||||
"iban": contr_iban or None,
|
||||
"status": "New",
|
||||
})
|
||||
try:
|
||||
doc.insert(ignore_permissions=True)
|
||||
created += 1
|
||||
if dr_cr == "D":
|
||||
suppliers_created += 1
|
||||
else:
|
||||
customers_created += 1
|
||||
except frappe.DuplicateEntryError:
|
||||
skipped += 1
|
||||
except Exception as ins_e:
|
||||
frappe.log_error(f"Insert client failed for {_mask(contr_raw)}: {ins_e}", "KB Load Clients")
|
||||
frappe.log_error(f"Insert counterparty failed for {_mask(contr_raw)}: {ins_e}", "KB Load Counterparties")
|
||||
|
||||
frappe.db.commit()
|
||||
frappe.log_error("\n".join(debug_log), "KB Load Counterparties Debug")
|
||||
|
||||
# Always log the debug info so we can diagnose issues
|
||||
frappe.log_error("\n".join(debug_log), "KB Load Clients Debug")
|
||||
|
||||
return {"success": True, "created": created, "skipped": skipped, "debug": debug_log}
|
||||
return {
|
||||
"success": True,
|
||||
"customers_created": customers_created,
|
||||
"suppliers_created": suppliers_created,
|
||||
"skipped": skipped,
|
||||
"debug": debug_log,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"load_clients_from_statements: {e}\n{frappe.get_traceback()}", "KB Bank API")
|
||||
frappe.log_error(f"load_counterparties_from_statements: {e}\n{frappe.get_traceback()}", "KB Bank API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
|
|
@ -222,9 +239,10 @@ def get_kb_reference_data_summary():
|
|||
return {"total": total, "mapped": mapped}
|
||||
|
||||
summary = {
|
||||
"accounts": counts("Kapital Bank Account", {"status": "Mapped"}),
|
||||
"cards": counts("Kapital Bank Card", {"status": "Mapped"}),
|
||||
"clients": counts("Kapital Bank Client", {"status": ["in", ["Mapped", "Active"]]}),
|
||||
"accounts": counts("Kapital Bank Account", {"status": "Mapped"}),
|
||||
"cards": counts("Kapital Bank Card", {"status": "Mapped"}),
|
||||
"customers": counts("Kapital Bank Customer", {"status": "Mapped"}),
|
||||
"suppliers": counts("Kapital Bank Supplier", {"status": "Mapped"}),
|
||||
}
|
||||
return {"success": True, "summary": summary}
|
||||
except Exception as e:
|
||||
|
|
@ -259,14 +277,24 @@ def get_kb_reference_data_list(data_type, limit=100, offset=0):
|
|||
order_by="card_type asc"
|
||||
)
|
||||
|
||||
elif data_type == "clients":
|
||||
total = frappe.db.count("Kapital Bank Client")
|
||||
elif data_type == "customers":
|
||||
total = frappe.db.count("Kapital Bank Customer")
|
||||
data = frappe.get_all(
|
||||
"Kapital Bank Client",
|
||||
fields=["name", "client_name", "tax_id", "iban", "bank_code",
|
||||
"status", "mapped_party_type", "mapped_party", "creation"],
|
||||
"Kapital Bank Customer",
|
||||
fields=["name", "customer_name", "tax_id", "iban", "bank_code",
|
||||
"status", "mapped_customer", "creation"],
|
||||
limit=limit, start=offset,
|
||||
order_by="client_name asc"
|
||||
order_by="customer_name asc"
|
||||
)
|
||||
|
||||
elif data_type == "suppliers":
|
||||
total = frappe.db.count("Kapital Bank Supplier")
|
||||
data = frappe.get_all(
|
||||
"Kapital Bank Supplier",
|
||||
fields=["name", "supplier_name", "tax_id", "iban", "bank_code",
|
||||
"status", "mapped_supplier", "creation"],
|
||||
limit=limit, start=offset,
|
||||
order_by="supplier_name asc"
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -308,105 +336,110 @@ def get_unmapped_accounts():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unmapped_clients():
|
||||
"""Return Kapital Bank Clients with status='New' not yet in client_mappings."""
|
||||
def get_unmapped_customers():
|
||||
"""Return Kapital Bank Customers with status='New' not yet in customer_mappings."""
|
||||
try:
|
||||
settings = frappe.get_single("Kapital Bank Settings")
|
||||
already_mapped = {row.client_name for row in settings.client_mappings if row.client_name}
|
||||
already_mapped = {row.kb_customer_name for row in settings.customer_mappings if row.kb_customer_name}
|
||||
|
||||
clients = frappe.get_all(
|
||||
"Kapital Bank Client",
|
||||
customers = frappe.get_all(
|
||||
"Kapital Bank Customer",
|
||||
filters={"status": "New"},
|
||||
fields=["name", "client_name", "tax_id", "iban", "mapped_party_type", "mapped_party"]
|
||||
fields=["name", "customer_name", "tax_id", "iban"]
|
||||
)
|
||||
unmapped = [c for c in clients if c.client_name not in already_mapped]
|
||||
return {"success": True, "clients": unmapped}
|
||||
unmapped = [c for c in customers if c.name not in already_mapped]
|
||||
return {"success": True, "customers": unmapped}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(str(e), "KB get_unmapped_clients")
|
||||
frappe.log_error(str(e), "KB get_unmapped_customers")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unmapped_suppliers():
|
||||
"""Return Kapital Bank Suppliers with status='New' not yet in supplier_mappings."""
|
||||
try:
|
||||
settings = frappe.get_single("Kapital Bank Settings")
|
||||
already_mapped = {row.kb_supplier_name for row in settings.supplier_mappings if row.kb_supplier_name}
|
||||
|
||||
suppliers = frappe.get_all(
|
||||
"Kapital Bank Supplier",
|
||||
filters={"status": "New"},
|
||||
fields=["name", "supplier_name", "tax_id", "iban"]
|
||||
)
|
||||
unmapped = [s for s in suppliers if s.name not in already_mapped]
|
||||
return {"success": True, "suppliers": unmapped}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(str(e), "KB get_unmapped_suppliers")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLIENT MATCHING & CREATION
|
||||
# CUSTOMER MATCHING & CREATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@frappe.whitelist()
|
||||
def match_similar_clients():
|
||||
"""Auto-match Kapital Bank Clients to ERPNext Customer/Supplier by name similarity.
|
||||
Follows invoice_az pattern exactly."""
|
||||
def match_similar_customers():
|
||||
"""Auto-match Kapital Bank Customers to ERPNext Customers by name similarity."""
|
||||
try:
|
||||
doc = frappe.get_doc("Kapital Bank Settings")
|
||||
threshold = flt(doc.similarity_threshold or 95) / 100.0
|
||||
consider_azeri = bool(doc.consider_azeri_chars)
|
||||
|
||||
new_clients = frappe.get_all(
|
||||
"Kapital Bank Client",
|
||||
new_customers = frappe.get_all(
|
||||
"Kapital Bank Customer",
|
||||
filters={"status": "New"},
|
||||
fields=["name", "client_name", "tax_id"]
|
||||
fields=["name", "customer_name", "tax_id"]
|
||||
)
|
||||
if not new_clients:
|
||||
if not new_customers:
|
||||
return {"success": True, "matched_count": 0, "total_processed": 0}
|
||||
|
||||
customers = frappe.get_all("Customer", fields=["name", "customer_name", "tax_id"])
|
||||
suppliers = frappe.get_all("Supplier", fields=["name", "supplier_name", "tax_id"])
|
||||
erp_customers = frappe.get_all("Customer", fields=["name", "customer_name", "tax_id"])
|
||||
|
||||
# VÖEN lookup: tax_id → (party_name, party_type) for exact matching
|
||||
# VÖEN lookup for exact matching
|
||||
voen_index = {}
|
||||
for c in customers:
|
||||
for c in erp_customers:
|
||||
if c.tax_id:
|
||||
voen_index[c.tax_id.strip()] = (c.name, "Customer")
|
||||
for s in suppliers:
|
||||
if s.tax_id:
|
||||
voen_index[s.tax_id.strip()] = (s.name, "Supplier")
|
||||
voen_index[c.tax_id.strip()] = c.name
|
||||
|
||||
# Build dict of existing mappings: client_name → erp_party
|
||||
# Build dict of existing mappings: kb_customer_name → erp_customer
|
||||
existing_mappings = {
|
||||
row.client_name: row.erp_party
|
||||
for row in doc.client_mappings if row.client_name
|
||||
row.kb_customer_name: row.erp_customer
|
||||
for row in doc.customer_mappings if row.kb_customer_name
|
||||
}
|
||||
|
||||
# Group 1: clients already in the table but with empty erp_party
|
||||
clients_with_empty_mapping = []
|
||||
for client_name, erp_party in existing_mappings.items():
|
||||
if not erp_party:
|
||||
for c in new_clients:
|
||||
if c.client_name == client_name:
|
||||
clients_with_empty_mapping.append(c)
|
||||
# Group 1: customers already in the table but with empty erp_customer
|
||||
customers_with_empty_mapping = []
|
||||
for kb_name, erp_customer in existing_mappings.items():
|
||||
if not erp_customer:
|
||||
for c in new_customers:
|
||||
if c.name == kb_name:
|
||||
customers_with_empty_mapping.append(c)
|
||||
break
|
||||
|
||||
# Group 2: clients not in the table at all
|
||||
clients_not_in_table = [c for c in new_clients if c.client_name not in existing_mappings]
|
||||
# Group 2: customers not in the table at all
|
||||
customers_not_in_table = [c for c in new_customers if c.name not in existing_mappings]
|
||||
|
||||
# Process both groups (same as invoice_az)
|
||||
clients_to_process = clients_not_in_table + clients_with_empty_mapping
|
||||
customers_to_process = customers_not_in_table + customers_with_empty_mapping
|
||||
|
||||
matched = 0
|
||||
for client in clients_to_process:
|
||||
for customer in customers_to_process:
|
||||
best_party = None
|
||||
best_type = None
|
||||
|
||||
# Priority 1: exact VÖEN match
|
||||
if client.tax_id and client.tax_id.strip() in voen_index:
|
||||
best_party, best_type = voen_index[client.tax_id.strip()]
|
||||
if customer.tax_id and customer.tax_id.strip() in voen_index:
|
||||
best_party = voen_index[customer.tax_id.strip()]
|
||||
else:
|
||||
# Priority 2: name similarity
|
||||
kb_name = _normalize(client.client_name, consider_azeri)
|
||||
kb_name = _normalize(customer.customer_name, consider_azeri)
|
||||
best_score = 0
|
||||
|
||||
for c in customers:
|
||||
for c in erp_customers:
|
||||
score = SequenceMatcher(None, kb_name, _normalize(c.customer_name, consider_azeri)).ratio()
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_party = c.name
|
||||
best_type = "Customer"
|
||||
|
||||
for s in suppliers:
|
||||
score = SequenceMatcher(None, kb_name, _normalize(s.supplier_name, consider_azeri)).ratio()
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_party = s.name
|
||||
best_type = "Supplier"
|
||||
|
||||
if best_score < threshold:
|
||||
best_party = None
|
||||
|
|
@ -414,28 +447,25 @@ def match_similar_clients():
|
|||
if best_party:
|
||||
# Update existing row if present, else append new row
|
||||
existing_idx = None
|
||||
for idx, row in enumerate(doc.client_mappings):
|
||||
if row.client_name == client.client_name:
|
||||
for idx, row in enumerate(doc.customer_mappings):
|
||||
if row.kb_customer_name == customer.name:
|
||||
existing_idx = idx
|
||||
break
|
||||
|
||||
if existing_idx is not None:
|
||||
doc.client_mappings[existing_idx].party_type = best_type
|
||||
doc.client_mappings[existing_idx].erp_party = best_party
|
||||
doc.client_mappings[existing_idx].mapping_type = "Automatic"
|
||||
doc.customer_mappings[existing_idx].erp_customer = best_party
|
||||
doc.customer_mappings[existing_idx].mapping_type = "Automatic"
|
||||
else:
|
||||
doc.append("client_mappings", {
|
||||
"client_name": client.client_name,
|
||||
"tax_id": client.tax_id or None,
|
||||
"party_type": best_type,
|
||||
"erp_party": best_party,
|
||||
"mapping_type": "Automatic",
|
||||
doc.append("customer_mappings", {
|
||||
"kb_customer_name": customer.name,
|
||||
"tax_id": customer.tax_id or None,
|
||||
"erp_customer": best_party,
|
||||
"mapping_type": "Automatic",
|
||||
})
|
||||
|
||||
frappe.db.set_value("Kapital Bank Client", client.name, {
|
||||
"mapped_party_type": best_type,
|
||||
"mapped_party": best_party,
|
||||
"status": "Mapped",
|
||||
frappe.db.set_value("Kapital Bank Customer", customer.name, {
|
||||
"mapped_customer": best_party,
|
||||
"status": "Mapped",
|
||||
}, update_modified=False)
|
||||
matched += 1
|
||||
|
||||
|
|
@ -443,112 +473,254 @@ def match_similar_clients():
|
|||
doc.save(ignore_permissions=True)
|
||||
|
||||
frappe.db.commit()
|
||||
return {"success": True, "matched_count": matched, "total_processed": len(clients_to_process)}
|
||||
return {"success": True, "matched_count": matched, "total_processed": len(customers_to_process)}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"match_similar_clients: {e}\n{frappe.get_traceback()}", "KB Client Matching")
|
||||
frappe.log_error(f"match_similar_customers: {e}\n{frappe.get_traceback()}", "KB Customer Matching")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unmapped_clients():
|
||||
"""Create Customer/Supplier records for New Kapital Bank Clients that have no mapped party.
|
||||
Follows invoice_az pattern: also appends created rows to Settings.client_mappings."""
|
||||
def match_similar_suppliers():
|
||||
"""Auto-match Kapital Bank Suppliers to ERPNext Suppliers by name similarity."""
|
||||
try:
|
||||
clients = frappe.get_all(
|
||||
"Kapital Bank Client",
|
||||
filters={"status": "New"},
|
||||
fields=["name", "client_name", "tax_id", "mapped_party_type"]
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Kapital Bank Settings")
|
||||
already_in_mappings = {row.client_name for row in doc.client_mappings if row.client_name}
|
||||
threshold = flt(doc.similarity_threshold or 95) / 100.0
|
||||
consider_azeri = bool(doc.consider_azeri_chars)
|
||||
|
||||
created = customers_count = suppliers_count = 0
|
||||
new_suppliers = frappe.get_all(
|
||||
"Kapital Bank Supplier",
|
||||
filters={"status": "New"},
|
||||
fields=["name", "supplier_name", "tax_id"]
|
||||
)
|
||||
if not new_suppliers:
|
||||
return {"success": True, "matched_count": 0, "total_processed": 0}
|
||||
|
||||
for c in clients:
|
||||
if not c.mapped_party_type:
|
||||
continue
|
||||
try:
|
||||
if c.mapped_party_type == "Customer":
|
||||
party = frappe.new_doc("Customer")
|
||||
party.customer_name = c.client_name
|
||||
if doc.default_customer_group:
|
||||
party.customer_group = doc.default_customer_group
|
||||
if doc.default_territory:
|
||||
party.territory = doc.default_territory
|
||||
if doc.default_payment_terms:
|
||||
party.payment_terms = doc.default_payment_terms
|
||||
party.insert(ignore_permissions=True)
|
||||
customers_count += 1
|
||||
elif c.mapped_party_type == "Supplier":
|
||||
party = frappe.new_doc("Supplier")
|
||||
party.supplier_name = c.client_name
|
||||
if doc.default_supplier_group:
|
||||
party.supplier_group = doc.default_supplier_group
|
||||
if doc.default_payment_terms:
|
||||
party.payment_terms = doc.default_payment_terms
|
||||
party.insert(ignore_permissions=True)
|
||||
suppliers_count += 1
|
||||
erp_suppliers = frappe.get_all("Supplier", fields=["name", "supplier_name", "tax_id"])
|
||||
|
||||
# VÖEN lookup for exact matching
|
||||
voen_index = {}
|
||||
for s in erp_suppliers:
|
||||
if s.tax_id:
|
||||
voen_index[s.tax_id.strip()] = s.name
|
||||
|
||||
# Build dict of existing mappings: kb_supplier_name → erp_supplier
|
||||
existing_mappings = {
|
||||
row.kb_supplier_name: row.erp_supplier
|
||||
for row in doc.supplier_mappings if row.kb_supplier_name
|
||||
}
|
||||
|
||||
# Group 1: suppliers already in the table but with empty erp_supplier
|
||||
suppliers_with_empty_mapping = []
|
||||
for kb_name, erp_supplier in existing_mappings.items():
|
||||
if not erp_supplier:
|
||||
for s in new_suppliers:
|
||||
if s.name == kb_name:
|
||||
suppliers_with_empty_mapping.append(s)
|
||||
break
|
||||
|
||||
# Group 2: suppliers not in the table at all
|
||||
suppliers_not_in_table = [s for s in new_suppliers if s.name not in existing_mappings]
|
||||
|
||||
suppliers_to_process = suppliers_not_in_table + suppliers_with_empty_mapping
|
||||
|
||||
matched = 0
|
||||
for supplier in suppliers_to_process:
|
||||
best_party = None
|
||||
|
||||
# Priority 1: exact VÖEN match
|
||||
if supplier.tax_id and supplier.tax_id.strip() in voen_index:
|
||||
best_party = voen_index[supplier.tax_id.strip()]
|
||||
else:
|
||||
# Priority 2: name similarity
|
||||
kb_name = _normalize(supplier.supplier_name, consider_azeri)
|
||||
best_score = 0
|
||||
|
||||
for s in erp_suppliers:
|
||||
score = SequenceMatcher(None, kb_name, _normalize(s.supplier_name, consider_azeri)).ratio()
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_party = s.name
|
||||
|
||||
if best_score < threshold:
|
||||
best_party = None
|
||||
|
||||
if best_party:
|
||||
existing_idx = None
|
||||
for idx, row in enumerate(doc.supplier_mappings):
|
||||
if row.kb_supplier_name == supplier.name:
|
||||
existing_idx = idx
|
||||
break
|
||||
|
||||
if existing_idx is not None:
|
||||
doc.supplier_mappings[existing_idx].erp_supplier = best_party
|
||||
doc.supplier_mappings[existing_idx].mapping_type = "Automatic"
|
||||
else:
|
||||
doc.append("supplier_mappings", {
|
||||
"kb_supplier_name": supplier.name,
|
||||
"tax_id": supplier.tax_id or None,
|
||||
"erp_supplier": best_party,
|
||||
"mapping_type": "Automatic",
|
||||
})
|
||||
|
||||
frappe.db.set_value("Kapital Bank Supplier", supplier.name, {
|
||||
"mapped_supplier": best_party,
|
||||
"status": "Mapped",
|
||||
}, update_modified=False)
|
||||
matched += 1
|
||||
|
||||
if matched > 0:
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
frappe.db.commit()
|
||||
return {"success": True, "matched_count": matched, "total_processed": len(suppliers_to_process)}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"match_similar_suppliers: {e}\n{frappe.get_traceback()}", "KB Supplier Matching")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unmapped_customers(settings_name=None):
|
||||
"""Create Customer records for unmapped customers from Settings.customer_mappings table."""
|
||||
try:
|
||||
doc = frappe.get_doc("Kapital Bank Settings")
|
||||
|
||||
unmapped = []
|
||||
for mapping in doc.customer_mappings:
|
||||
if not mapping.erp_customer and mapping.kb_customer_name:
|
||||
if frappe.db.exists("Kapital Bank Customer", mapping.kb_customer_name):
|
||||
kb_customer = frappe.get_doc("Kapital Bank Customer", mapping.kb_customer_name)
|
||||
unmapped.append((mapping, kb_customer))
|
||||
|
||||
if not unmapped:
|
||||
return {"success": True, "created_count": 0, "message": "No unmapped customers in table to create"}
|
||||
|
||||
created = 0
|
||||
for mapping, kb_customer in unmapped:
|
||||
try:
|
||||
customer_name = kb_customer.customer_name
|
||||
if frappe.db.exists("Customer", {"customer_name": customer_name}):
|
||||
continue
|
||||
|
||||
frappe.db.set_value("Kapital Bank Client", c.name, {
|
||||
"mapped_party_type": c.mapped_party_type,
|
||||
"mapped_party": party.name,
|
||||
"status": "Mapped",
|
||||
}, update_modified=False)
|
||||
party = frappe.new_doc("Customer")
|
||||
party.customer_name = customer_name
|
||||
party.customer_type = "Company"
|
||||
|
||||
if c.client_name not in already_in_mappings:
|
||||
doc.append("client_mappings", {
|
||||
"client_name": c.client_name,
|
||||
"tax_id": c.tax_id or None,
|
||||
"party_type": c.mapped_party_type,
|
||||
"erp_party": party.name,
|
||||
"mapping_type": "Manual",
|
||||
})
|
||||
already_in_mappings.add(c.client_name)
|
||||
customer_group = mapping.customer_group or doc.default_customer_group
|
||||
if not customer_group or not frappe.db.exists("Customer Group", customer_group):
|
||||
customer_group = "All Customer Groups"
|
||||
party.customer_group = customer_group
|
||||
|
||||
territory = mapping.territory or doc.default_territory or "All Territories"
|
||||
if not frappe.db.exists("Territory", territory):
|
||||
territory = "All Territories"
|
||||
party.territory = territory
|
||||
|
||||
if kb_customer.tax_id:
|
||||
party.tax_id = kb_customer.tax_id
|
||||
|
||||
payment_terms = mapping.payment_terms or doc.default_payment_terms
|
||||
if payment_terms:
|
||||
party.payment_terms = payment_terms
|
||||
|
||||
party.insert(ignore_permissions=True)
|
||||
|
||||
# Update mapping row
|
||||
mapping.erp_customer = party.name
|
||||
mapping.mapping_type = "Automatic"
|
||||
|
||||
# Update KB Customer status
|
||||
frappe.db.set_value("Kapital Bank Customer", kb_customer.name, {
|
||||
"status": "Mapped",
|
||||
"mapped_customer": party.name,
|
||||
"customer_group": party.customer_group,
|
||||
"territory": party.territory,
|
||||
"payment_terms": payment_terms or None,
|
||||
}, update_modified=False)
|
||||
|
||||
created += 1
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
existing = None
|
||||
if c.mapped_party_type == "Customer":
|
||||
existing = frappe.db.get_value("Customer", {"customer_name": c.client_name}, "name")
|
||||
elif c.mapped_party_type == "Supplier":
|
||||
existing = frappe.db.get_value("Supplier", {"supplier_name": c.client_name}, "name")
|
||||
|
||||
if existing:
|
||||
frappe.db.set_value("Kapital Bank Client", c.name, {
|
||||
"mapped_party": existing,
|
||||
"status": "Mapped",
|
||||
}, update_modified=False)
|
||||
if c.client_name not in already_in_mappings:
|
||||
doc.append("client_mappings", {
|
||||
"client_name": c.client_name,
|
||||
"tax_id": c.tax_id or None,
|
||||
"party_type": c.mapped_party_type,
|
||||
"erp_party": existing,
|
||||
"mapping_type": "Manual",
|
||||
})
|
||||
already_in_mappings.add(c.client_name)
|
||||
|
||||
except Exception as inner_e:
|
||||
frappe.log_error(f"create_unmapped_clients: error for {c.name}: {inner_e}", "KB Client Creation")
|
||||
except Exception as e:
|
||||
frappe.log_error(f"create_unmapped_customers: error for {kb_customer.customer_name}: {e}", "KB Customer Creation")
|
||||
|
||||
if created > 0:
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
frappe.db.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"created_count": created,
|
||||
"customers_count": customers_count,
|
||||
"suppliers_count": suppliers_count,
|
||||
}
|
||||
return {"success": True, "created_count": created}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"create_unmapped_clients: {e}\n{frappe.get_traceback()}", "KB Client Creation")
|
||||
frappe.log_error(f"create_unmapped_customers: {e}\n{frappe.get_traceback()}", "KB Customer Creation")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unmapped_suppliers(settings_name=None):
|
||||
"""Create Supplier records for unmapped suppliers from Settings.supplier_mappings table."""
|
||||
try:
|
||||
doc = frappe.get_doc("Kapital Bank Settings")
|
||||
|
||||
unmapped = []
|
||||
for mapping in doc.supplier_mappings:
|
||||
if not mapping.erp_supplier and mapping.kb_supplier_name:
|
||||
if frappe.db.exists("Kapital Bank Supplier", mapping.kb_supplier_name):
|
||||
kb_supplier = frappe.get_doc("Kapital Bank Supplier", mapping.kb_supplier_name)
|
||||
unmapped.append((mapping, kb_supplier))
|
||||
|
||||
if not unmapped:
|
||||
return {"success": True, "created_count": 0, "message": "No unmapped suppliers in table to create"}
|
||||
|
||||
created = 0
|
||||
for mapping, kb_supplier in unmapped:
|
||||
try:
|
||||
supplier_name = kb_supplier.supplier_name
|
||||
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
|
||||
continue
|
||||
|
||||
party = frappe.new_doc("Supplier")
|
||||
party.supplier_name = supplier_name
|
||||
party.supplier_type = "Company"
|
||||
|
||||
supplier_group = mapping.supplier_group or doc.default_supplier_group
|
||||
if not supplier_group or not frappe.db.exists("Supplier Group", supplier_group):
|
||||
supplier_group = "All Supplier Groups"
|
||||
party.supplier_group = supplier_group
|
||||
|
||||
if kb_supplier.tax_id:
|
||||
party.tax_id = kb_supplier.tax_id
|
||||
|
||||
payment_terms = mapping.payment_terms or doc.default_payment_terms
|
||||
if payment_terms:
|
||||
party.payment_terms = payment_terms
|
||||
|
||||
party.insert(ignore_permissions=True)
|
||||
|
||||
# Update mapping row
|
||||
mapping.erp_supplier = party.name
|
||||
mapping.mapping_type = "Automatic"
|
||||
|
||||
# Update KB Supplier status
|
||||
frappe.db.set_value("Kapital Bank Supplier", kb_supplier.name, {
|
||||
"status": "Mapped",
|
||||
"mapped_supplier": party.name,
|
||||
"supplier_group": party.supplier_group,
|
||||
"payment_terms": payment_terms or None,
|
||||
}, update_modified=False)
|
||||
|
||||
created += 1
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"create_unmapped_suppliers: error for {kb_supplier.supplier_name}: {e}", "KB Supplier Creation")
|
||||
|
||||
if created > 0:
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
frappe.db.commit()
|
||||
return {"success": True, "created_count": created}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"create_unmapped_suppliers: {e}\n{frappe.get_traceback()}", "KB Supplier Creation")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
|
|
@ -610,6 +782,245 @@ def get_card_statement(login_name=None, account_no=None, from_date=None, to_date
|
|||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STATEMENT IMPORT (BIRBank → Payment Entry)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _parse_txn_date(trn_dt):
|
||||
"""Parse transaction date string into a date object. Returns None on failure."""
|
||||
trn_dt = (trn_dt or "").strip()
|
||||
if not trn_dt:
|
||||
return None
|
||||
for fmt in ("%b %d, %Y", "%b %d,%Y"):
|
||||
try:
|
||||
return datetime.strptime(trn_dt, fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _parse_counterparty(contr_raw):
|
||||
"""Parse contrAccount string → (name, voen)."""
|
||||
parts = [p.strip() for p in (contr_raw or "").split(" / ")]
|
||||
contr_name = parts[1] if len(parts) > 1 else ""
|
||||
contr_voen = parts[2] if len(parts) > 2 else ""
|
||||
return contr_name, contr_voen
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_statement_transactions(from_date, to_date, account_iban, login_name=None):
|
||||
"""Fetch bank statement and return parsed transactions for UI selection.
|
||||
|
||||
Filters out duplicates (already imported by reference_no).
|
||||
Returns: {success, transactions: [...], total_fetched, skipped_duplicates}
|
||||
"""
|
||||
try:
|
||||
account_iban = account_iban.strip()
|
||||
|
||||
# Find cust_ac_no for this IBAN
|
||||
cust_ac_no = frappe.db.get_value("Kapital Bank Account", account_iban, "cust_ac_no")
|
||||
if not cust_ac_no:
|
||||
return {"success": False, "message": f"Account {account_iban} not found in registry."}
|
||||
|
||||
# Fetch statement
|
||||
client = BIRBankClient(login_name)
|
||||
resp = client.get(
|
||||
"/v2/statement/account",
|
||||
params={
|
||||
"accountNumber": cust_ac_no,
|
||||
"fromDate": _fmt_date(from_date),
|
||||
"toDate": _fmt_date(to_date),
|
||||
}
|
||||
)
|
||||
if not resp.ok:
|
||||
return {"success": False, "message": f"HTTP {resp.status_code}: {resp.text[:200]}"}
|
||||
|
||||
data = resp.json()
|
||||
resp_code = data.get("response", {}).get("code", "?")
|
||||
if resp_code != "0":
|
||||
resp_msg = data.get("response", {}).get("message", "")
|
||||
return {"success": False, "message": f"API error code={resp_code}: {resp_msg}"}
|
||||
|
||||
statement_list = (
|
||||
data.get("responseData", {})
|
||||
.get("operations", {})
|
||||
.get("statementList", [])
|
||||
)
|
||||
|
||||
transactions = []
|
||||
skipped_duplicates = 0
|
||||
|
||||
for txn in statement_list:
|
||||
trn_ref = (txn.get("trnRefNo") or "").strip()
|
||||
|
||||
# Deduplication
|
||||
if trn_ref and frappe.db.exists("Payment Entry", {"reference_no": trn_ref}):
|
||||
skipped_duplicates += 1
|
||||
continue
|
||||
|
||||
# Parse date
|
||||
parsed_date = _parse_txn_date(txn.get("trnDt"))
|
||||
if not parsed_date:
|
||||
continue
|
||||
|
||||
# Amount and currency
|
||||
amount = abs(flt(txn.get("fcyAmount") or txn.get("lcyAmount") or 0))
|
||||
currency = (txn.get("acCcy") or "AZN").strip()
|
||||
|
||||
# Direction
|
||||
dr_cr = (txn.get("drcrInd") or "").upper()
|
||||
|
||||
# Counterparty
|
||||
contr_name, contr_voen = _parse_counterparty(txn.get("contrAccount"))
|
||||
|
||||
# Purpose
|
||||
purpose = (txn.get("purpose") or "").strip()
|
||||
|
||||
transactions.append({
|
||||
"ref_no": trn_ref,
|
||||
"date": str(parsed_date),
|
||||
"counterparty": contr_name,
|
||||
"contr_voen": contr_voen,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"drcr": dr_cr or "D",
|
||||
"purpose": purpose,
|
||||
"raw_data": txn,
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"transactions": transactions,
|
||||
"total_fetched": len(statement_list),
|
||||
"skipped_duplicates": skipped_duplicates,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"get_statement_transactions: {e}\n{frappe.get_traceback()}", "KB Import Statement")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_single_transaction(txn_data, account_iban, login_name=None):
|
||||
"""Import a single transaction → create Payment Entry.
|
||||
|
||||
txn_data comes as JSON string from the JS client.
|
||||
Returns: {success, payment_entry} or {success: False, error_type, message}
|
||||
"""
|
||||
try:
|
||||
if isinstance(txn_data, str):
|
||||
txn_data = json.loads(txn_data)
|
||||
|
||||
settings = frappe.get_single("Kapital Bank Settings")
|
||||
|
||||
# Build lookup dicts
|
||||
purpose_rules = []
|
||||
for row in settings.purpose_mappings:
|
||||
if row.purpose_keyword and row.paid_from and row.paid_to:
|
||||
purpose_rules.append((row.purpose_keyword.lower(), row.paid_from, row.paid_to))
|
||||
|
||||
# Build party lookups from BOTH customer and supplier mappings
|
||||
voen_to_party = {}
|
||||
name_to_party = {}
|
||||
|
||||
for row in settings.customer_mappings:
|
||||
if not row.erp_customer:
|
||||
continue
|
||||
entry = {"party_type": "Customer", "erp_party": row.erp_customer}
|
||||
if row.tax_id:
|
||||
voen_to_party[row.tax_id.strip()] = entry
|
||||
if row.kb_customer_name:
|
||||
# Resolve KB Customer name → customer_name for name matching
|
||||
kb_customer_name = frappe.db.get_value("Kapital Bank Customer", row.kb_customer_name, "customer_name")
|
||||
if kb_customer_name:
|
||||
name_to_party[kb_customer_name.strip()] = entry
|
||||
|
||||
for row in settings.supplier_mappings:
|
||||
if not row.erp_supplier:
|
||||
continue
|
||||
entry = {"party_type": "Supplier", "erp_party": row.erp_supplier}
|
||||
if row.tax_id:
|
||||
voen_to_party[row.tax_id.strip()] = entry
|
||||
if row.kb_supplier_name:
|
||||
kb_supplier_name = frappe.db.get_value("Kapital Bank Supplier", row.kb_supplier_name, "supplier_name")
|
||||
if kb_supplier_name:
|
||||
name_to_party[kb_supplier_name.strip()] = entry
|
||||
|
||||
# Parse fields
|
||||
ref_no = txn_data.get("ref_no", "")
|
||||
parsed_date = txn_data.get("date")
|
||||
amount = abs(flt(txn_data.get("amount", 0)))
|
||||
dr_cr = (txn_data.get("drcr") or "D").upper()
|
||||
payment_type = "Pay" if dr_cr == "D" else "Receive"
|
||||
purpose = (txn_data.get("purpose") or "").strip()
|
||||
contr_name = (txn_data.get("counterparty") or "").strip()
|
||||
contr_voen = (txn_data.get("contr_voen") or "").strip()
|
||||
|
||||
# Purpose mapping
|
||||
purpose_lower = purpose.lower()
|
||||
paid_from = paid_to = None
|
||||
for keyword, pf, pt in purpose_rules:
|
||||
if keyword in purpose_lower:
|
||||
paid_from = pf
|
||||
paid_to = pt
|
||||
break
|
||||
|
||||
if not paid_from or not paid_to:
|
||||
return {
|
||||
"success": False,
|
||||
"error_type": "unmapped_purpose",
|
||||
"message": f"No purpose mapping for: {purpose}",
|
||||
"unmatched_purpose": [{"purpose": purpose}],
|
||||
}
|
||||
|
||||
# Party mapping: VOEN → name → None
|
||||
party_type = None
|
||||
erp_party = None
|
||||
if contr_voen and contr_voen in voen_to_party:
|
||||
party_type = voen_to_party[contr_voen]["party_type"]
|
||||
erp_party = voen_to_party[contr_voen]["erp_party"]
|
||||
elif contr_name and contr_name in name_to_party:
|
||||
party_type = name_to_party[contr_name]["party_type"]
|
||||
erp_party = name_to_party[contr_name]["erp_party"]
|
||||
|
||||
if not party_type or not erp_party:
|
||||
return {
|
||||
"success": False,
|
||||
"error_type": "unmapped_party",
|
||||
"message": f"No party mapping for: {contr_name} (VOEN: {contr_voen})",
|
||||
"unmatched_party": [{"name": contr_name, "voen": contr_voen}],
|
||||
}
|
||||
|
||||
# Create Payment Entry
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = settings.default_company
|
||||
pe.posting_date = parsed_date
|
||||
pe.paid_from = paid_from
|
||||
pe.paid_to = paid_to
|
||||
pe.paid_amount = amount
|
||||
pe.received_amount = amount
|
||||
pe.reference_no = ref_no
|
||||
pe.reference_date = parsed_date
|
||||
pe.remarks = purpose
|
||||
pe.party_type = party_type
|
||||
pe.party = erp_party
|
||||
|
||||
try:
|
||||
pe.insert(ignore_permissions=True)
|
||||
pe.submit()
|
||||
except Exception as pe_err:
|
||||
frappe.db.rollback()
|
||||
frappe.local.message_log = []
|
||||
return {"success": False, "error_type": "import_error", "message": str(pe_err)}
|
||||
|
||||
return {"success": True, "payment_entry": pe.name}
|
||||
|
||||
except Exception as e:
|
||||
frappe.local.message_log = []
|
||||
return {"success": False, "error_type": "import_error", "message": str(e)}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# INTERNAL HELPERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -1,72 +1,592 @@
|
|||
frappe.ui.form.on('Payment Entry', {
|
||||
refresh(frm) {
|
||||
if (frm.doc.docstatus !== 1) return;
|
||||
const KBImport = {
|
||||
cancelLoading: false,
|
||||
loadingErrors: [],
|
||||
PROGRESS_UPDATE_DELAY: 50,
|
||||
};
|
||||
|
||||
const kb_status = frm.doc.kb_transfer_status;
|
||||
// ======= ERROR MANAGEMENT =======
|
||||
KBImport.errors = {
|
||||
add: function(txnData, errorType, errorMessage, additionalData = {}) {
|
||||
const errorEntry = {
|
||||
ref_no: txnData.ref_no || 'Unknown',
|
||||
date: txnData.date || 'Unknown',
|
||||
counterparty: txnData.counterparty || 'Unknown',
|
||||
amount: txnData.amount || 0,
|
||||
error_type: errorType,
|
||||
error_message: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
...additionalData
|
||||
};
|
||||
|
||||
frm.add_custom_button(__('Send Transfer'), () => {
|
||||
kb_send(frm, 'kapital_bank.payment_api.send_transfer', __('Transfer sent'));
|
||||
}, __('Kapital Bank'));
|
||||
KBImport.loadingErrors.push(errorEntry);
|
||||
console.error('KB Import Error:', errorEntry);
|
||||
},
|
||||
|
||||
frm.add_custom_button(__('Send Card Transfer'), () => {
|
||||
kb_send(frm, 'kapital_bank.payment_api.send_card_transfer', __('Card transfer sent'));
|
||||
}, __('Kapital Bank'));
|
||||
clear: function() {
|
||||
frappe.confirm(
|
||||
__('Are you sure you want to clear the error log?'),
|
||||
function() {
|
||||
KBImport.loadingErrors = [];
|
||||
frappe.show_alert({
|
||||
message: __('Error log cleared'),
|
||||
indicator: 'green'
|
||||
}, 2);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
frm.add_custom_button(__('Send VAT Payment'), () => {
|
||||
kb_send(frm, 'kapital_bank.payment_api.send_vat_payment', __('VAT payment sent'));
|
||||
}, __('Kapital Bank'));
|
||||
showDetails: function() {
|
||||
if (KBImport.loadingErrors.length === 0) {
|
||||
frappe.msgprint(__('No errors to display'));
|
||||
return;
|
||||
}
|
||||
|
||||
frm.add_custom_button(__('Check Status'), () => {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.payment_api.check_and_update_status',
|
||||
args: { payment_entry_name: frm.doc.name },
|
||||
freeze: true,
|
||||
freeze_message: __('Checking status...'),
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Status: {0}', [r.message.status]),
|
||||
indicator: 'blue'
|
||||
}, 5);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
message: r.message ? r.message.message : __('Unknown error'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
let errorsHtml = '<div class="error-log-container">';
|
||||
|
||||
KBImport.loadingErrors.forEach(function(error) {
|
||||
let documentDate = 'No date';
|
||||
if (error.date && error.date !== 'Unknown') {
|
||||
try {
|
||||
documentDate = moment(error.date).format('DD.MM.YYYY');
|
||||
} catch (e) {
|
||||
documentDate = 'Invalid date';
|
||||
}
|
||||
});
|
||||
}, __('Kapital Bank'));
|
||||
}
|
||||
|
||||
// Show KB status badge if present
|
||||
if (kb_status) {
|
||||
const color = kb_status === 'Confirmed' ? 'green'
|
||||
: kb_status === 'Cancelled' ? 'red'
|
||||
: 'blue';
|
||||
frm.dashboard.add_indicator(__('KB: {0}', [kb_status]), color);
|
||||
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
||||
|
||||
// Header
|
||||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||||
'<strong>' + error.ref_no + '</strong></h5>';
|
||||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||||
errorsHtml += '</div>';
|
||||
errorsHtml += '<small style="color: var(--text-muted);">' + error.counterparty + '</small>';
|
||||
errorsHtml += '</div>';
|
||||
|
||||
// Error message
|
||||
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
|
||||
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
||||
errorsHtml += '</div>';
|
||||
|
||||
// Details — unmapped purpose
|
||||
if (error.unmatched_purpose && error.unmatched_purpose.length > 0) {
|
||||
errorsHtml += '<div class="error-details">';
|
||||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped purpose:') + '</div>';
|
||||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||||
error.unmatched_purpose.forEach(function(item) {
|
||||
errorsHtml += '<li>' + item.purpose + '</li>';
|
||||
});
|
||||
errorsHtml += '</ul></div>';
|
||||
}
|
||||
|
||||
// Details — unmapped party
|
||||
if (error.unmatched_party && error.unmatched_party.length > 0) {
|
||||
errorsHtml += '<div class="error-details">';
|
||||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped party:') + '</div>';
|
||||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||||
error.unmatched_party.forEach(function(party) {
|
||||
errorsHtml += '<li>' + party.name;
|
||||
if (party.voen) {
|
||||
errorsHtml += ' <em>(VOEN: ' + party.voen + ')</em>';
|
||||
}
|
||||
errorsHtml += '</li>';
|
||||
});
|
||||
errorsHtml += '</ul></div>';
|
||||
}
|
||||
|
||||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||||
errorsHtml += '</div>';
|
||||
|
||||
errorsHtml += '</div>';
|
||||
});
|
||||
|
||||
errorsHtml += '</div>';
|
||||
|
||||
// Statistics
|
||||
const errorTypes = {};
|
||||
KBImport.loadingErrors.forEach(function(error) {
|
||||
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
|
||||
});
|
||||
|
||||
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
||||
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||
statsHtml += '<div>';
|
||||
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + KBImport.loadingErrors.length + '<br>';
|
||||
statsHtml += '<span style="color: var(--text-muted);">';
|
||||
Object.keys(errorTypes).forEach(function(type, index) {
|
||||
if (index > 0) statsHtml += ' • ';
|
||||
statsHtml += type + ': ' + errorTypes[type];
|
||||
});
|
||||
statsHtml += '</span>';
|
||||
statsHtml += '</div>';
|
||||
statsHtml += '<div>';
|
||||
statsHtml += '<button class="btn btn-default btn-sm" id="kb-export-csv-dialog-btn" style="margin-right: 5px;">' +
|
||||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
|
||||
statsHtml += '<button class="btn btn-warning btn-sm" id="kb-clear-log-dialog-btn">' +
|
||||
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
||||
statsHtml += '</div></div>';
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Error Details') + ' (' + KBImport.loadingErrors.length + ')',
|
||||
size: 'large',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'stats_html',
|
||||
fieldtype: 'HTML',
|
||||
options: statsHtml
|
||||
},
|
||||
{
|
||||
fieldname: 'errors_html',
|
||||
fieldtype: 'HTML',
|
||||
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Close'),
|
||||
primary_action: function() {
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
setTimeout(function() {
|
||||
$('#kb-export-csv-dialog-btn').off('click').on('click', function() {
|
||||
KBImport.errors.exportCSV();
|
||||
});
|
||||
|
||||
$('#kb-clear-log-dialog-btn').off('click').on('click', function() {
|
||||
KBImport.errors.clear();
|
||||
d.hide();
|
||||
});
|
||||
}, 200);
|
||||
},
|
||||
|
||||
exportCSV: function() {
|
||||
if (KBImport.loadingErrors.length === 0) {
|
||||
frappe.msgprint(__('No errors to export'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let csvContent = "data:text/csv;charset=utf-8,";
|
||||
csvContent += "Ref No,Date,Counterparty,Amount,Error Type,Error Message,Timestamp\n";
|
||||
|
||||
KBImport.loadingErrors.forEach(function(error) {
|
||||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||||
const cleanCounterparty = (error.counterparty || '').replace(/"/g, '""');
|
||||
|
||||
const row = [
|
||||
error.ref_no,
|
||||
error.date,
|
||||
cleanCounterparty,
|
||||
error.amount,
|
||||
error.error_type,
|
||||
cleanMessage,
|
||||
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
||||
].map(function(field) {
|
||||
return '"' + (field || '') + '"';
|
||||
}).join(',');
|
||||
|
||||
csvContent += row + "\n";
|
||||
});
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "kb_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
frappe.show_alert({
|
||||
message: __('Error log exported successfully'),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
} catch (e) {
|
||||
console.error('Export error:', e);
|
||||
frappe.show_alert({
|
||||
message: __('Failed to export error log'),
|
||||
indicator: 'red'
|
||||
}, 3);
|
||||
}
|
||||
},
|
||||
|
||||
showSummary: function(totalTransactions, processedCount, errorsCount) {
|
||||
if (errorsCount === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('Import Completed Successfully'),
|
||||
indicator: 'green',
|
||||
message: __('All ') + totalTransactions + __(' transactions were imported successfully.')
|
||||
});
|
||||
} else {
|
||||
const title = errorsCount === totalTransactions ? __('Import Failed') : __('Import Completed with Errors');
|
||||
const indicator = processedCount > 0 ? 'orange' : 'red';
|
||||
|
||||
let message = '<div style="margin-bottom: 15px;">';
|
||||
if (processedCount > 0) {
|
||||
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' transactions') + '<br>';
|
||||
}
|
||||
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' transactions');
|
||||
message += '</div>';
|
||||
|
||||
message += '<div style="text-align: center;">' +
|
||||
'<button class="btn btn-primary btn-sm" id="kb-view-error-details-btn" style="margin-right: 10px;">' +
|
||||
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
||||
'<button class="btn btn-default btn-sm" id="kb-export-error-log-btn">' +
|
||||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||||
'</div>';
|
||||
|
||||
const summaryDialog = frappe.msgprint({
|
||||
title: title,
|
||||
indicator: indicator,
|
||||
message: message
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
$('#kb-view-error-details-btn').off('click').on('click', function() {
|
||||
summaryDialog.hide();
|
||||
KBImport.errors.showDetails();
|
||||
});
|
||||
|
||||
$('#kb-export-error-log-btn').off('click').on('click', function() {
|
||||
KBImport.errors.exportCSV();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function kb_send(frm, method, success_msg) {
|
||||
frappe.call({
|
||||
method: method,
|
||||
args: { payment_entry_name: frm.doc.name },
|
||||
freeze: true,
|
||||
freeze_message: __('Sending to BIRBank...'),
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({ message: success_msg, indicator: 'green' }, 5);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('BIRBank Error'),
|
||||
message: r.message ? r.message.message : __('Unknown error'),
|
||||
indicator: 'red'
|
||||
// ======= IMPORT MODULE =======
|
||||
KBImport.import = {
|
||||
showDialog: function() {
|
||||
frappe.call({
|
||||
method: 'frappe.client.get',
|
||||
args: { doctype: 'Kapital Bank Settings', name: 'Kapital Bank Settings' },
|
||||
callback: function(r) {
|
||||
if (!r.message) {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Could not load Kapital Bank Settings') });
|
||||
return;
|
||||
}
|
||||
const mappings = r.message.account_mappings || [];
|
||||
const iban_options = mappings.filter(m => m.iban).map(m => m.iban);
|
||||
if (iban_options.length === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('No Accounts'),
|
||||
indicator: 'orange',
|
||||
message: __('Please add account mappings in Kapital Bank Settings first.')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const today = frappe.datetime.get_today();
|
||||
const first_day = frappe.datetime.month_start(today);
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Import Bank Transactions'),
|
||||
fields: [
|
||||
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period') },
|
||||
{ fieldname: 'date_from', fieldtype: 'Date', label: __('Date From'), reqd: 1, default: first_day },
|
||||
{ fieldname: 'col_break', fieldtype: 'Column Break' },
|
||||
{ fieldname: 'date_to', fieldtype: 'Date', label: __('Date To'), reqd: 1, default: today },
|
||||
{ fieldname: 'sec_account', fieldtype: 'Section Break', label: __('Account') },
|
||||
{
|
||||
fieldname: 'account_iban',
|
||||
fieldtype: 'Select',
|
||||
label: __('Account IBAN'),
|
||||
reqd: 1,
|
||||
options: iban_options.join('\n')
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Search'),
|
||||
primary_action: function() {
|
||||
const values = d.get_values();
|
||||
d.hide();
|
||||
KBImport.import.loadTransactions(values.date_from, values.date_to, values.account_iban);
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadTransactions: function(fromDate, toDate, accountIban) {
|
||||
frappe.show_progress(__('Loading...'), 0, 100, __('Fetching transactions from Kapital Bank...'));
|
||||
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.get_statement_transactions',
|
||||
args: {
|
||||
from_date: fromDate,
|
||||
to_date: toDate,
|
||||
account_iban: accountIban
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.hide_progress();
|
||||
|
||||
if (r.message && r.message.success) {
|
||||
const txns = r.message.transactions || [];
|
||||
const skipped = r.message.skipped_duplicates || 0;
|
||||
|
||||
if (txns.length === 0) {
|
||||
let msg = __('No new transactions found for the specified period.');
|
||||
if (skipped > 0) {
|
||||
msg += ' ' + __('({0} already imported)', [skipped]);
|
||||
}
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
message: msg
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipped > 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Skipped {0} already imported transactions', [skipped]),
|
||||
indicator: 'blue'
|
||||
}, 5);
|
||||
}
|
||||
|
||||
KBImport.import.showTransactionSelection(txns, accountIban);
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: r.message ? r.message.message : __('Unknown error')
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
frappe.hide_progress();
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Network error while fetching transactions') });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showTransactionSelection: function(txns, accountIban) {
|
||||
let txnTable = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered kb-txn-table" style="width: 100%; table-layout: fixed;">';
|
||||
txnTable += '<thead><tr>' +
|
||||
'<th style="width: 5%;"><input type="checkbox" class="kb-select-all-txns"></th>' +
|
||||
'<th style="width: 15%;">' + __('Ref No') + '</th>' +
|
||||
'<th style="width: 12%;">' + __('Date') + '</th>' +
|
||||
'<th style="width: 28%;">' + __('Counterparty') + '</th>' +
|
||||
'<th style="width: 14%; text-align: right;">' + __('Amount') + '</th>' +
|
||||
'<th style="width: 6%; text-align: center;">' + __('Type') + '</th>' +
|
||||
'<th style="width: 20%;">' + __('Purpose') + '</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
txns.forEach(function(txn, idx) {
|
||||
const displayDate = txn.date ? moment(txn.date).format('DD.MM.YYYY') : '';
|
||||
const amountFmt = parseFloat(txn.amount || 0).toFixed(2);
|
||||
const drcrLabel = txn.drcr === 'D'
|
||||
? '<span class="label label-danger">' + __('Pay') + '</span>'
|
||||
: '<span class="label label-success">' + __('Receive') + '</span>';
|
||||
|
||||
txnTable += '<tr>' +
|
||||
'<td><input type="checkbox" class="kb-select-txn" data-idx="' + idx + '"></td>' +
|
||||
'<td style="word-break: break-word;">' + (txn.ref_no || '') + '</td>' +
|
||||
'<td>' + displayDate + '</td>' +
|
||||
'<td style="word-break: break-word;">' + (txn.counterparty || '') + '</td>' +
|
||||
'<td style="text-align: right;">' + amountFmt + ' ' + (txn.currency || '') + '</td>' +
|
||||
'<td style="text-align: center;">' + drcrLabel + '</td>' +
|
||||
'<td style="word-break: break-word; font-size: 0.9em;">' + (txn.purpose || '') + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
txnTable += '</tbody></table></div>';
|
||||
|
||||
const infoMessage = '<div class="alert alert-info">' +
|
||||
__('Transactions found: ') + txns.length +
|
||||
'</div>';
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Select Transactions to Import'),
|
||||
size: 'large',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'info_html',
|
||||
fieldtype: 'HTML',
|
||||
options: infoMessage
|
||||
},
|
||||
{
|
||||
fieldname: 'txns_html',
|
||||
fieldtype: 'HTML',
|
||||
options: txnTable
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Load selected'),
|
||||
primary_action: function() {
|
||||
const selectedTxns = [];
|
||||
d.$wrapper.find('.kb-select-txn:checked').each(function() {
|
||||
const idx = $(this).data('idx');
|
||||
selectedTxns.push(txns[idx]);
|
||||
});
|
||||
|
||||
if (selectedTxns.length === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('Warning'),
|
||||
indicator: 'orange',
|
||||
message: __('No transactions selected')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
d.hide();
|
||||
KBImport.cancelLoading = false;
|
||||
KBImport.import.loadSelectedTransactions(selectedTxns, accountIban, 0, 0);
|
||||
},
|
||||
secondary_action_label: __('Cancel'),
|
||||
secondary_action: function() {
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.$wrapper.find('.modal-dialog').css({
|
||||
'max-width': '80%',
|
||||
'width': '80%',
|
||||
'margin': '30px auto'
|
||||
});
|
||||
|
||||
d.$wrapper.find('.modal-body').css({
|
||||
'padding': '15px'
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
d.$wrapper.find('.kb-select-all-txns').on('change', function() {
|
||||
const isChecked = $(this).prop('checked');
|
||||
d.$wrapper.find('.kb-select-txn').prop('checked', isChecked);
|
||||
});
|
||||
},
|
||||
|
||||
loadSelectedTransactions: function(txnList, accountIban, processedCount, currentIndex) {
|
||||
if (processedCount === 0 && currentIndex === 0) {
|
||||
KBImport.loadingErrors = [];
|
||||
window.kbTotalToProcess = txnList.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).off('progress-cancel.kb_loading_txns');
|
||||
|
||||
if (txnList.length === 0) {
|
||||
frappe.hide_progress();
|
||||
|
||||
const errorsCount = KBImport.loadingErrors.length;
|
||||
const totalTxns = window.kbTotalToProcess || processedCount;
|
||||
|
||||
KBImport.errors.showSummary(totalTxns, processedCount, errorsCount);
|
||||
window.kbTotalToProcess = null;
|
||||
|
||||
if (cur_list) {
|
||||
cur_list.refresh();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (KBImport.cancelLoading) {
|
||||
txnList = [];
|
||||
frappe.hide_progress();
|
||||
KBImport.import.loadSelectedTransactions(txnList, accountIban, processedCount, currentIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
const txnData = txnList.shift();
|
||||
const isLast = txnList.length === 0;
|
||||
const totalToProcess = window.kbTotalToProcess || (processedCount + txnList.length + 1);
|
||||
|
||||
currentIndex++;
|
||||
|
||||
frappe.show_progress(__('Importing Transactions'), currentIndex - 1, totalToProcess,
|
||||
__('Importing transaction ') + currentIndex + __(' of ') + totalToProcess, null, true);
|
||||
|
||||
$(document).on('progress-cancel.kb_loading_txns', function() {
|
||||
KBImport.cancelLoading = true;
|
||||
frappe.show_alert({
|
||||
message: __('Cancelling import... Finishing current transaction.'),
|
||||
indicator: 'orange'
|
||||
}, 3);
|
||||
frappe.hide_progress();
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.import_single_transaction',
|
||||
args: {
|
||||
txn_data: JSON.stringify(txnData),
|
||||
account_iban: accountIban
|
||||
},
|
||||
callback: function(importR) {
|
||||
KBImport.import._handleImportResult(importR, txnData,
|
||||
txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
||||
KBImport.errors.add(txnData, 'Network Error', errorMessage);
|
||||
KBImport.import._continueOrFinish(txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handleImportResult: function(importR, txnData, txnList, accountIban, processedCount, currentIndex, isLast) {
|
||||
if (importR.message && importR.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Transaction imported: ') + (txnData.ref_no || ''),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
|
||||
processedCount++;
|
||||
KBImport.import._continueOrFinish(txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
|
||||
} else if (importR.message && importR.message.error_type === 'unmapped_purpose') {
|
||||
KBImport.errors.add(txnData, 'Unmapped Purpose', importR.message.message, {
|
||||
unmatched_purpose: importR.message.unmatched_purpose || []
|
||||
});
|
||||
KBImport.import._continueOrFinish(txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
|
||||
} else if (importR.message && importR.message.error_type === 'unmapped_party') {
|
||||
KBImport.errors.add(txnData, 'Unmapped Party', importR.message.message, {
|
||||
unmatched_party: importR.message.unmatched_party || []
|
||||
});
|
||||
KBImport.import._continueOrFinish(txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
|
||||
} else {
|
||||
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
||||
KBImport.errors.add(txnData, 'Import Error', errorMessage);
|
||||
KBImport.import._continueOrFinish(txnList, accountIban, processedCount, currentIndex, isLast);
|
||||
}
|
||||
},
|
||||
|
||||
_continueOrFinish: function(txnList, accountIban, processedCount, currentIndex, isLast) {
|
||||
if (isLast) {
|
||||
frappe.hide_progress();
|
||||
$(document).off('progress-cancel.kb_loading_txns');
|
||||
|
||||
const totalToProcess = window.kbTotalToProcess || processedCount;
|
||||
const errorsCount = KBImport.loadingErrors.length;
|
||||
KBImport.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||||
|
||||
if (cur_list) {
|
||||
cur_list.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if (txnList.length === 0) {
|
||||
frappe.hide_progress();
|
||||
}
|
||||
|
||||
KBImport.import.loadSelectedTransactions(txnList, accountIban, processedCount, currentIndex);
|
||||
}, KBImport.PROGRESS_UPDATE_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
// ======= LIST VIEW SETUP =======
|
||||
frappe.listview_settings['Payment Entry'] = {
|
||||
onload(listview) {
|
||||
listview.page.add_menu_item(__('Import from Kapital Bank'), function() {
|
||||
KBImport.import.showDialog();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ app_license = "unlicense"
|
|||
after_install = "kapital_bank.setup.after_install"
|
||||
after_migrate = "kapital_bank.setup.after_migrate"
|
||||
|
||||
# Document Events
|
||||
doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_cancel": "kapital_bank.payment_api.on_cancel_payment_entry"
|
||||
}
|
||||
# JS for ERPNext forms
|
||||
doctype_js = {
|
||||
"Payment Entry": "client/payment_entry.js"
|
||||
}
|
||||
|
||||
doctype_list_js = {
|
||||
"Payment Entry": "client/payment_entry.js"
|
||||
}
|
||||
|
||||
# Scheduled Tasks (refresh JWT every 4 minutes)
|
||||
|
|
@ -23,11 +25,6 @@ scheduler_events = {
|
|||
}
|
||||
}
|
||||
|
||||
# JS for ERPNext forms
|
||||
doctype_js = {
|
||||
"Payment Entry": "kapital_bank/client/payment_entry.js"
|
||||
}
|
||||
|
||||
# Apps
|
||||
# ------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2026-02-24 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_id",
|
||||
"client_name",
|
||||
"party_type",
|
||||
"erp_party",
|
||||
"mapping_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax ID (VÖEN)"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Type",
|
||||
"options": "\nCustomer\nSupplier"
|
||||
},
|
||||
{
|
||||
"fieldname": "erp_party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "ERP Party",
|
||||
"options": "party_type"
|
||||
},
|
||||
{
|
||||
"default": "Manual",
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mapping Type",
|
||||
"options": "Manual\nAutomatic",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-24 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Client Mapping",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:customer_name",
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"status",
|
||||
"column_break_1",
|
||||
"iban",
|
||||
"bank_code",
|
||||
"customer_mapping_section",
|
||||
"mapped_customer",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"payment_terms"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer Name",
|
||||
"length": 500,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax ID"
|
||||
},
|
||||
{
|
||||
"default": "New",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "New\nMapped",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Bank Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_mapping_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Customer Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "mapped_customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mapped Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Group",
|
||||
"options": "Customer Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms",
|
||||
"options": "Payment Terms Template",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Customer",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -2,5 +2,5 @@ import frappe
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class KapitalBankClientMapping(Document):
|
||||
pass
|
||||
class KapitalBankCustomer(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"kb_customer_name",
|
||||
"tax_id",
|
||||
"erp_customer",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"payment_terms",
|
||||
"mapping_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "kb_customer_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "KB Customer Name",
|
||||
"options": "Kapital Bank Customer",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "erp_customer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "ERP Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Group",
|
||||
"options": "Customer Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"default": "Manual",
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mapping Type",
|
||||
"options": "Manual\nAutomatic",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Customer Mapping",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class KapitalBankCustomerMapping(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purpose_keyword",
|
||||
"column_break_1",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "purpose_keyword",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Purpose Keyword",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_from",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Paid From (Account)",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_to",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Paid To (Account)",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Data",
|
||||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 0,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Purpose Mapping",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -2,5 +2,5 @@ import frappe
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class KapitalBankClient(Document):
|
||||
class KapitalBankPurposeMapping(Document):
|
||||
pass
|
||||
|
|
@ -14,70 +14,127 @@ frappe.ui.form.on('Kapital Bank Settings', {
|
|||
}
|
||||
}, __('Accounts'));
|
||||
|
||||
// ── Clients buttons ───────────────────────────────────────────────────
|
||||
frm.add_custom_button(__('Match clients by similar name'), () => {
|
||||
frappe.confirm(
|
||||
__('This will automatically match unmapped clients with similar Customer/Supplier names. Continue?'),
|
||||
() => {
|
||||
frappe.show_alert({ message: __('Matching clients...'), indicator: 'blue' });
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.match_similar_clients',
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Matched {0} of {1} clients', [r.message.matched_count, r.message.total_processed]),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error matching clients') });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}, __('Clients'));
|
||||
|
||||
frm.add_custom_button(__('Create matching clients'), () => {
|
||||
frappe.confirm(
|
||||
__('<div style="color:red;font-weight:bold;">WARNING! This will create new Customer/Supplier records for all unmapped clients!</div><p>This action is irreversible. Are you sure?</p>'),
|
||||
() => {
|
||||
frappe.confirm(__('Are you really sure? This cannot be undone.'), () => {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.create_unmapped_clients',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating clients...'),
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Created {0} new parties ({1} customers, {2} suppliers)',
|
||||
[r.message.created_count, r.message.customers_count, r.message.suppliers_count]),
|
||||
indicator: 'green'
|
||||
}, 6);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error creating clients') });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}, __('Clients'));
|
||||
|
||||
frm.add_custom_button(__('Add unmapped clients'), () => {
|
||||
// ── Customers buttons ─────────────────────────────────────────────────
|
||||
frm.add_custom_button(__('Match customers by similar name'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frm.save().then(() => add_unmapped_clients(frm));
|
||||
frappe.confirm(
|
||||
__('Document contains unsaved changes. Save before performing the operation?'),
|
||||
() => frm.save().then(() => _match_customers(frm)),
|
||||
() => frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' }, 5)
|
||||
);
|
||||
} else {
|
||||
add_unmapped_clients(frm);
|
||||
_match_customers(frm);
|
||||
}
|
||||
}, __('Clients'));
|
||||
}, __('Customers'));
|
||||
|
||||
frm.add_custom_button(__('Create matching customers'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frappe.confirm(
|
||||
__('Document contains unsaved changes. Save before performing the operation?'),
|
||||
() => frm.save().then(() => create_unmapped_customers(frm)),
|
||||
() => frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' }, 5)
|
||||
);
|
||||
} else {
|
||||
create_unmapped_customers(frm);
|
||||
}
|
||||
}, __('Customers'));
|
||||
|
||||
frm.add_custom_button(__('Add unmapped customers'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frm.save().then(() => add_unmapped_customers(frm));
|
||||
} else {
|
||||
add_unmapped_customers(frm);
|
||||
}
|
||||
}, __('Customers'));
|
||||
|
||||
// ── Suppliers buttons ─────────────────────────────────────────────────
|
||||
frm.add_custom_button(__('Match suppliers by similar name'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frappe.confirm(
|
||||
__('Document contains unsaved changes. Save before performing the operation?'),
|
||||
() => frm.save().then(() => _match_suppliers(frm)),
|
||||
() => frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' }, 5)
|
||||
);
|
||||
} else {
|
||||
_match_suppliers(frm);
|
||||
}
|
||||
}, __('Suppliers'));
|
||||
|
||||
frm.add_custom_button(__('Create matching suppliers'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frappe.confirm(
|
||||
__('Document contains unsaved changes. Save before performing the operation?'),
|
||||
() => frm.save().then(() => create_unmapped_suppliers(frm)),
|
||||
() => frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' }, 5)
|
||||
);
|
||||
} else {
|
||||
create_unmapped_suppliers(frm);
|
||||
}
|
||||
}, __('Suppliers'));
|
||||
|
||||
frm.add_custom_button(__('Add unmapped suppliers'), () => {
|
||||
if (frm.is_dirty()) {
|
||||
frm.save().then(() => add_unmapped_suppliers(frm));
|
||||
} else {
|
||||
add_unmapped_suppliers(frm);
|
||||
}
|
||||
}, __('Suppliers'));
|
||||
|
||||
// ── Populate data tabs ────────────────────────────────────────────────
|
||||
load_data_tabs(frm);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MATCH HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _match_customers(frm) {
|
||||
frappe.confirm(
|
||||
__('This will automatically match unmapped customers with similar Customer names. Continue?'),
|
||||
() => {
|
||||
frappe.show_alert({ message: __('Matching customers...'), indicator: 'blue' });
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.match_similar_customers',
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Matched {0} of {1} customers', [r.message.matched_count, r.message.total_processed]),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error matching customers') });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function _match_suppliers(frm) {
|
||||
frappe.confirm(
|
||||
__('This will automatically match unmapped suppliers with similar Supplier names. Continue?'),
|
||||
() => {
|
||||
frappe.show_alert({ message: __('Matching suppliers...'), indicator: 'blue' });
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.match_similar_suppliers',
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Matched {0} of {1} suppliers', [r.message.matched_count, r.message.total_processed]),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error matching suppliers') });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// LOAD DATA DIALOG
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -98,15 +155,16 @@ function open_load_dialog(frm, summary) {
|
|||
<div class="row">`;
|
||||
|
||||
const types = [
|
||||
{ key: 'accounts', label: __('Accounts') },
|
||||
{ key: 'cards', label: __('Cards') },
|
||||
{ key: 'clients', label: __('Clients') }
|
||||
{ key: 'accounts', label: __('Accounts') },
|
||||
{ key: 'cards', label: __('Cards') },
|
||||
{ key: 'customers', label: __('Customers') },
|
||||
{ key: 'suppliers', label: __('Suppliers') }
|
||||
];
|
||||
types.forEach(t => {
|
||||
const d = summary[t.key] || { total: 0, mapped: 0 };
|
||||
const unmapped = d.total - d.mapped;
|
||||
const pct = d.total > 0 ? Math.round(d.mapped / d.total * 100) : 0;
|
||||
summary_html += `<div class="col-sm-4">
|
||||
summary_html += `<div class="col-sm-3">
|
||||
<div class="text-center" style="border:1px solid #ddd;padding:10px;border-radius:4px;background:#fff;">
|
||||
<h6 style="margin-bottom:8px;">${t.label}</h6>
|
||||
<div><strong>${d.total}</strong> ${__('total')}</div>
|
||||
|
|
@ -126,7 +184,7 @@ function open_load_dialog(frm, summary) {
|
|||
title: __('Load Data from BIRBank'),
|
||||
fields: [
|
||||
{ fieldname: 'summary_html', fieldtype: 'HTML', options: summary_html },
|
||||
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period (for clients from statements)') },
|
||||
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period (for counterparties from statements)') },
|
||||
{ fieldname: 'date_from', fieldtype: 'Date', label: __('Date From'), default: first_day, reqd: 0 },
|
||||
{ fieldname: 'col_break', fieldtype: 'Column Break' },
|
||||
{ fieldname: 'date_to', fieldtype: 'Date', label: __('Date To'), default: last_day, reqd: 0 },
|
||||
|
|
@ -135,13 +193,13 @@ function open_load_dialog(frm, summary) {
|
|||
description: __('Fetch current accounts from GET /accounts') },
|
||||
{ fieldname: 'load_cards', fieldtype: 'Check', label: __('Load Cards'), default: 1,
|
||||
description: __('Fetch business cards from GET /cards') },
|
||||
{ fieldname: 'load_clients', fieldtype: 'Check', label: __('Load Clients from statements'), default: 1,
|
||||
{ fieldname: 'load_counterparties', fieldtype: 'Check', label: __('Load Counterparties from statements'), default: 1,
|
||||
description: __('Extract counterparties from account statement transactions for the selected period') }
|
||||
],
|
||||
primary_action_label: __('Load'),
|
||||
primary_action(values) {
|
||||
if (values.load_clients && (!values.date_from || !values.date_to)) {
|
||||
frappe.msgprint(__('Please set Date From and Date To to load clients from statements.'));
|
||||
if (values.load_counterparties && (!values.date_from || !values.date_to)) {
|
||||
frappe.msgprint(__('Please set Date From and Date To to load counterparties from statements.'));
|
||||
return;
|
||||
}
|
||||
d.hide();
|
||||
|
|
@ -152,7 +210,7 @@ function open_load_dialog(frm, summary) {
|
|||
}
|
||||
|
||||
function start_loading(frm, values) {
|
||||
let total = (values.load_accounts ? 1 : 0) + (values.load_cards ? 1 : 0) + (values.load_clients ? 1 : 0);
|
||||
let total = (values.load_accounts ? 1 : 0) + (values.load_cards ? 1 : 0) + (values.load_counterparties ? 1 : 0);
|
||||
if (total === 0) { frappe.show_alert({ message: __('Nothing selected'), indicator: 'orange' }, 3); return; }
|
||||
|
||||
let done = 0;
|
||||
|
|
@ -198,19 +256,20 @@ function start_loading(frm, values) {
|
|||
});
|
||||
}
|
||||
|
||||
if (values.load_clients) {
|
||||
frappe.show_progress(__('Loading clients...'), 50, 100, __('Fetching statements to extract counterparties...'));
|
||||
if (values.load_counterparties) {
|
||||
frappe.show_progress(__('Loading counterparties...'), 50, 100, __('Fetching statements to extract counterparties...'));
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.load_clients_from_statements',
|
||||
method: 'kapital_bank.bank_api.load_counterparties_from_statements',
|
||||
args: { from_date: values.date_from, to_date: values.date_to },
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
on_done(__('Clients: {0} new, {1} skipped (already exist)', [r.message.created, r.message.skipped]));
|
||||
on_done(__('Counterparties: {0} customers, {1} suppliers, {2} skipped',
|
||||
[r.message.customers_created, r.message.suppliers_created, r.message.skipped]));
|
||||
} else {
|
||||
on_done(__('Clients error: {0}', [r.message ? r.message.message : 'unknown']));
|
||||
on_done(__('Counterparties error: {0}', [r.message ? r.message.message : 'unknown']));
|
||||
}
|
||||
},
|
||||
error() { on_done(__('Clients: network error')); }
|
||||
error() { on_done(__('Counterparties: network error')); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -247,36 +306,126 @@ function add_unmapped_accounts(frm) {
|
|||
});
|
||||
}
|
||||
|
||||
function add_unmapped_clients(frm) {
|
||||
function add_unmapped_customers(frm) {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.get_unmapped_clients',
|
||||
method: 'kapital_bank.bank_api.get_unmapped_customers',
|
||||
callback(r) {
|
||||
if (!r.message || !r.message.success) {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error') });
|
||||
return;
|
||||
}
|
||||
const clients = r.message.clients;
|
||||
if (!clients || clients.length === 0) {
|
||||
frappe.show_alert({ message: __('No unmapped clients found'), indicator: 'blue' }, 5);
|
||||
const customers = r.message.customers;
|
||||
if (!customers || customers.length === 0) {
|
||||
frappe.show_alert({ message: __('No unmapped customers found'), indicator: 'blue' }, 5);
|
||||
return;
|
||||
}
|
||||
clients.forEach(c => {
|
||||
const row = frm.add_child('client_mappings');
|
||||
row.tax_id = c.tax_id || '';
|
||||
row.client_name = c.client_name || '';
|
||||
row.party_type = c.mapped_party_type || '';
|
||||
row.erp_party = c.mapped_party || '';
|
||||
customers.forEach(c => {
|
||||
const row = frm.add_child('customer_mappings');
|
||||
row.kb_customer_name = c.name;
|
||||
row.tax_id = c.tax_id || '';
|
||||
row.mapping_type = 'Manual';
|
||||
});
|
||||
frm.refresh_field('client_mappings');
|
||||
frm.refresh_field('customer_mappings');
|
||||
frm.save().then(() => {
|
||||
frappe.show_alert({ message: __('Added {0} clients to mapping', [clients.length]), indicator: 'green' }, 5);
|
||||
load_clients_tab();
|
||||
frappe.show_alert({ message: __('Added {0} customers to mapping', [customers.length]), indicator: 'green' }, 5);
|
||||
load_customers_tab();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function add_unmapped_suppliers(frm) {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.get_unmapped_suppliers',
|
||||
callback(r) {
|
||||
if (!r.message || !r.message.success) {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: r.message ? r.message.message : __('Error') });
|
||||
return;
|
||||
}
|
||||
const suppliers = r.message.suppliers;
|
||||
if (!suppliers || suppliers.length === 0) {
|
||||
frappe.show_alert({ message: __('No unmapped suppliers found'), indicator: 'blue' }, 5);
|
||||
return;
|
||||
}
|
||||
suppliers.forEach(s => {
|
||||
const row = frm.add_child('supplier_mappings');
|
||||
row.kb_supplier_name = s.name;
|
||||
row.tax_id = s.tax_id || '';
|
||||
row.mapping_type = 'Manual';
|
||||
});
|
||||
frm.refresh_field('supplier_mappings');
|
||||
frm.save().then(() => {
|
||||
frappe.show_alert({ message: __('Added {0} suppliers to mapping', [suppliers.length]), indicator: 'green' }, 5);
|
||||
load_suppliers_tab();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function create_unmapped_customers(frm) {
|
||||
frappe.confirm(
|
||||
__('<div style="color:red;font-weight:bold;">WARNING! This will create new Customer records for all unmapped customers!</div><p>This action is irreversible. Are you sure?</p>'),
|
||||
function() {
|
||||
frappe.confirm(
|
||||
__('Are you really sure? This cannot be undone.'),
|
||||
function() {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.create_unmapped_customers',
|
||||
args: { 'settings_name': frm.doc.name },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Created {0} new customers', [r.message.created_count]),
|
||||
indicator: 'green'
|
||||
}, 6);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: r.message ? r.message.message : __('Error creating customers')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function create_unmapped_suppliers(frm) {
|
||||
frappe.confirm(
|
||||
__('<div style="color:red;font-weight:bold;">WARNING! This will create new Supplier records for all unmapped suppliers!</div><p>This action is irreversible. Are you sure?</p>'),
|
||||
function() {
|
||||
frappe.confirm(
|
||||
__('Are you really sure? This cannot be undone.'),
|
||||
function() {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.create_unmapped_suppliers',
|
||||
args: { 'settings_name': frm.doc.name },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
frappe.show_alert({
|
||||
message: __('Created {0} new suppliers', [r.message.created_count]),
|
||||
indicator: 'green'
|
||||
}, 6);
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: r.message ? r.message.message : __('Error creating suppliers')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DATA TABS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -284,7 +433,8 @@ function add_unmapped_clients(frm) {
|
|||
function load_data_tabs(frm) {
|
||||
load_accounts_tab();
|
||||
load_cards_tab();
|
||||
load_clients_tab();
|
||||
load_customers_tab();
|
||||
load_suppliers_tab();
|
||||
}
|
||||
|
||||
// ─── Accounts tab ────────────────────────────────────────────────────────────
|
||||
|
|
@ -409,64 +559,121 @@ function render_cards_list(cards, total_count) {
|
|||
});
|
||||
}
|
||||
|
||||
// ─── Clients tab ─────────────────────────────────────────────────────────────
|
||||
// ─── Customers tab ───────────────────────────────────────────────────────────
|
||||
|
||||
function load_clients_tab() {
|
||||
function load_customers_tab() {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.get_kb_reference_data_list',
|
||||
args: { data_type: 'clients', limit: 100, offset: 0 },
|
||||
args: { data_type: 'customers', limit: 100, offset: 0 },
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
render_clients_list(r.message.data, r.message.total_count);
|
||||
render_customers_list(r.message.data, r.message.total_count);
|
||||
} else {
|
||||
const msg = r.message ? r.message.message : __('Unknown error');
|
||||
$('#kb-clients-container').html(error_html(msg, load_clients_tab));
|
||||
$('#kb-customers-container').html(error_html(msg, load_customers_tab));
|
||||
}
|
||||
},
|
||||
error() { $('#kb-clients-container').html(error_html(__('Network error'), load_clients_tab)); }
|
||||
error() { $('#kb-customers-container').html(error_html(__('Network error'), load_customers_tab)); }
|
||||
});
|
||||
}
|
||||
|
||||
function render_clients_list(clients, total_count) {
|
||||
let html = list_header(__('Clients'), total_count, 'kb-refresh-clients');
|
||||
function render_customers_list(customers, total_count) {
|
||||
let html = list_header(__('Customers'), total_count, 'kb-refresh-customers');
|
||||
|
||||
if (clients && clients.length > 0) {
|
||||
if (customers && customers.length > 0) {
|
||||
html += `<table class="table table-bordered" style="font-size:13px;">
|
||||
<thead class="grid-heading-row"><tr>
|
||||
<th>${__('Client Name')}</th>
|
||||
<th>${__('Tax ID (VÖEN)')}</th>
|
||||
<th>${__('Customer Name')}</th>
|
||||
<th>${__('Tax ID (VOEN)')}</th>
|
||||
<th>${__('IBAN')}</th>
|
||||
<th>${__('Status')}</th>
|
||||
<th>${__('Party Type')}</th>
|
||||
<th>${__('Mapped To')}</th>
|
||||
<th>${__('Created')}</th>
|
||||
</tr></thead><tbody>`;
|
||||
clients.forEach(c => {
|
||||
const pill = c.status === 'Mapped' ? 'green' : (c.status === 'Active' ? 'blue' : 'red');
|
||||
const mapped_to = c.mapped_party ? `<a href="/app/${(c.mapped_party_type || 'customer').toLowerCase()}/${c.mapped_party}">${c.mapped_party}</a>` : '-';
|
||||
customers.forEach(c => {
|
||||
const pill = c.status === 'Mapped' ? 'green' : 'red';
|
||||
const mapped_to = c.mapped_customer ? `<a href="/app/customer/${c.mapped_customer}">${c.mapped_customer}</a>` : '-';
|
||||
const created = c.creation ? frappe.datetime.str_to_user(c.creation) : '-';
|
||||
html += `<tr>
|
||||
<td><a href="/app/kapital-bank-client/${c.name}">${c.client_name}</a></td>
|
||||
<td><a href="/app/kapital-bank-customer/${c.name}">${c.customer_name}</a></td>
|
||||
<td>${c.tax_id || '-'}</td>
|
||||
<td>${c.iban || '-'}</td>
|
||||
<td><span class="indicator-pill ${pill}">${__(c.status)}</span></td>
|
||||
<td>${c.mapped_party_type || '-'}</td>
|
||||
<td>${mapped_to}</td>
|
||||
<td>${created}</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
if (total_count > clients.length) {
|
||||
html += `<p class="text-muted small">${__('Showing {0} of {1}', [clients.length, total_count])} — <a href="/app/kapital-bank-client">${__('View All')}</a></p>`;
|
||||
if (total_count > customers.length) {
|
||||
html += `<p class="text-muted small">${__('Showing {0} of {1}', [customers.length, total_count])} — <a href="/app/kapital-bank-customer">${__('View All')}</a></p>`;
|
||||
}
|
||||
} else {
|
||||
html += empty_state(__('No clients registered yet.'));
|
||||
html += empty_state(__('No customers registered yet.'));
|
||||
}
|
||||
|
||||
$('#kb-clients-container').html(html);
|
||||
$('#kb-refresh-clients').on('click', () => {
|
||||
$('#kb-clients-container').html(loading_html());
|
||||
load_clients_tab();
|
||||
$('#kb-customers-container').html(html);
|
||||
$('#kb-refresh-customers').on('click', () => {
|
||||
$('#kb-customers-container').html(loading_html());
|
||||
load_customers_tab();
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Suppliers tab ───────────────────────────────────────────────────────────
|
||||
|
||||
function load_suppliers_tab() {
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.get_kb_reference_data_list',
|
||||
args: { data_type: 'suppliers', limit: 100, offset: 0 },
|
||||
callback(r) {
|
||||
if (r.message && r.message.success) {
|
||||
render_suppliers_list(r.message.data, r.message.total_count);
|
||||
} else {
|
||||
const msg = r.message ? r.message.message : __('Unknown error');
|
||||
$('#kb-suppliers-container').html(error_html(msg, load_suppliers_tab));
|
||||
}
|
||||
},
|
||||
error() { $('#kb-suppliers-container').html(error_html(__('Network error'), load_suppliers_tab)); }
|
||||
});
|
||||
}
|
||||
|
||||
function render_suppliers_list(suppliers, total_count) {
|
||||
let html = list_header(__('Suppliers'), total_count, 'kb-refresh-suppliers');
|
||||
|
||||
if (suppliers && suppliers.length > 0) {
|
||||
html += `<table class="table table-bordered" style="font-size:13px;">
|
||||
<thead class="grid-heading-row"><tr>
|
||||
<th>${__('Supplier Name')}</th>
|
||||
<th>${__('Tax ID (VOEN)')}</th>
|
||||
<th>${__('IBAN')}</th>
|
||||
<th>${__('Status')}</th>
|
||||
<th>${__('Mapped To')}</th>
|
||||
<th>${__('Created')}</th>
|
||||
</tr></thead><tbody>`;
|
||||
suppliers.forEach(s => {
|
||||
const pill = s.status === 'Mapped' ? 'green' : 'red';
|
||||
const mapped_to = s.mapped_supplier ? `<a href="/app/supplier/${s.mapped_supplier}">${s.mapped_supplier}</a>` : '-';
|
||||
const created = s.creation ? frappe.datetime.str_to_user(s.creation) : '-';
|
||||
html += `<tr>
|
||||
<td><a href="/app/kapital-bank-supplier/${s.name}">${s.supplier_name}</a></td>
|
||||
<td>${s.tax_id || '-'}</td>
|
||||
<td>${s.iban || '-'}</td>
|
||||
<td><span class="indicator-pill ${pill}">${__(s.status)}</span></td>
|
||||
<td>${mapped_to}</td>
|
||||
<td>${created}</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
if (total_count > suppliers.length) {
|
||||
html += `<p class="text-muted small">${__('Showing {0} of {1}', [suppliers.length, total_count])} — <a href="/app/kapital-bank-supplier">${__('View All')}</a></p>`;
|
||||
}
|
||||
} else {
|
||||
html += empty_state(__('No suppliers registered yet.'));
|
||||
}
|
||||
|
||||
$('#kb-suppliers-container').html(html);
|
||||
$('#kb-refresh-suppliers').on('click', () => {
|
||||
$('#kb-suppliers-container').html(loading_html());
|
||||
load_suppliers_tab();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,14 +22,22 @@
|
|||
"accounts_list_html",
|
||||
"cards_tab",
|
||||
"cards_list_html",
|
||||
"clients_tab",
|
||||
"clients_list_html",
|
||||
"customers_tab",
|
||||
"customers_list_html",
|
||||
"suppliers_tab",
|
||||
"suppliers_list_html",
|
||||
"account_mappings_tab",
|
||||
"account_mappings_section",
|
||||
"account_mappings",
|
||||
"client_mappings_tab",
|
||||
"client_mappings_section",
|
||||
"client_mappings"
|
||||
"customer_mappings_tab",
|
||||
"customer_mappings_section",
|
||||
"customer_mappings",
|
||||
"supplier_mappings_tab",
|
||||
"supplier_mappings_section",
|
||||
"supplier_mappings",
|
||||
"purpose_mappings_tab",
|
||||
"purpose_mappings_section",
|
||||
"purpose_mappings"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -135,15 +143,26 @@
|
|||
"options": "<div id=\"kb-cards-container\"><div class=\"text-center text-muted\" style=\"padding:20px;\">Click \"Load Data\" to fetch cards from BIRBank</div></div>"
|
||||
},
|
||||
{
|
||||
"fieldname": "clients_tab",
|
||||
"fieldname": "customers_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Clients"
|
||||
"label": "Customers"
|
||||
},
|
||||
{
|
||||
"fieldname": "clients_list_html",
|
||||
"fieldname": "customers_list_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Clients List",
|
||||
"options": "<div id=\"kb-clients-container\"><div class=\"text-center text-muted\" style=\"padding:20px;\">Loading...</div></div>"
|
||||
"label": "Customers List",
|
||||
"options": "<div id=\"kb-customers-container\"><div class=\"text-center text-muted\" style=\"padding:20px;\">Loading...</div></div>"
|
||||
},
|
||||
{
|
||||
"fieldname": "suppliers_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Suppliers"
|
||||
},
|
||||
{
|
||||
"fieldname": "suppliers_list_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Suppliers List",
|
||||
"options": "<div id=\"kb-suppliers-container\"><div class=\"text-center text-muted\" style=\"padding:20px;\">Loading...</div></div>"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_mappings_tab",
|
||||
|
|
@ -162,26 +181,58 @@
|
|||
"options": "Kapital Bank Account Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_mappings_tab",
|
||||
"fieldname": "customer_mappings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Client Mappings"
|
||||
"label": "Customer Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_mappings_section",
|
||||
"fieldname": "customer_mappings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Client Mappings"
|
||||
"label": "Customer Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_mappings",
|
||||
"fieldname": "customer_mappings",
|
||||
"fieldtype": "Table",
|
||||
"label": "Client Mappings",
|
||||
"options": "Kapital Bank Client Mapping"
|
||||
"label": "Customer Mappings",
|
||||
"options": "Kapital Bank Customer Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_mappings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Supplier Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_mappings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_mappings",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplier Mappings",
|
||||
"options": "Kapital Bank Supplier Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "purpose_mappings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Purpose Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "purpose_mappings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purpose Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "purpose_mappings",
|
||||
"fieldtype": "Table",
|
||||
"label": "Purpose Mappings",
|
||||
"options": "Kapital Bank Purpose Mapping"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-25 00:00:00.000000",
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Settings",
|
||||
|
|
|
|||
|
|
@ -3,29 +3,55 @@ from frappe.model.document import Document
|
|||
|
||||
|
||||
class KapitalBankSettings(Document):
|
||||
def on_update(self):
|
||||
self._sync_client_statuses()
|
||||
def on_update(self):
|
||||
self._sync_customer_statuses()
|
||||
self._sync_supplier_statuses()
|
||||
|
||||
def _sync_client_statuses(self):
|
||||
"""Reset Kapital Bank Client status to 'New' for clients removed from client_mappings."""
|
||||
mapped_names = {row.client_name for row in self.client_mappings if row.client_name}
|
||||
def _sync_customer_statuses(self):
|
||||
"""Reset Kapital Bank Customer status to 'New' for customers removed from customer_mappings."""
|
||||
mapped_names = {row.kb_customer_name for row in self.customer_mappings if row.kb_customer_name}
|
||||
|
||||
# Find clients marked as Mapped but no longer present in the mapping table
|
||||
orphaned = frappe.get_all(
|
||||
"Kapital Bank Client",
|
||||
filters={"status": "Mapped"},
|
||||
fields=["name", "client_name"],
|
||||
)
|
||||
orphaned = frappe.get_all(
|
||||
"Kapital Bank Customer",
|
||||
filters={"status": "Mapped"},
|
||||
fields=["name", "customer_name"],
|
||||
)
|
||||
|
||||
for client in orphaned:
|
||||
if client.client_name not in mapped_names:
|
||||
frappe.db.set_value(
|
||||
"Kapital Bank Client",
|
||||
client.name,
|
||||
{
|
||||
"status": "New",
|
||||
"mapped_party_type": None,
|
||||
"mapped_party": None,
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
for customer in orphaned:
|
||||
if customer.name not in mapped_names:
|
||||
frappe.db.set_value(
|
||||
"Kapital Bank Customer",
|
||||
customer.name,
|
||||
{
|
||||
"status": "New",
|
||||
"mapped_customer": None,
|
||||
"customer_group": None,
|
||||
"territory": None,
|
||||
"payment_terms": None,
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
def _sync_supplier_statuses(self):
|
||||
"""Reset Kapital Bank Supplier status to 'New' for suppliers removed from supplier_mappings."""
|
||||
mapped_names = {row.kb_supplier_name for row in self.supplier_mappings if row.kb_supplier_name}
|
||||
|
||||
orphaned = frappe.get_all(
|
||||
"Kapital Bank Supplier",
|
||||
filters={"status": "Mapped"},
|
||||
fields=["name", "supplier_name"],
|
||||
)
|
||||
|
||||
for supplier in orphaned:
|
||||
if supplier.name not in mapped_names:
|
||||
frappe.db.set_value(
|
||||
"Kapital Bank Supplier",
|
||||
supplier.name,
|
||||
{
|
||||
"status": "New",
|
||||
"mapped_supplier": None,
|
||||
"supplier_group": None,
|
||||
"payment_terms": None,
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,36 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:client_name",
|
||||
"creation": "2026-02-24 00:00:00.000000",
|
||||
"autoname": "field:supplier_name",
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"client_name",
|
||||
"supplier_name",
|
||||
"tax_id",
|
||||
"status",
|
||||
"column_break_1",
|
||||
"iban",
|
||||
"bank_code",
|
||||
"mapping_section",
|
||||
"mapped_party_type",
|
||||
"mapped_party"
|
||||
"supplier_mapping_section",
|
||||
"mapped_supplier",
|
||||
"supplier_group",
|
||||
"payment_terms"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "client_name",
|
||||
"fieldname": "supplier_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client Name",
|
||||
"reqd": 1
|
||||
"label": "Supplier Name",
|
||||
"length": 500,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax ID (VÖEN)",
|
||||
"unique": 1
|
||||
"label": "Tax ID"
|
||||
},
|
||||
{
|
||||
"default": "New",
|
||||
|
|
@ -37,7 +38,8 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "New\nMapped\nActive"
|
||||
"options": "New\nMapped",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
|
|
@ -54,29 +56,38 @@
|
|||
"label": "Bank Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "mapping_section",
|
||||
"fieldname": "supplier_mapping_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "ERP Mapping"
|
||||
"label": "Supplier Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "mapped_party_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mapped Party Type",
|
||||
"options": "\nCustomer\nSupplier"
|
||||
"fieldname": "mapped_supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mapped Supplier",
|
||||
"options": "Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mapped_party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Mapped Party",
|
||||
"options": "mapped_party_type"
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Group",
|
||||
"options": "Supplier Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms",
|
||||
"options": "Payment Terms Template",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-24 00:00:00.000000",
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Client",
|
||||
"name": "Kapital Bank Supplier",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
|
@ -91,11 +102,21 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "client_name"
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class KapitalBankSupplier(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"kb_supplier_name",
|
||||
"tax_id",
|
||||
"erp_supplier",
|
||||
"supplier_group",
|
||||
"payment_terms",
|
||||
"mapping_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "kb_supplier_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "KB Supplier Name",
|
||||
"options": "Kapital Bank Supplier",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "erp_supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "ERP Supplier",
|
||||
"options": "Supplier"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Group",
|
||||
"options": "Supplier Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"default": "Manual",
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mapping Type",
|
||||
"options": "Manual\nAutomatic",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-26 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Supplier Mapping",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class KapitalBankSupplierMapping(Document):
|
||||
pass
|
||||
|
|
@ -1,277 +1 @@
|
|||
import frappe
|
||||
import json
|
||||
from frappe.utils import nowdate
|
||||
from kapital_bank.api import BIRBankClient
|
||||
from kapital_bank.auth import get_default_login
|
||||
|
||||
|
||||
def _get_client(login_name=None):
|
||||
return BIRBankClient(login_name)
|
||||
|
||||
|
||||
def _pe_doc(payment_entry_name):
|
||||
return frappe.get_doc("Payment Entry", payment_entry_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_transfer(payment_entry_name, login_name=None):
|
||||
"""POST /api/b2b/v2/internal-transfer — domestic bank transfer."""
|
||||
pe = _pe_doc(payment_entry_name)
|
||||
client = _get_client(login_name)
|
||||
|
||||
operation_name = pe.get("kb_operation_name") or pe.name[:20]
|
||||
|
||||
payload = {
|
||||
"fromAccount": pe.get("kb_from_iban") or pe.paid_from,
|
||||
"transferData": {
|
||||
"operationName": operation_name,
|
||||
"benBankCode": pe.get("kb_ben_bank_code", ""),
|
||||
"toAccount": pe.get("kb_to_iban") or pe.paid_to,
|
||||
"toTaxNo": pe.get("kb_to_tax_no", ""),
|
||||
"toCustName": pe.party_name or pe.party,
|
||||
"amount": pe.paid_amount,
|
||||
"purpose1": (pe.remarks or "")[:64],
|
||||
"purpose2": "",
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.post_json("/v2/internal-transfer", json=payload)
|
||||
except Exception as e:
|
||||
frappe.log_error(f"send_transfer failed for {payment_entry_name}: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
status = data.get("status", "")
|
||||
operation_id = data.get("operationId", "")
|
||||
|
||||
if status == "Success":
|
||||
frappe.db.set_value("Payment Entry", payment_entry_name, {
|
||||
"kb_operation_name": operation_name,
|
||||
"kb_bank_id": operation_id,
|
||||
"kb_transfer_status": "Submitted",
|
||||
"kb_transfer_type": "Internal Transfer",
|
||||
}, update_modified=False)
|
||||
frappe.db.commit()
|
||||
return {"success": True, "operation_id": operation_id}
|
||||
else:
|
||||
err = data.get("message") or data.get("errorMessage") or str(data)
|
||||
frappe.log_error(f"send_transfer error for {payment_entry_name}: {err}", "KB Payment API")
|
||||
return {"success": False, "message": err}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_bulk_transfers(payment_entry_names, login_name=None):
|
||||
"""POST /api/b2b/v2/internal-transfers — bulk domestic transfer (up to 50)."""
|
||||
if isinstance(payment_entry_names, str):
|
||||
payment_entry_names = json.loads(payment_entry_names)
|
||||
|
||||
client = _get_client(login_name)
|
||||
results = []
|
||||
|
||||
# BIRBank requires a sign file for bulk transfers.
|
||||
# We send the JSON part as a multipart form.
|
||||
transfer_datas = []
|
||||
for name in payment_entry_names:
|
||||
pe = _pe_doc(name)
|
||||
transfer_datas.append({
|
||||
"operationName": pe.get("kb_operation_name") or pe.name[:20],
|
||||
"benBankCode": pe.get("kb_ben_bank_code", ""),
|
||||
"toAccount": pe.get("kb_to_iban") or pe.paid_to,
|
||||
"toTaxNo": pe.get("kb_to_tax_no", ""),
|
||||
"toCustName": pe.party_name or pe.party,
|
||||
"amount": pe.paid_amount,
|
||||
"purpose1": (pe.remarks or "")[:64],
|
||||
})
|
||||
|
||||
first_pe = _pe_doc(payment_entry_names[0])
|
||||
payload_json = json.dumps({
|
||||
"fromAccount": first_pe.get("kb_from_iban") or first_pe.paid_from,
|
||||
"transferDatas": transfer_datas,
|
||||
})
|
||||
|
||||
try:
|
||||
resp = client.post("/v2/internal-transfers", data={"json": payload_json}, files=[])
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
frappe.log_error(f"send_bulk_transfers failed: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
successes = data.get("successes", {}).get("transferDatas", [])
|
||||
errors = data.get("errors", {}).get("transferDatas", [])
|
||||
|
||||
for item in successes:
|
||||
op_name = item.get("operationName")
|
||||
op_id = item.get("operationId")
|
||||
frappe.db.set_value("Payment Entry", op_name, {
|
||||
"kb_bank_id": op_id,
|
||||
"kb_transfer_status": "Submitted",
|
||||
"kb_transfer_type": "Internal Transfer",
|
||||
}, update_modified=False)
|
||||
|
||||
frappe.db.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"successes": len(successes),
|
||||
"errors": [e.get("errorMessages") for e in errors],
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_card_transfer(payment_entry_name, login_name=None):
|
||||
"""POST /api/b2b/transfer/account-to-card — transfer to business card."""
|
||||
pe = _pe_doc(payment_entry_name)
|
||||
client = _get_client(login_name)
|
||||
|
||||
operation_name = pe.get("kb_operation_name") or pe.name[:20]
|
||||
payload_data = {
|
||||
"operationName": operation_name,
|
||||
"fromAccount": pe.get("kb_from_iban") or pe.paid_from,
|
||||
"toAccount": pe.get("kb_to_iban") or pe.paid_to,
|
||||
"amount": str(pe.paid_amount),
|
||||
"purpose": pe.get("kb_purpose_code") or "1",
|
||||
}
|
||||
|
||||
try:
|
||||
resp = client.post(
|
||||
"/transfer/account-to-card",
|
||||
data={"json": frappe.as_json(payload_data)},
|
||||
files=[]
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
frappe.log_error(f"send_card_transfer failed for {payment_entry_name}: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
status = data.get("status", "")
|
||||
operation_id = data.get("data", {}).get("operationId", "")
|
||||
|
||||
if status == "Success":
|
||||
frappe.db.set_value("Payment Entry", payment_entry_name, {
|
||||
"kb_operation_name": operation_name,
|
||||
"kb_bank_id": operation_id,
|
||||
"kb_transfer_status": "Submitted",
|
||||
"kb_transfer_type": "Card Transfer",
|
||||
}, update_modified=False)
|
||||
frappe.db.commit()
|
||||
return {"success": True, "operation_id": operation_id}
|
||||
else:
|
||||
err = data.get("message") or str(data)
|
||||
return {"success": False, "message": err}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_vat_payment(payment_entry_name, login_name=None):
|
||||
"""POST /api/b2b/internal-transfer/vat — 18% VAT payment."""
|
||||
pe = _pe_doc(payment_entry_name)
|
||||
client = _get_client(login_name)
|
||||
|
||||
operation_name = pe.get("kb_operation_name") or pe.name[:20]
|
||||
payload = {
|
||||
"operationName": operation_name,
|
||||
"amount": pe.paid_amount if pe.paid_amount else None,
|
||||
"purpose": pe.remarks or None,
|
||||
}
|
||||
|
||||
try:
|
||||
data = client.post_json("/internal-transfer/vat", json=payload)
|
||||
except Exception as e:
|
||||
frappe.log_error(f"send_vat_payment failed for {payment_entry_name}: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
status = data.get("status", "")
|
||||
resp_data = data.get("data", {})
|
||||
operation_id = resp_data.get("operationId", "")
|
||||
|
||||
if status == "Success":
|
||||
frappe.db.set_value("Payment Entry", payment_entry_name, {
|
||||
"kb_operation_name": operation_name,
|
||||
"kb_bank_id": operation_id,
|
||||
"kb_transfer_status": "Submitted",
|
||||
"kb_transfer_type": "VAT Payment",
|
||||
}, update_modified=False)
|
||||
frappe.db.commit()
|
||||
return {"success": True, "operation_id": operation_id}
|
||||
else:
|
||||
err = data.get("message") or str(data)
|
||||
return {"success": False, "message": err}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_transfer_status(operation_name, login_name=None):
|
||||
"""GET /api/b2b/internal-transfer/status — check transfer status."""
|
||||
try:
|
||||
client = _get_client(login_name)
|
||||
data = client.get_json("/internal-transfer/status", params={"operatorName": operation_name})
|
||||
status_list = data.get("data", [])
|
||||
return {"success": True, "status": data.get("status"), "data": status_list}
|
||||
except Exception as e:
|
||||
frappe.log_error(f"get_transfer_status failed for {operation_name}: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_and_update_status(payment_entry_name, login_name=None):
|
||||
"""Check status and update Payment Entry kb_transfer_status field."""
|
||||
pe = _pe_doc(payment_entry_name)
|
||||
operation_name = pe.get("kb_operation_name")
|
||||
if not operation_name:
|
||||
return {"success": False, "message": "No KB operation name on this Payment Entry"}
|
||||
|
||||
result = get_transfer_status(operation_name, login_name)
|
||||
if not result.get("success"):
|
||||
return result
|
||||
|
||||
status_data = result.get("data", [])
|
||||
if status_data:
|
||||
bank_status = status_data[0].get("status", "")
|
||||
frappe.db.set_value("Payment Entry", payment_entry_name, "kb_transfer_status", bank_status, update_modified=False)
|
||||
frappe.db.commit()
|
||||
return {"success": True, "status": bank_status, "data": status_data}
|
||||
|
||||
return {"success": True, "status": "Unknown", "data": []}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_transfer(payment_entry_name, login_name=None):
|
||||
"""DELETE /api/b2b/internal-transfer — cancel a pending transfer."""
|
||||
pe = _pe_doc(payment_entry_name)
|
||||
operation_name = pe.get("kb_operation_name")
|
||||
if not operation_name:
|
||||
return {"success": False, "message": "No KB operation name on this Payment Entry"}
|
||||
|
||||
try:
|
||||
client = _get_client(login_name)
|
||||
resp = client.delete("/internal-transfer", params={"operationId": operation_name})
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
frappe.log_error(f"cancel_transfer failed for {payment_entry_name}: {e}\n{frappe.get_traceback()}", "KB Payment API")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
if data.get("message") == "Success":
|
||||
frappe.db.set_value("Payment Entry", payment_entry_name, "kb_transfer_status", "Cancelled", update_modified=False)
|
||||
frappe.db.commit()
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": str(data)}
|
||||
|
||||
|
||||
def on_cancel_payment_entry(doc, method):
|
||||
"""doc_events hook — try to cancel in BIRBank when Payment Entry is cancelled."""
|
||||
if not doc.get("kb_operation_name"):
|
||||
return
|
||||
|
||||
status = doc.get("kb_transfer_status", "")
|
||||
# Don't try to cancel if already confirmed/completed
|
||||
if status in ("Confirmed", "Completed", "Cancelled"):
|
||||
return
|
||||
|
||||
try:
|
||||
cancel_transfer(doc.name)
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Auto-cancel failed for Payment Entry {doc.name}: {e}\n{frappe.get_traceback()}",
|
||||
"KB Payment Entry Cancel"
|
||||
)
|
||||
# Removed: outgoing payment functions were removed.
|
||||
|
|
|
|||
|
|
@ -1,83 +1,23 @@
|
|||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
def after_install():
|
||||
create_payment_entry_custom_fields()
|
||||
cleanup_payment_entry_custom_fields()
|
||||
|
||||
|
||||
def after_migrate():
|
||||
create_payment_entry_custom_fields()
|
||||
cleanup_payment_entry_custom_fields()
|
||||
|
||||
|
||||
def create_payment_entry_custom_fields():
|
||||
custom_fields = {
|
||||
"Payment Entry": [
|
||||
{
|
||||
"fieldname": "kb_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Kapital Bank",
|
||||
"insert_after": "letter_head",
|
||||
"collapsible": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_transfer_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "KB Transfer Type",
|
||||
"options": "\nInternal Transfer\nCard Transfer\nVAT Payment",
|
||||
"insert_after": "kb_section",
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_operation_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB Operation Name",
|
||||
"insert_after": "kb_transfer_type",
|
||||
"read_only": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_bank_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB Bank ID",
|
||||
"insert_after": "kb_operation_name",
|
||||
"read_only": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_transfer_status",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB Transfer Status",
|
||||
"insert_after": "kb_bank_id",
|
||||
"read_only": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_column_break",
|
||||
"fieldtype": "Column Break",
|
||||
"insert_after": "kb_transfer_status",
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_from_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB From IBAN",
|
||||
"insert_after": "kb_column_break",
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_to_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB To IBAN",
|
||||
"insert_after": "kb_from_iban",
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_to_tax_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB To Tax No (VÖEN)",
|
||||
"insert_after": "kb_to_iban",
|
||||
},
|
||||
{
|
||||
"fieldname": "kb_ben_bank_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "KB Ben Bank Code",
|
||||
"insert_after": "kb_to_tax_no",
|
||||
},
|
||||
]
|
||||
}
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
KB_FIELDS = [
|
||||
"kb_section", "kb_transfer_type", "kb_operation_name", "kb_bank_id",
|
||||
"kb_transfer_status", "kb_column_break", "kb_from_iban", "kb_to_iban",
|
||||
"kb_to_tax_no", "kb_ben_bank_code",
|
||||
]
|
||||
|
||||
|
||||
def cleanup_payment_entry_custom_fields():
|
||||
for fieldname in KB_FIELDS:
|
||||
if frappe.db.exists("Custom Field", {"dt": "Payment Entry", "fieldname": fieldname}):
|
||||
frappe.delete_doc("Custom Field", f"Payment Entry-{fieldname}", ignore_missing=True)
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
Loading…
Reference in New Issue