diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 55648813cd4..b3b4fb4cd85 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -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 diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index 21704709731..0086ed9cb58 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -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') diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index ff281966ef6..4ee0bdf2a2f 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -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