diff --git a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
index 05709e63..2a087688 100644
--- a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
+++ b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
@@ -41,6 +41,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'fusion_plating_batch',
'fusion_plating_shopfloor',
'fusion_plating_configurator',
+ 'hr',
'mrp',
'mrp_workorder',
'mrp_account',
@@ -49,6 +50,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
],
'data': [
'security/ir.model.access.csv',
+ 'data/fp_work_role_data.xml',
'wizard/fp_recipe_config_wizard_views.xml',
'views/mrp_workcenter_views.xml',
'views/mrp_workorder_views.xml',
@@ -58,6 +60,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'views/fp_batch_views.xml',
'views/fp_workorder_priority_views.xml',
'views/fp_job_consumption_views.xml',
+ 'views/fp_work_role_views.xml',
],
'installable': True,
'application': False,
diff --git a/fusion_plating/fusion_plating_bridge_mrp/data/fp_work_role_data.xml b/fusion_plating/fusion_plating_bridge_mrp/data/fp_work_role_data.xml
new file mode 100644
index 00000000..b50c5ca8
--- /dev/null
+++ b/fusion_plating/fusion_plating_bridge_mrp/data/fp_work_role_data.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+ Masking
+ masking
+ 10
+ fa-scissors
+ Applies masking tape/lacquer before plating and removes after.
+
+
+
+ Racking
+ racking
+ 20
+ fa-cogs
+ Fixtures parts onto racks/barrels for processing.
+
+
+
+ Plating Operator
+ plating_op
+ 30
+ fa-flask
+ Runs the plating line — chemistry checks, dwell, thickness.
+
+
+
+ De-Mask
+ demask
+ 40
+ fa-scissors
+ Removes masking material after plating.
+
+
+
+ Oven / Bake
+ oven
+ 50
+ fa-fire
+ Loads and operates embrittlement-relief ovens.
+
+
+
+ De-Rack
+ derack
+ 60
+ fa-cogs
+ Removes parts from racks/barrels for inspection.
+
+
+
+ Inspection / QA
+ inspection
+ 70
+ fa-search
+ Post-plate inspection, Fischerscope, first-piece sign-off.
+
+
+
+ Rework
+ rework
+ 80
+ fa-wrench
+ Strips bad plating; routes parts back for re-processing.
+
+
+
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py b/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py
index 180afc4c..d2015fef 100644
--- a/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py
@@ -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
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/fp_process_node.py b/fusion_plating/fusion_plating_bridge_mrp/models/fp_process_node.py
new file mode 100644
index 00000000..58784235
--- /dev/null
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/fp_process_node.py
@@ -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.',
+ )
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/fp_work_role.py b/fusion_plating/fusion_plating_bridge_mrp/models/fp_work_role.py
new file mode 100644
index 00000000..7d9e32e9
--- /dev/null
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/fp_work_role.py
@@ -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.'),
+ ]
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py b/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py
new file mode 100644
index 00000000..bf5d4acc
--- /dev/null
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py
@@ -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.',
+ )
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
index 8df85933..3a2bf7f0 100644
--- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
@@ -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
diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py
index 3e8a79f4..eefa3152 100644
--- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py
+++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py
@@ -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
diff --git a/fusion_plating/fusion_plating_bridge_mrp/security/ir.model.access.csv b/fusion_plating/fusion_plating_bridge_mrp/security/ir.model.access.csv
index 61911f24..65167f51 100644
--- a/fusion_plating/fusion_plating_bridge_mrp/security/ir.model.access.csv
+++ b/fusion_plating/fusion_plating_bridge_mrp/security/ir.model.access.csv
@@ -15,3 +15,5 @@ access_fp_recipe_config_wizard_line_manager,fp.recipe.config.wizard.line.manager
access_fp_job_consumption_operator,fp.job.consumption.operator,model_fp_job_consumption,fusion_plating.group_fusion_plating_operator,1,1,1,0
access_fp_job_consumption_supervisor,fp.job.consumption.supervisor,model_fp_job_consumption,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_job_consumption_manager,fp.job.consumption.manager,model_fp_job_consumption,fusion_plating.group_fusion_plating_manager,1,1,1,1
+access_fp_work_role_operator,fp.work.role.operator,model_fp_work_role,fusion_plating.group_fusion_plating_operator,1,0,0,0
+access_fp_work_role_manager,fp.work.role.manager,model_fp_work_role,fusion_plating.group_fusion_plating_manager,1,1,1,1
diff --git a/fusion_plating/fusion_plating_bridge_mrp/views/fp_work_role_views.xml b/fusion_plating/fusion_plating_bridge_mrp/views/fp_work_role_views.xml
new file mode 100644
index 00000000..12075356
--- /dev/null
+++ b/fusion_plating/fusion_plating_bridge_mrp/views/fp_work_role_views.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+ fp.work.role.list
+ fp.work.role
+
+
+
+
+
+
+
+
+
+
+
+
+
+ fp.work.role.form
+ fp.work.role
+
+
+
+
+
+
+ Shop Roles
+ fp.work.role
+ list,form
+
+
+ Define the roles on your shop floor
+
+
+ Tag each employee with the roles they can perform and tag each
+ recipe step with the role that performs it. Work orders will
+ auto-route to the right worker when an MO is confirmed.
+