From 59771e956a4f630db00922513afabcc245d44d72 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 4 Jan 2024 15:53:41 +0100 Subject: [PATCH] [REF] shopfloor: change lot Change lot process extracted to the module stock_move_line_change_lot --- shopfloor/__manifest__.py | 1 + shopfloor/actions/change_package_lot.py | 144 +++++------------- .../tests/test_actions_change_package_lot.py | 47 +++--- test-requirements.txt | 1 + 4 files changed, 64 insertions(+), 129 deletions(-) diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 008b91b6de8..12c91bb1a84 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -25,6 +25,7 @@ "stock_helper", "stock_picking_completion_info", # OCA / stock-logistics-workflow + "stock_move_line_change_lot", "stock_quant_package_dimension", "stock_quant_package_product_packaging", "stock_picking_progress", diff --git a/shopfloor/actions/change_package_lot.py b/shopfloor/actions/change_package_lot.py index fd44a520c59..243d9ef665c 100644 --- a/shopfloor/actions/change_package_lot.py +++ b/shopfloor/actions/change_package_lot.py @@ -1,11 +1,17 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, exceptions +from odoo import _, fields, exceptions +from odoo.exceptions import UserError from odoo.tools.float_utils import float_compare, float_is_zero from odoo.addons.component.core import Component +class InventoryError(UserError): + pass + + class ChangePackageLot(Component): """Provide methods for changing a package or a lot on a move line""" @@ -58,116 +64,44 @@ def change_lot(self, move_line, lot, response_ok_func, response_error_func): def _change_pack_lot_change_lot( self, move_line, lot, response_ok_func, response_error_func ): - def is_lesser(value, other, rounding): - return float_compare(value, other, precision_rounding=rounding) == -1 - - inventory = self._actions_for("inventory") - product = move_line.product_id - if lot.product_id != product: - return response_error_func( - move_line, message=self.msg_store.lot_on_wrong_product(lot.name) - ) previous_lot = move_line.lot_id - # Changing the lot on the move line updates the reservation on the quants - - message_parts = [] - - values = {"lot_id": lot.id} - - available_quantity = self.env["stock.quant"]._get_available_quantity( - product, move_line.location_id, lot_id=lot, strict=True - ) + previous_reserved_uom_qty = move_line.reserved_uom_qty - if move_line.package_id: - move_line.package_level_id.explode_package() - values["package_id"] = False + inventory = self._actions_for("inventory") - to_assign_moves = self.env["stock.move"] - if float_is_zero( - available_quantity, precision_rounding=product.uom_id.rounding - ): - quants = self.env["stock.quant"]._gather( - product, move_line.location_id, lot_id=lot, strict=True - ) - if quants: - # we have quants but they are all reserved by other lines: - # unreserve the other lines and reserve them again after - unreservable_lines = self.env["stock.move.line"].search( - [ - ("lot_id", "=", lot.id), - ("product_id", "=", product.id), - ("location_id", "=", move_line.location_id.id), - ("qty_done", "=", 0), - ] - ) - if not unreservable_lines: - return response_error_func( - move_line, - message=self.msg_store.cannot_change_lot_already_picked(lot), - ) - available_quantity = sum(unreservable_lines.mapped("reserved_qty")) - to_assign_moves = unreservable_lines.move_id - # if we leave the package level, it will try to reserve the same - # one again - unreservable_lines.package_level_id.explode_package() - # unreserve qties of other lines - unreservable_lines.unlink() - else: - # * we have *no* quant: - # The lot is not found at all, but the user scanned it, which means - # it's an error in the stock data! To allow the user to continue, - # we post an inventory to add the missing quantity, and a second - # draft inventory to check later - inventory.create_stock_correction( - move_line.move_id, - move_line.location_id, - self.env["stock.quant.package"].browse(), - lot, - move_line.reserved_qty, - ) - inventory.create_control_stock( - move_line.location_id, - move_line.product_id, - move_line.package_id, - move_line.lot_id, - _("Pick: stock issue on lot: {} found in {}").format( - lot.name, move_line.location_id.name - ), - ) - message_parts.append( - _("A draft inventory has been created for control.") - ) - - # re-evaluate float_is_zero because we may have changed available_quantity - if not float_is_zero( - available_quantity, precision_rounding=product.uom_id.rounding - ) and is_lesser( - available_quantity, move_line.reserved_qty, product.uom_id.rounding - ): - new_uom_qty = product.uom_id._compute_quantity( - available_quantity, move_line.product_uom_id, rounding_method="HALF-UP" + try: + with self.env.cr.savepoint(): + move_line.write({ + "lot_id": lot.id, + "package_id": False, + "result_package_id": False, + }) + rounding = move_line.product_id.uom_id.rounding + if float_is_zero(move_line.reserved_uom_qty, precision_rounding=rounding): + # The lot is not found at all, but the user scanned it, which means + # it's an error in the stock data! + raise InventoryError("Lot not available") + except InventoryError: + inventory.create_control_stock( + move_line.location_id, + move_line.product_id, + lot=move_line.lot_id, + name=_("Pick: stock issue on lot: {} found in {}").format( + lot.name, move_line.location_id.name + ), ) - values["reserved_uom_qty"] = new_uom_qty - - move_line.write(values) - - if "reserved_uom_qty" in values: - # when we change the quantity of the move, the state - # will still be "assigned" and be skipped by "_action_assign", - # recompute the state to be "partially_available" - move_line.move_id._recompute_state() - - # if the new package has less quantities, assign will create new move - # lines - move_line.move_id._action_assign() - - # Find other available goods for the lines which were using the - # lot before... - to_assign_moves._action_assign() + message = self.msg_store.cannot_change_lot_already_picked(lot) + return response_error_func(move_line, message=message) + except UserError as e: + message = { + "message_type": "error", + "body": str(e), + } + return response_error_func(move_line, message=message) message = self.msg_store.lot_replaced_by_lot(previous_lot, lot) - if message_parts: - message["body"] = "{} {}".format(message["body"], " ".join(message_parts)) + if float_compare(move_line.reserved_uom_qty, previous_reserved_uom_qty, precision_rounding=rounding) < 0: + message["body"] += " " + _("The quantity to do has changed!") return response_ok_func(move_line, message=message) def _package_content_replacement_allowed(self, package, move_line): diff --git a/shopfloor/tests/test_actions_change_package_lot.py b/shopfloor/tests/test_actions_change_package_lot.py index 93d974ed5ba..119cd2fd902 100644 --- a/shopfloor/tests/test_actions_change_package_lot.py +++ b/shopfloor/tests/test_actions_change_package_lot.py @@ -95,13 +95,13 @@ def test_change_lot_less_quantity_ok(self): new_lot = self._create_lot(self.product_a) # ensure we have our new package in the same location self._update_qty_in_location(source_location, line.product_id, 8, lot=new_lot) + expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot) + expected_message["body"] += " The quantity to do has changed!" self.change_package_lot.change_lot( line, new_lot, # success callback - lambda move_line, message=None: self.assertEqual( - message, self.msg_store.lot_replaced_by_lot(initial_lot, new_lot) - ), + lambda move_line, message=None: self.assertEqual(message, expected_message), # failure callback self.unreachable_func, ) @@ -114,12 +114,10 @@ def test_change_lot_less_quantity_ok(self): self.assert_quant_reserved_qty(line, lambda: 2, lot=initial_lot) self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot) - def test_change_lot_zero_quant_ok(self): + def test_change_lot_zero_quant_error(self): """No quant in the location for the scanned lot As the user scanned it, it's an inventory error. - We expect a new posted inventory that updates the quantity. - And another control one. """ initial_lot = self._create_lot(self.product_a) self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot) @@ -127,21 +125,20 @@ def test_change_lot_zero_quant_ok(self): picking.action_assign() line = picking.move_line_ids new_lot = self._create_lot(self.product_a) - expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot) - expected_message["body"] += " A draft inventory has been created for control." + expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot) self.change_package_lot.change_lot( line, new_lot, # success callback - lambda move_line, message=None: self.assertEqual(message, expected_message), - # failure callback self.unreachable_func, + # failure callback + lambda move_line, message=None: self.assertEqual(message, expected_message), ) - self.assertRecordValues(line, [{"lot_id": new_lot.id, "reserved_qty": 10}]) - # check that reservations have been updated - self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot) - self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot) + self.assertRecordValues(line, [{"lot_id": initial_lot.id, "reserved_qty": 10}]) + # check that reservations have not been updated + self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot) + self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot) def test_change_lot_package_explode_ok(self): """Scan a lot on units replacing a package""" @@ -247,6 +244,7 @@ def test_change_lot_reserved_partial_qty_ok(self): self.assertEqual(line2.lot_id, new_lot) expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot) + expected_message["body"] += " The quantity to do has changed!" self.change_package_lot.change_lot( line, new_lot, @@ -312,7 +310,8 @@ def test_change_lot_reserved_qty_done_error(self): self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot) self.assert_quant_reserved_qty(line2, lambda: line2.reserved_qty, lot=new_lot) - def test_change_lot_different_location_ok(self): + def test_change_lot_different_location_error(self): + "If the scanned lot is in a different location, we cannot process it" self.product_a.tracking = "lot" initial_lot = self._create_lot(self.product_a) self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot) @@ -320,23 +319,22 @@ def test_change_lot_different_location_ok(self): picking.action_assign() line = picking.move_line_ids new_lot = self._create_lot(self.product_a) - # ensure we have our new package in a different location + # ensure we have our new lot in a different location self._update_qty_in_location(self.shelf2, line.product_id, 10, lot=new_lot) - expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot) - expected_message["body"] += " A draft inventory has been created for control." + expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot) self.change_package_lot.change_lot( line, new_lot, # success callback - lambda move_line, message=None: self.assertEqual(message, expected_message), - # failure callback self.unreachable_func, + # failure callback + lambda move_line, message=None: self.assertEqual(message, expected_message), ) - self.assertRecordValues(line, [{"lot_id": new_lot.id}]) - # check that reservations have been updated - self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot) - self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot) + self.assertRecordValues(line, [{"lot_id": initial_lot.id}]) + # check that reservations have not been updated + self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot) + self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot) def test_change_lot_in_several_packages_error(self): self.product_a.tracking = "lot" @@ -588,6 +586,7 @@ def test_change_pack_different_location(self): picking = self._create_picking(lines=[(self.product_a, 10)]) picking.action_assign() line = picking.move_line_ids + self.assertEqual(line.package_id, initial_package) # when the operator wants to pick the initial package, in shelf1, the new # package is in front of the other so they want to change the package self.change_package_lot.change_package( diff --git a/test-requirements.txt b/test-requirements.txt index 7b222d1cccd..f74fe644b59 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,6 +6,7 @@ odoo-addon-shopfloor-base @ git+https://github.com/OCA/wms.git@refs/pull/642/hea # OCA/stock-logistics-workflow odoo-addon-stock-picking-progress @ git+https://github.com/OCA/stock-logistics-workflow.git@refs/pull/1330/head#subdirectory=setup/stock_picking_progress +odoo-addon-stock-move-line-change-lot @ git+https://github.com/OCA/stock-logistics-workflow.git@refs/pull/1469/head#subdirectory=setup/stock_move_line_change_lot # OCA/product-attribute odoo-addon-product-packaging-level @ git+https://github.com/OCA/product-attribute.git@refs/pull/1215/head#subdirectory=setup/product_packaging_level