274 lines
10 KiB
Python
Executable File
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)
|