[FIX] mrp: delivery slip mixing kits and non kits
Current behaviour: When you generate a Delivery Slip for a list of kit and non kit products with no backorder, kit and non kit products get mixed. ie: there are kit products in the section "products not associated with a kit" Steps to reproduce: 1. Create 4 products (K1,P1,P2,C1,C2) 2. Create a Bills of Materials for K1 3. Set Type as Kit 4. Add C1 and C2 as components 5. Create a sale quotation for K1, P1, P2 6. Set the quantity at 4 for all products 7. On the quotation, click on Delivery 8. In Done, put 4 for P1 and 3 for C1,C2,P2 9. Validate and select No Backorder 10. Click on Print, Delivery Slip 11. In the document: 12. P2 is in the kit section (K1) 13. C1,C2 in the "not associated with a kit" section Expected behaviour: Only C1 and C2 should be in the K1 section Only P1 and P2 should be in the "Products not associated with a kit" section opw-3568390 closes odoo/odoo#159138 X-original-commit: 5d61d7023b3b0a5e6a47ad71bb12d548c797b5f9 Signed-off-by: Steve Van Essche <svs@odoo.com> Signed-off-by: Antoine Demany (ande) <ande@odoo.com>
This commit is contained in:
@@ -76,6 +76,13 @@ class StockMoveLine(models.Model):
|
||||
move_line._log_message(production, move_line, 'mrp.track_production_move_template', vals)
|
||||
return super(StockMoveLine, self).write(vals)
|
||||
|
||||
def _get_aggregated_properties(self, move_line=False, move=False):
|
||||
aggregated_properties = super()._get_aggregated_properties(move_line, move)
|
||||
bom = aggregated_properties['move'].bom_line_id.bom_id
|
||||
aggregated_properties['bom'] = bom or False
|
||||
aggregated_properties['line_key'] += f'_{bom.id if bom else ""}'
|
||||
return aggregated_properties
|
||||
|
||||
def _get_aggregated_product_quantities(self, **kwargs):
|
||||
"""Returns dictionary of products and corresponding values of interest grouped by optional kit_name
|
||||
|
||||
@@ -88,10 +95,25 @@ class StockMoveLine(models.Model):
|
||||
"""
|
||||
aggregated_move_lines = super()._get_aggregated_product_quantities(**kwargs)
|
||||
kit_name = kwargs.get('kit_name')
|
||||
if kit_name:
|
||||
for aggregated_move_line in aggregated_move_lines:
|
||||
if aggregated_move_lines[aggregated_move_line]['description'] == kit_name:
|
||||
|
||||
to_be_removed = []
|
||||
for aggregated_move_line in aggregated_move_lines:
|
||||
bom = aggregated_move_lines[aggregated_move_line]['bom']
|
||||
is_phantom = bom.type == 'phantom' if bom else False
|
||||
if kit_name:
|
||||
product = bom.product_id or bom.product_tmpl_id if bom else False
|
||||
display_name = product.display_name if product else False
|
||||
description = aggregated_move_lines[aggregated_move_line]['description']
|
||||
if not is_phantom or display_name != kit_name:
|
||||
to_be_removed.append(aggregated_move_line)
|
||||
elif description == kit_name:
|
||||
aggregated_move_lines[aggregated_move_line]['description'] = ""
|
||||
elif not kwargs and is_phantom:
|
||||
to_be_removed.append(aggregated_move_line)
|
||||
|
||||
for move_line in to_be_removed:
|
||||
del aggregated_move_lines[move_line]
|
||||
|
||||
return aggregated_move_lines
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form
|
||||
|
||||
@@ -440,3 +441,62 @@ class TestKitPicking(common.TestMrpCommon):
|
||||
{'product_id': product.id, 'quantity': 1, 'state': 'done'},
|
||||
{'product_id': compo.id, 'quantity': 1, 'state': 'done'},
|
||||
])
|
||||
|
||||
def test_move_line_aggregated_product_quantities_with_kit(self):
|
||||
""" Test the `stock.move.line` method `_get_aggregated_product_quantities`,
|
||||
who returns data used to print delivery slips, using kits.
|
||||
"""
|
||||
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
kit, kit_component_1, kit_component_2, not_kit_1, not_kit_2 = self.env['product.product'].create([{
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'uom_id': uom_unit.id,
|
||||
} for name in ['Kit', 'Kit Component 1', 'Kit Component 2', 'Not Kit 1', 'Not Kit 2']])
|
||||
|
||||
bom_kit = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_uom_id': kit.product_tmpl_id.uom_id.id,
|
||||
'product_id': kit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
Command.create({
|
||||
'product_id': kit_component_1.id,
|
||||
'product_qty': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': kit_component_2.id,
|
||||
'product_qty': 1,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
delivery_form = Form(self.env['stock.picking'])
|
||||
delivery_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = bom_kit.product_id
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = not_kit_1
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = not_kit_2
|
||||
move.product_uom_qty = 3
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).quantity = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).quantity = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).quantity = 4
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).quantity = 2
|
||||
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()
|
||||
|
||||
aggregate_not_kit_values = delivery.move_line_ids._get_aggregated_product_quantities()
|
||||
self.assertEqual(len(aggregate_not_kit_values.keys()), 2)
|
||||
self.assertTrue(all('Not' in val for val in aggregate_not_kit_values), 'Only non kit products should be included')
|
||||
|
||||
aggregate_kit_values = delivery.move_line_ids._get_aggregated_product_quantities(kit_name=bom_kit.product_id.name)
|
||||
self.assertEqual(len(aggregate_kit_values.keys()), 2)
|
||||
self.assertTrue(all('Component' in val for val in aggregate_kit_values), 'Only kit products should be included')
|
||||
|
||||
@@ -744,6 +744,24 @@ class StockMoveLine(models.Model):
|
||||
self.env['stock.move.line'].browse(to_unlink_candidate_ids).unlink()
|
||||
move_to_reassign._action_assign()
|
||||
|
||||
def _get_aggregated_properties(self, move_line=False, move=False):
|
||||
move = move or move_line.move_id
|
||||
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:
|
||||
description = False
|
||||
product = move.product_id
|
||||
line_key = f'{product.id}_{product.display_name}_{description or ""}_{uom.id}_{move.product_packaging_id or ""}'
|
||||
return {
|
||||
'line_key': line_key,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'product_uom': uom,
|
||||
'move': move,
|
||||
'packaging': move.product_packaging_id,
|
||||
}
|
||||
|
||||
def _get_aggregated_product_quantities(self, **kwargs):
|
||||
""" Returns a dictionary of products (key = id+name+description+uom+packaging) and corresponding values of interest.
|
||||
|
||||
@@ -756,17 +774,6 @@ class StockMoveLine(models.Model):
|
||||
"""
|
||||
aggregated_move_lines = {}
|
||||
|
||||
def get_aggregated_properties(move_line=False, move=False):
|
||||
move = move or move_line.move_id
|
||||
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:
|
||||
description = False
|
||||
product = move.product_id
|
||||
line_key = f'{product.id}_{product.display_name}_{description or ""}_{uom.id}_{move.product_packaging_id or ""}'
|
||||
return (line_key, name, description, uom, move.product_packaging_id)
|
||||
|
||||
def _compute_packaging_qtys(aggregated_move_lines):
|
||||
# Needs to be computed after aggregation of line qtys
|
||||
for line in aggregated_move_lines.values():
|
||||
@@ -785,7 +792,8 @@ class StockMoveLine(models.Model):
|
||||
for move_line in self:
|
||||
if kwargs.get('except_package') and move_line.result_package_id:
|
||||
continue
|
||||
line_key, name, description, uom, packaging = get_aggregated_properties(move_line=move_line)
|
||||
aggregated_properties = self._get_aggregated_properties(move_line=move_line)
|
||||
line_key, uom = aggregated_properties['line_key'], aggregated_properties['product_uom']
|
||||
quantity = move_line.product_uom_id._compute_quantity(move_line.quantity, uom)
|
||||
if line_key not in aggregated_move_lines:
|
||||
qty_ordered = None
|
||||
@@ -794,22 +802,19 @@ class StockMoveLine(models.Model):
|
||||
# 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
|
||||
lambda ml: self._get_aggregated_properties(move=ml.move_id)['line_key'] == 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
|
||||
lambda ml: self._get_aggregated_properties(move=ml.move_id)['line_key'] == line_key and ml.id != move_line.id
|
||||
)
|
||||
qty_ordered -= sum([m.product_uom_id._compute_quantity(m.quantity, uom) for m in previous_move_lines])
|
||||
aggregated_move_lines[line_key] = {
|
||||
'name': name,
|
||||
'description': description,
|
||||
**aggregated_properties,
|
||||
'quantity': quantity,
|
||||
'qty_ordered': qty_ordered or quantity,
|
||||
'product_uom': uom,
|
||||
'product': move_line.product_id,
|
||||
'packaging': packaging,
|
||||
}
|
||||
else:
|
||||
aggregated_move_lines[line_key]['qty_ordered'] += quantity
|
||||
@@ -824,18 +829,16 @@ class StockMoveLine(models.Model):
|
||||
if not (empty_move.state == "cancel" and empty_move.product_uom_qty
|
||||
and float_is_zero(empty_move.quantity, precision_rounding=empty_move.product_uom.rounding)):
|
||||
continue
|
||||
line_key, name, description, uom, packaging = get_aggregated_properties(move=empty_move)
|
||||
aggregated_properties = self._get_aggregated_properties(move=empty_move)
|
||||
line_key = aggregated_properties['line_key']
|
||||
|
||||
if line_key not in aggregated_move_lines:
|
||||
qty_ordered = empty_move.product_uom_qty
|
||||
aggregated_move_lines[line_key] = {
|
||||
'name': name,
|
||||
'description': description,
|
||||
**aggregated_properties,
|
||||
'quantity': False,
|
||||
'qty_ordered': qty_ordered,
|
||||
'product_uom': uom,
|
||||
'product': empty_move.product_id,
|
||||
'packaging': packaging,
|
||||
}
|
||||
else:
|
||||
aggregated_move_lines[line_key]['qty_ordered'] += empty_move.product_uom_qty
|
||||
|
||||
Reference in New Issue
Block a user