fix(master-data): create Items like fixtures (ignore_links) + re-sync after setup wizard

Two problems on image-based deployments where `bench migrate` runs only at
build time (before the setup wizard) and never again:

1. Item insert hit _validate_links and crashed migrate when standard UOMs
   ('Litre'/'Nos') weren't loaded yet. validate() itself passes — only the
   link check fails — so set doc.flags.ignore_links = True, exactly as Frappe's
   fixture import does (import_file.py). The referenced data always exists in a
   real running program, so no dangling links in practice.

2. ERPNext creates the Item Group root with the *translated* name in the setup
   wizard (e.g. Azerbaijani "Bütün Element Qrupları"), after migrate. Groups
   created at build time were parented to the non-existent "All Item Groups"
   and stayed orphaned/invisible in the tree.

Add a setup_wizard_complete hook (after_setup_wizard) that re-runs both syncs
once the wizard has created the real root and standard data: groups get
reparented under the real root and skipped items get created. It never raises
(a throwing task would fail the whole wizard), logging and swallowing instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-07-21 16:09:33 +00:00
parent ae1d0a9782
commit 3044e8ae8a
2 changed files with 49 additions and 6 deletions

View File

@ -25,6 +25,12 @@ after_migrate = [
"taxes_az.setup_accounts.check_and_create_accounts" "taxes_az.setup_accounts.check_and_create_accounts"
] ]
# On image-based deployments migrate runs only at build time, before the setup
# wizard has created the Item Group root and standard UOMs. Re-sync once the
# wizard completes so the groups nest under the real (localized) root and the
# items resolve their UOMs. See sync.after_setup_wizard.
setup_wizard_complete = "taxes_az.master_data.sync.after_setup_wizard"
# Bəyannamə düsturlarının runtime-ı. Generasiya olunmuş Client Script-lər yalnız # Bəyannamə düsturlarının runtime-ı. Generasiya olunmuş Client Script-lər yalnız
# CONFIG daşıyır və bu runtime-ı çağırır — formula_editor tətbiqinə heç bir # CONFIG daşıyır və bu runtime-ı çağırır — formula_editor tətbiqinə heç bir
# runtime asılılığı qalmır, ona görə onu iş maşınlarından silmək olar. # runtime asılılığı qalmır, ona görə onu iş maşınlarından silmək olar.

View File

@ -282,12 +282,18 @@ def sync_items() -> dict:
missing_group += 1 missing_group += 1
continue continue
# Isolate each insert in a savepoint: on a bare/half-provisioned site a # Isolate each insert in a savepoint so nothing can abort `bench migrate`
# missing link (e.g. UOM 'Litre') raises LinkValidationError, and an # — on these deployments migrate runs only once (during image build /
# unhandled exception here would abort the whole `bench migrate`. Master # setup) and never again, so a crash here would permanently leave the
# data must never do that — roll back just this item and carry on. The # site without these items.
# item stays missing and a later migrate (once the site is complete) #
# will create it, since sync only inserts items that don't exist yet. # ignore_links mirrors how Frappe imports fixtures (see
# frappe/modules/import_file.py: `doc.flags.ignore_links = True`): it
# skips _validate_links(), which is the *only* thing that fails when a
# referenced record (e.g. UOM 'Litre') is not loaded yet at build time.
# validate() itself passes, so the item is created now and its links
# resolve once the standard data is in place — which it always is in a
# real running program.
frappe.db.savepoint("taxes_az_item") frappe.db.savepoint("taxes_az_item")
try: try:
doc = frappe.new_doc("Item") doc = frappe.new_doc("Item")
@ -296,6 +302,7 @@ def sync_items() -> dict:
doc.name = name doc.name = name
doc.flags.name_set = True doc.flags.name_set = True
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.flags.ignore_links = True
doc.insert(ignore_if_duplicate=True) doc.insert(ignore_if_duplicate=True)
except Exception as e: except Exception as e:
frappe.db.rollback(save_point="taxes_az_item") frappe.db.rollback(save_point="taxes_az_item")
@ -358,3 +365,33 @@ def sync_item_groups() -> dict:
f"(root={root!r})" f"(root={root!r})"
) )
return result return result
def after_setup_wizard(args=None) -> None:
"""Re-run the master-data sync once the setup wizard has completed.
On image-based deployments `bench migrate` runs only during image build
*before* the setup wizard so ERPNext has not yet created the Item Group
tree root or the standard UOMs. Groups created at build time are therefore
parented to a root that does not exist yet (ERPNext names the root with the
*translated* "All Item Groups", e.g. the Azerbaijani "Bütün Element
Qrupları"), which leaves them orphaned and invisible in the tree, and the
fuel items cannot resolve their UOMs.
The wizard fires ``setup_wizard_complete`` after it has created the root and
the standard data, so a second sync here reparents the groups under the real
root and creates any items skipped at build time. Both syncs are idempotent,
so this is safe on sites where migrate did run after setup too.
Never raises: a task that throws here would make the whole setup wizard fail
(process_setup_stages rolls back and re-raises), so any error is logged and
swallowed completing setup matters more than this master-data refresh.
"""
print("[taxes_az] setup_wizard_complete: re-syncing master data")
try:
sync_item_groups()
sync_items()
except Exception:
frappe.db.rollback()
frappe.log_error(title="[taxes_az] after_setup_wizard failed")
print("[taxes_az] setup_wizard_complete: sync FAILED (logged, setup not blocked)")