diff --git a/setup/stock_quant_package_fast_move/odoo/addons/stock_quant_package_fast_move b/setup/stock_quant_package_fast_move/odoo/addons/stock_quant_package_fast_move new file mode 120000 index 000000000000..cdad94666caa --- /dev/null +++ b/setup/stock_quant_package_fast_move/odoo/addons/stock_quant_package_fast_move @@ -0,0 +1 @@ +../../../../stock_quant_package_fast_move \ No newline at end of file diff --git a/setup/stock_quant_package_fast_move/setup.py b/setup/stock_quant_package_fast_move/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_quant_package_fast_move/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_quant_package_fast_move/README.rst b/stock_quant_package_fast_move/README.rst new file mode 100644 index 000000000000..eab37a9b37c5 --- /dev/null +++ b/stock_quant_package_fast_move/README.rst @@ -0,0 +1,134 @@ +=================================== +Fast Package Move Between Locations +=================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:53129da11b93c9b036f2a132594fc1be4de37f42f675ed6e844c0ad3136f7565 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_quant_package_fast_move + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_quant_package_fast_move + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module simplifies package movement between different warehouse locations. + +You can simply select several packages and use a wizard that will perform all the underlying inventory operations. + +NB: this module depends on the [Stock Picking Move Package to Another Package](https://github.com/OCA/stock-logistics-workflow/stock_picking_move_package_to_package) module. +You should configure it properly in case you want to use the 'destination package' option. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Imagine that you need to transfer different packages between different locations in your warehouse. + +Regular Odoo flow looks like this: + +- Go to the "Inventory -> Overview" and select an operation type (eg 'Internal Transfer') +- Create a corresponding picking +- Add packages if 'Move Entire Packages' is enabled or add all the package products +- Set quantities done +- Validate + + +However if you are working with packages you will primary operate from the "Packages" menu. + +And it would be much convenient if you could move packages directly from this menu. + +This is what this module is designed for. + +Configuration +============= + +- Go to the "Inventory -> Configuration -> Settings" +- Activate the "Packages" checkbox +- In the "Package Move Operation" select the operation type that will be used for the easy package transfers. This setting is configured for each company separately + +.. image:: https://raw.githubusercontent.com/OCA/stock-logistics-workflow/16.0/stock_quant_package_fast_move/static/img/package_fast_move_configure.png + +Usage +===== + +- Go to the "Inventory -> Products -> Packages" + +- Open a package or select several packages in the list vies. Important: all selected packages must be located in the same location + +- Open the "Action" menu and select "Move packages" + +- In the opened wizard select the destination location where you want to move the packages + +- You can select an optional destination package in the "Destination Package" field. If selected the content of the selected packages will be moved into the destination package located at the destination location. + +- If you check 'Validate', the created internal picking will be validated. Otherwise it will remain in the "Ready" state. + +- Click the "Move" button + +.. image:: https://raw.githubusercontent.com/OCA/stock-logistics-workflow/16.0/stock_quant_package_fast_move/static/img/package_fast_move_action.png + +Known issues / Roadmap +====================== + +Only packages residing in the same source location can be moved. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Cetmix + +Contributors +~~~~~~~~~~~~ + +* Cetmix + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_quant_package_fast_move/__init__.py b/stock_quant_package_fast_move/__init__.py new file mode 100644 index 000000000000..d00a5937cb8c --- /dev/null +++ b/stock_quant_package_fast_move/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/stock_quant_package_fast_move/__manifest__.py b/stock_quant_package_fast_move/__manifest__.py new file mode 100644 index 000000000000..201a43e47adf --- /dev/null +++ b/stock_quant_package_fast_move/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Fast Package Move Between Locations", + "version": "16.0.1.0.0", + "category": "Inventory/Inventory", + "summary": "Move packages between locations directly from the 'Package' menu", + "depends": ["stock_picking_move_package_to_package"], + "website": "https://github.com/OCA/stock-logistics-workflow", + "author": "Cetmix, Odoo Community Association (OCA)", + "installable": True, + "data": [ + "security/ir.model.access.csv", + "wizard/stock_quant_package_fast_move_wizard.xml", + "views/res_config_settings_view.xml", + ], + "license": "AGPL-3", +} diff --git a/stock_quant_package_fast_move/models/__init__.py b/stock_quant_package_fast_move/models/__init__.py new file mode 100644 index 000000000000..d5b6bb4f056d --- /dev/null +++ b/stock_quant_package_fast_move/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import stock_quant_package +from . import res_company +from . import res_config_settings diff --git a/stock_quant_package_fast_move/models/res_company.py b/stock_quant_package_fast_move/models/res_company.py new file mode 100644 index 000000000000..a744878b4704 --- /dev/null +++ b/stock_quant_package_fast_move/models/res_company.py @@ -0,0 +1,12 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + package_move_picking_type_id = fields.Many2one( + "stock.picking.type", string="Package Move Operation" + ) diff --git a/stock_quant_package_fast_move/models/res_config_settings.py b/stock_quant_package_fast_move/models/res_config_settings.py new file mode 100644 index 000000000000..4ad3129027a6 --- /dev/null +++ b/stock_quant_package_fast_move/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + package_move_picking_type_id = fields.Many2one( + related="company_id.package_move_picking_type_id", readonly=False + ) diff --git a/stock_quant_package_fast_move/models/stock_quant_package.py b/stock_quant_package_fast_move/models/stock_quant_package.py new file mode 100644 index 000000000000..d893bebf1737 --- /dev/null +++ b/stock_quant_package_fast_move/models/stock_quant_package.py @@ -0,0 +1,110 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, exceptions, models + + +class StockQuantPackage(models.Model): + _inherit = "stock.quant.package" + + def action_show_package_fast_move_wizard(self): + """Open wizard for fast package movement.""" + return { + "type": "ir.actions.act_window", + "name": _("Move Packages"), + "res_model": "stock.quant.package.fast.move.wizard", + "target": "new", + "view_id": self.env.ref( + "stock_quant_package_fast_move.stock_quant_package_fast_move_wizard_view_form" + ).id, + "view_mode": "form", + "context": self.env.context, + } + + def check_source_location(self): + if any(package.location_id != self[0].location_id for package in self): + raise exceptions.UserError( + _("All packages must have the same source location.") + ) + + def _move_to_location(self, location, destination_package=None, validate=False): + """ + Move packages to a specified location. + + Parameters: + - location (recordset of stock.location): The destination location. + - destination_package (recordset of stock.quant.package, optional): + Optional destination package. If provided, it must belong to the specified location. + - validate (boolean, optional): + If set to True, the created picking will be validated. + Otherwise it will remain in the "Ready" state. Defaults to False. + + Returns: + - bool: True if the move is successful. + + Raises: + - exceptions.UserError: If the source location of the packages is different, + If the destination location is the same as the current location + or if the destination package does not belong to the specified location. + """ + self.check_source_location() + + # Check if the location is different from the current location + if location == self[0].location_id and not destination_package: + raise exceptions.UserError( + _("The destination location is the same as the current location.") + ) + + # Check if the destination package belongs to the same location + # If the location_id is False it means that the package is new or empty + if ( + destination_package + and destination_package.location_id + and destination_package.location_id != location + ): + raise exceptions.UserError( + _("The destination package does not belong to the specified location.") + ) + + active_company = self.env.company + picking_type_id = active_company.package_move_picking_type_id + + # Create a picking + picking_vals = { + "location_id": self[0].location_id.id, + "location_dest_id": location.id, + "picking_type_id": picking_type_id.id, + } + picking = self.env["stock.picking"].create(picking_vals) + + for package in self: + # Create a package_level record for each package + package_level_vals = { + "package_id": package.id, + "package_dest_id": destination_package.id + if destination_package + else False, + "picking_id": picking.id, + "company_id": active_company.id, + "location_id": package.location_id.id, + "location_dest_id": location.id, + } + package_level = self.env["stock.package_level"].create(package_level_vals) + package_level.write({"is_done": True}) + + picking.action_confirm() + if validate: + # Validate the picking + picking.button_validate() + + return True + + def create_new_package(self): + """ + Create a new stock package. + + Returns: + - package (recordset of stock.quant.package): The newly created package object. + """ + package = self.env["stock.quant.package"].create({}) + return package diff --git a/stock_quant_package_fast_move/readme/CONFIGURE.rst b/stock_quant_package_fast_move/readme/CONFIGURE.rst new file mode 100644 index 000000000000..eb28bde35db5 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +- Go to the "Inventory -> Configuration -> Settings" +- Activate the "Packages" checkbox +- In the "Package Move Operation" select the operation type that will be used for the easy package transfers. This setting is configured for each company separately + +.. image:: ../static/img/package_fast_move_configure.png diff --git a/stock_quant_package_fast_move/readme/CONTEXT.rst b/stock_quant_package_fast_move/readme/CONTEXT.rst new file mode 100644 index 000000000000..0df1e68c9014 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONTEXT.rst @@ -0,0 +1,16 @@ +Imagine that you need to transfer different packages between different locations in your warehouse. + +Regular Odoo flow looks like this: + +- Go to the "Inventory -> Overview" and select an operation type (eg 'Internal Transfer') +- Create a corresponding picking +- Add packages if 'Move Entire Packages' is enabled or add all the package products +- Set quantities done +- Validate + + +However if you are working with packages you will primary operate from the "Packages" menu. + +And it would be much convenient if you could move packages directly from this menu. + +This is what this module is designed for. \ No newline at end of file diff --git a/stock_quant_package_fast_move/readme/CONTRIBUTORS.rst b/stock_quant_package_fast_move/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..b27e61c8dc42 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Cetmix \ No newline at end of file diff --git a/stock_quant_package_fast_move/readme/DESCRIPTION.rst b/stock_quant_package_fast_move/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..366ed344549a --- /dev/null +++ b/stock_quant_package_fast_move/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module simplifies package movement between different warehouse locations. + +You can simply select several packages and use a wizard that will perform all the underlying inventory operations. + +NB: this module depends on the [Stock Picking Move Package to Another Package](https://github.com/OCA/stock-logistics-workflow/stock_picking_move_package_to_package) module. +You should configure it properly in case you want to use the 'destination package' option. \ No newline at end of file diff --git a/stock_quant_package_fast_move/readme/ROADMAP.rst b/stock_quant_package_fast_move/readme/ROADMAP.rst new file mode 100644 index 000000000000..d42f46ef5d2c --- /dev/null +++ b/stock_quant_package_fast_move/readme/ROADMAP.rst @@ -0,0 +1 @@ +Only packages residing in the same source location can be moved. \ No newline at end of file diff --git a/stock_quant_package_fast_move/readme/USAGE.rst b/stock_quant_package_fast_move/readme/USAGE.rst new file mode 100644 index 000000000000..8f7ef104cd31 --- /dev/null +++ b/stock_quant_package_fast_move/readme/USAGE.rst @@ -0,0 +1,15 @@ +- Go to the "Inventory -> Products -> Packages" + +- Open a package or select several packages in the list vies. Important: all selected packages must be located in the same location + +- Open the "Action" menu and select "Move packages" + +- In the opened wizard select the destination location where you want to move the packages + +- You can select an optional destination package in the "Destination Package" field. If selected the content of the selected packages will be moved into the destination package located at the destination location. + +- If you check 'Validate', the created internal picking will be validated. Otherwise it will remain in the "Ready" state. + +- Click the "Move" button + +.. image:: ../static/img/package_fast_move_action.png \ No newline at end of file diff --git a/stock_quant_package_fast_move/security/ir.model.access.csv b/stock_quant_package_fast_move/security/ir.model.access.csv new file mode 100644 index 000000000000..0b67743a94ff --- /dev/null +++ b/stock_quant_package_fast_move/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +stock_quant_package_fast_move.access_stock_quant_package_fast_move_wizard,access_stock_quant_package_fast_move_wizard,stock_quant_package_fast_move.model_stock_quant_package_fast_move_wizard,stock.group_stock_user,1,1,1,1 diff --git a/stock_quant_package_fast_move/static/description/icon.png b/stock_quant_package_fast_move/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/stock_quant_package_fast_move/static/description/icon.png differ diff --git a/stock_quant_package_fast_move/static/description/icon.svg b/stock_quant_package_fast_move/static/description/icon.svg new file mode 100644 index 000000000000..a7a26d0932ab --- /dev/null +++ b/stock_quant_package_fast_move/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/stock_quant_package_fast_move/static/description/index.html b/stock_quant_package_fast_move/static/description/index.html new file mode 100644 index 000000000000..f92d18ab8d60 --- /dev/null +++ b/stock_quant_package_fast_move/static/description/index.html @@ -0,0 +1,124 @@ +
+
+
+

Module name

+

This module was written to extend the functionality of ... to support ... and allow you to ...

+
+
+
+ +
+
+
+

Installation

+
+
+

To install this module, you need to: +

    +
  • ...
  • +
+

+
+
+
+ + + +
+
+
+
+ +
+
+
+

Configuration

+
+
+

To configure this module, you need to: +

    +
  • ...
  • +
+

+
+
+
+ + + +
+
+
+
+ +
+
+
+

Usage

+
+
+

To use this module, you need to: +

    +
  • ...
  • +
+

+

For further information, please visit: +

+

+
+
+
+ + + +
+
+
+
+ +
+
+
+

Known issues / Roadmap

+
+
+

+

    +
  • ...
  • +
+

+
+
+
+ + + +
+
+
+
+ +
+
+
+

Credits

+
+
+

Contributors

+ +
+
+

Maintainer

+

+ This module is maintained by the OCA.
+ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
+ To contribute to this module, please visit http://odoo-community.org.
+ +

+
+
+
diff --git a/stock_quant_package_fast_move/static/img/package_fast_move_action.png b/stock_quant_package_fast_move/static/img/package_fast_move_action.png new file mode 100644 index 000000000000..00628729068b Binary files /dev/null and b/stock_quant_package_fast_move/static/img/package_fast_move_action.png differ diff --git a/stock_quant_package_fast_move/static/img/package_fast_move_configure.png b/stock_quant_package_fast_move/static/img/package_fast_move_configure.png new file mode 100644 index 000000000000..f9970febcd6e Binary files /dev/null and b/stock_quant_package_fast_move/static/img/package_fast_move_configure.png differ diff --git a/stock_quant_package_fast_move/tests/__init__.py b/stock_quant_package_fast_move/tests/__init__.py new file mode 100644 index 000000000000..f7ac9ab2bcd5 --- /dev/null +++ b/stock_quant_package_fast_move/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_stock_quant_package_fast_move diff --git a/stock_quant_package_fast_move/tests/test_stock_quant_package_fast_move.py b/stock_quant_package_fast_move/tests/test_stock_quant_package_fast_move.py new file mode 100644 index 000000000000..2f11c5e48767 --- /dev/null +++ b/stock_quant_package_fast_move/tests/test_stock_quant_package_fast_move.py @@ -0,0 +1,686 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError +from odoo.tests import TransactionCase +from odoo.tests.common import Form + + +class TestStockQuantPackageFastMove(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.ResCompany = cls.env["res.company"] + cls.StockPicking = cls.env["stock.picking"] + cls.StockPackage = cls.env["stock.quant.package"] + cls.StockQuant = cls.env["stock.quant"] + cls.Product = cls.env["product.product"] + cls.PackageLevel = cls.env["stock.package_level"] + cls.StockMoveLine = cls.env["stock.move.line"] + cls.company = cls.ResCompany.create({"name": "Company A"}) + cls.user_demo = cls.env["res.users"].create( + { + "login": "firstnametest", + "name": "User Demo", + "email": "firstnametest@example.org", + "groups_id": [ + (4, cls.env.ref("base.group_user").id), + (4, cls.env.ref("stock.group_stock_user").id), + ], + } + ) + group_stock_multi_locations = cls.env.ref("stock.group_stock_multi_locations") + group_stock_adv_location = cls.env.ref("stock.group_adv_location") + group_tracking_lot = cls.env.ref("stock.group_tracking_lot") + cls.user_demo.write( + { + "company_id": cls.company.id, + "company_ids": [(4, cls.company.id)], + "groups_id": [ + (4, group_stock_multi_locations.id, 0), + (4, group_stock_adv_location.id, 0), + (4, group_tracking_lot.id, 0), + ], + } + ) + cls.stock_location = ( + cls.env["stock.location"] + .sudo() + .search( + [("name", "=", "Stock"), ("company_id", "=", cls.company.id)], limit=1 + ) + ) + cls.child_stock_location = cls.env["stock.location"].create( + { + "name": "Test Location", + "location_id": cls.stock_location.id, + "company_id": cls.company.id, + } + ) + cls.warehouse = cls.stock_location.warehouse_id + cls.warehouse.write({"reception_steps": "two_steps"}) + cls.input_location = cls.warehouse.wh_input_stock_loc_id + cls.in_type = cls.warehouse.in_type_id + cls.in_type.write({"show_entire_packs": True}) + cls.int_type = cls.warehouse.int_type_id + cls.int_type.write({"show_entire_packs": True}) + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.company.write({"package_move_picking_type_id": cls.int_type.id}) + cls.product1 = cls.Product.create( + { + "name": "Test Product 1", + "type": "product", + "default_code": "TEST_PROD1", + "tracking": "none", + } + ) + cls.product2 = cls.Product.create( + { + "name": "Test Product 2", + "type": "product", + "default_code": "TEST_PROD2", + "tracking": "none", + } + ) + cls.product3 = cls.Product.create( + { + "name": "Test Product 3", + "type": "product", + "default_code": "TEST_PROD3", + "tracking": "lot", + } + ) + cls.product4 = cls.Product.create( + { + "name": "Test Product 4", + "type": "product", + "default_code": "TEST_PROD4", + "tracking": "serial", + } + ) + # Create a IN picking with product 1, confirm it and move it in Stock location + incoming_picking1 = ( + cls.StockPicking.with_context(default_company_id=cls.company.id) + .with_user(cls.user_demo) + .create( + { + "location_dest_id": cls.input_location.id, + "picking_type_id": cls.in_type.id, + } + ) + ) + cls.StockMoveLine.create( + { + "product_id": cls.product1.id, + "product_uom_id": cls.uom_unit.id, + "qty_done": 3.0, + "picking_id": incoming_picking1.id, + } + ) + incoming_picking1.action_put_in_pack() + incoming_picking1.action_confirm() + incoming_picking1.button_validate() + + internal_picking1 = cls.StockPicking.search( + [("origin", "=", incoming_picking1.name)] + ) + package_level1 = cls.PackageLevel.search( + [("picking_id", "=", internal_picking1.id)] + ) + package_level1.write({"is_done": True}) + internal_picking1.action_confirm() + internal_picking1.button_validate() + + cls.package1 = package_level1.package_id + + # Create a IN picking with product 2 and product 3, + # put in pack and leave it in Input location + incoming_picking2 = ( + cls.StockPicking.with_context(default_company_id=cls.company.id) + .with_user(cls.user_demo) + .create( + { + "location_dest_id": cls.input_location.id, + "picking_type_id": cls.in_type.id, + } + ) + ) + cls.StockMoveLine.create( + [ + { + "product_id": cls.product2.id, + "product_uom_id": cls.uom_unit.id, + "qty_done": 2.0, + "picking_id": incoming_picking2.id, + }, + { + "product_id": cls.product3.id, + "product_uom_id": cls.uom_unit.id, + "lot_name": "LOT/1", + "qty_done": 2.0, + "picking_id": incoming_picking2.id, + }, + ] + ) + incoming_picking2.action_put_in_pack() + incoming_picking2.action_confirm() + incoming_picking2.button_validate() + + internal_picking2 = cls.StockPicking.search( + [("origin", "=", incoming_picking2.name)] + ) + + package_level2 = cls.PackageLevel.search( + [("picking_id", "=", internal_picking2.id)] + ) + cls.package2 = package_level2.package_id + + # Unreserve + internal_picking2.do_unreserve() + internal_picking2.unlink() + + # Create a IN picking with product 4, put in pack and leave it in Input location + incoming_picking3 = ( + cls.StockPicking.with_context(default_company_id=cls.company.id) + .with_user(cls.user_demo) + .create( + { + "location_dest_id": cls.input_location.id, + "picking_type_id": cls.in_type.id, + } + ) + ) + cls.StockMoveLine.create( + [ + { + "product_id": cls.product4.id, + "product_uom_id": cls.uom_unit.id, + "qty_done": 1.0, + "lot_name": "SERIAL/1", + "picking_id": incoming_picking3.id, + } + ] + ) + incoming_picking3.action_put_in_pack() + incoming_picking3.action_confirm() + incoming_picking3.button_validate() + + internal_picking3 = cls.StockPicking.search( + [("origin", "=", incoming_picking3.name)] + ) + + package_level3 = cls.PackageLevel.search( + [("picking_id", "=", internal_picking3.id)] + ) + cls.package3 = package_level3.package_id + # Unreserve + internal_picking3.do_unreserve() + internal_picking3.unlink() + + def test_stock_quant_package_fast_move(self): + # Test if the optional field destination package is empty + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[self.package2.id, self.package3.id]) + ) as f1: + f1.location_dest_id = self.int_type.default_location_dest_id + f1.validate = True + + wizard = f1.save() + wizard.action_move() + + # Check the available product qty of product1 product2 + # product3 and product 4 after the operations + self.assertEqual( + self.product1.qty_available, + 3.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product2.qty_available, + 2.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product3.qty_available, + 2.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product4.qty_available, + 1.0, + "Total product quantity is not as expected.", + ) + + # Test if the destination package contains the content from the source package + self.assertNotIn( + self.product2, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertNotIn( + self.product3, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertNotIn( + self.product4, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + + # Check the location_id of the moved packages + self.assertEqual( + self.package2.location_id.id, + self.int_type.default_location_dest_id.id, + "Wrong location on the package.", + ) + self.assertEqual( + self.package3.location_id.id, + self.int_type.default_location_dest_id.id, + "Wrong location on the package.", + ) + + def test_stock_quant_package_fast_move_package(self): + # Test if the optional field destination package is set + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[self.package2.id, self.package3.id]) + ) as f1: + f1.location_dest_id = self.int_type.default_location_dest_id + f1.package_dest_id = self.package1 + f1.validate = True + + wizard = f1.save() + wizard.action_move() + + # Check the available product qty of product1 product2 + # product3 and product 4 after the operations + self.assertEqual( + self.product1.qty_available, + 3.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product2.qty_available, + 2.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product3.qty_available, + 2.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product4.qty_available, + 1.0, + "Total product quantity is not as expected.", + ) + + # Test if the destination package contains the content from the source packages + self.assertIn( + self.product1, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product2, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product3, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product4, + self.package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + + def test_stock_quant_package_fast_move_package_error_same_location(self): + # Check if the location is different from the current location + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[self.package2.id, self.package3.id]) + ) as f1: + f1.location_dest_id = self.in_type.default_location_dest_id + f1.validate = True + + wizard = f1.save() + with self.assertRaises(UserError): + wizard.action_move() + + def test_stock_quant_package_fast_move_package_error_package_same_location(self): + # Check if the destination package belongs to the same location + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[self.package2.id]) + ) as f1: + f1.location_dest_id = self.int_type.default_location_dest_id + f1.package_dest_id = self.package3 + f1.validate = True + + wizard = f1.save() + with self.assertRaises(UserError): + wizard.action_move() + + def test_stock_quant_package_fast_move_packages_to_child_location(self): + # Receive Package1 through all steps + # Create a IN picking with product 1, put in pack, confirm it + # and move it in Stock location + incoming_picking1 = ( + self.StockPicking.with_context(default_company_id=self.company.id) + .with_user(self.user_demo) + .create( + { + "location_dest_id": self.input_location.id, + "picking_type_id": self.in_type.id, + } + ) + ) + self.StockMoveLine.create( + { + "product_id": self.product1.id, + "product_uom_id": self.uom_unit.id, + "qty_done": 3.0, + "picking_id": incoming_picking1.id, + } + ) + incoming_picking1.action_put_in_pack() + incoming_picking1.action_confirm() + incoming_picking1.button_validate() + + internal_picking1 = self.StockPicking.search( + [("origin", "=", incoming_picking1.name)] + ) + package_level1 = self.PackageLevel.search( + [("picking_id", "=", internal_picking1.id)] + ) + package_level1.write({"is_done": True}) + internal_picking1.action_confirm() + internal_picking1.button_validate() + + package1 = package_level1.package_id + + # Receive Package2 through all steps + # Create a IN picking with product 2 and product 3, put in pack, confirm it + # and move it in Stock location + incoming_picking2 = ( + self.StockPicking.with_context(default_company_id=self.company.id) + .with_user(self.user_demo) + .create( + { + "location_dest_id": self.input_location.id, + "picking_type_id": self.in_type.id, + } + ) + ) + self.StockMoveLine.create( + [ + { + "product_id": self.product2.id, + "product_uom_id": self.uom_unit.id, + "qty_done": 3.0, + "picking_id": incoming_picking2.id, + }, + { + "product_id": self.product3.id, + "product_uom_id": self.uom_unit.id, + "lot_name": "LOT/11", + "qty_done": 2.0, + "picking_id": incoming_picking2.id, + }, + ] + ) + incoming_picking2.action_put_in_pack() + incoming_picking2.action_confirm() + incoming_picking2.button_validate() + + internal_picking2 = self.StockPicking.search( + [("origin", "=", incoming_picking2.name)] + ) + package_level2 = self.PackageLevel.search( + [("picking_id", "=", internal_picking2.id)] + ) + package_level2.write({"is_done": True}) + internal_picking2.action_confirm() + internal_picking2.button_validate() + + package2 = package_level2.package_id + + # Move Package1 to child location + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[package1.id]) + ) as f1: + f1.location_dest_id = self.child_stock_location + f1.validate = True + + wizard = f1.save() + wizard.action_move() + + # Move Package2 to the same location and select Package1 as a destination package + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[package2.id]) + ) as f2: + f2.location_dest_id = self.child_stock_location + f2.package_dest_id = package1 + f2.validate = True + + wizard = f2.save() + wizard.action_move() + + # Check the available product qty of product1 product2 and + # product3 after the operations + self.assertEqual( + self.product1.qty_available, + 6.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product2.qty_available, + 5.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product3.qty_available, + 4.0, + "Total product quantity is not as expected.", + ) + + # Test if the destination package contains the content from the source packages + self.assertIn( + self.product1, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product2, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product3, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + + def test_stock_quant_package_fast_move_packages_from_same_location(self): + # Receive Package1 through all steps + # Create a IN picking with product 1 and product 2, put in pack, confirm it + # and move it in Stock location + incoming_picking1 = ( + self.StockPicking.with_context(default_company_id=self.company.id) + .with_user(self.user_demo) + .create( + { + "location_dest_id": self.input_location.id, + "picking_type_id": self.in_type.id, + } + ) + ) + self.StockMoveLine.create( + [ + { + "product_id": self.product1.id, + "product_uom_id": self.uom_unit.id, + "qty_done": 3.0, + "picking_id": incoming_picking1.id, + }, + { + "product_id": self.product2.id, + "product_uom_id": self.uom_unit.id, + "qty_done": 3.0, + "picking_id": incoming_picking1.id, + }, + ] + ) + incoming_picking1.action_put_in_pack() + incoming_picking1.action_confirm() + incoming_picking1.button_validate() + + internal_picking1 = self.StockPicking.search( + [("origin", "=", incoming_picking1.name)] + ) + package_level1 = self.PackageLevel.search( + [("picking_id", "=", internal_picking1.id)] + ) + package_level1.write({"is_done": True}) + internal_picking1.action_confirm() + internal_picking1.button_validate() + + package1 = package_level1.package_id + + # Receive Package2 through all steps + # Create a IN picking with product 3, put in pack, confirm it + # and move it in Stock location + incoming_picking2 = ( + self.StockPicking.with_context(default_company_id=self.company.id) + .with_user(self.user_demo) + .create( + { + "location_dest_id": self.input_location.id, + "picking_type_id": self.in_type.id, + } + ) + ) + self.StockMoveLine.create( + { + "product_id": self.product3.id, + "product_uom_id": self.uom_unit.id, + "lot_name": "LOT/11", + "qty_done": 2.0, + "picking_id": incoming_picking2.id, + }, + ) + incoming_picking2.action_put_in_pack() + incoming_picking2.action_confirm() + incoming_picking2.button_validate() + + internal_picking2 = self.StockPicking.search( + [("origin", "=", incoming_picking2.name)] + ) + package_level2 = self.PackageLevel.search( + [("picking_id", "=", internal_picking2.id)] + ) + package_level2.write({"is_done": True}) + internal_picking2.action_confirm() + internal_picking2.button_validate() + + package2 = package_level2.package_id + + # Move Package2 content to Package1 + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[package2.id]) + ) as f1: + f1.location_dest_id = self.stock_location + f1.package_dest_id = package1 + f1.validate = True + + wizard = f1.save() + wizard.action_move() + + # Check the available product qty of product1 product2 and + # product3 after the operations + self.assertEqual( + self.product1.qty_available, + 6.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product2.qty_available, + 5.0, + "Total product quantity is not as expected.", + ) + self.assertEqual( + self.product3.qty_available, + 4.0, + "Total product quantity is not as expected.", + ) + + # Test if the destination package contains the content from the source packages + self.assertIn( + self.product1, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product2, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + self.assertIn( + self.product3, + package1.quant_ids.mapped("product_id"), + msg="Product not found in the destination package.", + ) + + def test_stock_quant_package_fast_move_no_picking_validation(self): + # Test no picking validation + with Form( + self.env["stock.quant.package.fast.move.wizard"] + .with_company(self.company.id) + .with_context(active_ids=[self.package2.id, self.package3.id]) + ) as f1: + f1.location_dest_id = self.int_type.default_location_dest_id + + wizard = f1.save() + wizard.action_move() + + # Check the location_id; it should remain unchanged. + self.assertNotEqual( + self.package2.location_id.id, + self.int_type.default_location_dest_id.id, + "Wrong location on the package.", + ) + self.assertNotEqual( + self.package3.location_id.id, + self.int_type.default_location_dest_id.id, + "Wrong location on the package.", + ) + # Check that the state of the created internal picking is set to 'assigned'. + picking_ids = ( + self.env["stock.move.line"] + .search([("result_package_id", "in", [self.package2.id, self.package3.id])]) + .picking_id + ) + picking_id = picking_ids.filtered( + lambda picking: picking.picking_type_code == "internal" + ) + self.assertEqual( + picking_id.state, + "assigned", + "Wrong picking state.", + ) diff --git a/stock_quant_package_fast_move/views/res_config_settings_view.xml b/stock_quant_package_fast_move/views/res_config_settings_view.xml new file mode 100644 index 000000000000..c2f0983e4fe6 --- /dev/null +++ b/stock_quant_package_fast_move/views/res_config_settings_view.xml @@ -0,0 +1,33 @@ + + + + stock.quant.package.fast.move.res.config.settings.view.form + res.config.settings + + + +
+
+
+
+
+
+
+
diff --git a/stock_quant_package_fast_move/wizard/__init__.py b/stock_quant_package_fast_move/wizard/__init__.py new file mode 100644 index 000000000000..fc7dc11b204c --- /dev/null +++ b/stock_quant_package_fast_move/wizard/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import stock_quant_package_fast_move_wizard diff --git a/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.py b/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.py new file mode 100644 index 000000000000..5116be2ccff5 --- /dev/null +++ b/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.py @@ -0,0 +1,41 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class StockQuantPackageFastMoveWizard(models.TransientModel): + _name = "stock.quant.package.fast.move.wizard" + _description = "Stock Quant Package Fast Move Wizard" + + package_dest_id = fields.Many2one( + "stock.quant.package", + "Destination Package", + domain="[('location_id', '=', location_dest_id)]", + ) + location_dest_id = fields.Many2one( + "stock.location", "Destination Location", required=True + ) + put_in_new_package = fields.Boolean("Put in New Package") + validate = fields.Boolean() + + def action_move(self): + """ + Execute the package movement based on the specified parameters. + """ + package_ids = self._context.get("active_ids") + packages = self.env["stock.quant.package"].browse(package_ids) + packages.check_source_location() + + if self.put_in_new_package: + destination_package_id = packages.create_new_package() + else: + destination_package_id = self.package_dest_id + + packages._move_to_location( + self.location_dest_id, + destination_package=destination_package_id, + validate=self.validate, + ) + + return {"type": "ir.actions.act_window_close"} diff --git a/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.xml b/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.xml new file mode 100644 index 000000000000..2d6dc80d5cc9 --- /dev/null +++ b/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.xml @@ -0,0 +1,41 @@ + + + + + stock.quant.package.fast.move.wizard.form + stock.quant.package.fast.move.wizard + +
+ + + + + + +
+
+
+
+
+ + + Move Packages + ir.actions.server + + + code + + action = model.action_show_package_fast_move_wizard() + + +
diff --git a/test-requirements.txt b/test-requirements.txt index 4ad8e0eceaa8..d4755be7b0e6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ odoo-test-helper +odoo-addon-stock_picking_move_package_to_package @ git+https://github.com/OCA/stock-logistics-workflow@refs/pull/1422/head#subdirectory=setup/stock_picking_move_package_to_package