Files
enterprise-addons/mrp_mps/tests/test_mrp_mps.py
T

2401 lines
115 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from dateutil.relativedelta import relativedelta
from datetime import date, datetime, timedelta
from odoo.tests import common, Form
from odoo import Command
from odoo.tools.date_utils import start_of, subtract
class TestMpsMps(common.TransactionCase):
@classmethod
def setUpClass(cls):
""" Define a multi level BoM and generate a production schedule with
default value for each of the products.
BoM 1:
Table
|
------------------------------------
1 Drawer 2 Table Legs
| |
---------------- -------------------
4 Screw 2 Table Legs 4 Screw 4 Bolt
|
-------------------
4 Screw 4 Bolt
BoM 2 and 3:
Wardrobe Chair
| |
3 Drawer 4 Table Legs
"""
super().setUpClass()
cls.mps_dates_month = cls.env.company._get_date_range()
cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
cls.table = cls.env['product.product'].create({
'name': 'Table',
'is_storable': True,
'route_ids': [Command.set([cls.manufacture_route.id])],
})
cls.drawer = cls.env['product.product'].create({
'name': 'Drawer',
'is_storable': True,
'route_ids': [Command.set([cls.manufacture_route.id])],
})
cls.table_leg = cls.env['product.product'].create({
'name': 'Table Leg',
'is_storable': True,
'route_ids': [Command.set([cls.manufacture_route.id])],
})
cls.screw = cls.env['product.product'].create({
'name': 'Screw',
'is_storable': True,
})
cls.bolt = cls.env['product.product'].create({
'name': 'Bolt',
'is_storable': True,
})
bom_form_table = Form(cls.env['mrp.bom'])
bom_form_table.product_tmpl_id = cls.table.product_tmpl_id
bom_form_table.product_qty = 1
cls.bom_table = bom_form_table.save()
with Form(cls.bom_table) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cls.drawer
line.product_qty = 1
with bom.bom_line_ids.new() as line:
line.product_id = cls.table_leg
line.product_qty = 2
bom_form_drawer = Form(cls.env['mrp.bom'])
bom_form_drawer.product_tmpl_id = cls.drawer.product_tmpl_id
bom_form_drawer.product_qty = 1
cls.bom_drawer = bom_form_drawer.save()
with Form(cls.bom_drawer) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cls.table_leg
line.product_qty = 2
with bom.bom_line_ids.new() as line:
line.product_id = cls.screw
line.product_qty = 4
bom_form_table_leg = Form(cls.env['mrp.bom'])
bom_form_table_leg.product_tmpl_id = cls.table_leg.product_tmpl_id
bom_form_table_leg.product_qty = 1
cls.bom_table_leg = bom_form_table_leg.save()
with Form(cls.bom_table_leg) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cls.bolt
line.product_qty = 4
with bom.bom_line_ids.new() as line:
line.product_id = cls.screw
line.product_qty = 4
cls.wardrobe = cls.env['product.product'].create({
'name': 'Wardrobe',
'is_storable': True,
})
bom_form_wardrobe = Form(cls.env['mrp.bom'])
bom_form_wardrobe.product_tmpl_id = cls.wardrobe.product_tmpl_id
bom_form_wardrobe.product_qty = 1
cls.bom_wardrobe = bom_form_wardrobe.save()
with Form(cls.bom_wardrobe) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cls.drawer
# because pim-odoo said '3 drawers because 4 is too much'
line.product_qty = 3
cls.chair = cls.env['product.product'].create({
'name': 'Chair',
'is_storable': True,
})
bom_form_chair = Form(cls.env['mrp.bom'])
bom_form_chair.product_tmpl_id = cls.chair.product_tmpl_id
bom_form_chair.product_qty = 1
cls.bom_chair = bom_form_chair.save()
with Form(cls.bom_chair) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cls.table_leg
line.product_qty = 4
cls.warehouse = cls.env['stock.warehouse'].search([], limit=1)
cls.mps_table = cls.env['mrp.production.schedule'].create({
'product_id': cls.table.id,
'warehouse_id': cls.warehouse.id,
'bom_id': cls.bom_table.id,
})
cls.mps_wardrobe = cls.env['mrp.production.schedule'].create({
'product_id': cls.wardrobe.id,
'warehouse_id': cls.warehouse.id,
'bom_id': cls.bom_wardrobe.id,
})
cls.mps_chair = cls.env['mrp.production.schedule'].create({
'product_id': cls.chair.id,
'warehouse_id': cls.warehouse.id,
'bom_id': cls.bom_chair.id,
})
cls.mps_drawer = cls.env['mrp.production.schedule'].create({
'product_id': cls.drawer.id,
'warehouse_id': cls.warehouse.id,
'bom_id': cls.bom_drawer.id,
})
cls.mps_table_leg = cls.env['mrp.production.schedule'].create({
'product_id': cls.table_leg.id,
'warehouse_id': cls.warehouse.id,
'bom_id': cls.bom_table_leg.id,
})
cls.mps_screw = cls.env['mrp.production.schedule'].search([
('product_id', '=', cls.screw.id)
])
cls.mps_bolt = cls.env['mrp.production.schedule'].search([
('product_id', '=', cls.bolt.id)
])
cls.mps = cls.mps_table | cls.mps_wardrobe | cls.mps_chair |\
cls.mps_drawer | cls.mps_table_leg | cls.mps_screw | cls.mps_bolt
def _create_and_process_delivery_at_date(self, products_and_quantities, date=False, to_validate=True):
""" Create an out delivery order for the given products and quantities, at the given date.
:param products_and_quantities: list of tuples [(product, quantity)]
:param date: date of the operation, now if not specified
:param to_validate: if True (default), the delivery is validated, otherwise it is only confirmed
:return: the created out delivery
"""
date = date or datetime.now()
delivery_type = self.env.ref('stock.warehouse0').out_type_id
with freeze_time(date):
delivery = self.env['stock.picking'].create({
'picking_type_id': delivery_type.id,
'location_id': delivery_type.default_location_src_id.id,
'location_dest_id': delivery_type.default_location_dest_id.id,
'move_ids': [Command.create({
'location_id': delivery_type.default_location_src_id.id,
'location_dest_id': delivery_type.default_location_dest_id.id,
'product_id': product.id,
'quantity': qty,
'product_uom_qty': qty,
}) for (product, qty) in products_and_quantities],
})
delivery.action_confirm()
if to_validate:
delivery.action_assign()
delivery.button_validate()
return delivery
def test_basic_state(self):
""" Testing master product scheduling default values for client
action rendering.
"""
mps_state = self.mps.get_mps_view_state()
self.assertTrue(len(mps_state['manufacturing_period']), 12)
# Remove demo data
production_schedule_ids = [s for s in mps_state['production_schedule_ids'] if s['id'] in self.mps.ids]
# Check that 7 states are returned (one by production schedule)
self.assertEqual(len(production_schedule_ids), 7)
self.assertEqual(mps_state['company_id'], self.env.user.company_id.id)
company_groups = mps_state['groups'][0]
self.assertTrue(company_groups['mrp_mps_show_starting_inventory'])
self.assertTrue(company_groups['mrp_mps_show_demand_forecast'])
self.assertTrue(company_groups['mrp_mps_show_indirect_demand'])
self.assertTrue(company_groups['mrp_mps_show_to_replenish'])
self.assertTrue(company_groups['mrp_mps_show_safety_stock'])
self.assertFalse(company_groups['mrp_mps_show_actual_demand'])
self.assertFalse(company_groups['mrp_mps_show_actual_replenishment'])
self.assertFalse(company_groups['mrp_mps_show_available_to_promise'])
# Check that quantity on forecast are empty
self.assertTrue(all([not forecast['starting_inventory_qty'] for forecast in production_schedule_ids[0]['forecast_ids']]))
self.assertTrue(all([not forecast['forecast_qty'] for forecast in production_schedule_ids[0]['forecast_ids']]))
self.assertTrue(all([not forecast['replenish_qty'] for forecast in production_schedule_ids[0]['forecast_ids']]))
self.assertTrue(all([not forecast['safety_stock_qty'] for forecast in production_schedule_ids[0]['forecast_ids']]))
self.assertTrue(all([not forecast['indirect_demand_qty'] for forecast in production_schedule_ids[0]['forecast_ids']]))
# Check that there is 12 periods for each forecast
self.assertTrue(all([len(production_schedule_id['forecast_ids']) == 12 for production_schedule_id in production_schedule_ids]))
def test_forecast_1(self):
""" Testing master product scheduling """
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': date.today(),
'forecast_qty': 100
})
screw_mps_state = self.mps_screw.get_production_schedule_view_state()[0]
forecast_at_first_period = screw_mps_state['forecast_ids'][0]
self.assertEqual(forecast_at_first_period['forecast_qty'], 100)
self.assertEqual(forecast_at_first_period['replenish_qty'], 100)
self.assertEqual(forecast_at_first_period['safety_stock_qty'], 0)
self.env['stock.quant']._update_available_quantity(self.mps_screw.product_id, self.warehouse.lot_stock_id, 50)
# Invalidate qty_available on product.product
self.env.invalidate_all()
screw_mps_state = self.mps_screw.get_production_schedule_view_state()[0]
forecast_at_first_period = screw_mps_state['forecast_ids'][0]
self.assertEqual(forecast_at_first_period['forecast_qty'], 100)
self.assertEqual(forecast_at_first_period['replenish_qty'], 50)
self.assertEqual(forecast_at_first_period['safety_stock_qty'], 0)
def test_replenish(self):
""" Test to run procurement for forecasts. Check that replenish for
different periods will not merger purchase order line and create
multiple docurements. Also modify the existing quantity replenished on
a forecast and run the replenishment again, ensure the purchase order
line is updated.
"""
self.env.company.horizon_days = 0
self.env.user.tz = 'UTC'
forecast_screw = self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 100
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': self.mps_dates_month[1][0],
'forecast_qty': 100
})
partner = self.env['res.partner'].create({
'name': 'Jhon'
})
self.env['product.supplierinfo'].create({
'product_id': self.screw.id,
'partner_id': partner.id,
'price': 12.0,
'delay': 0
})
self.mps_screw.action_replenish()
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertTrue(purchase_order_line)
# It should not create a procurement since it exists already one for the
# current period and the sum of lead time should be 0.
self.mps_screw.action_replenish(based_on_lead_time=True)
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertEqual(len(purchase_order_line), 1)
self.mps_screw.action_replenish()
purchase_order_lines = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertEqual(len(purchase_order_lines), 2)
self.assertEqual(len(purchase_order_lines.mapped('order_id')), 2)
# This replenish should be withtout effect since everything is already
# plannified.
self.mps_screw.action_replenish()
purchase_order_lines = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertEqual(len(purchase_order_lines), 2)
# Replenish an existing forecast with a procurement in progress
forecast_screw.forecast_qty = 150
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(screw_forecast_1['state'], 'to_relaunch')
self.assertTrue(screw_forecast_1['to_replenish'])
self.assertTrue(screw_forecast_1['forced_replenish'])
self.mps_screw.action_replenish(based_on_lead_time=True)
purchase_order_lines = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertEqual(len(purchase_order_lines), 2)
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)], order='date_planned', limit=1)
self.assertEqual(purchase_order_line.product_qty, 150)
forecast_screw.forecast_qty = 50
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(screw_forecast_1['state'], 'to_correct')
self.assertFalse(screw_forecast_1['to_replenish'])
self.assertFalse(screw_forecast_1['forced_replenish'])
def test_batch_resplenish(self):
""" Ensure it will consider the batch size when resplenishing,
with or without a specified bom
"""
self.bom_table_leg.write({
'enable_batch_size': True,
'batch_size': 20.0,
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table_leg.id,
'date': date.today(),
'forecast_qty': 1
})
table_leg_mps_state = self.mps_table_leg.get_production_schedule_view_state()[0]
forecast_at_first_period = table_leg_mps_state['forecast_ids'][0]
self.assertEqual(forecast_at_first_period['forecast_qty'], 1)
self.assertEqual(forecast_at_first_period['replenish_qty'], 20)
self.assertEqual(forecast_at_first_period['safety_stock_qty'], 19)
self.mps_table_leg.unlink()
mps_table_leg_without_bom = self.env['mrp.production.schedule'].create({
'product_id': self.table_leg.id,
'warehouse_id': self.warehouse.id,
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': mps_table_leg_without_bom.id,
'date': date.today(),
'forecast_qty': 1
})
table_leg_mps_state_without_bom = mps_table_leg_without_bom.get_production_schedule_view_state()[0]
forecast_at_first_period = table_leg_mps_state_without_bom['forecast_ids'][0]
self.assertEqual(forecast_at_first_period['forecast_qty'], 1)
self.assertEqual(forecast_at_first_period['replenish_qty'], 20)
self.assertEqual(forecast_at_first_period['safety_stock_qty'], 19)
def test_lead_times(self):
""" Manufacture, supplier and rules uses delay. The forecasts to
replenish are impacted by those delay. Ensure that the MPS state and
the period to replenish are correct.
"""
self.env.company.horizon_days = 0
self.env.company.manufacturing_period = 'week'
partner = self.env['res.partner'].create({
'name': 'Jhon'
})
seller = self.env['product.supplierinfo'].create({
'product_id': self.screw.id,
'partner_id': partner.id,
'price': 12.0,
'delay': 7,
})
mps_dates_week = self.env.company._get_date_range()
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': mps_dates_week[0][0],
'forecast_qty': 100
})
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
# Check screw forecasts state
self.assertEqual(screw_forecast_1['state'], 'to_launch')
# launched because it's in lead time frame but it do not require a
# replenishment. The state launched is used in order to render the cell
# with a grey background.
self.assertEqual(screw_forecast_2['state'], 'launched')
self.assertEqual(screw_forecast_3['state'], 'to_launch')
self.assertTrue(screw_forecast_1['to_replenish'])
self.assertFalse(screw_forecast_2['to_replenish'])
self.assertFalse(screw_forecast_3['to_replenish'])
self.assertTrue(screw_forecast_1['forced_replenish'])
self.assertFalse(screw_forecast_2['forced_replenish'])
self.assertFalse(screw_forecast_3['forced_replenish'])
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': mps_dates_week[1][0],
'forecast_qty': 100
})
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_1['state'], 'to_launch')
self.assertEqual(screw_forecast_2['state'], 'to_launch')
self.assertEqual(screw_forecast_3['state'], 'to_launch')
self.assertTrue(screw_forecast_1['to_replenish'])
self.assertTrue(screw_forecast_2['to_replenish'])
self.assertFalse(screw_forecast_3['to_replenish'])
self.assertTrue(screw_forecast_1['forced_replenish'])
self.assertFalse(screw_forecast_2['forced_replenish'])
self.assertFalse(screw_forecast_3['forced_replenish'])
seller.delay = 14
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_1['state'], 'to_launch')
self.assertEqual(screw_forecast_2['state'], 'to_launch')
self.assertEqual(screw_forecast_3['state'], 'launched')
self.assertTrue(screw_forecast_1['to_replenish'])
self.assertTrue(screw_forecast_2['to_replenish'])
self.assertFalse(screw_forecast_3['to_replenish'])
self.assertTrue(screw_forecast_1['forced_replenish'])
self.assertFalse(screw_forecast_2['forced_replenish'])
self.assertFalse(screw_forecast_3['forced_replenish'])
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_screw.id,
'date': mps_dates_week[2][0],
'forecast_qty': 100
})
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_1['state'], 'to_launch')
self.assertEqual(screw_forecast_2['state'], 'to_launch')
self.assertEqual(screw_forecast_3['state'], 'to_launch')
self.assertTrue(screw_forecast_1['to_replenish'])
self.assertTrue(screw_forecast_2['to_replenish'])
self.assertTrue(screw_forecast_3['to_replenish'])
self.assertTrue(screw_forecast_1['forced_replenish'])
self.assertFalse(screw_forecast_2['forced_replenish'])
self.assertFalse(screw_forecast_3['forced_replenish'])
self.mps_screw.action_replenish(based_on_lead_time=True)
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertEqual(len(purchase_order_line.mapped('order_id')), 3)
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_1['state'], 'launched')
self.assertEqual(screw_forecast_2['state'], 'launched')
self.assertEqual(screw_forecast_3['state'], 'launched')
self.assertFalse(screw_forecast_1['to_replenish'])
self.assertFalse(screw_forecast_2['to_replenish'])
self.assertFalse(screw_forecast_3['to_replenish'])
self.assertFalse(screw_forecast_1['forced_replenish'])
self.assertFalse(screw_forecast_2['forced_replenish'])
self.assertFalse(screw_forecast_3['forced_replenish'])
def test_lead_times_2(self):
""" In the case of a multilevel bom with each their own lead time, we want
to make sure that indirect demand forecast is made at the correct time.
E.g.:
- table bom has a lead time of 10 day
- drawer bom has a lead time of 15 day
- table leg bom has a lead time of 10 day
if a forecast demand of 1 table is made for June, the indirect demand for all components will be:
- 1 drawer for May: 1st of June minus lead time of 10 days
- 4 table legs for May: 2 from table bom (June 1st - 10 days) and 2 from table>drawer boms (June 1st - 10+15 days)
- 12 screws for May: 4 from table>drawer (June 1st - 10+15 days)
and 8 from table>table leg (June 1st - 10+10 days)
- 8 screws for April: table>drawer>table leg (June 1st - 10+15+10 days)
- 8 bolts for May: table>table leg (June 1st - 10+10 days)
- 8 bolts for April: table>drawer>table leg (June 1st - 10+15+10 days)
"""
self.env.company.horizon_days = 0
self.env.company.manufacturing_period = 'month'
self.table.write({
'route_ids': [(6, 0, [self.ref('mrp.route_warehouse0_manufacture')])]
})
self.drawer.write({
'route_ids': [(6, 0, [self.ref('mrp.route_warehouse0_manufacture')])]
})
self.table_leg.write({
'route_ids': [(6, 0, [self.ref('mrp.route_warehouse0_manufacture')])]
})
self.bom_table.produce_delay = 10
self.bom_drawer.produce_delay = 15
self.bom_table_leg.produce_delay = 10
# Create a forecast demand of 1 for table 4 months from now, on the 1st
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table.id,
'date': self.mps_dates_month[3][0],
'forecast_qty': 1
})
drawer_forecasts = self.mps_drawer.get_production_schedule_view_state()[0]['forecast_ids']
self.assertEqual(drawer_forecasts[2]['indirect_demand_qty'], 1)
table_leg_forecasts = self.mps_table_leg.get_production_schedule_view_state()[0]['forecast_ids']
self.assertEqual(table_leg_forecasts[2]['indirect_demand_qty'], 4)
screw_forecasts = self.mps_screw.get_production_schedule_view_state()[0]['forecast_ids']
self.assertEqual(screw_forecasts[2]['indirect_demand_qty'], 12)
self.assertEqual(screw_forecasts[1]['indirect_demand_qty'], 8)
bolt_forecasts = self.mps_bolt.get_production_schedule_view_state()[0]['forecast_ids']
self.assertEqual(bolt_forecasts[2]['indirect_demand_qty'], 8)
self.assertEqual(bolt_forecasts[1]['indirect_demand_qty'], 8)
@freeze_time('2024-10-01')
def test_lead_times_3(self):
""" When showing a bigger period type (e.g. year > week), we want to make sure
that lead times are applied on the day of the forecast and not the first day
of the period. """
self.env.company.horizon_days = 0
self.env.company.manufacturing_period = 'month'
self.table.write({
'route_ids': [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]
})
self.bom_table.produce_delay = 1
self.mps_table.set_forecast_qty(1, 10)
self.mps_table.set_forecast_qty(2, 10)
self.mps_table.set_forecast_qty(3, 10)
self.mps_table.set_forecast_qty(4, 10)
mps_table, mps_drawer = (self.mps_table | self.mps_drawer).get_production_schedule_view_state(period_scale='year')
self.assertListEqual([f['forecast_qty'] for f in mps_table['forecast_ids']], [20, 20, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_drawer['forecast_ids']], [30, 10, 0])
def test_lead_times_with_multi_lvl_bom(self):
""" Using the configuration from setup BOM 1 with:
- the table has lead time
- the table leg has lead time and a safety stock target
ensure that the indirect demand of the component is correctly distributed across multiple
period when creating a table for week 3.
Material needed for producing the table in week 3:
- Table leg:
- week 2: 4
- Screws:
- week 2: 20
For the Safety stock of the table leg:
- Screws:
- week 0: 40
"""
self.env.company.manufacturing_period = 'week'
self.table.write({
'route_ids': [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]
})
self.bom_table.produce_delay = 1
self.bom_table_leg.produce_delay = 1
self.mps_table_leg.write({'forecast_target_qty': 10})
self.mps_table.set_forecast_qty(3, 1)
mps_table = self.mps_table.get_production_schedule_view_state()[0]
mps_table_leg = self.mps_table_leg.get_production_schedule_view_state()[0]
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
self.assertListEqual([f['indirect_demand_qty'] for f in mps_table['forecast_ids'][:4]], [0, 0, 0, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_table_leg['forecast_ids'][:4]], [0, 0, 4, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_screw['forecast_ids'][:4]], [40, 0, 20, 0])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_table['forecast_ids'][:4]], [0, 0, 0, 0])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_table_leg['forecast_ids'][:4]], [0, 10, 10, 10])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_screw['forecast_ids'][:4]], [0, 0, 0, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_table['forecast_ids'][:4]], [0, 0, 0, 1])
self.assertListEqual([f['replenish_qty'] for f in mps_table_leg['forecast_ids'][:4]], [10, 0, 4, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_screw['forecast_ids'][:4]], [40, 0, 20, 0])
def test_long_lead_times_with_multi_lvl_bom(self):
""" Using the configuration from setup BOM 1 with:
- the table has a long lead time.
- the drawers has a long lead time.
- the table leg has a lead time and a safety stock target.
ensure that the indirect demand of the component is correctly distributed across multiple
period when creating a table for week 6 and a drawer in week 5.
Material needed for producing the table in week 6:
- Drawers:
- week 2: 1
- Table leg:
- week 2: 2
- week 1: 2
- Screws:
- week 2: 8
- week 1: 12
For producing the drawer in week 5:
- Table leg:
- week 3: 2
- Screws:
- week 3: 12
For the Safety stock of the table leg:
- Screws:
- week 0: 40
"""
self.env.company.manufacturing_period = 'week'
self.table.write({
'route_ids': [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]
})
self.bom_table.produce_delay = 22
self.bom_drawer.produce_delay = 8
self.bom_table_leg.produce_delay = 1
self.mps_table_leg.write({'forecast_target_qty': 10})
self.mps_table.set_forecast_qty(6, 1)
self.mps_drawer.set_forecast_qty(5, 1)
mps_table = self.mps_table.get_production_schedule_view_state()[0]
mps_drawer = self.mps_drawer.get_production_schedule_view_state()[0]
mps_table_leg = self.mps_table_leg.get_production_schedule_view_state()[0]
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
self.assertListEqual([f['indirect_demand_qty'] for f in mps_table['forecast_ids'][:8]], [0, 0, 0, 0, 0, 0, 0, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_drawer['forecast_ids'][:8]], [0, 0, 1, 0, 0, 0, 0, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_table_leg['forecast_ids'][:8]], [0, 2, 2, 2, 0, 0, 0, 0])
self.assertListEqual([f['indirect_demand_qty'] for f in mps_screw['forecast_ids'][:8]], [40, 12, 8, 12, 0, 0, 0, 0])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_table['forecast_ids'][:8]], [0, 0, 0, 0, 0, 0, 0, 0])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_drawer['forecast_ids'][:8]], [0, 0, 0, 0, 0, 0, 0, 0])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_table_leg['forecast_ids'][:8]], [0, 10, 10, 10, 10, 10, 10, 10])
self.assertListEqual([f['starting_inventory_qty'] for f in mps_screw['forecast_ids'][:8]], [0, 0, 0, 0, 0, 0, 0, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_table['forecast_ids'][:8]], [0, 0, 0, 0, 0, 0, 1, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_drawer['forecast_ids'][:8]], [0, 0, 1, 0, 0, 1, 0, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_table_leg['forecast_ids'][:8]], [10, 2, 2, 2, 0, 0, 0, 0])
self.assertListEqual([f['replenish_qty'] for f in mps_screw['forecast_ids'][:8]], [40, 12, 8, 12, 0, 0, 0, 0])
def test_indirect_demand(self):
""" On a multiple BoM relation, ensure that the replenish quantity on
a production schedule impact the indirect demand on other production
that have a component as product.
"""
self.env.company.horizon_days = 0
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 2
})
# 2 drawer from table
mps_drawer = self.mps_drawer.get_production_schedule_view_state()[0]
drawer_forecast_1 = mps_drawer['forecast_ids'][0]
self.assertEqual(drawer_forecast_1['indirect_demand_qty'], 2)
# Screw for 2 tables:
# 2 * 2 legs * 4 screw = 16
# 1 drawer = 4 + 2 * legs * 4 = 12
# 16 + 2 drawers = 16 + 24 = 40
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(screw_forecast_1['indirect_demand_qty'], 40)
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_wardrobe.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 3
})
# 2 drawer from table + 9 from wardrobe (3 x 3)
mps_drawer, mps_screw = (self.mps_drawer | self.mps_screw).get_production_schedule_view_state()
drawer_forecast_1 = mps_drawer['forecast_ids'][0]
self.assertEqual(drawer_forecast_1['indirect_demand_qty'], 11)
# Screw for 2 tables + 3 wardrobe:
# 11 drawer = 11 * 12 = 132
# + 2 * 2 legs * 4 = 16
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(screw_forecast_1['indirect_demand_qty'], 148)
# Ensure that a forecast on another period will not impact the forecast
# for current period.
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table.id,
'date': self.mps_dates_month[1][0],
'forecast_qty': 2
})
mps_drawer, mps_screw = (self.mps_drawer | self.mps_screw).get_production_schedule_view_state()
drawer_forecast_1 = mps_drawer['forecast_ids'][0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
self.assertEqual(drawer_forecast_1['indirect_demand_qty'], 11)
self.assertEqual(screw_forecast_1['indirect_demand_qty'], 148)
self.assertEqual(screw_forecast_2['indirect_demand_qty'], 40)
# Ensure that a forecast on an intermediate schedule will correctly be added.
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table_leg.id,
'date': self.mps_dates_month[1][0],
'forecast_qty': 4
})
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
self.assertEqual(screw_forecast_2['indirect_demand_qty'], 56)
# Ensure that a manual replenish on an intermediate schedule will correctly
# move the difference to the next period
self.mps_table_leg.set_replenish_qty(date_index=1, quantity=3)
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_2['indirect_demand_qty'], 20)
self.assertEqual(screw_forecast_3['indirect_demand_qty'], 36)
def test_indirect_demand_kit(self):
""" On changing demand of a product whose BOM contains kit with a
component, ensure that the replenish quantity on a production schedule
impacts the indirect demand of kit's component.
"""
cabinet = self.env['product.product'].create({
'name': 'Cabinet',
'is_storable': True,
})
wood_kit = self.env['product.product'].create({
'name': 'Wood Kit',
'is_storable': True,
})
wood = self.env['product.product'].create({
'name': 'Wood',
'is_storable': True,
})
self.env['mrp.bom'].create({
'product_tmpl_id': cabinet.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [
Command.create({'product_id': wood_kit.id, 'product_qty': 1}),
],
})
self.env['mrp.bom'].create({
'product_tmpl_id': wood_kit.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [
Command.create({'product_id': wood.id, 'product_qty': 2}),
],
})
mps_cabinet = self.env['mrp.production.schedule'].create({
'product_id': cabinet.id,
'warehouse_id': self.warehouse.id,
})
mps_wood = self.env['mrp.production.schedule'].create({
'product_id': wood.id,
'warehouse_id': self.warehouse.id,
})
self.mps |= mps_cabinet | mps_wood
self.env['mrp.product.forecast'].create({
'production_schedule_id': mps_cabinet.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 2
})
# 4 wood from cabinet
mps_wood = mps_wood.get_production_schedule_view_state()[0]
wood_forecast_1 = mps_wood['forecast_ids'][0]
self.assertEqual(wood_forecast_1['indirect_demand_qty'], 4)
def test_delivery_quantity_kit(self):
"""On ordering a kit product containing a component ressuplied from another warehouse,
ensure the correct amount of component are ordered.
"""
second_warehouse = self.env['stock.warehouse'].create({
'name': 'Second Warehouse',
'code': 'WH2',
'resupply_wh_ids': [Command.link(self.warehouse.id)],
})
resupply_route = self.env['stock.route'].search([('supplier_wh_id', '=', self.warehouse.id), ('supplied_wh_id', '=', second_warehouse.id)], limit=1)
self.drawer.route_ids = [Command.set(resupply_route.ids)]
self.env['mrp.bom'].create({
'product_tmpl_id': self.wardrobe.product_tmpl_id.id,
'type': 'phantom',
'product_qty': 2,
'bom_line_ids': [
Command.create({'product_id': self.drawer.id, 'product_qty': 6}),
],
})
mps_wardrobe = self.env['mrp.production.schedule'].create({
'product_id': self.wardrobe.id,
'warehouse_id': second_warehouse.id,
'route_id': resupply_route.id,
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': mps_wardrobe.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 4,
})
mps_wardrobe.action_replenish()
self.assertEqual(self.env['stock.move'].search([('product_id', '=', self.drawer.id)], limit=1).product_qty, 12)
def test_impacted_schedule(self):
impacted_schedules = self.mps_screw.get_impacted_schedule()
self.assertEqual(sorted(impacted_schedules), sorted((self.mps - (self.mps_screw | self.mps_bolt)).ids))
impacted_schedules = self.mps_drawer.get_impacted_schedule()
self.assertEqual(sorted(impacted_schedules), sorted((self.mps_table |
self.mps_wardrobe | self.mps_table_leg | self.mps_screw | self.mps_bolt).ids))
def test_3_steps(self):
self.warehouse.manufacture_steps = 'pbm_sam'
self.table_leg.write({
'route_ids': [(6, 0, [self.ref('mrp.route_warehouse0_manufacture')])]
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table_leg.id,
'date': date.today(),
'forecast_qty': 25
})
self.mps_table_leg.action_replenish()
mps_table_leg = self.mps_table_leg.get_production_schedule_view_state()[0]
self.assertEqual(mps_table_leg['forecast_ids'][0]['forecast_qty'], 25.0, "Wrong resulting value of to_supply")
self.assertEqual(mps_table_leg['forecast_ids'][0]['incoming_qty'], 25.0, "Wrong resulting value of incoming quantity")
def test_interwh_delay(self):
"""
Suppose an interwarehouse configuration. The user adds some delays on
each rule of the interwh route. Then, the user defines a replenishment
qty on the MPS view and calls the replenishment action. This test
ensures that the MPS view includes the delays for the incoming quantity
"""
main_warehouse = self.warehouse
second_warehouse = self.env['stock.warehouse'].create({
'name': 'Second Warehouse',
'code': 'WH02',
})
main_warehouse.write({
'resupply_wh_ids': [(6, 0, second_warehouse.ids)]
})
interwh_route = self.env['stock.route'].search([('supplied_wh_id', '=', main_warehouse.id), ('supplier_wh_id', '=', second_warehouse.id)])
interwh_route.rule_ids.delay = 1
product = self.env['product.product'].create({
'name': 'SuperProduct',
'is_storable': True,
'route_ids': [(6, 0, interwh_route.ids)],
})
mps = self.env['mrp.production.schedule'].create({
'product_id': product.id,
'warehouse_id': main_warehouse.id,
})
interval_index = 3
mps.set_replenish_qty(interval_index, 1)
mps.action_replenish()
state = mps.get_production_schedule_view_state()[0]
for index, forecast in enumerate(state['forecast_ids']):
self.assertEqual(forecast['incoming_qty'], 1 if index == interval_index else 0, 'Incoming qty is incorrect for index %s' % index)
def test_outgoing_sm_and_lead_time_out_of_date_range(self):
"""
Set a lead time on delivery rule. Then generate an outgoing SM based on
that rule with:
- its date in dates range of MPS
- its date + rule's lead time outside the dates range of MPS
As a result, for the product mps, each outgoing quantity should be zero
"""
self.env.company.manufacturing_period = 'day'
self.env.company.manufacturing_period_to_display_day = 10
customer_location = self.env.ref('stock.stock_location_customers')
stock_location = self.warehouse.lot_stock_id
delivery_rule = self.env['stock.rule'].search([
('warehouse_id', '=', self.warehouse.id),
('location_src_id', '=', stock_location.id),
('location_dest_id', '=', customer_location.id),
('action', '=', 'pull')
], limit=1)
delivery_rule.delay = 15
product = self.env['product.product'].create({'name': 'SuperProduct', 'is_storable': True})
procurement = self.env["stock.rule"].Procurement(
product, 1, product.uom_id,
customer_location,
product.name,
"/",
self.env.company,
{
"warehouse_id": self.warehouse,
"date_planned": date.today() + timedelta(days=16),
}
)
self.env["stock.rule"].run([procurement])
tomorrow = start_of(datetime.now() + timedelta(days=1), 'day')
move = self.env['stock.move'].search([('product_id', '=', product.id)], limit=1)
self.assertEqual(move.date, tomorrow)
mps = self.env['mrp.production.schedule'].create({
'product_id': product.id,
'warehouse_id': self.warehouse.id,
})
state = mps.get_production_schedule_view_state()[0]
self.assertTrue(all(forecast['outgoing_qty'] == 0 for forecast in state['forecast_ids']))
def test_product_variants_in_mps(self):
"""
Test that only the impacted components are updated when the forecast demand of a product is changed.
"""
# create the attribute size with two values ('M', 'L')
size_attribute = self.env['product.attribute'].create({'name': 'Size', 'sequence': 4})
self.env['product.attribute.value'].create([{
'name': name,
'attribute_id': size_attribute.id,
'sequence': 1,
} for name in ('M', 'L')])
product, c1, c2 = self.env['product.product'].create([{
'name': i,
'is_storable': True,
} for i in range(3)])
product_template = product.product_tmpl_id
size_attribute_line = self.env['product.template.attribute.line'].create([{
'product_tmpl_id': product_template.id,
'attribute_id': size_attribute.id,
'value_ids': [(6, 0, size_attribute.value_ids.ids)]
}])
# Check that two product variant are created
self.assertEqual(product_template.product_variant_count, 2)
# Create a BoM with two components ('c1 applied only in m' and 'c2 applied only in L')
self.env['mrp.bom'].create({
'product_tmpl_id': product_template.id,
'product_uom_id': product_template.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({
'product_id': c1.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [(4, size_attribute_line.product_template_value_ids[0].id)]}), # M size
Command.create({
'product_id': c2.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [(4, size_attribute_line.product_template_value_ids[1].id)]}), # L size
]
})
mps_p_m, mps_p_l, mps_c1, mps_c2 = self.env['mrp.production.schedule'].create([{
'product_id': product,
'warehouse_id': self.warehouse.id,
} for product in (product_template.product_variant_ids[0] | product_template.product_variant_ids[1] | c1 | c2).ids])
# check the mps of the product variant M
mps_impacted = mps_p_m[0].get_impacted_schedule()
self.assertEqual(len(mps_impacted), 1)
self.assertEqual(mps_impacted[0], mps_c1.id)
# check the mps of the product variant L
mps_impacted = mps_p_l[0].get_impacted_schedule()
self.assertEqual(len(mps_impacted), 1)
self.assertEqual(mps_impacted[0], mps_c2.id)
def test_replenish_trigger(self):
"""Test that the replenish_trigger of components is 'manual'
and that 'automated' correctly triggers in the cron.
"""
mps_components = self.mps_drawer + self.mps_table_leg + self.mps_screw + self.mps_bolt
self.assertTrue(all(record.replenish_trigger == 'manual' for record in mps_components))
partner = self.env['res.partner'].create({'name': 'Bob Palindrome MacScam'})
seller = self.env['product.supplierinfo'].create({
'product_id': self.screw.id,
'partner_id': partner.id,
'price': 2,
'delay': 3
})
self.mps_screw.write({
'replenish_trigger': 'automated',
'supplier_id': seller.id
})
self.env.company.manufacturing_period = 'month'
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table.id,
'date': datetime.today(),
'forecast_qty': 1
})
self.env['mrp.production.schedule'].action_cron_replenish()
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
self.assertTrue(purchase_order_line)
self.assertEqual(purchase_order_line.date_planned.date(), self.mps_dates_month[0][0])
self.assertEqual(purchase_order_line.date_order.date(), self.mps_dates_month[0][0] - timedelta(days=3))
self.assertEqual(purchase_order_line.product_qty, 20)
self.assertEqual(purchase_order_line.price_subtotal, 40)
def test_set_forecast_qty(self):
""" Test that adding/removing quantities from the MPS
when manufacturing_period is 'month' or 'week'.
"""
self.env.company.manufacturing_period = 'month'
for delta in (3, 6, 12, 21):
self.env['mrp.product.forecast'].create({
'production_schedule_id': self.mps_table.id,
'date': self.mps_dates_month[1][0] + timedelta(days=delta),
'forecast_qty': delta
})
forecast_records = self.env['mrp.product.forecast'].search([('production_schedule_id', '=', self.mps_table.id)])
self.assertEqual(len(forecast_records), 4)
self.assertEqual(sum(forecast_records.mapped('forecast_qty')), 42, 'This is not the Answer to Life, the Universe, and Everything.')
self.mps_table.set_forecast_qty(1, 64)
forecast_records = self.env['mrp.product.forecast'].search([('production_schedule_id', '=', self.mps_table.id)])
self.assertEqual(len(forecast_records), 5)
self.assertEqual(forecast_records.mapped('forecast_qty'), [22, 3, 6, 12, 21])
self.assertEqual(forecast_records[0].date, self.mps_dates_month[1][0])
self.mps_table.set_forecast_qty(1, 84)
self.assertEqual(forecast_records.mapped('forecast_qty'), [42, 3, 6, 12, 21])
self.mps_table.set_forecast_qty(1, 72)
self.assertEqual(forecast_records.mapped('forecast_qty'), [42, 3, 6, 12, 9])
self.mps_table.set_forecast_qty(1, 50)
self.assertEqual(forecast_records.mapped('forecast_qty'), [42, 3, 5, 0, 0])
self.mps_table.set_forecast_qty(1, 21)
self.assertEqual(forecast_records.mapped('forecast_qty'), [21, 0, 0, 0, 0])
self.mps_table.set_forecast_qty(1, -13)
self.assertEqual(forecast_records.mapped('forecast_qty'), [-13, 0, 0, 0, 0])
def test_mps_sequence(self):
""" Test that products added automatically have a higher sequence than their parent. """
self.assertEqual(self.mps_table.mps_sequence, 10)
self.assertEqual(self.mps_chair.mps_sequence, 10)
self.assertEqual(self.mps_wardrobe.mps_sequence, 10)
self.assertEqual(self.mps_table_leg.mps_sequence, 11)
self.assertEqual(self.mps_drawer.mps_sequence, 11)
self.assertEqual(self.mps_screw.mps_sequence, 12)
self.assertEqual(self.mps_bolt.mps_sequence, 12)
def test_mps_sequence_2(self):
shelf = self.env['product.product'].create({
'name': 'shelf',
'is_storable': True,
})
plank = self.env['product.product'].create({
'name': 'plank',
'is_storable': True,
})
wood = self.env['product.product'].create({
'name': 'wood',
'is_storable': True,
})
bom_shelf = self.env['mrp.bom'].create({
'product_id': shelf.id,
'product_tmpl_id': shelf.product_tmpl_id.id,
'product_uom_id': shelf.uom_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({'product_id': plank.id, 'product_qty': 2}),
Command.create({'product_id': self.screw.id, 'product_qty': 3}),
],
})
bom_plank = self.env['mrp.bom'].create({
'product_id': plank.id,
'product_tmpl_id': plank.product_tmpl_id.id,
'product_uom_id': plank.uom_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({'product_id': wood.id, 'product_qty': 2}),
],
})
mps_wood = self.env['mrp.production.schedule'].create({
'product_id': wood.id,
'warehouse_id': self.warehouse.id,
})
self.assertEqual(mps_wood.mps_sequence, 10)
mps_shelf = self.env['mrp.production.schedule'].create({
'product_id': shelf.id,
'warehouse_id': self.warehouse.id,
'bom_id': bom_shelf.id
})
self.assertEqual(mps_shelf.mps_sequence, 10)
self.assertEqual(self.mps_screw.mps_sequence, 11)
self.assertEqual(mps_wood.mps_sequence, 12)
mps_plank = self.env['mrp.production.schedule'].create({
'product_id': plank.id,
'warehouse_id': self.warehouse.id,
'bom_id': bom_plank.id
})
self.assertEqual(mps_plank.mps_sequence, 11)
self.assertEqual(mps_wood.mps_sequence, 12)
def test_is_indirect(self):
""" Test that products added automatically are flagged as indirect demand products. """
mps_components = self.mps_drawer + self.mps_table_leg + self.mps_screw + self.mps_bolt
self.assertTrue(all(record.is_indirect for record in mps_components))
def test_no_route_product_in_mps(self):
""" Test that adding a product with no route enabled does not trigger an error. """
self.mps_table.unlink()
self.table.route_ids = [Command.clear()]
self.assertEqual(len(self.table.route_ids), 0)
mps_table_2 = self.env['mrp.production.schedule'].create({
'product_id': self.table.id,
'warehouse_id': self.warehouse.id,
'bom_id': self.bom_table.id,
})
self.assertFalse(mps_table_2.route_id)
@freeze_time('2024-01-01')
def test_periods_display(self):
""" Test that each period type (year, month, week, day) returns the correct
number of columns with the correct column title. """
self.env.company.manufacturing_period_to_display_year = 5
self.env.company.manufacturing_period_to_display_month = 15
self.env.company.manufacturing_period_to_display_week = 7
self.env.company.manufacturing_period_to_display_day = 21
mps_state_default = self.mps.get_mps_view_state()
self.assertEqual(mps_state_default['manufacturing_period'], 'month')
self.assertEqual(len(mps_state_default['dates']), self.env.company.manufacturing_period_to_display_month)
self.assertListEqual(mps_state_default['dates'][9:], ['Oct 2024', 'Nov 2024', 'Dec 2024', 'Jan 2025', 'Feb 2025', 'Mar 2025'])
mps_state_year = self.mps.get_mps_view_state(period_scale='year')
self.assertEqual(mps_state_year['manufacturing_period'], 'year')
self.assertEqual(len(mps_state_year['dates']), self.env.company.manufacturing_period_to_display_year)
self.assertListEqual(mps_state_year['dates'], ['2024', '2025', '2026', '2027', '2028'])
mps_state_week = self.mps.get_mps_view_state(period_scale='week')
self.assertEqual(len(mps_state_week['dates']), self.env.company.manufacturing_period_to_display_week)
self.assertEqual(mps_state_week['dates'][2:5], ['Week 3 (15-21/Jan)', 'Week 4 (22-28/Jan)', 'Week 5 (29-4/Feb)'])
mps_state_day = self.mps.get_mps_view_state(period_scale='day')
self.assertEqual(len(mps_state_day['dates']), self.env.company.manufacturing_period_to_display_day)
self.assertEqual(mps_state_day['dates'][7:12], ['Jan 8', 'Jan 9', 'Jan 10', 'Jan 11', 'Jan 12'])
def test_outgoing_move_with_different_uom(self):
"""
Test that the outgoing and incoming quantities are computed in the product's UoM.
"""
product_a = self.env['product.product'].create({
'name': 'product a test',
'is_storable': True,
})
mps = self.env['mrp.production.schedule'].create({
'product_id': product_a.id,
'warehouse_id': self.warehouse.id,
})
outgoing_move = self.env['stock.move'].create({
'product_id': product_a.id,
'product_uom_qty': 1,
'product_uom': self.env.ref('uom.product_uom_dozen').id,
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
incoming_move = self.env['stock.move'].create({
'product_id': product_a.id,
'product_uom_qty': 1,
'product_uom': self.env.ref('uom.product_uom_dozen').id,
'location_id': self.env.ref('stock.stock_location_customers').id,
'location_dest_id': self.warehouse.lot_stock_id.id,
})
(outgoing_move | incoming_move)._action_confirm()
state = mps.get_production_schedule_view_state()[0]
outgoing_qty = state['forecast_ids'][0]['outgoing_qty']
incoming_qty = state['forecast_ids'][0]['incoming_qty']
self.assertEqual(outgoing_qty, 12, 'outgoing qty is incorrect')
self.assertEqual(incoming_qty, 12, 'incoming qty is incorrect')
def test_forecast_target_qty(self):
""" Test that adding a safety stock target does not break indirect demand computation.
All periods are in month starting at January for more clarity. """
# Base case, set the safety stock target for the schedule of a final product
# table stock target = 3
# -> January: leg replenish qty == 12, screw replenish qty == 60
# -> October: table starting qty == 3
self.env.company.horizon_days = 0
self.mps_table.forecast_target_qty = 3
mps_table, mps_table_leg, mps_screw = (self.mps_table | self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
table_forecast_10 = mps_table['forecast_ids'][9]
leg_forecast_1 = mps_table_leg['forecast_ids'][0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(leg_forecast_1['replenish_qty'], 12)
self.assertEqual(screw_forecast_1['replenish_qty'], 60)
self.assertEqual(table_forecast_10['starting_inventory_qty'], 3)
# Manually set the replenish qty of that same schedule
# January: table replenish qty = 1
# -> January: leg replenish qty == 4, screw replenish qty == 20
# -> February: leg replenish qty == 8, screw replenish qty == 40
self.mps_table.set_replenish_qty(date_index=0, quantity=1)
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
leg_forecast_1 = mps_table_leg['forecast_ids'][0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
leg_forecast_2 = mps_table_leg['forecast_ids'][1]
screw_forecast_2 = mps_screw['forecast_ids'][1]
self.assertEqual(leg_forecast_1['replenish_qty'], 4)
self.assertEqual(screw_forecast_1['replenish_qty'], 20)
self.assertEqual(leg_forecast_2['replenish_qty'], 8)
self.assertEqual(screw_forecast_2['replenish_qty'], 40)
# Set the forecasted demand of that same intermediate component
# January: leg demand qty = 1
# -> January: bolt replenish qty == 20, screw replenish qty == 24
# -> February: bolt replenish qty == 24
self.mps_table_leg.set_forecast_qty(date_index=0, quantity=1)
mps_bolt, mps_screw = (self.mps_bolt | self.mps_screw).get_production_schedule_view_state()
bolt_forecast_1 = mps_bolt['forecast_ids'][0]
bolt_forecast_2 = mps_bolt['forecast_ids'][1]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(bolt_forecast_1['replenish_qty'], 20)
self.assertEqual(bolt_forecast_2['replenish_qty'], 32)
self.assertEqual(screw_forecast_1['replenish_qty'], 24)
# Manually set the replenish qty of that same intermediate component
# January: leg replenish qty = 6
# -> February: leg replenish qty == 7, bolt replenish qty == 28
self.mps_table_leg.set_replenish_qty(date_index=0, quantity=6)
mps_table_leg, mps_bolt = (self.mps_table_leg | self.mps_bolt).get_production_schedule_view_state()
leg_forecast_2 = mps_table_leg['forecast_ids'][1]
bolt_forecast_2 = mps_bolt['forecast_ids'][1]
self.assertEqual(leg_forecast_2['replenish_qty'], 7)
self.assertEqual(bolt_forecast_2['replenish_qty'], 28)
# Set the safety stock target of an intermediate component that needs the previous component
# drawer stock target = 5
# -> January: drawer replenish qty == 6, screw replenish qty == 48
# -> February: leg replenish qty == 17, screw replenish qty == 76
# -> September: drawer starting qty == 5
self.mps_drawer.forecast_target_qty = 5
mps_drawer, mps_table_leg, mps_screw = (self.mps_drawer | self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
drawer_forecast_1 = mps_drawer['forecast_ids'][0]
drawer_forecast_9 = mps_drawer['forecast_ids'][8]
leg_forecast_2 = mps_table_leg['forecast_ids'][1]
screw_forecast_1 = mps_screw['forecast_ids'][0]
screw_forecast_2 = mps_screw['forecast_ids'][1]
self.assertEqual(drawer_forecast_1['replenish_qty'], 6)
self.assertEqual(drawer_forecast_9['starting_inventory_qty'], 5)
self.assertEqual(leg_forecast_2['replenish_qty'], 17)
self.assertEqual(screw_forecast_1['replenish_qty'], 48)
self.assertEqual(screw_forecast_2['replenish_qty'], 76)
def test_min_to_replenish_qty(self):
""" Test that setting a minimum qty to replenish like a logical person computes correctly.
All periods are in month starting at January for more clarity. """
self.mps_table_leg.write({'min_to_replenish_qty': 5})
# Replenish qty is inferior to min_to_replenish_qty
# January: table demand qty = 1
# -> January: leg indirect demand == 4, leg replenish qty == 5, screw replenish qty == 24
self.mps_table.set_forecast_qty(date_index=0, quantity=1)
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
leg_forecast_1 = mps_table_leg['forecast_ids'][0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(leg_forecast_1['indirect_demand_qty'], 4)
self.assertEqual(leg_forecast_1['replenish_qty'], 5)
self.assertEqual(screw_forecast_1['replenish_qty'], 24)
# Replenish qty is above min_to_replenish_qty
# January: table demand qty = 2
# -> January: leg indirect demand == 8, leg replenish qty == 8, screw replenish qty == 40
self.mps_table.set_forecast_qty(date_index=0, quantity=2)
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
leg_forecast_1 = mps_table_leg['forecast_ids'][0]
screw_forecast_1 = mps_screw['forecast_ids'][0]
self.assertEqual(leg_forecast_1['indirect_demand_qty'], 8)
self.assertEqual(leg_forecast_1['replenish_qty'], 8)
self.assertEqual(screw_forecast_1['replenish_qty'], 40)
@freeze_time('2025-01-01')
def test_starting_inventory_qty(self):
self.env.company.horizon_days = 0
self.env['stock.quant'].create({
'product_id': self.table.id,
'inventory_quantity': 5,
'location_id': self.warehouse.lot_stock_id.id
}).action_apply_inventory()
self.mps_table.set_forecast_qty(2, 3)
mps_table = self.mps_table.get_production_schedule_view_state()[0]
table_forecast_1 = mps_table['forecast_ids'][0]
table_forecast_3 = mps_table['forecast_ids'][2]
table_forecast_5 = mps_table['forecast_ids'][4]
self.assertEqual(table_forecast_1['starting_inventory_qty'], 5)
self.assertEqual(table_forecast_3['starting_inventory_qty'], 5)
self.assertEqual(table_forecast_3['replenish_qty'], 0)
self.assertEqual(table_forecast_3['safety_stock_qty'], 2)
self.assertEqual(table_forecast_5['starting_inventory_qty'], 2)
self.mps_table.set_forecast_qty(1, 4)
mps_table, mps_table_leg, mps_screw = (self.mps_table | self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
table_forecast_2 = mps_table['forecast_ids'][1]
table_forecast_3 = mps_table['forecast_ids'][2]
table_forecast_7 = mps_table['forecast_ids'][6]
leg_forecast_3 = mps_table_leg['forecast_ids'][2]
screw_forecast_3 = mps_screw['forecast_ids'][2]
self.assertEqual(table_forecast_2['starting_inventory_qty'], 5)
self.assertEqual(table_forecast_2['replenish_qty'], 0)
self.assertEqual(table_forecast_2['safety_stock_qty'], 1)
self.assertEqual(table_forecast_3['starting_inventory_qty'], 1)
self.assertEqual(table_forecast_3['replenish_qty'], 2)
self.assertEqual(table_forecast_7['starting_inventory_qty'], 0)
self.assertEqual(leg_forecast_3['replenish_qty'], 8)
self.assertEqual(screw_forecast_3['replenish_qty'], 40)
# test with lead times
self.bom_table.produce_delay = 1
self.table.write({'route_ids': [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]})
mps_table, mps_table_leg, mps_screw = (self.mps_table | self.mps_table_leg | self.mps_screw).get_production_schedule_view_state()
table_forecast_7 = mps_table['forecast_ids'][6]
leg_forecast_2 = mps_table_leg['forecast_ids'][1]
screw_forecast_2 = mps_screw['forecast_ids'][1]
self.assertEqual(table_forecast_7['starting_inventory_qty'], 0)
self.assertEqual(leg_forecast_2['replenish_qty'], 8)
self.assertEqual(screw_forecast_2['replenish_qty'], 40)
# test with lead times and period switches
# Switch period type to week
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state(period_scale='week')
leg_forecast_8 = mps_table_leg['forecast_ids'][7] # 3rd week of February 2025
leg_forecast_9 = mps_table_leg['forecast_ids'][8] # last week of February 2025
screw_forecast_9 = mps_screw['forecast_ids'][8] # last week of February 2025
self.assertEqual(leg_forecast_8['indirect_demand_qty'], 0)
self.assertEqual(leg_forecast_9['indirect_demand_qty'], 8)
self.assertEqual(screw_forecast_9['indirect_demand_qty'], 40)
# While in week view, set new forecasted demand
self.mps_table.set_forecast_qty(8, 0, period_scale='week') # last week of February 2025 (ends on March 2nd), set to 0
self.mps_table.set_forecast_qty(9, 3, period_scale='week') # first FULL week of March 2025, set to 3
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state(period_scale='week')
leg_forecast_9 = mps_table_leg['forecast_ids'][8] # last week of February 2025
screw_forecast_9 = mps_screw['forecast_ids'][8] # last week of February 2025
self.assertEqual(leg_forecast_9['indirect_demand_qty'], 8)
self.assertEqual(screw_forecast_9['indirect_demand_qty'], 40)
# Switch period type back to month
# The indirect demand should be on the same month as the forecasted
# demand because the previous week is still part of the same month.
mps_table_leg, mps_screw = (self.mps_table_leg | self.mps_screw).get_production_schedule_view_state(period_scale='month')
leg_forecast_2 = mps_table_leg['forecast_ids'][1] # February 2025
leg_forecast_3 = mps_table_leg['forecast_ids'][2] # March 2025
screw_forecast_3 = mps_screw['forecast_ids'][2] # March 2025
self.assertEqual(leg_forecast_2['indirect_demand_qty'], 0)
self.assertEqual(leg_forecast_3['indirect_demand_qty'], 8)
self.assertEqual(screw_forecast_3['indirect_demand_qty'], 40)
def test_actual_replenishment_wizard(self):
""" We check that the replenishment popup shows the correct values of
what has been order for replenishment:
- MO for 1 unit of table
- PO for 24 units (2 dozen) of screw
- RFQ for 12 units (1 dozen) of screw """
partner = self.env['res.partner'].create({'name': 'Bob Palindrome MacScam'})
self.env['product.supplierinfo'].create({
'product_id': self.screw.id,
'partner_id': partner.id,
'price': 12.0,
'delay': 0
})
self.table.route_ids = [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]
# Create a MO for 1 table and a PO for 20 screws
self.mps_table.set_forecast_qty(0, 1)
(self.mps_table | self.mps_screw).action_replenish()
# Change the POL qty from 20 screws to 2 dozen screws (4 more than necessary), validate the PO
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
purchase_order_line.write({
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
'product_qty': 2,
})
purchase_order_line.order_id.button_confirm()
# Create a new demand for 7 screws, 4 will be taken from the excess of the first PO
# Create a second PO for 3 screws
self.mps_screw.set_forecast_qty(0, 7)
self.mps_screw.action_replenish()
table_action = self.mps_table.action_open_actual_replenishment_details('Wizard table testing', self.mps_dates_month[0][0], self.mps_dates_month[0][1])
table_wizard = Form.from_action(self.env, table_action)
self.assertEqual(table_wizard.manufacture_qty, 1)
screw_action = self.mps_screw.action_open_actual_replenishment_details('Wizard screw testing', self.mps_dates_month[0][0], self.mps_dates_month[0][1])
screw_wizard = Form.from_action(self.env, screw_action)
self.assertEqual(screw_wizard.moves_qty, 24)
self.assertEqual(screw_wizard.rfq_qty, 3)
# Change the POL qty from 3 screws to 1 dozen screws
purchase_order_line_2 = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id), ('id', '!=', purchase_order_line.id)])
purchase_order_line_2.write({
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
'product_qty': 1,
})
screw_wizard_2 = Form.from_action(self.env, screw_action)
self.assertEqual(screw_wizard_2.moves_qty, 24)
self.assertEqual(screw_wizard_2.rfq_qty, 12)
def test_actual_indirect_demand(self):
"""Test that Indirect Actual Demand is correctly computed apart from
Actual Demand (direct).
Indirect Actual Demand is affected by MOs or moves to subcontracting locations.
(Direct) Actual Demand is affected by direct SOs.
"""
today = datetime.now().date()
# create OUT delivery for drawer (Direct Actual Demand)
self._create_and_process_delivery_at_date(
[(self.drawer, 10)], today
)
# create MO for table to check Indirect Actual Demand of drawer
production = self.env['mrp.production'].create({
'product_id': self.table.id,
'product_qty': 5,
'product_uom_id': self.table.uom_id.id,
'bom_id': self.bom_table.id,
'location_src_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.warehouse.lot_stock_id.id,
'origin': 'Test MPS',
})
production.action_confirm()
production.button_mark_done()
mps_drawer = self.mps_drawer.get_production_schedule_view_state()[0]
drawer_forecast = mps_drawer['forecast_ids'][0]
self.assertEqual(drawer_forecast['outgoing_qty'], 10)
self.assertEqual(drawer_forecast['indirect_outgoing_qty'], 5)
def test_early_replenishment_calculation(self):
""" Test that if an early replenishment occured in a period where the replenishment
is greater than the demand, the next periods take into account these extra quantities
when setting replenish_qty.
"""
partner = self.env['res.partner'].create({'name': 'Al-Khwarizmi'})
self.env['product.supplierinfo'].create({
'product_id': self.screw.id,
'partner_id': partner.id,
'price': 12.0,
'delay': 0
})
self.table.route_ids = [Command.set([self.ref('mrp.route_warehouse0_manufacture')])]
# Create an MO for 1 table and a PO for 20 screws
self.mps_table.set_forecast_qty(0, 1)
(self.mps_table | self.mps_screw).action_replenish()
# Change the POL qty from 20 screws to 2 dozen screws (4 more than necessary), validate the PO
purchase_order_line = self.env['purchase.order.line'].search([('product_id', '=', self.screw.id)])
purchase_order_line.write({
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
'product_qty': 2,
})
purchase_order_line.order_id.button_confirm()
# Create a demand for 2 screws in period 1, no replenish_qty should be set in period 1
self.mps_screw.set_forecast_qty(1, 2)
# Create a demand for 5 screws in period 2, replenish_qty = 3 should be set in period 2
self.mps_screw.set_forecast_qty(2, 5)
mps_screw = self.mps_screw.get_production_schedule_view_state()[0]
screw_forecast_1 = mps_screw['forecast_ids'][1]
self.assertEqual(screw_forecast_1['replenish_qty'], 0)
screw_forecast_2 = mps_screw['forecast_ids'][2]
self.assertEqual(screw_forecast_2['replenish_qty'], 3)
def test_actual_demand_multisteps(self):
""" Test that actual demand is correctly calculated when deliveries are in multi-steps.
It should only take into account the last move of the delivery chain.
When that move is marked as done, it should instead use the next move if there's one. """
self.env['stock.quant']._update_available_quantity(self.table, self.warehouse.lot_stock_id, 5)
self.warehouse.delivery_steps = 'pick_pack_ship'
reference = self.env['stock.reference'].create({'name': 'Test-MPS-actual-demand-multisteps'})
self.env['stock.rule'].run([
self.env['stock.rule'].Procurement(
self.table,
5.0,
self.table.uom_id,
self.env.ref('stock.stock_location_customers'),
reference.name,
reference.name,
self.warehouse.company_id,
{
'warehouse_id': self.warehouse,
'reference_ids': reference,
},
),
])
# Check that the outgoing quantity is 5 and that the related picking is the pick_picking
pick_picking = reference.picking_ids
mps_table = self.mps_table.get_production_schedule_view_state()[0]
self.assertEqual(mps_table['forecast_ids'][0]['outgoing_qty'], 5, 'actual demand qty is incorrect')
# Get the domain_moves, it is always the same so no need to get it again
domain_moves = self.mps_table._get_moves_domain(self.mps_dates_month[0][0], self.mps_dates_month[0][1], 'outgoing')
mps_picking_1 = self.mps_table._get_moves_and_date(domain_moves)[0][0].picking_id
self.assertEqual(mps_picking_1, pick_picking, 'It should be the pick_picking')
# Validate the pick_picking, check that the outgoing quantity is still 5 and that the related picking is the pack_picking
pick_picking.button_validate()
pack_picking = pick_picking.move_ids.move_dest_ids.picking_id
mps_table = self.mps_table.get_production_schedule_view_state()[0]
mps_picking_2 = self.mps_table._get_moves_and_date(domain_moves)[0][0].picking_id
self.assertEqual(mps_table['forecast_ids'][0]['outgoing_qty'], 5, 'actual demand qty is incorrect')
self.assertEqual(mps_picking_2, pack_picking, 'It should be the pack_picking')
# Validate the pack_picking, check that the outgoing quantity is still 5 and that the related picking is the ship_picking
pack_picking.button_validate()
ship_picking = pack_picking.move_ids.move_dest_ids.picking_id
mps_table = self.mps_table.get_production_schedule_view_state()[0]
mps_picking_3 = self.mps_table._get_moves_and_date(domain_moves)[0][0].picking_id
self.assertEqual(mps_table['forecast_ids'][0]['outgoing_qty'], 5, 'actual demand qty is incorrect')
self.assertEqual(mps_picking_3, ship_picking, 'It should be the ship_picking')
def test_actual_demand_multisteps_2(self):
""" Test that actual demand is correctly calculated when inter-warehouse deliveries are in multi-steps.
It should only take into account the move 'Output -> Transit' from the first warehouse since it is
created from the start. """
# Create a second warehouse CC that is supplied by WH
second_warehouse = self.env['stock.warehouse'].create({
'name': 'Cainhurst Castle',
'code': 'CC',
'resupply_wh_ids': [Command.link(self.warehouse.id)],
})
# Link the resupply route to the product
resupply_route = self.env['stock.route'].search([('supplier_wh_id', '=', self.warehouse.id), ('supplied_wh_id', '=', second_warehouse.id)])
self.table.route_ids = [Command.set([resupply_route.id])]
self.env['stock.quant']._update_available_quantity(self.table, self.warehouse.lot_stock_id, 5)
self.warehouse.delivery_steps = 'pick_pack_ship'
reference = self.env['stock.reference'].create({'name': 'Test-MPS-actual-demand-multisteps-interwarehouse'})
self.env['stock.rule'].run([
self.env['stock.rule'].Procurement(
self.table,
5.0,
self.table.uom_id,
second_warehouse.lot_stock_id,
reference.name,
reference.name,
second_warehouse.company_id,
{
'warehouse_id': second_warehouse,
'reference_ids': reference,
},
),
])
# Check that the outgoing quantity is 5 and that the related picking is the transit picking for WH
wh_transit_picking = reference.move_ids.filtered(lambda m: m.location_dest_id.usage == 'transit').picking_id
mps_table_wh0 = self.mps_table.get_production_schedule_view_state()[0]
self.assertEqual(mps_table_wh0['forecast_ids'][0]['outgoing_qty'], 5, 'actual demand qty is incorrect')
# Get the domain_moves for outgoing moves for WH
domain_moves = self.mps_table._get_moves_domain(self.mps_dates_month[0][0], self.mps_dates_month[0][1], 'outgoing')
mps_picking_wh_out = self.mps_table._get_moves_and_date(domain_moves)[0][0].picking_id
self.assertEqual(mps_picking_wh_out, wh_transit_picking, 'It should be the transit picking for WH')
# Create an MPS record for Table for the second warehouse CC
mps_record_table_cc = self.env['mrp.production.schedule'].create({
'product_id': self.table.id,
'warehouse_id': second_warehouse.id,
'bom_id': self.bom_table.id,
})
# Check that the incoming quantity is 5 and that the related picking is the transit picking for CC
cc_transit_picking = reference.move_ids.filtered(lambda m: m.location_id.usage == 'transit').picking_id
mps_table_cc0 = mps_record_table_cc.get_production_schedule_view_state()[0]
self.assertEqual(mps_table_cc0['forecast_ids'][0]['incoming_qty'], 5, 'actual replenishment qty is incorrect')
# Get the domain_moves for incoming moves for CC
domain_moves = mps_record_table_cc._get_moves_domain(self.mps_dates_month[0][0], self.mps_dates_month[0][1], 'incoming')
mps_picking_cc_in = mps_record_table_cc._get_moves_and_date(domain_moves)[0][0].picking_id
self.assertEqual(mps_picking_cc_in, cc_transit_picking, 'It should be the transit picking for CC')
def test_isolated_access(self):
dummy = self.env['res.users'].create({
'name': 'mps user',
'login': 'mps',
'email': 'test@test.test',
# no 'Admin / Access Rights' group
'group_ids': [Command.set((
self.env.ref('mrp.group_mrp_manager').id,
))],
})
for fname in self.env.company._fields:
if self.env.company._is_field_mps_display_group(fname):
self.env.company.with_user(dummy).write({fname: not self.env.company[fname]})
# no access errors
self.assertTrue(True)
def test_mps_replenish_correct_bom(self):
"""Test that the manufacturing orders created by MPS replenishment use the correct BOM."""
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture').id
# Create a product with two BoMs
product = self.env['product.product'].create({
'name': 'MPS Test Product',
'is_storable': True,
'route_ids': [Command.set([manufacture_route])]
})
# BoM 1
self.env['mrp.bom'].create({
'product_tmpl_id': product.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [
Command.create({'product_id': self.screw.id, 'product_qty': 2}),
],
})
# BoM 2
bom2 = self.env['mrp.bom'].create({
'product_tmpl_id': product.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [
Command.create({'product_id': self.bolt.id, 'product_qty': 4}),
],
})
# Trigger procurement using the second BoM
mps = self.env['mrp.production.schedule'].create({
'product_id': product.id,
'warehouse_id': self.warehouse.id,
'bom_id': bom2.id,
'route_id': manufacture_route,
})
self.env['mrp.product.forecast'].create({
'production_schedule_id': mps.id,
'date': self.mps_dates_month[0][0],
'forecast_qty': 100
})
mps.action_replenish()
# Check if the MO is created with the second BoM
production = self.env['mrp.production'].search([('product_id', '=', product.id)], limit=1)
self.assertEqual(production.bom_id, bom2, "MO was created with an incorrect BOM")
@freeze_time("2024-02-14")
def test_suggestion_for_years_with_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
date = today - relativedelta(years=1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(months=10)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(years=2)
self._create_and_process_delivery_at_date(
[(self.table, 4)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_year = 5
# choose to suggest only for first period/year (index=1).
# when choosing a certain period for suggestion, only last year moves are considered
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
'period': 1
})
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 15)
else:
# since we are choosing a certain period (first year), other periods = 0
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose to suggest only for second period/year (index=2)
table_suggestion_wizard.period = 2
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
# from previous suggestion, unchanged
self.assertEqual(table_forecast['forecast_qty'], 15)
elif i == 1:
# this takes into account year - 1 (this year) confirmed moves
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
# since we are choosing a certain period, other periods = 0
self.assertEqual(table_forecast['forecast_qty'], 0)
@freeze_time("2024-02-14")
def test_suggestion_for_months_with_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
date = today - relativedelta(years=1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(months=10)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(years=2)
self._create_and_process_delivery_at_date(
[(self.table, 4)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_month = 25
# choose to suggest only for first period/month (index=1)
# when choosing a certain period for suggestion, only last year moves are considered
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
'period': 1
})
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose to suggest only for third period/month (index=3)
table_suggestion_wizard.period = 3
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
# from previous suggestion, unchanged
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 2:
# this takes the moves from 10 months ago, for that month it's last year moves
self.assertEqual(table_forecast['forecast_qty'], 5)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
@freeze_time("2024-02-14")
def test_suggestion_for_weeks_with_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
based_on_year = today.year - 1
date = start_of(subtract(today, years=today.year - based_on_year), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = start_of(subtract(today, weeks=50), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = start_of(subtract(today, years=today.year - based_on_year, weeks=2), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = start_of(today, 'week')
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_week = 105
# choose the first period/week for suggestion (index=1)
# when choosing a certain period for suggestion, only last year moves are considered
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
'period': 1
})
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose the third period/week for suggestion (index=3)
table_suggestion_wizard.period = 3
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
# same from previous suggestion, unchanged
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 2:
# this takes the moves from 50 weeks ago, for that week it's last year moves
self.assertEqual(table_forecast['forecast_qty'], 5)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
@freeze_time("2024-02-26")
def test_suggestion_for_days_with_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
date = today + relativedelta(years=-1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(days=363)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(years=2)
self._create_and_process_delivery_at_date(
[(self.table, 4)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_day = 365
# choose the first period/day for suggestion (index=1)
# when choosing a certain period for suggestion, only last year moves are considered
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
'period': 1
})
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose the third period/day for suggestion (index=3)
table_suggestion_wizard.period = 3
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
# from previous suggestion, unchanged
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 2:
# this takes the moves from 363 days ago, for that day it's last year moves
self.assertEqual(table_forecast['forecast_qty'], 5)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
@freeze_time("2024-02-14")
def test_suggestion_for_years_with_no_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
self.table.uom_id.rounding = 1.0
date = today - relativedelta(years=1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(months=10)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(days=20)
self._create_and_process_delivery_at_date(
[(self.table, 100)], date
)
date = today - relativedelta(days=60)
self._create_and_process_delivery_at_date(
[(self.table, 90)], date
)
date = today - relativedelta(years=2)
self._create_and_process_delivery_at_date(
[(self.table, 4)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_year = 5
# we choose suggestion to be based on 'last year', so every period's suggestion
# is taken from same period but previous year. (i.e. 2026 based on 2025 and so on)
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
})
table_suggestion_wizard.based_on = 'last_year'
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 105)
elif i == 1:
self.assertEqual(table_forecast['forecast_qty'], 110)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose suggestion to be based on Actual Demand, every period's suggestion is
# based on the present moves in that period, nothing in the past.
table_suggestion_wizard.based_on = 'actual_demand'
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 120)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# when choosing suggestion to be based on last 30 days, last 3 months or
# last 12 months, this takes into consideration the moves in last x days
# from today and takes a ratio out of it depending on the period (year, month, week or day).
# for example if last 30 day suggestion is 60 but my current period type is days,
# I sould suggest (1/30 of that amount so suggestion for each period(day) = 1/30 * 60)
table_suggestion_wizard.based_on = '30_days'
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 1440)
table_suggestion_wizard.based_on = 'three_months'
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 840)
table_suggestion_wizard.based_on = 'one_year'
table_suggestion_wizard.with_context({'period_scale': 'year'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='year')[0]
for i in range(self.env.company.manufacturing_period_to_display_year):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 225)
@freeze_time("2024-02-14")
def test_suggestion_for_months_with_no_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
self.table.uom_id.rounding = 1.0
date = today - relativedelta(years=1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(months=10)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(days=20)
self._create_and_process_delivery_at_date(
[(self.table, 100)], date
)
date = today - relativedelta(days=60)
self._create_and_process_delivery_at_date(
[(self.table, 90)], date
)
date = today - relativedelta(years=2)
self._create_and_process_delivery_at_date(
[(self.table, 4)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_month = 25
# we choose suggestion to be based on 'last year', so every period's suggestion
# is taken from same period but previous year. (May 2026 based on May 2025 and so on)
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
})
table_suggestion_wizard.based_on = 'last_year'
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 2:
self.assertEqual(table_forecast['forecast_qty'], 5)
elif i == 10:
self.assertEqual(table_forecast['forecast_qty'], 90)
elif i == 11:
self.assertEqual(table_forecast['forecast_qty'], 100)
elif i == 12:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose suggestion to be based on Actual Demand, every period's suggestion is
# based on the present moves in that period, nothing in the past.
table_suggestion_wizard.based_on = 'actual_demand'
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 20)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# when choosing suggestion to be based on last 30 days, last 3 months or
# last 12 months, this takes into consideration the moves in last x days
# from today and takes a ratio out of it depending on the period (year, month, week or day).
# for example if last 30 day suggestion is 60 but my current period type is days,
# I sould suggest (1/30 of that amount so suggestion for each period(day) = 1/30 * 60)
table_suggestion_wizard.based_on = '30_days'
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 120)
table_suggestion_wizard.based_on = 'three_months'
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 70)
table_suggestion_wizard.based_on = 'one_year'
table_suggestion_wizard.with_context({'period_scale': 'month'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='month')[0]
for i in range(self.env.company.manufacturing_period_to_display_month):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 19)
@freeze_time("2024-02-14")
def test_suggestion_for_weeks_with_no_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
based_on_year = today.year - 1
self.table.uom_id.rounding = 1.0
date = start_of(subtract(today, years=today.year - based_on_year), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = start_of(subtract(today, weeks=3), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 100)], date
)
date = start_of(subtract(today, weeks=9), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 90)], date
)
date = start_of(subtract(today, years=today.year - based_on_year, weeks=2), 'week')
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = start_of(today, 'week')
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_week = 105
# we choose suggestion to be based on 'last year', so every period's suggestion
# is taken from same period but previous year. (week 4, 2026 based on week 4, 2025 and so on)
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
})
table_suggestion_wizard.based_on = 'last_year'
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 49:
self.assertEqual(table_forecast['forecast_qty'], 100)
elif i == 43:
self.assertEqual(table_forecast['forecast_qty'], 90)
elif i == 52:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose suggestion to be based on Actual Demand, every period's suggestion is
# based on the present moves in that period, nothing in the past.
table_suggestion_wizard.based_on = 'actual_demand'
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 20)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# when choosing suggestion to be based on last 30 days, last 3 months or
# last 12 months, this takes into consideration the moves in last x days
# from today and takes a ratio out of it depending on the period (year, month, week or day).
# for example if last 30 day suggestion is 60 but my current period type is days,
# I sould suggest (1/30 of that amount so suggestion for each period(day) = 1/30 * 60)
table_suggestion_wizard.based_on = '30_days'
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 28)
table_suggestion_wizard.based_on = 'three_months'
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 17)
table_suggestion_wizard.based_on = 'one_year'
table_suggestion_wizard.with_context({'period_scale': 'week'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='week')[0]
for i in range(self.env.company.manufacturing_period_to_display_week):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 5)
@freeze_time("2024-02-26")
def test_suggestion_for_days_with_no_period(self):
"""
Make some moves in the past with different dates.
Moves with date more than 1 year ago shouldn't affect any suggestion.
Moves with date = today, are for Actual Demand testing (one is confirmed, one is not),
and both should reflect on Actual Demand suggestion.
"""
today = datetime.now().date()
self.table.uom_id.rounding = 1.0
date = today - relativedelta(years=1)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
date = today - relativedelta(years=1, days=2)
self._create_and_process_delivery_at_date(
[(self.table, 5)], date
)
date = today - relativedelta(days=20)
self._create_and_process_delivery_at_date(
[(self.table, 100)], date
)
date = today - relativedelta(days=60)
self._create_and_process_delivery_at_date(
[(self.table, 90)], date
)
date = today
self._create_and_process_delivery_at_date(
[(self.table, 10)], date
)
self._create_and_process_delivery_at_date(
[(self.table, 10)], date, to_validate=False
)
self.env.company.manufacturing_period_to_display_day = 365
# we choose suggestion to be based on 'last year', so every period's suggestion
# is taken from same period but previous year. (May 4, 2026 based on May 4, 2025 and so on)
table_suggestion_wizard = self.env['mrp.mps.forecast.suggestion'].create({
'mrp_mps_id': self.mps_table.id,
})
table_suggestion_wizard.based_on = 'last_year'
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 10)
elif i == 345:
self.assertEqual(table_forecast['forecast_qty'], 100)
elif i == 305:
self.assertEqual(table_forecast['forecast_qty'], 90)
elif i == 365:
self.assertEqual(table_forecast['forecast_qty'], 10)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# choose suggestion to be based on Actual Demand, every period's suggestion is
# based on the present moves in that period, nothing in the past.
table_suggestion_wizard.based_on = 'actual_demand'
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
if i == 0:
self.assertEqual(table_forecast['forecast_qty'], 20)
else:
self.assertEqual(table_forecast['forecast_qty'], 0)
# when choosing suggestion to be based on last 30 days, last 3 months or
# last 12 months, this takes into consideration the moves in last x days
# from today and takes a ratio out of it depending on the period (year, month, week or day).
# for example if last 30 day suggestion is 60 but my current period type is days,
# I sould suggest (1/30 of that amount so suggestion for each period(day) = 1/30 * 60)
table_suggestion_wizard.based_on = '30_days'
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 4)
table_suggestion_wizard.based_on = 'three_months'
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 3)
table_suggestion_wizard.based_on = 'one_year'
table_suggestion_wizard.with_context({'period_scale': 'day'}).apply_forecast_quantity_suggestion()
mps_table = self.mps_table.get_production_schedule_view_state(period_scale='day')[0]
for i in range(self.env.company.manufacturing_period_to_display_day):
table_forecast = mps_table['forecast_ids'][i]
self.assertEqual(table_forecast['forecast_qty'], 1)
def test_mps_replenish_rfq_merging(self):
"""
Test that POs created through subsequent MPS replenishments (for the
same day) get correctly merged.
"""
self.env.user.tz = 'Europe/Brussels'
buy_route = self.warehouse.route_ids.filtered(lambda r: any(rule.action == 'buy' for rule in r.rule_ids))
product = self.env['product.product'].create({'name': 'A super product'})
vendor = self.env['res.partner'].create({'name': 'A super vendor'})
self.env['product.supplierinfo'].create({
'product_id': product.id,
'partner_id': vendor.id,
})
mps = self.env['mrp.production.schedule'].with_context({'period_scale': 'month'}).create({
'product_id': product.id,
'warehouse_id': self.warehouse.id,
'route_id': buy_route.id,
})
mps.set_forecast_qty(date_index=0, quantity=10)
mps.action_replenish(based_on_lead_time=True)
pos = self.env['purchase.order'].search([('product_id', '=', product.id)])
self.assertEqual(pos.order_line.product_qty, 10)
mps.set_forecast_qty(date_index=0, quantity=30)
mps.action_replenish(based_on_lead_time=True)
pos = self.env['purchase.order'].search([('product_id', '=', product.id)])
self.assertEqual(len(pos), 1)
self.assertEqual(pos.order_line.product_qty, 30)
mps.set_forecast_qty(date_index=0, quantity=70)
mps.action_replenish(based_on_lead_time=True)
pos = self.env['purchase.order'].search([('product_id', '=', product.id)])
self.assertEqual(len(pos), 1)
self.assertEqual(pos.order_line.product_qty, 70)
def test_indirect_multiple_boms(self):
""" This test ensure that having multiples BoMs for a product does not negatively impact the MPS.
MPS should select in priority the BoM configured on the schedule, not the BoM returned by `_bom_find`
"""
# Create final product & component
fns, cmp = self.env['product.product'].create([{
'name': n,
'is_storable': True,
} for n in ('fns', 'cmp')])
# Create the BoMs
# This V1 BoM is not used in mps, but will be the one returned by `_bom_find`
self.env['mrp.bom'].create({
'code': 'V1',
'product_tmpl_id': fns.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [],
})
fns_bom_2 = self.env['mrp.bom'].create({
'code': 'V2',
'product_tmpl_id': fns.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [
Command.create({'product_id': cmp.id, 'product_qty': 1}),
],
})
# Create the MPS records, set the safety stock and the minimum replenish qty
mps_fns = self.env['mrp.production.schedule'].create({
'product_id': fns.id,
'warehouse_id': self.warehouse.id,
'bom_id': fns_bom_2.id,
})
mps_cmp = self.env['mrp.production.schedule'].search([('product_id', '=', cmp.id)])
self.assertTrue(bool(mps_cmp))
mps_fns.set_forecast_qty(0, 10)
# Ensure that the indirect demand for the component was computed (using fns_bom_2)
forecast_fns, forecast_cmp = (mps_fns | mps_cmp).get_production_schedule_view_state()
self.assertEqual(forecast_fns['forecast_ids'][0]['forecast_qty'], 10)
self.assertEqual(forecast_cmp['forecast_ids'][0]['indirect_demand_qty'], 10)