# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. from odoo import api, fields, models, _ from odoo.exceptions import UserError class FpReceiving(models.Model): """Parts receiving record. Created automatically when a sale order is confirmed. Tracks quantity verification, condition inspection, and damage logging for customer parts arriving at the shop. """ _name = 'fp.receiving' _description = 'Fusion Plating — Receiving' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'received_date desc, id desc' name = fields.Char(string='Reference', readonly=True, copy=False, default='New') sale_order_id = fields.Many2one( 'sale.order', string='Sale Order', required=True, ondelete='restrict', tracking=True, ) partner_id = fields.Many2one( 'res.partner', string='Customer', related='sale_order_id.partner_id', store=True, readonly=True, ) po_number = fields.Char( string='Customer PO #', related='sale_order_id.x_fc_po_number', store=True, readonly=True, ) received_by_id = fields.Many2one( 'res.users', string='Received By', default=lambda self: self.env.user, tracking=True, ) received_date = fields.Datetime( string='Received Date', default=fields.Datetime.now, tracking=True, ) state = fields.Selection( [ ('draft', 'Awaiting Parts'), ('inspecting', 'Inspecting'), ('accepted', 'Accepted'), ('discrepancy', 'Discrepancy'), ('resolved', 'Resolved'), ], string='Status', default='draft', tracking=True, required=True, ) expected_qty = fields.Integer(string='Expected Qty', help='Total quantity expected from the sale order.') received_qty = fields.Integer(string='Received Qty', help='Total quantity actually received.') qty_match = fields.Boolean( string='Qty Match', compute='_compute_qty_match', store=True, ) carrier_name = fields.Char(string='Carrier', help='Who delivered the parts (Purolator, customer drop-off, etc.).') carrier_tracking = fields.Char(string='Inbound Tracking #') notes = fields.Html(string='Notes') line_ids = fields.One2many('fp.receiving.line', 'receiving_id', string='Receiving Lines') damage_ids = fields.One2many('fp.receiving.damage', 'receiving_id', string='Damage Log') damage_count = fields.Integer(string='Damage Count', compute='_compute_damage_count') unresolved_damage_count = fields.Integer( string='Unresolved Damage', compute='_compute_damage_count', ) attachment_ids = fields.Many2many( 'ir.attachment', 'fp_receiving_attachment_rel', 'receiving_id', 'attachment_id', string='Photos / Documents', ) @api.depends('expected_qty', 'received_qty') def _compute_qty_match(self): for rec in self: rec.qty_match = rec.expected_qty > 0 and rec.received_qty == rec.expected_qty @api.depends('damage_ids', 'damage_ids.resolved') def _compute_damage_count(self): for rec in self: rec.damage_count = len(rec.damage_ids) rec.unresolved_damage_count = len(rec.damage_ids.filtered(lambda d: not d.resolved)) # ------------------------------------------------------------------------- # Sequence # ------------------------------------------------------------------------- @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.receiving') or 'New' return super().create(vals_list) # ------------------------------------------------------------------------- # State actions # ------------------------------------------------------------------------- def action_start_inspection(self): """Move from draft to inspecting.""" for rec in self: if rec.state != 'draft': raise UserError(_('Only draft records can start inspection.')) rec.state = 'inspecting' rec.received_by_id = self.env.user rec.received_date = fields.Datetime.now() def action_accept(self): """Accept the receiving — parts match and condition is OK.""" for rec in self: if rec.state not in ('inspecting', 'resolved'): raise UserError(_('Can only accept from Inspecting or Resolved state.')) if rec.unresolved_damage_count > 0: raise UserError(_('Cannot accept — there are %d unresolved damage entries.') % rec.unresolved_damage_count) rec.state = 'accepted' rec._update_so_receiving_status() rec.message_post(body=_('Parts accepted — quantity: %d, all checks passed.') % rec.received_qty) def action_flag_discrepancy(self): """Flag a discrepancy — qty mismatch or damage found.""" for rec in self: if rec.state != 'inspecting': raise UserError(_('Can only flag discrepancy from Inspecting state.')) rec.state = 'discrepancy' rec._update_so_receiving_status() # Create follow-up activity for the sales team rec.activity_schedule( 'mail.mail_activity_data_todo', summary=_('Receiving discrepancy — %s') % rec.name, note=_('Qty expected: %d, received: %d. Check damage log for details.') % ( rec.expected_qty, rec.received_qty), ) rec.message_post(body=_('Discrepancy flagged — follow-up required.')) def action_resolve(self): """Resolve a discrepancy after customer follow-up.""" for rec in self: if rec.state != 'discrepancy': raise UserError(_('Can only resolve from Discrepancy state.')) rec.state = 'resolved' rec._update_so_receiving_status() rec.message_post(body=_('Discrepancy resolved.')) def _update_so_receiving_status(self): """Update the linked sale order's receiving status.""" for rec in self: if rec.sale_order_id: if rec.state in ('accepted', 'resolved'): rec.sale_order_id.x_fc_receiving_status = 'received' elif rec.state == 'discrepancy': rec.sale_order_id.x_fc_receiving_status = 'partial' elif rec.state == 'inspecting': rec.sale_order_id.x_fc_receiving_status = 'partial'