diff --git a/addons/stock/models/product_strategy.py b/addons/stock/models/product_strategy.py index 1a504959687..78bc95f4d56 100644 --- a/addons/stock/models/product_strategy.py +++ b/addons/stock/models/product_strategy.py @@ -89,13 +89,27 @@ class StockPutawayRule(models.Model): if not child_location_count or not self.location_out_id: self.location_out_id = self.location_in_id + @api.model_create_multi + def create(self, vals_list): + rules = super().create(vals_list) + rules._enable_show_reserved() + return rules + def write(self, vals): if 'company_id' in vals: for rule in self: if rule.company_id.id != vals['company_id']: raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one.")) + self._enable_show_reserved() return super(StockPutawayRule, self).write(vals) + def _enable_show_reserved(self): + out_locations = self.location_out_id + if out_locations: + self.env['stock.picking.type'].with_context(active_test=False)\ + .search([('default_location_dest_id', 'in', out_locations.ids)])\ + .write({'show_reserved': True}) + def _get_putaway_location(self, product, quantity=0, package=None, packaging=None, qty_by_location=None): # find package type on package or packaging package_type = self.env['stock.package.type'] diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 289f3b44381..c6f813a7d6a 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -102,12 +102,14 @@ class Location(models.Model): 'outgoing_move_line_ids.state', 'incoming_move_line_ids.state', 'outgoing_move_line_ids.product_id.weight', 'outgoing_move_line_ids.product_id.weight', 'quant_ids.quantity', 'quant_ids.product_id.weight') + @api.depends_context('exclude_sml_ids') def _compute_weight(self): for location in self: location.net_weight = 0 quants = location.quant_ids.filtered(lambda q: q.product_id.type != 'service') - incoming_move_lines = location.incoming_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel']) - outgoing_move_lines = location.outgoing_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel']) + excluded_sml_ids = self._context.get('exclude_sml_ids', []) + incoming_move_lines = location.incoming_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'] and ml.id not in excluded_sml_ids) + outgoing_move_lines = location.outgoing_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'] and ml.id not in excluded_sml_ids) for quant in quants: location.net_weight += quant.product_id.weight * quant.quantity location.forecast_weight = location.net_weight @@ -248,6 +250,7 @@ class Location(models.Model): if locations.storage_category_id: if package and package.package_type_id: move_line_data = self.env['stock.move.line']._read_group([ + ('id', 'not in', self._context.get('exclude_sml_ids', [])), ('result_package_id.package_type_id', '=', package_type.id), ('state', 'not in', ['draft', 'cancel', 'done']), ], ['result_package_id:count_distinct'], ['location_dest_id']) @@ -261,6 +264,7 @@ class Location(models.Model): qty_by_location[values['location_id'][0]] += values['package_id'] else: move_line_data = self.env['stock.move.line']._read_group([ + ('id', 'not in', self._context.get('exclude_sml_ids', [])), ('product_id', '=', product.id), ('location_dest_id', 'in', locations.ids), ('state', 'not in', ['draft', 'done', 'cancel']) @@ -329,15 +333,20 @@ class Location(models.Model): specified.""" self.ensure_one() if self.storage_category_id: - # check weight - if self.storage_category_id.max_weight < self.forecast_weight + product.weight * quantity: - return False # check if enough space if package and package.package_type_id: + # check weight + package_smls = self.env['stock.move.line'].search([('result_package_id', '=', package.id)]) + if self.storage_category_id.max_weight < self.forecast_weight + sum(package_smls.mapped(lambda sml: sml.reserved_qty * sml.product_id.weight)): + return False + # check if enough space package_capacity = self.storage_category_id.package_capacity_ids.filtered(lambda pc: pc.package_type_id == package.package_type_id) if package_capacity and location_qty >= package_capacity.quantity: return False else: + # check weight + if self.storage_category_id.max_weight < self.forecast_weight + product.weight * quantity: + return False product_capacity = self.storage_category_id.product_capacity_ids.filtered(lambda pc: pc.product_id == product) # To handle new line without quantity in order to avoid suggesting a location already full if product_capacity and location_qty >= product_capacity.quantity: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index f1ca50c762b..3aeb8ee8e07 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -355,6 +355,7 @@ class StockMove(models.Model): # do not impact reservation here move_line = self.env['stock.move.line'].create(dict(move._prepare_move_line_vals(), qty_done=quantity_done)) move.write({'move_line_ids': [(4, move_line.id)]}) + move_line._apply_putaway_strategy() elif len(move_lines) == 1: move_lines[0].qty_done = quantity_done else: @@ -1119,13 +1120,9 @@ class StockMove(models.Model): else: move_lines = self.move_line_nosuggest_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_name) - if origin_move_line: - location_dest = origin_move_line.location_dest_id - else: - location_dest = self.location_dest_id._get_putaway_strategy(self.product_id, quantity=1, packaging=self.product_packaging_id) + loc_dest = origin_move_line and origin_move_line.location_dest_id move_line_vals = { 'picking_id': self.picking_id.id, - 'location_dest_id': location_dest.id, 'location_id': self.location_id.id, 'product_id': self.product_id.id, 'product_uom_id': self.product_id.uom_id.id, @@ -1142,6 +1139,7 @@ class StockMove(models.Model): }) move_lines_commands = [] + qty_by_location = defaultdict(float) for lot_name in lot_names: # We write the lot name on an existing move line (if we have still one)... if move_lines: @@ -1149,11 +1147,14 @@ class StockMove(models.Model): 'lot_name': lot_name, 'qty_done': 1, })) + qty_by_location[move_lines[0].location_dest_id.id] += 1 move_lines = move_lines[1:] # ... or create a new move line with the serial name. else: - move_line_cmd = dict(move_line_vals, lot_name=lot_name) + loc = loc_dest or self.location_dest_id._get_putaway_strategy(self.product_id, quantity=1, packaging=self.product_packaging_id, additional_qty=qty_by_location) + move_line_cmd = dict(move_line_vals, lot_name=lot_name, location_dest_id=loc.id) move_lines_commands.append((0, 0, move_line_cmd)) + qty_by_location[loc.id] += 1 return move_lines_commands def _get_new_picking_values(self): @@ -1299,6 +1300,7 @@ class StockMove(models.Model): 'product_id': self.product_id.id, 'product_uom_id': self.product_uom.id, 'location_id': self.location_id.id, + 'location_dest_id': self.location_dest_id.id, 'picking_id': self.picking_id.id, 'company_id': self.company_id.id, } @@ -1321,9 +1323,6 @@ class StockMove(models.Model): package_id=package.id or False, owner_id =reserved_quant.owner_id.id or False, ) - # apply putaway - location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id, quantity=quantity or 0, package=package, packaging=self.product_packaging_id).id - vals['location_dest_id'] = location_dest_id return vals def _update_reserved_quantity(self, need, available_quantity, location_id, lot_id=None, package_id=None, owner_id=None, strict=True): @@ -1454,6 +1453,9 @@ class StockMove(models.Model): reserved_availability = {move: move.reserved_availability for move in self} roundings = {move: move.product_id.uom_id.rounding for move in self} move_line_vals_list = [] + # Once the quantities are assigned, we want to find a better destination location thanks + # to the putaway rules. This redirection will be applied on moves of `moves_to_redirect`. + moves_to_redirect = OrderedSet() for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']): rounding = roundings[move] missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move] @@ -1493,6 +1495,7 @@ class StockMove(models.Model): else: move_line_vals_list.append(move._prepare_move_line_vals(quantity=missing_reserved_quantity)) assigned_moves_ids.add(move.id) + moves_to_redirect.add(move.id) else: if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): assigned_moves_ids.add(move.id) @@ -1512,6 +1515,7 @@ class StockMove(models.Model): taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, package_id=forced_package_id, strict=False) if float_is_zero(taken_quantity, precision_rounding=rounding): continue + moves_to_redirect.add(move.id) if float_compare(need, taken_quantity, precision_rounding=rounding) == 0: assigned_moves_ids.add(move.id) else: @@ -1542,6 +1546,7 @@ class StockMove(models.Model): taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id) if float_is_zero(taken_quantity, precision_rounding=rounding): continue + moves_to_redirect.add(move.id) if float_is_zero(need - taken_quantity, precision_rounding=rounding): assigned_moves_ids.add(move.id) break @@ -1555,6 +1560,7 @@ class StockMove(models.Model): if self.env.context.get('bypass_entire_pack'): return self.mapped('picking_id')._check_entire_pack() + StockMove.browse(moves_to_redirect).move_line_ids._apply_putaway_strategy() def _action_cancel(self): if any(move.state == 'done' and not move.scrapped for move in self): @@ -1855,7 +1861,11 @@ class StockMove(models.Model): looking for trouble...). @param qty: quantity in the UoM of move.product_uom """ + existing_smls = self.move_line_ids self.move_line_ids = self._set_quantity_done_prepare_vals(qty) + # `_set_quantity_done_prepare_vals` may return some commands to create new SMLs + # These new SMLs need to be redirected thanks to putaway rules + (self.move_line_ids - existing_smls)._apply_putaway_strategy() def _set_quantities_to_reservation(self): for move in self: diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 0977fd29c04..c523dd95e7f 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -5,7 +5,7 @@ from collections import Counter, defaultdict from odoo import _, api, fields, tools, models from odoo.exceptions import UserError, ValidationError -from odoo.tools import OrderedSet +from odoo.tools import OrderedSet, groupby from odoo.tools.float_utils import float_compare, float_is_zero, float_round from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG @@ -203,10 +203,35 @@ class StockMoveLine(models.Model): if not self.id and self.user_has_groups('stock.group_stock_multi_locations') and self.product_id and self.qty_done: qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id) default_dest_location = self._get_default_dest_location() - additional_qty = self._get_putaway_additional_qty() - self.location_dest_id = default_dest_location._get_putaway_strategy( + self.location_dest_id = default_dest_location.with_context(exclude_sml_ids=self.ids)._get_putaway_strategy( self.product_id, quantity=qty_done, package=self.result_package_id, - packaging=self.move_id.product_packaging_id, additional_qty=additional_qty) + packaging=self.move_id.product_packaging_id) + + def _apply_putaway_strategy(self): + for package, smls in groupby(self, lambda sml: sml.result_package_id): + smls = self.env['stock.move.line'].concat(*smls) + excluded_smls = smls + if package.package_type_id: + best_loc = smls.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids)._get_putaway_strategy(self.env['product.product'], package=package) + smls.location_dest_id = smls.package_level_id.location_dest_id = best_loc + elif package: + used_locations = set() + for sml in smls: + if len(used_locations) > 1: + break + sml.location_dest_id = sml.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids)._get_putaway_strategy(sml.product_id, quantity=sml.reserved_uom_qty) + excluded_smls -= sml + used_locations.add(sml.location_dest_id) + if len(used_locations) > 1: + smls.location_dest_id = smls.move_id.location_dest_id + else: + smls.package_level_id.location_dest_id = smls.location_dest_id + else: + for sml in smls: + sml.location_dest_id = sml.move_id.location_dest_id.with_context(exclude_sml_ids=excluded_smls.ids)._get_putaway_strategy( + sml.product_id, quantity=sml.reserved_uom_qty, packaging=sml.move_id.product_packaging_id, + ) + excluded_smls -= sml def _get_default_dest_location(self): if not self.user_has_groups('stock.group_stock_storage_categories'): diff --git a/addons/stock/tests/test_generate_serial_numbers.py b/addons/stock/tests/test_generate_serial_numbers.py index 5f8ffe5c877..377c6a9a837 100644 --- a/addons/stock/tests/test_generate_serial_numbers.py +++ b/addons/stock/tests/test_generate_serial_numbers.py @@ -378,3 +378,76 @@ class StockGenerate(TransactionCase): self.assertEqual(move_line.lot_name, filtered_value_list.pop(0)) for move_line in (move.move_line_ids - move.move_line_nosuggest_ids): self.assertEqual(move_line.lot_name, False) + + def test_generate_with_putaway_02(self): + """ + Suppose a tracked-by-USN product P + Sub locations in WH/Stock + Storage Category + The Storage Category adds a capacity constraint (max 1 x P / Location) + - Plan a receipt with 2 x P + - Receive 4 x P + -> The test ensures that the destination locations are correct + """ + stock_location = self.warehouse.lot_stock_id + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_storage_categories').id)]}) + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_multi_locations').id)]}) + + # max 1 x product_serial + stor_category = self.env['stock.storage.category'].create({ + 'name': 'Super Storage Category', + 'product_capacity_ids': [(0, 0, { + 'product_id': self.product_serial.id, + 'quantity': 1, + })] + }) + + # 5 sub locations with the storage category + # (the last one should never be used) + sub_loc_01, sub_loc_02, sub_loc_03, sub_loc_04, dummy = self.env['stock.location'].create([{ + 'name': 'Sub Location %s' % i, + 'usage': 'internal', + 'location_id': stock_location.id, + 'storage_category_id': stor_category.id, + } for i in [1, 2, 3, 4, 5]]) + + self.env['stock.putaway.rule'].create({ + 'location_in_id': stock_location.id, + 'location_out_id': stock_location.id, + 'product_id': self.product_serial.id, + 'storage_category_id': stor_category.id, + }) + + # Receive 1 x P + receipt_picking = self.env['stock.picking'].create({ + 'picking_type_id': self.warehouse.in_type_id.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': stock_location.id, + }) + move = self.env['stock.move'].create({ + 'name': self.product_serial.name, + 'product_id': self.product_serial.id, + 'product_uom': self.product_serial.uom_id.id, + 'product_uom_qty': 2.0, + 'picking_id': receipt_picking.id, + 'location_id': receipt_picking.location_id.id, + 'location_dest_id': receipt_picking.location_dest_id.id, + }) + receipt_picking.action_confirm() + + self.assertEqual(move.move_line_ids[0].location_dest_id, sub_loc_01) + self.assertEqual(move.move_line_ids[1].location_dest_id, sub_loc_02) + + form_wizard = Form(self.env['stock.assign.serial'].with_context( + default_move_id=move.id, + default_next_serial_number='001', + default_next_serial_count=4, + )) + wiz = form_wizard.save() + wiz.generate_serial_numbers() + + self.assertRecordValues(move.move_line_ids, [ + {'qty_done': 1, 'lot_name': '001', 'location_dest_id': sub_loc_01.id}, + {'qty_done': 1, 'lot_name': '002', 'location_dest_id': sub_loc_02.id}, + {'qty_done': 1, 'lot_name': '003', 'location_dest_id': sub_loc_03.id}, + {'qty_done': 1, 'lot_name': '004', 'location_dest_id': sub_loc_04.id}, + ]) diff --git a/addons/stock/tests/test_packing.py b/addons/stock/tests/test_packing.py index ee6dc10869f..f693d148825 100644 --- a/addons/stock/tests/test_packing.py +++ b/addons/stock/tests/test_packing.py @@ -929,3 +929,210 @@ class TestPacking(TestPackingCommon): picking.button_validate() self.assertEqual(package_level.location_id, shelf1_location) + + def test_pack_in_receipt_two_step_multi_putaway_02(self): + """ + Suppose a product P, its weight is equal to 1kg + We have 100 x P on two pallets. + Receipt in two steps + Sub locations in WH/Stock + Storage Category + The Storage Category adds some constraints on weight/pallets capacity + """ + warehouse = self.stock_location.warehouse_id + warehouse.reception_steps = "two_steps" + self.productA.weight = 1.0 + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_storage_categories').id)]}) + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_multi_locations').id)]}) + + package_type = self.env['stock.package.type'].create({ + 'name': "Super Pallet", + }) + package_01, package_02 = self.env['stock.quant.package'].create([{ + 'name': 'Pallet %s' % i, + 'package_type_id': package_type.id, + } for i in [1, 2]]) + + # max 100kg (so 100 x P) and max 1 pallet -> we will work with pallets, + # so the pallet capacity constraint should be the effective one + stor_category = self.env['stock.storage.category'].create({ + 'name': 'Super Storage Category', + 'max_weight': 100, + 'package_capacity_ids': [(0, 0, { + 'package_type_id': package_type.id, + 'quantity': 1, + })] + }) + + # 3 sub locations with the storage category + # (the third location should never be used) + sub_loc_01, sub_loc_02, dummy = self.env['stock.location'].create([{ + 'name': 'Sub Location %s' % i, + 'usage': 'internal', + 'location_id': self.stock_location.id, + 'storage_category_id': stor_category.id, + } for i in [1, 2, 3]]) + + self.env['stock.putaway.rule'].create({ + 'location_in_id': self.stock_location.id, + 'location_out_id': self.stock_location.id, + 'package_type_ids': [(4, package_type.id)], + 'storage_category_id': stor_category.id, + }) + + # Receive 100 x P + receipt_picking = self.env['stock.picking'].create({ + 'picking_type_id': warehouse.in_type_id.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': warehouse.wh_input_stock_loc_id.id, + }) + self.env['stock.move'].create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom': self.productA.uom_id.id, + 'product_uom_qty': 100.0, + 'picking_id': receipt_picking.id, + 'location_id': receipt_picking.location_id.id, + 'location_dest_id': receipt_picking.location_dest_id.id, + }) + receipt_picking.action_confirm() + + # Distribute the products on two pallets, one with 49 x P and a second + # one with 51 x P (to easy the debugging in case of trouble) + move_form = Form(receipt_picking.move_ids, view="stock.view_stock_move_operations") + with move_form.move_line_ids.edit(0) as line: + line.qty_done = 49 + line.result_package_id = package_01 + with move_form.move_line_ids.new() as line: + line.qty_done = 51 + line.result_package_id = package_02 + move_form.save() + receipt_picking.button_validate() + + # We are in two-steps receipt -> check the internal picking + internal_picking = self.env['stock.picking'].search([], order='id desc', limit=1) + self.assertRecordValues(internal_picking.move_line_ids, [ + {'reserved_uom_qty': 51, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_01.id}, + {'reserved_uom_qty': 49, 'qty_done': 0, 'result_package_id': package_01.id, 'location_dest_id': sub_loc_02.id}, + ]) + + # Change the constraints of the storage category: + # max 75kg (so 75 x P) and max 2 pallet -> this time, the weight + # constraint should be the effective one + stor_category.max_weight = 75 + stor_category.package_capacity_ids.quantity = 2 + internal_picking.do_unreserve() + internal_picking.action_assign() + self.assertRecordValues(internal_picking.move_line_ids, [ + {'reserved_uom_qty': 51, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_01.id}, + {'reserved_uom_qty': 49, 'qty_done': 0, 'result_package_id': package_01.id, 'location_dest_id': sub_loc_02.id}, + ]) + + move_form = Form(internal_picking.move_ids, view="stock.view_stock_move_operations") + # lines order is reversed: [Pallet 02, Pallet 01] + with move_form.move_line_ids.edit(0) as line: + line.qty_done = 51 + with move_form.move_line_ids.edit(1) as line: + line.qty_done = 49 + move_form.save() + self.assertRecordValues(internal_picking.move_line_ids, [ + {'reserved_uom_qty': 51, 'qty_done': 51, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_01.id}, + {'reserved_uom_qty': 49, 'qty_done': 49, 'result_package_id': package_01.id, 'location_dest_id': sub_loc_02.id}, + ]) + + def test_pack_in_receipt_two_step_multi_putaway_03(self): + """ + Two sublocations (max 100kg, max 2 pallet) + Two products P1, P2, weight = 1kg + There are 10 x P1 on a pallet in the first sub location + Receive a pallet of 50 x P1 + 50 x P2 => because of weight constraint, should be redirected to the + second sub location + Then, same with max 200kg max 1 pallet => same result, this time because of pallet count constraint + """ + warehouse = self.stock_location.warehouse_id + warehouse.reception_steps = "two_steps" + self.productA.weight = 1.0 + self.productB.weight = 1.0 + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_storage_categories').id)]}) + self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_multi_locations').id)]}) + + package_type = self.env['stock.package.type'].create({ + 'name': "Super Pallet", + }) + package_01, package_02 = self.env['stock.quant.package'].create([{ + 'name': 'Pallet %s' % i, + 'package_type_id': package_type.id, + } for i in [1, 2]]) + + # max 100kg and max 2 pallets + stor_category = self.env['stock.storage.category'].create({ + 'name': 'Super Storage Category', + 'max_weight': 100, + 'package_capacity_ids': [(0, 0, { + 'package_type_id': package_type.id, + 'quantity': 2, + })] + }) + + # 3 sub locations with the storage category + # (the third location should never be used) + sub_loc_01, sub_loc_02, dummy = self.env['stock.location'].create([{ + 'name': 'Sub Location %s' % i, + 'usage': 'internal', + 'location_id': self.stock_location.id, + 'storage_category_id': stor_category.id, + } for i in [1, 2, 3]]) + + self.env['stock.quant']._update_available_quantity(self.productA, sub_loc_01, 10, package_id=package_01) + + self.env['stock.putaway.rule'].create({ + 'location_in_id': self.stock_location.id, + 'location_out_id': self.stock_location.id, + 'package_type_ids': [(4, package_type.id)], + 'storage_category_id': stor_category.id, + }) + + # Receive 50 x P_A and 50 x P_B + receipt_picking = self.env['stock.picking'].create({ + 'picking_type_id': warehouse.in_type_id.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': warehouse.wh_input_stock_loc_id.id, + }) + self.env['stock.move'].create([{ + 'name': p.name, + 'product_id': p.id, + 'product_uom': p.uom_id.id, + 'product_uom_qty': 50, + 'picking_id': receipt_picking.id, + 'location_id': receipt_picking.location_id.id, + 'location_dest_id': receipt_picking.location_dest_id.id, + } for p in [self.productA, self.productB]]) + receipt_picking.action_confirm() + + move_form = Form(receipt_picking.move_ids[0], view="stock.view_stock_move_operations") + with move_form.move_line_ids.edit(0) as line: + line.qty_done = 50 + line.result_package_id = package_02 + move_form.save() + move_form = Form(receipt_picking.move_ids[1], view="stock.view_stock_move_operations") + with move_form.move_line_ids.edit(0) as line: + line.qty_done = 50 + line.result_package_id = package_02 + move_form.save() + receipt_picking.button_validate() + + # We are in two-steps receipt -> check the internal picking + internal_picking = self.env['stock.picking'].search([], order='id desc', limit=1) + self.assertRecordValues(internal_picking.move_line_ids, [ + {'product_id': self.productA.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, + {'product_id': self.productB.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, + ]) + + # Change the constraints of the storage category: + # max 200kg and max 1 pallet + stor_category.max_weight = 200 + stor_category.package_capacity_ids.quantity = 1 + internal_picking.do_unreserve() + internal_picking.action_assign() + self.assertRecordValues(internal_picking.move_line_ids, [ + {'product_id': self.productA.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, + {'product_id': self.productB.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, + ])