Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions stock_picking_report_valued_sale_mrp/models/stock_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ def _get_components_per_kit(self):
or sale_line.product_id.ids == sale_line.product_id.get_components()
):
return 0
component_demand = sum(
sale_line.move_ids.filtered(
lambda x: x.product_id == self.product_id
and not x.origin_returned_move_id
and (
x.state != "cancel"
or (x.state == "cancel" and x.picking_id.backorder_id)
)
).mapped("product_uom_qty")
kit_moves = sale_line.move_ids.filtered(
lambda x: x.product_id == self.product_id
and (
x.state != "cancel"
or (x.state == "cancel" and x.picking_id.backorder_id)
)
)

kit_moves_to_sum = kit_moves.filtered(lambda x: not x.origin_returned_move_id)
kit_moves_to_subtract = kit_moves - kit_moves_to_sum

component_demand = sum(kit_moves_to_sum.mapped("product_uom_qty")) - sum(
kit_moves_to_subtract.mapped("product_uom_qty")
)
return component_demand / sale_line.product_uom_qty
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,51 @@ def setUpClass(cls):
cls.product_2 = cls.product_product.create(
{"name": "Product test 2", "type": "product"}
)
order_form = Form(cls.env["sale.order"])
order_form.partner_id = cls.partner
(
cls.sale_order_3,
cls.order_line,
cls.order_out_picking,
) = cls._create_sale_order_with_lines(
cls,
product=cls.product_kit,
quantity=5,
price_unit=29.9,
tax=cls.tax10,
)

def _create_sale_order_with_lines(self, product, quantity, price_unit, tax=None):
"""Create a sale order with a single line, confirm it, and return
the sale order, filtered order line, and picking.

Args:
product: product.product to add to the order
quantity: quantity for the order line
price_unit: unit price for the order line
tax: account.tax to add (optional)

Returns:
Tuple of (sale.order, filtered order_line, stock.picking)
"""
order_form = Form(self.env["sale.order"])
order_form.partner_id = self.partner

with order_form.order_line.new() as line_form:
line_form.product_id = cls.product_kit
line_form.product_uom_qty = 5
line_form.price_unit = 29.9
line_form.product_id = product
line_form.product_uom_qty = quantity
line_form.price_unit = price_unit
line_form.tax_id.clear()
line_form.tax_id.add(cls.tax10)
cls.sale_order_3 = order_form.save()
cls.sale_order_3.action_confirm()
# Maybe other modules create additional lines in the create
# method in sale.order model, so let's find the correct line.
cls.order_line = cls.sale_order_3.order_line.filtered(
lambda r: r.product_id == cls.product_kit
)
cls.order_out_picking = cls.sale_order_3.picking_ids
if tax:
line_form.tax_id.add(tax)

sale_order = order_form.save()
sale_order.action_confirm()

# Filter to find the correct line (handles modules that add extra lines)
order_line = sale_order.order_line.filtered(lambda r: r.product_id == product)
# Return the first picking, not the recordset
picking = sale_order.picking_ids[0] if sale_order.picking_ids else None

return sale_order, order_line, picking

def test_01_picking_confirmed(self):
for line in self.order_out_picking.move_ids:
Expand All @@ -73,3 +102,94 @@ def test_01_picking_confirmed(self):
self.env["ir.actions.report"]._render_qweb_html(
self.env.ref("stock.action_report_delivery"), self.order_out_picking.ids
)

def _create_sale_order_for_kits(self, qty):
"""Create a new sale order for the configured kit product."""
return self._create_sale_order_with_lines(
product=self.product_kit,
quantity=qty,
price_unit=29.9,
)

def test_02_get_components_per_kit_return_redelivery(self):
"""_get_components_per_kit() must return correct component-per-kit value
after full return + redelivery, using a clean, fresh SO."""

# ---------------------------------------------------------
# Create a fresh SO with quantity = 2 kits
# ---------------------------------------------------------
sale, so_line, picking = self._create_sale_order_for_kits(qty=2)
picking = picking[0]

# ---------------------------------------------------------
# First delivery
# ---------------------------------------------------------
picking.action_assign()
for line in picking.move_ids:
line.quantity_done = line.product_uom_qty

picking.button_validate()

# ---------------------------------------------------------
# Return delivery (Odoo core pattern)
# ---------------------------------------------------------
return_wizard_form = Form(
self.env["stock.return.picking"].with_context(
active_id=picking.id,
active_model="stock.picking",
)
)

return_wiz = return_wizard_form.save()
res = return_wiz.create_returns()["res_id"]
return_picking = self.env["stock.picking"].browse(res)

return_picking.action_assign()
for line in return_picking.move_ids:
line.quantity_done = line.product_uom_qty
return_picking.button_validate()

# ---------------------------------------------------------
# Redelivery
# ---------------------------------------------------------
sale._action_cancel()
sale.action_draft()
sale.action_confirm()
redelivery = sale.picking_ids.filtered(
lambda p: p.state not in ("done", "cancel")
)
self.assertTrue(redelivery)
redelivery = redelivery[0]

redelivery.action_assign()
for line in redelivery.move_ids:
line.quantity_done = line.product_uom_qty
redelivery.button_validate()

# ---------------------------------------------------------
# Validate `_get_components_per_kit()` correctly calculates
# BOM component quantities PER KIT (not doubled)
# ---------------------------------------------------------
kit_lines = redelivery.move_line_ids.filtered("phantom_product_id")
self.assertTrue(kit_lines)
expected_per_kit = {
self.product_kit_comp_1.id: 2.0,
self.product_kit_comp_2.id: 4.0,
}

for sale_line in kit_lines.mapped("sale_line"):
move_lines = kit_lines.filtered(lambda x: x.sale_line == sale_line)
phantom_line = move_lines[:1]
if not phantom_line:
continue

move = phantom_line.move_id
expected = expected_per_kit[move.product_id.id]
got = move._get_components_per_kit()

self.assertEqual(
got,
expected,
f"_get_components_per_kit returned {got} but expected {expected} "
f"for component {move.product_id.display_name}",
)