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 <field domain=> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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`.
|
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/<version>/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.
|
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/<version>/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 `<field name="tax_totals" widget="account-tax-totals-field"/>` 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 '<xpath expr="//field[@name='amount_total']">' 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 `<field domain="...">`**: 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/<version>/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:**
|
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
|
```xml
|
||||||
<!-- In downstream module's security XML -->
|
<!-- In downstream module's security XML -->
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -101,7 +101,7 @@ class FpMigrationPreview(models.Model):
|
|||||||
owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False)
|
owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False)
|
||||||
if not owner_grp:
|
if not owner_grp:
|
||||||
return
|
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:
|
if not owners:
|
||||||
_logger.warning('Fusion Plating migration preview %s: no Owner users to notify', self.id)
|
_logger.warning('Fusion Plating migration preview %s: no Owner users to notify', self.id)
|
||||||
return
|
return
|
||||||
@@ -228,7 +228,7 @@ class FpMigrationPreview(models.Model):
|
|||||||
safe_to_unlink = []
|
safe_to_unlink = []
|
||||||
skipped = []
|
skipped = []
|
||||||
for old_group in self.env['res.groups'].browse(old_group_ids).exists():
|
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:
|
if active_users:
|
||||||
skipped.append((old_group.name, active_users.mapped('login')))
|
skipped.append((old_group.name, active_users.mapped('login')))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -11,10 +11,13 @@
|
|||||||
<page string="Plating Designated Officials"
|
<page string="Plating Designated Officials"
|
||||||
groups="fusion_plating.group_fp_owner">
|
groups="fusion_plating.group_fp_owner">
|
||||||
<group>
|
<group>
|
||||||
<field name="x_fc_cgp_designated_official_id"
|
<!-- No domain on the picker: Owner picks freely.
|
||||||
domain="[('all_group_ids', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
ref() in XML domains trips Odoo 19's view validator
|
||||||
<field name="x_fc_nadcap_authority_user_id"
|
(interpreted as field access on res.company).
|
||||||
domain="[('all_group_ids', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
The QM/Owner eligibility constraint is enforced
|
||||||
|
in Python via @api.constrains on res.company. -->
|
||||||
|
<field name="x_fc_cgp_designated_official_id"/>
|
||||||
|
<field name="x_fc_nadcap_authority_user_id"/>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="0">
|
<data noupdate="0">
|
||||||
<record id="rule_fp_certificate_fair_nadcap_qm_only" model="ir.rule">
|
<!-- fp.certificate.certificate_type Selection values (per fp_certificate.py:27):
|
||||||
<field name="name">FP Certificate: FAIR/Nadcap edit restricted to Quality Manager</field>
|
'coc', 'thickness_report', 'mill_test', 'nadcap_cert', 'customer_specific'.
|
||||||
|
FAIR is a separate model (fusion.plating.fair); no 'fair' value here.
|
||||||
|
Nadcap is the only QM-restricted type at the model level. -->
|
||||||
|
<record id="rule_fp_certificate_nadcap_qm_only" model="ir.rule">
|
||||||
|
<field name="name">FP Certificate: Nadcap edit restricted to Quality Manager</field>
|
||||||
<field name="model_id" ref="model_fp_certificate"/>
|
<field name="model_id" ref="model_fp_certificate"/>
|
||||||
<field name="domain_force">[('cert_type', 'not in', ('fair', 'nadcap'))]</field>
|
<field name="domain_force">[('certificate_type', '!=', 'nadcap_cert')]</field>
|
||||||
<field name="groups" eval="[(4, ref('fusion_plating.group_fp_manager'))]"/>
|
<field name="groups" eval="[(4, ref('fusion_plating.group_fp_manager'))]"/>
|
||||||
<field name="perm_read" eval="False"/>
|
<field name="perm_read" eval="False"/>
|
||||||
<field name="perm_write" eval="True"/>
|
<field name="perm_write" eval="True"/>
|
||||||
|
|||||||
@@ -39,19 +39,17 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<header>
|
<header>
|
||||||
<!-- Phase D5 — Nadcap certs are QM-only to Issue per spec
|
<!-- Phase D5 — Nadcap-cert restriction enforced at MODEL
|
||||||
section 2.C (FAIR/Nadcap sign/issue restricted to
|
layer via ir.rule (rule_fp_certificate_nadcap_qm_only
|
||||||
Quality Manager). Strategy B: single button visible
|
in fp_cert_security.xml). Single Issue button visible
|
||||||
to all when state=draft and cert_type is routine
|
to all Manager+ when state=draft. Manager clicking
|
||||||
(coc/thickness_report/mill_test/customer_specific);
|
Issue on a Nadcap cert gets AccessError from the rule.
|
||||||
hidden for non-QM when cert_type=nadcap_cert. The
|
(Strategy B with user_has_groups() inside invisible=
|
||||||
ir.rule from Phase C also restricts model writes on
|
was rejected by Odoo 19 view validator — see CLAUDE.md
|
||||||
FAIR/Nadcap so model-layer enforcement is independent.
|
rule 13f.) -->
|
||||||
No separate action_sign exists on this model — Issue
|
|
||||||
is the sign + publish action. -->
|
|
||||||
<button name="action_issue" string="Issue"
|
<button name="action_issue" string="Issue"
|
||||||
type="object" class="btn-primary"
|
type="object" class="btn-primary"
|
||||||
invisible="state != 'draft' or (certificate_type == 'nadcap_cert' and not user_has_groups('fusion_plating.group_fp_quality_manager'))"/>
|
invisible="state != 'draft'"/>
|
||||||
<!-- Print = the same EN report action the gear-menu
|
<!-- Print = the same EN report action the gear-menu
|
||||||
Print > Certificate of Conformance (English)
|
Print > Certificate of Conformance (English)
|
||||||
calls. Routes through fusion_pdf_preview's
|
calls. Routes through fusion_pdf_preview's
|
||||||
|
|||||||
@@ -378,13 +378,10 @@
|
|||||||
<xpath expr="//field[@name='order_line']/list/field[@name='price_subtotal']" position="attributes">
|
<xpath expr="//field[@name='order_line']/list/field[@name='price_subtotal']" position="attributes">
|
||||||
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='amount_total']" position="attributes">
|
<!-- Odoo 19: amount_total / amount_untaxed / amount_tax are rendered
|
||||||
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
by the single tax_totals widget; no separate fields. Gate the
|
||||||
</xpath>
|
widget itself to hide the entire totals block from non-Sales-Rep. -->
|
||||||
<xpath expr="//field[@name='amount_untaxed']" position="attributes">
|
<xpath expr="//field[@name='tax_totals']" position="attributes">
|
||||||
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='amount_tax']" position="attributes">
|
|
||||||
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
<attribute name="groups">fusion_plating.group_fp_sales_rep</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
@@ -74,41 +74,41 @@
|
|||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="type"
|
<field name="type"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="ncr_id"
|
<field name="ncr_id"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="facility_id" readonly="1"/>
|
<field name="facility_id" readonly="1"/>
|
||||||
<field name="owner_id"
|
<field name="owner_id"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="due_date"
|
<field name="due_date"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="is_overdue" readonly="1"/>
|
<field name="is_overdue" readonly="1"/>
|
||||||
<field name="verification_date"
|
<field name="verification_date"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="verification_by_id"
|
<field name="verification_by_id"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="is_effective" readonly="1"/>
|
<field name="is_effective" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Description">
|
<page string="Description">
|
||||||
<field name="description"
|
<field name="description"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Root Cause Analysis">
|
<page string="Root Cause Analysis">
|
||||||
<field name="root_cause_analysis"
|
<field name="root_cause_analysis"
|
||||||
placeholder="5 Whys, fishbone, or any other structured method."
|
placeholder="5 Whys, fishbone, or any other structured method."
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Action Plan">
|
<page string="Action Plan">
|
||||||
<field name="action_plan"
|
<field name="action_plan"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Effectiveness">
|
<page string="Effectiveness">
|
||||||
<field name="effectiveness_notes"
|
<field name="effectiveness_notes"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
|||||||
@@ -38,43 +38,43 @@
|
|||||||
stays visible — only inputs lock for non-QM. -->
|
stays visible — only inputs lock for non-QM. -->
|
||||||
<label for="name"/>
|
<label for="name"/>
|
||||||
<h1><field name="name"
|
<h1><field name="name"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/></h1>
|
/></h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="code"
|
<field name="code"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="revision"
|
<field name="revision"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="spec_type"
|
<field name="spec_type"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="partner_id"
|
<field name="partner_id"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="effective_date"
|
<field name="effective_date"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
<field name="document_url" widget="url"
|
<field name="document_url" widget="url"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Applicable Processes" name="applicable_processes">
|
<group string="Applicable Processes" name="applicable_processes">
|
||||||
<field name="process_type_ids" widget="many2many_tags" nolabel="1"
|
<field name="process_type_ids" widget="many2many_tags" nolabel="1"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Applicable Recipes" name="applicable_recipes">
|
<group string="Applicable Recipes" name="applicable_recipes">
|
||||||
<field name="recipe_ids" widget="many2many_tags" nolabel="1"
|
<field name="recipe_ids" widget="many2many_tags" nolabel="1"
|
||||||
options="{'no_create_edit': True}"
|
options="{'no_create_edit': True}"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="print_on_cert"
|
<field name="print_on_cert"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Notes">
|
<page string="Notes">
|
||||||
<field name="notes"
|
<field name="notes"
|
||||||
readonly="not user_has_groups('fusion_plating.group_fp_quality_manager')"/>
|
/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
|||||||
Reference in New Issue
Block a user