# -*- 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 class FpSerial(models.Model): """Serial number registry. One record per "occurrence of a part on an order line". The same part ordered six months later gets a different serial. The serial is the common thread linking the SO line to the MO, Delivery, and Invoice records it spawns downstream. Most serials are customer-supplied (pass-through from the customer's own end-user); a smaller share are shop-generated via the sequence. The registry is optional — SO lines can carry no serial at all. """ _name = 'fp.serial' _description = 'Fusion Plating — Serial Number' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc, id desc' _rec_name = 'name' name = fields.Char( required=True, tracking=True, help='Customer-supplied serial (most common) or shop-generated ' 'sequence value. Typed-in values are accepted as-is.', ) company_id = fields.Many2one( 'res.company', required=True, default=lambda s: s.env.company, ) sale_order_line_id = fields.Many2one( 'sale.order.line', string='Source Sale Order Line', ondelete='set null', copy=False, tracking=True, ) sale_order_id = fields.Many2one( related='sale_order_line_id.order_id', store=True, string='Sale Order', ) customer_id = fields.Many2one( related='sale_order_line_id.order_id.partner_id', store=True, string='Customer', ) part_id = fields.Many2one( related='sale_order_line_id.x_fc_part_catalog_id', store=True, string='Part', ) notes = fields.Text(string='Notes') # Reverse link to invoice lines — safe here because account.move.line # lives in this same module. Production (mrp) and delivery (logistics) # reverse links are defined in their own modules' fp_serial inherits # to keep module load order consistent. invoice_line_ids = fields.One2many( 'account.move.line', 'x_fc_serial_id', string='Invoice Lines', ) invoice_ids = fields.Many2many( 'account.move', compute='_compute_invoice_ids', string='Invoices', ) invoice_count = fields.Integer(compute='_compute_counts') # production_count / delivery_count are declared in the inheriting # modules (bridge_mrp / logistics) so the O2Ms exist alongside them. _sql_constraints = [ ('fp_serial_name_company_uniq', 'unique(company_id, name)', 'Serial number must be unique within the company.'), ] # ---- Computes ------------------------------------------------------------ @api.depends('invoice_line_ids.move_id') def _compute_counts(self): # Base compute sets invoice_count only. bridge_mrp + logistics # override this to also populate production_count / delivery_count. for rec in self: rec.invoice_count = len(rec.invoice_line_ids.mapped('move_id')) @api.depends('invoice_line_ids.move_id') def _compute_invoice_ids(self): for rec in self: rec.invoice_ids = rec.invoice_line_ids.mapped('move_id') # ---- Actions ------------------------------------------------------------- def action_view_sale_order(self): self.ensure_one() if not self.sale_order_id: return False return { 'type': 'ir.actions.act_window', 'res_model': 'sale.order', 'res_id': self.sale_order_id.id, 'view_mode': 'form', } def action_view_productions(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Manufacturing Orders'), 'res_model': 'mrp.production', 'domain': [('id', 'in', self.production_ids.ids)], 'view_mode': 'list,form', } def action_view_deliveries(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Deliveries'), 'res_model': 'fusion.plating.delivery', 'domain': [('id', 'in', self.delivery_ids.ids)], 'view_mode': 'list,form', } def action_view_invoices(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Invoices'), 'res_model': 'account.move', 'domain': [('id', 'in', self.invoice_ids.ids)], 'view_mode': 'list,form', } def action_view_part(self): self.ensure_one() if not self.part_id: return False return { 'type': 'ir.actions.act_window', 'res_model': 'fp.part.catalog', 'res_id': self.part_id.id, 'view_mode': 'form', }