[FIX] stock: unreserve the correct quantity

case:
- create a UoM 'Hundreds' related to UoM 'Units', bigger by factor x100
- create a delivery picking
- create a move with the UoM Hundreds, indicate 10 Hundreds
- validate, 1000 should be reserved
- unreserve, only 10 units are unreserved, 990 are still reserved

This is because the stock move line takes the UoM of the stock move by default on the `quantity` field
and `unreserve` uses that field instead of `quantity_product_uom`

This bug led to the discovery of other cases where `quantity_product_uom` should be used instead of `quantity`

closes odoo/odoo#144574

Related: odoo/enterprise#51964
Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
snd
2023-12-04 13:23:37 +00:00
parent dcec4ad28d
commit a4b21bf5c8
10 changed files with 27 additions and 27 deletions
+1 -1
View File
@@ -273,7 +273,7 @@ class TestMrpProductionBackorder(TestMrpCommon):
p2_bo_mls = production.procurement_group_id.mrp_production_ids[-1].move_raw_ids.filtered(lambda m: m.product_id == p2).move_line_ids
self.assertEqual(len(p2_bo_mls), 1)
self.assertEqual(p2_bo_mls.lot_id, lot1)
self.assertEqual(p2_bo_mls.quantity, 2)
self.assertEqual(p2_bo_mls.quantity_product_uom, 2)
def test_uom_backorder(self):
"""
@@ -412,7 +412,7 @@ class TestPurchaseOrder(ValuationReconciliationTestCommon):
self.assertEqual(purchase_order.state, 'purchase')
self.assertEqual(len(purchase_order.picking_ids), 1)
self.assertEqual(len(purchase_order.picking_ids.move_line_ids), 1)
self.assertEqual(purchase_order.picking_ids.move_line_ids.quantity, 7)
self.assertEqual(purchase_order.picking_ids.move_line_ids.quantity_product_uom, 7)
purchase_order.order_line.product_qty = 4
@@ -421,7 +421,7 @@ class TestPurchaseOrder(ValuationReconciliationTestCommon):
purchase_order.button_confirm()
self.assertEqual(len(purchase_order.picking_ids), 1)
self.assertEqual(len(purchase_order.picking_ids.move_line_ids), 1)
self.assertEqual(purchase_order.picking_ids.move_line_ids.quantity, 4)
self.assertEqual(purchase_order.picking_ids.move_line_ids.quantity_product_uom, 4)
def test_message_qty_already_received(self):
self.env.user.write({'company_id': self.company_data['company'].id})
+1 -1
View File
@@ -259,7 +259,7 @@ class Repair(models.Model):
@api.depends('move_ids', 'state', 'move_ids.product_uom_qty')
def _compute_unreserve_visible(self):
for repair in self:
already_reserved = repair.state not in ('done', 'cancel') and any(repair.mapped('move_ids.move_line_ids.quantity'))
already_reserved = repair.state not in ('done', 'cancel') and any(repair.mapped('move_ids.move_line_ids.quantity_product_uom'))
repair.unreserve_visible = already_reserved
repair.reserve_visible = (
+1 -1
View File
@@ -207,7 +207,7 @@ class Location(models.Model):
for f in {'usage', 'scrap_location'}))
reserved_quantities = self.env['stock.move.line'].search_count([
('location_id', 'in', modified_locations.ids),
('quantity', '>', 0),
('quantity_product_uom', '>', 0),
])
if reserved_quantities:
raise UserError(_(
+4 -4
View File
@@ -1565,7 +1565,7 @@ Please change the quantity done or the rounding precision of your unit of measur
quantity += ml.product_uom_id._compute_quantity(ml.quantity, ml.product_id.uom_id)
grouped_move_lines_out[k] = quantity
for k, g in groupby(move_lines_out_reserved, key=_keys_out_groupby):
grouped_move_lines_out[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('quantity'))
grouped_move_lines_out[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('quantity_product_uom'))
return grouped_move_lines_out
@@ -1683,11 +1683,11 @@ Please change the quantity done or the rounding precision of your unit of measur
available_move_lines = move._get_available_move_lines(assigned_moves_ids, partially_available_moves_ids)
if not available_move_lines:
continue
for move_line in move.move_line_ids.filtered(lambda m: m.quantity):
for move_line in move.move_line_ids.filtered(lambda m: m.quantity_product_uom):
if available_move_lines.get((move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)):
available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.quantity
available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.quantity_product_uom
for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
need = move.product_qty - sum(move.move_line_ids.mapped('quantity'))
need = move.product_qty - sum(move.move_line_ids.mapped('quantity_product_uom'))
# `quantity` is what is brought by chained done move lines. We double check
# here this quantity is available on the quants themselves. If not, this
# could be the result of an inventory adjustment that removed totally of
+4 -4
View File
@@ -290,7 +290,7 @@ class StockMoveLine(models.Model):
ON
stock_move_line (id, company_id, product_id, lot_id, location_id, owner_id, package_id)
WHERE
(state IS NULL OR state NOT IN ('cancel', 'done')) AND quantity > 0 AND not picked""")
(state IS NULL OR state NOT IN ('cancel', 'done')) AND quantity_product_uom > 0 AND not picked""")
@api.model_create_multi
def create(self, vals_list):
@@ -484,9 +484,9 @@ class StockMoveLine(models.Model):
quants_by_product = self.env['stock.quant']._get_quants_by_products_locations(self.product_id, self.location_id)
for ml in self:
# Unlinking a move line should unreserve.
if not float_is_zero(ml.quantity, precision_digits=precision) and ml.move_id and not ml.move_id._should_bypass_reservation(ml.location_id):
if not float_is_zero(ml.quantity_product_uom, precision_digits=precision) and ml.move_id and not ml.move_id._should_bypass_reservation(ml.location_id):
quants = quants_by_product[ml.product_id.id]
quants._update_reserved_quantity(ml.product_id, ml.location_id, -ml.quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
quants._update_reserved_quantity(ml.product_id, ml.location_id, -ml.quantity_product_uom, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
moves = self.mapped('move_id')
res = super().unlink()
if moves:
@@ -705,7 +705,7 @@ class StockMoveLine(models.Model):
('location_id', '=', location_id.id),
('owner_id', '=', owner_id.id if owner_id else False),
('package_id', '=', package_id.id if package_id else False),
('quantity', '>', 0.0),
('quantity_product_uom', '>', 0.0),
('picked', '=', False),
('id', 'not in', tuple(ml_ids_to_ignore)),
]
+4 -4
View File
@@ -2062,12 +2062,12 @@ class StockMove(TransactionCase):
'location_id': self.stock_location.id,
'location_dest_id': self.supplier_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
'product_uom': self.uom_dozen.id,
'product_uom_qty': 10.0,
})
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
self.assertEqual(move1.availability, 100.0)
self.assertEqual(move1.availability, 120.0)
# confirmation
move1._action_confirm()
@@ -2077,7 +2077,7 @@ class StockMove(TransactionCase):
move1._action_assign()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 50.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 30.0)
# unreserve
move1._do_unreserve()
+2 -2
View File
@@ -2622,7 +2622,7 @@ class TestStockUOM(TestStockCommon):
('lot_id', '=', quant_LtDA.lot_id.id),
('package_id', '=', quant_LtDA.package_id.id),
('owner_id', '=', quant_LtDA.owner_id.id),
('quantity', '!=', 0)
('quantity_product_uom', '!=', 0)
])
reserved_on_move_lines_LtDA = sum(move_lines_LtDA.mapped('quantity_product_uom'))
@@ -2632,7 +2632,7 @@ class TestStockUOM(TestStockCommon):
('lot_id', '=', quant_GtDA.lot_id.id),
('package_id', '=', quant_GtDA.package_id.id),
('owner_id', '=', quant_GtDA.owner_id.id),
('quantity', '!=', 0)
('quantity_product_uom', '!=', 0)
])
reserved_on_move_lines_GtDA = sum(move_lines_GtDA.mapped('quantity_product_uom'))
+7 -7
View File
@@ -1152,9 +1152,9 @@ class StockQuantRemovalStrategy(TransactionCase):
self.assertEqual(len(move.move_line_ids), 12)
self.assertRecordValues(
move.move_line_ids,
[{'quantity': 1000}] +
[{'quantity': 50}] * 5 +
[{'quantity': 5}] * 6
[{'quantity_product_uom': 1000}] +
[{'quantity_product_uom': 50}] * 5 +
[{'quantity_product_uom': 5}] * 6
)
def test_least_package_removal_strategy_not_possible(self):
@@ -1183,7 +1183,7 @@ class StockQuantRemovalStrategy(TransactionCase):
self.assertEqual(len(move.move_line_ids), 2)
self.assertRecordValues(
move.move_line_ids,
[{'quantity': 10}] + [{'quantity': 3}]
[{'quantity_product_uom': 10}] + [{'quantity_product_uom': 3}]
)
# Make sure it selects the smallest possible package as best leaf.
self.assertEqual(
@@ -1217,9 +1217,9 @@ class StockQuantRemovalStrategy(TransactionCase):
self.assertEqual(len(move.move_line_ids), 8)
self.assertRecordValues(
move.move_line_ids,
[{'quantity': 2}] +
[{'quantity': 10}] * 5 +
[{'quantity': 5}] * 2
[{'quantity_product_uom': 2}] +
[{'quantity_product_uom': 10}] * 5 +
[{'quantity_product_uom': 5}] * 2
)
def test_clean_quant_after_package_move(self):
+1 -1
View File
@@ -2410,7 +2410,7 @@ class TestStockFlow(TestStockCommon):
in_stock_picking = in_stock_move.picking_id
in_stock_picking.button_validate()
self.assertEqual(out_move.move_line_ids.quantity, 1.0, 'The out move should be reserved')
self.assertEqual(out_move.move_line_ids.quantity_product_uom, 1.0, 'The out move should be reserved')
def test_assign_done_sml_and_validate_it(self):
"""