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>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
# -*- 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
|