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)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# 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"?>
|
<?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"?>
|
<?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