add stock_inventory_product_exhausted: prevent for removing zero quantity stock quants. Add exhausted products to inventory adjustments

This commit is contained in:
Luis 2025-10-01 13:27:34 +02:00
parent e4050a6e31
commit 17cc64c522
15 changed files with 565 additions and 0 deletions

View file

@ -0,0 +1,133 @@
==========================================
Stock Inventory Exhausted Products
==========================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:...
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_inventory_product_exhausted
:alt: OCA/stock-logistics-warehouse
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-16-0/stock-logistics-warehouse-16-0-stock_inventory_product_exhausted
: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-warehouse&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module extends the functionality of the `stock_inventory` module from the OCA/stock-logistics-warehouse repository to include exhausted products (with zero quantity) in inventory adjustments.
In recent versions of Odoo, when a product reaches zero available quantity, its `stock.quant` is automatically deleted. This module allows creating quants with zero quantity for exhausted products when creating a new inventory adjustment, facilitating the physical counting of these products.
**Table of contents**
.. contents::
:local:
Features
========
* **Configurable field**: Includes a boolean field `include_exhausted_products` to enable/disable the functionality
* **Default behavior**: Maintains the original behavior of the base module when the option is disabled
* **Support for all selection types**: Works with manual selection, by category, specific product, or all products
* **Storable products only**: Creates quants only for products of type `product`, not for services
* **Prevents duplicates**: Does not create duplicate quants if they already exist for the product and location
* **User interface**: Field visible in the inventory adjustment form
Configuration
=============
The module requires no additional configuration. The `include_exhausted_products` field defaults to `False` to maintain compatibility with the original behavior.
Usage
=====
To use this module:
#. Go to **Inventory > Operations > Inventory Adjustments**
#. Create a new inventory adjustment
#. Configure the desired locations and product selection
#. **Enable** the "Include Exhausted Products" field if you want to include products without stock
#. Click on "Start Inventory"
Use cases
---------
**With the field enabled:**
* Zero quantity quants will be created for products that have no stock in the selected locations
* Allows physically counting products that are exhausted in the system
* Useful for complete inventories where you want to verify that there really is no stock of certain products
**With the field disabled:**
* Standard behavior of the original module
* Only products that already have existing quants are included
Known issues / Roadmap
======================
* Review the necessity of the StockQuant class override
* Consider performance optimizations for large product catalogs
* Add option to filter by product categories when including exhausted products
Bug Tracker
============
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smash it by providing detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_inventory_product_exhausted%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Criptomart
Contributors
~~~~~~~~~~~~
* Luis Norte <luisnore@example.com>
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.
.. |maintainer-luisnore| image:: https://github.com/luisnore.png?size=40px
:target: https://github.com/luisnore
:alt: luisnore
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-luisnore|
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_inventory_product_exhausted>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,16 @@
{
"name": "Stock Inventory Exhausted Products",
"version": "16.0.1.0.0",
"development_status": "Beta",
"depends": ["stock_inventory"],
"author": "Criptomart, Odoo Community Association (OCA)",
"maintainers": ["luisnore"],
"website": "https://github.com/OCA/stock-logistics-warehouse",
"license": "AGPL-3",
"category": "Warehouse",
"summary": "Create quants for products with zero quantity in stock adjustments",
"data": [
"views/stock_inventory.xml",
],
"installable": True,
}

View file

@ -0,0 +1 @@
from . import stock_inventory

View file

@ -0,0 +1,113 @@
# Copyright 2025 Your Name or Company
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, models, api, fields
import logging
_logger = logging.getLogger(__name__)
class StockInventory(models.Model):
_inherit = "stock.inventory"
include_exhausted_products = fields.Boolean(
string="Include Exhausted Products",
help="If enabled, products with zero stock will be included "
"in the inventory adjustment by creating zero-quantity quants.",
default=False,
)
def _create_zero_quants(self, locations, products):
"""Create zero-quantity quants for products without existing quants.
This method creates stock quants with zero quantity for storable products
that don't have existing quants in the specified locations. This allows
these products to be included in inventory adjustments even when they
have no current stock.
Args:
locations (stock.location recordset): Locations to check
products (product.product recordset): Products to process
"""
Quant = self.env["stock.quant"]
# Get existing quants to avoid duplicates
existing_quants = Quant.search(
[
("product_id", "in", products.ids),
("location_id", "in", locations.ids),
]
)
existing_combinations = {
(q.product_id.id, q.location_id.id) for q in existing_quants
}
quants_to_create = []
for location in locations:
for product in products.filtered(lambda p: p.type == "product"):
if (product.id, location.id) not in existing_combinations:
_logger.debug(
"Creating zero quant for product %s in location %s",
product.name,
location.name,
)
quants_to_create.append(
{
"product_id": product.id,
"location_id": location.id,
"quantity": 0,
"company_id": location.company_id.id or self.env.company.id,
}
)
if quants_to_create:
Quant.create(quants_to_create)
def _get_quants(self, locations):
"""Override to include zero-quantity quants when requested.
This method extends the base functionality to optionally include
products with zero stock by creating zero-quantity quants before
retrieving the final quant list.
Args:
locations (stock.location recordset): Locations for inventory
Returns:
stock.quant recordset: Quants to include in inventory
"""
quants = super()._get_quants(locations)
if self.include_exhausted_products:
# Determine products based on selection criteria
if self.product_selection == "category":
products = self.env["product.product"].search(
[("categ_id", "child_of", self.category_id.id)]
)
elif self.product_selection in ("manual", "one", "lot"):
products = self.product_ids
else: # "all"
products = self.env["product.product"].search(
[("type", "=", "product")]
)
self._create_zero_quants(locations, products)
# Re-fetch quants to include newly created ones
return super()._get_quants(locations)
return quants
class StockQuant(models.Model):
_inherit = "stock.quant"
@api.model
def _unlink_zero_quants(self):
"""Prevent automatic unlinking of zero quants.
This method overrides the default behavior to prevent
zero quants from being automatically removed.
This may need review to ensure it doesn't conflict
with standard Odoo behavior.
"""
_logger.debug("Preventing automatic unlinking of zero quants")

View file

@ -0,0 +1,6 @@
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smash it by providing detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_inventory_product_exhausted%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.

View file

@ -0,0 +1 @@
The module requires no additional configuration. The `include_exhausted_products` field defaults to `False` to maintain compatibility with the original behavior.

View file

@ -0,0 +1,21 @@
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.
.. |maintainer-yourusername| image:: https://github.com/yourusername.png?size=40px
:target: https://github.com/yourusername
:alt: yourusername
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-yourusername|
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_inventory_product_exhausted>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,3 @@
This module extends the functionality of the `stock_inventory` module from the OCA/stock-logistics-warehouse repository to include exhausted products (with zero quantity) in inventory adjustments.
In recent versions of Odoo, when a product reaches zero available quantity, its `stock.quant` is automatically deleted. This module allows creating quants with zero quantity for exhausted products when creating a new inventory adjustment, facilitating the physical counting of these products.

View file

@ -0,0 +1,3 @@
This is a beta version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

View file

@ -0,0 +1,6 @@
* **Configurable field**: Includes a boolean field `include_exhausted_products` to enable/disable the functionality
* **Default behavior**: Maintains the original behavior of the base module when the option is disabled
* **Support for all selection types**: Works with manual selection, by category, specific product, or all products
* **Storable products only**: Creates quants only for products of type `product`, not for services
* **Prevents duplicates**: Does not create duplicate quants if they already exist for the product and location
* **User interface**: Field visible in the inventory adjustment form

View file

@ -0,0 +1,21 @@
To use this module:
#. Go to **Inventory > Operations > Inventory Adjustments**
#. Create a new inventory adjustment
#. Configure the desired locations and product selection
#. **Enable** the "Include Exhausted Products" field if you want to include products without stock
#. Click on "Start Inventory"
Use cases
---------
**With the field enabled:**
* Zero quantity quants will be created for products that have no stock in the selected locations
* Allows physically counting products that are exhausted in the system
* Useful for complete inventories where you want to verify that there really is no stock of certain products
**With the field disabled:**
* Standard behavior of the original module
* Only products that already have existing quants are included

View file

@ -0,0 +1 @@
from . import test_stock_inventory_exhausted

View file

@ -0,0 +1,227 @@
from odoo.tests.common import TransactionCase
class TestStockInventoryExhausted(TransactionCase):
def setUp(self):
super().setUp()
# Create test products
self.product_with_stock = self.env["product.product"].create(
{
"name": "Product With Stock",
"type": "product",
"categ_id": self.env.ref("product.product_category_all").id,
}
)
self.product_without_stock = self.env["product.product"].create(
{
"name": "Product Without Stock",
"type": "product",
"categ_id": self.env.ref("product.product_category_all").id,
}
)
# Create test location
self.location = self.env["stock.location"].create(
{
"name": "Test Location",
"usage": "internal",
}
)
# Create initial stock for one product
self.env["stock.quant"].create(
{
"product_id": self.product_with_stock.id,
"location_id": self.location.id,
"quantity": 10.0,
}
)
def test_inventory_without_exhausted_products_flag(self):
"""Test that without the flag, zero stock products are not included"""
inventory = self.env["stock.inventory"].create(
{
"name": "Test Inventory without flag",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "all",
"include_exhausted_products": False,
}
)
inventory.action_start()
# Should only have quants for products with existing stock
self.assertTrue(
any(
q.product_id == self.product_with_stock
for q in inventory.stock_quant_ids
)
)
self.assertFalse(
any(
q.product_id == self.product_without_stock
for q in inventory.stock_quant_ids
)
)
def test_inventory_with_exhausted_products_flag(self):
"""Test that with the flag, zero stock products are included"""
inventory = self.env["stock.inventory"].create(
{
"name": "Test Inventory with flag",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "all",
"include_exhausted_products": True,
}
)
inventory.action_start()
# Should have quants for both products (including zero stock product)
self.assertTrue(
any(
q.product_id == self.product_with_stock
for q in inventory.stock_quant_ids
)
)
self.assertTrue(
any(
q.product_id == self.product_without_stock
for q in inventory.stock_quant_ids
)
)
# Check that zero stock product has quantity 0
zero_quant = inventory.stock_quant_ids.filtered(
lambda q: q.product_id == self.product_without_stock
)
self.assertEqual(zero_quant.quantity, 0.0)
def test_inventory_manual_selection_with_exhausted_flag(self):
"""Test manual product selection with exhausted products flag"""
inventory = self.env["stock.inventory"].create(
{
"name": "Test Manual Selection",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "manual",
"product_ids": [(6, 0, [self.product_without_stock.id])],
"include_exhausted_products": True,
}
)
inventory.action_start()
# Should have quant for the selected product even if it has zero stock
self.assertTrue(
any(
q.product_id == self.product_without_stock
for q in inventory.stock_quant_ids
)
)
zero_quant = inventory.stock_quant_ids.filtered(
lambda q: q.product_id == self.product_without_stock
)
self.assertEqual(zero_quant.quantity, 0.0)
def test_inventory_category_selection_with_exhausted_flag(self):
"""Test category selection with exhausted products flag"""
# Create a specific category
test_category = self.env["product.category"].create({"name": "Test Category"})
# Create product in this category
product_in_category = self.env["product.product"].create(
{
"name": "Product In Category",
"type": "product",
"categ_id": test_category.id,
}
)
inventory = self.env["stock.inventory"].create(
{
"name": "Test Category Selection",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "category",
"category_id": test_category.id,
"include_exhausted_products": True,
}
)
inventory.action_start()
# Should have quant for the product in category even if it has zero stock
self.assertTrue(
any(q.product_id == product_in_category for q in inventory.stock_quant_ids)
)
def test_create_zero_quants_only_for_product_type(self):
"""Test that zero quants are only created for products with type 'product'"""
# Create a service product
service_product = self.env["product.product"].create(
{
"name": "Service Product",
"type": "service",
}
)
inventory = self.env["stock.inventory"].create(
{
"name": "Test Service Product",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "manual",
"product_ids": [(6, 0, [service_product.id])],
"include_exhausted_products": True,
}
)
inventory.action_start()
# Should not create quants for service products
self.assertFalse(
any(q.product_id == service_product for q in inventory.stock_quant_ids)
)
def test_no_duplicate_quants_created(self):
"""Test that no duplicate quants are created if they already exist"""
# Create a quant with zero quantity manually
existing_quant = self.env["stock.quant"].create(
{
"product_id": self.product_without_stock.id,
"location_id": self.location.id,
"quantity": 0.0,
}
)
inventory = self.env["stock.inventory"].create(
{
"name": "Test No Duplicates",
"location_ids": [(6, 0, [self.location.id])],
"product_selection": "manual",
"product_ids": [(6, 0, [self.product_without_stock.id])],
"include_exhausted_products": True,
}
)
inventory.action_start()
# Should use existing quant, not create a new one
quants_for_product = self.env["stock.quant"].search(
[
("product_id", "=", self.product_without_stock.id),
("location_id", "=", self.location.id),
]
)
self.assertEqual(len(quants_for_product), 1)
self.assertEqual(quants_for_product.id, existing_quant.id)
def test_field_default_value(self):
"""Test that include_exhausted_products field defaults to False"""
inventory = self.env["stock.inventory"].create(
{
"name": "Test Default Value",
"location_ids": [(6, 0, [self.location.id])],
}
)
self.assertFalse(inventory.include_exhausted_products)

View file

@ -0,0 +1,12 @@
<odoo>
<record id="view_stock_inventory_form_exhausted" model="ir.ui.view">
<field name="name">stock.inventory.form.exhausted</field>
<field name="model">stock.inventory</field>
<field name="inherit_id" ref="stock_inventory.view_inventory_group_form"/>
<field name="arch" type="xml">
<field name="exclude_sublocation" position="after">
<field name="include_exhausted_products"/>
</field>
</field>
</record>
</odoo>