feat(certificates): fp.certificate model + views + menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,2 +1,196 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo></odoo>
|
||||
<odoo>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LIST VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="fp_certificate_view_list" model="ir.ui.view">
|
||||
<field name="name">fp.certificate.list</field>
|
||||
<field name="model">fp.certificate</field>
|
||||
<field name="arch" type="xml">
|
||||
<list default_order="issue_date desc"
|
||||
decoration-muted="state == 'draft'"
|
||||
decoration-success="state == 'issued'"
|
||||
decoration-danger="state == 'voided'">
|
||||
<field name="issue_date"/>
|
||||
<field name="name"/>
|
||||
<field name="certificate_type" widget="badge"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="part_number"/>
|
||||
<field name="po_number"/>
|
||||
<field name="entech_wo_number"/>
|
||||
<field name="process_description"/>
|
||||
<field name="quantity_shipped"/>
|
||||
<field name="issued_by_id"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-success="state == 'issued'"
|
||||
decoration-danger="state == 'voided'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- FORM VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="fp_certificate_view_form" model="ir.ui.view">
|
||||
<field name="name">fp.certificate.form</field>
|
||||
<field name="model">fp.certificate</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_issue" string="Issue"
|
||||
type="object" class="btn-primary"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_void" string="Void"
|
||||
type="object" class="btn-danger"
|
||||
invisible="state != 'issued'"/>
|
||||
<button name="action_send_to_customer" string="Send to Customer"
|
||||
type="object"
|
||||
invisible="state != 'issued'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,issued"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="certificate_type"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="production_id"/>
|
||||
<field name="portal_job_id"/>
|
||||
<field name="issue_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="part_number"/>
|
||||
<field name="po_number"/>
|
||||
<field name="entech_wo_number"/>
|
||||
<field name="process_description"/>
|
||||
<field name="spec_reference"/>
|
||||
<field name="quantity_shipped"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="issued_by_id"/>
|
||||
<field name="certified_by_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="mean_nip_mils" readonly="1"/>
|
||||
<field name="reading_count" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Thickness Readings" name="readings">
|
||||
<field name="thickness_reading_ids">
|
||||
<list editable="bottom">
|
||||
<field name="reading_number"/>
|
||||
<field name="nip_mils"/>
|
||||
<field name="ni_percent"/>
|
||||
<field name="p_percent"/>
|
||||
<field name="position_label"/>
|
||||
<field name="equipment_model"/>
|
||||
<field name="operator_id"/>
|
||||
<field name="reading_datetime"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Certificate PDF" name="pdf">
|
||||
<group>
|
||||
<field name="attachment_id"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Void" name="void"
|
||||
invisible="state != 'voided'">
|
||||
<group>
|
||||
<field name="void_reason"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter>
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</chatter>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SEARCH VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="fp_certificate_view_search" model="ir.ui.view">
|
||||
<field name="name">fp.certificate.search</field>
|
||||
<field name="model">fp.certificate</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="part_number"/>
|
||||
<field name="po_number"/>
|
||||
<field name="entech_wo_number"/>
|
||||
<separator/>
|
||||
<filter name="filter_coc" string="CoC"
|
||||
domain="[('certificate_type', '=', 'coc')]"/>
|
||||
<filter name="filter_thickness" string="Thickness Report"
|
||||
domain="[('certificate_type', '=', 'thickness_report')]"/>
|
||||
<filter name="filter_mill_test" string="Mill Test"
|
||||
domain="[('certificate_type', '=', 'mill_test')]"/>
|
||||
<filter name="filter_nadcap" string="Nadcap"
|
||||
domain="[('certificate_type', '=', 'nadcap_cert')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_draft" string="Draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="filter_issued" string="Issued"
|
||||
domain="[('state', '=', 'issued')]"/>
|
||||
<filter name="filter_voided" string="Voided"
|
||||
domain="[('state', '=', 'voided')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_this_week" string="This Week"
|
||||
domain="[('issue_date', '>=', (context_today() - datetime.timedelta(weeks=1)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter name="filter_this_month" string="This Month"
|
||||
domain="[('issue_date', '>=', time.strftime('%Y-%m-01'))]"/>
|
||||
<separator/>
|
||||
<group expand="1" string="Group By">
|
||||
<filter name="group_customer" string="Customer"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter name="group_type" string="Type"
|
||||
context="{'group_by': 'certificate_type'}"/>
|
||||
<filter name="group_issued_by" string="Issued By"
|
||||
context="{'group_by': 'issued_by_id'}"/>
|
||||
<filter name="group_month" string="Month"
|
||||
context="{'group_by': 'issue_date:month'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- WINDOW ACTION -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="action_fp_certificate" model="ir.actions.act_window">
|
||||
<field name="name">Certificates</field>
|
||||
<field name="res_model">fp.certificate</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="fp_certificate_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first certificate
|
||||
</p>
|
||||
<p>
|
||||
Certificates of Conformance, thickness reports, and other quality
|
||||
documents are tracked here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,2 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo></odoo>
|
||||
<odoo>
|
||||
|
||||
<!-- Actions BEFORE menus -->
|
||||
<record id="action_fp_certificate_coc" model="ir.actions.act_window">
|
||||
<field name="name">Certificates of Conformance</field>
|
||||
<field name="res_model">fp.certificate</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('certificate_type', '=', 'coc')]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_certificate_thickness" model="ir.actions.act_window">
|
||||
<field name="name">Thickness Reports</field>
|
||||
<field name="res_model">fp.certificate</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('certificate_type', '=', 'thickness_report')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu under Fusion Plating root -->
|
||||
<menuitem id="menu_fp_certificates"
|
||||
name="Certificates"
|
||||
parent="fusion_plating.menu_fp_root"
|
||||
sequence="15"
|
||||
groups="fusion_plating.group_fusion_plating_supervisor"/>
|
||||
|
||||
<menuitem id="menu_fp_certificates_all"
|
||||
name="All Certificates"
|
||||
parent="menu_fp_certificates"
|
||||
action="action_fp_certificate"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_certificates_coc"
|
||||
name="Certificates of Conformance"
|
||||
parent="menu_fp_certificates"
|
||||
action="action_fp_certificate_coc"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_certificates_thickness"
|
||||
name="Thickness Reports"
|
||||
parent="menu_fp_certificates"
|
||||
action="action_fp_certificate_thickness"
|
||||
sequence="30"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user