stock_no_negative/tests/test_stock_no_negative.py
2026-01-12 16:43:00 +07:00

274 lines
10 KiB
Python
Executable File

# Copyright 2015-2016 Akretion (http://www.akretion.com) - Alexis de Lattre
# Copyright 2016 ForgeFlow (http://www.forgeflow.com)
# Copyright 2016 Serpent Consulting Services (<http://www.serpentcs.com>)
# Copyright 2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import TransactionCase
class TestStockNoNegative(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_model = cls.env["product.product"]
cls.product_ctg_model = cls.env["product.category"]
cls.lot_model = cls.env["stock.lot"]
cls.picking_type_id = cls.env.ref("stock.picking_type_out")
cls.location_id = cls.env.ref("stock.stock_location_stock")
cls.location_dest_id = cls.env.ref("stock.stock_location_customers")
# Create product category
cls.product_ctg = cls._create_product_category(cls)
# Create a Product
cls.product = cls._create_product(cls, "test_product1")
# Create a Product With Lot
cls.product_with_lot = cls._create_product_with_lot(cls, "test_lot_product1")
# Create Lot
cls.lot1 = cls._create_lot(cls, "lot1")
cls._create_picking(cls)
cls._create_picking_with_lot(cls)
def _create_product_category(self):
product_ctg = self.product_ctg_model.create(
{"name": "test_product_ctg", "allow_negative_stock": False}
)
return product_ctg
def _create_product(self, name):
product = self.product_model.create(
{
"name": name,
"categ_id": self.product_ctg.id,
"is_storable": True,
"type": "consu",
"allow_negative_stock": False,
}
)
return product
def _create_product_with_lot(self, name):
product = self.product_model.create(
{
"name": name,
"categ_id": self.product_ctg.id,
"is_storable": True,
"type": "consu",
"tracking": "lot",
"allow_negative_stock": False,
}
)
return product
def _create_lot(self, name):
lot = self.lot_model.create(
{
"name": name,
"product_id": self.product_with_lot.id,
"company_id": self.env.company.id,
}
)
return lot
def _create_picking(self):
self.stock_picking = (
self.env["stock.picking"]
.with_context(test_stock_no_negative=True)
.create(
{
"picking_type_id": self.picking_type_id.id,
"move_type": "direct",
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
}
)
)
self.stock_move = self.env["stock.move"].create(
{
"name": "Test Move",
"product_id": self.product.id,
"product_uom_qty": 100.0,
"product_uom": self.product.uom_id.id,
"picking_id": self.stock_picking.id,
"state": "draft",
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
"quantity": 100.0,
}
)
def _create_picking_with_lot(self):
self.stock_picking_with_lot = (
self.env["stock.picking"]
.with_context(test_stock_no_negative=True)
.create(
{
"picking_type_id": self.picking_type_id.id,
"move_type": "direct",
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
}
)
)
self.stock_move_with_lot = self.env["stock.move"].create(
{
"name": "Test Move",
"product_id": self.product_with_lot.id,
"product_uom_qty": 100.0,
"product_uom": self.product_with_lot.uom_id.id,
"picking_id": self.stock_picking_with_lot.id,
"state": "draft",
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
}
)
def test_check_constrains(self):
"""Assert that constraint is raised when user
tries to validate the stock operation which would
make the stock level of the product negative"""
self.stock_picking.action_confirm()
with self.assertRaises(ValidationError):
self.stock_picking.button_validate()
def test_check_constrains_with_lot(self):
"""Assert that constraint is raised when user
tries to validate the stock operation which would
make the stock level of the product negative with
a product with lot"""
self.stock_picking_with_lot.action_confirm()
self.stock_move_line_with_lot = self.env["stock.move.line"].create(
{
"product_id": self.product_with_lot.id,
"quantity": 100.0,
"picking_id": self.stock_picking_with_lot.id,
"move_id": self.stock_move_with_lot.id,
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
"lot_id": self.lot1.id,
}
)
with self.assertRaises(ValidationError):
self.stock_picking_with_lot.button_validate()
def test_true_allow_negative_stock_product(self):
"""Assert that negative stock levels are allowed when
the allow_negative_stock is set active in the product"""
self.product.allow_negative_stock = True
self.stock_picking.action_confirm()
self.stock_picking.button_validate()
quant = self.env["stock.quant"].search(
[
("product_id", "=", self.product.id),
("location_id", "=", self.location_id.id),
]
)
self.assertEqual(quant.quantity, -100)
def test_true_allow_negative_stock_location(self):
"""Assert that negative stock levels are allowed when
the allow_negative_stock is set active in the product"""
self.product.allow_negative_stock = False
self.location_id.allow_negative_stock = True
self.stock_picking.action_confirm()
self.stock_picking.button_validate()
quant = self.env["stock.quant"].search(
[
("product_id", "=", self.product.id),
("location_id", "=", self.location_id.id),
]
)
self.assertEqual(quant.quantity, -100)
def test_true_allow_negative_stock_product_with_lot(self):
"""Assert that negative stock levels are allowed when
the allow_negative_stock is set active in the product with lot"""
self.product_with_lot.allow_negative_stock = True
self.stock_picking_with_lot.action_confirm()
with self.assertRaises(UserError):
self.stock_picking_with_lot.button_validate()
# create Detail Operations (move line with lot)
self.stock_move_line_with_lot = self.env["stock.move.line"].create(
{
"product_id": self.product_with_lot.id,
"quantity": 100.0,
"picking_id": self.stock_picking_with_lot.id,
"move_id": self.stock_move_with_lot.id,
"location_id": self.location_id.id,
"location_dest_id": self.location_dest_id.id,
"lot_id": self.lot1.id,
}
)
self.stock_picking_with_lot.button_validate()
quant = self.env["stock.quant"].search(
[
("product_id", "=", self.product_with_lot.id),
("location_id", "=", self.location_id.id),
("lot_id", "=", self.lot1.id),
]
)
self.assertEqual(quant.quantity, -100)
def test_allow_negative_stock_subcontracting_location(self):
"""Assert that negative stock levels are allowed in subcontracting locations
even when allow_negative_stock is False on product and location"""
# Skip test if mrp_subcontracting is not installed
if not hasattr(self.env['stock.location'], 'is_subcontracting_location'):
self.skipTest("mrp_subcontracting module not installed")
# Create a subcontracting location
subcontracting_location = self.env['stock.location'].create({
'name': 'Test Subcontracting Location',
'usage': 'internal',
'is_subcontracting_location': True,
'allow_negative_stock': False,
})
# Create picking from subcontracting location to customer
stock_picking_subcontract = (
self.env["stock.picking"]
.with_context(test_stock_no_negative=True)
.create(
{
"picking_type_id": self.picking_type_id.id,
"move_type": "direct",
"location_id": subcontracting_location.id,
"location_dest_id": self.location_dest_id.id,
}
)
)
stock_move_subcontract = self.env["stock.move"].create(
{
"name": "Test Subcontract Move",
"product_id": self.product.id,
"product_uom_qty": 100.0,
"product_uom": self.product.uom_id.id,
"picking_id": stock_picking_subcontract.id,
"state": "draft",
"location_id": subcontracting_location.id,
"location_dest_id": self.location_dest_id.id,
"quantity": 100.0,
}
)
# Ensure product and location don't allow negative stock
self.product.allow_negative_stock = False
subcontracting_location.allow_negative_stock = False
# This should not raise ValidationError because it's a subcontracting location
stock_picking_subcontract.action_confirm()
stock_picking_subcontract.button_validate()
# Verify negative stock is allowed
quant = self.env["stock.quant"].search(
[
("product_id", "=", self.product.id),
("location_id", "=", subcontracting_location.id),
]
)
self.assertEqual(quant.quantity, -100)