From 7bcbcb40086b415fa9174be684598c9651cde6f1 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 09:07:13 -0400 Subject: [PATCH] fix(plating-perms): deploy-time cascade fixes from entech I3 5 fixes discovered during the live deploy to entech LXC 111: 1. pre-migrate.py to rename old configurator's 'Shop Manager' group BEFORE new core 'Shop Manager v2' XML loads (cross-module name collision on res_groups_name_uniq). 2. res_company_views.xml: dropped ref() inside attribute (Odoo 19 view validator interprets it as a field name). 3. sale_order_views.xml: replaced 3 separate xpaths for amount_total / amount_untaxed / amount_tax with a single xpath on tax_totals widget (Odoo 19 sale.view_order_form uses one widget instead of separate fields). 4. fp_cert_security.xml: certificate_type field, not cert_type. FAIR is a separate model so the rule only restricts cert_type='nadcap_cert' now. 5. fp_certificate_views.xml + fp_capa_views.xml + fp_customer_spec_views.xml: stripped user_has_groups() from invisible= / readonly= attrs (Odoo 19 view validator interprets as field name). Model-layer ACLs and ir.rules already enforce the same restrictions. Also fixed res.groups.users -> user_ids in fp_migration.py (Odoo 19 rename, caught when manually invoking _fp_notify_owners post-deploy). CLAUDE.md updated with 4 new rules (13e cross-module name collisions, 13f ref() in domain, 13g tax_totals widget, 13h user_has_groups in attrs). Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/CLAUDE.md | 23 +++++++++ .../migrations/19.0.21.1.0/pre-migrate.py | 47 +++++++++++++++++++ .../fusion_plating/models/fp_migration.py | 4 +- .../views/res_company_views.xml | 11 +++-- .../security/fp_cert_security.xml | 10 ++-- .../views/fp_certificate_views.xml | 20 ++++---- .../views/sale_order_views.xml | 11 ++--- .../views/fp_capa_views.xml | 20 ++++---- .../views/fp_customer_spec_views.xml | 22 ++++----- 9 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 fusion_plating/fusion_plating/migrations/19.0.21.1.0/pre-migrate.py diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 3a6a027f..e107bd54 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -220,6 +220,29 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval Reference: `/usr/lib/python3/dist-packages/odoo/addons/web/static/src/views/kanban/kanban_arch_parser.js`. Pre-existing `fp_rack_views.xml` still uses the old name and would also fail at render — fix when next touched. Caught 2026-05-24 by final reviewer of permissions-overhaul branch. 13c. **`res.users.group_ids` NOT `groups_id`**: Odoo 19 renamed the m2m field. Old name doesn't resolve; `@api.depends('groups_id')` raises `ValueError` at module load. Also: domain on relational pickers should use `all_group_ids` (transitive set incl. implied) instead of `group_ids` (only directly-assigned) — otherwise an Owner user won't match a domain looking for QM members. See `feedback_odoo19_groups_id_renamed.md`. 13d. **`post_init_hook` ONLY fires on INSTALL, not UPGRADE** in Odoo 19. For logic that must run on `-u` of an existing install (entech case), add a `migrations//post-migrate.py` with a `migrate(cr, version)` function that calls the same helper. The hook still works on fresh install; the migration script bridges the gap on `-u`. Both should be idempotent so re-runs are safe. +13g. **Odoo 19's `sale.view_order_form` uses a single `` widget instead of separate `amount_total` / `amount_untaxed` / `amount_tax` fields**. Inheriting xpaths targeting any of the three separate fields will fail at view load: `Element '' cannot be located in parent view`. To gate or modify totals, target the `tax_totals` widget (one xpath hides the whole totals block). Other views in the file (kanban, list, pivot) DO still have the individual fields — only the FORM view consolidated to the widget. Same likely applies to `purchase.purchase_order_form` and `account.view_move_form` — verify per-view before porting Odoo 17/18 xpaths. Caught 2026-05-24. +13h. **`user_has_groups('xmlid')` is NOT available inside Odoo 19's `invisible=`/`readonly=`/`required=` attribute expressions**. The view validator parses `user_has_groups` as a field name on the host model and fails: `field 'user_has_groups' does not exist in model 'X'`. Group-based UI gating must use the `groups=` attribute on the element instead. To combine group-AND-state logic, EITHER split into two elements with mutually-exclusive `invisible` AND different `groups=`, OR enforce one half at the model layer (ir.rule / @api.constrains) and the other in the view. Caught 2026-05-24 when a single button used `invisible="state != 'draft' or (cert_type == 'nadcap' and not user_has_groups(...))"` — rewrote as a single button with `groups="group_fp_manager"` + `invisible="state != 'draft'"` and let the ir.rule enforce the Nadcap-write restriction (Manager clicking Issue on a Nadcap cert now raises AccessError). +13f. **Odoo 19 view validator rejects `ref('xmlid')` inside ``**: the validator parses `ref(...)` as a field-access on the host model and fails with `field 'ref' does not exist in model 'X'`. Even though `ref()` IS resolved at runtime by the client, validation fires first and aborts module load. Workarounds (pick one): + - **Drop the domain** and enforce eligibility via `@api.constrains` on the Python side (simplest — used for `res.company.x_fc_cgp_designated_official_id` in this project; the Owner makes a deliberate choice and Python validates at save time). + - **Pre-compute eligible IDs** in a stored `Many2many` compute on the host model, then `domain="[('id', 'in', eligible_ids_field)]"`. + - Move the domain into the field definition in Python (`fields.Many2one(..., domain="[...]")`) — but Python-side domains have the same `ref()` limitation, so this isn't always an escape. + Caught 2026-05-24 deploying permissions-overhaul to entech. +13e. **`res_groups_name_uniq` constraint is `(privilege_id, name)` — cross-module display-name collisions during `-u` need a `pre-migrate.py` rename**. If a base module's new XML defines a group with the same display name as a DOWNSTREAM module's existing group (e.g. core adds new `Shop Manager (v2)` while configurator already has old `Shop Manager`), the new INSERT collides with the still-named-the-same downstream row, because Odoo loads modules in dep order and the downstream rename via XML hasn't happened yet. The fix is a `migrations//pre-migrate.py` in the BASE module that SQL-renames the downstream row before the new XML loads: + ```python + def migrate(cr, version): + cr.execute(\"\"\" + UPDATE res_groups + SET name = jsonb_build_object('en_US', '[DEPRECATED] Shop Manager (...)') + WHERE id IN ( + SELECT res_id FROM ir_model_data + WHERE module = 'fusion_plating_configurator' + AND name = 'group_fp_shop_manager' + AND model = 'res.groups' + ) + AND (name IS NULL OR name->>'en_US' NOT LIKE '[DEPRECATED]%'); + \"\"\") + ``` + Pre-migrate scripts run BEFORE the module's data files reload, so the constraint is clear by the time the new group XML INSERTs. Caught 2026-05-24 during permissions-overhaul deploy — `fp_security_v2.xml` claimed `'Shop Manager'` while old configurator's `group_fp_shop_manager` still held that display name in the DB. Same pattern applies to ANY base-module XML adding groups with names that overlap downstream-module groups. 13a. **Cross-module xmlid refs — base modules CANNOT forward-ref downstream xmlids**: A BASE module's data XML cannot `ref('downstream_module.some_xmlid')` because at fresh install, the base module loads FIRST and `ir.model.data` has no row for the downstream xmlid yet → `ValueError: External ID not found`. This bites on entech (existing DB has the row) but breaks fresh CI/test/demo/new-client installs. **Fix pattern: relocate the cross-module link to the downstream module's own security/data file, using an additive write to the BASE module's record:** ```xml diff --git a/fusion_plating/fusion_plating/migrations/19.0.21.1.0/pre-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.21.1.0/pre-migrate.py new file mode 100644 index 00000000..cdd59231 --- /dev/null +++ b/fusion_plating/fusion_plating/migrations/19.0.21.1.0/pre-migrate.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""Phase A bootstrap: rename old configurator's Shop Manager before new +core group_fp_shop_manager_v2 tries to claim the 'Shop Manager' display name. + +Load order: + 1. fusion_plating loads -> fp_security.xml renames its own old groups (Operator, + Supervisor, Manager, Administrator) to '[DEPRECATED] X'. Then fp_security_v2.xml + creates new groups (Technician, ..., Shop Manager v2 with display name 'Shop Manager'). + 2. fusion_plating_configurator loads later -> would rename its own + group_fp_shop_manager to '[DEPRECATED] Shop Manager'. + +But step 1 crashes because the OLD configurator's group is still named just +'Shop Manager' in the DB (the rename in step 2 hasn't run yet), and the unique +constraint res_groups_name_uniq blocks the new 'Shop Manager'. + +This pre-migrate script runs BEFORE any of fusion_plating's data files reload, +patching the old configurator row's display name via SQL. After that, the +constraint is clear and fp_security_v2.xml can create its new groups safely. +The configurator's later -u will then push the canonical '[DEPRECATED] Shop +Manager' display name from its XML data. +""" +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + # Find old configurator Shop Manager row via ir.model.data and rename + # its display name to avoid the constraint collision. + cr.execute(""" + UPDATE res_groups + SET name = jsonb_build_object('en_US', '[DEPRECATED] Shop Manager (Mgr+Estimator bundle)') + WHERE id IN ( + SELECT res_id FROM ir_model_data + WHERE module = 'fusion_plating_configurator' + AND name = 'group_fp_shop_manager' + AND model = 'res.groups' + ) + AND (name IS NULL OR name->>'en_US' NOT LIKE '[DEPRECATED]%'); + """) + rows = cr.rowcount + if rows: + _logger.info( + 'Fusion Plating: pre-migrate renamed %d old configurator Shop Manager ' + 'row(s) to clear name collision with new group_fp_shop_manager_v2', + rows, + ) diff --git a/fusion_plating/fusion_plating/models/fp_migration.py b/fusion_plating/fusion_plating/models/fp_migration.py index 18be39a7..df237205 100644 --- a/fusion_plating/fusion_plating/models/fp_migration.py +++ b/fusion_plating/fusion_plating/models/fp_migration.py @@ -101,7 +101,7 @@ class FpMigrationPreview(models.Model): owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False) if not owner_grp: return - owners = owner_grp.users.filtered(lambda u: u.active and not u.share) + owners = owner_grp.user_ids.filtered(lambda u: u.active and not u.share) if not owners: _logger.warning('Fusion Plating migration preview %s: no Owner users to notify', self.id) return @@ -228,7 +228,7 @@ class FpMigrationPreview(models.Model): safe_to_unlink = [] skipped = [] for old_group in self.env['res.groups'].browse(old_group_ids).exists(): - active_users = old_group.users.filtered(lambda u: u.active and not u.share) + active_users = old_group.user_ids.filtered(lambda u: u.active and not u.share) if active_users: skipped.append((old_group.name, active_users.mapped('login'))) else: diff --git a/fusion_plating/fusion_plating/views/res_company_views.xml b/fusion_plating/fusion_plating/views/res_company_views.xml index 8bfaf513..aa45204d 100644 --- a/fusion_plating/fusion_plating/views/res_company_views.xml +++ b/fusion_plating/fusion_plating/views/res_company_views.xml @@ -11,10 +11,13 @@ - - + + + diff --git a/fusion_plating/fusion_plating_certificates/security/fp_cert_security.xml b/fusion_plating/fusion_plating_certificates/security/fp_cert_security.xml index 99d11ec8..90e8fb44 100644 --- a/fusion_plating/fusion_plating_certificates/security/fp_cert_security.xml +++ b/fusion_plating/fusion_plating_certificates/security/fp_cert_security.xml @@ -1,10 +1,14 @@ - - FP Certificate: FAIR/Nadcap edit restricted to Quality Manager + + + FP Certificate: Nadcap edit restricted to Quality Manager - [('cert_type', 'not in', ('fair', 'nadcap'))] + [('certificate_type', '!=', 'nadcap_cert')] diff --git a/fusion_plating/fusion_plating_certificates/views/fp_certificate_views.xml b/fusion_plating/fusion_plating_certificates/views/fp_certificate_views.xml index 2b280275..b4b0dc36 100644 --- a/fusion_plating/fusion_plating_certificates/views/fp_certificate_views.xml +++ b/fusion_plating/fusion_plating_certificates/views/fp_certificate_views.xml @@ -39,19 +39,17 @@
- +