Files
Odoo-Modules/fusion_plating/fusion_plating_quality/models/fp_quality_hold.py
gsinghpal 2a4909be25 changes
2026-04-27 08:16:20 -04:00

205 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from markupsafe import Markup
from odoo import api, fields, models
class FpQualityHold(models.Model):
"""Quality Hold — parts pulled from production for quality review.
Enables the Steelhead-style "Move Parts Into Quality Management"
workflow. An operator can split a partial quantity off a job and
place it on hold for inspection, rework, or scrap.
"""
_name = 'fusion.plating.quality.hold'
_description = 'Fusion Plating — Quality Hold'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
# ----- What's on hold -----
# Phase 1 (Sub 11) — native plating-job link replaces the legacy
# workorder_id / production_id pair that lived in bridge_mrp.
# The bridge fields stay during the migration window so existing
# records keep their FKs; Phase 5 removes bridge_mrp entirely.
job_id = fields.Many2one(
'fp.job', string='Plating Job',
index=True, ondelete='set null',
)
step_id = fields.Many2one(
'fp.job.step', string='Job Step',
domain="[('job_id', '=', job_id)]",
ondelete='set null',
)
part_ref = fields.Char(string='Part Number')
# ----- Hold details -----
qty_on_hold = fields.Integer(string='Qty on Hold', required=True)
qty_original = fields.Integer(string='Original Qty')
mark_for_scrap = fields.Boolean(string='Mark for Scrap', default=False)
hold_reason = fields.Selection(
[
('damaged', 'Parts Damaged'),
('out_of_spec', 'Out of Specification'),
('contamination', 'Contamination'),
('customer_complaint', 'Customer Complaint'),
('process_deviation', 'Process Deviation'),
# v19.0.4.8.0 — Distinct bucket so QA can split QC-failed
# holds (auto-spawned by fusion.plating.quality.check.action_fail)
# from operator-flagged process deviations / contamination.
('qc_failure', 'QC Failure'),
('other', 'Other'),
],
string='Hold Reason',
required=True,
tracking=True,
help='Required so QA can triage holds by category.',
)
description = fields.Text(
string='Description',
required=True,
help='Required — every hold needs an inspector narrative.',
)
attachment_ids = fields.Many2many(
'ir.attachment',
string='Attachments',
)
# ----- Location / station context -----
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Station',
)
current_process_node = fields.Char(string='Current Process Node')
# ----- Status -----
state = fields.Selection(
[
('on_hold', 'On Hold'),
('under_review', 'Under Review'),
('released', 'Released to Production'),
('scrapped', 'Scrapped'),
('reworked', 'Sent to Rework'),
],
string='Status',
default='on_hold',
required=True,
tracking=True,
)
# ----- Resolution -----
ncr_id = fields.Many2one('fusion.plating.ncr', string='Linked NCR')
resolved_by_id = fields.Many2one('res.users', string='Resolved By')
resolution_date = fields.Datetime(string='Resolution Date')
resolution_notes = fields.Text(string='Resolution Notes')
# ----- Housekeeping -----
operator_id = fields.Many2one(
'res.users',
string='Held By',
default=lambda self: self.env.user,
)
company_id = fields.Many2one(
'res.company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
# ------------------------------------------------------------------
# Defaults / create
# ------------------------------------------------------------------
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code(
'fusion.plating.quality.hold',
)
return seq or '/'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == '/':
vals['name'] = self._default_name()
return super().create(vals_list)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_start_review(self):
self.write({'state': 'under_review'})
self._post_state_message('Under Review')
def action_release(self):
self.write({
'state': 'released',
'resolved_by_id': self.env.user.id,
'resolution_date': fields.Datetime.now(),
})
self._post_state_message('Released to Production')
def action_scrap(self):
self.write({
'state': 'scrapped',
'mark_for_scrap': True,
'resolved_by_id': self.env.user.id,
'resolution_date': fields.Datetime.now(),
})
self._post_state_message('Scrapped')
def action_send_to_rework(self):
self.write({
'state': 'reworked',
'resolved_by_id': self.env.user.id,
'resolution_date': fields.Datetime.now(),
})
self._post_state_message('Sent to Rework')
def action_create_ncr(self):
"""Create a linked NCR from this hold record."""
self.ensure_one()
ncr = self.env['fusion.plating.ncr'].create({
'facility_id': self.facility_id.id,
'source': 'shop_floor',
'severity': 'medium',
'part_ref': self.part_ref,
'quantity_affected': self.qty_on_hold,
'description': self.description or '',
})
self.write({'ncr_id': ncr.id})
self._post_state_message(f'NCR {ncr.name} created')
return {
'name': 'NCR',
'type': 'ir.actions.act_window',
'res_model': 'fusion.plating.ncr',
'res_id': ncr.id,
'view_mode': 'form',
'target': 'current',
}
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _post_state_message(self, label):
for rec in self:
rec.message_post(
body=Markup("Hold status changed to <b>%s</b>.") % label,
message_type='comment',
subtype_xmlid='mail.mt_note',
)