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:
@@ -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>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user