feat(master-data): resolve Item Group root for groups too, and log results visibly

sync_item_groups now resolves the real tree root once and remaps the shipped
"All Item Groups" parent to it (via a new transform hook on sync_master_data),
so on localized sites (e.g. root "Bütün Element Qrupları") the tax groups nest
under the actual root instead of being orphaned.

Both sync_item_groups and sync_items now print explicit, always-visible lines
(resolved root, per-item creation, and a created/existed/skipped summary) so a
migrate run clearly shows whether the groups and items were created.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-07-21 09:52:48 +00:00
parent 9b1f2b79e0
commit e4ac3e328f
1 changed files with 56 additions and 12 deletions

View File

@ -17,7 +17,7 @@ from __future__ import annotations
import json
import os
from collections.abc import Iterable
from collections.abc import Callable, Iterable
import frappe
@ -77,6 +77,7 @@ def sync_master_data(
*,
is_tree: bool = False,
manage_deletes: bool = True,
transform: Callable[[dict], dict] | None = None,
) -> dict:
"""Reconcile a doctype against a JSON file, applying only the delta.
@ -89,6 +90,10 @@ def sync_master_data(
manage_deletes: when False, rows present in the DB but not in the JSON
are left untouched. Use False when the JSON is only a subset of the
table (e.g. Item Group also holds ERPNext and user-created groups).
transform: optional callback applied in-place to every loaded record
before the diff. Use it to rewrite site-dependent references (e.g.
remap a localized Item Group tree root) so both the insert and the
update-diff see the resolved values.
"""
if not os.path.exists(json_path):
frappe.logger().warning(f"[taxes_az] master data file not found: {json_path}")
@ -98,6 +103,10 @@ def sync_master_data(
if not target:
return {"empty_file": True}
if transform is not None:
for key, rec in target.items():
target[key] = transform(rec)
meta = frappe.get_meta(doctype)
# value-bearing fields actually present in the JSON, minus the key itself
sample = next(iter(target.values()))
@ -253,12 +262,15 @@ def sync_items() -> dict:
records = json.load(f)
root = _resolve_item_group_root()
inserted = skipped = 0
print(f"[taxes_az] Item: resolved tree root = {root!r}, {len(records)} shipped item(s)")
inserted = existed = missing_group = 0
for rec in records:
name = rec.get("name") or rec.get("item_code")
if not name or frappe.db.exists("Item", name):
skipped += 1
if not name:
continue
if frappe.db.exists("Item", name):
existed += 1
continue
group = rec.get("item_group")
@ -266,10 +278,8 @@ def sync_items() -> dict:
if group in _ROOT_ALIASES and root:
group = root
if not group or not frappe.db.exists("Item Group", group):
frappe.logger().warning(
f"[taxes_az] Item '{name}': Item Group '{group}' not found — skipped"
)
skipped += 1
print(f"[taxes_az] Item '{name}': Item Group {group!r} not found — SKIPPED")
missing_group += 1
continue
doc = frappe.new_doc("Item")
@ -279,12 +289,21 @@ def sync_items() -> dict:
doc.flags.name_set = True
doc.flags.ignore_permissions = True
doc.insert(ignore_if_duplicate=True)
print(f"[taxes_az] Item: created {name!r} ({rec.get('item_name')}) in {group!r}")
inserted += 1
if inserted:
frappe.db.commit()
print(f"[taxes_az] Item: +{inserted} (skipped {skipped})")
return {"doctype": "Item", "inserted": inserted, "skipped": skipped}
print(
f"[taxes_az] Item: created {inserted}, already existed {existed}, "
f"skipped (missing group) {missing_group}"
)
return {
"doctype": "Item",
"inserted": inserted,
"existed": existed,
"missing_group": missing_group,
}
def sync_item_groups() -> dict:
@ -293,8 +312,33 @@ def sync_item_groups() -> dict:
Item Group is a tree doctype shared with ERPNext and user-created groups,
so the JSON is only a subset: manage_deletes=False keeps everything else
untouched, and is_tree=True rebuilds lft/rgt after the diff.
The shipped JSON parents its top-level groups under ERPNext's "All Item
Groups". On localized sites the root is renamed (e.g. "Bütün Element
Qrupları"), so we resolve the real root once and remap that parent — this
keeps the tree properly nested instead of leaving the groups orphaned.
"""
path = frappe.get_app_path("taxes_az", "master_data", "item_group.json")
return sync_master_data(
"Item Group", path, key_field="name", is_tree=True, manage_deletes=False
root = _resolve_item_group_root()
print(f"[taxes_az] Item Group: resolved tree root = {root!r}")
def _remap_root_parent(rec: dict) -> dict:
if rec.get("parent_item_group") in _ROOT_ALIASES and root:
rec["parent_item_group"] = root
return rec
result = sync_master_data(
"Item Group",
path,
key_field="name",
is_tree=True,
manage_deletes=False,
transform=_remap_root_parent,
)
print(
f"[taxes_az] Item Group: created {result.get('inserted', 0)}, "
f"updated {result.get('updated', 0)} "
f"(root={root!r})"
)
return result