feat(bank-integration): fall back to first leaf node when no group is configured + filter group fields

Creating customers/suppliers no longer requires a default Customer/
Supplier Group or Territory on the Bank Integration — when none is set,
the first leaf (non-group) node of the tree is used. A configured value
is still validated (must exist and be a leaf), with a clear message.
Also: the default-group / default-territory Link fields (and the
matching mapping child-table fields) now filter to is_group=0 so a group
node can't be selected in the first place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-12 10:53:23 +00:00
parent 94ce72e4fb
commit 1abdedce20
2 changed files with 52 additions and 39 deletions

View File

@ -4,6 +4,27 @@ import frappe
from frappe import _ from frappe import _
def _pick_leaf(doctype, configured, label):
"""Resolve `configured` to a valid leaf node of an NSM tree doctype
(Customer Group / Supplier Group / Territory).
- If configured: validate it exists and is NOT a group node.
- If not configured: fall back to the first leaf node (so creation works
without forcing a default on the Bank Integration).
Raises with a clear message if it can't.
"""
if configured:
if not frappe.db.exists(doctype, configured):
frappe.throw(_("{0} '{1}' does not exist.").format(label, configured))
if frappe.db.get_value(doctype, configured, "is_group"):
frappe.throw(_("{0} '{1}' is a group node — pick a leaf (non-group) {0}.").format(label, configured))
return configured
leaf = frappe.db.get_value(doctype, {"is_group": 0}, "name", order_by="lft asc")
if not leaf:
frappe.throw(_("No non-group {0} exists. Create one first.").format(label))
return leaf
@frappe.whitelist() @frappe.whitelist()
def create_unmapped_customers(bank_integration): def create_unmapped_customers(bank_integration):
"""For each Bank Integration Customer Mapping row without erp_customer, create an ERPNext Customer.""" """For each Bank Integration Customer Mapping row without erp_customer, create an ERPNext Customer."""
@ -19,32 +40,19 @@ def create_unmapped_customers(bank_integration):
if not unmapped: if not unmapped:
return {"success": True, "created_count": 0, "message": "No unmapped customers in table"} return {"success": True, "created_count": 0, "message": "No unmapped customers in table"}
# Validate config upfront — clearer than ERPNext's errors thrown deep inside # Resolve group / territory per row upfront (raises a clear message on bad
# party creation ("Could not find Customer Group" / "Cannot select a Group type"). # config). If nothing is configured, _pick_leaf falls back to the first leaf
for mapping, _bi_cust in unmapped: # node — so creation works without a default set on the Bank Integration.
group = mapping.customer_group or bi.default_customer_group resolved = [
if not group or not frappe.db.exists("Customer Group", group): (
frappe.throw(_( _pick_leaf("Customer Group", mapping.customer_group or bi.default_customer_group, "Customer Group"),
"Set a Default Customer Group on the Bank Integration " _pick_leaf("Territory", mapping.territory or bi.default_territory, "Territory"),
"(or a Customer Group on each customer mapping row) before creating customers." )
)) for mapping, _bi_cust in unmapped
if frappe.db.get_value("Customer Group", group, "is_group"): ]
frappe.throw(_(
"Customer Group '{0}' is a group node — pick a leaf (non-group) Customer Group."
).format(group))
territory = mapping.territory or bi.default_territory
if not territory or not frappe.db.exists("Territory", territory):
frappe.throw(_(
"Set a Default Territory on the Bank Integration "
"(or a Territory on each customer mapping row) before creating customers."
))
if frappe.db.get_value("Territory", territory, "is_group"):
frappe.throw(_(
"Territory '{0}' is a group node — pick a leaf (non-group) Territory."
).format(territory))
created = 0 created = 0
for mapping, bi_cust in unmapped: for (mapping, bi_cust), (customer_group, territory) in zip(unmapped, resolved):
try: try:
customer_name = bi_cust.customer_name customer_name = bi_cust.customer_name
if frappe.db.exists("Customer", {"customer_name": customer_name}): if frappe.db.exists("Customer", {"customer_name": customer_name}):
@ -53,8 +61,8 @@ def create_unmapped_customers(bank_integration):
party = frappe.new_doc("Customer") party = frappe.new_doc("Customer")
party.customer_name = customer_name party.customer_name = customer_name
party.customer_type = "Company" party.customer_type = "Company"
party.customer_group = mapping.customer_group or bi.default_customer_group party.customer_group = customer_group
party.territory = mapping.territory or bi.default_territory party.territory = territory
if bi_cust.tax_id: if bi_cust.tax_id:
party.tax_id = bi_cust.tax_id party.tax_id = bi_cust.tax_id
@ -103,20 +111,13 @@ def create_unmapped_suppliers(bank_integration):
if not unmapped: if not unmapped:
return {"success": True, "created_count": 0, "message": "No unmapped suppliers in table"} return {"success": True, "created_count": 0, "message": "No unmapped suppliers in table"}
for mapping, _bi_supp in unmapped: resolved = [
group = mapping.supplier_group or bi.default_supplier_group _pick_leaf("Supplier Group", mapping.supplier_group or bi.default_supplier_group, "Supplier Group")
if not group or not frappe.db.exists("Supplier Group", group): for mapping, _bi_supp in unmapped
frappe.throw(_( ]
"Set a Default Supplier Group on the Bank Integration "
"(or a Supplier Group on each supplier mapping row) before creating suppliers."
))
if frappe.db.get_value("Supplier Group", group, "is_group"):
frappe.throw(_(
"Supplier Group '{0}' is a group node — pick a leaf (non-group) Supplier Group."
).format(group))
created = 0 created = 0
for mapping, bi_supp in unmapped: for (mapping, bi_supp), supplier_group in zip(unmapped, resolved):
try: try:
supplier_name = bi_supp.supplier_name supplier_name = bi_supp.supplier_name
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}): if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
@ -125,7 +126,7 @@ def create_unmapped_suppliers(bank_integration):
party = frappe.new_doc("Supplier") party = frappe.new_doc("Supplier")
party.supplier_name = supplier_name party.supplier_name = supplier_name
party.supplier_type = "Company" party.supplier_type = "Company"
party.supplier_group = mapping.supplier_group or bi.default_supplier_group party.supplier_group = supplier_group
if bi_supp.tax_id: if bi_supp.tax_id:
party.tax_id = bi_supp.tax_id party.tax_id = bi_supp.tax_id

View File

@ -2,6 +2,18 @@
// Mirrors the kapital_bank_settings.js layout for familiarity. // Mirrors the kapital_bank_settings.js layout for familiarity.
frappe.ui.form.on('Bank Integration', { frappe.ui.form.on('Bank Integration', {
setup(frm) {
// Customer/Supplier Group and Territory must be leaf (non-group) nodes —
// don't let group nodes be picked here or in the mapping child tables.
const leaf = () => ({ filters: { is_group: 0 } });
frm.set_query('default_customer_group', leaf);
frm.set_query('default_supplier_group', leaf);
frm.set_query('default_territory', leaf);
frm.set_query('customer_group', 'customer_mappings', leaf);
frm.set_query('territory', 'customer_mappings', leaf);
frm.set_query('supplier_group', 'supplier_mappings', leaf);
},
refresh(frm) { refresh(frm) {
if (frm.is_new()) return; if (frm.is_new()) return;