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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user