Files
Odoo-Modules/fusion_plating/fusion_plating/models/fp_job.py
gsinghpal b45a134aa4 refactor(jobs): address code review feedback on fp.job
- Sequence: add noupdate="1" to fp_job_sequences.xml. Without it,
  every module update resets number_next to 1, corrupting the live
  job-number stream. Matches fp_sequence_data.xml convention.
- action_cancel now raises UserError on an already-cancelled job
  instead of silently rewriting state. Audit-grade traceability
  expects explicit failures.
- Added TODO marker for action_hold / action_resume /
  action_revert_to_confirmed so future authors don't bypass the
  state-machine guards.
- Tests: added cannot_cancel_done (covers the dead-code UserError
  branch) and cannot_cancel_already_cancelled.

Manifest version bumped 19.0.8.2.0 -> 19.0.8.2.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:36:58 -04:00

121 lines
4.3 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# fp.job — native plating job model.
#
# Replaces mrp.production for plating. One record per shop-floor job.
# Header data lives here; per-operation detail on fp.job.step (Task 1.5).
# Recipe template (fusion.plating.process.node) is unchanged — this
# model just instantiates from it via fp.job.step.recipe_node_id.
#
# State machine:
# draft -> confirmed -> in_progress -> done
# | ^
# v |
# cancelled (rework reverts here)
# on_hold can be entered from confirmed or in_progress.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FpJob(models.Model):
_name = 'fp.job'
_description = 'Plating Job'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'priority desc, date_deadline asc, id desc'
_rec_name = 'name'
name = fields.Char(
required=True,
copy=False,
readonly=True,
default=lambda self: _('New'),
index=True,
)
state = fields.Selection(
[
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('in_progress', 'In Progress'),
('on_hold', 'On Hold'),
('done', 'Done'),
('cancelled', 'Cancelled'),
],
default='draft',
required=True,
tracking=True,
index=True,
)
priority = fields.Selection(
[
('low', 'Low'),
('normal', 'Normal'),
('high', 'High'),
('rush', 'Rush'),
],
default='normal',
tracking=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
tracking=True,
)
product_id = fields.Many2one('product.product', string='Reference Product')
qty = fields.Float(string='Quantity', required=True, default=1.0)
qty_done = fields.Float(string='Quantity Completed')
qty_scrapped = fields.Float(string='Quantity Scrapped')
date_deadline = fields.Datetime(string='Deadline', tracking=True)
date_planned_start = fields.Datetime(string='Planned Start')
date_started = fields.Datetime(string='Actual Start', readonly=True)
date_finished = fields.Datetime(string='Actual Finish', readonly=True)
origin = fields.Char(string='Source SO', help='Sale Order name for traceability.')
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
facility_id = fields.Many2one('fusion.plating.facility', string='Facility')
manager_id = fields.Many2one('res.users', string='Plating Manager')
company_id = fields.Many2one(
'res.company',
default=lambda self: self.env.company,
required=True,
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('fp.job') or _('New')
return super().create(vals_list)
# ------------------------------------------------------------------
# State machine — actions
# ------------------------------------------------------------------
# TODO(fp.job state-machine completeness): action_hold, action_resume,
# action_revert_to_confirmed (rework path) — to be added when shopfloor
# / rework workflows are wired up. For now, draft → confirmed and the
# cancel paths are the only enforced transitions; everything else is
# an explicit `state` write by privileged code.
def action_confirm(self):
for job in self:
if job.state != 'draft':
raise UserError(_(
"Job %s is in state '%s' - only draft jobs can be confirmed."
) % (job.name, job.state))
job.state = 'confirmed'
return True
def action_cancel(self):
for job in self:
if job.state == 'done':
raise UserError(_(
"Job %s is done — cannot cancel."
) % job.name)
if job.state == 'cancelled':
raise UserError(_(
"Job %s is already cancelled."
) % job.name)
job.state = 'cancelled'
return True