diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index f7191c47b66..aa904354ab8 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -731,7 +731,7 @@ class StockMoveLine(models.Model):
def get_aggregated_properties(move_line=False, move=False):
move = move or move_line.move_id
- uom = move_line and move_line.product_uom_id or move.product_uom
+ uom = move.product_uom or move_line.product_uom_id
name = move.product_id.display_name
description = move.description_picking
if description == name or description == move.product_id.name:
@@ -748,35 +748,45 @@ class StockMoveLine(models.Model):
pickings = pickings.backorder_ids
for move_line in self:
+ if kwargs.get('except_package') and move_line.result_package_id:
+ continue
line_key, name, description, uom = get_aggregated_properties(move_line=move_line)
+ qty_done = move_line.product_uom_id._compute_quantity(move_line.qty_done, uom)
if line_key not in aggregated_move_lines:
- qty_ordered = move_line.move_id.product_uom_qty
- if backorders:
+ qty_ordered = None
+ if backorders and not kwargs.get('strict'):
+ qty_ordered = move_line.move_id.product_uom_qty
# Filters on the aggregation key (product, description and uom) to add the
# quantities delayed to backorders to retrieve the original ordered qty.
following_move_lines = backorders.move_line_ids.filtered(
lambda ml: get_aggregated_properties(move=ml.move_id)[0] == line_key
)
qty_ordered += sum(following_move_lines.move_id.mapped('product_uom_qty'))
+ # Remove the done quantities of the other move lines of the stock move
+ previous_move_lines = move_line.move_id.move_line_ids.filtered(
+ lambda ml: get_aggregated_properties(move=ml.move_id)[0] == line_key and ml.id != move_line.id
+ )
+ qty_ordered -= sum(map(lambda m: m.product_uom_id._compute_quantity(m.qty_done, uom), previous_move_lines))
aggregated_move_lines[line_key] = {'name': name,
'description': description,
- 'qty_done': move_line.qty_done,
- 'qty_ordered': qty_ordered,
+ 'qty_done': qty_done,
+ 'qty_ordered': qty_ordered or qty_done,
'product_uom': uom,
'product': move_line.product_id}
else:
- aggregated_move_lines[line_key]['qty_done'] += move_line.qty_done
+ aggregated_move_lines[line_key]['qty_ordered'] += qty_done
+ aggregated_move_lines[line_key]['qty_done'] += qty_done
# Does the same for empty move line to retrieve the ordered qty. for partially done moves
# (as they are splitted when the transfer is done and empty moves don't have move lines).
if kwargs.get('strict'):
return aggregated_move_lines
pickings = (self.picking_id | backorders)
- for empty_move in pickings.move_ids.filtered(
- lambda m: m.state == "cancel" and m.product_uom_qty
- and float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding)
- ):
+ for empty_move in pickings.move_ids:
+ if not (empty_move.state == "cancel" and empty_move.product_uom_qty
+ and float_is_zero(empty_move.quantity_done, precision_rounding=empty_move.product_uom.rounding)):
+ continue
line_key, name, description, uom = get_aggregated_properties(move=empty_move)
if line_key not in aggregated_move_lines:
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
index 3b19329da3d..67fb4f7e92a 100644
--- a/addons/stock/report/report_deliveryslip.xml
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -129,18 +129,16 @@
-
+
+
-
-
-
-
-
+
+
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index a552e06891b..c4b4ccd004b 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -5386,6 +5386,154 @@ class StockMove(TransactionCase):
self.assertEqual(aggregate_val_2['qty_ordered'], 2)
self.assertEqual(aggregate_val_2['qty_done'], 0)
+ def test_move_line_aggregated_product_quantities_duplicate_stock_move(self):
+ """ Test the `stock.move.line` method `_get_aggregated_product_quantities`,
+ which returns data used to print delivery slips, with two stock moves of the same product
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 25)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_transit_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 10,
+ 'product_uom_id': move1.product_uom.id,
+ 'picking_id': picking.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })
+ self.env['stock.move.line'].create({
+ 'move_id': move2.id,
+ 'product_id': move2.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move2.product_uom.id,
+ 'picking_id': picking.id,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })
+ aggregate_values = picking.move_line_ids._get_aggregated_product_quantities()
+ aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 15)
+ picking.button_validate()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 15)
+
+ def test_move_line_aggregated_product_quantities_two_packages(self):
+ """ Test the `stock.move.line` method `_get_aggregated_product_quantities`,
+ which returns data used to print delivery slips, with two packages
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 25)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ move1.quantity_done = 5
+ picking.action_put_in_pack() # Create a first package
+ self.assertEqual(len(picking.move_line_ids), 2)
+
+ unpacked_ml = picking.move_line_ids.filtered(lambda ml: not ml.result_package_id)
+ self.assertEqual(unpacked_ml.reserved_qty, 10)
+ unpacked_ml.qty_done = 10
+ picking.action_put_in_pack() # Create a second package
+ self.assertEqual(len(picking.move_line_ids), 2)
+
+ picking.button_validate()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 15)
+
+ aggregate_values1 = picking.move_line_ids[0]._get_aggregated_product_quantities(strict=True)
+ aggregated_val = aggregate_values1[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 10)
+
+ aggregate_values2 = picking.move_line_ids[1]._get_aggregated_product_quantities(strict=True)
+ aggregated_val = aggregate_values2[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 5)
+
+ def test_move_line_aggregated_product_quantities_incomplete_package(self):
+ """ Test the `stock.move.line` method `_get_aggregated_product_quantities`,
+ which returns data used to print delivery slips, with an incomplete order put in packages
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 25)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1.quantity_done = 5
+ picking.action_put_in_pack() # Create a package
+
+ delivery_form = Form(picking)
+ delivery = delivery_form.save()
+ delivery.action_confirm()
+
+ backorder_wizard_dict = delivery.button_validate()
+ backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
+ backorder_wizard_form.save().process_cancel_backorder() # Don't create a backorder
+
+ aggregate_values = picking.move_line_ids._get_aggregated_product_quantities()
+ aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 15)
+ self.assertEqual(aggregated_val['qty_done'], 5)
+
+ aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(strict=True)
+ aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 5)
+ self.assertEqual(aggregated_val['qty_done'], 5)
+
+ aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(except_package=True)
+ aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}']
+ self.assertEqual(aggregated_val['qty_ordered'], 10)
+ self.assertEqual(aggregated_val['qty_done'], False)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 20)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 5)
+
def test_move_sn_warning(self):
""" Check that warnings pop up when duplicate SNs added or when SN isn't in
expected location.