mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
When you procure a kit, it doesn't matter what the route configuration is on the product itself but on its children, so it is not needed to check anything else but the BoM to assign the 'phantom' supply method. Also do not assign a BoM when the supply method is not phantom or manufacture.
919 lines
37 KiB
Python
919 lines
37 KiB
Python
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
|
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from odoo import fields
|
|
|
|
from .common import TestMrpMultiLevelCommon
|
|
|
|
|
|
class TestMrpMultiLevel(TestMrpMultiLevelCommon):
|
|
def test_01_mrp_levels(self):
|
|
"""Tests computation of MRP levels."""
|
|
self.assertEqual(self.fp_1.llc, 0)
|
|
self.assertEqual(self.fp_2.llc, 0)
|
|
self.assertEqual(self.sf_1.llc, 1)
|
|
self.assertEqual(self.sf_2.llc, 1)
|
|
self.assertEqual(self.pp_1.llc, 2)
|
|
self.assertEqual(self.pp_2.llc, 2)
|
|
|
|
def test_02_product_mrp_area(self):
|
|
"""Tests that mrp products are generated correctly."""
|
|
product_mrp_area = self.product_mrp_area_obj.search(
|
|
[("product_id", "=", self.pp_1.id)]
|
|
)
|
|
self.assertEqual(product_mrp_area.supply_method, "buy")
|
|
self.assertEqual(product_mrp_area.main_supplier_id, self.vendor)
|
|
self.assertEqual(product_mrp_area.qty_available, 10.0)
|
|
product_mrp_area = self.product_mrp_area_obj.search(
|
|
[("product_id", "=", self.sf_1.id)]
|
|
)
|
|
self.assertEqual(product_mrp_area.supply_method, "manufacture")
|
|
self.assertFalse(product_mrp_area.main_supplier_id)
|
|
self.assertFalse(product_mrp_area.main_supplierinfo_id)
|
|
# Archiving the product should archive parameters:
|
|
self.assertTrue(product_mrp_area.active)
|
|
self.sf_1.active = False
|
|
self.assertFalse(product_mrp_area.active)
|
|
|
|
def test_03_mrp_moves(self):
|
|
"""Tests for mrp moves generated."""
|
|
moves = self.mrp_move_obj.search([("product_id", "=", self.pp_1.id)])
|
|
self.assertEqual(len(moves), 3)
|
|
self.assertNotIn("s", moves.mapped("mrp_type"))
|
|
for move in moves:
|
|
self.assertTrue(move.planned_order_up_ids)
|
|
if move.planned_order_up_ids.product_mrp_area_id.product_id == self.fp_1:
|
|
# Demand coming from FP-1
|
|
self.assertEqual(move.planned_order_up_ids.mrp_action, "manufacture")
|
|
self.assertEqual(move.mrp_qty, -200.0)
|
|
elif move.planned_order_up_ids.product_mrp_area_id.product_id == self.sf_1:
|
|
# Demand coming from FP-2 -> SF-1
|
|
self.assertEqual(move.planned_order_up_ids.mrp_action, "manufacture")
|
|
if move.mrp_date == self.date_5:
|
|
self.assertEqual(move.mrp_qty, -90.0)
|
|
elif move.mrp_date == self.date_8:
|
|
self.assertEqual(move.mrp_qty, -72.0)
|
|
# Check actions:
|
|
planned_orders = self.planned_order_obj.search(
|
|
[("product_id", "=", self.pp_1.id)]
|
|
)
|
|
self.assertEqual(len(planned_orders), 3)
|
|
for plan in planned_orders:
|
|
self.assertEqual(plan.mrp_action, "buy")
|
|
# Check PP-2 PO being accounted:
|
|
po_move = self.mrp_move_obj.search(
|
|
[("product_id", "=", self.pp_2.id), ("mrp_type", "=", "s")]
|
|
)
|
|
self.assertEqual(len(po_move), 1)
|
|
self.assertEqual(po_move.purchase_order_id, self.po)
|
|
self.assertEqual(po_move.purchase_line_id, self.po.order_line)
|
|
|
|
def test_04_mrp_multi_level(self):
|
|
"""Tests MRP inventories created."""
|
|
# FP-1
|
|
fp_1_inventory_lines = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.fp_1.id)]
|
|
)
|
|
self.assertEqual(len(fp_1_inventory_lines), 1)
|
|
self.assertEqual(fp_1_inventory_lines.date, self.date_7)
|
|
self.assertEqual(fp_1_inventory_lines.demand_qty, 100.0)
|
|
self.assertEqual(fp_1_inventory_lines.to_procure, 100.0)
|
|
# FP-2
|
|
fp_2_line_1 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.fp_2.id),
|
|
("date", "=", self.date_7),
|
|
]
|
|
)
|
|
self.assertEqual(len(fp_2_line_1), 1)
|
|
self.assertEqual(fp_2_line_1.demand_qty, 15.0)
|
|
self.assertEqual(fp_2_line_1.to_procure, 15.0)
|
|
fp_2_line_2 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.fp_2.id),
|
|
("date", "=", self.date_10),
|
|
]
|
|
)
|
|
self.assertEqual(len(fp_2_line_2), 1)
|
|
self.assertEqual(fp_2_line_2.demand_qty, 0.0)
|
|
self.assertEqual(fp_2_line_2.to_procure, 0.0)
|
|
self.assertEqual(fp_2_line_2.supply_qty, 12.0)
|
|
|
|
# SF-1
|
|
sf_1_line_1 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.sf_1.id),
|
|
("date", "=", self.date_6),
|
|
]
|
|
)
|
|
self.assertEqual(len(sf_1_line_1), 1)
|
|
self.assertEqual(sf_1_line_1.demand_qty, 30.0)
|
|
self.assertEqual(sf_1_line_1.to_procure, 30.0)
|
|
sf_1_line_2 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.sf_1.id),
|
|
("date", "=", self.date_9),
|
|
]
|
|
)
|
|
self.assertEqual(len(sf_1_line_2), 1)
|
|
self.assertEqual(sf_1_line_2.demand_qty, 24.0)
|
|
self.assertEqual(sf_1_line_2.to_procure, 24.0)
|
|
# SF-2
|
|
sf_2_line_1 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.sf_2.id),
|
|
("date", "=", self.date_6),
|
|
]
|
|
)
|
|
self.assertEqual(len(sf_2_line_1), 1)
|
|
self.assertEqual(sf_2_line_1.demand_qty, 45.0)
|
|
self.assertEqual(sf_2_line_1.to_procure, 30.0)
|
|
sf_2_line_2 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.sf_2.id),
|
|
("date", "=", self.date_9),
|
|
]
|
|
)
|
|
self.assertEqual(len(sf_2_line_2), 1)
|
|
self.assertEqual(sf_2_line_2.demand_qty, 36.0)
|
|
self.assertEqual(sf_2_line_2.to_procure, 36.0)
|
|
|
|
# PP-1
|
|
pp_1_line_1 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_1.id),
|
|
("date", "=", self.date_5),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_1_line_1), 1)
|
|
self.assertEqual(pp_1_line_1.demand_qty, 290.0)
|
|
self.assertEqual(pp_1_line_1.to_procure, 280.0)
|
|
pp_1_line_2 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_1.id),
|
|
("date", "=", self.date_8),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_1_line_2), 1)
|
|
self.assertEqual(pp_1_line_2.demand_qty, 72.0)
|
|
self.assertEqual(pp_1_line_2.to_procure, 72.0)
|
|
# PP-2
|
|
pp_2_line_1 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_2.id),
|
|
("date", "=", self.date_3),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_2_line_1), 1)
|
|
self.assertEqual(pp_2_line_1.demand_qty, 90.0)
|
|
# 90.0 demand - 20.0 on hand - 5.0 on PO = 65.0
|
|
self.assertEqual(pp_2_line_1.to_procure, 65.0)
|
|
pp_2_line_2 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_2.id),
|
|
("date", "=", self.date_5),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_2_line_2), 1)
|
|
self.assertEqual(pp_2_line_2.demand_qty, 360.0)
|
|
self.assertEqual(pp_2_line_2.to_procure, 360.0)
|
|
pp_2_line_3 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_2.id),
|
|
("date", "=", self.date_6),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_2_line_3), 1)
|
|
self.assertEqual(pp_2_line_3.demand_qty, 108.0)
|
|
self.assertEqual(pp_2_line_3.to_procure, 108.0)
|
|
pp_2_line_4 = self.mrp_inventory_obj.search(
|
|
[
|
|
("product_mrp_area_id.product_id", "=", self.pp_2.id),
|
|
("date", "=", self.date_8),
|
|
]
|
|
)
|
|
self.assertEqual(len(pp_2_line_4), 1)
|
|
self.assertEqual(pp_2_line_4.demand_qty, 48.0)
|
|
self.assertEqual(pp_2_line_4.to_procure, 48.0)
|
|
|
|
def test_05_planned_availability(self):
|
|
"""Test planned availability computation."""
|
|
# Running availability for PP-1:
|
|
invs = self.mrp_inventory_obj.search(
|
|
[("product_id", "=", self.pp_1.id)], order="date"
|
|
)
|
|
self.assertEqual(len(invs), 2)
|
|
expected = [0.0, 0.0] # No grouping, lot size nor safety stock.
|
|
self.assertEqual(invs.mapped("running_availability"), expected)
|
|
|
|
def test_06_procure_mo(self):
|
|
"""Test procurement wizard with MOs."""
|
|
mos = self.mo_obj.search([("product_id", "=", self.fp_1.id)])
|
|
self.assertFalse(mos)
|
|
mrp_inv = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.fp_1.id)]
|
|
)
|
|
self.mrp_inventory_procure_wiz.with_context(
|
|
active_model="mrp.inventory",
|
|
active_ids=mrp_inv.ids,
|
|
active_id=mrp_inv.id,
|
|
).create({}).make_procurement()
|
|
mos = self.mo_obj.search([("product_id", "=", self.fp_1.id)])
|
|
self.assertTrue(mos)
|
|
self.assertEqual(mos.product_qty, 100.0)
|
|
mo_date_start = fields.Date.to_date(mos.date_planned_start)
|
|
self.assertEqual(mo_date_start, self.date_5)
|
|
|
|
def test_07_adjust_qty_to_order(self):
|
|
"""Test the adjustments made to the qty to procure when minimum,
|
|
maximum order quantities and quantity multiple are set."""
|
|
# minimum order quantity:
|
|
mrp_inv_min = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.prod_min.id)]
|
|
)
|
|
self.assertEqual(mrp_inv_min.to_procure, 50.0)
|
|
# maximum order quantity:
|
|
mrp_inv_max = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.prod_max.id)]
|
|
)
|
|
self.assertEqual(mrp_inv_max.to_procure, 150)
|
|
plans = self.planned_order_obj.search([("product_id", "=", self.prod_max.id)])
|
|
self.assertEqual(len(plans), 2)
|
|
self.assertIn(100.0, plans.mapped("mrp_qty"))
|
|
self.assertIn(50.0, plans.mapped("mrp_qty"))
|
|
# quantity multiple:
|
|
mrp_inv_multiple = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.prod_multiple.id)]
|
|
)
|
|
self.assertEqual(mrp_inv_multiple.to_procure, 125)
|
|
|
|
def test_08_group_demand(self):
|
|
"""Test demand grouping functionality, `nbr_days`."""
|
|
pickings = self.stock_picking_obj.search(
|
|
[
|
|
("product_id", "=", self.prod_test.id),
|
|
("location_id", "=", self.sec_loc.id),
|
|
]
|
|
)
|
|
self.assertEqual(len(pickings), 5)
|
|
moves = self.mrp_move_obj.search(
|
|
[
|
|
("product_id", "=", self.prod_test.id),
|
|
("mrp_area_id", "=", self.secondary_area.id),
|
|
]
|
|
)
|
|
supply_plans = self.planned_order_obj.search(
|
|
[
|
|
("product_id", "=", self.prod_test.id),
|
|
("mrp_area_id", "=", self.secondary_area.id),
|
|
]
|
|
)
|
|
moves_demand = moves.filtered(lambda m: m.mrp_type == "d")
|
|
self.assertEqual(len(moves_demand), 5)
|
|
# two groups expected:
|
|
# 1. days 8, 9 and 10.
|
|
# 2. days 20, and 22.
|
|
self.assertEqual(len(supply_plans), 2)
|
|
quantities = supply_plans.mapped("mrp_qty")
|
|
week_1_expected = sum(moves_demand[0:3].mapped("mrp_qty"))
|
|
self.assertIn(abs(week_1_expected), quantities)
|
|
week_2_expected = sum(moves_demand[3:].mapped("mrp_qty"))
|
|
self.assertIn(abs(week_2_expected), quantities)
|
|
|
|
def test_09_isolated_mrp_area_run(self):
|
|
"""Test running MRP for just one area."""
|
|
self.mrp_multi_level_wiz.with_user(self.mrp_manager).create(
|
|
{"mrp_area_ids": [(6, 0, self.secondary_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
this = self.mrp_inventory_obj.search(
|
|
[("mrp_area_id", "=", self.secondary_area.id)], limit=1
|
|
)
|
|
self.assertTrue(this)
|
|
# Only recently exectued areas should have been created by test user:
|
|
self.assertEqual(this.create_uid, self.mrp_manager)
|
|
prev = self.mrp_inventory_obj.search(
|
|
[("mrp_area_id", "!=", self.secondary_area.id)], limit=1
|
|
)
|
|
self.assertNotEqual(this.create_uid, prev.create_uid)
|
|
|
|
def test_11_special_scenario_1(self):
|
|
"""When grouping demand supply and demand are in the same day but
|
|
supply goes first."""
|
|
moves = self.mrp_move_obj.search(
|
|
[("product_id", "=", self.product_scenario_1.id)]
|
|
)
|
|
self.assertEqual(len(moves), 4)
|
|
mrp_invs = self.mrp_inventory_obj.search(
|
|
[("product_id", "=", self.product_scenario_1.id)]
|
|
)
|
|
self.assertEqual(len(mrp_invs), 2)
|
|
# Net needs = 124 + 90 - 87 = 127 -> 130 (because of qty multiple)
|
|
self.assertEqual(mrp_invs[0].to_procure, 130)
|
|
# Net needs = 18, available on-hand = 3 -> 15
|
|
self.assertEqual(mrp_invs[1].to_procure, 15)
|
|
|
|
def test_12_bom_line_attribute_value_skip(self):
|
|
"""Check for the correct demand on components of a product with
|
|
multiple variants"""
|
|
product_4b_demand = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.product_4b.id)]
|
|
)
|
|
self.assertTrue(product_4b_demand)
|
|
self.assertEqual(product_4b_demand.to_procure, 100)
|
|
product_4c_demand = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.product_4c.id)]
|
|
)
|
|
self.assertTrue(product_4c_demand)
|
|
self.assertEqual(product_4c_demand.to_procure, 1)
|
|
# Testing variant BoM
|
|
# Supply of one unit for AV-12 or AV-21
|
|
av_12_supply = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.av_12.id)]
|
|
)
|
|
self.assertEqual(av_12_supply.to_procure, 1.0)
|
|
av_21_supply = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.av_21.id)]
|
|
)
|
|
self.assertEqual(av_21_supply.to_procure, 1.0)
|
|
# Testing template BoM
|
|
# Supply of 150 units for AV-11 and AV-22
|
|
av_11_supply = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.av_11.id)]
|
|
)
|
|
self.assertEqual(av_11_supply.to_procure, 100.0)
|
|
av_22_supply = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.av_22.id)]
|
|
)
|
|
self.assertTrue(av_22_supply.to_procure, 100.0)
|
|
|
|
def test_13_timezone_handling(self):
|
|
self.calendar.tz = "Australia/Sydney" # Oct-Apr/Apr-Oct: UTC+11/UTC+10
|
|
date_move = datetime(2090, 4, 19, 20, 00) # Apr 20 6/7 am in Sidney
|
|
sidney_date = date(2090, 4, 20)
|
|
self._create_picking_in(
|
|
self.product_tz, 10.0, date_move, location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", self.product_tz.id),
|
|
]
|
|
)
|
|
self.assertEqual(len(inventory), 1)
|
|
self.assertEqual(inventory.date, sidney_date)
|
|
|
|
def test_14_timezone_not_set(self):
|
|
self.wh.calendar_id = False
|
|
date_move = datetime(2090, 4, 19, 20, 00)
|
|
self._create_picking_in(
|
|
self.product_tz, 10.0, date_move, location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", self.product_tz.id),
|
|
]
|
|
)
|
|
self.assertEqual(len(inventory), 1)
|
|
self.assertEqual(inventory.date, date_move.date())
|
|
|
|
def test_15_units_case(self):
|
|
"""When a product has a different purchase unit of measure than
|
|
the general unit of measure and the supply is coming from an RFQ"""
|
|
prod_uom_test_inventory_lines = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.prod_uom_test.id)]
|
|
)
|
|
self.assertEqual(len(prod_uom_test_inventory_lines), 1)
|
|
self.assertEqual(prod_uom_test_inventory_lines.supply_qty, 12.0)
|
|
# Supply qty has to be 12 has a dozen of units are in a RFQ.
|
|
|
|
def test_16_phantom_comp_planning(self):
|
|
"""
|
|
Phantom components will not appear in MRP Inventory or Planned Orders.
|
|
MRP Parameter will have 'phantom' supply method.
|
|
"""
|
|
# SF-3
|
|
sf_3_line_1 = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.sf_3.id)]
|
|
)
|
|
self.assertEqual(len(sf_3_line_1), 0)
|
|
sf_3_planned_order_1 = self.planned_order_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.sf_3.id)]
|
|
)
|
|
self.assertEqual(sf_3_planned_order_1.mrp_action, "phantom")
|
|
self.assertEqual(sf_3_planned_order_1.mrp_qty, 10.0)
|
|
# PP-3
|
|
pp_3_line_1 = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
|
|
)
|
|
self.assertEqual(len(pp_3_line_1), 1)
|
|
self.assertEqual(pp_3_line_1.demand_qty, 20.0)
|
|
pp_3_planned_orders = self.planned_order_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
|
|
)
|
|
self.assertEqual(len(pp_3_planned_orders), 2)
|
|
# PP-4
|
|
pp_4_line_1 = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_4.id)]
|
|
)
|
|
self.assertEqual(len(pp_4_line_1), 1)
|
|
self.assertEqual(pp_4_line_1.demand_qty, 30.0)
|
|
pp_4_planned_orders = self.planned_order_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_4.id)]
|
|
)
|
|
self.assertEqual(len(pp_4_planned_orders), 1)
|
|
|
|
def test_17_supply_method(self):
|
|
"""Test supply method computation."""
|
|
self.fp_4.route_ids = [(5, 0, 0)]
|
|
product_mrp_area = self.product_mrp_area_obj.search(
|
|
[("product_id", "=", self.fp_4.id)]
|
|
)
|
|
self.assertEqual(product_mrp_area.supply_method, "none")
|
|
self.fp_4.route_ids = [(4, self.env.ref("stock.route_warehouse0_mto").id)]
|
|
product_mrp_area._compute_supply_method()
|
|
self.assertEqual(product_mrp_area.supply_method, "pull")
|
|
self.fp_4.route_ids = [(4, self.env.ref("mrp.route_warehouse0_manufacture").id)]
|
|
product_mrp_area._compute_supply_method()
|
|
self.assertEqual(product_mrp_area.supply_method, "manufacture")
|
|
self.fp_4.route_ids = [
|
|
(4, self.env.ref("purchase_stock.route_warehouse0_buy").id)
|
|
]
|
|
product_mrp_area._compute_supply_method()
|
|
self.assertEqual(product_mrp_area.supply_method, "buy")
|
|
kit_bom = self.mrp_bom_obj.create(
|
|
{
|
|
"product_tmpl_id": self.fp_4.product_tmpl_id.id,
|
|
"product_id": self.fp_4.id,
|
|
"type": "phantom",
|
|
}
|
|
)
|
|
product_mrp_area._compute_supply_method()
|
|
self.assertEqual(product_mrp_area.supply_method, "phantom")
|
|
self.assertEqual(product_mrp_area.supply_bom_id, kit_bom)
|
|
|
|
def test_18_priorize_safety_stock(self):
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
"mrp_applicable": True, # needed?
|
|
}
|
|
)
|
|
self._create_picking_out(
|
|
product, 6.0, now + timedelta(days=3), location=self.cases_loc
|
|
)
|
|
self._create_picking_in(
|
|
product, 10.0, now + timedelta(days=7), location=self.cases_loc
|
|
)
|
|
self._create_picking_out(
|
|
product, 12.0, now + timedelta(days=14), location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", product.id),
|
|
]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date(),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 5.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 10.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=3),
|
|
"demand_qty": 6.0,
|
|
"final_on_hand_qty": -1.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 6.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=7),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 9.0,
|
|
"initial_on_hand_qty": -1.0,
|
|
"running_availability": 25.0,
|
|
"supply_qty": 10.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=14),
|
|
"demand_qty": 12.0,
|
|
"final_on_hand_qty": -3.0,
|
|
"initial_on_hand_qty": 9.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 2.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_19_on_hand_with_lots(self):
|
|
"""Check that on-hand is correctly computed when tracking by lots."""
|
|
lots_line_1 = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.product_lots.id)]
|
|
)
|
|
self.assertEqual(len(lots_line_1), 1)
|
|
self.assertEqual(lots_line_1.initial_on_hand_qty, 210)
|
|
self.assertEqual(lots_line_1.final_on_hand_qty, 185)
|
|
|
|
def test_20_prioritize_safety_stock_grouped_1(self):
|
|
"""Test grouped demand MRP but with a short nbr days.
|
|
Safety stock should be ordered."""
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
"mrp_nbr_days": 2,
|
|
}
|
|
)
|
|
self._create_picking_out(
|
|
product, 6.0, now + timedelta(days=3), location=self.cases_loc
|
|
)
|
|
self._create_picking_in(
|
|
product, 10.0, now + timedelta(days=7), location=self.cases_loc
|
|
)
|
|
self._create_picking_out(
|
|
product, 12.0, now + timedelta(days=14), location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", product.id),
|
|
]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date(),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 5.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 10.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=3),
|
|
"demand_qty": 6.0,
|
|
"final_on_hand_qty": -1.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 6.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=7),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 9.0,
|
|
"initial_on_hand_qty": -1.0,
|
|
"running_availability": 25.0,
|
|
"supply_qty": 10.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=14),
|
|
"demand_qty": 12.0,
|
|
"final_on_hand_qty": -3.0,
|
|
"initial_on_hand_qty": 9.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 2.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_21_prioritize_safety_stock_grouped_2(self):
|
|
"""Test grouped demand MRP but with a longer nbr days.
|
|
Safety stock should be ordered."""
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
"mrp_nbr_days": 7,
|
|
}
|
|
)
|
|
self._create_picking_out(
|
|
product, 6.0, now + timedelta(days=3), location=self.cases_loc
|
|
)
|
|
self._create_picking_in(
|
|
product, 10.0, now + timedelta(days=7), location=self.cases_loc
|
|
)
|
|
self._create_picking_out(
|
|
product, 12.0, now + timedelta(days=12), location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", product.id),
|
|
]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date(),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 5.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 21.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 16.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=3),
|
|
"demand_qty": 6.0,
|
|
"final_on_hand_qty": -1.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=7),
|
|
"demand_qty": 0.0,
|
|
"final_on_hand_qty": 9.0,
|
|
"initial_on_hand_qty": -1.0,
|
|
"running_availability": 27.0,
|
|
"supply_qty": 10.0,
|
|
"to_procure": 2.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=12),
|
|
"demand_qty": 12.0,
|
|
"final_on_hand_qty": -3.0,
|
|
"initial_on_hand_qty": 9.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_22_prioritize_safety_stock_grouped_3(self):
|
|
"""Test grouped demand MRP but with an existing incoming supply
|
|
Safety stock should NOT be ordered."""
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
"mrp_nbr_days": 7,
|
|
}
|
|
)
|
|
self._create_picking_in(
|
|
product, 30.0, now + timedelta(days=3), location=self.cases_loc
|
|
)
|
|
self._create_picking_out(
|
|
product, 6.0, now + timedelta(days=7), location=self.cases_loc
|
|
)
|
|
self._create_picking_out(
|
|
product, 12.0, now + timedelta(days=12), location=self.cases_loc
|
|
)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[
|
|
("mrp_area_id", "=", self.cases_area.id),
|
|
("product_id", "=", product.id),
|
|
]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date() + timedelta(days=3),
|
|
"demand_qty": 0.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"final_on_hand_qty": 35.0,
|
|
"running_availability": 35.0,
|
|
"supply_qty": 30.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=7),
|
|
"demand_qty": 6.0,
|
|
"initial_on_hand_qty": 35.0,
|
|
"final_on_hand_qty": 29.0,
|
|
"running_availability": 29.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
{
|
|
"date": now.date() + timedelta(days=12),
|
|
"demand_qty": 12.0,
|
|
"initial_on_hand_qty": 29.0,
|
|
"final_on_hand_qty": 17.0,
|
|
"running_availability": 17.0,
|
|
"supply_qty": 0.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_23_prioritize_safety_stock_with_mrp_moves_today(self):
|
|
"""Test MRP but with moves today. Safety stock should not be ordered."""
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
}
|
|
)
|
|
self._create_picking_out(product, 10.0, now, location=self.cases_loc)
|
|
self._create_picking_in(product, 20.0, now, location=self.cases_loc)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date(),
|
|
"demand_qty": 10.0,
|
|
"final_on_hand_qty": 15.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 20.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_24_prioritize_safety_stock_with_mrp_moves_today_grouped(self):
|
|
"""Test grouped demand MRP but with moves today. Safety stock should not be ordered."""
|
|
now = datetime.now()
|
|
product = self.prod_test # has Buy route
|
|
product.seller_ids[0].delay = 2 # set a purchase lead time
|
|
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
|
|
self.product_mrp_area_obj.create(
|
|
{
|
|
"product_id": product.id,
|
|
"mrp_area_id": self.cases_area.id,
|
|
"mrp_minimum_stock": 15,
|
|
"mrp_nbr_days": 2,
|
|
}
|
|
)
|
|
self._create_picking_out(product, 10.0, now, location=self.cases_loc)
|
|
self._create_picking_in(product, 20.0, now, location=self.cases_loc)
|
|
self.mrp_multi_level_wiz.create(
|
|
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
|
|
).run_mrp_multi_level()
|
|
inventory = self.mrp_inventory_obj.search(
|
|
[("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)]
|
|
)
|
|
expected = [
|
|
{
|
|
"date": now.date(),
|
|
"demand_qty": 10.0,
|
|
"final_on_hand_qty": 15.0,
|
|
"initial_on_hand_qty": 5.0,
|
|
"running_availability": 15.0,
|
|
"supply_qty": 20.0,
|
|
"to_procure": 0.0,
|
|
},
|
|
]
|
|
self.assertEqual(len(expected), len(inventory))
|
|
for test_vals, inv in zip(expected, inventory):
|
|
for key in test_vals:
|
|
self.assertEqual(
|
|
test_vals[key],
|
|
inv[key],
|
|
f"unexpected value for {key}: {inv[key]} "
|
|
f"(expected {test_vals[key]} on {inv.date})",
|
|
)
|
|
|
|
def test_25_phantom_comp_on_hand(self):
|
|
"""
|
|
A phantom product with positive qty_available (which is computed from the
|
|
availability of its components) should not satisfy demand, because this leads
|
|
to double counting qty_available of its component products.
|
|
"""
|
|
quant = self.quant_obj.sudo().create(
|
|
{
|
|
"product_id": self.pp_3.id,
|
|
"inventory_quantity": 10.0,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
quant.action_apply_inventory()
|
|
quant = self.quant_obj.sudo().create(
|
|
{
|
|
"product_id": self.pp_4.id,
|
|
"inventory_quantity": 30.0,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
quant.action_apply_inventory()
|
|
self.assertEqual(self.sf_3.qty_available, 10.0)
|
|
self.mrp_multi_level_wiz.create({}).run_mrp_multi_level()
|
|
# PP-3
|
|
pp_3_line_1 = self.mrp_inventory_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
|
|
)
|
|
self.assertEqual(len(pp_3_line_1), 1)
|
|
self.assertEqual(pp_3_line_1.demand_qty, 20.0)
|
|
self.assertEqual(pp_3_line_1.to_procure, 10.0)
|
|
pp_3_planned_orders = self.planned_order_obj.search(
|
|
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
|
|
)
|
|
self.assertEqual(len(pp_3_planned_orders), 1)
|
|
self.assertEqual(pp_3_planned_orders.mrp_qty, 10)
|
|
sf3_planned_orders = self.env["mrp.planned.order"].search(
|
|
[("product_id", "=", self.sf_3.id)]
|
|
)
|
|
self.assertEqual(len(sf3_planned_orders), 1)
|
|
# Trying to procure a kit planned order will have no effect.
|
|
procure_wizard = (
|
|
self.env["mrp.inventory.procure"]
|
|
.with_context(
|
|
active_model="mrp.planned.order", active_ids=sf3_planned_orders.ids
|
|
)
|
|
.create({})
|
|
)
|
|
self.assertEqual(len(procure_wizard.item_ids), 0)
|