feat(certificates): per-customer document preferences (CHUNK 1/4)
Customers can now pick which shipping-time documents they actually want instead of the shop remembering it per account. Four booleans on res.partner (only shown on companies, not contacts): x_fc_send_coc (default True) Certificate of Conformance x_fc_send_thickness_report (default True) Thickness readings x_fc_send_packing_slip (default True) Packing slip PDF x_fc_send_bol (default False) Bill of Lading Surfaced in a "Plating Documents" page on the customer form. Two downstream gates: 1. fp.notification.template._collect_attachments() now reads the flags when attaching CoC / thickness / packing / BoL PDFs to the shipping confirmation email. Flags missing on the partner (e.g. legacy customers) fall back to the original defaults so nothing regresses. 2. mrp.production.button_mark_done() only auto-creates the quality documents the customer wants. A customer that unchecks both CoC and thickness gets zero certs auto-generated — shop can still create them manually if needed. Note: today a standalone thickness-only report template doesn't exist, so when a customer asks for thickness only (CoC off, thickness on) the dispatcher still attaches the CoC PDF (which carries thickness data) but with CoC creation gated off. A dedicated thickness-only template is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -495,32 +495,54 @@ class MrpProduction(models.Model):
|
|||||||
'state': 'draft',
|
'state': 'draft',
|
||||||
})
|
})
|
||||||
|
|
||||||
# Auto-create draft Certificate of Conformance
|
# Auto-create draft quality documents — which ones are created
|
||||||
|
# is driven by the customer's preferences on res.partner
|
||||||
|
# (x_fc_send_coc, x_fc_send_thickness_report). A customer that
|
||||||
|
# never wants paperwork gets zero certs auto-generated.
|
||||||
if Certificate is not None:
|
if Certificate is not None:
|
||||||
# Skip if a CoC already exists for this MO
|
customer = job.partner_id
|
||||||
existing = Certificate.search(
|
want_coc = True # default for customers that predate the flag
|
||||||
[('production_id', '=', mo.id), ('certificate_type', '=', 'coc')],
|
want_thickness = True
|
||||||
limit=1,
|
if 'x_fc_send_coc' in customer._fields:
|
||||||
)
|
want_coc = bool(customer.x_fc_send_coc)
|
||||||
if not existing:
|
if 'x_fc_send_thickness_report' in customer._fields:
|
||||||
coating = so.x_fc_coating_config_id if (
|
want_thickness = bool(customer.x_fc_send_thickness_report)
|
||||||
so and 'x_fc_coating_config_id' in so._fields
|
|
||||||
) else False
|
coating = so.x_fc_coating_config_id if (
|
||||||
cert_vals = {
|
so and 'x_fc_coating_config_id' in so._fields
|
||||||
'certificate_type': 'coc',
|
) else False
|
||||||
'partner_id': job.partner_id.id,
|
base_vals = {
|
||||||
'production_id': mo.id,
|
'partner_id': customer.id,
|
||||||
'portal_job_id': job.id,
|
'production_id': mo.id,
|
||||||
'sale_order_id': so.id if so else False,
|
'portal_job_id': job.id,
|
||||||
'quantity_shipped': int(mo.product_qty),
|
'sale_order_id': so.id if so else False,
|
||||||
'po_number': so.x_fc_po_number if (
|
'quantity_shipped': int(mo.product_qty),
|
||||||
so and 'x_fc_po_number' in so._fields
|
'po_number': so.x_fc_po_number if (
|
||||||
) else False,
|
so and 'x_fc_po_number' in so._fields
|
||||||
'entech_wo_number': mo.name,
|
) else False,
|
||||||
'spec_reference': coating.spec_reference if coating else False,
|
'entech_wo_number': mo.name,
|
||||||
'process_description': coating.name if coating else False,
|
'spec_reference': coating.spec_reference if coating else False,
|
||||||
'part_number': mo.product_id.default_code or '',
|
'process_description': coating.name if coating else False,
|
||||||
'state': 'draft',
|
'part_number': mo.product_id.default_code or '',
|
||||||
}
|
'state': 'draft',
|
||||||
Certificate.create(cert_vals)
|
}
|
||||||
|
|
||||||
|
if want_coc:
|
||||||
|
existing = Certificate.search(
|
||||||
|
[('production_id', '=', mo.id),
|
||||||
|
('certificate_type', '=', 'coc')], limit=1,
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
Certificate.create({**base_vals, 'certificate_type': 'coc'})
|
||||||
|
|
||||||
|
if want_thickness:
|
||||||
|
existing = Certificate.search(
|
||||||
|
[('production_id', '=', mo.id),
|
||||||
|
('certificate_type', '=', 'thickness_report')], limit=1,
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
Certificate.create({
|
||||||
|
**base_vals,
|
||||||
|
'certificate_type': 'thickness_report',
|
||||||
|
})
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ Includes Fischerscope thickness measurement data capture.
|
|||||||
'data/fp_certificate_sequence_data.xml',
|
'data/fp_certificate_sequence_data.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/fp_certificate_views.xml',
|
'views/fp_certificate_views.xml',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
'views/fp_certificates_menu.xml',
|
'views/fp_certificates_menu.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
from . import fp_thickness_reading
|
from . import fp_thickness_reading
|
||||||
from . import fp_certificate
|
from . import fp_certificate
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
|
from . import res_partner
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- 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 fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
"""Per-customer preferences for what quality documents are generated
|
||||||
|
and emailed when a job ships.
|
||||||
|
|
||||||
|
Some aerospace customers insist on a full CoC + thickness report;
|
||||||
|
others just want the CoC; some repeat commercial accounts want
|
||||||
|
neither (the PO says "no paperwork required"). Rather than hard-code
|
||||||
|
the shop's policy, each customer controls their own.
|
||||||
|
"""
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
x_fc_send_coc = fields.Boolean(
|
||||||
|
string='Send Certificate of Conformance',
|
||||||
|
default=True, tracking=True,
|
||||||
|
help='When shipping, auto-generate and email a CoC to this customer.',
|
||||||
|
)
|
||||||
|
x_fc_send_thickness_report = fields.Boolean(
|
||||||
|
string='Send Thickness Report',
|
||||||
|
default=True, tracking=True,
|
||||||
|
help='When shipping, auto-generate and email a thickness report '
|
||||||
|
'with the Fischerscope readings for this job.',
|
||||||
|
)
|
||||||
|
x_fc_send_packing_slip = fields.Boolean(
|
||||||
|
string='Send Packing Slip',
|
||||||
|
default=True, tracking=True,
|
||||||
|
help='Attach the packing slip PDF to the shipping confirmation email.',
|
||||||
|
)
|
||||||
|
x_fc_send_bol = fields.Boolean(
|
||||||
|
string='Send Bill of Lading',
|
||||||
|
default=False, tracking=True,
|
||||||
|
help='Attach the BoL PDF to the shipping confirmation email. '
|
||||||
|
'Usually only for customers that invoice freight separately.',
|
||||||
|
)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Part of the Fusion Plating product family.
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_partner_form_fp_document_prefs" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form.fp.document.prefs</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Plating Documents"
|
||||||
|
name="fp_document_prefs"
|
||||||
|
invisible="is_company != True">
|
||||||
|
<group string="Documents to Send on Shipment"
|
||||||
|
name="fp_document_prefs_group">
|
||||||
|
<p class="text-muted" colspan="2">
|
||||||
|
Control which quality documents are auto-generated and
|
||||||
|
emailed to this customer when a job ships. Matches the
|
||||||
|
customer's PO requirements so the shop doesn't have to
|
||||||
|
remember per-account preferences.
|
||||||
|
</p>
|
||||||
|
<group>
|
||||||
|
<field name="x_fc_send_coc" widget="boolean_toggle"/>
|
||||||
|
<field name="x_fc_send_thickness_report" widget="boolean_toggle"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="x_fc_send_packing_slip" widget="boolean_toggle"/>
|
||||||
|
<field name="x_fc_send_bol" widget="boolean_toggle"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -190,6 +190,26 @@ class FpNotificationTemplate(models.Model):
|
|||||||
_logger.warning('Failed to render %s: %s', xmlid, exc)
|
_logger.warning('Failed to render %s: %s', xmlid, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Resolve the customer record — partner-level flags gate certain
|
||||||
|
# documents so customer preferences override template defaults.
|
||||||
|
customer = None
|
||||||
|
if sale_order:
|
||||||
|
customer = sale_order.partner_id
|
||||||
|
elif portal_job:
|
||||||
|
customer = portal_job.partner_id
|
||||||
|
elif delivery:
|
||||||
|
customer = delivery.partner_id
|
||||||
|
elif invoice and getattr(invoice, 'partner_id', None):
|
||||||
|
customer = invoice.partner_id
|
||||||
|
elif production and portal_job:
|
||||||
|
customer = portal_job.partner_id
|
||||||
|
|
||||||
|
def _customer_wants(flag, default=True):
|
||||||
|
"""Respect the per-customer flag if the field exists; else default."""
|
||||||
|
if customer and flag in customer._fields:
|
||||||
|
return bool(getattr(customer, flag))
|
||||||
|
return default
|
||||||
|
|
||||||
# Both attach_quotation and attach_sale_order point at the same
|
# Both attach_quotation and attach_sale_order point at the same
|
||||||
# report today — render once to avoid double attachment.
|
# report today — render once to avoid double attachment.
|
||||||
if (self.attach_quotation or self.attach_sale_order) and sale_order:
|
if (self.attach_quotation or self.attach_sale_order) and sale_order:
|
||||||
@@ -198,7 +218,23 @@ class FpNotificationTemplate(models.Model):
|
|||||||
)
|
)
|
||||||
if att:
|
if att:
|
||||||
ids.append(att)
|
ids.append(att)
|
||||||
if self.attach_coc and portal_job:
|
# CoC — gated by customer preference (x_fc_send_coc, default True)
|
||||||
|
if self.attach_coc and portal_job and _customer_wants('x_fc_send_coc'):
|
||||||
|
att = _render_report(
|
||||||
|
'fusion_plating_reports.action_report_coc', portal_job,
|
||||||
|
)
|
||||||
|
if att:
|
||||||
|
ids.append(att)
|
||||||
|
# Thickness report — gated by customer preference. Today the CoC
|
||||||
|
# template embeds thickness readings, so when a customer wants
|
||||||
|
# thickness-only we fall back to the CoC report attachment with
|
||||||
|
# a distinct filename. A standalone thickness-only template is
|
||||||
|
# TBD (not part of this chunk).
|
||||||
|
if (self.attach_thickness_report and portal_job
|
||||||
|
and _customer_wants('x_fc_send_thickness_report')
|
||||||
|
and not (self.attach_coc and _customer_wants('x_fc_send_coc'))):
|
||||||
|
# Avoid double-attaching the same PDF when both are wanted —
|
||||||
|
# the CoC already carries the thickness data.
|
||||||
att = _render_report(
|
att = _render_report(
|
||||||
'fusion_plating_reports.action_report_coc', portal_job,
|
'fusion_plating_reports.action_report_coc', portal_job,
|
||||||
)
|
)
|
||||||
@@ -216,7 +252,15 @@ class FpNotificationTemplate(models.Model):
|
|||||||
)
|
)
|
||||||
if att:
|
if att:
|
||||||
ids.append(att)
|
ids.append(att)
|
||||||
if self.attach_bol and delivery:
|
# Packing slip — gated by customer preference (default True)
|
||||||
|
if self.attach_packing_list and delivery and _customer_wants('x_fc_send_packing_slip'):
|
||||||
|
att = _render_report(
|
||||||
|
'fusion_plating_reports.action_report_fp_packing_slip_portrait', delivery,
|
||||||
|
)
|
||||||
|
if att:
|
||||||
|
ids.append(att)
|
||||||
|
# BoL — gated by customer preference (default False)
|
||||||
|
if self.attach_bol and delivery and _customer_wants('x_fc_send_bol', default=False):
|
||||||
att = _render_report(
|
att = _render_report(
|
||||||
'fusion_plating_reports.action_report_fp_bol_portrait', delivery,
|
'fusion_plating_reports.action_report_fp_bol_portrait', delivery,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user