Skip to content

Commit 38ebd56

Browse files
committed
[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.
1 parent 4c650f3 commit 38ebd56

11 files changed

+325
-0
lines changed

Diff for: put_in_pack_stock/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import tests

Diff for: put_in_pack_stock/__manifest__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
'name': 'Put In Pack -Stock',
3+
'category': 'Stock/PutInPack2',
4+
'summary': 'Full Traceability Report Demo Data',
5+
'author': 'Odoo S.A.',
6+
'depends': ['stock','purchase'],
7+
'description': "Custom put in pack button for Inventory Transfer",
8+
'license': 'LGPL-3',
9+
'installable': True,
10+
'data': [
11+
'views/view_custom_put_in_pack.xml',
12+
'views/view_stock_move_put_in_pack.xml',
13+
'views/stock_move_views.xml',
14+
'views/stock_quant_views.xml',
15+
]
16+
}

Diff for: put_in_pack_stock/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import stock_picking
2+
from . import stock_move

Diff for: put_in_pack_stock/models/stock_move.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from odoo import api, models, fields
2+
from odoo.exceptions import UserError
3+
4+
5+
class StockMove(models.Model):
6+
_inherit = 'stock.move'
7+
8+
# Boolean field to control whether the 'Put in Pack' button is visible
9+
# based on the related picking type's configuration.
10+
put_in_pack_toggle = fields.Boolean(
11+
string="Put In Pack",
12+
related="picking_type_id.put_in_pack_toggle"
13+
)
14+
# Field to define the number of packages to create.
15+
package_qty = fields.Integer(
16+
string="Package Qty:",
17+
help="Number of packages to create for the product.",
18+
default=1
19+
)
20+
# Selection field to specify the type of package used (e.g., Box or Carton).
21+
package_type = fields.Selection(
22+
selection=[
23+
('box', "Box"),
24+
('carton', "Carton")
25+
],
26+
string="Package Type",
27+
help="Defines the type of package used for the product."
28+
)
29+
# Field to define the maximum quantity of the product that fits into one package.
30+
package_size = fields.Integer(
31+
string="Package Size",
32+
help="Number of product units contained in each package.",
33+
default=0
34+
)
35+
def make_package(self):
36+
"""Creates a new stock package with a name based on the product."""
37+
package = self.env['stock.quant.package'].create({
38+
'name': f"{self.product_id.name} Package"
39+
})
40+
return package
41+
def make_move_line(self, package, qty_done, lot_id=False, lot_name=False):
42+
"""
43+
Creates a stock move line linked to the given package.
44+
45+
:param package: The package where the product will be moved.
46+
:param qty_done: The quantity of the product in the package.
47+
:param lot_id: (Optional) Lot ID if tracking by lot.
48+
:param lot_name: (Optional) Lot name if tracking by lot.
49+
"""
50+
self.env['stock.move.line'].create({
51+
'move_id': self.id,
52+
'result_package_id': package.id,
53+
'qty_done': qty_done, # Uses the total quantity of the product
54+
'product_id': self.product_id.id,
55+
'product_uom_id': self.product_id.uom_id.id,
56+
'lot_id': lot_id,
57+
'lot_name': lot_name
58+
})
59+
def action_custom_put_in_pack(self):
60+
"""
61+
Custom action to package the entire quantity of the product in one package.
62+
- Ensures that there is available quantity to package.
63+
- Removes existing move lines before packaging.
64+
- Creates a new package and assigns the move line to it.
65+
"""
66+
self.ensure_one()
67+
if self.product_uom_qty <= 0:
68+
raise UserError("No quantity available to package.")
69+
# Remove existing move lines before creating a new packaged move line
70+
self.move_line_ids.unlink()
71+
# Create a new package and assign the entire quantity
72+
package = self.make_package()
73+
self.make_move_line(package=package, qty_done=self.product_uom_qty)
74+
def action_generate_package(self):
75+
"""
76+
Generates multiple packages based on package size and tracking type.
77+
- If tracking is none, the product is split into multiple packages.
78+
- If tracking is by lot, packages are created per lot.
79+
"""
80+
self.ensure_one()
81+
# Case: No tracking (all quantities can be freely split into packages)
82+
if self.has_tracking == 'none':
83+
self.move_line_ids.unlink()
84+
demand = self.product_uom_qty
85+
correct_uom = self.product_id.uom_id.id
86+
if not correct_uom:
87+
raise ValueError(f"Product {self.product_id.name} does not have a valid UoM set!")
88+
# Create the required number of packages based on demand and package size
89+
for _ in range(self.package_qty):
90+
if demand <= 0:
91+
break
92+
package = self.make_package()
93+
qty_to_pack = min(demand, self.package_size)
94+
self.make_move_line(package=package, qty_done=qty_to_pack)
95+
demand -= qty_to_pack
96+
# Case: Tracking by lot (each lot must be packaged separately)
97+
elif self.has_tracking == 'lot':
98+
correct_uom = self.product_id.uom_id.id
99+
temp_store_package = []
100+
for line in self.move_line_ids:
101+
lot_quantity = line.quantity
102+
lot_id = self.id
103+
# Split each lot quantity into separate packages
104+
while lot_quantity:
105+
package = self.make_package()
106+
qty_to_pack = min(lot_quantity, self.package_size)
107+
# Store package details before creating move lines
108+
temp_store_package.append({
109+
'lot_id': line.lot_id.id,
110+
'lot_name': line.lot_name,
111+
'result_package_id': package,
112+
'qty_done': qty_to_pack
113+
})
114+
lot_quantity -= qty_to_pack
115+
# Remove old move lines before creating new ones
116+
self.move_line_ids.unlink()
117+
# Assign products to the newly created packages
118+
for package_data in temp_store_package:
119+
self.make_move_line(
120+
package=package_data['result_package_id'],
121+
qty_done=package_data['qty_done'],
122+
lot_id=package_data['lot_id'],
123+
lot_name=package_data['lot_name']
124+
)

Diff for: put_in_pack_stock/models/stock_picking.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from odoo import fields, models
2+
3+
4+
class PickingType(models.Model):
5+
_inherit = "stock.picking.type"
6+
7+
# Boolean field to enable or disable the 'Put In Pack' feature for this picking type
8+
put_in_pack_toggle = fields.Boolean(
9+
string="Put In Pack"
10+
)
11+
12+
class Picking(models.Model):
13+
_inherit = "stock.picking"
14+
15+
# Related field to get the 'Put In Pack' setting from the associated picking type
16+
put_in_pack_toggle = fields.Boolean(
17+
related="picking_type_id.put_in_pack_toggle"
18+
)

Diff for: put_in_pack_stock/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_put_in_pack

Diff for: put_in_pack_stock/tests/test_put_in_pack.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from odoo.tests.common import TransactionCase
2+
from odoo.exceptions import UserError
3+
4+
5+
class TestPutInPack(TransactionCase):
6+
@classmethod
7+
def setUpClass(cls):
8+
super(TestPutInPack, cls).setUpClass()
9+
10+
#Create a Test Product
11+
cls.productDenim = cls.env['product.product'].create({
12+
'name': 'Denim Jeans',
13+
'type': 'consu',
14+
'tracking': 'none'
15+
})
16+
17+
cls.productKurti = cls.env['product.product'].create({
18+
'name': 'Kurti',
19+
'type': 'consu',
20+
'tracking': 'lot'
21+
})
22+
23+
# Create a Test Picking Type with 'Put In Pack' enabled
24+
cls.picking_type = cls.env['stock.picking.type'].create({
25+
'name': 'Test Picking Type',
26+
'sequence_code': 'outgoing',
27+
'put_in_pack_toggle': True
28+
})
29+
30+
# Create a Test Picking
31+
cls.picking = cls.env['stock.picking'].create({
32+
'picking_type_id': cls.picking_type.id,
33+
'put_in_pack_toggle': True
34+
})
35+
36+
# Create a Test Stock Move
37+
cls.move_productDenim = cls.env['stock.move'].create({
38+
'name': 'Denim Move',
39+
'product_id': cls.productDenim.id,
40+
'product_uom_qty': 10,
41+
'product_uom': cls.env.ref('uom.product_uom_unit').id,
42+
'location_id': cls.env.ref('stock.stock_location_stock').id,
43+
'location_dest_id': cls.env.ref('stock.stock_location_customers').id,
44+
'picking_id': cls.picking.id,
45+
'picking_type_id': cls.picking.picking_type_id.id,
46+
})
47+
cls.move_productKurti = cls.env['stock.move'].create({
48+
'name': 'Kurti Move',
49+
'product_id': cls.productKurti.id,
50+
'product_uom_qty': 10,
51+
'product_uom': cls.env.ref('uom.product_uom_unit').id,
52+
'location_id': cls.env.ref('stock.stock_location_stock').id,
53+
'location_dest_id': cls.env.ref('stock.stock_location_customers').id,
54+
'picking_id': cls.picking.id,
55+
'picking_type_id': cls.picking_type.id,
56+
'has_tracking': 'lot'
57+
})
58+
59+
def test_action_custom_put_in_pack_productDenim(self):
60+
"""Test the custom put in pack action for Denim Jeans"""
61+
self.move_productDenim.action_custom_put_in_pack()
62+
self.assertTrue(self.move_productDenim.move_line_ids.result_package_id, "Denim Jeans Package was not created correctly")
63+
64+
def test_action_custom_put_in_pack_productKurti(self):
65+
"""Test the custom put in pack action for Kurti"""
66+
self.move_productKurti.action_custom_put_in_pack()
67+
self.assertTrue(self.move_productKurti.move_line_ids.result_package_id, "Kurti Package was not created correctly")
68+
69+
def test_no_quantity_to_pack(self):
70+
"""Ensure error is raised when no quantity is available to package"""
71+
self.move_productDenim.product_uom_qty=0
72+
with self.assertRaises(UserError):
73+
self.move_productDenim.action_custom_put_in_pack()
74+
75+
def test_package_size_handling(self):
76+
"""Ensure package size is correctly handled when generating packages."""
77+
self.move_productDenim.package_size = 4
78+
self.move_productDenim.package_qty = 3
79+
self.move_productDenim.action_generate_package()
80+
81+
self.assertEqual(len(self.move_productDenim.move_line_ids), 3, "Package size handling incorrect.")
82+
83+
def test_multiple_packages_for_lots(self):
84+
"""Ensure that products tracked by lots generate multiple packages correctly."""
85+
lot = self.env['stock.lot'].create({
86+
'name': 'KURTI-001',
87+
'product_id': self.productKurti.id,
88+
})
89+
move_line = self.env['stock.move.line'].create({
90+
'move_id': self.move_productKurti.id,
91+
'product_id': self.productKurti.id,
92+
'qty_done': 10,
93+
'lot_id': lot.id
94+
})
95+
self.move_productKurti.package_size = 2
96+
self.move_productKurti.action_generate_package()
97+
self.assertTrue(self.move_productKurti.move_line_ids.result_package_id, "Lot-based packaging not created.")

Diff for: put_in_pack_stock/views/stock_move_views.xml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<odoo>
2+
<record id="view_detailed_operation_custom" model="ir.ui.view">
3+
<field name="name">Detailed Operation Inherit</field>
4+
<field name="model">stock.move</field>
5+
<field name="inherit_id" ref="stock.view_stock_move_operations"/>
6+
<field name="arch" type="xml">
7+
<xpath expr="//form/group/group" position="after">
8+
<group invisible="not put_in_pack_toggle">
9+
<field name="package_qty" invisible="has_tracking not in ('none')"/>
10+
<field name="package_size"/>
11+
<field name="package_type"/>
12+
<button name="action_generate_package" type="object" class="btn-primary" string="Generate Packages"/>
13+
</group>
14+
</xpath>
15+
</field>
16+
</record>
17+
</odoo>

Diff for: put_in_pack_stock/views/stock_quant_views.xml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<odoo>
2+
<record id="view_stock_quant_tree_inherit" model="ir.ui.view">
3+
<field name="name">stock.quant.tree.inherit</field>
4+
<field name="model">stock.quant</field>
5+
<field name="inherit_id" ref="stock.quant_search_view"/>
6+
<field name="arch" type="xml">
7+
<xpath expr="//filter[@name='company']" position="after">
8+
<filter name="group_by_packaging" string="Packaging" domain="[]" context="{'group_by': 'package_id'}"/>
9+
</xpath>
10+
</field>
11+
</record>
12+
</odoo>

Diff for: put_in_pack_stock/views/view_custom_put_in_pack.xml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<odoo>
2+
<record id="view_picking_type_tree_inherit" model="ir.ui.view">
3+
<field name="name">Operation types Inherit</field>
4+
<field name="model">stock.picking.type</field>
5+
<field name="inherit_id" ref="stock.view_picking_type_tree" />
6+
<field name="arch" type="xml">
7+
<xpath expr="//field[@name='company_id']" position="before">
8+
<field name="put_in_pack_toggle" string="Put in Pack" widget="boolean_toggle"/>
9+
</xpath>
10+
</field>
11+
</record>
12+
</odoo>
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<odoo>
2+
<record id="view_picking_form_inherit" model="ir.ui.view">
3+
<field name="name">stock.picking.form.inherit</field>
4+
<field name="model">stock.picking</field>
5+
<field name="inherit_id" ref="stock.view_picking_form" />
6+
<field name="arch" type="xml">
7+
<xpath expr="//page[@name='operations']//field[@name='quantity']" position="before">
8+
<field name="put_in_pack_toggle" column_invisible="1"/>
9+
<button
10+
name="action_custom_put_in_pack"
11+
class="btn-secondary"
12+
string="Put in pack"
13+
type="object"
14+
invisible="put_in_pack_toggle != True"/>
15+
</xpath>
16+
<xpath expr="//page[@name='operations']//field[@name='product_packaging_id']" position="attributes">
17+
<attribute name="column_invisible">parent.put_in_pack_toggle</attribute>
18+
</xpath>
19+
<xpath expr="//page[@name='operations']//button[@name='action_put_in_pack']" position="attributes">
20+
<attribute name="invisible">state in ('draft', 'done', 'cancel') or put_in_pack_toggle</attribute>
21+
</xpath>
22+
</field>
23+
</record>
24+
</odoo>

0 commit comments

Comments
 (0)