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:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Configurator',
|
'name': 'Fusion Plating — Configurator',
|
||||||
'version': '19.0.22.5.0',
|
'version': '19.0.22.6.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -63,8 +63,6 @@
|
|||||||
<field name="name" readonly="1"/>
|
<field name="name" readonly="1"/>
|
||||||
<span class="o_fp_xpr_pill">EXPRESS</span>
|
<span class="o_fp_xpr_pill">EXPRESS</span>
|
||||||
</h1>
|
</h1>
|
||||||
<field name="user_id" readonly="state != 'draft'"
|
|
||||||
options="{'no_create': True}"/>
|
|
||||||
<field name="view_source" invisible="1"/>
|
<field name="view_source" invisible="1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,16 +85,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
PO Block fills LEFT half (cols 1-2) across rows 2-5.
|
PO Block fills LEFT half (cols 1-2) across rows 2-7.
|
||||||
RIGHT half (cols 3-4) flows 4 pairs of fields
|
RIGHT half (cols 3-4) flows 6 pairs of fields
|
||||||
alongside it — Customer Job #/Job Sorting, Material
|
alongside it — Customer Job #/Job Sorting, Material
|
||||||
Process/Lead Time, Payment Terms/Delivery Method,
|
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
|
Net: PO block height matches 6 × ~60px right stack —
|
||||||
stack — no dead air on either side.
|
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">
|
<div class="o_fp_xpr_po_head">
|
||||||
<span>CUSTOMER PO</span>
|
<span>CUSTOMER PO</span>
|
||||||
<field name="po_status" widget="badge"
|
<field name="po_status" widget="badge"
|
||||||
@@ -185,12 +184,14 @@
|
|||||||
<field name="validity_date" nolabel="1"/>
|
<field name="validity_date" nolabel="1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 6 (after PO block ends) — Blanket + Invoice + conditional % -->
|
<!-- Right side row 6: Blanket Sales Order + Invoice Strategy -->
|
||||||
<div class="o_fp_xpr_cell">
|
<div class="o_fp_xpr_cell o_fp_xpr_inline_label">
|
||||||
<label for="is_blanket_order">Blanket Sales Order</label>
|
<label for="is_blanket_order">Blanket Sales Order</label>
|
||||||
<div class="o_fp_xpr_inline_pair">
|
<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"
|
<field name="block_partial_shipments" nolabel="1"
|
||||||
|
widget="boolean_toggle"
|
||||||
invisible="not is_blanket_order"/>
|
invisible="not is_blanket_order"/>
|
||||||
<span class="o_fp_xpr_inline_help"
|
<span class="o_fp_xpr_inline_help"
|
||||||
invisible="not is_blanket_order">Block partial</span>
|
invisible="not is_blanket_order">Block partial</span>
|
||||||
@@ -200,6 +201,14 @@
|
|||||||
<label for="invoice_strategy">Invoice Strategy</label>
|
<label for="invoice_strategy">Invoice Strategy</label>
|
||||||
<field name="invoice_strategy" nolabel="1"/>
|
<field name="invoice_strategy" nolabel="1"/>
|
||||||
</div>
|
</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'">
|
<div class="o_fp_xpr_cell" invisible="invoice_strategy != 'deposit'">
|
||||||
<label for="deposit_percent">Deposit %</label>
|
<label for="deposit_percent">Deposit %</label>
|
||||||
<field name="deposit_percent" nolabel="1"/>
|
<field name="deposit_percent" nolabel="1"/>
|
||||||
@@ -298,6 +307,12 @@
|
|||||||
options="{'no_quick_create': True}"
|
options="{'no_quick_create': True}"
|
||||||
invisible="not part_catalog_id"
|
invisible="not part_catalog_id"
|
||||||
optional="hide"/>
|
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"/>
|
<field name="currency_id" column_invisible="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
@@ -328,7 +343,14 @@
|
|||||||
<div class="o_fp_xpr_card o_fp_xpr_totals">
|
<div class="o_fp_xpr_card o_fp_xpr_totals">
|
||||||
<div class="o_fp_xpr_total_row">
|
<div class="o_fp_xpr_total_row">
|
||||||
<span class="o_fp_xpr_total_label">Subtotal</span>
|
<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"
|
widget="monetary"
|
||||||
options="{'currency_field': 'currency_id'}"
|
options="{'currency_field': 'currency_id'}"
|
||||||
readonly="1" nolabel="1"/>
|
readonly="1" nolabel="1"/>
|
||||||
|
|||||||
@@ -342,9 +342,24 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
line_ids = fields.One2many(
|
line_ids = fields.One2many(
|
||||||
'fp.direct.order.line', 'wizard_id', string='Order Lines',
|
'fp.direct.order.line', 'wizard_id', string='Order Lines',
|
||||||
)
|
)
|
||||||
total_amount = fields.Monetary(
|
total_subtotal = fields.Monetary(
|
||||||
string='Order Total',
|
string='Subtotal (pre-tax)',
|
||||||
compute='_compute_totals', currency_field='currency_id',
|
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_qty = fields.Integer(string='Total Qty', compute='_compute_totals')
|
||||||
total_line_count = fields.Integer(
|
total_line_count = fields.Integer(
|
||||||
@@ -366,10 +381,66 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
return super().create(vals_list)
|
return super().create(vals_list)
|
||||||
|
|
||||||
# ---- Computes ----
|
# ---- 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):
|
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:
|
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_qty = sum(rec.line_ids.mapped('quantity'))
|
||||||
rec.total_line_count = len(rec.line_ids)
|
rec.total_line_count = len(rec.line_ids)
|
||||||
|
|
||||||
@@ -824,6 +895,30 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
if line.tax_ids else False),
|
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
|
# 5. Create — stays in draft / quotation. Sub 1: user reviews
|
||||||
# and manually clicks Confirm / Send. No auto-confirm, no
|
# and manually clicks Confirm / Send. No auto-confirm, no
|
||||||
# auto-email to the client.
|
# auto-email to the client.
|
||||||
|
|||||||
Reference in New Issue
Block a user