oca-technical/odoo-bringout-oca-ddmrp-ddmrp/ddmrp/tests/test_ddmrp.py
2025-08-29 15:43:03 +02:00

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)