diff --git a/mrp_multi_level_estimate/README.rst b/mrp_multi_level_estimate/README.rst new file mode 100644 index 000000000..a5d4447f2 --- /dev/null +++ b/mrp_multi_level_estimate/README.rst @@ -0,0 +1,91 @@ +======================== +MRP Multi Level Estimate +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |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/15.0/mrp_multi_level_estimate + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-15-0/manufacture-15-0-mrp_multi_level_estimate + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/129/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Integration for MRP Multi Level and `Stock Demand Estimates `_ system. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +You can edit how to consolidate your estimates as demand at product MRP area +level using the field *Group Days of Estimates*. This number represents the +days to group your estimates as demand for the MRP, e.g: if set to 7, you will +have your estimates (regardless of the date range used) grouped in weekly +demand. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Lois Rilo +* Pimolnat Suntian + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px + :target: https://github.com/LoisRForgeFlow + :alt: LoisRForgeFlow + +Current `maintainer `__: + +|maintainer-LoisRForgeFlow| + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_multi_level_estimate/__init__.py b/mrp_multi_level_estimate/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/mrp_multi_level_estimate/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/mrp_multi_level_estimate/__manifest__.py b/mrp_multi_level_estimate/__manifest__.py new file mode 100644 index 000000000..b6dff80a8 --- /dev/null +++ b/mrp_multi_level_estimate/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2019-23 ForgeFlow S.L. (http://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "MRP Multi Level Estimate", + "version": "16.0.1.1.0", + "development_status": "Production/Stable", + "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"], + "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/i18n/it.po b/mrp_multi_level_estimate/i18n/it.po new file mode 100644 index 000000000..6279825fd --- /dev/null +++ b/mrp_multi_level_estimate/i18n/it.po @@ -0,0 +1,109 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_multi_level_estimate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-01-13 18:44+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__all +msgid "Always consider all sources" +msgstr "Considera sempre tutte le fonti" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,help:mrp_multi_level_estimate.field_mrp_area__estimate_demand_and_other_sources_strat +msgid "" +"Define the strategy to follow in MRP multi level when there is acoexistence " +"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." +msgstr "" +"Definisce la strategia da seguire con l'MRP multi livello quando c'è la " +"coesistenza di domanda dalle stime e altre fonti.\n" +"* Considera sempre tutte le fonti: niente è escluso o ignorato.\n" +"* Ignora altre fonti quando ci sono stime: quando ci sono stime per il " +"prodotto e sono per periodi presenti o futuri, tutte le altre fonti di " +"richieste vengono ignorate per il prodotto.\n" +"* Ignora altre fonti nel periodo delle stime: quando si crea una richiesta " +"da stime per un periodo e prodotto, altre fonti di richiesta verranno " +"ignorate per il periodo di quel prodotto." + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,field_description:mrp_multi_level_estimate.field_mrp_area__estimate_demand_and_other_sources_strat +msgid "Demand Estimates and Other Demand Sources Strategy" +msgstr "Strategia per la domanda stimata e altre fonti di domanda" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,field_description:mrp_multi_level_estimate.field_product_mrp_area__group_estimate_days +msgid "Group Days of Estimates" +msgstr "Gruppo giorni delle stime" + +#. module: mrp_multi_level_estimate +#: model:ir.model.constraint,message:mrp_multi_level_estimate.constraint_product_mrp_area_group_estimate_days_check +msgid "Group Days of Estimates must be greater than or equal to zero." +msgstr "Il gruppo dei gironi delle stime deve essere maggiore o uguale a zero." + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__ignore_overlapping +msgid "Ignore other sources during periods with estimates" +msgstr "Ignora altre fonti nei periodi con stime" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__ignore_others_if_estimates +msgid "Ignore other sources for products with estimates" +msgstr "Ignora altre fonti per prodotti con stime" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_mrp_area +msgid "MRP Area" +msgstr "Area MRP" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_mrp_multi_level +msgid "Multi Level MRP" +msgstr "MRP multi livello" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_product_mrp_area +msgid "Product MRP Area" +msgstr "Area MRP prodotto" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,help:mrp_multi_level_estimate.field_product_mrp_area__group_estimate_days +msgid "" +"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 caseonly grouping until the total length of the date " +"range will be done." +msgstr "" +"I giorni per raggruppare le stime come domanda per l'MRP. Può essere diverso " +"dalla lunghezza del periodo delle date utilizzate nelle stime ma non " +"dovrebbe essere maggiore, in tal caso verrà eseguito solo il raggruppamento " +"fino alla lunghezza totale dell'intervallo di date." + +#~ msgid "" +#~ "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 caseonly grouping until the total " +#~ "lenght of the date range will be done." +#~ msgstr "" +#~ "I giorni per raggruppare le stime come richiesta per l'MRP. Può differire " +#~ "dall'ampiezza dell'intervallo di date utilizzato per la stima ma non " +#~ "dovrebbe essere maggiore, solo in quel caso verrà eseguito il " +#~ "raggruppamento fino all'ampiezza totale dell'intervallo di date." diff --git a/mrp_multi_level_estimate/i18n/mrp_multi_level_estimate.pot b/mrp_multi_level_estimate/i18n/mrp_multi_level_estimate.pot new file mode 100644 index 000000000..806447e13 --- /dev/null +++ b/mrp_multi_level_estimate/i18n/mrp_multi_level_estimate.pot @@ -0,0 +1,77 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_multi_level_estimate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__all +msgid "Always consider all sources" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,help:mrp_multi_level_estimate.field_mrp_area__estimate_demand_and_other_sources_strat +msgid "" +"Define the strategy to follow in MRP multi level when there is acoexistence 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." +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,field_description:mrp_multi_level_estimate.field_mrp_area__estimate_demand_and_other_sources_strat +msgid "Demand Estimates and Other Demand Sources Strategy" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,field_description:mrp_multi_level_estimate.field_product_mrp_area__group_estimate_days +msgid "Group Days of Estimates" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.constraint,message:mrp_multi_level_estimate.constraint_product_mrp_area_group_estimate_days_check +msgid "Group Days of Estimates must be greater than or equal to zero." +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__ignore_overlapping +msgid "Ignore other sources during periods with estimates" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields.selection,name:mrp_multi_level_estimate.selection__mrp_area__estimate_demand_and_other_sources_strat__ignore_others_if_estimates +msgid "Ignore other sources for products with estimates" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_mrp_area +msgid "MRP Area" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_mrp_multi_level +msgid "Multi Level MRP" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model,name:mrp_multi_level_estimate.model_product_mrp_area +msgid "Product MRP Area" +msgstr "" + +#. module: mrp_multi_level_estimate +#: model:ir.model.fields,help:mrp_multi_level_estimate.field_product_mrp_area__group_estimate_days +msgid "" +"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 caseonly grouping until the total length of the date" +" range will be done." +msgstr "" diff --git a/mrp_multi_level_estimate/models/__init__.py b/mrp_multi_level_estimate/models/__init__.py new file mode 100644 index 000000000..d9e76cf8c --- /dev/null +++ b/mrp_multi_level_estimate/models/__init__.py @@ -0,0 +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..7f920ea8a --- /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 LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.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 new file mode 100644 index 000000000..bb36a7e4d --- /dev/null +++ b/mrp_multi_level_estimate/models/product_mrp_area.py @@ -0,0 +1,26 @@ +# Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com) +# - Lois Rilo Antelo +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models + + +class ProductMRPArea(models.Model): + _inherit = "product.mrp.area" + + group_estimate_days = fields.Integer( + string="Group Days of Estimates", + default=1, + 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 length of the date range will be done.", + ) + + _sql_constraints = [ + ( + "group_estimate_days_check", + "CHECK( group_estimate_days >= 0 )", + "Group Days of Estimates must be greater than or equal to zero.", + ), + ] diff --git a/mrp_multi_level_estimate/readme/CONFIGURE.rst b/mrp_multi_level_estimate/readme/CONFIGURE.rst new file mode 100644 index 000000000..1b720c2e6 --- /dev/null +++ b/mrp_multi_level_estimate/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +You can edit how to consolidate your estimates as demand at product MRP area +level using the field *Group Days of Estimates*. This number represents the +days to group your estimates as demand for the MRP, e.g: if set to 7, you will +have your estimates (regardless of the date range used) grouped in weekly +demand. diff --git a/mrp_multi_level_estimate/readme/CONTRIBUTORS.rst b/mrp_multi_level_estimate/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..0e3a0d76e --- /dev/null +++ b/mrp_multi_level_estimate/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Lois Rilo +* Pimolnat Suntian diff --git a/mrp_multi_level_estimate/readme/DESCRIPTION.rst b/mrp_multi_level_estimate/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e7f56f84a --- /dev/null +++ b/mrp_multi_level_estimate/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Integration for MRP Multi Level and `Stock Demand Estimates `_ system. diff --git a/mrp_multi_level_estimate/static/description/icon.png b/mrp_multi_level_estimate/static/description/icon.png new file mode 100644 index 000000000..b1cef6c4c Binary files /dev/null and b/mrp_multi_level_estimate/static/description/icon.png differ diff --git a/mrp_multi_level_estimate/static/description/index.html b/mrp_multi_level_estimate/static/description/index.html new file mode 100644 index 000000000..9131b0323 --- /dev/null +++ b/mrp_multi_level_estimate/static/description/index.html @@ -0,0 +1,431 @@ + + + + + + +MRP Multi Level Estimate + + + +
+

MRP Multi Level Estimate

+ + +

Production/Stable 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

+ +
+

Configuration

+

You can edit how to consolidate your estimates as demand at product MRP area +level using the field Group Days of Estimates. This number represents the +days to group your estimates as demand for the MRP, e.g: if set to 7, you will +have your estimates (regardless of the date range used) grouped in weekly +demand.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

LoisRForgeFlow

+

This module is part of the OCA/manufacture project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mrp_multi_level_estimate/tests/__init__.py b/mrp_multi_level_estimate/tests/__init__.py new file mode 100644 index 000000000..f26efbd60 --- /dev/null +++ b/mrp_multi_level_estimate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_multi_level_estimate 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 new file mode 100644 index 000000000..a06126550 --- /dev/null +++ b/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py @@ -0,0 +1,335 @@ +# 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 odoo.addons.mrp_multi_level.tests.common import TestMrpMultiLevelCommon + + +class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.estimate_obj = cls.env["stock.demand.estimate"] + + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + + # Create new clean area: + cls.estimate_loc = cls.loc_obj.create( + { + "name": "Test location for estimates", + "usage": "internal", + "location_id": cls.wh.view_location_id.id, + } + ) + cls.estimate_area = cls.mrp_area_obj.create( + { + "name": "Test", + "warehouse_id": cls.wh.id, + "location_id": cls.estimate_loc.id, + } + ) + cls.test_mrp_parameter = cls.product_mrp_area_obj.create( + { + "product_id": cls.prod_test.id, + "mrp_area_id": cls.estimate_area.id, + "mrp_nbr_days": 7, + } + ) + + # Create 3 consecutive estimates of 1 week length each. + today = datetime.today().replace(hour=0) + 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) + + qty = 140.0 + for sd, ed in zip(start_dates, end_dates): + qty += 70.0 + 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_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, + "manual_date_from": date_from, + "manual_date_to": date_to, + } + ) + + def test_01_demand_estimates(self): + """Tests demand estimates integration.""" + estimates = self.estimate_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.stock_location.id), + ] + ) + self.assertEqual(len(estimates), 3) + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.mrp_area.id), + ] + ) + # 3 weeks - 3 days in the past = 18 days of valid estimates: + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 18) + quantities = moves_from_estimates.mapped("mrp_qty") + self.assertIn(-30.0, quantities) # 210 a week => 30.0 dayly: + self.assertIn(-40.0, quantities) # 280 a week => 40.0 dayly: + self.assertIn(-50.0, quantities) # 350 a week => 50.0 dayly: + plans = self.planned_order_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.mrp_area.id), + ] + ) + action = list(set(plans.mapped("mrp_action"))) + self.assertEqual(len(action), 1) + self.assertEqual(action[0], "buy") + self.assertEqual(len(plans), 18) + inventories = self.mrp_inventory_obj.search( + [("mrp_area_id", "=", self.estimate_area.id)] + ) + self.assertEqual(len(inventories), 18) + + def test_02_demand_estimates_group_plans(self): + """Test requirement grouping functionality, `nbr_days`.""" + estimates = self.estimate_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.estimate_loc.id), + ] + ) + self.assertEqual(len(estimates), 3) + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.estimate_area.id), + ] + ) + supply_plans = self.planned_order_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: + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 18) + # 18 days of demand / 7 nbr_days = 2.57 => 3 supply moves expected. + self.assertEqual(len(supply_plans), 3) + quantities = supply_plans.mapped("mrp_qty") + week_1_expected = sum(moves_from_estimates[0:7].mapped("mrp_qty")) + self.assertIn(abs(week_1_expected), quantities) + week_2_expected = sum(moves_from_estimates[7:14].mapped("mrp_qty")) + self.assertIn(abs(week_2_expected), quantities) + week_3_expected = sum(moves_from_estimates[14:].mapped("mrp_qty")) + self.assertIn(abs(week_3_expected), quantities) + + 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( + {"mrp_area_ids": [(6, 0, self.estimate_area.ids)]} + ).run_mrp_multi_level() + estimates = self.estimate_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.estimate_loc.id), + ] + ) + self.assertEqual(len(estimates), 3) + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.estimate_area.id), + ] + ) + # 3 weekly estimates, demand from estimates grouped in batches of 7 + # days = 3 days of estimates mrp moves: + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 3) + # 210 weekly -> 30 daily -> 30 * 4 days not consumed = 120 + self.assertEqual(moves_from_estimates[0].mrp_qty, -120) + self.assertEqual(moves_from_estimates[1].mrp_qty, -280) + self.assertEqual(moves_from_estimates[2].mrp_qty, -350) + # 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( + {"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), + ] + ) + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 3) + self.assertEqual(moves_from_estimates[0].mrp_qty, -120) + self.assertEqual(moves_from_estimates[1].mrp_qty, -280) + self.assertEqual(moves_from_estimates[2].mrp_qty, -350) + # 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( + {"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), + ] + ) + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 5) + # Week 1 partially consumed, so only 4 remaining days considered. + # 30 daily x 4 days = 120 + self.assertEqual(moves_from_estimates[0].mrp_qty, -120) + # Week 2 divided in 2 (40 daily) -> 5 days = 200, 2 days = 80 + self.assertEqual(moves_from_estimates[1].mrp_qty, -200) + self.assertEqual(moves_from_estimates[2].mrp_qty, -80) + # Week 3 divided in 2, (50 daily) -> 5 days = 250, 2 days = 100 + self.assertEqual(moves_from_estimates[3].mrp_qty, -250) + self.assertEqual(moves_from_estimates[4].mrp_qty, -100) + + def test_04_group_demand_estimates_rounding(self): + """Test demand grouping functionality, `group_estimate_days` and rounding.""" + self.test_mrp_parameter.group_estimate_days = 7 + self.uom_unit.rounding = 1.00 + + estimates = self.estimate_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.estimate_loc.id), + ] + ) + self.assertEqual(len(estimates), 3) + # Change qty of estimates to quantities that divided by 7 days return a decimal result + qty = 400 + for estimate in estimates: + estimate.product_uom_qty = qty + qty += 100 + + 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 weekly estimates, demand from estimates grouped in batches of 7 + # days = 3 days of estimates mrp moves: + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d") + self.assertEqual(len(moves_from_estimates), 3) + # Rounding should be done at the end of the calculation, using the daily + # quantity already rounded can lead to errors. + # 400 weekly -> 57.41 daily -> 57.41 * 4 days not consumed = 228,57 = 229 + self.assertEqual(moves_from_estimates[0].mrp_qty, -229) + # 500 weekly -> 71.42 daily -> 71,42 * 7 = 500 + 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/views/product_mrp_area_views.xml b/mrp_multi_level_estimate/views/product_mrp_area_views.xml new file mode 100644 index 000000000..f614d34a3 --- /dev/null +++ b/mrp_multi_level_estimate/views/product_mrp_area_views.xml @@ -0,0 +1,14 @@ + + + + product.mrp.area.form - estimates + product.mrp.area + form + + + + + + + + diff --git a/mrp_multi_level_estimate/wizards/__init__.py b/mrp_multi_level_estimate/wizards/__init__.py new file mode 100644 index 000000000..869fb19c7 --- /dev/null +++ b/mrp_multi_level_estimate/wizards/__init__.py @@ -0,0 +1 @@ +from . import mrp_multi_level diff --git a/mrp_multi_level_estimate/wizards/mrp_multi_level.py b/mrp_multi_level_estimate/wizards/mrp_multi_level.py new file mode 100644 index 000000000..4065645f3 --- /dev/null +++ b/mrp_multi_level_estimate/wizards/mrp_multi_level.py @@ -0,0 +1,129 @@ +# Copyright 2019-22 ForgeFlow S.L. (http://www.forgeflow.com) +# - Lois Rilo +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +import logging +from datetime import timedelta + +from odoo import api, fields, models +from odoo.tools.float_utils import float_round + +logger = logging.getLogger(__name__) + + +class MultiLevelMrp(models.TransientModel): + _inherit = "mrp.multi.level" + + @api.model + def _prepare_mrp_move_data_from_estimate(self, estimate, product_mrp_area, date): + mrp_type = "d" + origin = "fc" + daily_qty_unrounded = estimate.daily_qty + daily_qty = float_round( + estimate.daily_qty, + precision_rounding=product_mrp_area.product_id.uom_id.rounding, + rounding_method="HALF-UP", + ) + days_consumed = 0 + if product_mrp_area.group_estimate_days > 1: + start = estimate.date_from + if start < date: + days_consumed = (date - start).days + group_estimate_days = min( + product_mrp_area.group_estimate_days, estimate.duration - days_consumed + ) + mrp_qty = float_round( + daily_qty_unrounded * group_estimate_days, + precision_rounding=product_mrp_area.product_id.uom_id.rounding, + rounding_method="HALF-UP", + ) + return { + "mrp_area_id": product_mrp_area.mrp_area_id.id, + "product_id": product_mrp_area.product_id.id, + "product_mrp_area_id": product_mrp_area.id, + "production_id": None, + "purchase_order_id": None, + "purchase_line_id": None, + "stock_move_id": None, + "mrp_qty": -mrp_qty, + "current_qty": -daily_qty, + "mrp_date": date, + "current_date": date, + "mrp_type": mrp_type, + "mrp_origin": origin, + "mrp_order_number": None, + "parent_product_id": None, + "name": "Forecast", + "state": "confirmed", + } + + @api.model + def _estimates_domain(self, product_mrp_area): + locations = product_mrp_area.mrp_area_id._get_locations() + return [ + ("product_id", "=", product_mrp_area.product_id.id), + ("location_id", "in", locations.ids), + ("date_to", ">=", fields.Date.today()), + ] + + @api.model + def _init_mrp_move_from_forecast(self, product_mrp_area): + res = super(MultiLevelMrp, self)._init_mrp_move_from_forecast(product_mrp_area) + if not product_mrp_area.group_estimate_days: + return False + today = fields.Date.today() + domain = self._estimates_domain(product_mrp_area) + estimates = self.env["stock.demand.estimate"].search(domain) + for rec in estimates: + start = rec.date_from + if start < today: + start = today + mrp_date = fields.Date.from_string(start) + date_end = fields.Date.from_string(rec.date_to) + delta = timedelta(days=product_mrp_area.group_estimate_days) + while mrp_date <= date_end: + mrp_move_data = self._prepare_mrp_move_data_from_estimate( + rec, product_mrp_area, mrp_date + ) + 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 diff --git a/setup/mrp_multi_level_estimate/odoo/addons/mrp_multi_level_estimate b/setup/mrp_multi_level_estimate/odoo/addons/mrp_multi_level_estimate new file mode 120000 index 000000000..02fe39d07 --- /dev/null +++ b/setup/mrp_multi_level_estimate/odoo/addons/mrp_multi_level_estimate @@ -0,0 +1 @@ +../../../../mrp_multi_level_estimate \ No newline at end of file diff --git a/setup/mrp_multi_level_estimate/setup.py b/setup/mrp_multi_level_estimate/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_multi_level_estimate/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)