feat(jobs): finish original plan — Job Margin, polish, legacy hide

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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 04:49:44 -04:00
parent f8ad224b1a
commit 7f84e66b72
8 changed files with 245 additions and 1 deletions

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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,
}