# -*- 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 FpCertificate(models.Model): """Unified certificate registry. Logs every quality document issued to customers: CoC, thickness reports, mill test reports, Nadcap certs, and customer-specific formats. Auto-created when reports are generated. """ _name = 'fp.certificate' _description = 'Fusion Plating — Certificate' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'issue_date desc, id desc' name = fields.Char(string='Reference', readonly=True, copy=False, default='New') certificate_type = fields.Selection( [ ('coc', 'Certificate of Conformance'), ('thickness_report', 'Thickness Report'), ('mill_test', 'Mill Test Report'), ('nadcap_cert', 'Nadcap Certificate'), ('customer_specific', 'Customer-Specific'), ], string='Type', required=True, default='coc', tracking=True, ) partner_id = fields.Many2one( 'res.partner', string='Customer', required=True, tracking=True, domain="[('customer_rank', '>', 0)]", ) sale_order_id = fields.Many2one('sale.order', string='Sale Order') production_id = fields.Many2one('mrp.production', string='Manufacturing Order') portal_job_id = fields.Many2one('fusion.plating.portal.job', string='Portal Job') part_number = fields.Char(string='Part Number', help='Denormalized for fast search.') process_description = fields.Char( string='Process', help='e.g. "ELECTROLESS NICKEL PLATING PER AMS 2404"', ) spec_reference = fields.Char(string='Spec Reference') po_number = fields.Char(string='Customer PO #') entech_wo_number = fields.Char(string='Entech WO #') quantity_shipped = fields.Integer(string='Qty Shipped') issued_by_id = fields.Many2one( 'res.users', string='Issued By', default=lambda self: self.env.user, ) certified_by_id = fields.Many2one( 'res.users', string='Certified By', help='Signing authority (e.g. Quality Manager).', ) issue_date = fields.Date(string='Issue Date', default=fields.Date.today, tracking=True) attachment_id = fields.Many2one('ir.attachment', string='Certificate PDF') thickness_reading_ids = fields.One2many( 'fp.thickness.reading', 'certificate_id', string='Thickness Readings', ) state = fields.Selection( [('draft', 'Draft'), ('issued', 'Issued'), ('voided', 'Voided')], string='Status', default='draft', tracking=True, required=True, ) void_reason = fields.Text(string='Void Reason') notes = fields.Html(string='Notes') # ----- Computed stats from readings ------------------------------------- reading_count = fields.Integer( string='Readings', compute='_compute_reading_stats', ) mean_nip_mils = fields.Float( string='Mean NiP (mils)', compute='_compute_reading_stats', digits=(10, 4), ) @api.depends('thickness_reading_ids', 'thickness_reading_ids.nip_mils') def _compute_reading_stats(self): for rec in self: readings = rec.thickness_reading_ids rec.reading_count = len(readings) if readings: nip_values = readings.mapped('nip_mils') rec.mean_nip_mils = sum(nip_values) / len(nip_values) if nip_values else 0 else: rec.mean_nip_mils = 0 # ----- 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.certificate') or 'New' return super().create(vals_list) # ----- State actions ---------------------------------------------------- def action_issue(self): for rec in self: if rec.state != 'draft': raise UserError(_('Only draft certificates can be issued.')) rec.state = 'issued' rec.message_post(body=_('Certificate issued.')) def action_void(self): for rec in self: if rec.state != 'issued': raise UserError(_('Only issued certificates can be voided.')) if not rec.void_reason: raise UserError(_('Please enter a void reason before voiding.')) rec.state = 'voided' rec.message_post(body=_('Certificate voided. Reason: %s') % rec.void_reason) def action_send_to_customer(self): """Open email composer with the certificate PDF attached.""" self.ensure_one() template = self.env.ref('mail.email_compose_message_wizard_form', raise_if_not_found=False) ctx = { 'default_model': 'fp.certificate', 'default_res_ids': self.ids, 'default_composition_mode': 'comment', 'default_partner_ids': [self.partner_id.id] if self.partner_id else [], } if self.attachment_id: ctx['default_attachment_ids'] = [self.attachment_id.id] return { 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', 'view_mode': 'form', 'target': 'new', 'context': ctx, }