diff --git a/fusion-plating/fusion_plating_certificates/models/fp_certificate.py b/fusion-plating/fusion_plating_certificates/models/fp_certificate.py index 8c2367be..6dd7b013 100644 --- a/fusion-plating/fusion_plating_certificates/models/fp_certificate.py +++ b/fusion-plating/fusion_plating_certificates/models/fp_certificate.py @@ -3,4 +3,126 @@ # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -# Placeholder — fp.certificate model will be implemented in a subsequent task. +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, + } diff --git a/fusion-plating/fusion_plating_certificates/views/fp_certificate_views.xml b/fusion-plating/fusion_plating_certificates/views/fp_certificate_views.xml index 85d8c23e..096a3ac3 100644 --- a/fusion-plating/fusion_plating_certificates/views/fp_certificate_views.xml +++ b/fusion-plating/fusion_plating_certificates/views/fp_certificate_views.xml @@ -1,2 +1,196 @@ - + + + + + + + fp.certificate.list + fp.certificate + + + + + + + + + + + + + + + + + + + + + + fp.certificate.form + fp.certificate + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + + fp.certificate.search + fp.certificate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Certificates + fp.certificate + list,form + + +

+ Create your first certificate +

+

+ Certificates of Conformance, thickness reports, and other quality + documents are tracked here. +

+
+
+ +
diff --git a/fusion-plating/fusion_plating_certificates/views/fp_certificates_menu.xml b/fusion-plating/fusion_plating_certificates/views/fp_certificates_menu.xml index 85d8c23e..0aed7576 100644 --- a/fusion-plating/fusion_plating_certificates/views/fp_certificates_menu.xml +++ b/fusion-plating/fusion_plating_certificates/views/fp_certificates_menu.xml @@ -1,2 +1,44 @@ - + + + + + Certificates of Conformance + fp.certificate + list,form + [('certificate_type', '=', 'coc')] + + + + Thickness Reports + fp.certificate + list,form + [('certificate_type', '=', 'thickness_report')] + + + + + + + + + + + +