mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
395 lines
14 KiB
Python
395 lines
14 KiB
Python
# Copyright 2014 Numérigraphe SARL
|
||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||
|
||
from odoo.osv.expression import TRUE_LEAF
|
||
from odoo.tests.common import TransactionCase
|
||
|
||
|
||
class TestPotentialQty(TransactionCase):
|
||
"""Test the potential quantity on a product with a multi-line BoM"""
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
|
||
self.product_model = self.env["product.product"]
|
||
self.bom_model = self.env["mrp.bom"]
|
||
self.bom_line_model = self.env["mrp.bom.line"]
|
||
self.stock_quant_model = self.env["stock.quant"]
|
||
self.config = self.env["ir.config_parameter"]
|
||
self.location = self.env["stock.location"]
|
||
self.main_company = self.browse_ref("base.main_company")
|
||
# Get the warehouses
|
||
self.wh_main = self.browse_ref("stock.warehouse0")
|
||
self.wh_ch = self.browse_ref("stock.stock_warehouse_shop0")
|
||
# An interesting product (multi-line BoM, variants)
|
||
self.tmpl = self.browse_ref("mrp.product_product_table_kit_product_template")
|
||
# First variant
|
||
self.var1 = self.browse_ref("mrp.product_product_table_kit")
|
||
# Second variant
|
||
self.var2 = self.browse_ref("stock_available_mrp.product_kit_1a")
|
||
# Make bolt a stockable product to be able to change its stock
|
||
# we need to unreserve the existing move before being able to do it.
|
||
bolt = self.env.ref("mrp.product_product_computer_desk_bolt")
|
||
bolt.stock_move_ids._do_unreserve()
|
||
bolt.type = "product"
|
||
# Components that can be used to make the product
|
||
components = [
|
||
# Bolt
|
||
bolt,
|
||
# Wood Panel
|
||
self.browse_ref("mrp.product_product_wood_panel"),
|
||
]
|
||
|
||
# Zero-out the inventory of all variants and components
|
||
for component in components + [v for v in self.tmpl.product_variant_ids]:
|
||
component.stock_quant_ids.unlink()
|
||
|
||
# A product without a BoM
|
||
self.product_wo_bom = self.browse_ref("product.product_product_11")
|
||
|
||
# Record the initial quantity available for sale
|
||
self.initial_usable_qties = {
|
||
i.id: i.immediately_usable_qty
|
||
for i in [self.tmpl, self.var1, self.var2, self.product_wo_bom]
|
||
}
|
||
|
||
def _create_inventory(self, location_id, company_id):
|
||
inventory = self.env["stock.inventory"].create(
|
||
{
|
||
"name": "Test inventory",
|
||
"company_id": company_id,
|
||
"location_ids": [(4, location_id)],
|
||
"start_empty": True,
|
||
}
|
||
)
|
||
inventory.action_start()
|
||
return inventory
|
||
|
||
def _create_inventory_line(self, inventory_id, product_id, location_id, qty):
|
||
self.env["stock.inventory.line"].create(
|
||
{
|
||
"inventory_id": inventory_id,
|
||
"product_id": product_id,
|
||
"location_id": location_id,
|
||
"product_qty": qty,
|
||
}
|
||
)
|
||
|
||
def create_inventory(self, product_id, qty, location_id=None, company_id=None):
|
||
if location_id is None:
|
||
location_id = self.wh_main.lot_stock_id.id
|
||
|
||
if company_id is None:
|
||
company_id = self.main_company.id
|
||
|
||
inventory = self._create_inventory(location_id, company_id)
|
||
self._create_inventory_line(inventory.id, product_id, location_id, qty)
|
||
inventory._action_done()
|
||
|
||
def create_simple_bom(
|
||
self, product, sub_product, product_qty=1, sub_product_qty=1, routing_id=False
|
||
):
|
||
bom = self.bom_model.create(
|
||
{
|
||
"product_tmpl_id": product.product_tmpl_id.id,
|
||
"product_id": product.id,
|
||
"product_qty": product_qty,
|
||
"routing_id": routing_id,
|
||
}
|
||
)
|
||
self.bom_line_model.create(
|
||
{
|
||
"bom_id": bom.id,
|
||
"product_id": sub_product.id,
|
||
"product_qty": sub_product_qty,
|
||
}
|
||
)
|
||
|
||
return bom
|
||
|
||
def assertPotentialQty(self, record, qty, msg):
|
||
record.refresh()
|
||
# Check the potential
|
||
self.assertEqual(record.potential_qty, qty, msg)
|
||
# Check the variation of quantity available for sale
|
||
self.assertEqual(
|
||
(record.immediately_usable_qty - self.initial_usable_qties[record.id]),
|
||
qty,
|
||
msg,
|
||
)
|
||
|
||
def test_potential_qty_no_bom(self):
|
||
# Check the potential when there's no BoM
|
||
self.assertPotentialQty(
|
||
self.product_wo_bom, 0.0, "The potential without a BoM should be 0"
|
||
)
|
||
|
||
def test_potential_qty_no_bom_for_company(self):
|
||
chicago_id = self.ref("stock.res_company_1")
|
||
# Receive 1000x Wood Panel owned by Chicago
|
||
self.create_inventory(
|
||
product_id=self.env.ref("mrp.product_product_wood_panel").id,
|
||
qty=1000.0,
|
||
location_id=self.wh_ch.lot_stock_id.id,
|
||
company_id=chicago_id,
|
||
)
|
||
# Put Bolt owned by Chicago for 1000x the 1st variant in main WH
|
||
self.create_inventory(
|
||
product_id=self.env.ref("mrp.product_product_computer_desk_bolt").id,
|
||
qty=1000.0,
|
||
location_id=self.wh_ch.lot_stock_id.id,
|
||
company_id=chicago_id,
|
||
)
|
||
self.assertPotentialQty(
|
||
self.tmpl, 250.0, "Wrong template potential after receiving components"
|
||
)
|
||
|
||
test_user = self.env["res.users"].create(
|
||
{
|
||
"name": "test_demo",
|
||
"login": "test_demo",
|
||
"company_id": self.main_company.id,
|
||
"company_ids": [(4, self.main_company.id), (4, chicago_id)],
|
||
"groups_id": [
|
||
(4, self.ref("stock.group_stock_user")),
|
||
(4, self.ref("mrp.group_mrp_user")),
|
||
],
|
||
}
|
||
)
|
||
|
||
bom = self.env["mrp.bom"].search([("product_tmpl_id", "=", self.tmpl.id)])
|
||
|
||
test_user_tmpl = self.tmpl.with_user(test_user)
|
||
self.assertPotentialQty(
|
||
test_user_tmpl, 250.0, "Simple user can access to the potential_qty"
|
||
)
|
||
|
||
# Set the bom on the main company (visible to members of main company)
|
||
# and all products without company (visible to all)
|
||
# and the demo user on Chicago (child of main company)
|
||
self.env["product.product"].search([TRUE_LEAF]).write({"company_id": False})
|
||
test_user.write({"company_ids": [(6, 0, self.main_company.ids)]})
|
||
bom.company_id = self.main_company
|
||
self.assertPotentialQty(
|
||
test_user_tmpl,
|
||
0,
|
||
"The bom should not be visible to non members of the bom's "
|
||
"company or company child of the bom's company",
|
||
)
|
||
bom.company_id = chicago_id
|
||
test_user.write({"company_ids": [(4, chicago_id)]})
|
||
self.assertPotentialQty(test_user_tmpl, 250.0, "")
|
||
|
||
def test_potential_qty(self):
|
||
for i in [self.tmpl, self.var1, self.var2]:
|
||
self.assertPotentialQty(i, 0.0, "The potential quantity should start at 0")
|
||
|
||
# Receive 1000x Wood Panel
|
||
self.create_inventory(
|
||
product_id=self.env.ref("mrp.product_product_wood_panel").id,
|
||
qty=1000.0,
|
||
location_id=self.wh_main.lot_stock_id.id,
|
||
)
|
||
for i in [self.tmpl, self.var1, self.var2]:
|
||
self.assertPotentialQty(
|
||
i,
|
||
0.0,
|
||
"Receiving a single component should not change the "
|
||
"potential of %s" % i,
|
||
)
|
||
|
||
# Receive enough bolt to make 1000x the 1st variant in main WH
|
||
self.create_inventory(
|
||
product_id=self.env.ref("mrp.product_product_computer_desk_bolt").id,
|
||
qty=1000.0,
|
||
location_id=self.wh_main.lot_stock_id.id,
|
||
)
|
||
self.assertPotentialQty(
|
||
self.tmpl, 250.0, "Wrong template potential after receiving components"
|
||
)
|
||
self.assertPotentialQty(
|
||
self.var1, 250.0, "Wrong variant 1 potential after receiving components"
|
||
)
|
||
self.assertPotentialQty(
|
||
self.var2,
|
||
0.0,
|
||
"Receiving variant 1's component should not change "
|
||
"variant 2's potential",
|
||
)
|
||
|
||
# Receive enough components to make 213 the 2nd variant at Chicago
|
||
inventory = self._create_inventory(
|
||
self.wh_ch.lot_stock_id.id, self.ref("stock.res_company_1")
|
||
)
|
||
self._create_inventory_line(
|
||
inventory.id,
|
||
self.ref("mrp.product_product_wood_panel"),
|
||
self.wh_ch.lot_stock_id.id,
|
||
1000.0,
|
||
)
|
||
self._create_inventory_line(
|
||
inventory.id,
|
||
self.ref("stock_available_mrp.product_computer_desk_bolt_white"),
|
||
self.wh_ch.lot_stock_id.id,
|
||
852.0,
|
||
)
|
||
inventory._action_done()
|
||
self.assertPotentialQty(
|
||
self.tmpl.with_context(test=True),
|
||
250.0,
|
||
"Wrong template potential after receiving components",
|
||
)
|
||
self.assertPotentialQty(
|
||
self.var1,
|
||
250.0,
|
||
"Receiving variant 2's component should not change "
|
||
"variant 1's potential",
|
||
)
|
||
self.assertPotentialQty(
|
||
self.var2, 213.0, "Wrong variant 2 potential after receiving components"
|
||
)
|
||
# Check by warehouse
|
||
self.assertPotentialQty(
|
||
self.tmpl.with_context(warehouse=self.wh_main.id),
|
||
250.0,
|
||
"Wrong potential quantity in main WH",
|
||
)
|
||
self.assertPotentialQty(
|
||
self.tmpl.with_context(warehouse=self.wh_ch.id),
|
||
213.0,
|
||
"Wrong potential quantity in Chicago WH",
|
||
)
|
||
# Check by location
|
||
self.assertPotentialQty(
|
||
self.tmpl.with_context(location=self.wh_main.lot_stock_id.id),
|
||
250.0,
|
||
"Wrong potential quantity in main WH location",
|
||
)
|
||
self.assertPotentialQty(
|
||
self.tmpl.with_context(location=self.wh_ch.lot_stock_id.id),
|
||
213.0,
|
||
"Wrong potential quantity in Chicago WH location",
|
||
)
|
||
|
||
def test_multi_unit_recursive_bom(self):
|
||
# Test multi-level and multi-units BOM
|
||
uom_unit = self.env.ref("uom.product_uom_unit")
|
||
uom_unit.rounding = 1.0
|
||
p1 = self.product_model.create(
|
||
{
|
||
"name": "Test product with BOM",
|
||
"type": "product",
|
||
"uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
p2 = self.product_model.create(
|
||
{
|
||
"name": "Test sub product with BOM",
|
||
"type": "consu",
|
||
"uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
p3 = self.product_model.create(
|
||
{
|
||
"name": "Test component",
|
||
"type": "product",
|
||
"uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
bom_p1 = self.bom_model.create(
|
||
{"product_tmpl_id": p1.product_tmpl_id.id, "product_id": p1.id}
|
||
)
|
||
|
||
self.bom_line_model.create(
|
||
{
|
||
"bom_id": bom_p1.id,
|
||
"product_id": p3.id,
|
||
"product_qty": 1,
|
||
"product_uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
# Two p2 which have a bom
|
||
self.bom_line_model.create(
|
||
{
|
||
"bom_id": bom_p1.id,
|
||
"product_id": p2.id,
|
||
"product_qty": 2,
|
||
"product_uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
bom_p2 = self.bom_model.create(
|
||
{
|
||
"product_tmpl_id": p2.product_tmpl_id.id,
|
||
"product_id": p2.id,
|
||
"type": "phantom",
|
||
}
|
||
)
|
||
|
||
# p2 need 2 unit of component
|
||
self.bom_line_model.create(
|
||
{
|
||
"bom_id": bom_p2.id,
|
||
"product_id": p3.id,
|
||
"product_qty": 2,
|
||
"product_uom_id": self.env.ref("uom.product_uom_unit").id,
|
||
}
|
||
)
|
||
|
||
p1.refresh()
|
||
|
||
# Need a least 5 units for one P1
|
||
self.assertEqual(0, p1.potential_qty)
|
||
|
||
self.create_inventory(p3.id, 1)
|
||
p1.refresh()
|
||
self.assertEqual(0, p1.potential_qty)
|
||
|
||
self.create_inventory(p3.id, 3)
|
||
p1.refresh()
|
||
self.assertEqual(0, p1.potential_qty)
|
||
|
||
self.create_inventory(p3.id, 5)
|
||
p1.refresh()
|
||
self.assertEqual(1.0, p1.potential_qty)
|
||
|
||
self.create_inventory(p3.id, 6)
|
||
p1.refresh()
|
||
self.assertEqual(1.0, p1.potential_qty)
|
||
|
||
self.create_inventory(p3.id, 10)
|
||
p1.refresh()
|
||
self.assertEqual(2.0, p1.potential_qty)
|
||
|
||
def test_potential_qty_list(self):
|
||
# Try to highlight a bug when _get_potential_qty is called on
|
||
# a recordset with multiple products
|
||
# Recursive compute is not working
|
||
|
||
p1 = self.product_model.create({"name": "Test P1"})
|
||
p2 = self.product_model.create({"name": "Test P2"})
|
||
p3 = self.product_model.create({"name": "Test P3", "type": "product"})
|
||
|
||
self.config.set_param("stock_available_mrp_based_on", "immediately_usable_qty")
|
||
|
||
# P1 need one P2
|
||
self.create_simple_bom(p1, p2)
|
||
# P2 need one P3
|
||
self.create_simple_bom(p2, p3)
|
||
|
||
self.create_inventory(p3.id, 3)
|
||
|
||
self.product_model.invalidate_cache()
|
||
|
||
products = self.product_model.search([("id", "in", [p1.id, p2.id, p3.id])])
|
||
|
||
self.assertEqual(
|
||
{p1.id: 3.0, p2.id: 3.0, p3.id: 0.0},
|
||
{p.id: p.potential_qty for p in products},
|
||
)
|