From 3044e8ae8a5d56e7be95333144f1df3b97fcadfd Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Tue, 21 Jul 2026 16:09:33 +0000 Subject: [PATCH] fix(master-data): create Items like fixtures (ignore_links) + re-sync after setup wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- taxes_az/hooks.py | 6 +++++ taxes_az/master_data/sync.py | 49 +++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/taxes_az/hooks.py b/taxes_az/hooks.py index d370abc..28c8cc1 100644 --- a/taxes_az/hooks.py +++ b/taxes_az/hooks.py @@ -25,6 +25,12 @@ after_migrate = [ "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 # 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. diff --git a/taxes_az/master_data/sync.py b/taxes_az/master_data/sync.py index 92c8a94..d1f439c 100644 --- a/taxes_az/master_data/sync.py +++ b/taxes_az/master_data/sync.py @@ -282,12 +282,18 @@ def sync_items() -> dict: missing_group += 1 continue - # Isolate each insert in a savepoint: on a bare/half-provisioned site a - # missing link (e.g. UOM 'Litre') raises LinkValidationError, and an - # unhandled exception here would abort the whole `bench migrate`. Master - # data must never do that — roll back just this item and carry on. The - # 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. + # Isolate each insert in a savepoint so nothing can abort `bench migrate` + # — on these deployments migrate runs only once (during image build / + # setup) and never again, so a crash here would permanently leave the + # site without these items. + # + # 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") try: doc = frappe.new_doc("Item") @@ -296,6 +302,7 @@ def sync_items() -> dict: doc.name = name doc.flags.name_set = True doc.flags.ignore_permissions = True + doc.flags.ignore_links = True doc.insert(ignore_if_duplicate=True) except Exception as e: frappe.db.rollback(save_point="taxes_az_item") @@ -358,3 +365,33 @@ def sync_item_groups() -> dict: f"(root={root!r})" ) 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)")