refactor(reports): consolidate SO Acknowledgement back into the Sales Order PDF

Earlier I built report_fp_so_acknowledgement.xml as a separate
customer-facing document. On review there was no good reason — our
existing report_fp_sale.xml already flips its title between
"Quotation" and "Sales Order" based on state, and carried ~90% of
the same content. Two documents would have meant the shop had to
remember which to send when, and the customer would get two
near-identical PDFs in their inbox.

Consolidation:

1. Merged the four unique blocks from the acknowledgement into
   report_fp_sale.xml (both portrait AND landscape variants):
   - CUSTOMER JOB # / PLANNED START / CUSTOMER DEADLINE / SHIP VIA
     info row (shown only when any of those fields is populated)
   - Blanket / block-partial highlight-box callout (shown only
     when the flags are set)
   - External notes (x_fc_external_note) block above Terms and
     Conditions

2. Deleted fusion_plating_reports/report/report_fp_so_acknowledgement.xml
   and removed it from the module manifest. Also purged the orphan
   ir.actions.report and ir.ui.view DB rows + the stale
   ir.model.data entries.

3. Re-pointed the fp_mail_template_so_confirmed mail template's
   report_template_ids from the now-gone acknowledgement report to
   action_report_fp_sale_portrait. Updated hooks.py accordingly; the
   hook now uses "set" semantics (replace all) instead of "add" so
   re-running it cleans up stale attachments from prior refactors.

4. UAT on S00071: the Send button pre-selects the FP: Order
   Confirmation template with SalesOrder_S00071.pdf attached. The
   PDF renders with the new plating rows populated — Customer Job #
   AMPH-2026-0420-01, Customer Deadline 05/14/2026 08:00:00 PM,
   "Partial shipments blocked" callout, all lines + totals.

One PDF, one Send button behaviour, matching what Odoo and most
ERP systems do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-20 01:30:06 -04:00
parent 54e56ed0e6
commit f09bef9083
5 changed files with 102 additions and 280 deletions

View File

@@ -122,8 +122,8 @@
</div>
</field>
<field name="report_template_ids"
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_so_acknowledgement')])]"/>
<field name="report_name">Acknowledgement_{{ (object.name or '').replace('/','_') }}</field>
eval="[(6, 0, [ref('fusion_plating_reports.action_report_fp_sale_portrait')])]"/>
<field name="report_name">SalesOrder_{{ (object.name or '').replace('/','_') }}</field>
</record>
<!-- ============================================================= -->

View File

@@ -26,7 +26,7 @@ def post_init_hook(env):
_apply_report_template(
env,
'fusion_plating_notifications.fp_mail_template_so_confirmed',
'fusion_plating_reports.action_report_fp_so_acknowledgement',
'fusion_plating_reports.action_report_fp_sale_portrait',
)
_apply_report_template(
env,
@@ -36,6 +36,13 @@ def post_init_hook(env):
def _apply_report_template(env, mail_template_xmlid, report_xmlid):
"""Replace the template's report_template_ids with exactly [report].
We use `set` semantics (replace all) rather than `add` so that old
attachments from previous refactors get cleaned up — e.g. when the
Acknowledgement report was consolidated into the Sales Order report,
the now-stale Acknowledgement reference gets removed here.
"""
mail_template = env.ref(mail_template_xmlid, raise_if_not_found=False)
report = env.ref(report_xmlid, raise_if_not_found=False)
if not mail_template or not report:
@@ -44,11 +51,12 @@ def _apply_report_template(env, mail_template_xmlid, report_xmlid):
mail_template_xmlid, report_xmlid,
)
return
if report.id not in mail_template.report_template_ids.ids:
current_ids = set(mail_template.report_template_ids.ids)
if current_ids != {report.id}:
mail_template.write({
'report_template_ids': [(4, report.id)],
'report_template_ids': [(6, 0, [report.id])],
})
_logger.info(
'fusion_plating_notifications: attached report %s to template %s',
'fusion_plating_notifications: set report %s on template %s',
report_xmlid, mail_template_xmlid,
)

View File

@@ -40,7 +40,6 @@
'report/report_wo_margin.xml',
# Quote-to-cash reports (portrait + landscape)
'report/report_fp_sale.xml',
'report/report_fp_so_acknowledgement.xml',
'report/report_fp_work_order.xml',
'report/report_fp_job_traveller.xml',
'report/report_fp_packing_slip.xml',

View File

@@ -97,6 +97,42 @@
</table>
</t>
<!-- Scheduling + customer job reference -->
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
<th class="info-header" style="width: 25%;">PLANNED START</th>
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
<th class="info-header" style="width: 25%;">SHIP VIA</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
<td class="text-center"><span t-field="doc.commitment_date"/></td>
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Blanket / block-partial callout (confirmed-order shipping flags) -->
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
<div class="highlight-box">
<t t-if="doc.x_fc_is_blanket_order">
<strong>Blanket Order.</strong>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines -->
<table class="bordered">
<thead>
@@ -189,6 +225,14 @@
</div>
</div>
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">
@@ -327,6 +371,42 @@
</table>
</t>
<!-- Scheduling + customer job reference -->
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
<table class="bordered info-table">
<thead>
<tr>
<th>CUSTOMER JOB #</th>
<th>PLANNED START</th>
<th>CUSTOMER DEADLINE</th>
<th>SHIP VIA</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
<td class="text-center"><span t-field="doc.commitment_date"/></td>
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Blanket / block-partial callout -->
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
<div class="highlight-box">
<t t-if="doc.x_fc_is_blanket_order">
<strong>Blanket Order.</strong>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines — hide discount column unless at least one line has a discount -->
<t t-set="has_discount" t-value="any(l.discount for l in doc.order_line)"/>
<t t-set="col_count" t-value="8 if has_discount else 7"/>
@@ -426,6 +506,14 @@
</div>
</div>
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">

View File

@@ -1,273 +0,0 @@
<?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.
Sales Order Acknowledgement (Phase D7) — customer-facing
confirmation sent shortly after action_confirm. Styled to match
the rest of the Fusion Plating report family (portrait; bordered
tables; company primary-colour header; totals-table footer;
sig-box signature pair).
-->
<odoo>
<record id="action_report_fp_so_acknowledgement" model="ir.actions.report">
<field name="name">Sales Order Acknowledgement</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_so_acknowledgement_doc</field>
<field name="report_file">fusion_plating_reports.report_fp_so_acknowledgement_doc</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Acknowledgement - %s' % object.name</field>
</record>
<template id="report_fp_so_acknowledgement_doc">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<!-- Title -->
<h4>
<span>Sales Order Acknowledgement </span>
<span t-field="doc.name"/>
</h4>
<!-- Billing / Shipping -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">SHIPPING ADDRESS</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 70px;">
<div t-field="doc.partner_invoice_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px;">
<div t-field="doc.partner_shipping_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- References -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">CUSTOMER PO #</th>
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
<th class="info-header" style="width: 25%;">ORDER DATE</th>
<th class="info-header" style="width: 25%;">SALESPERSON</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<span t-esc="doc.x_fc_po_number or '-'"/>
</td>
<td class="text-center">
<span t-esc="doc.x_fc_customer_job_number or '-'"/>
</td>
<td class="text-center">
<span t-field="doc.date_order"
t-options="{'widget': 'date'}"/>
</td>
<td class="text-center">
<span t-field="doc.user_id"/>
</td>
</tr>
</tbody>
</table>
<!-- Scheduling -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">PLANNED START</th>
<th class="info-header" style="width: 25%;">INTERNAL DEADLINE</th>
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
<th class="info-header" style="width: 25%;">SHIP VIA</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<span t-field="doc.x_fc_planned_start_date"/>
</td>
<td class="text-center">
<span t-field="doc.x_fc_internal_deadline"/>
</td>
<td class="text-center">
<span t-field="doc.commitment_date"/>
</td>
<td class="text-center">
<span t-esc="doc.x_fc_ship_via or '-'"/>
</td>
</tr>
</tbody>
</table>
<!-- Blanket / Block Partial callout -->
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
<div class="highlight-box">
<t t-if="doc.x_fc_is_blanket_order">
<strong>Blanket Order.</strong>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines -->
<table class="bordered">
<thead>
<tr>
<th style="width: 14%;">PART</th>
<th class="text-start" style="width: 36%;">DESCRIPTION</th>
<th style="width: 18%;">TREATMENT</th>
<th style="width: 8%;">QTY</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 12%;">SUBTOTAL</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.order_line.filtered(lambda l: not l.x_fc_archived and (not l.display_type or l.display_type in ('line_section', 'line_note', 'product')))"
t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr class="section-row">
<td colspan="6"><strong t-field="line.name"/></td>
</tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row">
<td colspan="6"><span t-field="line.name"/></td>
</tr>
</t>
<t t-else="">
<tr>
<td class="text-center">
<span t-esc="line.x_fc_part_catalog_id.part_number or '-'"/>
</td>
<td>
<t t-set="clean_name" t-value="line.name"/>
<t t-if="line.name and '] ' in line.name">
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
</t>
<span t-esc="clean_name"/>
</td>
<td class="text-center">
<span t-field="line.x_fc_coating_config_id"/>
</td>
<td class="text-center">
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
</td>
<td class="text-end">
<span t-field="line.price_unit"
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal"
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<t t-if="doc.x_fc_invoice_strategy">
<strong>Invoice Strategy: </strong>
<t t-set="inv_strat" t-value="dict(doc._fields['x_fc_invoice_strategy'].selection).get(doc.x_fc_invoice_strategy, '-')"/>
<span t-esc="inv_strat"/>
<t t-if="doc.x_fc_invoice_strategy == 'deposit' and doc.x_fc_deposit_percent">
(<span t-esc="doc.x_fc_deposit_percent"/>%)
</t>
<br/>
</t>
<t t-if="doc.payment_term_id.note">
<strong>Payment Terms:</strong><br/>
<span t-field="doc.payment_term_id.note"/>
</t>
</div>
<div class="col-6" style="text-align: right;">
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 150px;">Subtotal</td>
<td class="text-end" style="min-width: 110px;">
<span t-field="doc.amount_untaxed"
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td class="text-end">
<span t-field="doc.amount_tax"
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr style="background-color: #eaf2f8;">
<td><strong>Grand Total</strong></td>
<td class="text-end"><strong>
<span t-field="doc.amount_total"
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
</table>
</div>
</div>
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
<!-- Signature block -->
<div class="row" style="margin-top: 25px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Customer Acceptance (Signature / Date)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<t t-if="doc.signature">
<img t-att-src="image_data_uri(doc.signature)"
style="max-height: 3cm; max-width: 8cm;"/><br/>
<span t-field="doc.signed_by"/>
</t>
<t t-else="">
<div class="sig-line"/>
</t>
<div class="small-muted">Authorized Representative</div>
</div>
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>