From 7f84e66b7220f51994d02c69b2bf099fc5358d4a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 25 Apr 2026 04:49:44 -0400 Subject: [PATCH] =?UTF-8?q?feat(jobs):=20finish=20original=20plan=20?= =?UTF-8?q?=E2=80=94=20Job=20Margin,=20polish,=20legacy=20hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three batched changes that close out the original 10-phase migration plan. 1. Phase 5 — Job Margin report bound to fp.job (replaces the mrp.production-bound report_wo_margin). Per-step labour cost table + margin summary using existing fp.job.step.cost_total from Phase 1. 2. Polish: - Real implementations for fp.job.step.button_pause, button_skip, button_cancel (was NotImplementedError stubs). button_pause closes the open timelog and sums duration_actual, mirroring button_finish; button_skip/cancel transition state with UserError guards. - Explicit ondelete= policies on fp.job's cross-module Many2ones (part_catalog/coating restrict, customer_spec/portal/delivery set null) — was implicit set null. - Standard Nexa Systems author/website/maintainer/support block on fusion_plating_jobs manifest, suppressing the install warning. 3. Legacy hide: - New 'Plating Legacy Menus' group (group_fusion_plating_legacy_menus) — nobody in it by default. - Old shopfloor Manager Desk + Plant Overview + Tablet Station menus restricted to that group, hiding them from operators now that the native equivalents under 'Plating Jobs (Native)' exist. (Note: ir.ui.menu uses group_ids in Odoo 19, not the deprecated groups_id alias.) Manifest 19.0.2.4.0 → 19.0.3.0.0. fusion_plating_shopfloor added to depends so the legacy menu xmlid references resolve at install time. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_jobs/__manifest__.py | 12 ++- .../fusion_plating_jobs/models/__init__.py | 4 + .../fusion_plating_jobs/models/fp_job.py | 5 ++ .../fusion_plating_jobs/models/fp_job_step.py | 62 ++++++++++++++++ .../models/report_fp_job_margin.py | 52 +++++++++++++ .../report/report_fp_job_margin.xml | 74 +++++++++++++++++++ .../security/legacy_groups.xml | 12 +++ .../views/legacy_menu_hide.xml | 25 +++++++ 8 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 fusion_plating/fusion_plating_jobs/models/fp_job_step.py create mode 100644 fusion_plating/fusion_plating_jobs/models/report_fp_job_margin.py create mode 100644 fusion_plating/fusion_plating_jobs/report/report_fp_job_margin.xml create mode 100644 fusion_plating/fusion_plating_jobs/security/legacy_groups.xml create mode 100644 fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml 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 + + + + 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 @@ + + + + + Plating Legacy Menus + Internal group to hide legacy MO/WO menus that have been replaced by the native fp.job model. Add a user to this group only if they need to navigate historical mrp.production / mrp.workorder records directly. + + 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 @@ + + + + + + + + + + + + + + +