diff --git a/mrp_multi_level_estimate/__manifest__.py b/mrp_multi_level_estimate/__manifest__.py index b8bbfe011..02d5bd6c4 100644 --- a/mrp_multi_level_estimate/__manifest__.py +++ b/mrp_multi_level_estimate/__manifest__.py @@ -12,7 +12,7 @@ "website": "https://github.com/OCA/manufacture", "category": "Manufacturing", "depends": ["mrp_multi_level", "stock_demand_estimate_matrix"], - "data": ["views/product_mrp_area_views.xml"], + "data": ["views/product_mrp_area_views.xml", "views/mrp_area_views.xml"], "installable": True, "application": False, "auto_install": True, diff --git a/mrp_multi_level_estimate/models/__init__.py b/mrp_multi_level_estimate/models/__init__.py index 3bf74fdf9..d9e76cf8c 100644 --- a/mrp_multi_level_estimate/models/__init__.py +++ b/mrp_multi_level_estimate/models/__init__.py @@ -1 +1,2 @@ from . import product_mrp_area +from . import mrp_area diff --git a/mrp_multi_level_estimate/models/mrp_area.py b/mrp_multi_level_estimate/models/mrp_area.py new file mode 100644 index 000000000..3f53a5ca8 --- /dev/null +++ b/mrp_multi_level_estimate/models/mrp_area.py @@ -0,0 +1,36 @@ +# Copyright 2022 ForgeFlow S.L. (http://www.forgeflow.com) +# - Lois Rilo Antelo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class MRPArea(models.Model): + _inherit = "mrp.area" + + estimate_demand_and_other_sources_strat = fields.Selection( + string="Demand Estimates and Other Demand Sources Strategy", + selection=[ + ("all", "Always consider all sources"), + ( + "ignore_others_if_estimates", + "Ignore other sources for products with estimates", + ), + ( + "ignore_overlapping", + "Ignore other sources during periods with estimates", + ), + ], + default="all", + help="Define the strategy to follow in MRP multi level when there is a" + "coexistence of demand from demand estimates and other sources.\n" + "* Always consider all sources: nothing is excluded or ignored.\n" + "* Ignore other sources for products with estimates: When there " + "are estimates entered for product and they are in a present or " + "future period, all other sources of demand are ignored for those " + "products.\n" + "* Ignore other sources during periods with estimates: When " + "you create demand estimates for a period and product, " + "other sources of demand will be ignored during that period " + "for those products.", + ) diff --git a/mrp_multi_level_estimate/models/product_mrp_area.py b/mrp_multi_level_estimate/models/product_mrp_area.py index 88ceee090..3202a306f 100644 --- a/mrp_multi_level_estimate/models/product_mrp_area.py +++ b/mrp_multi_level_estimate/models/product_mrp_area.py @@ -14,7 +14,7 @@ class ProductMRPArea(models.Model): help="The days to group your estimates as demand for the MRP." "It can be different from the length of the date ranges you " "use in the estimates but it should not be greater, in that case" - "only grouping until the total lenght of the date range will be done.", + "only grouping until the total length of the date range will be done.", ) _sql_constraints = [ diff --git a/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py b/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py index 5ba32ae6c..e529e13e2 100644 --- a/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py +++ b/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py @@ -55,6 +55,8 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): } ) generator.action_apply() + cls.date_within_ranges = today - timedelta(days=2) + cls.date_without_ranges = today + timedelta(days=150) # Create Demand Estimates: ranges = cls.env["date.range"].search([("type_id", "=", cls.dr_type.id)]) @@ -152,7 +154,9 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): def test_03_group_demand_estimates(self): """Test demand grouping functionality, `group_estimate_days`.""" self.test_mrp_parameter.group_estimate_days = 7 - self.mrp_multi_level_wiz.create({}).run_mrp_multi_level() + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() estimates = self.estimate_obj.search( [ ("product_id", "=", self.prod_test.id), @@ -177,7 +181,9 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): # Test group_estimate_days greater than date range, it should not # generate greater demand. self.test_mrp_parameter.group_estimate_days = 10 - self.mrp_multi_level_wiz.create({}).run_mrp_multi_level() + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() moves = self.mrp_move_obj.search( [ ("product_id", "=", self.prod_test.id), @@ -192,7 +198,9 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): # Test group_estimate_days smaller than date range, it should not # generate greater demand. self.test_mrp_parameter.group_estimate_days = 5 - self.mrp_multi_level_wiz.create({}).run_mrp_multi_level() + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() moves = self.mrp_move_obj.search( [ ("product_id", "=", self.prod_test.id), @@ -229,7 +237,9 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): estimate.product_uom_qty = qty qty += 100 - self.mrp_multi_level_wiz.create({}).run_mrp_multi_level() + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() moves = self.mrp_move_obj.search( [ ("product_id", "=", self.prod_test.id), @@ -248,3 +258,86 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): self.assertEqual(moves_from_estimates[1].mrp_qty, -500) # 600 weekly -> 85.71 daily -> 85.71 * 7 = 600 self.assertEqual(moves_from_estimates[2].mrp_qty, -600) + + def test_05_estimate_and_other_sources_strat(self): + """Tests demand estimates and other sources strategies.""" + estimates = self.estimate_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.estimate_loc.id), + ] + ) + self.assertEqual(len(estimates), 3) + self._create_picking_out( + self.prod_test, 25, self.date_within_ranges, location=self.estimate_loc + ) + self._create_picking_out( + self.prod_test, 25, self.date_without_ranges, location=self.estimate_loc + ) + # 1. "all" + self.estimate_area.estimate_demand_and_other_sources_strat = "all" + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.estimate_area.id), + ] + ) + # 3 weeks - 3 days in the past = 18 days of valid estimates: + demand_from_estimates = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin == "fc" + ) + demand_from_other_sources = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin != "fc" + ) + self.assertEqual(len(demand_from_estimates), 18) + self.assertEqual(len(demand_from_other_sources), 2) + + # 2. "ignore_others_if_estimates" + self.estimate_area.estimate_demand_and_other_sources_strat = ( + "ignore_others_if_estimates" + ) + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.estimate_area.id), + ] + ) + demand_from_estimates = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin == "fc" + ) + demand_from_other_sources = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin != "fc" + ) + self.assertEqual(len(demand_from_estimates), 18) + self.assertEqual(len(demand_from_other_sources), 0) + + # 3. "ignore_overlapping" + self.estimate_area.estimate_demand_and_other_sources_strat = ( + "ignore_overlapping" + ) + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.estimate_area.id), + ] + ) + demand_from_estimates = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin == "fc" + ) + demand_from_other_sources = moves.filtered( + lambda m: m.mrp_type == "d" and m.mrp_origin != "fc" + ) + self.assertEqual(len(demand_from_estimates), 18) + self.assertEqual(len(demand_from_other_sources), 1) + self.assertEqual( + demand_from_other_sources.mrp_date, self.date_without_ranges.date() + ) diff --git a/mrp_multi_level_estimate/views/mrp_area_views.xml b/mrp_multi_level_estimate/views/mrp_area_views.xml new file mode 100644 index 000000000..1d767c175 --- /dev/null +++ b/mrp_multi_level_estimate/views/mrp_area_views.xml @@ -0,0 +1,13 @@ + + + + mrp.area.form - mrp_multi_level_estimate + mrp.area + + + + + + + + diff --git a/mrp_multi_level_estimate/wizards/mrp_multi_level.py b/mrp_multi_level_estimate/wizards/mrp_multi_level.py index bb7db1e68..300bef39f 100644 --- a/mrp_multi_level_estimate/wizards/mrp_multi_level.py +++ b/mrp_multi_level_estimate/wizards/mrp_multi_level.py @@ -88,3 +88,42 @@ class MultiLevelMrp(models.TransientModel): self.env["mrp.move"].create(mrp_move_data) mrp_date += delta return res + + def _exclude_considering_estimate_demand_and_other_sources_strat( + self, product_mrp_area, mrp_date + ): + estimate_strat = ( + product_mrp_area.mrp_area_id.estimate_demand_and_other_sources_strat + ) + if estimate_strat == "all": + return False + + domain = self._estimates_domain(product_mrp_area) + estimates = self.env["stock.demand.estimate"].search(domain) + if not estimates: + return False + + if estimate_strat == "ignore_others_if_estimates": + # Ignore + return True + if estimate_strat == "ignore_overlapping": + for estimate in estimates: + if mrp_date >= estimate.date_from and mrp_date <= estimate.date_to: + # Ignore + return True + return False + + @api.model + def _prepare_mrp_move_data_from_stock_move( + self, product_mrp_area, move, direction="in" + ): + res = super()._prepare_mrp_move_data_from_stock_move( + product_mrp_area, move, direction=direction + ) + if direction == "out": + mrp_date = res.get("mrp_date") + if self._exclude_considering_estimate_demand_and_other_sources_strat( + product_mrp_area, mrp_date + ): + return False + return res