feat(bridge_mrp): shop-role auto-routing + tablet worker mode (CHUNK 4/4)

Completes the worker-access story. Handoffs now route themselves.

New model fp.work.role with 8 seeded defaults (noupdate so shops can
rename/prune):
  masking · racking · plating_op · demask · oven · derack ·
  inspection · rework

Each one has a code, icon, description, sequence, active flag.
Config menu: Configuration → Shop Roles (manager-only).

Field additions:
  hr.employee.x_fc_work_role_ids (Many2many) — tag workers with the
    roles they perform. One-person shop: one employee, every role.
    Specialised shop: one role per employee. Cross-trained: multiple.
  fusion.plating.process.node.x_fc_work_role_id (Many2one) — tag
    each recipe operation with the role that performs it.
  mrp.workorder.x_fc_work_role_id (Many2one) — copied from the recipe
    operation on WO generation.

Auto-assignment on WO generation:
  _generate_workorders_from_recipe() now copies the operation's role
  onto the WO, then calls _fp_pick_worker_for_role() which picks the
  least-loaded employee (active WO count) with that role. WO lands in
  their Tablet "My Queue" the moment the MO is confirmed. No manual
  routing needed for the common case.

Tablet Station — worker mode:
  /fp/shopfloor/tablet_overview now filters to WOs where
  x_fc_assigned_user_id == env.user when the field is populated.
  KPIs (WOs Ready / In Progress) reflect the logged-in worker's load,
  not shop-wide totals. "My Queue" rows carry wo_state + can_start +
  can_finish so inline Start/Finish buttons appear.
  New JS handlers onStartWo / onFinishWo call /fp/shopfloor/start_wo
  and /fp/shopfloor/stop_wo (finish=true). One-tap progression.

Views:
  hr.employee form gets a "Shop Roles" notebook page with many2many_tags.
  Process node form gets x_fc_work_role_id inline after work_center_id.
  Work Order form shows role + assigned worker.

Smoke-tested end-to-end on WH/MO/00010:
  Masking      → Administrator (masking role)
  Racking      → Administrator (racking role)
  E-Nickel     → Andrew (plating_op, least-loaded tiebreaker)
  Demask       → Administrator (masking)
  Oven bake    → Andrew (oven)
  Derack       → Administrator (racking fallback)
  Post-plate QA → Administrator (inspection)

80 existing WOs backfilled with role + worker via name-match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-17 20:08:23 -04:00
parent 1c6a460ca1
commit f340c87b6a
14 changed files with 474 additions and 25 deletions

View File

@@ -15,3 +15,6 @@ from . import fp_job_node_override
from . import fp_job_consumption
from . import account_move
from . import sale_order
from . import fp_work_role
from . import hr_employee
from . import fp_process_node

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class FpProcessNode(models.Model):
"""Tag each recipe operation with the shop role that performs it.
The auto-assigner reads this when generating WOs: each WO inherits
its operation node's role, then hunts for an employee with a
matching x_fc_work_role_ids membership.
"""
_inherit = 'fusion.plating.process.node'
x_fc_work_role_id = fields.Many2one(
'fp.work.role', string='Performed By (Role)',
ondelete='set null',
help='Shop role that performs this step. When the WO is '
'generated it auto-routes to an employee with this role.',
)

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class FpWorkRole(models.Model):
"""A shop role assigned to a recipe step and to the employees who
can perform it.
Shops run the same part with different staffing models:
- One employee does every step (small shop): give them every role.
- Specialists per operation (masking person, racker, plater): one
role each.
- Cross-trained workers: multiple roles per worker.
The model is intentionally flat — no hierarchy, no workflow. Roles
are just tags that the WO auto-assignment compares.
"""
_name = 'fp.work.role'
_description = 'Fusion Plating — Shop Work Role'
_order = 'sequence, code'
name = fields.Char(string='Role Name', required=True, translate=True)
code = fields.Char(string='Code', required=True,
help='Short stable identifier used in auto-assignment.')
sequence = fields.Integer(default=10)
description = fields.Char(
string='Description',
help='Short operator-facing description of what this role covers.',
)
icon = fields.Selection(
[('fa-scissors', 'Scissors (masking)'),
('fa-cogs', 'Cogs (racking)'),
('fa-flask', 'Flask (plating)'),
('fa-fire', 'Fire (oven)'),
('fa-search', 'Inspection'),
('fa-wrench', 'Wrench (rework)'),
('fa-user', 'Generic worker')],
string='Icon', default='fa-user',
)
active = fields.Boolean(default=True)
_sql_constraints = [
('fp_work_role_code_uniq', 'unique(code)',
'Role code must be unique.'),
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class HrEmployee(models.Model):
"""Tag employees with the shop roles they can perform.
An employee with role 'masking' receives the masking steps when WOs
are generated; an employee with multiple roles receives WOs for all
of them. A small shop where the owner wears every hat just tags
themselves with every role.
"""
_inherit = 'hr.employee'
x_fc_work_role_ids = fields.Many2many(
'fp.work.role', 'fp_employee_work_role_rel',
'employee_id', 'role_id', string='Shop Roles',
help='Which shop roles this employee performs. Used by the '
'Manager Desk and auto-assignment on WO generation.',
)

View File

@@ -274,6 +274,36 @@ class MrpProduction(models.Model):
# ------------------------------------------------------------------
# Recipe → Work Order generation
# ------------------------------------------------------------------
@api.model
def _fp_pick_worker_for_role(self, role):
"""Pick the least-loaded employee with the given shop role.
Returns a res.users record, or None if no one has the role.
"""
if not role:
return None
Employee = self.env['hr.employee']
candidates = Employee.search(
[('x_fc_work_role_ids', 'in', role.id),
('user_id', '!=', False),
('active', '=', True)]
)
if not candidates:
return None
# Score by open WO count
WO = self.env['mrp.workorder']
best = None
best_count = 10 ** 9
for emp in candidates:
load = WO.search_count([
('x_fc_assigned_user_id', '=', emp.user_id.id),
('state', 'in', ('ready', 'progress', 'waiting', 'pending')),
])
if load < best_count:
best_count = load
best = emp.user_id
return best
def _generate_workorders_from_recipe(self):
"""Generate mrp.workorder records from the assigned recipe.
@@ -353,13 +383,25 @@ class MrpProduction(models.Model):
# store step instructions on the operation via the
# existing `operation_id.note` path, or just log them
# to the WO chatter.
wo_vals_list.append({
vals = {
'production_id': production.id,
'name': node.name,
'workcenter_id': mrp_wc,
'duration_expected': node.estimated_duration or 0,
'sequence': seq_counter[0],
})
}
# Inherit the operation's shop role (if the bridge
# module is installed) so WOs can auto-route to the
# right worker.
if 'x_fc_work_role_id' in node._fields and node.x_fc_work_role_id:
vals['x_fc_work_role_id'] = node.x_fc_work_role_id.id
# Find a worker with this role (least-loaded wins)
assignee = self._fp_pick_worker_for_role(
node.x_fc_work_role_id
)
if assignee:
vals['x_fc_assigned_user_id'] = assignee.id
wo_vals_list.append(vals)
if steps:
wo_steps[seq_counter[0]] = '\n'.join(steps)
seq_counter[0] += 10

View File

@@ -64,6 +64,11 @@ class MrpWorkorder(models.Model):
'manager; the Tablet Station shows only WOs assigned to the '
'logged-in user.',
)
x_fc_work_role_id = fields.Many2one(
'fp.work.role', string='Role',
help='Shop role required to perform this step (copied from the '
'recipe operation on WO generation).',
)
# ------------------------------------------------------------------
# Workflow step tracking