mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-19 09:12:02 +02:00
1305 lines
55 KiB
Python
1305 lines
55 KiB
Python
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
|
|
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
|
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
|
|
|
from datetime import datetime, time, timedelta
|
|
|
|
from odoo import fields
|
|
from odoo.exceptions import ValidationError
|
|
|
|
from .common import TestDdmrpCommon
|
|
|
|
|
|
class TestDdmrp(TestDdmrpCommon):
|
|
|
|
# TEST GROUP 1: ADU and Spikes
|
|
|
|
def test_01_adu_calculation_fixed(self):
|
|
"""Test fixed ADU assigned correctly with fixed method."""
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = 4
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_02_adu_calculation_past_120_days(self):
|
|
"""Test ADU calculation method that uses actual past stock moves,
|
|
excepting inventory loss moves.
|
|
"""
|
|
method = self.env.ref("ddmrp.adu_calculation_method_past_120")
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
self.assertEqual(self.buffer_a.adu, 0)
|
|
# Create past moves and process them.
|
|
days = 30
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
move_inv_loss = self.create_inventorylossA(date_move, 10)
|
|
self._do_move(move_inv_loss, date_move)
|
|
pick_out_1 = self.create_pickingoutA(date_move, 60)
|
|
self._do_picking(pick_out_1, date_move)
|
|
days = 60
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
pick_out_2 = self.create_pickingoutA(date_move, 60)
|
|
self._do_picking(pick_out_2, date_move)
|
|
# Compute ADU again and check result.
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = (60 + 60) / 120 # Doesn't include inventory loss qty
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_03_adu_calculation_window_past(self):
|
|
"""Test that the window considered to calculate the ADU is correct."""
|
|
self.warehouse.calendar_id = False
|
|
method = self.aducalcmethodModel.create(
|
|
{
|
|
"name": "Past actual demand (6 days)",
|
|
"method": "past",
|
|
"source_past": "actual",
|
|
"horizon_past": 6,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
# Today should be excluded
|
|
date_move_1 = datetime.today()
|
|
picking_1 = self.create_pickingoutA(date_move_1, 20)
|
|
self._do_picking(picking_1, date_move_1)
|
|
# The next moves should be considered
|
|
date_move_2 = datetime.today() - timedelta(days=1)
|
|
picking_2 = self.create_pickingoutA(date_move_2, 20)
|
|
self._do_picking(picking_2, date_move_2)
|
|
date_move_3 = datetime.today() - timedelta(days=4)
|
|
picking_3 = self.create_pickingoutA(date_move_3, 20)
|
|
self._do_picking(picking_3, date_move_3)
|
|
date_move_4 = datetime.today() - timedelta(days=6)
|
|
picking_4 = self.create_pickingoutA(date_move_4, 10)
|
|
self._do_picking(picking_4, date_move_4)
|
|
# This move should be ignored
|
|
date_move_5 = datetime.today() - timedelta(days=7)
|
|
picking_5 = self.create_pickingoutA(date_move_5, 12)
|
|
self._do_picking(picking_5, date_move_5)
|
|
|
|
# Check ADU:
|
|
self.buffer_a._calc_adu()
|
|
to_assert_value = (20 + 20 + 10) / 6
|
|
self.assertAlmostEqual(self.buffer_a.adu, to_assert_value, places=2)
|
|
|
|
def test_04_adu_calculation_window_past_calendar(self):
|
|
"""Test that the window considered to calculate the ADU is correct.
|
|
(With working days set)."""
|
|
self.warehouse.calendar_id = self.calendar
|
|
method = self.aducalcmethodModel.create(
|
|
{
|
|
"name": "Past actual demand (6 days)",
|
|
"method": "past",
|
|
"source_past": "actual",
|
|
"horizon_past": 6,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
# Today should be excluded
|
|
date_move_1 = datetime.today()
|
|
picking_1 = self.create_pickingoutA(date_move_1, 20)
|
|
self._do_picking(picking_1, date_move_1)
|
|
# The next moves should be considered
|
|
today = datetime.today()
|
|
# 8:00 AM is the start of working time for today.
|
|
# 7:59 AM will make odoo to use the start of woking time for previous
|
|
# days, which is 1:00 PM of yesterday.
|
|
day_dt = datetime.combine(today.date(), time(8, 0, 0))
|
|
days = 2
|
|
date_move_2 = self.calendar.plan_days(-1 * days - 1, day_dt)
|
|
picking_2 = self.create_pickingoutA(date_move_2, 20)
|
|
self._do_picking(picking_2, date_move_2)
|
|
days = 4
|
|
date_move_3 = self.calendar.plan_days(-1 * days - 1, day_dt)
|
|
picking_3 = self.create_pickingoutA(date_move_3, 20)
|
|
self._do_picking(picking_3, date_move_3)
|
|
days = 6
|
|
date_move_4 = self.calendar.plan_days(-1 * days - 1, day_dt)
|
|
picking_4 = self.create_pickingoutA(date_move_4, 10)
|
|
self._do_picking(picking_4, date_move_4)
|
|
# This move should be ignored
|
|
days = 7
|
|
date_move_5 = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
picking_5 = self.create_pickingoutA(date_move_5, 12)
|
|
self._do_picking(picking_5, date_move_5)
|
|
|
|
# Check ADU:
|
|
self.buffer_a._calc_adu()
|
|
to_assert_value = (20 + 20 + 10) / 6
|
|
self.assertAlmostEqual(self.buffer_a.adu, to_assert_value, places=2)
|
|
|
|
def test_05_adu_calculation_internal_past_120_days(self):
|
|
"""Test that internal moves will not affect ADU calculation."""
|
|
method = self.env.ref("ddmrp.adu_calculation_method_past_120")
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
self.assertEqual(self.buffer_a.adu, 0)
|
|
|
|
pickingInternals = self.pickingModel
|
|
days = 30
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
pickingInternals += self.create_pickinginternalA(date_move, 60)
|
|
days = 60
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
pickingInternals += self.create_pickinginternalA(date_move, 60)
|
|
for picking in pickingInternals:
|
|
picking.action_assign()
|
|
picking._action_done()
|
|
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
|
|
to_assert_value = 0
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_06_adu_calculation_future_120_days_actual(self):
|
|
"""Test ADU calculation method that uses actual future stock moves,
|
|
excepting inventory loss moves.
|
|
"""
|
|
method = self.aducalcmethodModel.create(
|
|
{
|
|
"name": "Future actual demand (120 days)",
|
|
"method": "future",
|
|
"source_future": "actual",
|
|
"horizon_future": 120,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
|
|
pickingOuts = self.pickingModel
|
|
days = 30
|
|
date_move = self.calendar.plan_days(+1 * days + 1, datetime.today())
|
|
move_inv_loss = self.create_inventorylossA(date_move, 10)
|
|
self._do_move(move_inv_loss, date_move)
|
|
pickingOuts += self.create_pickingoutA(date_move, 60)
|
|
days = 60
|
|
date_move = self.calendar.plan_days(+1 * days + 1, datetime.today())
|
|
pickingOuts += self.create_pickingoutA(date_move, 60)
|
|
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = (60 + 60) / 120 # Doesn't include inventory loss qty
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
# Create a move more than 120 days in the future
|
|
days = 150
|
|
date_move = self.calendar.plan_days(+1 * days + 1, datetime.today())
|
|
pickingOuts += self.create_pickingoutA(date_move, 1)
|
|
|
|
# The extra move should not affect to the average ADU
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_07_adu_calculation_future_120_days_estimated(self):
|
|
method = self.env.ref("ddmrp.adu_calculation_method_future_120")
|
|
self.estimateModel.create(
|
|
{
|
|
"manual_date_from": self.estimate_date_from,
|
|
"manual_date_to": self.estimate_date_to,
|
|
"product_id": self.productA.id,
|
|
"product_uom_qty": 120,
|
|
"product_uom": self.productA.uom_id.id,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = 120 / 120
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_08_adu_calculation_blended(self):
|
|
"""Test blended ADU calculation method."""
|
|
method = self.aducalcmethodModel.create(
|
|
{
|
|
"name": "Blended (120 d. actual past, 120 d. estimates future)",
|
|
"method": "blended",
|
|
"source_past": "actual",
|
|
"horizon_past": 120,
|
|
"factor_past": 0.5,
|
|
"source_future": "estimates",
|
|
"horizon_future": 120,
|
|
"factor_future": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
|
|
# Past. Generate past moves: 360 units / 120 days = 3 unit/day
|
|
days = 30
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
pick_out_1 = self.create_pickingoutA(date_move, 180)
|
|
self._do_picking(pick_out_1, date_move)
|
|
days = 60
|
|
date_move = self.calendar.plan_days(-1 * days - 1, datetime.today())
|
|
pick_out_2 = self.create_pickingoutA(date_move, 180)
|
|
self._do_picking(pick_out_2, date_move)
|
|
|
|
# Future. create estimate: 120 units / 120 days = 1 unit/day
|
|
self.estimateModel.create(
|
|
{
|
|
"manual_date_from": self.estimate_date_from,
|
|
"manual_date_to": self.estimate_date_to,
|
|
"product_id": self.productA.id,
|
|
"product_uom_qty": 120,
|
|
"product_uom": self.productA.uom_id.id,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = 3 * 0.5 + 1 * 0.5
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_09_adu_calculation_method_checks(self):
|
|
with self.assertRaises(ValidationError):
|
|
# missing horizon_past
|
|
self.aducalcmethodModel.create(
|
|
{
|
|
"name": "error horizon_past",
|
|
"method": "past",
|
|
"source_past": "actual",
|
|
"factor_past": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
# missing horizon_future
|
|
self.aducalcmethodModel.create(
|
|
{
|
|
"name": "error horizon_future",
|
|
"method": "future",
|
|
"source_future": "estimates",
|
|
"factor_future": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
# missing source_past
|
|
self.aducalcmethodModel.create(
|
|
{
|
|
"name": "error source_past",
|
|
"method": "past",
|
|
"horizon_past": 120,
|
|
"factor_past": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
# missing source_future
|
|
self.aducalcmethodModel.create(
|
|
{
|
|
"name": "error source_future",
|
|
"method": "future",
|
|
"horizon_future": 120,
|
|
"factor_future": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
# wrong factors for blended
|
|
self.aducalcmethodModel.create(
|
|
{
|
|
"name": "error factors",
|
|
"method": "blended",
|
|
"source_past": "actual",
|
|
"horizon_past": 30,
|
|
"factor_past": 0.2,
|
|
"source_future": "estimates",
|
|
"horizon_future": 30,
|
|
"factor_future": 0.6,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
|
|
def test_10_qualified_demand_1(self):
|
|
"""Moves within order spike horizon, outside the threshold but past
|
|
or today's demand."""
|
|
date_move = datetime.today()
|
|
expected_result = self.buffer_a.order_spike_threshold * 2
|
|
self.create_pickingoutA(date_move, expected_result)
|
|
self.bufferModel.cron_ddmrp()
|
|
self.assertEqual(self.buffer_a.qualified_demand, expected_result)
|
|
|
|
def test_11_qualified_demand_2(self):
|
|
"""Moves within order spike horizon, below threshold. Should have no
|
|
effect on the qualified demand."""
|
|
date_move = datetime.today() + timedelta(days=10)
|
|
self.create_pickingoutA(date_move, self.buffer_a.order_spike_threshold - 1)
|
|
self.bufferModel.cron_ddmrp()
|
|
expected_result = 0.0
|
|
self.assertEqual(self.buffer_a.qualified_demand, expected_result)
|
|
|
|
def test_12_qualified_demand_3(self):
|
|
"""Moves within order spike horizon, above threshold. Should have an
|
|
effect on the qualified demand"""
|
|
date_move = datetime.today() + timedelta(days=10)
|
|
self.create_pickingoutA(date_move, self.buffer_a.order_spike_threshold * 2)
|
|
self.bufferModel.cron_ddmrp()
|
|
expected_result = self.buffer_a.order_spike_threshold * 2
|
|
self.assertEqual(self.buffer_a.qualified_demand, expected_result)
|
|
|
|
def test_13_qualified_demand_4(self):
|
|
"""Moves outside of order spike horizon, above threshold. Should
|
|
have no effect on the qualified demand"""
|
|
date_move = datetime.today() + timedelta(days=100)
|
|
self.create_pickingoutA(date_move, self.buffer_a.order_spike_threshold * 2)
|
|
self.bufferModel.cron_ddmrp()
|
|
expected_result = 0.0
|
|
self.assertEqual(self.buffer_a.qualified_demand, expected_result)
|
|
|
|
def test_14_qualified_demand_5(self):
|
|
"""Internal moves within the zone designated by the buffer
|
|
should not be considered demand."""
|
|
date_move = datetime.today()
|
|
expected_result = 0
|
|
self.create_pickinginternalA(date_move, expected_result)
|
|
self.bufferModel.cron_ddmrp()
|
|
self.assertEqual(self.buffer_a.qualified_demand, expected_result)
|
|
|
|
def test_15_incoming_quantity_1(self):
|
|
date_move = datetime.today() + timedelta(days=5)
|
|
self.create_pickinginA(date_move, 20)
|
|
self.bufferModel.cron_ddmrp()
|
|
self.assertEqual(self.buffer_a.incoming_dlt_qty, 20.0)
|
|
|
|
def test_16_incoming_quantity_2(self):
|
|
"""Moves outside the DLT horizon are ignored as supply"""
|
|
date_move = datetime.today() + timedelta(days=100)
|
|
self.create_pickinginA(date_move, 20)
|
|
self.bufferModel.cron_ddmrp()
|
|
self.assertEqual(self.buffer_a.incoming_dlt_qty, 0.0)
|
|
self.assertEqual(self.buffer_a.incoming_outside_dlt_qty, 20.0)
|
|
|
|
def test_17_on_hand_qty_1(self):
|
|
"""Outgoing moves should be ignored once reserved as well as the
|
|
reserved qty."""
|
|
date_move = datetime.today()
|
|
outgoing_qty = 50
|
|
picking = self.create_pickingoutA(date_move, outgoing_qty)
|
|
self.buffer_a.cron_actions()
|
|
self.assertEqual(self.buffer_a.qualified_demand, outgoing_qty)
|
|
expected_on_hand = 200
|
|
self.assertEqual(
|
|
self.buffer_a.product_location_qty_available_not_res, expected_on_hand
|
|
)
|
|
# Once reserved, the outgoing qty is not considered for qualified
|
|
# demand and it is excluded from on hand position:
|
|
picking.action_assign()
|
|
self.buffer_a.invalidate_recordset()
|
|
self.buffer_a.cron_actions()
|
|
self.assertEqual(self.buffer_a.qualified_demand, 0)
|
|
expected_on_hand = 200
|
|
self.assertEqual(
|
|
self.buffer_a.product_location_qty_available_not_res,
|
|
expected_on_hand - outgoing_qty,
|
|
)
|
|
|
|
def test_18_on_hand_qty_2(self):
|
|
"""Internal moves should not affect in any way the on hand position of
|
|
a buffer."""
|
|
date_move = datetime.today()
|
|
internal_qty = 50
|
|
picking = self.create_pickinginternalA(date_move, internal_qty)
|
|
self.buffer_a.cron_actions()
|
|
self.assertEqual(self.buffer_a.qualified_demand, 0)
|
|
expected_on_hand = 200
|
|
self.assertEqual(
|
|
self.buffer_a.product_location_qty_available_not_res, expected_on_hand
|
|
)
|
|
# Once reserved, the internal qty is still considered in the on hand position:
|
|
picking.action_assign()
|
|
self.buffer_a.invalidate_recordset()
|
|
self.buffer_a.cron_actions()
|
|
self.assertEqual(picking.move_ids.reserved_availability, internal_qty)
|
|
self.assertEqual(self.buffer_a.qualified_demand, 0)
|
|
expected_on_hand = 200
|
|
self.assertEqual(
|
|
self.buffer_a.product_location_qty_available_not_res, expected_on_hand
|
|
)
|
|
|
|
def test_19_qualified_demand_6_uom(self):
|
|
"""Delivery spike in a secondary UoM and partially reserved, only
|
|
unreserved part should be considered."""
|
|
date_move = datetime.today() + timedelta(days=10)
|
|
picking = self.create_pickingoutA(date_move, 20, uom=self.dozen_unit)
|
|
available_qty = self.buffer_a.product_location_qty_available_not_res
|
|
picking.action_assign()
|
|
self.assertEqual(picking.move_ids.state, "partially_available")
|
|
self.bufferModel.cron_ddmrp()
|
|
# 20 dozens minus de available qty that has been reserved in this
|
|
# picking.
|
|
expected_result = (20 * 12) - available_qty
|
|
self.assertTrue(expected_result > self.buffer_a.order_spike_threshold)
|
|
self.assertAlmostEqual(
|
|
self.buffer_a.qualified_demand, expected_result, places=0
|
|
)
|
|
|
|
# TEST GROUP 2: Buffer zones and procurement
|
|
|
|
def _check_red_zone(
|
|
self, orderpoint, red_base_qty=0.0, red_safety_qty=0.0, red_zone_qty=0.0
|
|
):
|
|
# red base_qty = dlt * adu * lead time factor
|
|
self.assertEqual(orderpoint.red_base_qty, red_base_qty)
|
|
|
|
# red_safety_qty = red_base_qty * variability factor
|
|
self.assertEqual(orderpoint.red_safety_qty, red_safety_qty)
|
|
|
|
# red_zone_qty = red_base_qty + red_safety_qty
|
|
self.assertEqual(orderpoint.red_zone_qty, red_zone_qty)
|
|
|
|
def _check_yellow_zone(self, orderpoint, yellow_zone_qty=0.0, top_of_yellow=0.0):
|
|
# yellow_zone_qty = dlt * adu
|
|
self.assertEqual(orderpoint.yellow_zone_qty, yellow_zone_qty)
|
|
|
|
# top_of_yellow = yellow_zone_qty + red_zone_qty
|
|
self.assertEqual(orderpoint.top_of_yellow, top_of_yellow)
|
|
|
|
def _check_green_zone(
|
|
self,
|
|
orderpoint,
|
|
green_zone_oc=0.0,
|
|
green_zone_lt_factor=0.0,
|
|
green_zone_moq=0.0,
|
|
green_zone_qty=0.0,
|
|
top_of_green=0.0,
|
|
):
|
|
# green_zone_oc = order_cycle * adu
|
|
self.assertEqual(orderpoint.green_zone_oc, green_zone_oc)
|
|
|
|
# green_zone_lt_factor = dlt * adu * lead time factor
|
|
self.assertEqual(orderpoint.green_zone_lt_factor, green_zone_lt_factor)
|
|
|
|
# green_zone_moq = minimum_order_quantity
|
|
self.assertEqual(orderpoint.green_zone_moq, green_zone_moq)
|
|
|
|
# green_zone_qty = max(green_zone_oc, green_zone_lt_factor,
|
|
# green_zone_moq)
|
|
self.assertEqual(orderpoint.green_zone_qty, green_zone_qty)
|
|
|
|
# top_of_green = green_zone_qty + yellow_zone_qty + red_zone_qty
|
|
self.assertEqual(orderpoint.top_of_green, top_of_green)
|
|
|
|
def test_20_buffer_zones_red(self):
|
|
self._check_red_zone(
|
|
self.buffer_a, red_base_qty=20, red_safety_qty=10, red_zone_qty=30
|
|
)
|
|
|
|
self.buffer_a.buffer_profile_id.lead_time_id.factor = 1
|
|
self._check_red_zone(
|
|
self.buffer_a, red_base_qty=40, red_safety_qty=20, red_zone_qty=60
|
|
)
|
|
|
|
self.buffer_a.buffer_profile_id.variability_id.factor = 1
|
|
self._check_red_zone(
|
|
self.buffer_a, red_base_qty=40, red_safety_qty=40, red_zone_qty=80
|
|
)
|
|
|
|
self.buffer_a.adu_fixed = 2
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
self._check_red_zone(
|
|
self.buffer_a, red_base_qty=20, red_safety_qty=20, red_zone_qty=40
|
|
)
|
|
|
|
def test_21_buffer_zones_yellow(self):
|
|
self._check_yellow_zone(self.buffer_a, yellow_zone_qty=40.0, top_of_yellow=70.0)
|
|
|
|
self.buffer_a.adu_fixed = 2
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
self._check_yellow_zone(self.buffer_a, yellow_zone_qty=20.0, top_of_yellow=35.0)
|
|
|
|
self.buffer_a.buffer_profile_id.lead_time_id.factor = 1
|
|
self.buffer_a.buffer_profile_id.variability_id.factor = 1
|
|
self._check_yellow_zone(self.buffer_a, yellow_zone_qty=20.0, top_of_yellow=60.0)
|
|
|
|
def test_22_procure_recommended(self):
|
|
self.buffer_a._calc_adu()
|
|
self.bufferModel.cron_ddmrp()
|
|
# Now we prepare the shipment of 150
|
|
date_move = datetime.today()
|
|
pickingOut = self.create_pickingoutA(date_move, 150)
|
|
pickingOut.move_ids.quantity_done = 150
|
|
pickingOut._action_done()
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
expected_value = 40.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
# Now we change the net flow position.
|
|
# Net Flow position = 200 - 150 + 10 = 60
|
|
self.quantModel.create(
|
|
{
|
|
"location_id": self.binA.id,
|
|
"company_id": self.main_company.id,
|
|
"product_id": self.productA.id,
|
|
"quantity": 10.0,
|
|
}
|
|
)
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
expected_value = 30.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
# Now we change the top of green.
|
|
# red base = dlt * adu * lead time factor = 10 * 2 * 0.5 = 10
|
|
# red safety = red_base * variability factor = 10 * 0.5 = 5
|
|
# red zone = red_base + red_safety = 10 + 5 = 15
|
|
# Top Of Red (TOR) = red zone = 15
|
|
# yellow zone = dlt * adu = 10 * 2 = 20
|
|
# Top Of Yellow (TOY) = TOR + yellow zone = 15 + 20 = 35
|
|
# green_zone_oc = order_cycle * adu = 0 * 4 = 0
|
|
# green_zone_lt_factor = dlt * adu * lead time factor =10
|
|
# green_zone_moq = minimum_order_quantity = 0
|
|
# green_zone_qty = max(green_zone_oc, green_zone_lt_factor,
|
|
# green_zone_moq) = max(0, 10, 0) = 10
|
|
# Top Of Green (TOG) = TOY + green_zone_qty = 35 + 10 = 45
|
|
self.buffer_a.adu_fixed = 2
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
expected_value = 0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
self.buffer_a.buffer_profile_id.lead_time_id.factor = 1
|
|
# Now we change the top of green.
|
|
# red base = dlt * adu * lead time factor = 10 * 2 * 1 = 20
|
|
# red safety = red_base * variability factor = 20 * 0.5 = 10
|
|
# red zone = red_base + red_safety = 20 + 10 = 30
|
|
# Top Of Red (TOR) = red zone = 25
|
|
# yellow zone = dlt * adu = 10 * 2 = 20
|
|
# Top Of Yellow (TOY) = TOR + yellow zone = 30 + 20 = 50
|
|
# green_zone_oc = order_cycle * adu = 0 * 4 = 0
|
|
# green_zone_lt_factor = dlt * adu * lead time factor = 20
|
|
# green_zone_moq = minimum_order_quantity = 0
|
|
# green_zone_qty = max(green_zone_oc, green_zone_lt_factor,
|
|
# green_zone_moq) = max(0, 20, 0) = 20
|
|
# Top Of Green (TOG) = TOY + green_zone_qty = 50 + 20 = 70
|
|
expected_value = 0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
self.buffer_a.minimum_order_quantity = 40
|
|
# Now we change the top of green.
|
|
# red base = dlt * adu * lead time factor = 10 * 2 * 1 = 20
|
|
# red safety = red_base * variability factor = 20 * 0.5 = 10
|
|
# red zone = red_base + red_safety = 20 + 10 = 30
|
|
# Top Of Red (TOR) = red zone = 25
|
|
# yellow zone = dlt * adu = 10 * 2 = 20
|
|
# Top Of Yellow (TOY) = TOR + yellow zone = 30 + 20 = 50
|
|
# green_zone_oc = order_cycle * adu = 0 * 4 = 0
|
|
# green_zone_lt_factor = dlt * adu * lead time factor = 20
|
|
# green_zone_moq = minimum_order_quantity = 0
|
|
# green_zone_qty = max(green_zone_oc, green_zone_lt_factor,
|
|
# green_zone_moq) = max(0, 20, 40) = 40
|
|
# Top Of Green (TOG) = TOY + green_zone_qty = 50 + 40 = 90
|
|
expected_value = 0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
def test_23_buffer_zones_all(self):
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
# red base = dlt * adu * lead time factor = 10 * 4 * 0.5 = 20
|
|
# red safety = red_base * variability factor = 20 * 0.5 = 10
|
|
# red zone = red_base + red_safety = 20 + 10 = 30
|
|
# Top Of Red (TOR) = red zone = 30
|
|
self._check_red_zone(
|
|
self.buffer_a, red_base_qty=20.0, red_safety_qty=10.0, red_zone_qty=30.0
|
|
)
|
|
|
|
# yellow zone = dlt * adu = 10 * 4 = 40
|
|
# Top Of Yellow (TOY) = TOR + yellow zone = 30 + 40 = 70
|
|
self._check_yellow_zone(self.buffer_a, yellow_zone_qty=40.0, top_of_yellow=70.0)
|
|
|
|
# green_zone_oc = order_cycle * adu = 0 * 4 = 0
|
|
# green_zone_lt_factor = dlt * adu * lead time factor = 20
|
|
# green_zone_moq = minimum_order_quantity = 0
|
|
# green_zone_qty = max(green_zone_oc, green_zone_lt_factor,
|
|
# green_zone_moq) = max(0, 20, 0) = 20
|
|
# Top Of Green (TOG) = TOY + green_zone_qty = 70 + 20 = 90
|
|
self._check_green_zone(
|
|
self.buffer_a,
|
|
green_zone_oc=0.0,
|
|
green_zone_lt_factor=20.0,
|
|
green_zone_moq=0.0,
|
|
green_zone_qty=20.0,
|
|
top_of_green=90.0,
|
|
)
|
|
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
# Net Flow Position = on hand + incoming - qualified demand = 200 + 0
|
|
# - 0 = 200
|
|
expected_value = 200.0
|
|
self.assertEqual(self.buffer_a.net_flow_position, expected_value)
|
|
|
|
# Net Flow Position Percent = (Net Flow Position / TOG)*100 = (
|
|
# 200/90)*100 = 55.56 %
|
|
expected_value = 222.22
|
|
self.assertEqual(self.buffer_a.net_flow_position_percent, expected_value)
|
|
|
|
# Planning priority level
|
|
expected_value = "3_green"
|
|
self.assertEqual(self.buffer_a.planning_priority_level, expected_value)
|
|
|
|
# On hand/TOR = (200 / 30) * 100 = 666.67
|
|
expected_value = 666.67
|
|
self.assertEqual(self.buffer_a.on_hand_percent, expected_value)
|
|
|
|
# Execution priority level
|
|
expected_value = "3_green"
|
|
self.assertEqual(self.buffer_a.execution_priority_level, expected_value)
|
|
|
|
# Procure recommended quantity = TOG - Net Flow Position if > 0 = 90
|
|
# - 200 => 0.0
|
|
expected_value = 0.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
# Now we prepare the shipment of 150
|
|
date_move = datetime.today()
|
|
pickingOut = self.create_pickingoutA(date_move, 150)
|
|
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
# Net Flow Position = on hand + incoming - qualified demand = 200 + 0
|
|
# - 150 = 50
|
|
expected_value = 50.0
|
|
self.assertEqual(self.buffer_a.net_flow_position, expected_value)
|
|
|
|
# Net Flow Position Percent = (Net Flow Position / TOG)*100 = (
|
|
# 50/90)*100 = 55.56 %
|
|
expected_value = 55.56
|
|
self.assertEqual(self.buffer_a.net_flow_position_percent, expected_value)
|
|
|
|
# Planning priority level
|
|
expected_value = "2_yellow"
|
|
self.assertEqual(self.buffer_a.planning_priority_level, expected_value)
|
|
|
|
# On hand/TOR = (200 / 30) * 100 = 666.67
|
|
expected_value = 666.67
|
|
self.assertEqual(self.buffer_a.on_hand_percent, expected_value)
|
|
|
|
# Execution priority level
|
|
expected_value = "3_green"
|
|
self.assertEqual(self.buffer_a.execution_priority_level, expected_value)
|
|
|
|
# Now we confirm the shipment of the 150
|
|
pickingOut.action_assign()
|
|
pickingOut.move_ids.quantity_done = 150
|
|
pickingOut._action_done()
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
# On hand/TOR = (50 / 30) * 100 = 166.67
|
|
expected_value = 166.67
|
|
self.assertEqual(self.buffer_a.on_hand_percent, expected_value)
|
|
|
|
# Execution priority level. Considering that the quantity available
|
|
# unrestricted is 50, and top of red is 30, we are in the green on
|
|
# hand zone.
|
|
expected_value = "3_green"
|
|
self.assertEqual(self.buffer_a.execution_priority_level, expected_value)
|
|
|
|
# Procure recommended quantity = TOG - Net Flow Position if > 0 = 90
|
|
# - 50 => 40.0
|
|
expected_value = 40.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
# Now we ship them
|
|
pickingOut._action_done()
|
|
self.bufferModel.cron_ddmrp()
|
|
|
|
# Net Flow Position = on hand + incoming - qualified demand = 200 + 0
|
|
# - 150 = 50
|
|
expected_value = 50.0
|
|
self.assertEqual(self.buffer_a.net_flow_position, expected_value)
|
|
|
|
# Net Flow Position Percent = (Net Flow Position / TOG)*100 = (
|
|
# 50/90)*100 = 55.56 %
|
|
expected_value = 55.56
|
|
self.assertEqual(self.buffer_a.net_flow_position_percent, expected_value)
|
|
|
|
# Planning priority level
|
|
expected_value = "2_yellow"
|
|
self.assertEqual(self.buffer_a.planning_priority_level, expected_value)
|
|
|
|
# On hand/TOR = (50 / 30) * 100 = 166.67
|
|
expected_value = 166.67
|
|
self.assertEqual(self.buffer_a.on_hand_percent, expected_value)
|
|
|
|
# Execution priority level
|
|
expected_value = "3_green"
|
|
self.assertEqual(self.buffer_a.execution_priority_level, expected_value)
|
|
|
|
# Procure recommended quantity = TOG - Net Flow Position if > 0 = 90
|
|
# - 50 => 40.0
|
|
expected_value = 40.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
# Now we create a procurement order, based on the procurement
|
|
# recommendation
|
|
self.create_orderpoint_procurement(self.buffer_a)
|
|
# should have generated a manufacturing order
|
|
self.assertEqual(len(self.buffer_a.mrp_production_ids), 1)
|
|
self.assertEqual(self.buffer_a.mrp_production_ids.product_qty, 40.0)
|
|
|
|
# We expect that the procurement recommendation is now 0
|
|
self.buffer_a.invalidate_recordset()
|
|
expected_value = 0.0
|
|
self.assertEqual(self.buffer_a.procure_recommended_qty, expected_value)
|
|
|
|
def test_24_purchase_link(self):
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertFalse(pol)
|
|
self.assertGreater(self.buffer_purchase.procure_recommended_qty, 0)
|
|
self.create_orderpoint_procurement(self.buffer_purchase)
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertEqual(len(pol), 1)
|
|
self.assertIn(pol.buffer_ids, self.buffer_purchase)
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 0)
|
|
|
|
def test_25_auto_procure(self):
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertFalse(pol)
|
|
self.assertGreater(self.buffer_purchase.procure_recommended_qty, 0)
|
|
self.buffer_purchase.auto_procure = True
|
|
self.buffer_purchase.auto_procure_option = "stockout"
|
|
self.buffer_purchase.cron_actions()
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertFalse(pol) # Buffer is not in stockout.
|
|
# Change to standard, it should procure now.
|
|
self.buffer_purchase.auto_procure_option = "standard"
|
|
self.buffer_purchase.cron_actions()
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertEqual(len(pol), 1)
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 0)
|
|
|
|
def test_26_auto_procure_stockout_and_auto_nfp(self):
|
|
self.main_company.ddmrp_auto_update_nfp = True
|
|
self.buffer_purchase.auto_procure = True
|
|
self.buffer_purchase.auto_procure_option = "stockout"
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertFalse(pol)
|
|
initial_nfp = self.buffer_purchase.net_flow_position
|
|
self.assertEqual(initial_nfp, 0)
|
|
# Provoke an stockout:
|
|
date_move = datetime.today()
|
|
p_out_1 = self.create_picking_out(self.product_purchased, date_move, 10)
|
|
self._do_picking(p_out_1, date_move)
|
|
# A RFQ should have been created.
|
|
self.assertEqual(self.buffer_purchase.net_flow_position, -10)
|
|
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
|
|
self.assertEqual(len(pol), 1)
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 0)
|
|
|
|
def test_27_qty_multiple_tolerance(self):
|
|
original_vals = self.buffer_purchase.read()[0]
|
|
self.buffer_purchase.update(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_override.id,
|
|
"product_id": self.product_purchased.id,
|
|
"location_id": self.stock_location.id,
|
|
"warehouse_id": self.warehouse.id,
|
|
"qty_multiple": 250.0,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
"adu_fixed": 5.0,
|
|
"green_override": 250.0,
|
|
"yellow_override": 10.0,
|
|
"red_override": 10.0,
|
|
}
|
|
)
|
|
date_move = datetime.today()
|
|
self.create_picking_out(self.product_purchased, date_move, 2)
|
|
self.buffer_purchase.cron_actions()
|
|
self.assertEqual(self.buffer_purchase.net_flow_position, -2.0)
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 500)
|
|
# Set the tolerance
|
|
self.buffer_purchase.company_id.ddmrp_qty_multiple_tolerance = 10.0
|
|
# Tolerance: 10% 250 = 25, strictly needed 272 (under tolerance)
|
|
self.buffer_purchase.cron_actions()
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 250)
|
|
# Add more demand
|
|
self.create_picking_out(self.product_purchased, date_move, 20)
|
|
self.buffer_purchase.cron_actions()
|
|
self.assertEqual(self.buffer_purchase.net_flow_position, -22.0)
|
|
# Tolerance: 10% 250 = 25, strictly needed 294 (above tolerance)
|
|
self.buffer_purchase.cron_actions()
|
|
self.assertEqual(self.buffer_purchase.procure_recommended_qty, 500)
|
|
original_vals.pop("id")
|
|
self.buffer_purchase.update(original_vals)
|
|
|
|
def test_28_lead_days(self):
|
|
self._check_red_zone(
|
|
self.buffer_distributed, red_base_qty=50, red_safety_qty=25, red_zone_qty=75
|
|
)
|
|
self._check_yellow_zone(
|
|
self.buffer_distributed, yellow_zone_qty=100.0, top_of_yellow=175.0
|
|
)
|
|
self.buffer_distributed.lead_days = 30
|
|
self.buffer_distributed.cron_actions()
|
|
self._check_red_zone(
|
|
self.buffer_distributed,
|
|
red_base_qty=75,
|
|
red_safety_qty=37.5,
|
|
red_zone_qty=112.5,
|
|
)
|
|
self._check_yellow_zone(
|
|
self.buffer_distributed, yellow_zone_qty=150.0, top_of_yellow=262.5
|
|
)
|
|
|
|
# TEST SECTION 3: DLT, BoM's and misc
|
|
|
|
def test_30_bom_buffer_fields(self):
|
|
"""Check if is_buffered and buffer_id are set."""
|
|
self.assertTrue(self.bom_a.is_buffered)
|
|
self.assertEqual(self.bom_a.buffer_id, self.buffer_a)
|
|
product = self.productA
|
|
|
|
# Create another buffer with a location, then change the bom location
|
|
new_buffer = self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_pur.id,
|
|
"product_id": product.id,
|
|
"warehouse_id": self.warehouse.id,
|
|
"location_id": self.supplier_location.id,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
}
|
|
)
|
|
self.bom_a.context_location_id = self.supplier_location.id
|
|
self.assertTrue(self.bom_a.is_buffered)
|
|
self.assertEqual(self.bom_a.buffer_id, new_buffer)
|
|
new_bom = self.env["mrp.bom"].create(
|
|
{"product_tmpl_id": product.product_tmpl_id.id}
|
|
)
|
|
self.assertEqual(new_bom.is_buffered, True)
|
|
self.assertEqual(new_bom.buffer_id, self.buffer_a)
|
|
|
|
def test_31_bom_dlt_computation(self):
|
|
"""Tests that DLT computation is correct removing buffers."""
|
|
bom_fp01 = self.env.ref("ddmrp.mrp_bom_fp01")
|
|
self.assertEqual(bom_fp01.dlt, 22)
|
|
# Remove RM-01 buffer:
|
|
orderpoint_rm01 = self.env.ref("ddmrp.stock_buffer_rm01")
|
|
bom_line_rm01 = self.env.ref("ddmrp.mrp_bom_as01_line_rm01")
|
|
orderpoint_rm01.active = False
|
|
bom_line_rm01._compute_is_buffered()
|
|
self.assertFalse(bom_line_rm01.is_buffered)
|
|
bom_fp01._compute_dlt()
|
|
self.assertEqual(bom_fp01.dlt, 33.0)
|
|
|
|
def test_32_bom_dlt_computation(self):
|
|
"""Tests that DLT computation is correct adding buffers."""
|
|
product_as01 = self.env.ref("ddmrp.product_product_as01")
|
|
self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_mmm.id,
|
|
"product_id": product_as01.id,
|
|
"warehouse_id": self.warehouse.id,
|
|
"location_id": self.stock_location.id,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
}
|
|
)
|
|
bom_fp01 = self.env.ref("ddmrp.mrp_bom_fp01")
|
|
self.assertEqual(bom_fp01.dlt, 2.0)
|
|
|
|
def test_33_auto_compute_nfp_off(self):
|
|
self.main_company.ddmrp_auto_update_nfp = False
|
|
initial_nfp = self.buffer_a.net_flow_position
|
|
self.assertEqual(initial_nfp, 200)
|
|
self.assertEqual(self.buffer_a.product_location_qty_available_not_res, 200)
|
|
date_move = datetime.today()
|
|
self.create_pickingoutA(date_move, 120)
|
|
# NFP hasn't been updated.
|
|
self.assertEqual(self.buffer_a.net_flow_position, initial_nfp)
|
|
self.create_pickinginA(date_move, 35)
|
|
# NFP hasn't been updated.
|
|
self.assertEqual(self.buffer_a.net_flow_position, initial_nfp)
|
|
# Update buffer, expected NFP = 200 - 120 + 35 = 115
|
|
self.buffer_a.cron_actions()
|
|
expected = 200 - 120 + 35
|
|
self.assertEqual(self.buffer_a.net_flow_position, expected)
|
|
|
|
def test_34_auto_compute_nfp_on(self):
|
|
self.main_company.ddmrp_auto_update_nfp = True
|
|
initial_nfp = self.buffer_a.net_flow_position
|
|
self.assertEqual(initial_nfp, 200)
|
|
self.assertEqual(self.buffer_a.product_location_qty_available_not_res, 200)
|
|
date_move = datetime.today()
|
|
self.create_pickingoutA(date_move, 120)
|
|
# NFP has been updated after picking confirmation.
|
|
self.assertEqual(self.buffer_a.net_flow_position, 80)
|
|
self.create_pickinginA(date_move, 35)
|
|
# NFP has been updated after picking confirmation.
|
|
expected = 200 - 120 + 35
|
|
self.assertEqual(self.buffer_a.net_flow_position, expected)
|
|
|
|
def test_35_dlt_variants_computation(self):
|
|
# The seller_ids attribute is the same for all variants, but correct
|
|
# one needs to be applied when variant is specified.
|
|
self.assertEqual(self.buffer_c_blue.dlt, 5)
|
|
self.assertEqual(self.buffer_c_orange.dlt, 10)
|
|
self.p_c_supinfo_orange.unlink()
|
|
# Fall back to no-variant supplier info
|
|
self.assertEqual(self.buffer_c_orange.dlt, 8)
|
|
|
|
def test_36_dlt_extra_lead_time(self):
|
|
dlt = 5
|
|
adu = 5
|
|
self.assertEqual(self.buffer_c_blue.dlt, dlt)
|
|
self.assertEqual(self.buffer_c_blue.adu, adu)
|
|
# Profile: Purchased, med, med
|
|
previous_red = dlt * adu * 0.5 + dlt * adu * 0.5 * 0.5
|
|
self.assertEqual(self.buffer_c_blue.red_zone_qty, previous_red)
|
|
previous_yellow = dlt * adu
|
|
self.assertEqual(self.buffer_c_blue.yellow_zone_qty, previous_yellow)
|
|
previous_green = dlt * adu * 0.5
|
|
self.assertEqual(self.buffer_c_blue.green_zone_qty, previous_green)
|
|
# Add extra lead time.
|
|
extra = 2
|
|
self.buffer_c_blue.extra_lead_time = extra
|
|
self.buffer_c_blue.cron_actions()
|
|
self.assertEqual(self.buffer_c_blue.dlt, 5)
|
|
new_red = (dlt + extra) * adu * 0.5 + (dlt + extra) * adu * 0.5 * 0.5
|
|
self.assertEqual(self.buffer_c_blue.red_zone_qty, new_red)
|
|
new_yellow = (dlt + extra) * adu
|
|
self.assertEqual(self.buffer_c_blue.yellow_zone_qty, new_yellow)
|
|
new_green = (dlt + extra) * adu * 0.5
|
|
self.assertEqual(self.buffer_c_blue.green_zone_qty, new_green)
|
|
|
|
def test_37_bom_buffer_fields_multi_company(self):
|
|
self.assertEqual(self.bom_a.company_id, self.main_company)
|
|
self.assertTrue(self.bom_a.is_buffered)
|
|
self.assertEqual(self.bom_a.buffer_id, self.buffer_a)
|
|
bom_buffer_a = self.buffer_a._get_manufactured_bom()
|
|
self.assertEqual(bom_buffer_a, self.bom_a)
|
|
bom_line = self.bom_a.bom_line_ids.filtered(
|
|
lambda l: l.product_id == self.component_a1
|
|
)
|
|
self.assertTrue(bom_line)
|
|
self.assertFalse(bom_line.is_buffered)
|
|
# Add a buffer for a component but in a different company
|
|
component_buffer = self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_pur.id,
|
|
"product_id": self.component_a1.id,
|
|
"company_id": self.second_company.id,
|
|
"warehouse_id": self.warehouse_sc.id,
|
|
"location_id": self.stock_location_sc.id,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
}
|
|
)
|
|
bom_line.invalidate_recordset()
|
|
self.assertFalse(bom_line.is_buffered)
|
|
# Change company of the BoM
|
|
self.bom_a.company_id = self.second_company
|
|
self.bom_a.invalidate_recordset()
|
|
self.assertFalse(self.bom_a.is_buffered)
|
|
self.assertFalse(self.bom_a.buffer_id)
|
|
bom_buffer_a = self.buffer_a._get_manufactured_bom()
|
|
self.assertFalse(bom_buffer_a)
|
|
bom_line.invalidate_recordset()
|
|
self.assertTrue(bom_line.is_buffered)
|
|
self.assertEqual(bom_line.buffer_id, component_buffer)
|
|
|
|
def test_38_bom_dlt_computation_multi_location(self):
|
|
"""
|
|
If AS01 bom has no location it means that it can be manufactured
|
|
in more than one location.
|
|
"""
|
|
bom_fp01 = self.env.ref("ddmrp.mrp_bom_fp01")
|
|
buffer1_fp01 = self.env.ref("ddmrp.stock_buffer_fp01")
|
|
self.assertEqual(bom_fp01.dlt, 22.0)
|
|
self.assertEqual(bom_fp01.buffer_id, buffer1_fp01)
|
|
self.assertEqual(len(bom_fp01.bom_line_ids), 1)
|
|
self.assertEqual(bom_fp01.bom_line_ids.is_buffered, False)
|
|
# Now create buffers in another location and check in that context
|
|
product_fp01 = self.env.ref("ddmrp.product_product_fp01")
|
|
product_as01 = self.env.ref("ddmrp.product_product_as01")
|
|
buffer2_fp01 = self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_mmm.id,
|
|
"product_id": product_fp01.id,
|
|
"warehouse_id": self.warehouse.id,
|
|
"location_id": self.supplier_location.id,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
}
|
|
)
|
|
buffer_as01 = self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_mmm.id,
|
|
"product_id": product_as01.id,
|
|
"warehouse_id": self.warehouse.id,
|
|
"location_id": self.supplier_location.id,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
}
|
|
)
|
|
bom_fp01.context_location_id = self.supplier_location.id
|
|
bom_fp01.bom_line_ids._compute_is_buffered()
|
|
bom_fp01._compute_dlt()
|
|
bom_fp01.bom_line_ids._compute_dlt()
|
|
self.assertEqual(bom_fp01.dlt, 2.0)
|
|
self.assertEqual(bom_fp01.buffer_id, buffer2_fp01)
|
|
self.assertEqual(len(bom_fp01.bom_line_ids), 1)
|
|
self.assertEqual(bom_fp01.bom_line_ids.is_buffered, True)
|
|
self.assertEqual(bom_fp01.bom_line_ids.buffer_id, buffer_as01)
|
|
# Check at the same time the DLT of 2 buffers using the same bom:
|
|
buffers = buffer1_fp01 + buffer2_fp01
|
|
buffers.invalidate_recordset()
|
|
buffers._compute_dlt()
|
|
self.assertEqual(buffer1_fp01.dlt, 22)
|
|
self.assertEqual(buffer2_fp01.dlt, 2)
|
|
|
|
def test_40_bokeh_charts(self):
|
|
"""Check bokeh chart computation."""
|
|
date_move = datetime.today()
|
|
self.create_pickingoutA(date_move, 150)
|
|
self.create_pickinginA(date_move, 150)
|
|
self.buffer_a.cron_actions()
|
|
self.assertTrue(self.buffer_a.ddmrp_chart)
|
|
self.assertTrue(self.buffer_a.ddmrp_demand_chart)
|
|
self.assertTrue(self.buffer_a.ddmrp_supply_chart)
|
|
|
|
def test_41_archive_template(self):
|
|
# archive a product template:
|
|
self.template_c.toggle_active()
|
|
self.assertFalse(self.template_c.active)
|
|
self.assertFalse(self.product_c_blue.active)
|
|
self.assertFalse(self.product_c_orange.active)
|
|
self.assertFalse(self.buffer_c_blue.active)
|
|
self.assertFalse(self.buffer_c_orange.active)
|
|
|
|
def test_42_archive_variant(self):
|
|
# archive a variant
|
|
self.product_c_blue.toggle_active()
|
|
self.assertTrue(self.template_c.active)
|
|
self.assertFalse(self.product_c_blue.active)
|
|
self.assertTrue(self.product_c_orange.active)
|
|
self.assertFalse(self.buffer_c_blue.active)
|
|
self.assertTrue(self.buffer_c_orange.active)
|
|
# toggle a buffer before toggling product:
|
|
self.buffer_c_blue.toggle_active()
|
|
self.assertTrue(self.buffer_c_blue.active)
|
|
self.assertFalse(self.product_c_blue.active)
|
|
self.product_c_blue.toggle_active()
|
|
self.assertTrue(self.buffer_c_blue.active)
|
|
self.assertTrue(self.product_c_blue.active)
|
|
|
|
def test_43_get_product_sellers(self):
|
|
seller = self.product_purchased.seller_ids
|
|
vendor = seller.partner_id
|
|
today = fields.Date.context_today(seller)
|
|
tomorrow = fields.Date.add(today, days=1)
|
|
yesterday = fields.Date.subtract(today, days=1)
|
|
# Simple case: one seller available on the product
|
|
self.assertEqual(self.buffer_purchase._get_product_sellers(), seller)
|
|
# Create new sellers with different 'date_start'
|
|
seller2 = self.supinfo_model.create(
|
|
{
|
|
"product_tmpl_id": self.product_purchased.product_tmpl_id.id,
|
|
"partner_id": vendor.id,
|
|
"date_start": tomorrow,
|
|
}
|
|
)
|
|
seller3 = self.supinfo_model.create(
|
|
{
|
|
"product_tmpl_id": self.product_purchased.product_tmpl_id.id,
|
|
"partner_id": vendor.id,
|
|
"date_start": yesterday,
|
|
}
|
|
)
|
|
self.assertEqual(self.buffer_purchase._get_product_sellers(), seller | seller3)
|
|
# Set a 'date_end' on the sellers
|
|
seller2.date_start = False
|
|
seller2.date_end = today
|
|
seller.date_end = seller3.date_end = yesterday
|
|
self.assertEqual(self.buffer_purchase._get_product_sellers(), seller2)
|
|
|
|
def test_44_resupply_from_another_warehouse(self):
|
|
route = self.env["stock.route"].create(
|
|
{
|
|
"name": "Warehouse 2: Supply from Warehouse",
|
|
"product_categ_selectable": True,
|
|
"product_selectable": True,
|
|
"warehouse_selectable": True,
|
|
"rule_ids": [
|
|
(
|
|
0,
|
|
0,
|
|
{
|
|
"name": "Warehouse: Stock → Inter-warehouse transit",
|
|
"action": "pull",
|
|
"picking_type_id": self.ref("stock.picking_type_internal"),
|
|
"location_src_id": self.warehouse.lot_stock_id.id,
|
|
"location_dest_id": self.inter_wh.id,
|
|
"procure_method": "make_to_stock",
|
|
},
|
|
),
|
|
(
|
|
0,
|
|
0,
|
|
{
|
|
"name": "Warehouse2: Inter-warehouse transit → Stock",
|
|
"action": "pull",
|
|
"picking_type_id": self.ref("stock.picking_type_internal"),
|
|
"location_src_id": self.inter_wh.id,
|
|
"location_dest_id": self.warehouse2.lot_stock_id.id,
|
|
"procure_method": "make_to_order",
|
|
},
|
|
),
|
|
],
|
|
}
|
|
)
|
|
self.product_purchased.route_ids |= route
|
|
buffer_distributed = self.bufferModel.create(
|
|
{
|
|
"buffer_profile_id": self.buffer_profile_distr.id,
|
|
"product_id": self.product_purchased.id,
|
|
"location_id": self.warehouse2.lot_stock_id.id,
|
|
"warehouse_id": self.warehouse2.id,
|
|
"qty_multiple": 1.0,
|
|
"adu_calculation_method": self.adu_fixed.id,
|
|
"adu_fixed": 4.0,
|
|
"lead_days": 10.0,
|
|
"order_spike_horizon": 10.0,
|
|
}
|
|
)
|
|
self.assertEqual(
|
|
buffer_distributed.distributed_source_location_id,
|
|
self.warehouse.lot_stock_id,
|
|
)
|
|
|
|
def test_45_adu_calculation_blended_120_days_estimated_mrp(self):
|
|
"""Test blended ADU calculation method with direct and indirect demand."""
|
|
mrpMoveModel = self.env["mrp.move"]
|
|
mrpAreaModel = self.env["mrp.area"]
|
|
productMrpAreaModel = self.env["product.mrp.area"]
|
|
method = self.aducalcmethodModel.create(
|
|
{
|
|
"name": "Blended (120 d. estimates_mrp past, 120 d. estimates_mrp future)",
|
|
"method": "blended",
|
|
"source_past": "estimates_mrp",
|
|
"horizon_past": 120,
|
|
"factor_past": 0.5,
|
|
"source_future": "estimates_mrp",
|
|
"horizon_future": 120,
|
|
"factor_future": 0.5,
|
|
"company_id": self.main_company.id,
|
|
}
|
|
)
|
|
self.buffer_a.adu_calculation_method = method.id
|
|
mrp_area_id = mrpAreaModel.create(
|
|
{
|
|
"name": "WH/Stock",
|
|
"warehouse_id": self.warehouse.id,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
product_mrp_area_id = productMrpAreaModel.create(
|
|
{
|
|
"mrp_area_id": mrp_area_id.id,
|
|
"product_id": self.productA.id,
|
|
}
|
|
)
|
|
today = fields.Date.today()
|
|
|
|
# Past.
|
|
# create estimate: 120 units / 120 days = 1 unit/day
|
|
# create mrp move: 120 units / 120 days = 1 unit/day
|
|
dt = self.calendar.plan_days(-1 * 120, datetime.today())
|
|
estimate_date_from = dt.date()
|
|
estimate_date_to = self.calendar.plan_days(-1 * 2, datetime.today())
|
|
self.estimateModel.create(
|
|
{
|
|
"manual_date_from": estimate_date_from,
|
|
"manual_date_to": estimate_date_to,
|
|
"product_id": self.productA.id,
|
|
"product_uom_qty": 120,
|
|
"product_uom": self.productA.uom_id.id,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
mrpMoveModel.create(
|
|
{
|
|
"mrp_area_id": product_mrp_area_id.mrp_area_id.id,
|
|
"product_id": product_mrp_area_id.product_id.id,
|
|
"product_mrp_area_id": product_mrp_area_id.id,
|
|
"mrp_qty": -120,
|
|
"current_qty": 0,
|
|
"mrp_date": today - timedelta(days=5),
|
|
"current_date": None,
|
|
"mrp_type": "d",
|
|
"mrp_origin": "mrp",
|
|
}
|
|
)
|
|
|
|
# Future.
|
|
# create estimate: 120 units / 120 days = 1 unit/day
|
|
# create mrp move: 120 units / 120 days = 1 unit/day
|
|
self.estimateModel.create(
|
|
{
|
|
"manual_date_from": self.estimate_date_from,
|
|
"manual_date_to": self.estimate_date_to,
|
|
"product_id": self.productA.id,
|
|
"product_uom_qty": 120,
|
|
"product_uom": self.productA.uom_id.id,
|
|
"location_id": self.stock_location.id,
|
|
}
|
|
)
|
|
mrpMoveModel.create(
|
|
{
|
|
"mrp_area_id": product_mrp_area_id.mrp_area_id.id,
|
|
"product_id": product_mrp_area_id.product_id.id,
|
|
"product_mrp_area_id": product_mrp_area_id.id,
|
|
"mrp_qty": -120,
|
|
"current_qty": 0,
|
|
"mrp_date": today + timedelta(days=5),
|
|
"current_date": None,
|
|
"mrp_type": "d",
|
|
"mrp_origin": "mrp",
|
|
}
|
|
)
|
|
|
|
self.bufferModel.cron_ddmrp_adu()
|
|
to_assert_value = 2 * 0.5 + 2 * 0.5
|
|
self.assertEqual(self.buffer_a.adu, to_assert_value)
|
|
|
|
def test_46_disable_auto_create_orderpoint(self):
|
|
"""If a product has a buffer, do not create a new orderpoint for same location"""
|
|
op_a = self.orderpoint_model.search(
|
|
[
|
|
("product_id", "=", self.productA.id),
|
|
("location_id", "=", self.stock_location.id),
|
|
]
|
|
)
|
|
self.assertFalse(op_a)
|
|
nbp = self.productModel.create(
|
|
{
|
|
"name": "Non Buffered Product",
|
|
"standard_price": 1,
|
|
"type": "product",
|
|
"uom_id": self.uom_unit.id,
|
|
"default_code": "NBP",
|
|
}
|
|
)
|
|
op_nbp = self.orderpoint_model.search(
|
|
[
|
|
("product_id", "=", nbp.id),
|
|
("location_id", "=", self.stock_location.id),
|
|
]
|
|
)
|
|
self.assertFalse(op_nbp)
|
|
# Create a negative projection for both products:
|
|
date_move = datetime.today()
|
|
self.create_pickingoutA(date_move, 500, source_location=self.stock_location)
|
|
self.create_picking_out(
|
|
nbp, date_move, 500, source_location=self.stock_location
|
|
)
|
|
# Access "Replenishment" menu
|
|
self.orderpoint_model.action_open_orderpoints()
|
|
op_a = self.orderpoint_model.search(
|
|
[
|
|
("product_id", "=", self.productA.id),
|
|
("location_id", "=", self.stock_location.id),
|
|
]
|
|
)
|
|
self.assertFalse(op_a)
|
|
op_nbp = self.orderpoint_model.search(
|
|
[
|
|
("product_id", "=", nbp.id),
|
|
("location_id", "=", self.stock_location.id),
|
|
]
|
|
)
|
|
self.assertTrue(op_nbp)
|