Files
Odoo-Modules/fusion_plating/fusion_plating/models/res_users.py
gsinghpal 42036c23ab fix(plating-perms): Phase I post-deploy fixes (live entech test catches)
8 distinct bugs caught + fixed while testing the live admin DB on entech
after the migration was approved. Each surfaced a real Odoo 19 gotcha
now codified in CLAUDE.md (rules 13b-13l).

Picker architecture:
- res.users.x_fc_plating_landing_action_id and res.company.x_fc_default_landing_action_id
  now Many2one('ir.actions.actions') instead of ('ir.actions.act_window'),
  so the picker accepts BOTH window actions (Sale Orders / Quotations /
  Process Recipes) AND client actions (Manager Desk / Plant Kanban /
  Quality Dashboard). Picker went from 3 entries to 6.
- x_fc_pickable_landing field moved from the two subclasses to the
  ir.actions.actions base. Single source of truth.
- _render_resolved on the base dispatches to the correct subclass by
  action type.

Non-admin Preferences access:
- Added ACL grant: group_fp_technician (and all higher roles via
  implication) get read on ir.actions.actions. Without this, opening
  Preferences raised AccessError on the picker domain evaluation.
- Removed the accessible_landing_action_ids Many2many compute (failed
  for non-admins because field assignment requires write access on
  the comodel relation, even with sudo'd search). Picker now shows all
  6 entries to all users; resolver falls through gracefully if the
  user picks an action they can't reach.
- res.users SELF_WRITEABLE_FIELDS / SELF_READABLE_FIELDS extended via
  @property + super() (NOT class attribute — Odoo 19 changed the
  pattern). Non-admin users can now save the Preferences dialog with
  plating fields without hitting the standard write ACL.

Migration workflow:
- res.groups.users -> .user_ids (Odoo 19 rename; deprecated alias
  removed). Was crashing _fp_notify_owners and _cron_purge_expired.
- user.message_post -> user.partner_id.message_post (res.users uses
  _inherits delegation which doesn't expose mail.thread methods).
  Was crashing the Owner approval click.

Tablet lock screen:
- /fp/tablet/tiles points at group_fp_technician instead of the old
  group_fusion_plating_operator. Post-migration nobody holds the old
  group directly (only via implication), so res.groups.user_ids on
  the old xmlid returned empty — 'No operators configured' shown
  even with PIN set.
- PIN pad dots dark mode: empty dot now dark gray (#424245), filled
  dot now pure white. Previous version had both at light shades so
  user couldn't see PIN entry progress.
- Lock-screen logo frame dark mode: near-opaque white plate
  (rgba 0.95) so company logos designed for light backgrounds
  render correctly. Previous 0.08 alpha let the dark page bleed
  through.

Pre-deploy collision fix (already committed before deploy but
documented here for completeness):
- pre-migrate.py to rename old configurator's 'Shop Manager' group
  display name before new fp_security_v2.xml loads the new
  group_fp_shop_manager_v2 with the same display name (avoids
  res_groups_name_uniq violation).

Module versions bumped:
  fusion_plating: 19.0.21.1.0 -> 19.0.21.1.2
  fusion_plating_shopfloor: 19.0.32.0.4 -> 19.0.32.0.6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:02:32 -04:00

147 lines
5.6 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Fusion Plating role helpers on res.users.
The x_fc_plating_role Selection field is a clean UX wrapper around the
seven plating-role groups. Owner-only Team page reads/writes this field
via drag-and-drop on a kanban grouped by role.
"""
from markupsafe import Markup
from odoo import _, api, fields, models
_FP_PLATING_ROLE_TO_GROUP_XMLID = {
'technician': 'fusion_plating.group_fp_technician',
'sales_rep': 'fusion_plating.group_fp_sales_rep',
'shop_manager': 'fusion_plating.group_fp_shop_manager_v2',
'sales_manager': 'fusion_plating.group_fp_sales_manager',
'manager': 'fusion_plating.group_fp_manager',
'quality_manager': 'fusion_plating.group_fp_quality_manager',
'owner': 'fusion_plating.group_fp_owner',
}
# Highest precedence first — first match wins
_FP_ROLE_PRECEDENCE = (
'owner', 'quality_manager', 'manager', 'sales_manager',
'shop_manager', 'sales_rep', 'technician',
)
class ResUsers(models.Model):
_inherit = 'res.users'
# Allow non-admin users to write their OWN plating-related fields
# from the standard User Preferences dialog. SELF_WRITEABLE_FIELDS is
# a @property in Odoo 19 (not a class attribute) — must override via
# @property + super(). See CLAUDE.md rule 13k.
@property
def SELF_WRITEABLE_FIELDS(self):
return super().SELF_WRITEABLE_FIELDS + [
'x_fc_plating_landing_action_id', # personal landing-page override
'x_fc_signature_image', # "Plating Signature" used on reports
]
@property
def SELF_READABLE_FIELDS(self):
return super().SELF_READABLE_FIELDS + [
'x_fc_plating_landing_action_id',
'x_fc_signature_image',
'x_fc_plating_role',
'x_fc_tablet_pin_set_date',
]
x_fc_plating_role = fields.Selection(
[
('no', 'No'),
('technician', 'Technician'),
('sales_rep', 'Sales Representative'),
('shop_manager', 'Shop Manager'),
('sales_manager', 'Sales Manager'),
('manager', 'Manager'),
('quality_manager', 'Quality Manager'),
('owner', 'Owner'),
],
compute='_compute_plating_role',
inverse='_inverse_plating_role',
store=True,
string='Fusion Plating Role',
help='Highest plating role currently held by this user. Changing this '
'field reassigns the user to the corresponding res.groups (clears '
'old plating groups, adds new). Posts an audit chatter message.',
)
@api.depends('group_ids')
def _compute_plating_role(self):
# Resolve xmlids once
role_to_group = {}
for role, xmlid in _FP_PLATING_ROLE_TO_GROUP_XMLID.items():
grp = self.env.ref(xmlid, raise_if_not_found=False)
if grp:
role_to_group[role] = grp
for user in self:
user.x_fc_plating_role = 'no'
for candidate in _FP_ROLE_PRECEDENCE:
grp = role_to_group.get(candidate)
if grp and grp in user.group_ids:
user.x_fc_plating_role = candidate
break
def _inverse_plating_role(self):
# Resolve all plating-role group ids
all_role_ids = []
role_to_group = {}
for role, xmlid in _FP_PLATING_ROLE_TO_GROUP_XMLID.items():
grp = self.env.ref(xmlid, raise_if_not_found=False)
if grp:
role_to_group[role] = grp
all_role_ids.append(grp.id)
# I4 fix — capture old roles BEFORE the cache mutates by reading
# the stored x_fc_plating_role column directly from PostgreSQL.
# `user._origin.x_fc_plating_role` returns the IN-CACHE new value
# (the assignment that triggered the inverse), not the prior DB
# value, so the chatter audit displayed "X -> X" instead of the
# actual old -> new transition.
self.env.cr.execute(
"SELECT id, x_fc_plating_role FROM res_users WHERE id IN %s",
(tuple(self.ids),) if self.ids else ((0,),),
)
old_role_by_id = dict(self.env.cr.fetchall())
for user in self:
old_role = old_role_by_id.get(user.id) or 'unset'
new_role = user.x_fc_plating_role
if old_role == new_role:
# No actual change — skip both the writes and the audit so
# we don't spam chatter with "X -> X" rows.
continue
# Remove every plating-role group (additive-by-default Odoo
# m2m write of (3, id) removes single rows)
user.sudo().write({
'group_ids': [(3, gid) for gid in all_role_ids]
})
# Add the chosen role (no-op for 'no')
if new_role and new_role != 'no':
target = role_to_group.get(new_role)
if target:
user.sudo().write({
'group_ids': [(4, target.id)]
})
# Post audit (Markup so role names render bold, not literal HTML)
user.partner_id.message_post(
body=Markup(_(
'Plating role changed: <b>%(old)s</b> -> <b>%(new)s</b> by %(actor)s'
)) % {
'old': old_role,
'new': new_role or 'unset',
'actor': self.env.user.name,
},
message_type='notification',
)