From d3e4fce07c6e703b0bca4fb75dc4b4b1507eab55 Mon Sep 17 00:00:00 2001 From: "Merlin (megu)" Date: Wed, 26 Jan 2022 16:54:07 +0000 Subject: [PATCH] [FIX] stock: correct total ordered quantity on delivery slip The total ordered quantities on a delivery slip with order lines of the same product was wrong + this PR https://github.com/odoo/odoo/pull/83591 was incorrect, the undelivered products should be displayed once Steps to reproduce: 1. Install the Sales and Inventory app 2. Create and confirm a sale order with two order lines, both with the same product 3. Go to the delivery and confirm it with all demands met 4. Print the delivery slip 5. The ordered quantity of the product is wrong, it should be equal to the delivered quantity Solution: Use all empty moves in package-less products `qty_ordered` = `qty_done` when in a package (the potential remaining quantities will be displayed in the package-less products or in the backorder section) or when there is no backorder (as the undelivered products will either be added to the package-less products section or the ordered quantity will be incremented later) If we are not in a package and there is a backorder, adapt the ordered quantity to remove the quantity of the packages considered before Convert `qty_done` to the UoM of the stock move opw-2722203 opw-2758840 opw-2782572 closes odoo/odoo#87072 X-original-commit: 332ec796ac77c8383a04a924508013c735f67d1e Signed-off-by: William Henrotin (whe) Signed-off-by: Guillaume Merlin (megu) --- addons/stock/models/stock_move_line.py | 30 ++-- addons/stock/report/report_deliveryslip.xml | 10 +- addons/stock/tests/test_move.py | 148 ++++++++++++++++++++ 3 files changed, 172 insertions(+), 16 deletions(-) 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.