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..097ae8f25cfa --- /dev/null +++ b/stock_quant_package_fast_move/README.rst @@ -0,0 +1,156 @@ +=================================== +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 `__ +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 **Inventory -> Overview** and select an operation type (e.g., + **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 primarily operate +from the **Packages** menu. + +And it would be much more convenient if you could move packages directly +from this menu. + +This is what this module is designed for. + +Configuration +============= + +- Go to **Inventory -> Configuration -> Settings**. +- Activate the **Packages** checkbox. +- In the **Package Move Operation**, select the operation type that + will be used for easy package transfers. Make sure that the **Move + Entire Packages** checkbox is enabled. This setting is configured for + each company separately. + +|Package Fast Move Configuration| + +.. |Package Fast Move Configuration| 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 **Inventory -> Products -> Packages**. + +- Open a package or select several packages in the list view. + **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. + +|Package Fast Move Action| + +.. |Package Fast Move Action| 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..77824dd54f62 --- /dev/null +++ b/stock_quant_package_fast_move/models/res_company.py @@ -0,0 +1,15 @@ +# 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", + domain="[('show_entire_packs', '=', True)]", + ) + use_batch_transfers = fields.Boolean() 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..26c66700811c --- /dev/null +++ b/stock_quant_package_fast_move/models/res_config_settings.py @@ -0,0 +1,15 @@ +# 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 + ) + use_batch_transfers = fields.Boolean( + related="company_id.use_batch_transfers", 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..b99f2ca1cab2 --- /dev/null +++ b/stock_quant_package_fast_move/models/stock_quant_package.py @@ -0,0 +1,165 @@ +# 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 create_batch_transfer( + self, location, destination_package, validate, picking_type_id + ): + """ + Creates a new Batch Transfer and a separate picking for each package, + adds them to the Batch Transfer, and confirms the transfer. If the "validate" + option is enabled, the batch is also validated. + + 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): If set to True, the created picking will be validated. + Otherwise it will remain in the "Ready" state. Defaults to False. + - picking_type_id (recordset of stock.picking.type): + The picking type to be used for the transfer, as configured in settings. + """ + # Create a batch transfer + batch_vals = { + "picking_type_id": picking_type_id.id, + } + batch_transfer = self.env["stock.picking.batch"].create(batch_vals) + + for package in self: + # Create a picking + picking_vals = { + "location_id": package.location_id.id, + "location_dest_id": location.id, + "picking_type_id": picking_type_id.id, + } + picking = self.env["stock.picking"].create(picking_vals) + + # Create a package_level record + package_level_vals = { + "package_id": package.id, + "package_dest_id": destination_package.id + if destination_package + else False, + "picking_id": picking.id, + "company_id": self.env.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.write({"batch_id": batch_transfer.id}) + + # Confirm the Batch Transfer + batch_transfer.action_confirm() + + if validate: + # Validate the Batch Transfer + batch_transfer.action_done() + + 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: + - picking (recordset of stock.picking): Created transfer to location. + + 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. + """ + + # 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 + use_batch_transfers = active_company.use_batch_transfers + + if use_batch_transfers: + self.create_batch_transfer( + location, destination_package, validate, picking_type_id + ) + else: + # 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, + } + self.env["stock.package_level"].create(package_level_vals) + + picking.action_confirm() + # Set the is_done flag to True for all package levels + # associated with the created picking + picking.package_level_ids.write({"is_done": True}) + if validate: + # Validate the picking + picking.button_validate() + + return picking + + 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.md b/stock_quant_package_fast_move/readme/CONFIGURE.md new file mode 100644 index 000000000000..00d2dba67704 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONFIGURE.md @@ -0,0 +1,5 @@ +- Go to **Inventory -> Configuration -> Settings**. +- Activate the **Packages** checkbox. +- In the **Package Move Operation**, select the operation type that will be used for easy package transfers. Make sure that the **Move Entire Packages** checkbox is enabled. This setting is configured for each company separately. + +![Package Fast Move Configuration](../static/img/package_fast_move_configure.png) \ No newline at end of file diff --git a/stock_quant_package_fast_move/readme/CONTEXT.md b/stock_quant_package_fast_move/readme/CONTEXT.md new file mode 100644 index 000000000000..b032cedc7c56 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONTEXT.md @@ -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 **Inventory -> Overview** and select an operation type (e.g., **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 primarily operate from the **Packages** menu. + +And it would be much more 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.md b/stock_quant_package_fast_move/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..5e1b19792453 --- /dev/null +++ b/stock_quant_package_fast_move/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- [Cetmix](https://cetmix.com/) diff --git a/stock_quant_package_fast_move/readme/DESCRIPTION.md b/stock_quant_package_fast_move/readme/DESCRIPTION.md new file mode 100644 index 000000000000..da282f3dbb0a --- /dev/null +++ b/stock_quant_package_fast_move/readme/DESCRIPTION.md @@ -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/tree/16.0/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.md b/stock_quant_package_fast_move/readme/ROADMAP.md new file mode 100644 index 000000000000..d42f46ef5d2c --- /dev/null +++ b/stock_quant_package_fast_move/readme/ROADMAP.md @@ -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.md b/stock_quant_package_fast_move/readme/USAGE.md new file mode 100644 index 000000000000..1bc5107524f4 --- /dev/null +++ b/stock_quant_package_fast_move/readme/USAGE.md @@ -0,0 +1,15 @@ +- Go to **Inventory -> Products -> Packages**. + +- Open a package or select several packages in the list view. **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. + +![Package Fast Move Action](../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/index.html b/stock_quant_package_fast_move/static/description/index.html new file mode 100644 index 000000000000..e039297d5456 --- /dev/null +++ b/stock_quant_package_fast_move/static/description/index.html @@ -0,0 +1,487 @@ + + + + + +Fast Package Move Between Locations + + + +
+

Fast Package Move Between Locations

+ + +

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

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 +module. You should configure it properly in case you want to use the +destination package option.

+

Table of contents

+ +
+

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 Inventory -> Overview and select an operation type (e.g., +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 primarily operate +from the Packages menu.

+

And it would be much more convenient if you could move packages directly +from this menu.

+

This is what this module is designed for.

+
+
+

Configuration

+
    +
  • Go to Inventory -> Configuration -> Settings.
  • +
  • Activate the Packages checkbox.
  • +
  • In the Package Move Operation, select the operation type that +will be used for easy package transfers. Make sure that the Move +Entire Packages checkbox is enabled. This setting is configured for +each company separately.
  • +
+

Package Fast Move Configuration

+
+
+

Usage

+
    +
  • Go to Inventory -> Products -> Packages.
  • +
  • Open a package or select several packages in the list view. +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.
  • +
+

Package Fast Move Action

+
+
+

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
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/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..4969b456c8cf 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..798eac4f140e 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..07676c939a49 --- /dev/null +++ b/stock_quant_package_fast_move/tests/test_stock_quant_package_fast_move.py @@ -0,0 +1,729 @@ +# 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.", + ) + + def test_stock_quant_package_fast_move_use_batch_transfers(self): + # Test use batch transfers + self.company.write({"use_batch_transfers": True}) + 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 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.", + ) + + # Check if the batch transfer is created and validated + picking_ids = ( + self.env["stock.move.line"] + .search([("result_package_id", "in", [self.package2.id, self.package3.id])]) + .picking_id + ) + batch_id = picking_ids.batch_id + self.assertTrue( + batch_id, + "The Batch Transfer is not created.", + ) + self.assertEqual( + batch_id.state, + "done", + "The state of the Batch Transfer is wrong.", + ) 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..dee959fec65a --- /dev/null +++ b/stock_quant_package_fast_move/views/res_config_settings_view.xml @@ -0,0 +1,55 @@ + + + + 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..3e865a65404a --- /dev/null +++ b/stock_quant_package_fast_move/wizard/stock_quant_package_fast_move_wizard.py @@ -0,0 +1,40 @@ +# 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) + + 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() + + +