fix(plating): order-level Lot Order toggle replaces per-line lot checkbox
Express order entry now has a single "Lot Order" toggle on the header instead of a per-line "Lot" checkbox. When on, every line shows Lot Total and prices as a flat lot (unit price derived = lot total / qty, qty preserved for production); when off, the Lot Total column is hidden and lines price per unit as usual. Keeps the order summary clean for the common per-unit case. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Configurator',
|
'name': 'Fusion Plating — Configurator',
|
||||||
'version': '19.0.23.0.0',
|
'version': '19.0.23.1.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': """
|
||||||
|
|||||||
@@ -45,23 +45,42 @@ class TestChargeTaxLot(TransactionCase):
|
|||||||
self.assertAlmostEqual(wiz.total_tax, 19.5, places=2)
|
self.assertAlmostEqual(wiz.total_tax, 19.5, places=2)
|
||||||
self.assertAlmostEqual(wiz.total_amount, 169.5, places=2)
|
self.assertAlmostEqual(wiz.total_amount, 169.5, places=2)
|
||||||
|
|
||||||
# ----- Task 4: lot pricing -----
|
# ----- Task 4: lot pricing (order-level toggle) -----
|
||||||
def test_lot_onchange_derives_unit_price(self):
|
def test_lot_onchange_derives_unit_price(self):
|
||||||
wiz = self._make_wizard()
|
wiz = self._make_wizard(is_lot_order=True)
|
||||||
line = self.env['fp.direct.order.line'].new({
|
line = self.env['fp.direct.order.line'].new({
|
||||||
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
||||||
'is_lot_priced': True,
|
|
||||||
})
|
})
|
||||||
line._onchange_lot_pricing()
|
line._onchange_lot_pricing()
|
||||||
self.assertEqual(line.unit_price, 2.0)
|
self.assertEqual(line.unit_price, 2.0)
|
||||||
|
|
||||||
def test_lot_line_subtotal_uses_lot_total(self):
|
def test_lot_onchange_noop_when_not_lot_order(self):
|
||||||
# Even if unit_price wasn't derived (onchange didn't fire), the
|
# Editing lot_total on a non-lot order must NOT touch unit_price.
|
||||||
# summary subtotal uses the flat lot_total for lot-priced lines.
|
wiz = self._make_wizard()
|
||||||
wiz = self._make_wizard(tax_id=self.tax13.id)
|
line = self.env['fp.direct.order.line'].new({
|
||||||
|
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
||||||
|
'unit_price': 7.0,
|
||||||
|
})
|
||||||
|
line._onchange_lot_pricing()
|
||||||
|
self.assertEqual(line.unit_price, 7.0)
|
||||||
|
|
||||||
|
def test_lot_order_toggle_rederives_line_prices(self):
|
||||||
|
wiz = self._make_wizard()
|
||||||
self.env['fp.direct.order.line'].create({
|
self.env['fp.direct.order.line'].create({
|
||||||
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
||||||
'is_lot_priced': True, 'unit_price': 0.0,
|
'unit_price': 0.0,
|
||||||
|
})
|
||||||
|
wiz.is_lot_order = True
|
||||||
|
wiz._onchange_is_lot_order()
|
||||||
|
self.assertEqual(wiz.line_ids.unit_price, 2.0)
|
||||||
|
|
||||||
|
def test_lot_line_subtotal_uses_lot_total(self):
|
||||||
|
# On a lot order, the summary subtotal uses the flat lot_total
|
||||||
|
# per line (not qty × unit_price), even if unit_price is 0.
|
||||||
|
wiz = self._make_wizard(tax_id=self.tax13.id, is_lot_order=True)
|
||||||
|
self.env['fp.direct.order.line'].create({
|
||||||
|
'wizard_id': wiz.id, 'quantity': 500, 'lot_total': 1000.0,
|
||||||
|
'unit_price': 0.0,
|
||||||
})
|
})
|
||||||
wiz.invalidate_recordset()
|
wiz.invalidate_recordset()
|
||||||
self.assertEqual(wiz.total_subtotal, 1000.0)
|
self.assertEqual(wiz.total_subtotal, 1000.0)
|
||||||
|
|||||||
@@ -233,6 +233,10 @@
|
|||||||
<span><strong>OPEN</strong> open the part record</span>
|
<span><strong>OPEN</strong> open the part record</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 d-flex align-items-center gap-2">
|
||||||
|
<field name="is_lot_order" widget="boolean_toggle"/>
|
||||||
|
<span><strong>Lot Order</strong> — price each line as a flat lot total (qty preserved for production)</span>
|
||||||
|
</div>
|
||||||
<div class="mb-2 d-flex gap-2">
|
<div class="mb-2 d-flex gap-2">
|
||||||
<button name="action_add_from_prior_so"
|
<button name="action_add_from_prior_so"
|
||||||
type="object"
|
type="object"
|
||||||
@@ -286,17 +290,17 @@
|
|||||||
width="120px"/>
|
width="120px"/>
|
||||||
<field name="internal_description" string="Internal Notes" optional="show"/>
|
<field name="internal_description" string="Internal Notes" optional="show"/>
|
||||||
<field name="quantity" string="Qty" width="55px"/>
|
<field name="quantity" string="Qty" width="55px"/>
|
||||||
<field name="is_lot_priced" string="Lot" width="50px"/>
|
|
||||||
<field name="lot_total"
|
<field name="lot_total"
|
||||||
string="Lot Total"
|
string="Lot Total"
|
||||||
widget="monetary"
|
widget="monetary"
|
||||||
options="{'currency_field': 'currency_id'}"
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
column_invisible="not parent.is_lot_order"
|
||||||
width="90px"/>
|
width="90px"/>
|
||||||
<field name="unit_price"
|
<field name="unit_price"
|
||||||
string="Price"
|
string="Price"
|
||||||
widget="monetary"
|
widget="monetary"
|
||||||
options="{'currency_field': 'currency_id'}"
|
options="{'currency_field': 'currency_id'}"
|
||||||
readonly="is_lot_priced"
|
readonly="parent.is_lot_order"
|
||||||
width="80px"/>
|
width="80px"/>
|
||||||
<field name="line_subtotal"
|
<field name="line_subtotal"
|
||||||
string="Subtotal"
|
string="Subtotal"
|
||||||
|
|||||||
@@ -238,20 +238,17 @@ class FpDirectOrderLine(models.Model):
|
|||||||
string='Unit Price',
|
string='Unit Price',
|
||||||
currency_field='currency_id',
|
currency_field='currency_id',
|
||||||
)
|
)
|
||||||
is_lot_priced = fields.Boolean(
|
|
||||||
string='Lot Price',
|
|
||||||
help='Price the whole quantity as a flat lot total instead of '
|
|
||||||
'per unit. Unit price is derived = lot total / quantity; '
|
|
||||||
'the quantity is preserved for production.',
|
|
||||||
)
|
|
||||||
lot_total = fields.Monetary(
|
lot_total = fields.Monetary(
|
||||||
string='Lot Total', currency_field='currency_id',
|
string='Lot Total', currency_field='currency_id',
|
||||||
|
help='Flat total for the whole line quantity when the order is a '
|
||||||
|
'lot order. Unit price is derived = lot total / quantity; '
|
||||||
|
'the quantity is preserved for production.',
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.onchange('is_lot_priced', 'lot_total', 'quantity')
|
@api.onchange('lot_total', 'quantity')
|
||||||
def _onchange_lot_pricing(self):
|
def _onchange_lot_pricing(self):
|
||||||
for line in self:
|
for line in self:
|
||||||
if line.is_lot_priced and line.quantity:
|
if line.wizard_id.is_lot_order and line.quantity:
|
||||||
line.unit_price = (line.lot_total or 0.0) / line.quantity
|
line.unit_price = (line.lot_total or 0.0) / line.quantity
|
||||||
line_subtotal = fields.Monetary(
|
line_subtotal = fields.Monetary(
|
||||||
string='Subtotal',
|
string='Subtotal',
|
||||||
|
|||||||
@@ -336,6 +336,22 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
if rec.charge_type_id and not rec.charge_amount:
|
if rec.charge_type_id and not rec.charge_amount:
|
||||||
rec.charge_amount = rec.charge_type_id.default_amount
|
rec.charge_amount = rec.charge_type_id.default_amount
|
||||||
|
|
||||||
|
is_lot_order = fields.Boolean(
|
||||||
|
string='Lot Order',
|
||||||
|
help='Price the order as flat lots instead of per unit. When on, '
|
||||||
|
'each line takes a Lot Total and the unit price is derived = '
|
||||||
|
'lot total / quantity; the quantity is preserved for '
|
||||||
|
'production. When off, lines price per unit as usual.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('is_lot_order')
|
||||||
|
def _onchange_is_lot_order(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.is_lot_order:
|
||||||
|
for line in rec.line_ids:
|
||||||
|
if line.quantity:
|
||||||
|
line.unit_price = (line.lot_total or 0.0) / line.quantity
|
||||||
|
|
||||||
# ---- PO status pill (computed, display-only) ----
|
# ---- PO status pill (computed, display-only) ----
|
||||||
po_status = fields.Selection(
|
po_status = fields.Selection(
|
||||||
[('received', 'Received'),
|
[('received', 'Received'),
|
||||||
@@ -403,8 +419,8 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
@api.depends(
|
@api.depends(
|
||||||
'line_ids.quantity',
|
'line_ids.quantity',
|
||||||
'line_ids.unit_price',
|
'line_ids.unit_price',
|
||||||
'line_ids.is_lot_priced',
|
|
||||||
'line_ids.lot_total',
|
'line_ids.lot_total',
|
||||||
|
'is_lot_order',
|
||||||
'charge_amount',
|
'charge_amount',
|
||||||
'tooling_charge',
|
'tooling_charge',
|
||||||
'tax_id',
|
'tax_id',
|
||||||
@@ -420,11 +436,13 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
at action_create_order time (so it carries into the invoice).
|
at action_create_order time (so it carries into the invoice).
|
||||||
"""
|
"""
|
||||||
for rec in self:
|
for rec in self:
|
||||||
subtotal = sum(
|
if rec.is_lot_order:
|
||||||
(l.lot_total or 0.0) if l.is_lot_priced
|
subtotal = sum(l.lot_total or 0.0 for l in rec.line_ids)
|
||||||
else (l.quantity or 0) * (l.unit_price or 0.0)
|
else:
|
||||||
for l in rec.line_ids
|
subtotal = sum(
|
||||||
)
|
(l.quantity or 0) * (l.unit_price or 0.0)
|
||||||
|
for l in rec.line_ids
|
||||||
|
)
|
||||||
charge = rec.charge_amount or rec.tooling_charge or 0.0
|
charge = rec.charge_amount or rec.tooling_charge or 0.0
|
||||||
tax_total = 0.0
|
tax_total = 0.0
|
||||||
if rec.tax_id and (subtotal + charge):
|
if rec.tax_id and (subtotal + charge):
|
||||||
@@ -858,7 +876,7 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
'product_uom_qty': line.quantity,
|
'product_uom_qty': line.quantity,
|
||||||
'price_unit': (
|
'price_unit': (
|
||||||
(line.lot_total / line.quantity)
|
(line.lot_total / line.quantity)
|
||||||
if line.is_lot_priced and line.quantity
|
if self.is_lot_order and line.quantity
|
||||||
else (line.unit_price or 0.0)
|
else (line.unit_price or 0.0)
|
||||||
),
|
),
|
||||||
'x_fc_part_catalog_id': part.id,
|
'x_fc_part_catalog_id': part.id,
|
||||||
@@ -904,7 +922,7 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
# NB. Odoo 19 renamed the SO line field to tax_ids.
|
# NB. Odoo 19 renamed the SO line field to tax_ids.
|
||||||
'tax_ids': ([(6, 0, self.tax_id.ids)]
|
'tax_ids': ([(6, 0, self.tax_id.ids)]
|
||||||
if self.tax_id else False),
|
if self.tax_id else False),
|
||||||
'x_fc_is_lot_priced': line.is_lot_priced,
|
'x_fc_is_lot_priced': self.is_lot_order,
|
||||||
'x_fc_lot_total': line.lot_total or 0.0,
|
'x_fc_lot_total': line.lot_total or 0.0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user