[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) <whe@odoo.com>
Signed-off-by: Guillaume Merlin (megu) <megu@odoo.com>
This commit is contained in:
Merlin (megu)
2022-03-23 19:25:16 +01:00
parent 2f1bd13274
commit d3e4fce07c
3 changed files with 172 additions and 16 deletions
+20 -10
View File
@@ -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:
+4 -6
View File
@@ -129,18 +129,16 @@
</t>
<!-- Make sure we do another section for package-less products if they exist -->
<t t-set="move_lines" t-value="o.move_line_ids.filtered(lambda l: not l.result_package_id)"/>
<t t-if="move_lines" name="no_package_move_lines">
<t t-set="aggregated_lines" t-value="o.move_line_ids._get_aggregated_product_quantities(except_package=True)"/>
<t t-if="move_lines or aggregated_lines" name="no_package_move_lines">
<t t-call="stock.stock_report_delivery_no_package_section_line" name="no_package_section"/>
<t t-if="has_serial_number">
<tr t-foreach="move_lines" t-as="move_line">
<t t-call="stock.stock_report_delivery_has_serial_move_line"/>
</tr>
</t>
<t t-else="">
<t t-set="aggregated_lines" t-value="move_lines._get_aggregated_product_quantities()"/>
<t t-if="aggregated_lines">
<t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
</t>
<t t-elif="aggregated_lines">
<t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
</t>
</t>
</t>
+148
View File
@@ -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.