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.