fix(plating-perms): address final-reviewer critical + important issues
Pre-deploy fixes for Phase 1 permissions overhaul branch (catches by
final-reviewer subagent + main session).
CRITICAL FIXES:
C1: groups_id -> group_ids (Odoo 19 field rename). Affected ~30 sites
across 4 model files, 1 view, 7 test files. Documented project
gotcha (feedback_odoo19_groups_id_renamed.md) that the implementer
subagents missed because they don't see user memory.
C2: action_fp_resolve_plating_landing server action now calls
env['ir.actions.act_window'].sudo()._fp_resolve_landing_for_current_user()
instead of the old inline priority chain. Phase E's role-based
dispatch was previously dead code.
C3: New migrations/19.0.21.1.0/post-migrate.py triggers
_fp_post_init_role_migration(env) on -u. post_init_hook only fires
on INSTALL in Odoo 19, not UPGRADE -- so Phase H's preview creation
wouldn't have auto-fired on entech without this script. Module
version bumped to 19.0.21.1.0 to match the migration directory.
C4: Team kanban template rewritten for Odoo 19 (<t t-name='card'> with
semantic <aside>/<main>) instead of legacy <t t-name='kanban-box'>.
Previous template threw 'Missing card template' at render.
IMPORTANT FIXES:
I1: SO state=sent Confirm button (id='action_confirm') now also gated
to group_fp_sales_manager. Previously only the state=draft button
was gated; Sales Reps could send-and-confirm via the secondary path.
I2: Designated Officials picker domain uses all_group_ids (transitive)
instead of group_ids (explicit only). Owner users now correctly
appear as eligible CGP DO candidates via the implied_ids chain.
I3: test_menu_visibility.py compliance hub xmlid corrected to
fusion_plating.menu_fp_compliance_hub (was
fusion_plating_compliance.menu_fp_compliance_hub which doesn't exist
-- the hub menu is defined in core's fp_menu.xml). Tests were
silently skipTest-ing.
I4: _inverse_plating_role chatter audit reads old role from DB via SQL
(bypasses cache) so 'old -> new' displays actual values, and
short-circuits no-op writes.
I5: _FP_ROLE_MAPPING_RULES reordered: cgp_designated_official fires
BEFORE admin/uid_1_or_2 so admin+DO users keep the capability_delta
marker that triggers res.company.x_fc_cgp_designated_official_id
auto-set during migration.
I6: _cron_purge_expired_migrations skips groups with active users
instead of unlink-ing unconditionally. Defense against rollback
safety being bypassed by manual role assignments post-migration.
CLAUDE.md updated with 3 new durable rules (13b kanban card template,
13c group_ids vs all_group_ids, 13d post_init_hook only on install).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.21.0.6',
|
||||
'version': '19.0.21.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -24,47 +24,14 @@
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code"><![CDATA[
|
||||
# Resolve in priority order:
|
||||
# 1. user.x_fc_plating_landing_action_id (per-user override)
|
||||
# 2. company.x_fc_default_landing_action_id (company default)
|
||||
# 3. Shop Floor plant-view kanban (when x_fc_shopfloor_layout='v2')
|
||||
# 4. Sale Orders (when v2 flag unset / legacy)
|
||||
# 5. Process recipes (configurator absent)
|
||||
user = env.user
|
||||
target = False
|
||||
if 'x_fc_plating_landing_action_id' in user._fields and user.x_fc_plating_landing_action_id:
|
||||
target = user.x_fc_plating_landing_action_id.sudo()
|
||||
elif 'x_fc_default_landing_action_id' in env.company._fields and env.company.x_fc_default_landing_action_id:
|
||||
target = env.company.x_fc_default_landing_action_id.sudo()
|
||||
|
||||
if not target:
|
||||
# 2026-05-23 — plant-view dispatch. Read the layout flag and pick the
|
||||
# appropriate Shop Floor action. Falls through to Sale Orders if no
|
||||
# client action is registered (e.g. shopfloor module not installed).
|
||||
layout = env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_plating_shopfloor.layout', default='legacy',
|
||||
)
|
||||
if layout == 'v2':
|
||||
target = env.ref(
|
||||
'fusion_plating_shopfloor.action_fp_plant_kanban',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
# Legacy or v2-missing → fall through to Sale Orders
|
||||
if not target:
|
||||
target = env.ref(
|
||||
'fusion_plating_configurator.action_fp_sale_orders',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
|
||||
if target:
|
||||
action = target.sudo().read()[0]
|
||||
# Strip ids that confuse the act_window dispatcher.
|
||||
action.pop('id', None)
|
||||
else:
|
||||
# Last-ditch — open the Plating app's process recipes if even
|
||||
# the Sale Orders action is missing (e.g. configurator not installed).
|
||||
action = env.ref('fusion_plating.action_fp_process_recipe').sudo().read()[0]
|
||||
action.pop('id', None)
|
||||
# Delegates to the role-based dispatch helper on ir.actions.act_window
|
||||
# (and ir.actions.client for Manager Desk / Plant Kanban / Quality Dashboard).
|
||||
# Resolution chain in the helper:
|
||||
# 1. user.x_fc_plating_landing_action_id (per-user override)
|
||||
# 2. role-based default per spec Section 3 (Owner→ManagerDesk, etc.)
|
||||
# 3. company.x_fc_default_landing_action_id (company default)
|
||||
# 4. action_fp_sale_orders (hardcoded last-ditch)
|
||||
action = env['ir.actions.act_window'].sudo()._fp_resolve_landing_for_current_user() or False
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Phase H: fire role-migration preview creation on `-u fusion_plating`.
|
||||
|
||||
Odoo 19's `post_init_hook` ONLY fires on fresh install — never on
|
||||
upgrade. So on entech (and any other already-installed deployment),
|
||||
`-u fusion_plating` after this branch lands would otherwise leave the
|
||||
post_init_hook's `_fp_post_init_role_migration` un-fired and the
|
||||
migration preview never created.
|
||||
|
||||
This migration script bridges that gap: on every `-u` that crosses
|
||||
this version boundary, it invokes the same idempotent helper. The
|
||||
helper short-circuits if a preview is already pending or already
|
||||
applied + all users migrated, so re-running is safe.
|
||||
"""
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
from odoo import api, SUPERUSER_ID
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
try:
|
||||
from odoo.addons.fusion_plating import _fp_post_init_role_migration
|
||||
_fp_post_init_role_migration(env)
|
||||
_logger.info(
|
||||
'Fusion Plating: role-migration preview check ran via post-migrate.py'
|
||||
)
|
||||
except Exception as e:
|
||||
# Migration scripts must not block module upgrade — log and swallow
|
||||
_logger.exception(
|
||||
'Failed to run role-migration preview check (non-fatal): %s', e
|
||||
)
|
||||
@@ -227,7 +227,7 @@ class ResUsers(models.Model):
|
||||
'x_fc_plating_landing_action_id.',
|
||||
)
|
||||
|
||||
@api.depends('groups_id')
|
||||
@api.depends('group_ids')
|
||||
def _compute_accessible_landing_action_ids(self):
|
||||
Window = self.env['ir.actions.act_window']
|
||||
pickable = Window.sudo().search([('x_fc_pickable_landing', '=', True)])
|
||||
|
||||
@@ -145,13 +145,13 @@ class FpMigrationPreview(models.Model):
|
||||
|
||||
for line in self.line_ids:
|
||||
user = line.user_id
|
||||
# Snapshot current groups_id for rollback
|
||||
line.applied_groups_snapshot = json.dumps(user.groups_id.ids)
|
||||
# Snapshot current group_ids for rollback
|
||||
line.applied_groups_snapshot = json.dumps(user.group_ids.ids)
|
||||
|
||||
# Remove old plating-role groups
|
||||
if old_group_ids:
|
||||
user.sudo().write({
|
||||
'groups_id': [(3, gid) for gid in old_group_ids]
|
||||
'group_ids': [(3, gid) for gid in old_group_ids]
|
||||
})
|
||||
|
||||
# Add the new role group (no-op for 'no')
|
||||
@@ -159,7 +159,7 @@ class FpMigrationPreview(models.Model):
|
||||
if target_xmlid:
|
||||
target = self.env.ref(target_xmlid, raise_if_not_found=False)
|
||||
if target:
|
||||
user.sudo().write({'groups_id': [(4, target.id)]})
|
||||
user.sudo().write({'group_ids': [(4, target.id)]})
|
||||
|
||||
# Audit chatter on the user
|
||||
user.message_post(
|
||||
@@ -197,7 +197,7 @@ class FpMigrationPreview(models.Model):
|
||||
for line in self.line_ids:
|
||||
if line.applied_groups_snapshot:
|
||||
old_ids = json.loads(line.applied_groups_snapshot)
|
||||
line.user_id.sudo().write({'groups_id': [(6, 0, old_ids)]})
|
||||
line.user_id.sudo().write({'group_ids': [(6, 0, old_ids)]})
|
||||
self.state = 'rolled_back'
|
||||
|
||||
@api.model
|
||||
@@ -222,8 +222,25 @@ class FpMigrationPreview(models.Model):
|
||||
if g:
|
||||
old_group_ids.append(g.id)
|
||||
if old_group_ids:
|
||||
self.env['res.groups'].browse(old_group_ids).unlink()
|
||||
_logger.info('Fusion Plating migration: purged %d expired old plating groups', len(old_group_ids))
|
||||
# I6 safety check — never unlink a group that still has active
|
||||
# internal users on it. If anyone still references the group
|
||||
# we'd cascade-strip them silently from their permissions.
|
||||
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)
|
||||
if active_users:
|
||||
skipped.append((old_group.name, active_users.mapped('login')))
|
||||
else:
|
||||
safe_to_unlink.append(old_group.id)
|
||||
if skipped:
|
||||
_logger.warning(
|
||||
'Fusion Plating migration purge: skipped %d old groups with active users: %s',
|
||||
len(skipped), skipped)
|
||||
if safe_to_unlink:
|
||||
self.env['res.groups'].browse(safe_to_unlink).unlink()
|
||||
_logger.info('Fusion Plating migration: purged %d expired old plating groups',
|
||||
len(safe_to_unlink))
|
||||
|
||||
|
||||
class FpMigrationPreviewLine(models.Model):
|
||||
@@ -237,12 +254,12 @@ class FpMigrationPreviewLine(models.Model):
|
||||
capability_delta = fields.Char()
|
||||
warning = fields.Boolean()
|
||||
notes = fields.Text(help='Owner may annotate before approving')
|
||||
applied_groups_snapshot = fields.Text(help='JSON of pre-migration groups_id for rollback')
|
||||
applied_groups_snapshot = fields.Text(help='JSON of pre-migration group_ids for rollback')
|
||||
|
||||
@api.depends('user_id', 'user_id.groups_id')
|
||||
@api.depends('user_id', 'user_id.group_ids')
|
||||
def _compute_current_groups(self):
|
||||
for line in self:
|
||||
if line.user_id:
|
||||
line.current_groups = ', '.join(line.user_id.groups_id.mapped('name'))
|
||||
line.current_groups = ', '.join(line.user_id.group_ids.mapped('name'))
|
||||
else:
|
||||
line.current_groups = ''
|
||||
|
||||
@@ -38,15 +38,20 @@ _NEW_ROLE_XMLID = {
|
||||
# Highest precedence first; first match wins.
|
||||
# Predicate is a callable taking a res.users record; returns bool.
|
||||
_FP_ROLE_MAPPING_RULES = [
|
||||
# cgp_designated_official MUST be first so admin/uid_1/uid_2 users who ALSO
|
||||
# hold the DO group still get the capability_delta marker — which is what
|
||||
# triggers action_approve_and_run to set res.company.x_fc_cgp_designated_official_id.
|
||||
# If admin matched first, the DO field would never get populated for shops
|
||||
# where the admin is also the registered PSPC Designated Official.
|
||||
('cgp_designated_official',
|
||||
lambda u: u.has_group('fusion_plating_cgp.group_fusion_plating_cgp_designated_official'),
|
||||
'owner', 'Was CGP DO; field set on res.company'),
|
||||
('uid_1_or_2',
|
||||
lambda u: u.id in (1, 2),
|
||||
'owner', None),
|
||||
('admin',
|
||||
lambda u: u.has_group('fusion_plating.group_fusion_plating_admin'),
|
||||
'owner', None),
|
||||
('cgp_designated_official',
|
||||
lambda u: u.has_group('fusion_plating_cgp.group_fusion_plating_cgp_designated_official'),
|
||||
'owner', 'Was CGP DO; field set on res.company'),
|
||||
('cgp_officer',
|
||||
lambda u: u.has_group('fusion_plating_cgp.group_fusion_plating_cgp_officer'),
|
||||
'quality_manager', None),
|
||||
|
||||
@@ -53,7 +53,7 @@ class ResUsers(models.Model):
|
||||
'old plating groups, adds new). Posts an audit chatter message.',
|
||||
)
|
||||
|
||||
@api.depends('groups_id')
|
||||
@api.depends('group_ids')
|
||||
def _compute_plating_role(self):
|
||||
# Resolve xmlids once
|
||||
role_to_group = {}
|
||||
@@ -65,7 +65,7 @@ class ResUsers(models.Model):
|
||||
user.x_fc_plating_role = 'no'
|
||||
for candidate in _FP_ROLE_PRECEDENCE:
|
||||
grp = role_to_group.get(candidate)
|
||||
if grp and grp in user.groups_id:
|
||||
if grp and grp in user.group_ids:
|
||||
user.x_fc_plating_role = candidate
|
||||
break
|
||||
|
||||
@@ -79,14 +79,30 @@ class ResUsers(models.Model):
|
||||
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 = user._origin.x_fc_plating_role if user._origin else None
|
||||
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({
|
||||
'groups_id': [(3, gid) for gid in all_role_ids]
|
||||
'group_ids': [(3, gid) for gid in all_role_ids]
|
||||
})
|
||||
|
||||
# Add the chosen role (no-op for 'no')
|
||||
@@ -94,15 +110,15 @@ class ResUsers(models.Model):
|
||||
target = role_to_group.get(new_role)
|
||||
if target:
|
||||
user.sudo().write({
|
||||
'groups_id': [(4, target.id)]
|
||||
'group_ids': [(4, target.id)]
|
||||
})
|
||||
|
||||
# Post audit (Markup() so role names render as bold, not literal HTML)
|
||||
# Post audit (Markup so role names render bold, not literal HTML)
|
||||
user.message_post(
|
||||
body=Markup(_(
|
||||
'Plating role changed: <b>%(old)s</b> -> <b>%(new)s</b> by %(actor)s'
|
||||
)) % {
|
||||
'old': old_role or 'unset',
|
||||
'old': old_role,
|
||||
'new': new_role or 'unset',
|
||||
'actor': self.env.user.name,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ class TestAclMigration(TransactionCase):
|
||||
'login': f'fp_test_{login}',
|
||||
'name': f'FP Test {login.title()}',
|
||||
'email': f'fp_test_{login}@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref(group_xmlid).id])],
|
||||
'group_ids': [(6, 0, [self.env.ref(group_xmlid).id])],
|
||||
})
|
||||
|
||||
self.u_tech = make('tech', 'fusion_plating.group_fp_technician')
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestLandingResolver(TransactionCase):
|
||||
'login': f'land_{name}',
|
||||
'name': f'Land {name}',
|
||||
'email': f'land_{name}@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref(xmlid).id])],
|
||||
'group_ids': [(6, 0, [self.env.ref(xmlid).id])],
|
||||
})
|
||||
|
||||
self.u_tech = mk('tech', 'fusion_plating.group_fp_technician')
|
||||
|
||||
@@ -12,14 +12,14 @@ class TestMenuVisibility(TransactionCase):
|
||||
return Users.create({
|
||||
'login': f'menu_{name}', 'name': f'Menu Test {name}',
|
||||
'email': f'menu_{name}@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref(xmlid).id])] if xmlid else [(6, 0, [])],
|
||||
'group_ids': [(6, 0, [self.env.ref(xmlid).id])] if xmlid else [(6, 0, [])],
|
||||
})
|
||||
# "No" user has only base.group_user — no plating group
|
||||
no_user = Users.create({
|
||||
'login': 'menu_no', 'name': 'Menu Test no',
|
||||
'email': 'menu_no@example.com',
|
||||
})
|
||||
no_user.write({'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]})
|
||||
no_user.write({'group_ids': [(6, 0, [self.env.ref('base.group_user').id])]})
|
||||
self.u_no = no_user
|
||||
self.u_tech = mk('tech', 'fusion_plating.group_fp_technician')
|
||||
self.u_sr = mk('sr', 'fusion_plating.group_fp_sales_rep')
|
||||
@@ -73,13 +73,13 @@ class TestMenuVisibility(TransactionCase):
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_manager_does_not_see_compliance(self):
|
||||
result = self._visible(self.u_mgr, 'fusion_plating_compliance.menu_fp_compliance_hub')
|
||||
result = self._visible(self.u_mgr, 'fusion_plating.menu_fp_compliance_hub')
|
||||
if result is None:
|
||||
self.skipTest('Compliance hub not found')
|
||||
self.assertFalse(result, 'Manager must not see Compliance hub')
|
||||
|
||||
def test_qm_sees_compliance(self):
|
||||
result = self._visible(self.u_qm, 'fusion_plating_compliance.menu_fp_compliance_hub')
|
||||
result = self._visible(self.u_qm, 'fusion_plating.menu_fp_compliance_hub')
|
||||
if result is None:
|
||||
self.skipTest('Compliance hub not found')
|
||||
self.assertTrue(result)
|
||||
|
||||
@@ -12,14 +12,14 @@ class TestMigrationWorkflow(TransactionCase):
|
||||
self.owner = Users.create({
|
||||
'login': 'mig_owner', 'name': 'Mig Owner',
|
||||
'email': 'mig_owner@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_owner').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_owner').id])],
|
||||
})
|
||||
|
||||
def test_only_owner_can_approve(self):
|
||||
non_owner = self.env['res.users'].with_context(no_reset_password=True).create({
|
||||
'login': 'mig_nonowner', 'name': 'Non Owner',
|
||||
'email': 'mig_nonowner@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
})
|
||||
preview = self.env['fp.migration.preview'].create({})
|
||||
preview._fp_build_lines()
|
||||
@@ -52,7 +52,7 @@ class TestMigrationWorkflow(TransactionCase):
|
||||
u = self.env['res.users'].with_context(no_reset_password=True).create({
|
||||
'login': 'mig_rb', 'name': 'RB',
|
||||
'email': 'mig_rb@example.com',
|
||||
'groups_id': [(6, 0, [old_mgr.id])],
|
||||
'group_ids': [(6, 0, [old_mgr.id])],
|
||||
})
|
||||
before_ids = sorted(u.groups_id.ids)
|
||||
preview = self.env['fp.migration.preview'].create({})
|
||||
@@ -74,7 +74,7 @@ class TestMigrationWorkflow(TransactionCase):
|
||||
u = self.env['res.users'].with_context(no_reset_password=True).create({
|
||||
'login': 'mig_est', 'name': 'Est',
|
||||
'email': 'mig_est@example.com',
|
||||
'groups_id': [(6, 0, [est.id])],
|
||||
'group_ids': [(6, 0, [est.id])],
|
||||
})
|
||||
preview = self.env['fp.migration.preview'].create({})
|
||||
preview._fp_build_lines()
|
||||
|
||||
@@ -13,12 +13,12 @@ class TestQualitySplit(TransactionCase):
|
||||
self.u_mgr = Users.create({
|
||||
'login': 'qsplit_mgr', 'name': 'QSplit Mgr',
|
||||
'email': 'qsplit_mgr@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
})
|
||||
self.u_qm = Users.create({
|
||||
'login': 'qsplit_qm', 'name': 'QSplit QM',
|
||||
'email': 'qsplit_qm@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_quality_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_quality_manager').id])],
|
||||
})
|
||||
|
||||
# CAPA: Manager read-only, QM full
|
||||
|
||||
@@ -11,12 +11,12 @@ class TestSalesManagerGate(TransactionCase):
|
||||
self.u_sr = Users.create({
|
||||
'login': 'gate_sr', 'name': 'Gate SR',
|
||||
'email': 'gate_sr@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_sales_rep').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_sales_rep').id])],
|
||||
})
|
||||
self.u_smg = Users.create({
|
||||
'login': 'gate_smg', 'name': 'Gate SMg',
|
||||
'email': 'gate_smg@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_sales_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_sales_manager').id])],
|
||||
})
|
||||
partner = self.env['res.partner'].create({'name': 'Gate Test Customer'})
|
||||
product = self.env['product.product'].create({'name': 'Gate Test Product'})
|
||||
@@ -40,7 +40,7 @@ class TestSalesManagerGate(TransactionCase):
|
||||
u_mgr = self.env['res.users'].with_context(no_reset_password=True).create({
|
||||
'login': 'gate_mgr', 'name': 'Gate Mgr',
|
||||
'email': 'gate_mgr@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
})
|
||||
self.so.with_user(u_mgr).action_confirm()
|
||||
self.assertEqual(self.so.state, 'sale')
|
||||
|
||||
@@ -12,12 +12,12 @@ class TestTeamPage(TransactionCase):
|
||||
self.owner = Users.create({
|
||||
'login': 'team_owner', 'name': 'Team Owner',
|
||||
'email': 'team_owner@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_owner').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_owner').id])],
|
||||
})
|
||||
self.target = Users.create({
|
||||
'login': 'team_target', 'name': 'Team Target',
|
||||
'email': 'team_target@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_technician').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_technician').id])],
|
||||
})
|
||||
|
||||
def test_compute_returns_technician(self):
|
||||
@@ -25,7 +25,7 @@ class TestTeamPage(TransactionCase):
|
||||
|
||||
def test_compute_picks_highest_role(self):
|
||||
# Add Manager group on top of Technician
|
||||
self.target.write({'groups_id': [(4, self.env.ref('fusion_plating.group_fp_manager').id)]})
|
||||
self.target.write({'group_ids': [(4, self.env.ref('fusion_plating.group_fp_manager').id)]})
|
||||
self.target.invalidate_recordset(['x_fc_plating_role'])
|
||||
self.assertEqual(self.target.x_fc_plating_role, 'manager')
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestTeamPage(TransactionCase):
|
||||
mgr = self.env['res.users'].with_context(no_reset_password=True).create({
|
||||
'login': 'team_mgr', 'name': 'Team Mgr',
|
||||
'email': 'team_mgr@example.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
'group_ids': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])],
|
||||
})
|
||||
visible = self.env['ir.ui.menu'].with_user(mgr).search_count([('id', '=', menu.id)])
|
||||
self.assertFalse(visible, 'Manager must not see Team menu (Owner-only)')
|
||||
|
||||
@@ -23,22 +23,20 @@
|
||||
<field name="login_date"/>
|
||||
<field name="name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_image">
|
||||
<img t-att-src="kanban_image('res.users', 'image_128', record.id.raw_value)"
|
||||
alt="Photo"/>
|
||||
<t t-name="card" class="flex-row align-items-center">
|
||||
<aside class="o_kanban_aside_full">
|
||||
<field name="image_128" widget="image"
|
||||
options="{'preview_image': 'image_128', 'img_class': 'rounded'}"/>
|
||||
</aside>
|
||||
<main class="ms-2">
|
||||
<field name="name" class="fw-bolder fs-5"/>
|
||||
<div t-if="record.email.raw_value" class="text-muted small">
|
||||
<field name="email"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<strong><field name="name"/></strong>
|
||||
<div t-if="record.email.raw_value">
|
||||
<field name="email"/>
|
||||
</div>
|
||||
<div t-if="record.login_date.raw_value" class="text-muted">
|
||||
Last seen: <field name="login_date" widget="date"/>
|
||||
</div>
|
||||
<div t-if="record.login_date.raw_value" class="text-muted small">
|
||||
Last seen: <field name="login_date" widget="date"/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
groups="fusion_plating.group_fp_owner">
|
||||
<group>
|
||||
<field name="x_fc_cgp_designated_official_id"
|
||||
domain="[('groups_id', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
||||
domain="[('all_group_ids', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
||||
<field name="x_fc_nadcap_authority_user_id"
|
||||
domain="[('groups_id', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
||||
domain="[('all_group_ids', 'in', [ref('fusion_plating.group_fp_quality_manager'), ref('fusion_plating.group_fp_owner')])]"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
Reference in New Issue
Block a user