# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. # # Sub 8 — Racking-time inspection record. Captures the per-part # inspection the racking crew performs when they open the customer's # boxes (which is DIFFERENT from receiving — receiving is box count # only). One record per MO. from odoo import _, api, fields, models from odoo.exceptions import UserError class FpRackingInspection(models.Model): _name = 'fp.racking.inspection' _description = 'Racking-time Inspection' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc, id desc' name = fields.Char(compute='_compute_name', store=True) production_id = fields.Many2one( 'mrp.production', string='Manufacturing Order', required=True, ondelete='cascade', index=True, tracking=True, ) sale_order_id = fields.Many2one( 'sale.order', string='Sale Order', compute='_compute_sale_order', store=True, ) partner_id = fields.Many2one( 'res.partner', string='Customer', compute='_compute_sale_order', store=True, ) receiving_id = fields.Many2one( 'fp.receiving', string='Source Receiving', compute='_compute_receiving_id', store=True, help='The receiving record whose boxes feed this inspection.', ) state = fields.Selection( [('draft', 'Draft'), ('inspecting', 'Inspecting'), ('done', 'Done'), ('discrepancy_flagged', 'Discrepancy Flagged')], default='draft', required=True, tracking=True, ) inspector_id = fields.Many2one( 'res.users', string='Inspector', readonly=True, copy=False, tracking=True, ) inspection_started = fields.Datetime(readonly=True, copy=False) inspection_completed = fields.Datetime(readonly=True, copy=False) line_ids = fields.One2many( 'fp.racking.inspection.line', 'inspection_id', copy=True, ) notes = fields.Text() company_id = fields.Many2one( 'res.company', required=True, default=lambda s: s.env.company, ) line_count = fields.Integer(compute='_compute_line_stats') ok_count = fields.Integer(compute='_compute_line_stats') flagged_count = fields.Integer(compute='_compute_line_stats') has_variance = fields.Boolean(compute='_compute_line_stats') _sql_constraints = [ ('fp_racking_insp_mo_uniq', 'unique(production_id)', 'Only one racking inspection per manufacturing order.'), ] # ---- Computes ------------------------------------------------------------ @api.depends('production_id.name', 'partner_id.name') def _compute_name(self): for rec in self: if rec.production_id: rec.name = _('Inspection — %s') % rec.production_id.name else: rec.name = _('Racking Inspection') @api.depends('production_id.origin') def _compute_sale_order(self): SO = self.env['sale.order'] for rec in self: so = False if rec.production_id and rec.production_id.origin: so = SO.search( [('name', '=', rec.production_id.origin)], limit=1, ) rec.sale_order_id = so or False rec.partner_id = so.partner_id if so else False @api.depends('sale_order_id') def _compute_receiving_id(self): Receiving = self.env['fp.receiving'] for rec in self: rec.receiving_id = ( Receiving.search( [('sale_order_id', '=', rec.sale_order_id.id)], limit=1, ) if rec.sale_order_id else False ) @api.depends('line_ids.condition', 'line_ids.qty_variance') def _compute_line_stats(self): for rec in self: rec.line_count = len(rec.line_ids) rec.ok_count = len(rec.line_ids.filtered( lambda l: l.condition == 'ok' and l.qty_variance == 0 )) rec.flagged_count = len(rec.line_ids.filtered( lambda l: l.condition != 'ok' or l.qty_variance != 0 )) rec.has_variance = bool(rec.flagged_count) # ---- Actions ------------------------------------------------------------- def action_start(self): """Racker opens the boxes and begins inspecting.""" for rec in self: if rec.state != 'draft': raise UserError(_('Can only start a Draft inspection.')) rec.write({ 'state': 'inspecting', 'inspector_id': self.env.user.id, 'inspection_started': fields.Datetime.now(), }) rec.message_post(body=_('Inspection started by %s.') % self.env.user.name) def action_complete(self): """Racker is satisfied with the inspection. Advance to Done.""" for rec in self: if rec.state != 'inspecting': raise UserError(_('Can only complete an Inspecting record.')) new_state = 'discrepancy_flagged' if rec.flagged_count else 'done' rec.write({ 'state': new_state, 'inspection_completed': fields.Datetime.now(), }) if new_state == 'discrepancy_flagged': rec.activity_schedule( 'mail.mail_activity_data_todo', summary=_('Racking discrepancy on %s') % ( rec.production_id.name or '' ), note=_( '%(n)d line(s) flagged — review before starting ' 'the first plating WO.' ) % {'n': rec.flagged_count}, ) rec.message_post(body=_( 'Inspection completed — %(ok)d ok / %(flag)d flagged.' ) % {'ok': rec.ok_count, 'flag': rec.flagged_count}) def action_reopen(self): """Manager only — reopen a done inspection.""" if not self.env.user.has_group( 'fusion_plating.group_fusion_plating_manager'): raise UserError(_('Only a Plating Manager can reopen a completed ' 'racking inspection.')) for rec in self: rec.write({ 'state': 'inspecting', 'inspection_completed': False, }) rec.message_post(body=_('Reopened by %s.') % self.env.user.name) class FpRackingInspectionLine(models.Model): _name = 'fp.racking.inspection.line' _description = 'Racking Inspection Line' _order = 'inspection_id, sequence, id' inspection_id = fields.Many2one( 'fp.racking.inspection', required=True, ondelete='cascade', ) sequence = fields.Integer(default=10) part_catalog_id = fields.Many2one( 'fp.part.catalog', string='Part', ) part_number = fields.Char( related='part_catalog_id.part_number', store=True, ) part_revision = fields.Char( related='part_catalog_id.revision', store=True, ) qty_expected = fields.Integer(string='Expected Qty') qty_found = fields.Integer(string='Counted Qty') qty_variance = fields.Integer( compute='_compute_qty_variance', store=True, ) condition = fields.Selection( [('ok', 'OK'), ('minor', 'Minor Issue'), ('major', 'Major Issue'), ('reject', 'Reject')], default='ok', required=True, ) notes = fields.Char(string='Notes') @api.depends('qty_expected', 'qty_found') def _compute_qty_variance(self): for rec in self: rec.qty_variance = (rec.qty_found or 0) - (rec.qty_expected or 0)