From 38ebd56a65339453f6fc3dc4bcda56b04a1c60a9 Mon Sep 17 00:00:00 2001 From: Devendra Ladhani Date: Wed, 19 Mar 2025 17:43:18 +0530 Subject: [PATCH] [ADD] put_in_pack_stock: Created custom put in pack module - **Custom Put in Pack Button:** - Added a button in stock move list to package the full line quantity. - Removed existing move lines before creating a new packaged one. - **Detailed Operations Wizard Update:** - Updated the detailed operations wizard from the hamburger button. - Added fields for package quantity, package size, and package type. - Integrated a "Generate Packages" button inside the wizard. - **Lot Number Handling:** - Implemented logic for both **quantity-tracked** and **lot-tracked** products. - Assigned packages correctly for each lot while distributing quantities.. - **Inventory View Enhancements:** - Added a **Group By Packaging** filter in Inventory Locations view. - Allows users to view stock grouped by packaging type. --- put_in_pack_stock/__init__.py | 2 + put_in_pack_stock/__manifest__.py | 16 +++ put_in_pack_stock/models/__init__.py | 2 + put_in_pack_stock/models/stock_move.py | 124 ++++++++++++++++++ put_in_pack_stock/models/stock_picking.py | 18 +++ put_in_pack_stock/tests/__init__.py | 1 + put_in_pack_stock/tests/test_put_in_pack.py | 97 ++++++++++++++ put_in_pack_stock/views/stock_move_views.xml | 17 +++ put_in_pack_stock/views/stock_quant_views.xml | 12 ++ .../views/view_custom_put_in_pack.xml | 12 ++ .../views/view_stock_move_put_in_pack.xml | 24 ++++ 11 files changed, 325 insertions(+) create mode 100644 put_in_pack_stock/__init__.py create mode 100644 put_in_pack_stock/__manifest__.py create mode 100644 put_in_pack_stock/models/__init__.py create mode 100644 put_in_pack_stock/models/stock_move.py create mode 100644 put_in_pack_stock/models/stock_picking.py create mode 100644 put_in_pack_stock/tests/__init__.py create mode 100644 put_in_pack_stock/tests/test_put_in_pack.py create mode 100644 put_in_pack_stock/views/stock_move_views.xml create mode 100644 put_in_pack_stock/views/stock_quant_views.xml create mode 100644 put_in_pack_stock/views/view_custom_put_in_pack.xml create mode 100644 put_in_pack_stock/views/view_stock_move_put_in_pack.xml diff --git a/put_in_pack_stock/__init__.py b/put_in_pack_stock/__init__.py new file mode 100644 index 00000000000..0ee8b5073e2 --- /dev/null +++ b/put_in_pack_stock/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import tests diff --git a/put_in_pack_stock/__manifest__.py b/put_in_pack_stock/__manifest__.py new file mode 100644 index 00000000000..705d4d44048 --- /dev/null +++ b/put_in_pack_stock/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Put In Pack -Stock', + 'category': 'Stock/PutInPack2', + 'summary': 'Full Traceability Report Demo Data', + 'author': 'Odoo S.A.', + 'depends': ['stock','purchase'], + 'description': "Custom put in pack button for Inventory Transfer", + 'license': 'LGPL-3', + 'installable': True, + 'data': [ + 'views/view_custom_put_in_pack.xml', + 'views/view_stock_move_put_in_pack.xml', + 'views/stock_move_views.xml', + 'views/stock_quant_views.xml', + ] +} diff --git a/put_in_pack_stock/models/__init__.py b/put_in_pack_stock/models/__init__.py new file mode 100644 index 00000000000..3c71fd1b863 --- /dev/null +++ b/put_in_pack_stock/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_picking +from . import stock_move diff --git a/put_in_pack_stock/models/stock_move.py b/put_in_pack_stock/models/stock_move.py new file mode 100644 index 00000000000..e6d8e3feed1 --- /dev/null +++ b/put_in_pack_stock/models/stock_move.py @@ -0,0 +1,124 @@ +from odoo import api, models, fields +from odoo.exceptions import UserError + + +class StockMove(models.Model): + _inherit = 'stock.move' + + # Boolean field to control whether the 'Put in Pack' button is visible + # based on the related picking type's configuration. + put_in_pack_toggle = fields.Boolean( + string="Put In Pack", + related="picking_type_id.put_in_pack_toggle" + ) + # Field to define the number of packages to create. + package_qty = fields.Integer( + string="Package Qty:", + help="Number of packages to create for the product.", + default=1 + ) + # Selection field to specify the type of package used (e.g., Box or Carton). + package_type = fields.Selection( + selection=[ + ('box', "Box"), + ('carton', "Carton") + ], + string="Package Type", + help="Defines the type of package used for the product." + ) + # Field to define the maximum quantity of the product that fits into one package. + package_size = fields.Integer( + string="Package Size", + help="Number of product units contained in each package.", + default=0 + ) + def make_package(self): + """Creates a new stock package with a name based on the product.""" + package = self.env['stock.quant.package'].create({ + 'name': f"{self.product_id.name} Package" + }) + return package + def make_move_line(self, package, qty_done, lot_id=False, lot_name=False): + """ + Creates a stock move line linked to the given package. + + :param package: The package where the product will be moved. + :param qty_done: The quantity of the product in the package. + :param lot_id: (Optional) Lot ID if tracking by lot. + :param lot_name: (Optional) Lot name if tracking by lot. + """ + self.env['stock.move.line'].create({ + 'move_id': self.id, + 'result_package_id': package.id, + 'qty_done': qty_done, # Uses the total quantity of the product + 'product_id': self.product_id.id, + 'product_uom_id': self.product_id.uom_id.id, + 'lot_id': lot_id, + 'lot_name': lot_name + }) + def action_custom_put_in_pack(self): + """ + Custom action to package the entire quantity of the product in one package. + - Ensures that there is available quantity to package. + - Removes existing move lines before packaging. + - Creates a new package and assigns the move line to it. + """ + self.ensure_one() + if self.product_uom_qty <= 0: + raise UserError("No quantity available to package.") + # Remove existing move lines before creating a new packaged move line + self.move_line_ids.unlink() + # Create a new package and assign the entire quantity + package = self.make_package() + self.make_move_line(package=package, qty_done=self.product_uom_qty) + def action_generate_package(self): + """ + Generates multiple packages based on package size and tracking type. + - If tracking is none, the product is split into multiple packages. + - If tracking is by lot, packages are created per lot. + """ + self.ensure_one() + # Case: No tracking (all quantities can be freely split into packages) + if self.has_tracking == 'none': + self.move_line_ids.unlink() + demand = self.product_uom_qty + correct_uom = self.product_id.uom_id.id + if not correct_uom: + raise ValueError(f"Product {self.product_id.name} does not have a valid UoM set!") + # Create the required number of packages based on demand and package size + for _ in range(self.package_qty): + if demand <= 0: + break + package = self.make_package() + qty_to_pack = min(demand, self.package_size) + self.make_move_line(package=package, qty_done=qty_to_pack) + demand -= qty_to_pack + # Case: Tracking by lot (each lot must be packaged separately) + elif self.has_tracking == 'lot': + correct_uom = self.product_id.uom_id.id + temp_store_package = [] + for line in self.move_line_ids: + lot_quantity = line.quantity + lot_id = self.id + # Split each lot quantity into separate packages + while lot_quantity: + package = self.make_package() + qty_to_pack = min(lot_quantity, self.package_size) + # Store package details before creating move lines + temp_store_package.append({ + 'lot_id': line.lot_id.id, + 'lot_name': line.lot_name, + 'result_package_id': package, + 'qty_done': qty_to_pack + }) + lot_quantity -= qty_to_pack + # Remove old move lines before creating new ones + self.move_line_ids.unlink() + # Assign products to the newly created packages + for package_data in temp_store_package: + self.make_move_line( + package=package_data['result_package_id'], + qty_done=package_data['qty_done'], + lot_id=package_data['lot_id'], + lot_name=package_data['lot_name'] + ) diff --git a/put_in_pack_stock/models/stock_picking.py b/put_in_pack_stock/models/stock_picking.py new file mode 100644 index 00000000000..1630d168dfe --- /dev/null +++ b/put_in_pack_stock/models/stock_picking.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class PickingType(models.Model): + _inherit = "stock.picking.type" + + # Boolean field to enable or disable the 'Put In Pack' feature for this picking type + put_in_pack_toggle = fields.Boolean( + string="Put In Pack" + ) + +class Picking(models.Model): + _inherit = "stock.picking" + + # Related field to get the 'Put In Pack' setting from the associated picking type + put_in_pack_toggle = fields.Boolean( + related="picking_type_id.put_in_pack_toggle" + ) diff --git a/put_in_pack_stock/tests/__init__.py b/put_in_pack_stock/tests/__init__.py new file mode 100644 index 00000000000..9b012dc05f7 --- /dev/null +++ b/put_in_pack_stock/tests/__init__.py @@ -0,0 +1 @@ +from . import test_put_in_pack diff --git a/put_in_pack_stock/tests/test_put_in_pack.py b/put_in_pack_stock/tests/test_put_in_pack.py new file mode 100644 index 00000000000..70926a4abc9 --- /dev/null +++ b/put_in_pack_stock/tests/test_put_in_pack.py @@ -0,0 +1,97 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class TestPutInPack(TransactionCase): + @classmethod + def setUpClass(cls): + super(TestPutInPack, cls).setUpClass() + + #Create a Test Product + cls.productDenim = cls.env['product.product'].create({ + 'name': 'Denim Jeans', + 'type': 'consu', + 'tracking': 'none' + }) + + cls.productKurti = cls.env['product.product'].create({ + 'name': 'Kurti', + 'type': 'consu', + 'tracking': 'lot' + }) + + # Create a Test Picking Type with 'Put In Pack' enabled + cls.picking_type = cls.env['stock.picking.type'].create({ + 'name': 'Test Picking Type', + 'sequence_code': 'outgoing', + 'put_in_pack_toggle': True + }) + + # Create a Test Picking + cls.picking = cls.env['stock.picking'].create({ + 'picking_type_id': cls.picking_type.id, + 'put_in_pack_toggle': True + }) + + # Create a Test Stock Move + cls.move_productDenim = cls.env['stock.move'].create({ + 'name': 'Denim Move', + 'product_id': cls.productDenim.id, + 'product_uom_qty': 10, + 'product_uom': cls.env.ref('uom.product_uom_unit').id, + 'location_id': cls.env.ref('stock.stock_location_stock').id, + 'location_dest_id': cls.env.ref('stock.stock_location_customers').id, + 'picking_id': cls.picking.id, + 'picking_type_id': cls.picking.picking_type_id.id, + }) + cls.move_productKurti = cls.env['stock.move'].create({ + 'name': 'Kurti Move', + 'product_id': cls.productKurti.id, + 'product_uom_qty': 10, + 'product_uom': cls.env.ref('uom.product_uom_unit').id, + 'location_id': cls.env.ref('stock.stock_location_stock').id, + 'location_dest_id': cls.env.ref('stock.stock_location_customers').id, + 'picking_id': cls.picking.id, + 'picking_type_id': cls.picking_type.id, + 'has_tracking': 'lot' + }) + + def test_action_custom_put_in_pack_productDenim(self): + """Test the custom put in pack action for Denim Jeans""" + self.move_productDenim.action_custom_put_in_pack() + self.assertTrue(self.move_productDenim.move_line_ids.result_package_id, "Denim Jeans Package was not created correctly") + + def test_action_custom_put_in_pack_productKurti(self): + """Test the custom put in pack action for Kurti""" + self.move_productKurti.action_custom_put_in_pack() + self.assertTrue(self.move_productKurti.move_line_ids.result_package_id, "Kurti Package was not created correctly") + + def test_no_quantity_to_pack(self): + """Ensure error is raised when no quantity is available to package""" + self.move_productDenim.product_uom_qty=0 + with self.assertRaises(UserError): + self.move_productDenim.action_custom_put_in_pack() + + def test_package_size_handling(self): + """Ensure package size is correctly handled when generating packages.""" + self.move_productDenim.package_size = 4 + self.move_productDenim.package_qty = 3 + self.move_productDenim.action_generate_package() + + self.assertEqual(len(self.move_productDenim.move_line_ids), 3, "Package size handling incorrect.") + + def test_multiple_packages_for_lots(self): + """Ensure that products tracked by lots generate multiple packages correctly.""" + lot = self.env['stock.lot'].create({ + 'name': 'KURTI-001', + 'product_id': self.productKurti.id, + }) + move_line = self.env['stock.move.line'].create({ + 'move_id': self.move_productKurti.id, + 'product_id': self.productKurti.id, + 'qty_done': 10, + 'lot_id': lot.id + }) + self.move_productKurti.package_size = 2 + self.move_productKurti.action_generate_package() + self.assertTrue(self.move_productKurti.move_line_ids.result_package_id, "Lot-based packaging not created.") diff --git a/put_in_pack_stock/views/stock_move_views.xml b/put_in_pack_stock/views/stock_move_views.xml new file mode 100644 index 00000000000..895b8d9479e --- /dev/null +++ b/put_in_pack_stock/views/stock_move_views.xml @@ -0,0 +1,17 @@ + + + Detailed Operation Inherit + stock.move + + + + + + + +