From 4a560dbde5c347774b7b6951ecfc173f5b179d56 Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Thu, 26 Feb 2026 22:04:10 +0400
Subject: [PATCH] payment entry import works
---
kapital_bank/bank_api.py | 795 +++++++++++++-----
kapital_bank/client/payment_entry.js | 638 ++++++++++++--
kapital_bank/hooks.py | 17 +-
.../kapital_bank_client_mapping.json | 61 --
.../__init__.py | 0
.../kapital_bank_customer.json | 130 +++
.../kapital_bank_customer.py} | 4 +-
.../__init__.py | 0
.../kapital_bank_customer_mapping.json | 77 ++
.../kapital_bank_customer_mapping.py | 6 +
.../kapital_bank_purpose_mapping/__init__.py | 0
.../kapital_bank_purpose_mapping.json | 59 ++
.../kapital_bank_purpose_mapping.py} | 2 +-
.../kapital_bank_settings.js | 425 +++++++---
.../kapital_bank_settings.json | 87 +-
.../kapital_bank_settings.py | 72 +-
.../doctype/kapital_bank_supplier/__init__.py | 0
.../kapital_bank_supplier.json} | 75 +-
.../kapital_bank_supplier.py | 6 +
.../kapital_bank_supplier_mapping/__init__.py | 0
.../kapital_bank_supplier_mapping.json | 70 ++
.../kapital_bank_supplier_mapping.py | 6 +
kapital_bank/payment_api.py | 278 +-----
kapital_bank/setup.py | 86 +-
24 files changed, 2042 insertions(+), 852 deletions(-)
delete mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.json
rename kapital_bank/kapital_bank/doctype/{kapital_bank_client => kapital_bank_customer}/__init__.py (100%)
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.json
rename kapital_bank/kapital_bank/doctype/{kapital_bank_client_mapping/kapital_bank_client_mapping.py => kapital_bank_customer/kapital_bank_customer.py} (53%)
rename kapital_bank/kapital_bank/doctype/{kapital_bank_client_mapping => kapital_bank_customer_mapping}/__init__.py (100%)
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.json
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.py
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/__init__.py
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.json
rename kapital_bank/kapital_bank/doctype/{kapital_bank_client/kapital_bank_client.py => kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.py} (61%)
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_supplier/__init__.py
rename kapital_bank/kapital_bank/doctype/{kapital_bank_client/kapital_bank_client.json => kapital_bank_supplier/kapital_bank_supplier.json} (51%)
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.py
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/__init__.py
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.json
create mode 100644 kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.py
diff --git a/kapital_bank/bank_api.py b/kapital_bank/bank_api.py
index fa65a7a..d768da3 100644
--- a/kapital_bank/bank_api.py
+++ b/kapital_bank/bank_api.py
@@ -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
# ═══════════════════════════════════════════════════════════════════════════════
diff --git a/kapital_bank/client/payment_entry.js b/kapital_bank/client/payment_entry.js
index 694a056..10620a1 100644
--- a/kapital_bank/client/payment_entry.js
+++ b/kapital_bank/client/payment_entry.js
@@ -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 = '
';
+
+ 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 += '
';
+
+ // Header
+ errorsHtml += '';
+
+ // Error message
+ errorsHtml += '
';
+ errorsHtml += '' + error.error_message + '';
+ errorsHtml += '
';
+
+ // Details — unmapped purpose
+ if (error.unmatched_purpose && error.unmatched_purpose.length > 0) {
+ errorsHtml += '
';
+ errorsHtml += '
' + __('Unmapped purpose:') + '
';
+ errorsHtml += '
';
+ error.unmatched_purpose.forEach(function(item) {
+ errorsHtml += '- ' + item.purpose + '
';
+ });
+ errorsHtml += '
';
+ }
+
+ // Details — unmapped party
+ if (error.unmatched_party && error.unmatched_party.length > 0) {
+ errorsHtml += '
';
+ errorsHtml += '
' + __('Unmapped party:') + '
';
+ errorsHtml += '
';
+ error.unmatched_party.forEach(function(party) {
+ errorsHtml += '- ' + party.name;
+ if (party.voen) {
+ errorsHtml += ' (VOEN: ' + party.voen + ')';
+ }
+ errorsHtml += '
';
+ });
+ errorsHtml += '
';
+ }
+
+ errorsHtml += '
';
+ errorsHtml += '' + documentDate + '';
+ errorsHtml += '
';
+
+ errorsHtml += '
';
+ });
+
+ errorsHtml += '
';
+
+ // Statistics
+ const errorTypes = {};
+ KBImport.loadingErrors.forEach(function(error) {
+ errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
+ });
+
+ let statsHtml = '';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '' + __('Total Errors:') + ' ' + KBImport.loadingErrors.length + '
';
+ statsHtml += '';
+ Object.keys(errorTypes).forEach(function(type, index) {
+ if (index > 0) statsHtml += ' • ';
+ statsHtml += type + ': ' + errorTypes[type];
+ });
+ statsHtml += '';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '';
+ statsHtml += '';
+ statsHtml += '
';
+
+ 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: '
' + errorsHtml + '
'
+ }
+ ],
+ 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 = '
';
+ if (processedCount > 0) {
+ message += __('Successfully imported: ') + '' + processedCount + '' + __(' transactions') + '
';
+ }
+ message += __('Failed: ') + '' + errorsCount + '' + __(' transactions');
+ message += '
';
+
+ message += '
' +
+ '' +
+ '' +
+ '
';
+
+ 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 = '
';
+
+ const infoMessage = '
' +
+ __('Transactions found: ') + txns.length +
+ '
';
+
+ 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();
+ });
+ }
+};
diff --git a/kapital_bank/hooks.py b/kapital_bank/hooks.py
index 0d38b45..842cc25 100644
--- a/kapital_bank/hooks.py
+++ b/kapital_bank/hooks.py
@@ -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
# ------------------
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.json b/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.json
deleted file mode 100644
index 6c67657..0000000
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.json
+++ /dev/null
@@ -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": []
-}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client/__init__.py b/kapital_bank/kapital_bank/doctype/kapital_bank_customer/__init__.py
similarity index 100%
rename from kapital_bank/kapital_bank/doctype/kapital_bank_client/__init__.py
rename to kapital_bank/kapital_bank/doctype/kapital_bank_customer/__init__.py
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.json b/kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.json
new file mode 100644
index 0000000..e74d76b
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.json
@@ -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
+}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.py b/kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.py
similarity index 53%
rename from kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.py
rename to kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.py
index d07bb91..ecb8c1c 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/kapital_bank_client_mapping.py
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_customer/kapital_bank_customer.py
@@ -2,5 +2,5 @@ import frappe
from frappe.model.document import Document
-class KapitalBankClientMapping(Document):
- pass
+class KapitalBankCustomer(Document):
+ pass
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/__init__.py b/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/__init__.py
similarity index 100%
rename from kapital_bank/kapital_bank/doctype/kapital_bank_client_mapping/__init__.py
rename to kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/__init__.py
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.json b/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.json
new file mode 100644
index 0000000..a4418ef
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.json
@@ -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
+}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.py b/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.py
new file mode 100644
index 0000000..35cd54b
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_customer_mapping/kapital_bank_customer_mapping.py
@@ -0,0 +1,6 @@
+import frappe
+from frappe.model.document import Document
+
+
+class KapitalBankCustomerMapping(Document):
+ pass
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/__init__.py b/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.json b/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.json
new file mode 100644
index 0000000..e784a1c
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.json
@@ -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": []
+}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.py b/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.py
similarity index 61%
rename from kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.py
rename to kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.py
index 1232bc2..fa0ae11 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.py
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_purpose_mapping/kapital_bank_purpose_mapping.py
@@ -2,5 +2,5 @@ import frappe
from frappe.model.document import Document
-class KapitalBankClient(Document):
+class KapitalBankPurposeMapping(Document):
pass
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.js b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.js
index b367017..543ea0b 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.js
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.js
@@ -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(
- __('
WARNING! This will create new Customer/Supplier records for all unmapped clients!
This action is irreversible. Are you sure?
'),
- () => {
- 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) {
`;
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 += `
+ summary_html += `
${t.label}
${d.total} ${__('total')}
@@ -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(
+ __('
WARNING! This will create new Customer records for all unmapped customers!
This action is irreversible. Are you sure?
'),
+ 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(
+ __('
WARNING! This will create new Supplier records for all unmapped suppliers!
This action is irreversible. Are you sure?
'),
+ 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 += `
- | ${__('Client Name')} |
- ${__('Tax ID (VÖEN)')} |
+ ${__('Customer Name')} |
+ ${__('Tax ID (VOEN)')} |
${__('IBAN')} |
${__('Status')} |
- ${__('Party Type')} |
${__('Mapped To')} |
${__('Created')} |
`;
- clients.forEach(c => {
- const pill = c.status === 'Mapped' ? 'green' : (c.status === 'Active' ? 'blue' : 'red');
- const mapped_to = c.mapped_party ? `${c.mapped_party}` : '-';
+ customers.forEach(c => {
+ const pill = c.status === 'Mapped' ? 'green' : 'red';
+ const mapped_to = c.mapped_customer ? `${c.mapped_customer}` : '-';
const created = c.creation ? frappe.datetime.str_to_user(c.creation) : '-';
html += `
- | ${c.client_name} |
+ ${c.customer_name} |
${c.tax_id || '-'} |
${c.iban || '-'} |
${__(c.status)} |
- ${c.mapped_party_type || '-'} |
${mapped_to} |
${created} |
`;
});
html += '
';
- if (total_count > clients.length) {
- html += `
${__('Showing {0} of {1}', [clients.length, total_count])} — ${__('View All')}
`;
+ if (total_count > customers.length) {
+ html += `
${__('Showing {0} of {1}', [customers.length, total_count])} — ${__('View All')}
`;
}
} 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 += `
+
+ | ${__('Supplier Name')} |
+ ${__('Tax ID (VOEN)')} |
+ ${__('IBAN')} |
+ ${__('Status')} |
+ ${__('Mapped To')} |
+ ${__('Created')} |
+
`;
+ suppliers.forEach(s => {
+ const pill = s.status === 'Mapped' ? 'green' : 'red';
+ const mapped_to = s.mapped_supplier ? `${s.mapped_supplier}` : '-';
+ const created = s.creation ? frappe.datetime.str_to_user(s.creation) : '-';
+ html += `
+ | ${s.supplier_name} |
+ ${s.tax_id || '-'} |
+ ${s.iban || '-'} |
+ ${__(s.status)} |
+ ${mapped_to} |
+ ${created} |
+
`;
+ });
+ html += '
';
+ if (total_count > suppliers.length) {
+ html += `
${__('Showing {0} of {1}', [suppliers.length, total_count])} — ${__('View All')}
`;
+ }
+ } 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();
});
}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.json b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.json
index b7976d8..fa6d505 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.json
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.json
@@ -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": "
Click \"Load Data\" to fetch cards from BIRBank
"
},
{
- "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": "
"
+ "label": "Customers List",
+ "options": "
"
+ },
+ {
+ "fieldname": "suppliers_tab",
+ "fieldtype": "Tab Break",
+ "label": "Suppliers"
+ },
+ {
+ "fieldname": "suppliers_list_html",
+ "fieldtype": "HTML",
+ "label": "Suppliers List",
+ "options": "
"
},
{
"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",
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.py b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.py
index 5b9609a..d6c2357 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.py
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_settings/kapital_bank_settings.py
@@ -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,
+ )
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/__init__.py b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.json b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.json
similarity index 51%
rename from kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.json
rename to kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.json
index 689a1b3..586503d 100644
--- a/kapital_bank/kapital_bank/doctype/kapital_bank_client/kapital_bank_client.json
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.json
@@ -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
}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.py b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.py
new file mode 100644
index 0000000..cbd96c6
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier/kapital_bank_supplier.py
@@ -0,0 +1,6 @@
+import frappe
+from frappe.model.document import Document
+
+
+class KapitalBankSupplier(Document):
+ pass
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/__init__.py b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.json b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.json
new file mode 100644
index 0000000..f4f3115
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.json
@@ -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
+}
diff --git a/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.py b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.py
new file mode 100644
index 0000000..6633d59
--- /dev/null
+++ b/kapital_bank/kapital_bank/doctype/kapital_bank_supplier_mapping/kapital_bank_supplier_mapping.py
@@ -0,0 +1,6 @@
+import frappe
+from frappe.model.document import Document
+
+
+class KapitalBankSupplierMapping(Document):
+ pass
diff --git a/kapital_bank/payment_api.py b/kapital_bank/payment_api.py
index 9238546..0fb16f9 100644
--- a/kapital_bank/payment_api.py
+++ b/kapital_bank/payment_api.py
@@ -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.
diff --git a/kapital_bank/setup.py b/kapital_bank/setup.py
index 2b323aa..2cc7e61 100644
--- a/kapital_bank/setup.py
+++ b/kapital_bank/setup.py
@@ -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()