From 28dd7fdd76e686cd5a3077ffe4df517b12ae271a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 17 Apr 2026 19:54:54 -0400 Subject: [PATCH] feat(certificates): per-customer document preferences (CHUNK 1/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../models/mrp_production.py | 76 ++++++++++++------- .../__manifest__.py | 1 + .../models/__init__.py | 1 + .../models/res_partner.py | 41 ++++++++++ .../views/res_partner_views.xml | 40 ++++++++++ .../models/fp_notification_template.py | 48 +++++++++++- 6 files changed, 178 insertions(+), 29 deletions(-) create mode 100644 fusion_plating/fusion_plating_certificates/models/res_partner.py create mode 100644 fusion_plating/fusion_plating_certificates/views/res_partner_views.xml diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py index 81c5e32f..2d105062 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -495,32 +495,54 @@ class MrpProduction(models.Model): '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: - # Skip if a CoC already exists for this MO - existing = Certificate.search( - [('production_id', '=', mo.id), ('certificate_type', '=', 'coc')], - limit=1, - ) - if not existing: - coating = so.x_fc_coating_config_id if ( - so and 'x_fc_coating_config_id' in so._fields - ) else False - cert_vals = { - 'certificate_type': 'coc', - 'partner_id': job.partner_id.id, - 'production_id': mo.id, - 'portal_job_id': job.id, - 'sale_order_id': so.id if so else False, - 'quantity_shipped': int(mo.product_qty), - 'po_number': so.x_fc_po_number if ( - so and 'x_fc_po_number' in so._fields - ) else False, - 'entech_wo_number': mo.name, - 'spec_reference': coating.spec_reference if coating else False, - 'process_description': coating.name if coating else False, - 'part_number': mo.product_id.default_code or '', - 'state': 'draft', - } - Certificate.create(cert_vals) + customer = job.partner_id + want_coc = True # default for customers that predate the flag + want_thickness = True + if 'x_fc_send_coc' in customer._fields: + want_coc = bool(customer.x_fc_send_coc) + if 'x_fc_send_thickness_report' in customer._fields: + want_thickness = bool(customer.x_fc_send_thickness_report) + + coating = so.x_fc_coating_config_id if ( + so and 'x_fc_coating_config_id' in so._fields + ) else False + base_vals = { + 'partner_id': customer.id, + 'production_id': mo.id, + 'portal_job_id': job.id, + 'sale_order_id': so.id if so else False, + 'quantity_shipped': int(mo.product_qty), + 'po_number': so.x_fc_po_number if ( + so and 'x_fc_po_number' in so._fields + ) else False, + 'entech_wo_number': mo.name, + 'spec_reference': coating.spec_reference if coating else False, + 'process_description': coating.name if coating else False, + 'part_number': mo.product_id.default_code or '', + 'state': 'draft', + } + + 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 diff --git a/fusion_plating/fusion_plating_certificates/__manifest__.py b/fusion_plating/fusion_plating_certificates/__manifest__.py index c0588623..ed33a40f 100644 --- a/fusion_plating/fusion_plating_certificates/__manifest__.py +++ b/fusion_plating/fusion_plating_certificates/__manifest__.py @@ -35,6 +35,7 @@ Includes Fischerscope thickness measurement data capture. 'data/fp_certificate_sequence_data.xml', 'views/res_config_settings_views.xml', 'views/fp_certificate_views.xml', + 'views/res_partner_views.xml', 'views/fp_certificates_menu.xml', ], 'installable': True, diff --git a/fusion_plating/fusion_plating_certificates/models/__init__.py b/fusion_plating/fusion_plating_certificates/models/__init__.py index 7a788c39..9b8ea7b7 100644 --- a/fusion_plating/fusion_plating_certificates/models/__init__.py +++ b/fusion_plating/fusion_plating_certificates/models/__init__.py @@ -6,3 +6,4 @@ from . import fp_thickness_reading from . import fp_certificate from . import res_config_settings +from . import res_partner diff --git a/fusion_plating/fusion_plating_certificates/models/res_partner.py b/fusion_plating/fusion_plating_certificates/models/res_partner.py new file mode 100644 index 00000000..81ea6b52 --- /dev/null +++ b/fusion_plating/fusion_plating_certificates/models/res_partner.py @@ -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.', + ) diff --git a/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml b/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml new file mode 100644 index 00000000..646ff720 --- /dev/null +++ b/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml @@ -0,0 +1,40 @@ + + + + + + res.partner.form.fp.document.prefs + res.partner + + + + + +

+ 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. +

+ + + + + + + + +
+
+
+
+
+ +
diff --git a/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py b/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py index 2808af6a..768f709e 100644 --- a/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py +++ b/fusion_plating/fusion_plating_notifications/models/fp_notification_template.py @@ -190,6 +190,26 @@ class FpNotificationTemplate(models.Model): _logger.warning('Failed to render %s: %s', xmlid, exc) 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 # report today — render once to avoid double attachment. if (self.attach_quotation or self.attach_sale_order) and sale_order: @@ -198,7 +218,23 @@ class FpNotificationTemplate(models.Model): ) if 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( 'fusion_plating_reports.action_report_coc', portal_job, ) @@ -216,7 +252,15 @@ class FpNotificationTemplate(models.Model): ) if 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( 'fusion_plating_reports.action_report_fp_bol_portrait', delivery, )