Files
Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_serial.py
gsinghpal 25c3f6f8d1 feat(plating): Sub 5 — order-line fields (serial, job#, thickness, revision)
Four new fields on every sale.order.line, propagated through to MO,
Delivery, and Invoice for end-to-end traceability:

- fp.serial registry (new model in configurator) with smart-button
  traceability to Sale Order, MO, Delivery, Invoice, Part. M2O on SO
  line; optional; user types a customer serial or clicks Generate
  Serial for a sequence-backed one. Reverse O2M links split across
  configurator (invoice) / bridge_mrp (MO) / logistics (delivery) so
  module load order is respected.
- x_fc_job_number on SO line, auto-sequenced FP-JOB-NNNNN on SO
  confirm. Editable — shops can override for customer/legacy schemes.
- fp.coating.thickness (new child of fp.coating.config) with per-
  config discrete thickness options; x_fc_thickness_id on SO line
  domain-filtered to the line's coating. Auto-clears when coating
  changes.
- x_fc_revision_snapshot Char on SO line, frozen from
  x_fc_part_catalog_id.revision at save. Protects historical SOs from
  later catalog edits. Secondary "Revision" picker on the tree view
  lets users switch between prior revisions of the same part number;
  the Part M2O still surfaces only is_latest_revision rows.

Reports (CoC, packing slip, invoice, BoL) pick up all four via the
Sub 2 customer_line_header macro — one macro edit, four reports.

Smoke on entech: 11 assertions pass including revision snapshot,
generate-serial button, typed-serial create-on-fly, coating→thickness
domain reset, SO confirm auto job#, and MO traceability carry.

Module version bumps:
  fusion_plating_configurator  → 19.0.12.0.0
  fusion_plating_bridge_mrp    → 19.0.11.0.0
  fusion_plating_logistics     → 19.0.2.0.0 (+depends configurator)
  fusion_plating_reports       → 19.0.5.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:04:44 -04:00

152 lines
4.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 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',
}