Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 60 additions & 10 deletions stock_lock_lot/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ Stock Lock Lot

This module allows you to define whether a Serial Number/lot is blocked
or not. The default value can be set on the Product Category, in the
field "Block new Serial Numbers/lots". Is possible to specify in a
field "Block new Serial Numbers/lots". It's possible to specify in a
location if locked lots are allowed to move there.

Additionally, locked lots are automatically excluded from stock
reservations, preventing them from being allocated to outgoing orders.
This ensures that blocked inventory cannot be accidentally reserved or
shipped. The reservation exclusion can be bypassed using the
'force_allow_locked_lots' context when explicitly needed.

**Table of contents**

.. contents::
Expand All @@ -48,9 +54,19 @@ To allow a user to block or unblock a Lot:
Serial Numbers/Lots"

To allow move locked lots to a location: #. Open the locations (menu
"Inventory > Configuration > Warehouse Management > Locations") #. check
"Inventory > Configuration > Warehouse Management > Locations") #. Check
the box "Allow Locked"

To configure lot behavior at product category level: #. Open the product
categories (menu "Sales > Configuration > Product Categories") #. In the
"Warehouse" section, you can configure:

- "Block new Serial Numbers/lots": New lots will be created as blocked
by default
- "Allow reservation of locked lots": If checked, locked lots can still
be reserved for orders, but cannot be moved unless the destination
location allows locked lots

Usage
=====

Expand All @@ -61,6 +77,36 @@ To use this module, you need to:
3. Now you cannot move that 'Lot/Serial Number' to any location that
does not have the 'Allow Locked' field checked

**Reservation Behavior:**

By default, locked lots are automatically excluded from stock
reservations. When creating outgoing orders (sales orders, transfers,
etc.), the system will only reserve from unlocked lots. This prevents
blocked inventory from being allocated to orders.

However, you can configure this behavior at the product category level:

- Go to *Sales > Configuration > Product Categories*
- In the Warehouse section, check "Allow reservation of locked lots"
- When enabled, locked lots in this category can still be reserved for
orders, but they cannot be moved unless the destination location
allows locked lots

This is useful when you want to:

- Reserve specific inventory for future use but prevent actual movement
- Hold stock for quality inspection while still planning orders

To override this behavior in custom operations, use the
'force_allow_locked_lots' context.

**Example Scenarios:**

- **Quality Hold**: Lock a lot for quality inspection - it won't be
reserved for customer orders unless the category allows reservation
- **Expired Stock**: Lock expired lots to prevent them from being
shipped

Bug Tracker
===========

Expand All @@ -83,15 +129,19 @@ Authors
Contributors
------------

- Ana Juaristi <anajuaristi@avanzosc.es>
- Alfredo de la Fuente <alfredodelafuente@avanzosc.es>
- Oihane Crucelaegui <oihanecrucelaegi@avanzosc.es>
- Lionel Sausin <ls@numerigraphe.com>
- Ainara Galdona <ainaragaldona@avanzosc.es>
- `Tecnativa <https://www.tecnativa.com>`__:
- Ana Juaristi <anajuaristi@avanzosc.es>
- Alfredo de la Fuente <alfredodelafuente@avanzosc.es>
- Oihane Crucelaegui <oihanecrucelaegi@avanzosc.es>
- Lionel Sausin <ls@numerigraphe.com>
- Ainara Galdona <ainaragaldona@avanzosc.es>
- `Tecnativa <https://www.tecnativa.com>`__:

- Pedro M. Baeza
- Ernesto Tejeda

- `Open Source Integrators <https://www.opensourceintegrators.com>`__:

- Pedro M. Baeza
- Ernesto Tejeda
- Daniel Reis <dreis@opensourceintegrators.com>

Maintainers
-----------
Expand Down
1 change: 1 addition & 0 deletions stock_lock_lot/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from . import stock_lot
from . import stock_location
from . import stock_move_line
from . import stock_quant
8 changes: 8 additions & 0 deletions stock_lock_lot/models/product_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ class ProductCategory(models.Model):
"by default. This means they will not be available for use "
"in stock moves or other operations",
)
lot_reserve_locked = fields.Boolean(
string="Block reservation of locked lots",
help="If checked, locked lots in this category will be blocked from "
"reservation. If unchecked, locked lots can still be reserved for "
"orders, but they cannot be moved unless the destination "
"location allows locked lots",
default=False,
)
69 changes: 47 additions & 22 deletions stock_lock_lot/models/stock_lot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ class StockLot(models.Model):
locked = fields.Boolean(
string="Blocked",
tracking=True,
copy=False,
help="Indicates whether this lot is blocked for use.",
)
locked_reservation = fields.Boolean(
string="Block reservation when locked",
tracking=True,
copy=False,
help="If checked, this lot will be blocked for reservation when locked. "
"If unchecked, this lot can be reserved even when locked. "
"This overrides the category setting.",
)
product_id = fields.Many2one(tracking=True)

def _get_product_locked(self, product):
Expand All @@ -31,54 +40,70 @@ def _get_product_locked(self, product):
categ = categ.parent_id
return _locked

def _get_product_reserve_locked(self, product):
"""Should block reservation when locked?

@param product: browse-record for product.product
@return True when the category of the product
blocks reservation of locked lots
"""
if not product or not product.categ_id:
return False
return product.categ_id.lot_reserve_locked

@api.onchange("product_id")
def _onchange_product_id(self):
"""Instruct the client to lock/unlock a lot on product change"""
self.locked = self._get_product_locked(self.product_id)
self.locked_reservation = self._get_product_reserve_locked(self.product_id)

@api.constrains("locked")
def _check_lock_unlock(self):
if not self.user_has_groups(
"stock_lock_lot.group_lock_lot"
) and not self.env.context.get("bypass_lock_permission_check"):
has_lock_group = self.user_has_groups("stock_lock_lot.group_lock_lot")
can_bypass_check = self.env.context.get("bypass_lock_permission_check")
if not (has_lock_group or can_bypass_check):
raise exceptions.AccessError(
_("You are not allowed to block/unblock Serial Numbers/Lots")
)
reserved_quants = self.env["stock.quant"].search(
[("lot_id", "in", self.ids), ("reserved_quantity", "!=", 0.0)]
)
if reserved_quants:
raise exceptions.ValidationError(
_(
"You are not allowed to block/unblock, there are"
" reserved quantities for these Serial Numbers/Lots"
)
# The reserved check is kept for backward compatibility
reserved_check = self.env.context.get("reserved_lock_permission_check")
if reserved_check:
reserved_quants = self.env["stock.quant"].search(
[("lot_id", "in", self.ids), ("reserved_quantity", "!=", 0.0)]
)
if reserved_quants:
raise exceptions.ValidationError(
_(
"You are not allowed to block/unblock, there are"
" reserved quantities for these Serial Numbers/Lots"
)
)

@api.model
def create(self, vals):
@api.model_create_multi
def create(self, vals_list):
"""Force the locking/unlocking, ignoring the value of 'locked'."""
product = self.env["product.product"].browse(
vals.get(
for vals in vals_list:
product_id = vals.get(
"product_id",
# Web quick-create provide in context
self.env.context.get(
"product_id", self.env.context.get("default_product_id", False)
),
)
)
vals["locked"] = self._get_product_locked(product)
lot = super(
product = self.env["product.product"].browse(product_id)
vals["locked"] = self._get_product_locked(product)
vals["locked_reservation"] = self._get_product_reserve_locked(product)
lots = super(
StockLot, self.with_context(bypass_lock_permission_check=True)
).create(vals)

return self.browse(lot.id) # for cleaning context
).create(vals_list)
return lots

def write(self, values):
""" "Lock the lot if changing the product and locking is required"""
if "product_id" in values:
product = self.env["product.product"].browse(values["product_id"])
values["locked"] = self._get_product_locked(product)
values["locked_reservation"] = self._get_product_reserve_locked(product)
return super().write(values)

def _track_subtype(self, init_values):
Expand Down
34 changes: 34 additions & 0 deletions stock_lock_lot/models/stock_quant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2025 Open Source Integrators (http://www.opensourceintegrators.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import models
from odoo.osv import expression


class StockQuant(models.Model):
_inherit = "stock.quant"

def _get_gather_domain(
self,
product_id,
location_id,
lot_id=None,
package_id=None,
owner_id=None,
strict=False,
):
domain = super()._get_gather_domain(
product_id, location_id, lot_id, package_id, owner_id, strict
)
# Extend StockQuant to exclude locked lots from reservation domain
# Block locked lots unless they have reserve_locked=False
if not self.env.context.get("force_allow_locked_lots"):
filter_domain = [
"|",
"|",
("lot_id", "=", False),
("lot_id.locked", "=", False),
("lot_id.locked_reservation", "=", False),
]
domain = expression.AND([domain, filter_domain])
return domain
12 changes: 9 additions & 3 deletions stock_lock_lot/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ To allow a user to block or unblock a Lot:
2. In the "Warehouse" section, check the box "Allow to block/unblock
Serial Numbers/Lots"

To allow move locked lots to a location: \#. Open the locations (menu
"Inventory \> Configuration \> Warehouse Management \> Locations") \#.
check the box "Allow Locked"
To allow move locked lots to a location:
#. Open the locations (menu "Inventory \> Configuration \> Warehouse Management \> Locations")
#. Check the box "Allow Locked"

To configure lot behavior at product category level:
#. Open the product categories (menu "Sales \> Configuration \> Product Categories")
#. In the "Warehouse" section, you can configure:
- "Block new Serial Numbers/lots": New lots will be created as blocked by default
- "Allow reservation of locked lots": If checked, locked lots can still be reserved for orders, but cannot be moved unless the destination location allows locked lots
2 changes: 2 additions & 0 deletions stock_lock_lot/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
- [Tecnativa](https://www.tecnativa.com):
- Pedro M. Baeza
- Ernesto Tejeda
- [Open Source Integrators](https://www.opensourceintegrators.com):
- Daniel Reis \<<dreis@opensourceintegrators.com>>
8 changes: 7 additions & 1 deletion stock_lock_lot/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
This module allows you to define whether a Serial Number/lot is blocked
or not. The default value can be set on the Product Category, in the
field "Block new Serial Numbers/lots". Is possible to specify in a
field "Block new Serial Numbers/lots". It's possible to specify in a
location if locked lots are allowed to move there.

Additionally, locked lots are automatically excluded from stock reservations,
preventing them from being allocated to outgoing orders. This ensures that
blocked inventory cannot be accidentally reserved or shipped. The reservation
exclusion can be bypassed using the 'force_allow_locked_lots' context when
explicitly needed.
26 changes: 26 additions & 0 deletions stock_lock_lot/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,29 @@ To use this module, you need to:
2. Select one 'Lot/Serial Number' and check 'Blocked' field
3. Now you cannot move that 'Lot/Serial Number' to any location that
does not have the 'Allow Locked' field checked

**Reservation Behavior:**

By default, locked lots are automatically excluded from stock reservations.
When creating outgoing orders (sales orders, transfers, etc.), the system
will only reserve from unlocked lots. This prevents blocked inventory from
being allocated to orders.

However, you can configure this behavior at the product category level:

- Go to *Sales \> Configuration \> Product Categories*
- In the Warehouse section, check "Allow reservation of locked lots"
- When enabled, locked lots in this category can still be reserved for orders,
but they cannot be moved unless the destination location allows locked lots

This is useful when you want to:
- Reserve specific inventory for future use but prevent actual movement
- Hold stock for quality inspection while still planning orders

To override this behavior in custom operations, use the 'force_allow_locked_lots' context.

**Example Scenarios:**

- **Quality Hold**: Lock a lot for quality inspection - it won't be reserved
for customer orders unless the category allows reservation
- **Expired Stock**: Lock expired lots to prevent them from being shipped
Loading
Loading