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

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