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