fix(plating): show additional charge under subtotal on SO + invoice PDFs

Tooling/additional charge lines (any product line with no part catalog)
no longer print in the parts table — they render in the totals block
under the subtotal with their entered label + amount. Subtotal is now
parts-only; tax + grand total are unchanged (the charge is still a real
taxed line in the data). Applies to SO confirmation and invoice, both
portrait and landscape. Also aligns the invoice S/N cell to the SO's
multi-serial rendering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-30 00:04:33 -04:00
parent 69aa6b050b
commit 9826e03b4e
3 changed files with 78 additions and 13 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Reports',
'version': '19.0.11.32.0',
'version': '19.0.11.33.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [

View File

@@ -221,7 +221,8 @@
<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-elif="not line.display_type or line.display_type == 'product'">
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- Three stacked lines: Part #, Name, S/N -->
@@ -238,8 +239,8 @@
</div>
<div>
<strong>S/N:</strong>
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
<span t-esc="line.x_fc_serial_id.name"/>
<t t-if="line.x_fc_serial_ids">
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('name'))"/>
</t>
<t t-else=""></t>
</div>
@@ -279,15 +280,31 @@
</t>
</div>
<div class="col-6" style="text-align: right;">
<!-- Additional charges (tooling, rush, etc.) carry from
the SO as real taxed invoice lines but render here
under the subtotal, NOT in the parts table above.
Subtotal is parts-only so tax + grand total stay the
standard Odoo figures. -->
<t t-set="fp_charge_lines" t-value="doc.invoice_line_ids.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 150px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 110px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
<span t-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>
@@ -559,7 +576,8 @@
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type or line.display_type == 'product'">
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- Three stacked lines: Part #, Name, S/N -->
@@ -576,8 +594,8 @@
</div>
<div>
<strong>S/N:</strong>
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
<span t-esc="line.x_fc_serial_id.name"/>
<t t-if="line.x_fc_serial_ids">
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('name'))"/>
</t>
<t t-else=""></t>
</div>
@@ -620,15 +638,28 @@
</t>
</div>
<div class="col-5" style="text-align: right;">
<!-- Additional charges render under the subtotal (see the
portrait template for the rationale). -->
<t t-set="fp_charge_lines" t-value="doc.invoice_line_ids.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 200px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 150px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
<span t-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>

View File

@@ -392,7 +392,10 @@
<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-elif="not line.display_type or line.display_type == 'product'">
<!-- Charge / fee lines (product line with no part
catalog) are skipped here and rendered in the
totals block under the subtotal. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- Three stacked lines:
@@ -455,15 +458,32 @@
</t>
</div>
<div class="col-6" style="text-align: right;">
<!-- Additional charges (tooling, rush, etc.) are real
taxed SO lines but render here under the subtotal,
NOT in the parts table above. Any product line with
no part catalog is treated as a charge. Subtotal is
parts-only (amount_untaxed minus the charges), so the
tax + grand total stay the standard Odoo figures. -->
<t t-set="fp_charge_lines" t-value="doc.order_line.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 150px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 110px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
<span t-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>
@@ -668,7 +688,8 @@
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type or line.display_type == 'product'">
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
@@ -718,13 +739,26 @@
</t>
</div>
<div class="col-5" style="text-align: right;">
<!-- Additional charges render under the subtotal (see
the portrait template for the rationale). -->
<t t-set="fp_charge_lines" t-value="doc.order_line.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 200px;">Subtotal</td>
<td class="text-end" style="min-width: 150px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
<span t-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<td>Taxes</td>
<td class="text-end">