diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py
index a3a42796..285ade58 100644
--- a/fusion_plating/fusion_plating_jobs/__manifest__.py
+++ b/fusion_plating/fusion_plating_jobs/__manifest__.py
@@ -3,9 +3,15 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
- 'version': '19.0.2.4.0',
+ 'version': '19.0.3.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
+ 'author': 'Nexa Systems Inc.',
+ 'website': 'https://www.nexasystems.ca',
+ 'maintainer': 'Nexa Systems Inc.',
+ 'support': 'support@nexasystems.ca',
+ 'price': 0.00,
+ 'currency': 'CAD',
'description': """
Native Plating Job Bridge
=========================
@@ -34,16 +40,20 @@ full design rationale and §6.2 of the implementation plan for task list.
'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold
'fusion_plating_receiving', # fp.racking.inspection (Phase 3)
'fusion_plating_reports', # paperformat helpers, customer_line_header (Phase 5)
+ 'fusion_plating_shopfloor', # legacy menus restricted in views/legacy_menu_hide.xml
],
'data': [
+ 'security/legacy_groups.xml',
'security/ir.model.access.csv',
'views/res_config_settings_views.xml',
'views/job_process_tree_action.xml',
'views/job_overview_actions.xml',
'views/job_tablet_action.xml',
'views/fp_job_form_inherit.xml',
+ 'views/legacy_menu_hide.xml',
'report/report_fp_job_sticker.xml',
'report/report_fp_job_traveller.xml',
+ 'report/report_fp_job_margin.xml',
],
'assets': {
'web.assets_backend': [
diff --git a/fusion_plating/fusion_plating_jobs/models/__init__.py b/fusion_plating/fusion_plating_jobs/models/__init__.py
index 7cc8f7fc..7801e11e 100644
--- a/fusion_plating/fusion_plating_jobs/models/__init__.py
+++ b/fusion_plating/fusion_plating_jobs/models/__init__.py
@@ -6,6 +6,7 @@
# task-by-task in Tasks 2.2 onwards.
from . import fp_job
+from . import fp_job_step
from . import fp_job_node_override
from . import fp_portal_job
from . import account_move
@@ -23,3 +24,6 @@ from . import fp_racking_inspection
# Phase 4 — light refactors batch B (notifications, KPI source tag).
from . import fp_notification_trigger
from . import fusion_plating_kpi_value
+
+# Phase 5 — Job Margin report.
+from . import report_fp_job_margin
diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py
index d8087c2e..5e054f51 100644
--- a/fusion_plating/fusion_plating_jobs/models/fp_job.py
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py
@@ -26,22 +26,27 @@ class FpJob(models.Model):
part_catalog_id = fields.Many2one(
'fp.part.catalog',
string='Part',
+ ondelete='restrict',
)
coating_config_id = fields.Many2one(
'fp.coating.config',
string='Coating Configuration',
+ ondelete='restrict',
)
customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Customer Spec',
+ ondelete='set null',
)
portal_job_id = fields.Many2one(
'fusion.plating.portal.job',
string='Portal Job',
+ ondelete='set null',
)
delivery_id = fields.Many2one(
'fusion.plating.delivery',
string='Delivery',
+ ondelete='set null',
)
override_ids = fields.One2many(
'fp.job.node.override',
diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py
new file mode 100644
index 00000000..aa3ecd26
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+#
+# Real implementations for the state-machine action stubs that
+# fusion_plating core's fp.job.step shipped as NotImplementedError
+# placeholders. Per spec §5.2 state machine.
+
+from odoo import _, fields, models
+from odoo.exceptions import UserError
+
+
+class FpJobStep(models.Model):
+ _inherit = 'fp.job.step'
+
+ def button_pause(self):
+ """Pause an in-progress step (operator break, end of shift).
+
+ Closes the open timelog row, sums duration_actual, transitions
+ state to 'paused'. button_start re-opens a fresh timelog when
+ the operator resumes.
+ """
+ for step in self:
+ if step.state != 'in_progress':
+ raise UserError(_(
+ "Step '%s' is in state '%s' — only in-progress steps can pause."
+ ) % (step.name, step.state))
+ now = fields.Datetime.now()
+ open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
+ if open_log:
+ open_log.write({'date_finished': now})
+ step.state = 'paused'
+ step.duration_actual = sum(step.time_log_ids.mapped('duration_minutes'))
+ return True
+
+ def button_skip(self):
+ """Skip a pending/ready step (e.g. opt-in step the planner
+ decided not to activate for this job).
+ """
+ for step in self:
+ if step.state not in ('pending', 'ready'):
+ raise UserError(_(
+ "Step '%s' is in state '%s' — only pending/ready steps can be skipped."
+ ) % (step.name, step.state))
+ step.state = 'skipped'
+ return True
+
+ def button_cancel(self):
+ """Cancel a single step. Use fp.job.action_cancel to cancel
+ the whole job.
+ """
+ for step in self:
+ if step.state == 'done':
+ raise UserError(_(
+ "Step '%s' is done — cannot cancel."
+ ) % step.name)
+ if step.state == 'cancelled':
+ raise UserError(_(
+ "Step '%s' is already cancelled."
+ ) % step.name)
+ step.state = 'cancelled'
+ return True
diff --git a/fusion_plating/fusion_plating_jobs/models/report_fp_job_margin.py b/fusion_plating/fusion_plating_jobs/models/report_fp_job_margin.py
new file mode 100644
index 00000000..a4c1abee
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/models/report_fp_job_margin.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+#
+# Native fp.job margin report — replaces report_wo_margin which binds
+# to mrp.production. Uses fp.job.step.cost_total (already computed in
+# Phase 1: duration_actual / 60 * cost_per_hour).
+
+from odoo import api, models
+
+
+class ReportFpJobMargin(models.AbstractModel):
+ _name = 'report.fusion_plating_jobs.report_fp_job_margin'
+ _description = 'Plating Job Margin Report'
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ Job = self.env['fp.job']
+ jobs = Job.browse(docids)
+ rows = []
+ for job in jobs:
+ step_rows = []
+ total_labour = 0.0
+ total_minutes = 0.0
+ for step in job.step_ids.sorted('sequence'):
+ step_rows.append({
+ 'sequence': step.sequence,
+ 'name': step.name,
+ 'work_centre': step.work_centre_id.name if step.work_centre_id else '-',
+ 'duration_expected': step.duration_expected,
+ 'duration_actual': step.duration_actual,
+ 'rate': step.cost_per_hour,
+ 'cost': step.cost_total,
+ })
+ total_labour += step.cost_total
+ total_minutes += step.duration_actual
+ rows.append({
+ 'job': job,
+ 'steps': step_rows,
+ 'total_minutes': total_minutes,
+ 'total_labour': total_labour,
+ 'quoted_revenue': job.quoted_revenue,
+ 'actual_cost': job.actual_cost,
+ 'margin': job.margin,
+ 'margin_pct': job.margin_pct,
+ })
+ return {
+ 'doc_ids': docids,
+ 'doc_model': 'fp.job',
+ 'docs': jobs,
+ 'rows': rows,
+ }
diff --git a/fusion_plating/fusion_plating_jobs/report/report_fp_job_margin.xml b/fusion_plating/fusion_plating_jobs/report/report_fp_job_margin.xml
new file mode 100644
index 00000000..4114a195
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/report/report_fp_job_margin.xml
@@ -0,0 +1,74 @@
+
+
+
+ Job Margin Report
+ fp.job
+ qweb-pdf
+ fusion_plating_jobs.report_fp_job_margin_template
+ fusion_plating_jobs.report_fp_job_margin_template
+ 'Job Margin - %s' % (object.name or '').replace('/', '-')
+
+ report
+
+
+
+
+
+
+
+
Job Margin —
+
+ | Customer | |
+ | Recipe | |
+ | Quantity | |
+ | Status | |
+
+
+
Step Breakdown
+
+
+
+ | # |
+ Step |
+ Work Centre |
+ Expected (min) |
+ Actual (min) |
+ Rate / hr |
+ Cost |
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+ | Totals |
+ |
+ |
+ |
+ |
+
+
+
+
+
Margin Summary
+
+ | Quoted Revenue | |
+ | Actual Cost | |
+ | Margin | |
+ | Margin % | % |
+
+
+
+
+
+
+
diff --git a/fusion_plating/fusion_plating_jobs/security/legacy_groups.xml b/fusion_plating/fusion_plating_jobs/security/legacy_groups.xml
new file mode 100644
index 00000000..4c14fd59
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/security/legacy_groups.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml b/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml
new file mode 100644
index 00000000..0505f756
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+