From 11d298b10982c0b96b41986566ccb2d35ce3ed98 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Mon, 19 Dec 2022 13:27:35 +0100 Subject: [PATCH 1/3] [IMP] mrp_multi_level_estimate: Estimates and other demand sources strategy Introduces a new concept to define the strategy to apply when estimates coexist with other sources of demand. --- mrp_multi_level_estimate/__manifest__.py | 2 +- mrp_multi_level_estimate/models/__init__.py | 1 + mrp_multi_level_estimate/models/mrp_area.py | 36 +++++++ .../models/product_mrp_area.py | 2 +- .../tests/test_mrp_multi_level_estimate.py | 101 +++++++++++++++++- .../views/mrp_area_views.xml | 13 +++ .../wizards/mrp_multi_level.py | 39 +++++++ 7 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 mrp_multi_level_estimate/models/mrp_area.py create mode 100644 mrp_multi_level_estimate/views/mrp_area_views.xml diff --git a/mrp_multi_level_estimate/__manifest__.py b/mrp_multi_level_estimate/__manifest__.py index 68d60e315..c9b7c0958 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 916cb7463..d2322b851 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 From fb33482eebcaf0a54136f1af5a3d77028987a984 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Mon, 19 Dec 2022 16:10:02 +0100 Subject: [PATCH 2/3] [REW] mrp_multi_level_estimate: remove unneeded dependency Remove unneeded dependency to date_range and relicense to LGPL. --- mrp_multi_level_estimate/README.rst | 10 ++--- mrp_multi_level_estimate/__manifest__.py | 6 +-- mrp_multi_level_estimate/models/mrp_area.py | 2 +- .../models/product_mrp_area.py | 2 +- .../static/description/index.html | 2 +- .../tests/test_mrp_multi_level_estimate.py | 42 ++++++++----------- .../wizards/mrp_multi_level.py | 2 +- 7 files changed, 29 insertions(+), 37 deletions(-) diff --git a/mrp_multi_level_estimate/README.rst b/mrp_multi_level_estimate/README.rst index be7837e5d..ddd6fdd6e 100644 --- a/mrp_multi_level_estimate/README.rst +++ b/mrp_multi_level_estimate/README.rst @@ -10,9 +10,9 @@ MRP Multi Level Estimate .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github :target: https://github.com/OCA/manufacture/tree/13.0/mrp_multi_level_estimate :alt: OCA/manufacture @@ -23,7 +23,7 @@ MRP Multi Level Estimate :target: https://runbot.odoo-community.org/runbot/129/13.0 :alt: Try me on Runbot -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| Integration for MRP Multi Level and `Stock Demand Estimates `_ system. @@ -84,7 +84,7 @@ promote its widespread use. Current `maintainer `__: -|maintainer-LoisRForgeFlow| +|maintainer-LoisRForgeFlow| This module is part of the `OCA/manufacture `_ project on GitHub. diff --git a/mrp_multi_level_estimate/__manifest__.py b/mrp_multi_level_estimate/__manifest__.py index c9b7c0958..70d090a28 100644 --- a/mrp_multi_level_estimate/__manifest__.py +++ b/mrp_multi_level_estimate/__manifest__.py @@ -1,17 +1,17 @@ # Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/Agpl.html). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). { "name": "MRP Multi Level Estimate", "version": "13.0.1.0.2", "development_status": "Beta", - "license": "AGPL-3", + "license": "LGPL-3", "author": "ForgeFlow, Odoo Community Association (OCA)", "maintainers": ["LoisRForgeFlow"], "summary": "Allows to consider demand estimates using MRP multi level.", "website": "https://github.com/OCA/manufacture", "category": "Manufacturing", - "depends": ["mrp_multi_level", "stock_demand_estimate_matrix"], + "depends": ["mrp_multi_level", "stock_demand_estimate"], "data": ["views/product_mrp_area_views.xml", "views/mrp_area_views.xml"], "installable": True, "application": False, diff --git a/mrp_multi_level_estimate/models/mrp_area.py b/mrp_multi_level_estimate/models/mrp_area.py index 3f53a5ca8..7f920ea8a 100644 --- a/mrp_multi_level_estimate/models/mrp_area.py +++ b/mrp_multi_level_estimate/models/mrp_area.py @@ -1,6 +1,6 @@ # 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). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). from odoo import fields, models diff --git a/mrp_multi_level_estimate/models/product_mrp_area.py b/mrp_multi_level_estimate/models/product_mrp_area.py index 3202a306f..bb36a7e4d 100644 --- a/mrp_multi_level_estimate/models/product_mrp_area.py +++ b/mrp_multi_level_estimate/models/product_mrp_area.py @@ -1,6 +1,6 @@ # Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com) # - Lois Rilo Antelo -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). from odoo import fields, models diff --git a/mrp_multi_level_estimate/static/description/index.html b/mrp_multi_level_estimate/static/description/index.html index 92a82ae50..b9e98f81c 100644 --- a/mrp_multi_level_estimate/static/description/index.html +++ b/mrp_multi_level_estimate/static/description/index.html @@ -367,7 +367,7 @@ ul.auto-toc { !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/manufacture Translate me on Weblate Try me on Runbot

+

Beta License: LGPL-3 OCA/manufacture Translate me on Weblate Try me on Runbot

Integration for MRP Multi Level and Stock Demand Estimates system.

Table of contents

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 e529e13e2..a06126550 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 @@ -1,10 +1,8 @@ -# Copyright 2018-20 ForgeFlow S.L. (http://www.forgeflow.com) +# Copyright 2018-22 ForgeFlow S.L. (http://www.forgeflow.com) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). from datetime import datetime, timedelta -from dateutil.rrule import WEEKLY - from odoo.addons.mrp_multi_level.tests.common import TestMrpMultiLevelCommon @@ -39,44 +37,38 @@ class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): } ) - # Create Date Ranges: - cls.dr_type = cls.env["date.range.type"].create( - {"name": "Weeks", "company_id": False, "allow_overlap": False} - ) + # Create 3 consecutive estimates of 1 week length each. today = datetime.today().replace(hour=0) - generator = cls.env["date.range.generator"].create( - { - "date_start": today - timedelta(days=3), - "name_prefix": "W-", - "type_id": cls.dr_type.id, - "duration_count": 1, - "unit_of_time": str(WEEKLY), - "count": 3, - } - ) - generator.action_apply() + date_start_1 = today - timedelta(days=3) + date_end_1 = date_start_1 + timedelta(days=6) + date_start_2 = date_end_1 + timedelta(days=1) + date_end_2 = date_start_2 + timedelta(days=6) + date_start_3 = date_end_2 + timedelta(days=1) + date_end_3 = date_start_3 + timedelta(days=6) + start_dates = [date_start_1, date_start_2, date_start_3] + end_dates = [date_end_1, date_end_2, date_end_3] + 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)]) qty = 140.0 - for dr in ranges: + for sd, ed in zip(start_dates, end_dates): qty += 70.0 - cls._create_demand_estimate(cls.prod_test, cls.stock_location, dr, qty) - cls._create_demand_estimate(cls.prod_test, cls.estimate_loc, dr, qty) + cls._create_demand_estimate(cls.prod_test, cls.stock_location, sd, ed, qty) + cls._create_demand_estimate(cls.prod_test, cls.estimate_loc, sd, ed, qty) cls.mrp_multi_level_wiz.create({}).run_mrp_multi_level() @classmethod - def _create_demand_estimate(cls, product, location, date_range, qty): + def _create_demand_estimate(cls, product, location, date_from, date_to, qty): cls.estimate_obj.create( { "product_id": product.id, "location_id": location.id, "product_uom": product.uom_id.id, "product_uom_qty": qty, - "date_range_id": date_range.id, + "manual_date_from": date_from, + "manual_date_to": date_to, } ) diff --git a/mrp_multi_level_estimate/wizards/mrp_multi_level.py b/mrp_multi_level_estimate/wizards/mrp_multi_level.py index d2322b851..a57a2d3ed 100644 --- a/mrp_multi_level_estimate/wizards/mrp_multi_level.py +++ b/mrp_multi_level_estimate/wizards/mrp_multi_level.py @@ -1,6 +1,6 @@ # Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com) # - Lois Rilo -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). import logging from datetime import timedelta From 28aff9f5c285ccbb52ddc7440935d4f6449e1a47 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Tue, 3 Jan 2023 16:10:52 +0100 Subject: [PATCH 3/3] [IMP] mrp_multi_level: do not create moves if not data This improves extensibility, allowing to not create moves on certain situations by extending the prepare vals hook method. --- mrp_multi_level/wizards/mrp_multi_level.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index e37910b03..b7477e6c0 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -413,13 +413,15 @@ class MultiLevelMrp(models.TransientModel): move_data = self._prepare_mrp_move_data_from_stock_move( product_mrp_area, move, direction="in" ) - mrp_move_obj.create(move_data) + if move_data: + mrp_move_obj.create(move_data) if out_moves: for move in out_moves: move_data = self._prepare_mrp_move_data_from_stock_move( product_mrp_area, move, direction="out" ) - mrp_move_obj.create(move_data) + if move_data: + mrp_move_obj.create(move_data) return True @api.model