From e72ae1aa0c3c7fe7315393eda812ac21d5c4a33d Mon Sep 17 00:00:00 2001 From: fw-bot Date: Mon, 23 Dec 2019 16:43:13 +0000 Subject: [PATCH] [FIX] stock: assign SN on move Lines Before this commit, we can't use the Generate and Assign Serial Numbers on already existing move line. It always creates new one, even if the picking type use "Pre-fill Detailed Operations". Now, it will edit existing lines, except if we ask to generate more SN than we have unassigned move lines, or if the picking don't use the "Pre-fill Detailed Operations" option, in which cases it will still create new move line with the generated SN. Also, before this commit, paste a list of serial numbers to assign them on multiple move lines in once work only with creation of a new move line for each serial number. Now, it's also work with existing move lines. Finally, some code shared the same logic in `stock.move` in the move line onchange method and in the `_generate_serial_numbers` method. Moves this code in a new method, `_generate_serial_move_line_commands`, and call it in the two previous methods. task-2150561 closes odoo/odoo#42316 Signed-off-by: Simon Lejeune (sle) --- addons/stock/models/stock_move.py | 127 ++++++++++++------ .../tests/test_generate_serial_numbers.py | 16 ++- addons/stock/views/stock_move_views.xml | 21 +-- 3 files changed, 107 insertions(+), 57 deletions(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index f3d0ba9ffce..24fc4d79e5c 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -182,15 +182,14 @@ class StockMove(models.Model): if self.product_id: self.description_picking = self.product_id._get_description(self.picking_type_id) - @api.depends('has_tracking', 'picking_type_id.use_create_lots', 'picking_type_id.use_existing_lots', 'picking_type_id.show_reserved', 'picking_type_id.show_operations') + @api.depends('has_tracking', 'picking_type_id.use_create_lots', 'picking_type_id.use_existing_lots', 'state') def _compute_display_assign_serial(self): for move in self: move.display_assign_serial = ( move.has_tracking == 'serial' and move.state in ('partially_available', 'assigned', 'confirmed') and move.picking_type_id.use_create_lots and - not move.picking_type_id.use_existing_lots and - not move.picking_type_id.show_reserved + not move.picking_type_id.use_existing_lots ) @api.depends('picking_id.is_locked') @@ -641,23 +640,14 @@ class StockMove(models.Model): suffix = splitted[1] initial_number = int(initial_number) - move_lines_commands = [] - location_dest = self.location_dest_id._get_putaway_strategy(self.product_id) or self.location_dest_id + lot_names = [] for i in range(0, next_serial_count): - lot_name = '%s%s%s' % ( + lot_names.append('%s%s%s' % ( prefix, str(initial_number + i).zfill(padding), suffix - ) - move_lines_commands.append((0, 0, { - 'lot_name': lot_name, - 'qty_done': 1, - 'product_id': self.product_id.id, - 'product_uom_id': self.product_id.uom_id.id, - 'location_id': self.location_id.id, - 'location_dest_id': location_dest.id, - 'picking_id': self.picking_id.id, - })) + )) + move_lines_commands = self._generate_serial_move_line_commands(lot_names) self.write({'move_line_ids': move_lines_commands}) return True @@ -816,31 +806,34 @@ class StockMove(models.Model): self.name = product.partner_ref self.product_uom = product.uom_id.id - @api.onchange('move_line_nosuggest_ids') - def onchange_move_line_nosuggest_ids(self): + @api.onchange('move_line_ids', 'move_line_nosuggest_ids') + def onchange_move_line_ids(self): + if not self.picking_type_id.use_create_lots: + # This onchange manages the creation of multiple lot name. We don't + # need that if the picking type disallows the creation of new lots. + return + breaking_char = '\n' - move_lines_to_create = [] - for move_line in self.move_line_nosuggest_ids: + if self.picking_type_id.show_reserved: + move_lines = self.move_line_ids + else: + move_lines = self.move_line_nosuggest_ids + + for move_line in move_lines: # Look if the `lot_name` contains multiple values. if breaking_char in (move_line.lot_name or ''): - splitted_lines = move_line.lot_name.split(breaking_char) - splitted_lines = list(filter(lambda line: line, splitted_lines)) - move_line.lot_name = splitted_lines[0] - # For each SN line, set move ine data... - for line in splitted_lines[1:]: - move_line_data = { - 'lot_name': line, - 'qty_done': 1, - 'product_id': move_line.product_id.id, - 'product_uom_id': move_line.product_id.uom_id.id, - 'location_id': move_line.location_id.id, - 'location_dest_id': move_line.location_dest_id.id, - 'owner_id': move_line.owner_id or False, - 'package_id': move_line.package_id or False, - } - move_lines_to_create += [(0, 0, move_line_data)] - # ... then create these move lines. - self.update({'move_line_nosuggest_ids': move_lines_to_create}) + split_lines = move_line.lot_name.split(breaking_char) + split_lines = list(filter(None, split_lines)) + move_line.lot_name = split_lines[0] + move_lines_commands = self._generate_serial_move_line_commands( + split_lines[1:], + origin_move_line=move_line, + ) + if self.picking_type_id.show_reserved: + self.update({'move_line_ids': move_lines_commands}) + else: + self.update({'move_line_nosuggest_ids': move_lines_commands}) + break @api.onchange('product_uom') def onchange_product_uom(self): @@ -904,6 +897,64 @@ class StockMove(models.Model): def _assign_picking_post_process(self, new=False): pass + def _generate_serial_move_line_commands(self, lot_names, origin_move_line=None): + """Return a list of commands to update the move lines (write on + existing ones or create new ones). + Called when user want to create and assign multiple serial numbers in + one time (using the button/wizard or copy-paste a list in the field). + + :param lot_names: A list containing all serial number to assign. + :type lot_names: list + :param origin_move_line: A move line to duplicate the value from, default to None + :type origin_move_line: record of :class:`stock.move.line` + :return: A list of commands to create/update :class:`stock.move.line` + :rtype: list + """ + self.ensure_one() + + # Select the right move lines depending of the picking type configuration. + move_lines = self.env['stock.move.line'] + if self.picking_type_id.show_reserved: + move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_name) + 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) + move_line_vals = { + 'location_dest_id': location_dest.id or self.location_dest_id.id, + 'location_id': self.location_id.id, + 'product_id': self.product_id.id, + 'product_uom_id': self.product_id.uom_id.id, + 'qty_done': 1, + } + if origin_move_line: + # `owner_id` and `package_id` are taken only in the case we create + # new move lines from an existing move line. Also, updates the + # `qty_done` because it could be usefull for products tracked by lot. + move_line_vals.update({ + 'owner_id': origin_move_line.owner_id.id, + 'package_id': origin_move_line.package_id.id, + 'qty_done': origin_move_line.qty_done or 1, + }) + + move_lines_commands = [] + for lot_name in lot_names: + # We write the lot name on an existing move line (if we have still one)... + if move_lines: + move_lines_commands.append((1, move_lines[0].id, { + 'lot_name': lot_name, + 'qty_done': 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) + move_lines_commands.append((0, 0, move_line_cmd)) + return move_lines_commands + def _get_new_picking_values(self): """ return create values for new picking that will be linked with group of moves in self. diff --git a/addons/stock/tests/test_generate_serial_numbers.py b/addons/stock/tests/test_generate_serial_numbers.py index ab534983696..1c3e79454f1 100644 --- a/addons/stock/tests/test_generate_serial_numbers.py +++ b/addons/stock/tests/test_generate_serial_numbers.py @@ -17,7 +17,7 @@ class StockGenerate(SavepointCase): }) cls.uom_unit = cls.env.ref('uom.product_uom_unit') - warehouse = cls.env['stock.warehouse'].create({ + cls.warehouse = cls.env['stock.warehouse'].create({ 'name': 'Base Warehouse', 'reception_steps': 'one_step', 'delivery_steps': 'ship_only', @@ -25,11 +25,11 @@ class StockGenerate(SavepointCase): }) cls.location = cls.env['stock.location'].create({ 'name': 'Room A', - 'location_id': warehouse.lot_stock_id.id, + 'location_id': cls.warehouse.lot_stock_id.id, }) cls.location_dest = cls.env['stock.location'].create({ 'name': 'Room B', - 'location_id': warehouse.lot_stock_id.id, + 'location_id': cls.warehouse.lot_stock_id.id, }) cls.Wizard = cls.env['stock.assign.serial'] @@ -275,7 +275,12 @@ class StockGenerate(SavepointCase): has five new move lines with the right `lot_name`. """ nbre_of_lines = 10 + picking_type = self.env['stock.picking.type'].search([ + ('use_create_lots', '=', True), + ('warehouse_id', '=', self.warehouse.id) + ]) move = self.get_new_move(nbre_of_lines) + move.picking_type_id = picking_type # We must begin with a move with 10 move lines. self.assertEqual(len(move.move_line_ids), nbre_of_lines) @@ -307,7 +312,12 @@ class StockGenerate(SavepointCase): been correctly set. """ nbre_of_lines = 5 + picking_type = self.env['stock.picking.type'].search([ + ('use_create_lots', '=', True), + ('warehouse_id', '=', self.warehouse.id) + ]) move = self.get_new_move(nbre_of_lines) + move.picking_type_id = picking_type # We must begin with a move with five move lines. self.assertEqual(len(move.move_line_ids), nbre_of_lines) diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 97bb1e2d8fe..4aa69439b00 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -228,7 +228,7 @@ - + @@ -257,7 +257,10 @@ 'default_product_id': parent.product_id, }" /> - + @@ -300,20 +303,6 @@ - - stock.move.line.operations.tree - stock.move.line - 1000 - primary - - - - text - Write your SN/LN one by one or copy paste a list. - - - - stock.move.form stock.move