Skip to content

Commit 26f7184

Browse files
committed
[ADD] stock_lot_remove: Remove quants with a past removal date
1 parent 11f1b94 commit 26f7184

24 files changed

+1111
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../stock_lot_auto_remove
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../stock_lot_remove

setup/stock_lot_remove/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

stock_lot_remove/README.rst

Whitespace-only changes.

stock_lot_remove/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import models
2+
from . import wizards
3+
from .hooks import post_init_hook

stock_lot_remove/__manifest__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
{
5+
"name": "Stock Lot Auto Remove",
6+
"summary": """Automatically move remaining quants with a past removal date out "
7+
"of your stock""",
8+
"version": "16.0.1.0.0",
9+
"license": "AGPL-3",
10+
"author": "ACSONE SA/NV,BCIM,Odoo Community Association (OCA)",
11+
"website": "https://github.com/OCA/stock-logistics-workflow",
12+
"depends": [
13+
"stock",
14+
"product_expiry",
15+
],
16+
"data": [
17+
"wizards/stock_lot_removal_wizard.xml",
18+
"views/stock_warehouse.xml",
19+
"data/ir_cron.xml",
20+
"security/ir.model.access.csv",
21+
],
22+
"demo": [],
23+
"maintainers": ["lmignon"],
24+
"post_init_hook": "post_init_hook",
25+
}

stock_lot_remove/data/ir_cron.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!-- Copyright 2025 ACSONE SA/NV
3+
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
4+
<odoo>
5+
<record id="cron_stock_lot_auto_remove" model="ir.cron">
6+
<field name="name">Remove Expired Lots</field>
7+
<field name="model_id" ref="stock.model_stock_warehouse" />
8+
<field name="state">code</field>
9+
<field name="code">model._cron_remove_expired_lots()</field>
10+
<field name="interval_number">1</field>
11+
<field name="interval_type">days</field>
12+
<field name="numbercall">-1</field>
13+
<field name="doall" eval="False" />
14+
<field
15+
name="nextcall"
16+
eval="(DateTime.now().replace(hour=1, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"
17+
/>
18+
</record>
19+
20+
</odoo>

stock_lot_remove/hooks.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import SUPERUSER_ID, api
5+
6+
7+
def _get_next_picking_type_color(env):
8+
"""Choose the next available color for the operation types."""
9+
stock_picking_type = env["stock.picking.type"]
10+
picking_type = stock_picking_type.search_read(
11+
[("warehouse_id", "!=", False), ("color", "!=", False)],
12+
["color"],
13+
order="color",
14+
)
15+
all_used_colors = [res["color"] for res in picking_type]
16+
available_colors = [color for color in range(0, 12) if color not in all_used_colors]
17+
return available_colors[0] if available_colors else 0
18+
19+
20+
def create_picking_type(whs):
21+
env = whs.env
22+
ir_sequence_sudo = env["ir.sequence"].sudo()
23+
stock_picking_type = env["stock.picking.type"]
24+
color = _get_next_picking_type_color(env)
25+
stock_picking = stock_picking_type.search(
26+
[("sequence", "!=", False)], limit=1, order="sequence desc"
27+
)
28+
max_sequence = stock_picking.sequence or 0
29+
create_data = whs._get_picking_type_create_values(max_sequence)[0]
30+
sequence_data = whs._get_sequence_values()
31+
data = {}
32+
for picking_type, values in create_data.items():
33+
if picking_type in ["lot_remove_picking_type_id"] and not whs[picking_type]:
34+
picking_sequence = sequence_data[picking_type]
35+
sequence = ir_sequence_sudo.create(picking_sequence)
36+
values.update(
37+
warehouse_id=whs.id,
38+
color=color,
39+
sequence_id=sequence.id,
40+
)
41+
data[picking_type] = stock_picking_type.create(values).id
42+
if data:
43+
whs.write(data)
44+
45+
46+
def post_init_hook(cr, registry):
47+
env = api.Environment(cr, SUPERUSER_ID, {})
48+
49+
warehouses = env["stock.warehouse"].search([])
50+
for warehouse in warehouses:
51+
create_picking_type(warehouse)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import stock_warehouse
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import _, api, fields, models
5+
from odoo.exceptions import ValidationError
6+
7+
8+
class StockWarehouse(models.Model):
9+
10+
_inherit = "stock.warehouse"
11+
12+
lot_remove_enabled = fields.Boolean(
13+
string="Enable Expired Lot Removal",
14+
help="If checked, a move will be planned reserving the expired lot.",
15+
)
16+
17+
lot_remove_orig_location_ids = fields.Many2many(
18+
comodel_name="stock.location",
19+
string="Expired Lot Origin Locations",
20+
help="Locations from which expired lots will be moved.",
21+
compute="_compute_lot_remove_orig_location_ids",
22+
store=True,
23+
readonly=False,
24+
)
25+
26+
lot_remove_picking_type_id = fields.Many2one(
27+
comodel_name="stock.picking.type",
28+
string="Expired Lot Move Picking Type",
29+
help="Picking type used for moving expired lots.",
30+
)
31+
32+
@api.depends("lot_remove_enabled")
33+
def _onchange_lot_remove_enabled(self):
34+
"""Ensure that the origin and destination locations are set when
35+
enabling expired lot move."""
36+
if self.lot_remove_enabled and not self.lot_remove_orig_location_ids:
37+
self.lot_remove_orig_location_ids = self.lot_stock_id
38+
39+
@api.constrains("lot_remove_orig_location_ids", "lot_remove_enabled")
40+
def _check_expired_lot_locations(self):
41+
"""Ensure that:
42+
* the origin location is set,
43+
* the origin location is from the current warehouse,
44+
"""
45+
for record in self:
46+
if record.lot_remove_enabled:
47+
if not record.lot_remove_orig_location_ids:
48+
raise ValidationError(
49+
_(
50+
"Please set at least one origin location for expired lot moves."
51+
)
52+
)
53+
if record.lot_remove_orig_location_ids.mapped("warehouse_id") != record:
54+
raise ValidationError(
55+
_(
56+
"The origin locations for expired lot moves must be from the "
57+
"current warehouse."
58+
)
59+
)
60+
61+
@api.constrains("lot_remove_picking_type_id", "lot_remove_enabled")
62+
def _check_lot_remove_picking_type(self):
63+
"""Ensure that the picking type for expired lot moves is set when
64+
expired lot move is enabled."""
65+
for record in self:
66+
if record.lot_remove_enabled and not record.lot_remove_picking_type_id:
67+
raise ValidationError(
68+
_("Please set the picking type for expired lot moves.")
69+
)
70+
71+
def _get_picking_type_create_values(self, max_sequence):
72+
"""Override to set the picking type for expired lot moves."""
73+
create_data, max_sequence = super()._get_picking_type_create_values(
74+
max_sequence
75+
)
76+
max_sequence += 1
77+
create_data["lot_remove_picking_type_id"] = {
78+
"name": _("Expired Lot Removal"),
79+
"code": "internal",
80+
"use_create_lots": False,
81+
"use_existing_lots": True,
82+
"default_location_src_id": self.lot_stock_id.id,
83+
"default_location_dest_id": self.wh_qc_stock_loc_id.id,
84+
"sequence": max_sequence,
85+
"sequence_code": "EXP",
86+
"company_id": self.company_id.id,
87+
}
88+
return create_data, max_sequence
89+
90+
def _get_sequence_values(self, name=False, code=False):
91+
values = super(StockWarehouse, self)._get_sequence_values(name=name, code=code)
92+
count = self.env["ir.sequence"].search_count(
93+
[("prefix", "like", self.code + "/EXP%/%")]
94+
)
95+
values.update(
96+
{
97+
"lot_remove_picking_type_id": {
98+
"name": self.name + " " + _("Sequence Expired Lot Removal"),
99+
"prefix": self.code
100+
+ "/"
101+
+ (
102+
self.lot_remove_picking_type_id.sequence_code
103+
or (("EXP" + str(count)) if count else "EXP")
104+
)
105+
+ "/",
106+
"padding": 5,
107+
"company_id": self.company_id.id,
108+
},
109+
}
110+
)
111+
return values
112+
113+
@api.model
114+
def _cron_remove_expired_lots(self):
115+
"""Cron job to remove expired lots."""
116+
warehouses = self.env["stock.warehouse"].search(
117+
[("lot_remove_enabled", "=", True)]
118+
)
119+
for warehouse in warehouses:
120+
wizard = self.env["stock.lot.removal.wizard"].create(
121+
{
122+
"warehouse_id": warehouse.id,
123+
}
124+
)
125+
wizard.action_run()

0 commit comments

Comments
 (0)