[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:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user