diff --git a/stock_vertical_lift/models/vertical_lift_operation_base.py b/stock_vertical_lift/models/vertical_lift_operation_base.py index 682d35111..d75aa437f 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_base.py +++ b/stock_vertical_lift/models/vertical_lift_operation_base.py @@ -299,7 +299,7 @@ class VerticalLiftOperationBase(models.AbstractModel): self.ensure_one() if not self.step() == "release": return - self.next_step() + return self.next_step() def _render_product_packagings(self, product): if not product: diff --git a/stock_vertical_lift/models/vertical_lift_operation_pick.py b/stock_vertical_lift/models/vertical_lift_operation_pick.py index 217a36c85..484e6abda 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_pick.py +++ b/stock_vertical_lift/models/vertical_lift_operation_pick.py @@ -105,14 +105,15 @@ class VerticalLiftOperationPick(models.Model): def button_release(self): """Release the operation, go to the next""" - super().button_release() + res = super().button_release() if self.step() == "noop": # we don't need to release (close) the tray until we have reached # the last line: the release is implicit when a next line is # fetched self.shuttle_id.release_vertical_lift_tray() # sorry not sorry - return self._rainbow_man() + res = self._rainbow_man() + return res def button_skip(self): """Skip the operation, go to the next""" diff --git a/stock_vertical_lift/models/vertical_lift_operation_put.py b/stock_vertical_lift/models/vertical_lift_operation_put.py index ca17d2666..b19ff307f 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_put.py +++ b/stock_vertical_lift/models/vertical_lift_operation_put.py @@ -172,11 +172,12 @@ class VerticalLiftOperationPut(models.Model): self.current_move_line_id.fetch_vertical_lift_tray_dest() def button_release(self): - super().button_release() + res = super().button_release() if self.count_move_lines_to_do_all() == 0: # we don't need to release (close) the tray until we have reached # the last line: the release is implicit when a next line is # fetched if the tray change self.shuttle_id.release_vertical_lift_tray() # sorry not sorry - return self._rainbow_man() + res = self._rainbow_man() + return res diff --git a/stock_vertical_lift_empty_tray_check/__init__.py b/stock_vertical_lift_empty_tray_check/__init__.py new file mode 100644 index 000000000..d4b7188d6 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import models diff --git a/stock_vertical_lift_empty_tray_check/__manifest__.py b/stock_vertical_lift_empty_tray_check/__manifest__.py new file mode 100644 index 000000000..d3365f5ec --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +{ + "name": "Vertical Lift Empty Tray Check", + "summary": "Checks if the tray is actually empty.", + "version": "13.0.1.0.0", + "category": "Stock", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["stock", "stock_vertical_lift"], + "website": "https://github.com/OCA/stock-logistics-warehouse", + "data": [ + "views/res_config_setting_views.xml", + "views/vertical_lift_operation_pick_zero_check_views.xml", + ], + "installable": True, + "development_status": "Alpha", +} diff --git a/stock_vertical_lift_empty_tray_check/models/__init__.py b/stock_vertical_lift_empty_tray_check/models/__init__.py new file mode 100644 index 000000000..18af90d40 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from . import res_config_settings +from . import vertical_lift_operation_pick +from . import vertical_lift_operation_pick_zero_check diff --git a/stock_vertical_lift_empty_tray_check/models/res_config_settings.py b/stock_vertical_lift_empty_tray_check/models/res_config_settings.py new file mode 100644 index 000000000..b780f51f1 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/models/res_config_settings.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + vertical_lift_empty_tray_check = fields.Boolean( + "Vertical lift: Check Empty Tray", + default=False, + config_parameter="vertical_lift_empty_tray_check", + ) diff --git a/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick.py b/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick.py new file mode 100644 index 000000000..ace54dc24 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick.py @@ -0,0 +1,46 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.tools import float_is_zero + + +class VerticalLiftOperationPick(models.Model): + _inherit = "vertical.lift.operation.pick" + + def button_release(self): + """Release the operation, go to the next + + By default it asks the user to inspect visually if the tray is empty. + """ + icp = self.env["ir.config_parameter"].sudo() + tray_check = icp.get_param("vertical_lift_empty_tray_check") + skip_zero_quantity_check = self.env.context.get("skip_zero_quantity_check") + if not skip_zero_quantity_check and tray_check: + uom_rounding = self.product_id.uom_id.rounding + if float_is_zero(self.tray_qty, precision_rounding=uom_rounding): + return self._check_zero_quantity() + + return super().button_release() + + def _check_zero_quantity(self): + """Show the wizard to check for real-zero quantity.""" + view = self.env.ref( + "stock_vertical_lift_empty_tray_check." + "vertical_lift_operation_pick_zero_check_view_form" + ) + wizard_model = "vertical.lift.operation.pick.zero.check" + wizard = self.env[wizard_model].create( + {"vertical_lift_operation_pick_id": self.id} + ) + return { + "name": _("Is the tray empty?"), + "type": "ir.actions.act_window", + "view_mode": "form", + "target": "new", + "views": [(view.id, "form")], + "view_id": view.id, + "res_model": wizard_model, + "res_id": wizard.id, + "context": self.env.context, + } diff --git a/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick_zero_check.py b/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick_zero_check.py new file mode 100644 index 000000000..ff34ffcf8 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/models/vertical_lift_operation_pick_zero_check.py @@ -0,0 +1,87 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import _, fields, models + + +class VerticalLiftOperationPickZeroCheck(models.TransientModel): + _name = "vertical.lift.operation.pick.zero.check" + _description = "Make sure the tray location is empty" + + vertical_lift_operation_pick_id = fields.Many2one("vertical.lift.operation.pick") + + def _get_data_from_operation(self): + """Return picking, location and product from the operation shuttle""" + operation = self.vertical_lift_operation_pick_id + + # If the move is split into several move lines, it is + # moved to another picking, being a backorder of the + # original one. We are always interested in the original + # picking that was processed at first, so if the picking + # is a backorder of another picking, we take that other one. + picking = operation.picking_id.backorder_id or operation.picking_id + location = operation.current_move_line_id.location_id + product = operation.product_id + return operation, picking, location, product + + def button_confirm_empty(self): + """User confirms the tray location is empty + + This is in accordance with what we expected, because we only + call this action if we think the location is empty. We create + an inventory adjustment that states that a zero-check was + done for this location.""" + operation, picking, location, product = self._get_data_from_operation() + inventory_name = _(f"Zero check in location: {location.complete_name}") + inventory = ( + self.env["stock.inventory"] + .sudo() + .create( + { + "name": inventory_name, + "product_ids": [(4, product.id)], + "location_ids": [(4, location.id)], + "line_ids": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_qty": 0, + "theoretical_qty": 0, + "location_id": location.id, + }, + ), + ], + } + ) + ) + inventory.action_start() + inventory.action_validate() + + # Return to the execution of the release, + # but without checking again if the tray is empty. + return operation.with_context(skip_zero_quantity_check=True).button_release() + + def button_confirm_not_empty(self): + """User confirms the tray location is not empty + + This contradicts what we expected, because we only call this + action if we think the location is empty. We create a draft + inventory adjustment stating the mismatch. + """ + operation, picking, location, product = self._get_data_from_operation() + inventory_name = _( + f"{picking.name} zero check issue on location {location.complete_name}" + ) + self.env["stock.inventory"].sudo().create( + { + "name": inventory_name, + "product_ids": [(4, product.id)], + "location_ids": [(4, location.id)], + } + ) + + # Return to the execution of the release, + # but without checking again if the tray is empty. + return operation.with_context(skip_zero_quantity_check=True).button_release() diff --git a/stock_vertical_lift_empty_tray_check/readme/CONFIGURE.rst b/stock_vertical_lift_empty_tray_check/readme/CONFIGURE.rst new file mode 100644 index 000000000..546cb6842 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +General +~~~~~~~ + +In Inventory Settings, you must have activated the option: *Check Empty Tray* diff --git a/stock_vertical_lift_empty_tray_check/readme/CONTRIBUTORS.rst b/stock_vertical_lift_empty_tray_check/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1d2f3d485 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Carlos Serra-Toro diff --git a/stock_vertical_lift_empty_tray_check/readme/DESCRIPTION.rst b/stock_vertical_lift_empty_tray_check/readme/DESCRIPTION.rst new file mode 100644 index 000000000..858f08c0b --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +When a tray is released, and the system thinks it is empty, +it prompts the user to actually check that it is empty or not. +In any case, an inventory adjustment is done stating the +situation: posted to zero if the tray is actually empty, and +set to draft is it is not empty. diff --git a/stock_vertical_lift_empty_tray_check/tests/__init__.py b/stock_vertical_lift_empty_tray_check/tests/__init__.py new file mode 100644 index 000000000..b4a9152b6 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import test_pick diff --git a/stock_vertical_lift_empty_tray_check/tests/test_pick.py b/stock_vertical_lift_empty_tray_check/tests/test_pick.py new file mode 100644 index 000000000..b0fed13d6 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/tests/test_pick.py @@ -0,0 +1,84 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo.addons.stock_vertical_lift.tests.common import VerticalLiftCase + + +class TestPick(VerticalLiftCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.picking_out = cls.env.ref( + "stock_vertical_lift.stock_picking_out_demo_vertical_lift_1" + ) + cls.env["ir.config_parameter"].sudo().set_param( + "vertical_lift_empty_tray_check", True + ) + + def _test_location_empty_common(self, operation, tray_is_empty): + """Common part for tests checking the tray location is empty + + Returns the new inventory adjustment created.""" + self.assertEqual(operation.state, "scan_destination") + move_line = operation.current_move_line_id + customers_location = self.env.ref("stock.stock_location_customers") + customers_location.barcode = "CUSTOMERS" + operation.on_barcode_scanned(customers_location.barcode) + self.assertEqual(move_line.location_dest_id, customers_location) + self.assertEqual(operation.state, "save") + operation.button_save() + self.assertEqual(operation.state, "release") + self.assertEqual(operation.tray_qty, 0) + + old_inventories = self.env["stock.inventory"].search([]) + + res_dict = operation.button_release() + wizard = self.env[(res_dict.get("res_model"))].browse(res_dict.get("res_id")) + wizard = wizard.with_context( + active_id=operation.id, active_model=operation._name + ) + if tray_is_empty: + wizard.button_confirm_empty() + else: + wizard.button_confirm_not_empty() + + new_inventory = self.env["stock.inventory"].search([]) - old_inventories + return new_inventory + + def test_location_empty_is_empty(self): + """ Location is indicated as being empty, and it is""" + operation = self._open_screen("pick") + tray_location = operation.tray_location_id + tray_product = operation.current_move_line_id.product_id + inventory = self._test_location_empty_common(operation, tray_is_empty=True) + + self.assertEqual(len(inventory), 1) + self.assertEqual(inventory.state, "done") + self.assertEqual( + inventory.name, + "Zero check in location: {}".format(tray_location.complete_name), + ) + self.assertEqual(len(inventory.line_ids), 1) + self.assertEqual(inventory.line_ids[0].product_id, tray_product) + self.assertEqual(inventory.line_ids[0].location_id, tray_location) + self.assertEqual(inventory.line_ids[0].product_qty, 0) + self.assertEqual(inventory.line_ids[0].theoretical_qty, 0) + + def test_location_empty_is_not_empty(self): + """ Location is indicated as being empty, but it is not. + """ + operation = self._open_screen("pick") + tray_location = operation.tray_location_id + tray_product = operation.current_move_line_id.product_id + inventory = self._test_location_empty_common(operation, tray_is_empty=False) + self.assertEqual(len(inventory), 1) + self.assertEqual(inventory.state, "draft") + self.assertEqual( + inventory.name, + "{} zero check issue on location {}".format( + self.picking_out.name, tray_location.complete_name, + ), + ) + self.assertEqual(inventory.product_ids, tray_product) + self.assertEqual(inventory.location_ids, tray_location) diff --git a/stock_vertical_lift_empty_tray_check/views/res_config_setting_views.xml b/stock_vertical_lift_empty_tray_check/views/res_config_setting_views.xml new file mode 100644 index 000000000..06b0e2c86 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/views/res_config_setting_views.xml @@ -0,0 +1,31 @@ + + + + + res.config.settings.view.form.inherit.stock + res.config.settings + + + + +
+
+ +
+
+
+
+
+
+
+
+
diff --git a/stock_vertical_lift_empty_tray_check/views/vertical_lift_operation_pick_zero_check_views.xml b/stock_vertical_lift_empty_tray_check/views/vertical_lift_operation_pick_zero_check_views.xml new file mode 100644 index 000000000..12395b266 --- /dev/null +++ b/stock_vertical_lift_empty_tray_check/views/vertical_lift_operation_pick_zero_check_views.xml @@ -0,0 +1,34 @@ + + + + + vertical.lift.operation.pick.zero.check.view.form + vertical.lift.operation.pick.zero.check + +
+
+
+
+
+
+
+