fix(express): show Tax on totals + add tooling as real SO line

Three related fixes on the Express Orders totals card:

1. Totals card now breaks out Subtotal / Tax / Tooling Charge /
   Grand Total. Previously the "Subtotal" and "Grand Total" rows
   both read from total_amount (same value rendered twice) and no
   tax was shown at all. Customers on a fiscal position-mapped
   tax rate (Ontario HST, etc.) had their taxes silently dropped
   from the preview.

2. tooling_charge now feeds the Grand Total. The total_amount
   compute previously summed line subtotals only. Added a real
   SO line for the tooling charge in action_create_order so the
   eventual sale.order.amount_total matches the preview AND the
   invoice carries a "Tooling Charge" line item.

3. tax_ids is now visible as an optional column on the lines
   list. Operator can see + override the auto-applied tax per
   line. Default still comes from FP-SERVICE product mapped
   through partner.property_account_position_id (fiscal position).

New compute fields on fp.direct.order.wizard:
  - total_subtotal (sum of line.qty * line.unit_price, pre-tax)
  - total_tax (sum of line + tooling taxes via compute_all)
  - total_amount (subtotal + tax + tooling — was just subtotal)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-27 10:33:31 -04:00
parent 2f74d5ecb9
commit d1fc3d8720
3 changed files with 134 additions and 17 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Configurator',
'version': '19.0.22.5.0',
'version': '19.0.22.6.0',
'category': 'Manufacturing/Plating',
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
'description': """

View File

@@ -63,8 +63,6 @@
<field name="name" readonly="1"/>
<span class="o_fp_xpr_pill">EXPRESS</span>
</h1>
<field name="user_id" readonly="state != 'draft'"
options="{'no_create': True}"/>
<field name="view_source" invisible="1"/>
</div>
@@ -87,16 +85,17 @@
</div>
<!-- ============================================================
PO Block fills LEFT half (cols 1-2) across rows 2-5.
RIGHT half (cols 3-4) flows 4 pairs of fields
PO Block fills LEFT half (cols 1-2) across rows 2-7.
RIGHT half (cols 3-4) flows 6 pairs of fields
alongside it — Customer Job #/Job Sorting, Material
Process/Lead Time, Payment Terms/Delivery Method,
Pricelist/Quote Validity.
Pricelist/Quote Validity, Blanket SO/Invoice Strategy,
Sales Rep/conditional Deposit-or-Progress %.
Net: PO block ~250px height matches 4 × ~60px right
stack — no dead air on either side.
Net: PO block height matches 6 × ~60px right stack —
no dead air on either side.
============================================================ -->
<div class="o_fp_xpr_cell span-2 row-span-4 o_fp_xpr_po_block">
<div class="o_fp_xpr_cell span-2 row-span-6 o_fp_xpr_po_block">
<div class="o_fp_xpr_po_head">
<span>CUSTOMER PO</span>
<field name="po_status" widget="badge"
@@ -185,12 +184,14 @@
<field name="validity_date" nolabel="1"/>
</div>
<!-- Row 6 (after PO block ends) — Blanket + Invoice + conditional % -->
<div class="o_fp_xpr_cell">
<!-- Right side row 6: Blanket Sales Order + Invoice Strategy -->
<div class="o_fp_xpr_cell o_fp_xpr_inline_label">
<label for="is_blanket_order">Blanket Sales Order</label>
<div class="o_fp_xpr_inline_pair">
<field name="is_blanket_order" nolabel="1"/>
<field name="is_blanket_order" nolabel="1"
widget="boolean_toggle"/>
<field name="block_partial_shipments" nolabel="1"
widget="boolean_toggle"
invisible="not is_blanket_order"/>
<span class="o_fp_xpr_inline_help"
invisible="not is_blanket_order">Block partial</span>
@@ -200,6 +201,14 @@
<label for="invoice_strategy">Invoice Strategy</label>
<field name="invoice_strategy" nolabel="1"/>
</div>
<!-- Right side row 7: Sales Rep + conditional Deposit/Progress % -->
<div class="o_fp_xpr_cell">
<label for="user_id">Sales Rep</label>
<field name="user_id" nolabel="1"
readonly="state != 'draft'"
options="{'no_create': True}"/>
</div>
<div class="o_fp_xpr_cell" invisible="invoice_strategy != 'deposit'">
<label for="deposit_percent">Deposit %</label>
<field name="deposit_percent" nolabel="1"/>
@@ -298,6 +307,12 @@
options="{'no_quick_create': True}"
invisible="not part_catalog_id"
optional="hide"/>
<field name="tax_ids"
string="Tax"
widget="many2many_tags"
options="{'no_create': True}"
optional="show"
width="110px"/>
<field name="currency_id" column_invisible="1"/>
</list>
</field>
@@ -328,7 +343,14 @@
<div class="o_fp_xpr_card o_fp_xpr_totals">
<div class="o_fp_xpr_total_row">
<span class="o_fp_xpr_total_label">Subtotal</span>
<field name="total_amount"
<field name="total_subtotal"
widget="monetary"
options="{'currency_field': 'currency_id'}"
readonly="1" nolabel="1"/>
</div>
<div class="o_fp_xpr_total_row">
<span class="o_fp_xpr_total_label">Tax</span>
<field name="total_tax"
widget="monetary"
options="{'currency_field': 'currency_id'}"
readonly="1" nolabel="1"/>

View File

@@ -342,9 +342,24 @@ class FpDirectOrderWizard(models.Model):
line_ids = fields.One2many(
'fp.direct.order.line', 'wizard_id', string='Order Lines',
)
total_amount = fields.Monetary(
string='Order Total',
total_subtotal = fields.Monetary(
string='Subtotal (pre-tax)',
compute='_compute_totals', currency_field='currency_id',
help='Sum of (qty × unit_price) across all lines, before tax. '
'Excludes the tooling charge.',
)
total_tax = fields.Monetary(
string='Tax',
compute='_compute_totals', currency_field='currency_id',
help='Sum of taxes across all lines + the tooling charge. '
'Each line uses the partner\'s fiscal position to map the '
'FP-SERVICE product taxes to the right rate.',
)
total_amount = fields.Monetary(
string='Grand Total',
compute='_compute_totals', currency_field='currency_id',
help='Subtotal + tax + tooling charge. Matches the eventual '
'sale.order.amount_total when the order is confirmed.',
)
total_qty = fields.Integer(string='Total Qty', compute='_compute_totals')
total_line_count = fields.Integer(
@@ -366,10 +381,66 @@ class FpDirectOrderWizard(models.Model):
return super().create(vals_list)
# ---- Computes ----
@api.depends('line_ids.line_subtotal', 'line_ids.quantity')
@api.depends(
'line_ids.line_subtotal',
'line_ids.quantity',
'line_ids.unit_price',
'line_ids.tax_ids',
'tooling_charge',
'partner_id',
'currency_id',
)
def _compute_totals(self):
"""Roll up subtotal / tax / grand total across lines + tooling.
Each line's taxes are computed via account.tax.compute_all so the
Express form's totals card mirrors what the eventual SO will show
once the operator hits Confirm. The tooling charge is added at the
wizard level here AND pushed as an actual sale.order.line at
action_create_order time (so it carries into the invoice).
"""
for rec in self:
rec.total_amount = sum(rec.line_ids.mapped('line_subtotal'))
subtotal = 0.0
tax_total = 0.0
for line in rec.line_ids:
line_pre_tax = (line.quantity or 0) * (line.unit_price or 0.0)
subtotal += line_pre_tax
if line.tax_ids and line_pre_tax:
taxes_res = line.tax_ids.compute_all(
line.unit_price or 0.0,
currency=rec.currency_id,
quantity=line.quantity or 0,
product=None,
partner=rec.partner_id or None,
)
tax_total += (
taxes_res['total_included']
- taxes_res['total_excluded']
)
# Tooling charge: pick the tax set from the FIRST line that
# has one (best proxy for the customer's standard rate). If
# no line has taxes set yet, tooling is shown untaxed in the
# preview; the eventual SO line will apply product defaults.
tooling = rec.tooling_charge or 0.0
if tooling:
first_taxed_line = next(
(l for l in rec.line_ids if l.tax_ids), False,
)
if first_taxed_line:
tooling_res = first_taxed_line.tax_ids.compute_all(
tooling,
currency=rec.currency_id,
quantity=1,
product=None,
partner=rec.partner_id or None,
)
tax_total += (
tooling_res['total_included']
- tooling_res['total_excluded']
)
rec.total_subtotal = subtotal
rec.total_tax = tax_total
rec.total_amount = subtotal + tax_total + tooling
rec.total_qty = sum(rec.line_ids.mapped('quantity'))
rec.total_line_count = len(rec.line_ids)
@@ -824,6 +895,30 @@ class FpDirectOrderWizard(models.Model):
if line.tax_ids else False),
}))
# 4b. Tooling charge — surface as a real sale.order.line so it
# carries through SO.amount_total + invoice naturally, instead of
# sitting orphaned on the SO header. Tax: inherit from the first
# part line that has taxes set (best proxy for "the customer's
# standard tax rate"); falls back to product defaults if no line
# has taxes yet.
if self.tooling_charge:
tooling_taxes = False
first_taxed = next(
(l for l in self.line_ids if l.tax_ids), False,
)
if first_taxed:
tooling_taxes = [(6, 0, first_taxed.tax_ids.ids)]
so_vals['order_line'].append((0, 0, {
'product_id': product.id,
'name': _('Tooling Charge'),
'product_uom_qty': 1.0,
'price_unit': self.tooling_charge,
'x_fc_internal_description': _(
'One-time tooling fee added via Express Orders.'
),
'tax_ids': tooling_taxes,
}))
# 5. Create — stays in draft / quotation. Sub 1: user reviews
# and manually clicks Confirm / Send. No auto-confirm, no
# auto-email to the client.