diff --git a/.travis.yml b/.travis.yml index 132eef97d..7f676612b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,11 @@ addons: env: global: - - VERSION="11.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" - - TRANSIFEX_USER='transbot@odoo-community.org' - - secure: "jErjXdqkLMHIwGJIWJW0kJiL0MZsPutBHRkdZ0ERIC9q9Td6oExPcaiYuNMwEJsetoA51dnOuTk/pNCFvYZGectDXQgVk5D9vXYiHfwZZT56rTA5IrscrPr8LobFrV7i7JY1+wIK8lOsfEwZvmhV6NSg+vbI9UpP6sI5W8QSF3dfqh56NsaKII9v/q12S/QxkhQdIgqpg6le+yooFFNmLs9M1kqbsoGYOXnsNHx6wGuY/N5l16td3A2eO4ulCZLUu1Djc0PiwI6ZAJue50ZfMj3pW20X/+u9pSBUPBC2Jh77VXFln50A++0OZ27uYGsQJOC2abg7Zze6gdYz3qoIGq13F+wwqKkJ8rsfN9yI6Z41GnRQ6NOg+j/YWUJ6kWY4VDDHeYfLqX7AiGHLTsCcn/FV3WnoMSVj6Rm/nZoBZ3K9nrVaolCd5oFtddBXzvZHqdTKHRzzRyLLq9Lo+FBxoVjwQycHhoWlyFf39gu1cLGLKHaM9nSDjLTNXZTWtcS/kookNxb9QfYXGTEMPeXYlvOjQ6QaKeJ0Qrg1LQgsMb9cHUmGjdu525Nx1XJb5wkz0q21EbF88HlKzxtBKCxtJaGkk7Pf/cMKU54QEk++01oLNxuJ2C2qLChp1G2alrUxHaPIbX4LKcSSafrbeCsnwBeRJVgj6qqvMUWOCCLn07c=" + - VERSION="11.0" TESTS="0" LINT_CHECK="0" matrix: - LINT_CHECK="1" - - TRANSIFEX="1" - - TESTS="1" ODOO_REPO="odoo/odoo" + - TESTS="1" ODOO_REPO="odoo/odoo" MAKEPOT="1" - TESTS="1" ODOO_REPO="OCA/OCB" install: diff --git a/ddmrp/README.rst b/ddmrp/README.rst new file mode 100644 index 000000000..21cd7854d --- /dev/null +++ b/ddmrp/README.rst @@ -0,0 +1,21 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. diff --git a/ddmrp/__init__.py b/ddmrp/__init__.py new file mode 100644 index 000000000..8c4cde289 --- /dev/null +++ b/ddmrp/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizards +from . import report diff --git a/ddmrp/__manifest__.py b/ddmrp/__manifest__.py new file mode 100644 index 000000000..ee9d723c0 --- /dev/null +++ b/ddmrp/__manifest__.py @@ -0,0 +1,65 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "DDMRP", + "summary": "Demand Driven Material Requirements Planning", + "version": "11.0.1.0.0", + "development_status": "Beta", + "author": "Eficent, " + "Aleph Objects, Inc., " + "Odoo Community Association (OCA)", + "maintainers": ['jbeficent', 'lreficent'], + "website": "https://github.com/OCA/ddmrp", + "category": "Warehouse Management", + "depends": [ + "purchase", + "mrp_bom_location", + "web_tree_dynamic_colored_field", + "stock_warehouse_orderpoint_stock_info", + "stock_warehouse_orderpoint_stock_info_unreserved", + "stock_available_unreserved", + "stock_orderpoint_purchase_link", + "stock_orderpoint_uom", + "stock_orderpoint_manual_procurement", + "stock_demand_estimate", + "web_widget_bokeh_chart", + "mrp_multi_level", + "base_cron_exclusion", + "stock_warehouse_calendar", + ], + "data": [ + "data/product_adu_calculation_method_data.xml", + "data/stock_buffer_profile_variability_data.xml", + "data/stock_buffer_profile_lead_time_data.xml", + "data/stock.buffer.profile.csv", + "security/ir.model.access.csv", + "security/stock_security.xml", + "views/stock_buffer_profile_view.xml", + "views/stock_buffer_profile_variability_view.xml", + "views/stock_buffer_profile_lead_time_view.xml", + "views/product_adu_calculation_method_view.xml", + "views/stock_warehouse_views.xml", + "views/stock_warehouse_orderpoint_view.xml", + "views/mrp_production_view.xml", + "views/purchase_order_line_view.xml", + "views/mrp_bom_view.xml", + "views/stock_move_views.xml", + "views/report_mrpbomstructure.xml", + "wizards/ddmrp_run_view.xml", + "data/ir_cron.xml", + ], + "demo": [ + "demo/res_partner_demo.xml", + "demo/product_category_demo.xml", + "demo/product_product_demo.xml", + "demo/product_supplierinfo_demo.xml", + "demo/mrp_bom_demo.xml", + "demo/stock_warehouse_orderpoint_demo.xml", + ], + "license": "AGPL-3", + 'installable': True, + 'application': True, +} diff --git a/ddmrp/data/ir_cron.xml b/ddmrp/data/ir_cron.xml new file mode 100644 index 000000000..9500462ed --- /dev/null +++ b/ddmrp/data/ir_cron.xml @@ -0,0 +1,34 @@ + + + + + Reordering Rule DDMRP calculation + code + + 8 + hours + -1 + 6 + + + model.cron_ddmrp(True) + + + + DDMRP Buffer ADU calculation + code + + 1 + days + -1 + 3 + + + + model.cron_ddmrp_adu(True) + + + diff --git a/ddmrp/data/product_adu_calculation_method_data.xml b/ddmrp/data/product_adu_calculation_method_data.xml new file mode 100644 index 000000000..964afd98a --- /dev/null +++ b/ddmrp/data/product_adu_calculation_method_data.xml @@ -0,0 +1,24 @@ + + + + + Fixed + fixed + + + + Past (120 days) + past + 120 + + + Future (120 days) + future + True + 120 + + + diff --git a/ddmrp/data/stock.buffer.profile.csv b/ddmrp/data/stock.buffer.profile.csv new file mode 100644 index 000000000..7dc6ed655 --- /dev/null +++ b/ddmrp/data/stock.buffer.profile.csv @@ -0,0 +1,82 @@ +id,replenish_method,variability_id/id,item_type,lead_time_id/id +stock_buffer_profile_replenish_purchased_short_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_manufactured_short_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_distributed_short_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_purchased_short_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_manufactured_short_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_distributed_short_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_purchased_short_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_manufactured_short_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_distributed_short_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_purchased_medium_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_manufactured_medium_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_distributed_medium_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_purchased_medium_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_manufactured_medium_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_distributed_medium_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_purchased_medium_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_manufactured_medium_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_distributed_medium_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_purchased_long_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_manufactured_long_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_distributed_long_low,Replenished,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_purchased_long_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_manufactured_long_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_distributed_long_medium,Replenished,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_purchased_long_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_manufactured_long_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_distributed_long_high,Replenished,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_purchased_short_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_manufactured_short_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_distributed_short_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_purchased_short_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_manufactured_short_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_distributed_short_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_purchased_short_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_manufactured_short_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_distributed_short_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_min_max_purchased_medium_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_manufactured_medium_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_distributed_medium_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_purchased_medium_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_manufactured_medium_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_distributed_medium_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_purchased_medium_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_manufactured_medium_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_distributed_medium_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_min_max_purchased_long_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_manufactured_long_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_distributed_long_low,Min-max,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_purchased_long_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_manufactured_long_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_distributed_long_medium,Min-max,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_purchased_long_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_manufactured_long_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_min_max_distributed_long_high,Min-max,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_purchased_short_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_manufactured_short_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_distributed_short_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_purchased_short_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_manufactured_short_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_distributed_short_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_purchased_short_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_manufactured_short_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_distributed_short_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_short +stock_buffer_profile_replenish_override_purchased_medium_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_manufactured_medium_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_distributed_medium_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_purchased_medium_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_manufactured_medium_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_distributed_medium_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_purchased_medium_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_manufactured_medium_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_distributed_medium_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_medium +stock_buffer_profile_replenish_override_purchased_long_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_manufactured_long_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_distributed_long_low,Replenished Override,ddmrp.stock_buffer_profile_variability_low,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_purchased_long_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_manufactured_long_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_distributed_long_medium,Replenished Override,ddmrp.stock_buffer_profile_variability_medium,Distributed,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_purchased_long_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Purchased,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_manufactured_long_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Manufactured,ddmrp.stock_buffer_profile_lead_time_long +stock_buffer_profile_replenish_override_distributed_long_high,Replenished Override,ddmrp.stock_buffer_profile_variability_high,Distributed,ddmrp.stock_buffer_profile_lead_time_long diff --git a/ddmrp/data/stock_buffer_profile_lead_time_data.xml b/ddmrp/data/stock_buffer_profile_lead_time_data.xml new file mode 100644 index 000000000..4bbab8511 --- /dev/null +++ b/ddmrp/data/stock_buffer_profile_lead_time_data.xml @@ -0,0 +1,22 @@ + + + + + Short + 0.75 + + + + Medium + 0.5 + + + + Long + 0.25 + + + diff --git a/ddmrp/data/stock_buffer_profile_variability_data.xml b/ddmrp/data/stock_buffer_profile_variability_data.xml new file mode 100644 index 000000000..fea461ef2 --- /dev/null +++ b/ddmrp/data/stock_buffer_profile_variability_data.xml @@ -0,0 +1,22 @@ + + + + + Low + 0.25 + + + + Medium + 0.5 + + + + High + 0.75 + + + diff --git a/ddmrp/demo/mrp_bom_demo.xml b/ddmrp/demo/mrp_bom_demo.xml new file mode 100644 index 000000000..11cb7ca2a --- /dev/null +++ b/ddmrp/demo/mrp_bom_demo.xml @@ -0,0 +1,49 @@ + + + + + + + + + 5 + + + + + 1 + + + 5 + + + + + + + + + 5 + + + + + 1 + + 5 + + + + + + + 1 + + 10 + + + + + diff --git a/ddmrp/demo/product_category_demo.xml b/ddmrp/demo/product_category_demo.xml new file mode 100644 index 000000000..be4756837 --- /dev/null +++ b/ddmrp/demo/product_category_demo.xml @@ -0,0 +1,8 @@ + + + + + DDMRP + + + diff --git a/ddmrp/demo/product_product_demo.xml b/ddmrp/demo/product_product_demo.xml new file mode 100644 index 000000000..f1bed1ad9 --- /dev/null +++ b/ddmrp/demo/product_product_demo.xml @@ -0,0 +1,42 @@ + + + + + FP-01 + + product + + + 2 + + + + + AS-01 + + product + + + 6 + + + + + RM-01 + + product + + + + + + + RM-02 + + product + + + + + + diff --git a/ddmrp/demo/product_supplierinfo_demo.xml b/ddmrp/demo/product_supplierinfo_demo.xml new file mode 100644 index 000000000..a3ad55c83 --- /dev/null +++ b/ddmrp/demo/product_supplierinfo_demo.xml @@ -0,0 +1,20 @@ + + + + + + + 25 + 50 + 100 + + + + + + 14 + 10 + 100 + + + diff --git a/ddmrp/demo/res_partner_demo.xml b/ddmrp/demo/res_partner_demo.xml new file mode 100644 index 000000000..43acf1ef7 --- /dev/null +++ b/ddmrp/demo/res_partner_demo.xml @@ -0,0 +1,22 @@ + + + + + + Eficent + 1 + + + + eficent@yourcompany.example.com + Rambla de Catalunya + Barcelona + 08008 + http://www.eficent.com + + + + + + diff --git a/ddmrp/demo/stock_warehouse_orderpoint_demo.xml b/ddmrp/demo/stock_warehouse_orderpoint_demo.xml new file mode 100644 index 000000000..a1f23147a --- /dev/null +++ b/ddmrp/demo/stock_warehouse_orderpoint_demo.xml @@ -0,0 +1,30 @@ + + + + + + + + 60 + 15 + 0 + 0 + + + 5 + + + + + + + 50 + 30 + 0 + 0 + + + 50 + + + diff --git a/ddmrp/migrations/8.0.2.0.0/post-migration.py b/ddmrp/migrations/8.0.2.0.0/post-migration.py new file mode 100644 index 000000000..ee5bb2b44 --- /dev/null +++ b/ddmrp/migrations/8.0.2.0.0/post-migration.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# © 2016 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# © 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import pooler +from odoo import SUPERUSER_ID +import logging + + +_logger = logging.getLogger(__name__) + +__name__ = "Upgrade to 8.0.2.0.0" + + +def migrate_variability(cr): + pool = pooler.get_pool(cr.dbname) + variability_obj = pool['stock.buffer.profile.variability'] + cr.execute(""" + SELECT old_variability, old_variability_factor + FROM stock_buffer_profile + WHERE old_variability IS NOT NULL + AND old_variability_factor IS NOT NULL + GROUP by old_variability, old_variability_factor""") + for variability, variability_factor in cr.fetchall(): + var_id = variability_obj.create(cr, SUPERUSER_ID, { + 'name': variability, + 'factor': variability_factor + }) + cr.execute(""" + UPDATE stock_buffer_profile + SET variability_id = %s + WHERE old_variability_factor = %s + AND old_variability = '%s'""" % (var_id, variability_factor, + variability)) + + +def migrate_lead_time(cr): + pool = pooler.get_pool(cr.dbname) + lead_time_obj = pool['stock.buffer.profile.lead.time'] + cr.execute(""" + SELECT old_lead_time, old_lead_time_factor + FROM stock_buffer_profile + WHERE old_lead_time IS NOT NULL + AND old_lead_time_factor IS NOT NULL + GROUP by old_lead_time, old_lead_time_factor""") + for lead_time, lead_time_factor in cr.fetchall(): + lt_id = lead_time_obj.create(cr, SUPERUSER_ID, { + 'name': lead_time, + 'factor': lead_time_factor + }) + cr.execute(""" + UPDATE stock_buffer_profile + SET lead_time_id = %s + WHERE old_lead_time_factor = %s + AND old_lead_time = '%s'""" % (lt_id, lead_time_factor, lead_time)) + + +def run_cron_ddmrp(cr): + pool = pooler.get_pool(cr.dbname) + pool['stock.warehouse.orderpoint'].cron_ddmrp(cr, SUPERUSER_ID, + automatic=True) + + +def migrate(cr, version): + if not version: + return + migrate_variability(cr) + migrate_lead_time(cr) diff --git a/ddmrp/migrations/8.0.2.0.0/pre-migration.py b/ddmrp/migrations/8.0.2.0.0/pre-migration.py new file mode 100644 index 000000000..1ba58ebc7 --- /dev/null +++ b/ddmrp/migrations/8.0.2.0.0/pre-migration.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# © 2016 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# © 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + + +_logger = logging.getLogger(__name__) + +__name__ = "Upgrade to 8.0.2.0.0" + + +def copy_profile_variability(cr): + cr.execute("""SELECT column_name + FROM information_schema.columns + WHERE table_name='stock_buffer_profile' AND + column_name='old_variability'""") + if not cr.fetchone(): + cr.execute( + """ + ALTER TABLE stock_buffer_profile + ADD COLUMN old_variability + varchar(30); + COMMENT ON COLUMN stock_buffer_profile.old_variability + IS 'Old Variability'; + """) + cr.execute( + """ + UPDATE stock_buffer_profile as sir + SET old_variability = variability + """) + + +def copy_profile_variability_factor(cr): + cr.execute("""SELECT column_name + FROM information_schema.columns + WHERE table_name='stock_buffer_profile' AND + column_name='old_variability_factor'""") + if not cr.fetchone(): + cr.execute( + """ + ALTER TABLE stock_buffer_profile + ADD COLUMN old_variability_factor + double precision; + COMMENT ON COLUMN stock_buffer_profile.old_variability_factor + IS 'Old Variability Factor'; + """) + cr.execute( + """ + UPDATE stock_buffer_profile as sir + SET old_variability_factor = variability_factor + """) + + +def copy_profile_lead_time(cr): + cr.execute("""SELECT column_name + FROM information_schema.columns + WHERE table_name='stock_buffer_profile' AND + column_name='old_lead_time'""") + if not cr.fetchone(): + cr.execute( + """ + ALTER TABLE stock_buffer_profile + ADD COLUMN old_lead_time + varchar(30); + COMMENT ON COLUMN stock_buffer_profile.old_lead_time + IS 'Old Lead Time'; + """) + cr.execute( + """ + UPDATE stock_buffer_profile + SET old_lead_time = lead_time + """) + + +def copy_profile_lead_time_factor(cr): + cr.execute("""SELECT column_name + FROM information_schema.columns + WHERE table_name='stock_buffer_profile' AND + column_name='old_lead_time_factor'""") + if not cr.fetchone(): + cr.execute( + """ + ALTER TABLE stock_buffer_profile + ADD COLUMN old_lead_time_factor + double precision; + COMMENT ON COLUMN stock_buffer_profile.old_lead_time_factor + IS 'Old Lead Time Factor'; + """) + cr.execute( + """ + UPDATE stock_buffer_profile as sir + SET old_lead_time_factor = lead_time_factor + """) + + +def migrate(cr, version): + if not version: + return + copy_profile_variability(cr) + copy_profile_variability_factor(cr) + copy_profile_lead_time(cr) + copy_profile_lead_time_factor(cr) diff --git a/ddmrp/models/__init__.py b/ddmrp/models/__init__.py new file mode 100644 index 000000000..9291c7c47 --- /dev/null +++ b/ddmrp/models/__init__.py @@ -0,0 +1,13 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import stock_buffer_profile_variability +from . import stock_buffer_profile_lead_time +from . import stock_buffer_profile +from . import procurement_group +from . import procurement_rule +from . import product_adu_calculation_method +from . import stock_warehouse +from . import stock_warehouse_orderpoint +from . import mrp_production +from . import purchase_order +from . import mrp_bom diff --git a/ddmrp/models/mrp_bom.py b/ddmrp/models/mrp_bom.py new file mode 100644 index 000000000..0c4fcb7a3 --- /dev/null +++ b/ddmrp/models/mrp_bom.py @@ -0,0 +1,166 @@ +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from openerp import api, fields, models +_logger = logging.getLogger(__name__) + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + is_buffered = fields.Boolean( + string="Buffered?", compute="_compute_is_buffered", + help="True when the product has an DDMRP buffer associated.", + ) + orderpoint_id = fields.Many2one( + comodel_name='stock.warehouse.orderpoint', string='Orderpoint', + compute="_compute_orderpoint", + ) + dlt = fields.Float( + string="Decoupled Lead Time (days)", + compute="_compute_dlt", + ) + has_mto_rule = fields.Boolean( + string="MTO", + help="Follows an MTO Pull Rule", + compute="_compute_mto_rule", + ) + + def _get_search_buffer_domain(self): + product = self.product_id + if not product: + if self.product_tmpl_id.product_variant_ids: + product = self.product_tmpl_id.product_variant_ids[0] + domain = [('product_id', '=', product.id), + ('buffer_profile_id', '!=', False)] + if self.location_id: + domain.append(('location_id', '=', self.location_id.id)) + return domain + + @api.depends('product_id', 'product_tmpl_id', 'location_id') + def _compute_orderpoint(self): + for record in self: + domain = record._get_search_buffer_domain() + # NOTE: It can be possible to find multiple orderpoints. + # For example if the BoM has no location set, and there + # are orderpoints with the same product_id and buffer_profile_id + # You do not know which one the search function finds. + orderpoint = self.env['stock.warehouse.orderpoint'].search( + domain, limit=1) + record.orderpoint_id = orderpoint + + @api.depends('orderpoint_id') + def _compute_is_buffered(self): + for bom in self: + bom.is_buffered = True if bom.orderpoint_id else False + + @api.depends('location_id') + def _compute_mto_rule(self): + # TODO: fix + for rec in self: + rec.has_mto_rule = False + + @api.multi + def _get_longest_path(self): + if not self.bom_line_ids: + return 0.0 + paths = [0] * len(self.bom_line_ids) + i = 0 + for line in self.bom_line_ids: + if line.is_buffered: + i += 1 + elif line.product_id.bom_ids: + # If the a component is manufactured we continue exploding. + location = line.location_id + line_boms = line.product_id.bom_ids + bom = line_boms.filtered( + lambda bom: bom.location_id == location) or \ + line_boms.filtered(lambda bom: not bom.location_id) + if bom: + produce_delay = bom[0].product_id.produce_delay or \ + bom[0].product_tmpl_id.produce_delay + paths[i] += produce_delay + paths[i] += bom[0]._get_longest_path() + else: + _logger.info( + "ddmrp (dlt): Product %s has no BOM for location " + "%s." % (line.product_id.name, location.name)) + i += 1 + else: + # assuming they are purchased, + if line.product_id.seller_ids: + paths[i] = line.product_id.seller_ids[0].delay + else: + _logger.info( + "ddmrp (dlt): Product %s has no seller set." % + line.product_id.name) + i += 1 + return max(paths) + + @api.multi + def _get_manufactured_dlt(self): + """Computes the Decoupled Lead Time exploding all the branches of the + BOM until a buffered position and then selecting the greatest.""" + self.ensure_one() + dlt = self.product_id.produce_delay or \ + self.product_tmpl_id.produce_delay + dlt += self._get_longest_path() + return dlt + + def _compute_dlt(self): + for rec in self: + rec.dlt = rec._get_manufactured_dlt() + + +class MrpBomLine(models.Model): + _inherit = "mrp.bom.line" + + is_buffered = fields.Boolean( + string="Buffered?", compute="_compute_is_buffered", + help="True when the product has an DDMRP buffer associated.", + ) + orderpoint_id = fields.Many2one( + comodel_name='stock.warehouse.orderpoint', string='Orderpoint', + compute="_compute_is_buffered", + ) + dlt = fields.Float( + string="Decoupled Lead Time (days)", + compute="_compute_dlt", + ) + has_mto_rule = fields.Boolean( + string="MTO", + help="Follows an MTO Pull Rule", + compute="_compute_mto_rule", + ) + + def _get_search_buffer_domain(self): + product = self.product_id or \ + self.product_tmpl_id.product_variant_ids[0] + domain = [('product_id', '=', product.id), + ('buffer_profile_id', '!=', False)] + if self.location_id: + domain.append(('location_id', '=', self.location_id.id)) + return domain + + def _compute_is_buffered(self): + for line in self: + domain = line._get_search_buffer_domain() + orderpoint = self.env['stock.warehouse.orderpoint'].search( + domain, limit=1) + line.orderpoint_id = orderpoint + line.is_buffered = True if orderpoint else False + + @api.depends('product_id') + def _compute_dlt(self): + for rec in self: + if rec.product_id.bom_ids: + rec.dlt = rec.product_id.bom_ids[0].dlt + else: + rec.dlt = rec.product_id.seller_ids and \ + rec.product_id.seller_ids[0].delay or 0.0 + + @api.depends('location_id') + def _compute_mto_rule(self): + for rec in self: + rec.has_mto_rule = False diff --git a/ddmrp/models/mrp_production.py b/ddmrp/models/mrp_production.py new file mode 100644 index 000000000..89fba0b58 --- /dev/null +++ b/ddmrp/models/mrp_production.py @@ -0,0 +1,77 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models +from .stock_warehouse_orderpoint import _PRIORITY_LEVEL + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + orderpoint_id = fields.Many2one( + comodel_name='stock.warehouse.orderpoint', + index=True, + string="Reordering rule" + ) + execution_priority_level = fields.Selection( + string="Buffer On-Hand Alert Level", + selection=_PRIORITY_LEVEL, readonly=True, + ) + on_hand_percent = fields.Float( + string="On Hand/TOR (%)", + ) + + # TODO: remove after PR https://github.com/odoo/odoo/pull/25424 has + # been merged + def _generate_finished_moves(self): + move = super(MrpProduction, self)._generate_finished_moves() + move.write({ + 'date': self.date_planned_finished, + 'date_expected': self.date_planned_finished, + }) + + @api.model + def create(self, vals): + record = super(MrpProduction, self).create(vals) + record._calc_execution_priority() + return record + + @api.multi + def _calc_execution_priority(self): + """Technical note: this method cannot be decorated with api.depends, + otherwise it would generate a infinite recursion.""" + prods = self.filtered( + lambda r: r.orderpoint_id and r.state not in ['done', 'cancel']) + for rec in prods: + rec.execution_priority_level = \ + rec.orderpoint_id.execution_priority_level + rec.on_hand_percent = rec.orderpoint_id.on_hand_percent + (self - prods).write({ + 'execution_priority_level': None, + 'on_hand_percent': None, + }) + + def _search_execution_priority(self, operator, value): + """Search on the execution priority by evaluating on all + open manufacturing orders.""" + all_records = self.search([('state', 'not in', ['done', 'cancel'])]) + + if operator == '=': + found_ids = [a.id for a in all_records + if a.execution_priority_level == value] + elif operator == 'in' and isinstance(value, list): + found_ids = [a.id for a in all_records + if a.execution_priority_level in value] + elif operator in ("!=", "<>"): + found_ids = [a.id for a in all_records + if a.execution_priority_level != value] + elif operator == 'not in' and isinstance(value, list): + found_ids = [a.id for a in all_records + if a.execution_priority_level not in value] + else: + raise NotImplementedError( + 'Search operator %s not implemented for value %s' + % (operator, value) + ) + return [('id', 'in', found_ids)] diff --git a/ddmrp/models/procurement_group.py b/ddmrp/models/procurement_group.py new file mode 100644 index 000000000..11cdd1a62 --- /dev/null +++ b/ddmrp/models/procurement_group.py @@ -0,0 +1,20 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# Copyright 2018 Camptocamp SA https://www.camptocamp.com + +from odoo import api, models + + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def _procure_orderpoint_confirm(self, use_new_cursor=False, + company_id=False): + """ Override the standard method to disable the possibility to + automatically create procurements based on order points. + With DDMRP it is in the hands of the planner to manually + create procurements, based on the procure recommendations.""" + return {} diff --git a/ddmrp/models/procurement_rule.py b/ddmrp/models/procurement_rule.py new file mode 100644 index 000000000..dab0ba7ea --- /dev/null +++ b/ddmrp/models/procurement_rule.py @@ -0,0 +1,32 @@ +# Copyright 2018 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class ProcurementRule(models.Model): + _inherit = 'procurement.rule' + + def _prepare_mo_vals(self, product_id, product_qty, product_uom, + location_id, name, origin, values, bom): + result = super(ProcurementRule, self)._prepare_mo_vals( + product_id, product_qty, product_uom, location_id, + name, origin, values, bom + ) + # this field can be passed by + # StockWarehouseOrderpoint._prepare_procurement_values + # (yes as a recordset!) + if values.get('orderpoint_id'): + result['orderpoint_id'] = values['orderpoint_id'].id + return result + + def _run_manufacture(self, product_id, product_qty, product_uom, + location_id, name, origin, values): + super(ProcurementRule, self)._run_manufacture( + product_id, product_qty, product_uom, + location_id, name, origin, values + ) + orderpoint = values.get('orderpoint_id') + if orderpoint: + orderpoint.cron_actions() + return True diff --git a/ddmrp/models/product_adu_calculation_method.py b/ddmrp/models/product_adu_calculation_method.py new file mode 100644 index 000000000..15a3e10c6 --- /dev/null +++ b/ddmrp/models/product_adu_calculation_method.py @@ -0,0 +1,42 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models, _ +from odoo.exceptions import Warning as UserError + + +class ProductAduCalculationMethod(models.Model): + _name = 'product.adu.calculation.method' + _description = 'Product Average Daily Usage calculation method' + + @api.model + def _get_calculation_method(self): + return [ + ('fixed', _('Fixed ADU')), + ('past', _('Past-looking')), + ('future', _('Future-looking'))] + + name = fields.Char(string="Name", required=True) + + method = fields.Selection("_get_calculation_method", + string="Calculation method") + + use_estimates = fields.Boolean(sting="Use estimates/forecasted values") + horizon = fields.Float(string="Horizon", + help="Length-of-period horizon in days") + + company_id = fields.Many2one( + 'res.company', string='Company', required=True, + default=lambda self: + self.env['res.company']._company_default_get( + 'product.adu.calculation.method')) + + @api.multi + @api.constrains('method', 'horizon') + def _check_horizon(self): + for rec in self: + if rec.method in ['past', 'future'] and not rec.horizon: + raise UserError(_('Please indicate a length-of-period ' + 'horizon.')) diff --git a/ddmrp/models/purchase_order.py b/ddmrp/models/purchase_order.py new file mode 100644 index 000000000..1d0b9ab91 --- /dev/null +++ b/ddmrp/models/purchase_order.py @@ -0,0 +1,43 @@ +# Copyright 2017-18 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from .stock_warehouse_orderpoint import _PRIORITY_LEVEL + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + ddmrp_comment = fields.Text(string="Follow-up Notes") + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + execution_priority_level = fields.Selection( + string="Buffer On-Hand Status Level", + selection=_PRIORITY_LEVEL, readonly=True, + ) + on_hand_percent = fields.Float( + string="On Hand/TOR (%)", readonly=True, + ) + ddmrp_comment = fields.Text(related="order_id.ddmrp_comment") + + def create(self, vals): + record = super(PurchaseOrderLine, self).create(vals) + record._calc_execution_priority() + return record + + @api.multi + def _calc_execution_priority(self): + # TODO: handle serveral orderpoints? worst scenario, average? + to_compute = self.filtered( + lambda r: r.orderpoint_ids and r.state not in ['done', 'cancel']) + for rec in to_compute: + rec.execution_priority_level = \ + rec.orderpoint_ids[0].execution_priority_level + rec.on_hand_percent = rec.orderpoint_ids[0].on_hand_percent + (self - to_compute).write({ + 'execution_priority_level': None, + 'on_hand_percent': None, + }) diff --git a/ddmrp/models/stock_buffer_profile.py b/ddmrp/models/stock_buffer_profile.py new file mode 100644 index 000000000..eb6be443e --- /dev/null +++ b/ddmrp/models/stock_buffer_profile.py @@ -0,0 +1,53 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + +_REPLENISH_METHODS = [ + ('replenish', 'Replenished'), + ('replenish_override', 'Replenished Override'), + ('min_max', 'Min-max') +] +_ITEM_TYPES = [ + ('manufactured', 'Manufactured'), + ('purchased', 'Purchased'), + ('distributed', 'Distributed') +] + + +class StockBufferProfile(models.Model): + _name = 'stock.buffer.profile' + _string = 'Buffer Profile' + + @api.multi + @api.depends("item_type", "lead_time_id", "lead_time_id.name", + "lead_time_id.factor", "variability_id", + "variability_id.name", "variability_id.factor") + def _compute_name(self): + """Get the right summary for this job.""" + for rec in self: + rec.name = '%s %s, %s(%s), %s(%s)' % (rec.replenish_method, + rec.item_type, + rec.lead_time_id.name, + rec.lead_time_id.factor, + rec.variability_id.name, + rec.variability_id.factor) + + name = fields.Char(string="Name", compute="_compute_name", store=True) + replenish_method = fields.Selection(string="Replenishment method", + selection=_REPLENISH_METHODS, + required=True) + item_type = fields.Selection(string="Item Type", selection=_ITEM_TYPES, + required=True) + lead_time_id = fields.Many2one( + comodel_name='stock.buffer.profile.lead.time', + string='Lead Time Factor') + variability_id = fields.Many2one( + comodel_name='stock.buffer.profile.variability', + string='Variability Factor') + company_id = fields.Many2one( + 'res.company', 'Company', required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'stock.buffer.profile')) diff --git a/ddmrp/models/stock_buffer_profile_lead_time.py b/ddmrp/models/stock_buffer_profile_lead_time.py new file mode 100644 index 000000000..44965d606 --- /dev/null +++ b/ddmrp/models/stock_buffer_profile_lead_time.py @@ -0,0 +1,18 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockBufferProfileLeadTime(models.Model): + _name = 'stock.buffer.profile.lead.time' + _string = 'Buffer Profile Lead Time Factor' + + name = fields.Char(string='Name', required=True) + factor = fields.Float(string='Lead Time Factor', required=True) + company_id = fields.Many2one( + 'res.company', 'Company', required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'stock.buffer.profile.lead.time')) diff --git a/ddmrp/models/stock_buffer_profile_variability.py b/ddmrp/models/stock_buffer_profile_variability.py new file mode 100644 index 000000000..454e4ed8c --- /dev/null +++ b/ddmrp/models/stock_buffer_profile_variability.py @@ -0,0 +1,18 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockBufferProfileVariability(models.Model): + _name = 'stock.buffer.profile.variability' + _string = 'Buffer Profile Variability Factor' + + name = fields.Char(string='Name', required=True) + factor = fields.Float(string='Variability Factor', required=True) + company_id = fields.Many2one( + 'res.company', 'Company', required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'stock.buffer.profile.variability')) diff --git a/ddmrp/models/stock_warehouse.py b/ddmrp/models/stock_warehouse.py new file mode 100644 index 000000000..34e98ca94 --- /dev/null +++ b/ddmrp/models/stock_warehouse.py @@ -0,0 +1,14 @@ +# Copyright 2018 Eficent Business and IT Consulting Services, S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models + + +class StockWarehouse(models.Model): + _inherit = 'stock.warehouse' + + nfp_incoming_safety_factor = fields.Float( + 'Net Flow Position Incoming Safety Factor', + help='Factor used to compute the number of days to look into the ' + 'future for incoming shipments for the purposes of the Net ' + 'Flow position calculation.') diff --git a/ddmrp/models/stock_warehouse_orderpoint.py b/ddmrp/models/stock_warehouse_orderpoint.py new file mode 100644 index 000000000..6de13c4dd --- /dev/null +++ b/ddmrp/models/stock_warehouse_orderpoint.py @@ -0,0 +1,696 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import api, fields, models, _ +from datetime import datetime, timedelta +from odoo.addons import decimal_precision as dp +from odoo.tools import float_compare, float_round +import operator as py_operator + +_logger = logging.getLogger(__name__) +try: + from bokeh.plotting import figure + from bokeh.embed import components + from bokeh.models import Legend, ColumnDataSource, LabelSet +except (ImportError, IOError) as err: + _logger.debug(err) + + +OPERATORS = { + '<': py_operator.lt, + '>': py_operator.gt, + '<=': py_operator.le, + '>=': py_operator.ge, + '==': py_operator.eq, + '!=': py_operator.ne +} + + +UNIT = dp.get_precision('Product Unit of Measure') + + +_PRIORITY_LEVEL = [ + ('1_red', 'Red'), + ('2_yellow', 'Yellow'), + ('3_green', 'Green') +] + + +class StockWarehouseOrderpoint(models.Model): + _inherit = 'stock.warehouse.orderpoint' + _description = "Stock Buffer" + + @api.multi + @api.depends("dlt", "adu", "buffer_profile_id.lead_time_id.factor", + "buffer_profile_id.variability_id.factor", + "product_uom.rounding", "red_override", + "lead_days", "product_id.seller_ids.delay") + def _compute_red_zone(self): + for rec in self: + if rec.replenish_method in ['replenish', 'min_max']: + rec.red_base_qty = float_round( + rec.dlt * rec.adu * + rec.buffer_profile_id.lead_time_id.factor, + precision_rounding=rec.product_uom.rounding) + rec.red_safety_qty = float_round( + rec.red_base_qty * + rec.buffer_profile_id.variability_id.factor, + precision_rounding=rec.product_uom.rounding) + rec.red_zone_qty = rec.red_base_qty + rec.red_safety_qty + else: + rec.red_zone_qty = rec.red_override + + @api.multi + @api.depends("dlt", "adu", "buffer_profile_id.lead_time_id.factor", + "order_cycle", "minimum_order_quantity", + "product_uom.rounding", "green_override") + def _compute_green_zone(self): + for rec in self: + if rec.replenish_method in ['replenish', 'min_max']: + # Using imposed or desired minimum order cycle + rec.green_zone_oc = float_round( + rec.order_cycle * rec.adu, + precision_rounding=rec.product_uom.rounding) + # Using lead time factor + rec.green_zone_lt_factor = float_round( + rec.dlt*rec.adu*rec.buffer_profile_id.lead_time_id.factor, + precision_rounding=rec.product_uom.rounding) + # Using minimum order quantity + rec.green_zone_moq = float_round( + rec.minimum_order_quantity, + precision_rounding=rec.product_uom.rounding) + + # The biggest option of the above will be used as the green + # zone value + rec.green_zone_qty = max(rec.green_zone_oc, + rec.green_zone_lt_factor, + rec.green_zone_moq) + else: + rec.green_zone_qty = rec.green_override + rec.top_of_green = \ + rec.green_zone_qty + rec.yellow_zone_qty + rec.red_zone_qty + + @api.multi + @api.depends("dlt", "adu", "buffer_profile_id.lead_time_id.factor", + "buffer_profile_id.variability_id.factor", + "buffer_profile_id.replenish_method", + "order_cycle", "minimum_order_quantity", + "product_uom.rounding", "yellow_override", + "red_zone_qty") + def _compute_yellow_zone(self): + for rec in self: + if rec.replenish_method == 'min_max': + rec.yellow_zone_qty = 0 + elif rec.replenish_method == 'replenish': + rec.yellow_zone_qty = float_round( + rec.dlt * rec.adu, + precision_rounding=rec.product_uom.rounding) + else: + rec.yellow_zone_qty = rec.yellow_override + rec.top_of_yellow = rec.yellow_zone_qty + rec.red_zone_qty + + @api.multi + @api.depends("dlt") + def _compute_procure_recommended_date(self): + for rec in self: + dlt = int(rec.dlt) + # For purchased items we always consider calendar days, + # not work days. + if rec.warehouse_id.calendar_id and rec.buffer_profile_id and \ + rec.buffer_profile_id.item_type != 'purchased': + dt_to = rec.warehouse_id.calendar_id.plan_days( + dlt + 1, datetime.now()) + procure_recommended_date = fields.Date.to_string(dt_to) + else: + procure_recommended_date = \ + fields.date.today() + timedelta(days=dlt) + rec.procure_recommended_date = procure_recommended_date + + @api.multi + @api.depends("net_flow_position", "dlt", "adu", + "buffer_profile_id.lead_time_id.factor", + "red_zone_qty", "order_cycle", "minimum_order_quantity", + "qty_multiple", "product_uom", "procure_uom_id", + "product_uom.rounding") + def _compute_procure_recommended_qty(self): + subtract_qty = self._quantity_in_progress() + for rec in self: + procure_recommended_qty = 0.0 + if rec.net_flow_position < rec.top_of_yellow: + qty = (rec.top_of_green - + rec.net_flow_position - + subtract_qty[rec.id]) + if qty >= 0.0: + procure_recommended_qty = qty + else: + if subtract_qty[rec.id] > 0.0: + procure_recommended_qty -= subtract_qty[rec.id] + if procure_recommended_qty > 0.0: + reste = rec.qty_multiple > 0 and \ + procure_recommended_qty % rec.qty_multiple or 0.0 + + if rec.procure_uom_id: + rounding = rec.procure_uom_id.rounding + else: + rounding = rec.product_uom.rounding + + if float_compare( + reste, 0.0, + precision_rounding=rounding) > 0: + procure_recommended_qty += rec.qty_multiple - reste + + if rec.procure_uom_id: + product_qty = rec.product_id.uom_id._compute_quantity( + procure_recommended_qty, rec.procure_uom_id) + else: + product_qty = procure_recommended_qty + else: + product_qty = 0.0 + + rec.procure_recommended_qty = product_qty + + def _compute_ddmrp_chart(self): + """This method use the Bokeh library to create a buffer depiction.""" + for rec in self: + p = figure(plot_width=300, plot_height=400, + y_axis_label='Quantity') + p.xaxis.visible = False + red = p.vbar(x=1, bottom=0, top=rec.top_of_red, width=1, + color='red', legend=False) + yellow = p.vbar(x=1, bottom=rec.top_of_red, top=rec.top_of_yellow, + width=1, color='yellow', legend=False) + green = p.vbar(x=1, bottom=rec.top_of_yellow, top=rec.top_of_green, + width=1, color='green', legend=False) + net_flow = p.line( + [0, 2], [rec.net_flow_position, rec.net_flow_position], + line_width=2) + on_hand = p.line( + [0, 2], [rec.product_location_qty, rec.product_location_qty], + line_width=2, line_dash='dotted') + legend = Legend(items=[ + ("Red zone", [red]), + ("Yellow zone", [yellow]), + ("Green zone", [green]), + ("Net Flow Position", [net_flow]), + ("On-Hand Position", [on_hand]), + ]) + labels_source_data = { + 'height': [rec.net_flow_position, + rec.product_location_qty, + rec.top_of_red, + rec.top_of_yellow, + rec.top_of_green], + 'weight': [0.25, 1.75, 1, 1, 1], + 'names': [rec.net_flow_position, + rec.product_location_qty, + rec.top_of_red, + rec.top_of_yellow, + rec.top_of_green], + } + source = ColumnDataSource(data=labels_source_data) + labels = LabelSet( + x="weight", y="height", text="names", y_offset=1, + render_mode='canvas', text_font_size="8pt", + source=source, text_align='center') + p.add_layout(labels) + p.add_layout(legend, 'below') + + script, div = components(p) + rec.ddmrp_chart = '%s%s' % (div, script) + + @api.multi + @api.depends("red_zone_qty") + def _compute_order_spike_threshold(self): + # TODO: Add various methods to compute the spike threshold + for rec in self: + rec.order_spike_threshold = 0.5 * rec.red_zone_qty + + def _get_manufactured_bom(self): + return self.env['mrp.bom'].search( + ['|', + ('product_id', '=', self.product_id.id), + ('product_tmpl_id', '=', self.product_id.product_tmpl_id.id), + '|', + ('location_id', '=', self.location_id.id), + ('location_id', '=', False)], limit=1) + + @api.depends('lead_days', 'product_id.seller_ids.delay') + def _compute_dlt(self): + for rec in self: + if rec.buffer_profile_id.item_type == 'manufactured': + bom = rec._get_manufactured_bom() + rec.dlt = bom.dlt + elif rec.buffer_profile_id.item_type == 'distributed': + rec.dlt = rec.lead_days + else: + rec.dlt = rec.product_id.seller_ids and \ + rec.product_id.seller_ids[0].delay or rec.lead_days + + buffer_profile_id = fields.Many2one( + comodel_name='stock.buffer.profile', + string="Buffer Profile", + ) + replenish_method = fields.Selection( + related="buffer_profile_id.replenish_method", + readonly=True, + ) + green_override = fields.Float( + string="Green Zone (Override)", + ) + yellow_override = fields.Float( + string="Yellow Zone (Override)", + ) + red_override = fields.Float( + string="Red Zone (Override)", + ) + dlt = fields.Float(string="Decoupled Lead Time (days)", + compute="_compute_dlt") + adu = fields.Float(string="Average Daily Usage (ADU)", + default=0.0, digits=UNIT, readonly=True) + adu_calculation_method = fields.Many2one( + comodel_name="product.adu.calculation.method", + string="ADU calculation method") + adu_fixed = fields.Float(string="Fixed ADU", + default=1.0, digits=UNIT) + order_cycle = fields.Float(string="Minimum Order Cycle (days)") + minimum_order_quantity = fields.Float(string="Minimum Order Quantity", + digits=UNIT) + red_base_qty = fields.Float(string="Red Base Qty", + compute="_compute_red_zone", + digits=UNIT, store=True) + red_safety_qty = fields.Float(string="Red Safety Qty", + compute="_compute_red_zone", + digits=UNIT, store=True) + red_zone_qty = fields.Float(string="Red Zone Qty", + compute="_compute_red_zone", + digits=UNIT, store=True) + top_of_red = fields.Float(string="Top of Red", + related="red_zone_qty", store=True) + green_zone_qty = fields.Float(string="Green Zone Qty", + compute="_compute_green_zone", + digits=UNIT, store=True) + green_zone_lt_factor = fields.Float(string="Green Zone Lead Time Factor", + compute="_compute_green_zone", + help="Green zone Lead Time Factor", + store=True) + green_zone_moq = fields.Float(string="Green Zone Minimum Order Quantity", + compute="_compute_green_zone", + help="Green zone minimum order quantity", + digits=UNIT, store=True) + green_zone_oc = fields.Float(string="Green Zone Order Cycle", + compute="_compute_green_zone", + help="Green zone order cycle", store=True) + yellow_zone_qty = fields.Float(string="Yellow Zone Qty", + compute="_compute_yellow_zone", + digits=UNIT, store=True) + top_of_yellow = fields.Float(string="Top of Yellow", + compute="_compute_yellow_zone", + digits=UNIT, store=True) + top_of_green = fields.Float(string="Top of Green", + compute="_compute_green_zone", digits=UNIT, + store=True) + order_spike_horizon = fields. Float(string="Order Spike Horizon") + order_spike_threshold = fields.Float( + string="Order Spike Threshold", + compute="_compute_order_spike_threshold", digits=UNIT, store=True) + qualified_demand = fields.Float(string="Qualified demand", digits=UNIT, + readonly=True) + incoming_dlt_qty = fields.Float( + string="Incoming (Within DLT)", + readonly=True, + ) + net_flow_position = fields.Float(string="Net flow position", digits=UNIT, + readonly=True) + net_flow_position_percent = fields.Float( + string="Net flow position (% of TOG)", readonly=True) + planning_priority_level = fields.Selection( + string="Planning Priority Level", selection=_PRIORITY_LEVEL, + readonly=True) + execution_priority_level = fields.Selection( + string="On-Hand Alert Level", + selection=_PRIORITY_LEVEL, store=True, readonly=True) + on_hand_percent = fields.Float(string="On Hand/TOR (%)", + store=True, readonly=True) + # We override the calculation method for the procure recommended qty + procure_recommended_qty = fields.Float( + compute="_compute_procure_recommended_qty", store=True) + procure_recommended_date = fields.Date( + compute="_compute_procure_recommended_date") + mrp_production_ids = fields.One2many( + string='Manufacturing Orders', comodel_name='mrp.production', + inverse_name='orderpoint_id') + purchase_line_ids = fields.Many2many(comodel_name='purchase.order.line', + string='PO Lines', copy=False) + ddmrp_chart = fields.Text(string='DDMRP Chart', + compute=_compute_ddmrp_chart) + + _order = 'planning_priority_level asc, net_flow_position asc' + + @api.multi + @api.onchange("red_zone_qty") + def onchange_red_zone_qty(self): + for rec in self: + rec.product_min_qty = rec.red_zone_qty + + @api.multi + @api.onchange("adu_fixed", "adu_calculation_method") + def onchange_adu(self): + self._calc_adu() + + @api.multi + @api.onchange("top_of_green") + def onchange_green_zone_qty(self): + for rec in self: + rec.product_max_qty = rec.top_of_green + + @api.multi + def _search_open_stock_moves_domain(self): + self.ensure_one() + return [('product_id', '=', self.product_id.id), + ('state', 'in', ['draft', 'waiting', 'confirmed', + 'assigned']), + ('location_dest_id', '=', self.location_id.id)] + + @api.model + def _stock_move_tree_view(self, lines): + views = [] + tree_view = self.env.ref('stock.view_move_tree', False) + if tree_view: + views += [(tree_view.id, 'tree')] + form_view = self.env.ref( + 'stock.view_move_form', False) + if form_view: + views += [(form_view.id, 'form')] + + return { + 'name': _('Non-completed Moves'), + 'type': 'ir.actions.act_window', + 'res_model': 'stock.move', + 'view_type': 'form', + 'views': views, + 'view_mode': 'tree,form', + 'domain': str([('id', 'in', lines.ids)]), + } + + @api.multi + def open_moves(self): + self.ensure_one() + # Utility method used to add an "Open Moves" button in the buffer + # planning view + domain = self._search_open_stock_moves_domain() + records = self.env['stock.move'].search(domain) + return self._stock_move_tree_view(records) + + @api.multi + def _past_demand_estimate_domain(self, date_from, date_to, locations): + self.ensure_one() + return [('location_id', 'in', locations.ids), + ('product_id', '=', self.product_id.id), + ('date_range_id.date_start', '<=', date_to), + ('date_range_id.date_end', '>=', date_from)] + + @api.multi + def _past_moves_domain(self, date_from, locations): + self.ensure_one() + return [('state', '=', 'done'), ('location_id', 'in', locations.ids), + ('location_dest_id', 'not in', locations.ids), + ('product_id', '=', self.product_id.id), + ('date', '>=', date_from)] + + @api.multi + def _calc_adu_past_demand(self): + self.ensure_one() + horizon = self.adu_calculation_method.horizon or 0 + if self.warehouse_id.calendar_id: + dt_from = self.warehouse_id.calendar_id.plan_days( + -1 * horizon - 1, datetime.now()) + date_from = fields.Date.to_string(dt_from) + else: + date_from = fields.Date.to_string( + fields.date.today() - timedelta(days=horizon)) + date_to = fields.Date.today() + locations = self.env['stock.location'].search( + [('id', 'child_of', [self.location_id.id])]) + if self.adu_calculation_method.use_estimates: + qty = 0.0 + domain = self._past_demand_estimate_domain(date_from, date_to, + locations) + for estimate in self.env['stock.demand.estimate'].search(domain): + qty += estimate.get_quantity_by_date_range( + fields.Date.from_string(date_from), + fields.Date.from_string(date_to)) + return qty / horizon + else: + qty = 0.0 + domain = self._past_moves_domain(date_from, locations) + for group in self.env['stock.move'].read_group( + domain, ['product_id', 'product_qty'], ['product_id']): + qty += group['product_qty'] + return qty / horizon + + @api.multi + def _future_demand_estimate_domain(self, date_from, date_to, locations): + self.ensure_one() + return [('location_id', 'in', locations.ids), + ('product_id', '=', self.product_id.id), + ('date_range_id.date_start', '<=', date_to), + ('date_range_id.date_end', '>=', date_from)] + + @api.multi + def _future_moves_domain(self, date_to, locations): + self.ensure_one() + return [('state', 'not in', ['done', 'cancel']), + ('location_id', 'in', locations.ids), + ('location_dest_id', 'not in', locations.ids), + ('product_id', '=', self.product_id.id), + ('date_expected', '<=', date_to)] + + @api.multi + def _calc_adu_future_demand(self): + self.ensure_one() + horizon = self.adu_calculation_method.horizon or 1 + if self.warehouse_id.calendar_id: + dt_to = self.warehouse_id.calendar_id.plan_days( + horizon-1 + 1, datetime.now()) + date_to = fields.Date.to_string(dt_to) + else: + date_to = fields.Date.to_string( + fields.date.today() + timedelta(days=horizon-1)) + date_from = fields.Date.today() + locations = self.env['stock.location'].search( + [('id', 'child_of', [self.location_id.id])]) + if self.adu_calculation_method.use_estimates: + qty = 0.0 + domain = self._future_demand_estimate_domain(date_from, date_to, + locations) + for estimate in self.env['stock.demand.estimate'].search(domain): + qty += estimate.get_quantity_by_date_range( + fields.Date.from_string(date_from), + fields.Date.from_string(date_to)) + return qty / horizon + else: + qty = 0.0 + domain = self._future_moves_domain(date_to, locations) + for group in self.env['stock.move'].read_group( + domain, ['product_id', 'product_qty'], ['product_id']): + qty += group['product_qty'] + return qty / horizon + + @api.multi + def _calc_adu(self): + for orderpoint in self: + if orderpoint.adu_calculation_method.method == 'fixed': + orderpoint.adu = orderpoint.adu_fixed + elif orderpoint.adu_calculation_method.method == 'past': + orderpoint.adu = orderpoint._calc_adu_past_demand() + elif orderpoint.adu_calculation_method.method == 'future': + orderpoint.adu = orderpoint._calc_adu_future_demand() + return True + + @api.multi + def _search_stock_moves_qualified_demand_domain(self): + self.ensure_one() + horizon = self.order_spike_horizon + if self.warehouse_id.calendar_id: + dt_to = self.warehouse_id.calendar_id.plan_days( + horizon + 1, datetime.now()) + date_to = fields.Date.to_string(dt_to) + else: + date_to = fields.Date.to_string(fields.date.today() + + timedelta(days=horizon)) + locations = self.env['stock.location'].search( + [('id', 'child_of', [self.location_id.id])]) + return [('product_id', '=', self.product_id.id), + ('state', 'in', ['waiting', 'confirmed', 'assigned']), + ('location_id', 'in', locations.ids), + ('location_dest_id', 'not in', locations.ids), + ('date_expected', '<=', date_to)] + + @api.multi + def _search_stock_moves_incoming_domain(self): + self.ensure_one() + # We introduce a safety factor of 2 for incoming moves + factor = self.warehouse_id.nfp_incoming_safety_factor or 1 + horizon = int(self.dlt) * factor + # For purchased products we use calendar days, not work days + if self.warehouse_id.calendar_id and \ + self.buffer_profile_id.item_type != 'purchased': + dt_to = self.warehouse_id.calendar_id.plan_days( + horizon + 1, datetime.now()) + date_to = fields.Date.to_string(dt_to) + else: + date_to = fields.Date.to_string(fields.date.today() + + timedelta(days=horizon)) + locations = self.env['stock.location'].search( + [('id', 'child_of', [self.location_id.id])]) + return [('product_id', '=', self.product_id.id), + ('state', 'in', ['waiting', 'confirmed', 'assigned']), + ('location_id', 'not in', locations.ids), + ('location_dest_id', 'in', locations.ids), + ('date_expected', '<=', date_to)] + + @api.multi + def _calc_qualified_demand(self): + for rec in self: + rec.qualified_demand = 0.0 + domain = rec._search_stock_moves_qualified_demand_domain() + moves = self.env['stock.move'].search(domain) + demand_by_days = {} + move_dates = [fields.Datetime.from_string(dt).date() for dt in + moves.mapped('date_expected')] + for move_date in move_dates: + demand_by_days[move_date] = 0.0 + for move in moves: + date = fields.Datetime.from_string(move.date_expected).date() + demand_by_days[date] += \ + move.product_qty - move.reserved_availability + for date in demand_by_days: + if demand_by_days[date] >= rec.order_spike_threshold \ + or date <= fields.date.today(): + rec.qualified_demand += demand_by_days[date] + return True + + @api.multi + def _calc_incoming_dlt_qty(self): + for rec in self: + rec.incoming_dlt_qty = 0.0 + domain = rec._search_stock_moves_incoming_domain() + moves = self.env['stock.move'].search(domain) + rec.incoming_dlt_qty = sum(moves.mapped('product_qty')) + return True + + @api.multi + def _calc_net_flow_position(self): + for rec in self: + rec.net_flow_position = \ + rec.product_location_qty_available_not_res + \ + rec.incoming_dlt_qty - rec.qualified_demand + usage = 0.0 + if rec.top_of_green: + usage = round((rec.net_flow_position / + rec.top_of_green*100), 2) + rec.net_flow_position_percent = usage + return True + + @api.multi + def _calc_planning_priority(self): + for rec in self: + if rec.net_flow_position >= rec.top_of_yellow: + rec.planning_priority_level = '3_green' + elif rec.net_flow_position >= rec.top_of_red: + rec.planning_priority_level = '2_yellow' + else: + rec.planning_priority_level = '1_red' + + @api.multi + def _calc_execution_priority(self): + for rec in self: + if rec.product_location_qty_available_not_res >= rec.top_of_red: + rec.execution_priority_level = '3_green' + elif rec.product_location_qty_available_not_res >= \ + rec.top_of_red*0.5: + rec.execution_priority_level = '2_yellow' + else: + rec.execution_priority_level = '1_red' + if rec.top_of_red: + rec.on_hand_percent = round(( + (rec.product_location_qty_available_not_res / + rec.top_of_red)*100), 2) + else: + rec.on_hand_percent = 0.0 + + @api.model + def cron_ddmrp_adu(self, automatic=False): + """calculate ADU for each DDMRP buffer. Called by cronjob. + """ + _logger.info("Start cron_ddmrp_adu.") + orderpoints = self.search([]) + i = 0 + j = len(orderpoints) + for op in orderpoints: + try: + i += 1 + _logger.debug("ddmrp cron_adu: %s. (%s/%s)" % (op.name, i, j)) + if automatic: + with self.env.cr.savepoint(): + op._calc_adu() + else: + op._calc_adu() + except Exception: + _logger.exception( + 'Fail to compute ADU for orderpoint %s', op.name) + if not automatic: + raise + _logger.info("End cron_ddmrp_adu.") + return True + + @api.multi + def cron_actions(self): + """This method is meant to be inherited by other modules in order to + enhance extensibility.""" + self.ensure_one() + self._calc_qualified_demand() + self._calc_incoming_dlt_qty() + self._calc_net_flow_position() + self._calc_planning_priority() + self._calc_execution_priority() + self.mrp_production_ids._calc_execution_priority() + self.mapped("purchase_line_ids")._calc_execution_priority() + # FIXME: temporary patch to force the recalculation of zones. + self._compute_red_zone() + self._compute_yellow_zone() + self._compute_green_zone() + return True + + @api.model + def cron_ddmrp(self, automatic=False): + """calculate key DDMRP parameters for each orderpoint + Called by cronjob. + """ + _logger.info("Start cron_ddmrp.") + orderpoints = self.search([]) + i = 0 + j = len(orderpoints) + orderpoints.refresh() + for op in orderpoints: + i += 1 + _logger.debug("ddmrp cron: %s. (%s/%s)" % (op.name, i, j)) + try: + if automatic: + with self.env.cr.savepoint(): + op.cron_actions() + else: + op.cron_actions() + except Exception: + _logger.exception( + 'Fail to create recurring invoice for orderpoint %s', + op.name) + if not automatic: + raise + _logger.info("End cron_ddmrp.") + + return True diff --git a/ddmrp/readme/CONFIGURE.rst b/ddmrp/readme/CONFIGURE.rst new file mode 100644 index 000000000..71e6dfb35 --- /dev/null +++ b/ddmrp/readme/CONFIGURE.rst @@ -0,0 +1,29 @@ +Scheduled actions +~~~~~~~~~~~~~~~~~ + +* Go to *Settings > Technical*. +* 'DDMRP Buffer ADU calculation'. Computes the Average Daily Usage for all + Buffers. +* 'Reordering Rule DDMRP calculation'. Computes the Qualified Demand, Net + Flow Position, Planning and Execution priorities for all Buffers. + +Decoupled Lead Time computation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The DLT is automatically computed by the system. + +For manufactured products' buffers just remember to provide and +set properly the following information: + +* The *Manufacturing Lead Time* for the manufactured product. It can be found + at the product form view under the tab *Sales*. +* The *Delivery Lead Time* for the preferred vendor of a product. This is + important for the products which are purchased and are components in any + Bill of Materials. + +For purchased/distributed products' buffers the logic is simpler. + +* In the first place the system will look if there are Vendors for the product, + if so it will use the *Delivery Lead Time* of the preferred one. +* In case of absence of vendors, the *Lead Time* at the bottom of the Buffer + form view will be used. diff --git a/ddmrp/readme/CONTRIBUTORS.rst b/ddmrp/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b047a3768 --- /dev/null +++ b/ddmrp/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Jordi Ballester Alomar +* Lois Rilo Antelo +* Guewen Baconnier diff --git a/ddmrp/readme/CREDITS.rst b/ddmrp/readme/CREDITS.rst new file mode 100644 index 000000000..259e3b85f --- /dev/null +++ b/ddmrp/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The initial development of this module has been financially supported by: + +* Aleph Objects, Inc. diff --git a/ddmrp/readme/DESCRIPTION.rst b/ddmrp/readme/DESCRIPTION.rst new file mode 100644 index 000000000..c128b3bfa --- /dev/null +++ b/ddmrp/readme/DESCRIPTION.rst @@ -0,0 +1,57 @@ +Demand Driven Material Requirements Planning is a formal multi-echelon +planning and execution method developed by Ms. Carol Ptak and Mr. Chad Smith. + +DDMRP combines blended aspects of Material Requirements Planning (MRP), +Distribution Requirements Planning (DRP) with the pull and visibility +emphases found in Lean and the Theory of Constraints and the variability +reduction emphasis of Six Sigma. + +This method has five sequential components: + +#. *Strategic Inventory Positioning*. Answers the question "Given our system + and environment, where should we place inventory to have the best + protection?" and determines where should decoupling points of inventory be + placed. + +#. *Buffer Profiles and Levels*. Determine the amount of protection at those + decoupling points. + +#. *Dynamic Adjustments*. Allow the company to adapt buffers to group and + individual part trait changes over time through the use of several types + of adjustments. + +#. *Demand Driven Planning*. Allow to launch purchase orders (POs), + manufacturing orders (MOs) and Transfer Orders (TOs) based on the priority + dictated by the buffers. + +#. *Visible and Collaborative Execution*. These POs, MOs and TOs have to be + effectively managed to synchronize with the changes that often occur within + the "execution horizon." + +These five components work together to greatly dampen, if not eliminate, +the nervousness of traditional MRP systems and the bullwhip effect in +complex and challenging environments. + +This approach provides real information about those parts that are +truly at risk of negatively impacting the planned availability of inventory. + +DDMRP sorts the significant few items that require attention from +the many parts that are being managed. Under the DDMRP approach, +fewer planners can make better decisions more quickly. That means companies +will be better able to leverage their working and human capital. + +Demand Driven Material Requirements Planning is quickly being adopted +by a wide variety of leading companies across the world. + +Some of the benefits reported by the DDMRP method include: + +* High fill rate performance +* Lead time reductions +* Inventory reductions, while improving customer service +* Eliminate costs related to expedite +* Planners see priorities instead of constantly fighting the conflicting + messages of MRP + +It is highly recommended to read the book 'Demand Driven Material +Requirements Planning (DDMRP)' by Carol Ptak and Chad Smith. + diff --git a/ddmrp/readme/HISTORY.rst b/ddmrp/readme/HISTORY.rst new file mode 100644 index 000000000..b8c8b5d7e --- /dev/null +++ b/ddmrp/readme/HISTORY.rst @@ -0,0 +1,4 @@ +11.0.1.0.0 (2018-07-16) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history diff --git a/ddmrp/readme/INSTALL.rst b/ddmrp/readme/INSTALL.rst new file mode 100644 index 000000000..7a64fc090 --- /dev/null +++ b/ddmrp/readme/INSTALL.rst @@ -0,0 +1,6 @@ +We strongly recommend to **uninstall** ``procurement_jit`` (so deliveries +related to Sales Orders aren't automatically reserved) and to avoid to +reserve stock for specific moves, buffers are in fact a reservation of stock. +However, while **reservation is discouraged**, it is still available to be +used, in case of reserved stock be aware that the buffer will be blind to this +transfers and stock and you are bypassing the DDMRP reordering flow. diff --git a/ddmrp/readme/ROADMAP.rst b/ddmrp/readme/ROADMAP.rst new file mode 100644 index 000000000..8d914e265 --- /dev/null +++ b/ddmrp/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +The mis_builder `roadmap `_ +and `known issues `_ can +be found on GitHub. \ No newline at end of file diff --git a/ddmrp/readme/USAGE.rst b/ddmrp/readme/USAGE.rst new file mode 100644 index 000000000..5fce1c4cf --- /dev/null +++ b/ddmrp/readme/USAGE.rst @@ -0,0 +1,76 @@ +To easily identify were are you maintaining buffers in your Bill of +Materials, you will need to first provide location information on the Bills +of Materials. + +* Go to *Manufacturing / Products / Bill of Materials* and update the + 'Location' in all the Bill of Materials and associated lines, + indicating where will the parts be placed/used during the manufacturing + process. + +* Print the report 'BOM Structure' to display where in your BOM are you + maintaining buffers, and to identify the Lead Time (LT) of each product, and + Decouple Lead Time (DLT). + + +Buffers +~~~~~~~ + +To list the list of inventory buffers, go to one of the following: +* *Inventory / Master Data / Stock Buffer Planning* +* *Inventory / Master Data / Reordering Rules* + + +Buffer Profiles +~~~~~~~~~~~~~~~ +Buffer profiles make maintenance of buffers easier by grouping them in +profiles. Changes applied to the profiles will be applicable in the +associated buffer calculations. + +* Go to *Inventory / Configuration / Buffer Profiles*. + +The Buffer Profile Lead Time Factor influences the size of the Buffer Green +zone. Items with longer lead times will usually have smaller green zones, which +will translate in more frequent supply order generation. + +* Go to *Inventory / Configuration / Buffer Profile Lead Time Factor* to + chan + +The Buffer Profile Variability Factor influences the size of the Buffer Red +Safety zone. Items with longer lead times will usually have smaller green +zones, which will translate in more frequent supply order generation. + +* Go to *Inventory / Configuration / Buffer Profile Lead Time Factor*. + +Usual factors should range from 0.2 (long lead time) to 0.8 (short lead time). + + +Product attributes +~~~~~~~~~~~~~~~~~~ + +* For manufactured products, go to *Manufacturing / Products* and + update the 'Manufacturing Lead Time' field, available in the tab *Inventory*. +* For purchased products, go to go to *Purchasing / Products* and update the + *Delivery Lead Time* for each vendor, available in tab *Purchase* and section + *Vendors*. + + +ADU Calculation Methods +~~~~~~~~~~~~~~~~~~~~~~~ + +The Average Daily Usage (ADU) defines the frequency of demand of a product in a +certain location. + +#. Go to *Inventory / Configuration / ADU calculation methods*. +#. To create new, indicate a name, calculation method (fixed, past-looking, + future-looking), and the length of period consideration (in days). + +If you do not have prior history of stock moves in your system, it is advised +to use fixed method. If you have past-history of stock moves, best use +past-looking method. + +The ADU is computed every day by default in a background job independently +from the other buffer fields. This computation can be done with less frequency +but It is not recommended to run it less than weekly or more than daily. +Circumstantially, If you need to force the calculation of the ADU go to +*Inventory / Configuration / DDMRP / Run DDMRP* and click on +*Run ADU calculation*. diff --git a/ddmrp/report/__init__.py b/ddmrp/report/__init__.py new file mode 100644 index 000000000..9956baed8 --- /dev/null +++ b/ddmrp/report/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import bom_structure diff --git a/ddmrp/report/bom_structure.py b/ddmrp/report/bom_structure.py new file mode 100644 index 000000000..75b4a2946 --- /dev/null +++ b/ddmrp/report/bom_structure.py @@ -0,0 +1,24 @@ +# © 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class BomStructureReport(models.AbstractModel): + _inherit = 'report.mrp.mrp_bom_structure_report' + + @api.model + def _get_child_vals(self, record, level, qty, uom): + res = super(BomStructureReport, self)._get_child_vals( + record, level, qty, uom) + if record.product_id.bom_ids: + lead_time = record.product_id.produce_delay + else: + lead_time = record.product_id.seller_ids and \ + record.product_id.seller_ids[0].delay or 0.0 + res['is_buffered'] = record.is_buffered + res['has_mto_rule'] = record.has_mto_rule + res['lead_time'] = lead_time or '' + res['dlt'] = record.dlt + return res diff --git a/ddmrp/security/ir.model.access.csv b/ddmrp/security/ir.model.access.csv new file mode 100644 index 000000000..89ef80006 --- /dev/null +++ b/ddmrp/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_adu_calculation_method,product.adu.calculation.method,model_product_adu_calculation_method,stock.group_stock_user,1,0,0,0 +access_product_adu_calculation_method_system,product.adu.calculation.method system,model_product_adu_calculation_method,stock.group_stock_manager,1,1,1,1 +access_stock_buffer_profile,stock.buffer.profile,model_stock_buffer_profile,stock.group_stock_user,1,0,0,0 +access_stock_buffer_profile_system,stock.buffer.profile system,model_stock_buffer_profile,stock.group_stock_manager,1,1,1,1 +access_stock_buffer_profile_lead_time,stock.buffer.profile.lead.time stock user,model_stock_buffer_profile_lead_time,stock.group_stock_user,1,0,0,0 +access_stock_buffer_profile_lead_time_manager,stock.buffer.profile.lead.time stock user,model_stock_buffer_profile_lead_time,stock.group_stock_manager,1,1,1,1 +access_stock_buffer_profile_variability,stock.buffer.profile.variability stock user,model_stock_buffer_profile_variability,stock.group_stock_user,1,0,0,0 +access_stock_buffer_profile_variability_manager,stock.buffer.profile.variability stock user,model_stock_buffer_profile_variability,stock.group_stock_manager,1,1,1,1 diff --git a/ddmrp/security/stock_security.xml b/ddmrp/security/stock_security.xml new file mode 100644 index 000000000..c13b7e5a9 --- /dev/null +++ b/ddmrp/security/stock_security.xml @@ -0,0 +1,33 @@ + + + + + Buffer Profile Variability multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Buffer Profile Lead Time multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Buffer Profile multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Product ADU calculation method multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + diff --git a/ddmrp/static/description/icon.png b/ddmrp/static/description/icon.png new file mode 100644 index 000000000..a0b0ed73a Binary files /dev/null and b/ddmrp/static/description/icon.png differ diff --git a/ddmrp/static/img/has_mto_rule.png b/ddmrp/static/img/has_mto_rule.png new file mode 100644 index 000000000..f568780bb Binary files /dev/null and b/ddmrp/static/img/has_mto_rule.png differ diff --git a/ddmrp/static/img/is_buffered.png b/ddmrp/static/img/is_buffered.png new file mode 100644 index 000000000..a0b0ed73a Binary files /dev/null and b/ddmrp/static/img/is_buffered.png differ diff --git a/ddmrp/static/img/res_partner_eficent-image.jpg b/ddmrp/static/img/res_partner_eficent-image.jpg new file mode 100644 index 000000000..0cb83687f Binary files /dev/null and b/ddmrp/static/img/res_partner_eficent-image.jpg differ diff --git a/ddmrp/tests/__init__.py b/ddmrp/tests/__init__.py new file mode 100644 index 000000000..a1daa74da --- /dev/null +++ b/ddmrp/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_ddmrp diff --git a/ddmrp/tests/test_ddmrp.py b/ddmrp/tests/test_ddmrp.py new file mode 100644 index 000000000..354033bb7 --- /dev/null +++ b/ddmrp/tests/test_ddmrp.py @@ -0,0 +1,931 @@ +# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import odoo.tests.common as common +from odoo import fields +from datetime import datetime, timedelta + + +class TestDdmrp(common.SavepointCase): + + def createEstimatePeriod(self, name, date_from, date_to): + date_range_type = self.env['date.range.type'].create({ + 'name': 'Test Ranges', + }) + data = { + 'name': name, + 'date_start': fields.Date.to_string(date_from), + 'date_end': fields.Date.to_string(date_to), + 'type_id': date_range_type.id, + } + res = self.estimatePeriodModel.create(data) + return res + + @classmethod + def setUpClass(cls): + super(TestDdmrp, cls).setUpClass() + + # Models + cls.productModel = cls.env['product.product'] + cls.bomModel = cls.env['mrp.bom'] + cls.orderpointModel = cls.env['stock.warehouse.orderpoint'] + cls.pickingModel = cls.env['stock.picking'] + cls.quantModel = cls.env['stock.quant'] + cls.estimateModel = cls.env['stock.demand.estimate'] + cls.estimatePeriodModel = cls.env['date.range'] + cls.aducalcmethodModel = cls.env['product.adu.calculation.method'] + cls.locationModel = cls.env['stock.location'] + cls.make_procurement_orderpoint_model =\ + cls.env['make.procurement.orderpoint'] + cls.user_model = cls.env['res.users'] + + # Refs + cls.main_company = cls.env.ref('base.main_company') + cls.warehouse = cls.env.ref('stock.warehouse0') + cls.stock_location = cls.env.ref('stock.stock_location_stock') + cls.location_shelf1 = cls.env.ref('stock.stock_location_components') + cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') + cls.customer_location = cls.env.ref('stock.stock_location_customers') + cls.uom_unit = cls.env.ref('product.product_uom_unit') + cls.buffer_profile_pur = cls.env.ref( + 'ddmrp.stock_buffer_profile_replenish_purchased_medium_medium') + cls.group_stock_manager = cls.env.ref('stock.group_stock_manager') + cls.group_mrp_user = cls.env.ref('mrp.group_mrp_user') + cls.group_change_procure_qty = cls.env.ref( + 'stock_orderpoint_manual_procurement.' + 'group_change_orderpoint_procure_qty') + cls.calendar = cls.env.ref('resource.resource_calendar_std') + + cls.warehouse.calendar_id = cls.calendar + # Create users + cls.user = cls._create_user('user_1', + [cls.group_stock_manager, + cls.group_mrp_user, + cls.group_change_procure_qty]) + + manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture') + cls.productA = cls.productModel.create( + {'name': 'product A', + 'standard_price': 1, + 'type': 'product', + 'uom_id': cls.uom_unit.id, + 'default_code': 'A', + 'route_ids': [(6, 0, manufacture_route.ids)] + }) + cls.bomA = cls.bomModel.create({ + 'product_tmpl_id': cls.productA.product_tmpl_id.id, + 'product_id': cls.productA.id, + }) + + cls.binA = cls.locationModel.create({ + 'usage': 'internal', + 'name': 'Bin A', + 'location_id': cls.location_shelf1.id, + 'company_id': cls.main_company.id + }) + + cls.binB = cls.locationModel.create({ + 'usage': 'internal', + 'name': 'Bin B', + 'location_id': cls.location_shelf1.id, + 'company_id': cls.main_company.id + }) + + cls.locationModel._parent_store_compute() + + cls.quant = cls.quantModel.create( + {'location_id': cls.binA.id, + 'company_id': cls.main_company.id, + 'product_id': cls.productA.id, + 'quantity': 200.0}) + + @classmethod + def _create_user(cls, login, groups): + """ Create a user.""" + group_ids = [group.id for group in groups] + user = \ + cls.user_model.with_context({'no_reset_password': True}).create({ + 'name': 'Test User', + 'login': login, + 'password': 'demo', + 'email': 'test@yourcompany.com', + 'groups_id': [(6, 0, group_ids)] + }) + return user + + def create_pickingoutA(self, date_move, qty): + return self.pickingModel.sudo(self.user).create({ + 'picking_type_id': self.ref('stock.picking_type_out'), + 'location_id': self.binA.id, + 'location_dest_id': self.customer_location.id, + 'move_lines': [ + (0, 0, { + 'name': 'Test move', + 'product_id': self.productA.id, + 'date_expected': date_move, + 'date': date_move, + 'product_uom': self.productA.uom_id.id, + 'product_uom_qty': qty, + 'location_id': self.binA.id, + 'location_dest_id': self.customer_location.id, + })] + }) + + def create_pickinginA(self, date_move, qty): + return self.pickingModel.sudo(self.user).create({ + 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_id': self.supplier_location.id, + 'location_dest_id': self.binA.id, + 'move_lines': [ + (0, 0, { + 'name': 'Test move', + 'product_id': self.productA.id, + 'date_expected': date_move, + 'date': date_move, + 'product_uom': self.productA.uom_id.id, + 'product_uom_qty': qty, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.binA.id, + })] + }) + + def create_pickinginternalA(self, date_move, qty): + return self.pickingModel.sudo(self.user).create({ + 'picking_type_id': self.ref('stock.picking_type_internal'), + 'location_id': self.binA.id, + 'location_dest_id': self.binB.id, + 'move_lines': [ + (0, 0, { + 'name': 'Test move', + 'product_id': self.productA.id, + 'date_expected': date_move, + 'date': date_move, + 'product_uom': self.productA.uom_id.id, + 'product_uom_qty': qty, + 'location_id': self.binA.id, + 'location_dest_id': self.binB.id + })] + }) + + def create_orderpoint_procurement(self, orderpoint): + """Make Procurement from Reordering Rule""" + context = { + 'active_model': 'stock.warehouse.orderpoint', + 'active_ids': orderpoint.ids, + 'active_id': orderpoint.id + } + wizard = self.make_procurement_orderpoint_model.sudo(self.user).\ + with_context(context).create({}) + wizard.make_procurement() + return wizard + + def test_adu_calculation_fixed(self): + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + + to_assert_value = 4 + self.assertEqual(orderpointA.adu, to_assert_value) + + def test_adu_calculation_past_120_days(self): + + method = self.env.ref('ddmrp.adu_calculation_method_past_120') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + + self.assertEqual(orderpointA.adu, 0) + + pickingOuts = self.pickingModel + + days = 30 + date_move = self.calendar.plan_days( + -1 * days - 1, datetime.today()) + pickingOuts += self.create_pickingoutA(date_move, 60) + days = 60 + date_move = self.calendar.plan_days( + -1 * days - 1, datetime.today()) + pickingOuts += self.create_pickingoutA(date_move, 60) + for picking in pickingOuts: + picking.action_confirm() + picking.force_assign() + picking.move_lines.quantity_done = 60 + picking.action_done() + + self.orderpointModel.cron_ddmrp_adu() + to_assert_value = (60 + 60) / 120 + self.assertEqual(orderpointA.adu, to_assert_value) + + def test_adu_calculation_internal_past_120_days(self): + """ + Test that internal moves will not affect ADU calculation. + """ + method = self.env.ref('ddmrp.adu_calculation_method_past_120') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + + self.assertEqual(orderpointA.adu, 0) + + pickingInternals = self.pickingModel + days = 30 + date_move = self.calendar.plan_days( + -1 * days - 1, datetime.today()) + pickingInternals += self.create_pickinginternalA(date_move, 60) + days = 60 + date_move = self.calendar.plan_days( + -1 * days - 1, datetime.today()) + pickingInternals += self.create_pickinginternalA(date_move, 60) + for picking in pickingInternals: + picking.action_confirm() + picking.action_assign() + picking.action_done() + + self.orderpointModel.cron_ddmrp_adu() + + to_assert_value = 0 + self.assertEqual(orderpointA.adu, to_assert_value) + + def test_adu_calculation_future_120_days_actual(self): + method = self.aducalcmethodModel.create({ + 'name': 'Future actual demand (120 days)', + 'method': 'future', + 'use_estimates': False, + 'horizon': 120, + 'company_id': self.main_company.id + }) + + pickingOuts = self.pickingModel + days = 30 + date_move = self.calendar.plan_days( + +1 * days + 1, datetime.today()) + pickingOuts += self.create_pickingoutA(date_move, 60) + days = 60 + date_move = self.calendar.plan_days( + +1 * days + 1, datetime.today()) + pickingOuts += self.create_pickingoutA(date_move, 60) + + pickingOuts.action_confirm() + + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id + }) + self.orderpointModel.cron_ddmrp_adu() + + to_assert_value = (60 + 60) / 120 + self.assertEqual(orderpointA.adu, to_assert_value) + + # Create a move more than 120 days in the future + days = 150 + date_move = self.calendar.plan_days( + +1 * days + 1, datetime.today()) + pickingOuts += self.create_pickingoutA(date_move, 1) + + # The extra move should not affect to the average ADU + self.assertEqual(orderpointA.adu, to_assert_value) + + def test_adu_calculation_future_120_days_estimated(self): + + method = self.env.ref('ddmrp.adu_calculation_method_future_120') + # Create a period of 120 days. + date_from = self.calendar.plan_days(1, datetime.today()).date() + days = 119 + dt = self.calendar.plan_days( + +1 * days + 1, datetime.today()) + date_to = dt.date() + estimate_period_next_120 = self.createEstimatePeriod( + 'test_next_120', date_from, date_to) + + self.estimateModel.create({ + 'date_range_id': estimate_period_next_120.id, + 'product_id': self.productA.id, + 'product_uom_qty': 120, + 'product_uom': self.productA.uom_id.id, + 'location_id': self.stock_location.id + }) + + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id + }) + self.orderpointModel.cron_ddmrp_adu() + + to_assert_value = 120 / 120 + self.assertEqual(orderpointA.adu, to_assert_value) + + def test_qualified_demand_1(self): + """Moves within order spike horizon, outside the threshold but past + or today's demand.""" + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4, + 'adu': 4, + 'order_spike_horizon': 40 + }) + + date_move = datetime.today() + expected_result = orderpointA.order_spike_threshold * 2 + pickingOut1 = self.create_pickingoutA( + date_move, expected_result) + pickingOut1.action_confirm() + self.orderpointModel.cron_ddmrp() + self.assertEqual(orderpointA.qualified_demand, expected_result) + + def test_qualified_demand_2(self): + """Moves within order spike horizon, below threshold. Should have no + effect on the qualified demand.""" + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4, + 'adu': 4, + 'order_spike_horizon': 40 + }) + + date_move = datetime.today() + timedelta(days=10) + self.create_pickingoutA( + date_move, orderpointA.order_spike_threshold - 1) + self.orderpointModel.cron_ddmrp() + + self.assertEqual(orderpointA.qualified_demand, 0) + + def test_qualified_demand_3(self): + """Moves within order spike horizon, above threshold. Should have an + effect on the qualified demand""" + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4, + 'adu': 4, + 'order_spike_horizon': 40 + }) + + date_move = datetime.today() + timedelta(days=10) + self.create_pickingoutA(date_move, + orderpointA.order_spike_threshold * 2) + self.orderpointModel.cron_ddmrp() + + expected_result = orderpointA.order_spike_threshold * 2 + self.assertEqual(orderpointA.qualified_demand, expected_result) + + def test_qualified_demand_4(self): + """ Moves outside of order spike horizon, above threshold. Should + have no effect on the qualified demand""" + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4, + 'adu': 4, + 'order_spike_horizon': 40 + }) + + date_move = datetime.today() + timedelta(days=100) + self.create_pickingoutA(date_move, + orderpointA.order_spike_threshold * 2) + self.orderpointModel.cron_ddmrp() + + expected_result = 0.0 + self.assertEqual(orderpointA.qualified_demand, expected_result) + + def test_qualified_demand_5(self): + """Internal moves within the zone designated by the buffer + should not be considered demand.""" + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'adu_calculation_method': method.id, + 'adu_fixed': 4, + 'adu': 4, + 'order_spike_horizon': 40 + }) + + date_move = datetime.today() + expected_result = 0 + pickingInternal = self.create_pickinginternalA( + date_move, expected_result) + pickingInternal.action_confirm() + self.orderpointModel.cron_ddmrp() + self.assertEqual(orderpointA.qualified_demand, expected_result) + + def _check_red_zone(self, orderpoint, red_base_qty=0.0, red_safety_qty=0.0, + red_zone_qty=0.0): + + # red base_qty = dlt * adu * lead time factor + self.assertEqual(orderpoint.red_base_qty, red_base_qty) + + # red_safety_qty = red_base_qty * variability factor + self.assertEqual(orderpoint.red_safety_qty, red_safety_qty) + + # red_zone_qty = red_base_qty + red_safety_qty + self.assertEqual(orderpoint.red_zone_qty, red_zone_qty) + + def _check_yellow_zone(self, orderpoint, yellow_zone_qty=0.0, + top_of_yellow=0.0): + + # yellow_zone_qty = dlt * adu + self.assertEqual(orderpoint.yellow_zone_qty, yellow_zone_qty) + + # top_of_yellow = yellow_zone_qty + red_zone_qty + self.assertEqual(orderpoint.top_of_yellow, top_of_yellow) + + def _check_green_zone(self, orderpoint, green_zone_oc=0.0, + green_zone_lt_factor=0.0, green_zone_moq=0.0, + green_zone_qty=0.0, top_of_green=0.0): + + # green_zone_oc = order_cycle * adu + self.assertEqual(orderpoint.green_zone_oc, green_zone_oc) + + # green_zone_lt_factor = dlt * adu * lead time factor + self.assertEqual(orderpoint.green_zone_lt_factor, green_zone_lt_factor) + + # green_zone_moq = minimum_order_quantity + self.assertEqual(orderpoint.green_zone_moq, green_zone_moq) + + # green_zone_qty = max(green_zone_oc, green_zone_lt_factor, + # green_zone_moq) + self.assertEqual(orderpoint.green_zone_qty, green_zone_qty) + + # top_of_green = green_zone_qty + yellow_zone_qty + red_zone_qty + self.assertEqual(orderpoint.top_of_green, top_of_green) + + def test_buffer_zones_red(self): + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'lead_days': 10, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + + self._check_red_zone(orderpointA, red_base_qty=20, red_safety_qty=10, + red_zone_qty=30) + + orderpointA.lead_days = 20 + + self._check_red_zone(orderpointA, red_base_qty=40, red_safety_qty=20, + red_zone_qty=60) + + orderpointA.buffer_profile_id.lead_time_id.factor = 1 + + self._check_red_zone(orderpointA, red_base_qty=80, red_safety_qty=40, + red_zone_qty=120) + + orderpointA.buffer_profile_id.variability_id.factor = 1 + + self._check_red_zone(orderpointA, red_base_qty=80, red_safety_qty=80, + red_zone_qty=160) + + orderpointA.adu_fixed = 2 + self.orderpointModel.cron_ddmrp_adu() + + self._check_red_zone(orderpointA, red_base_qty=40, red_safety_qty=40, + red_zone_qty=80) + + def test_buffer_zones_yellow(self): + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'lead_days': 10, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + + self._check_yellow_zone(orderpointA, yellow_zone_qty=40.0, + top_of_yellow=70.0) + + orderpointA.lead_days = 20 + + self._check_yellow_zone(orderpointA, yellow_zone_qty=80.0, + top_of_yellow=140.0) + + orderpointA.adu_fixed = 2 + self.orderpointModel.cron_ddmrp_adu() + + self._check_yellow_zone(orderpointA, yellow_zone_qty=40.0, + top_of_yellow=70.0) + + orderpointA.buffer_profile_id.lead_time_id.factor = 1 + orderpointA.buffer_profile_id.variability_id.factor = 1 + + self._check_yellow_zone(orderpointA, yellow_zone_qty=40.0, + top_of_yellow=120.0) + + def test_procure_recommended(self): + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'lead_days': 10, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + orderpointA._calc_adu() + + self.orderpointModel.cron_ddmrp() + # Now we prepare the shipment of 150 + date_move = datetime.today() + pickingOut = self.create_pickingoutA(date_move, 150) + pickingOut.action_confirm() + pickingOut.force_assign() + pickingOut.move_lines.quantity_done = 150 + pickingOut.action_done() + self.orderpointModel.cron_ddmrp() + + expected_value = 40.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + # Now we change the net flow position. + # Net Flow position = 200 - 150 + 10 = 60 + self.quantModel.create( + {'location_id': self.binA.id, + 'company_id': self.main_company.id, + 'product_id': self.productA.id, + 'quantity': 10.0}) + self.orderpointModel.cron_ddmrp() + + expected_value = 30.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + # Now we change the top of green. + # red base = dlt * adu * lead time factor = 10 * 2 * 0.5 = 10 + # red safety = red_base * variability factor = 10 * 0.5 = 5 + # red zone = red_base + red_safety = 10 + 5 = 15 + # Top Of Red (TOR) = red zone = 15 + # yellow zone = dlt * adu = 10 * 2 = 20 + # Top Of Yellow (TOY) = TOR + yellow zone = 15 + 20 = 35 + # green_zone_oc = order_cycle * adu = 0 * 4 = 0 + # green_zone_lt_factor = dlt * adu * lead time factor =10 + # green_zone_moq = minimum_order_quantity = 0 + # green_zone_qty = max(green_zone_oc, green_zone_lt_factor, + # green_zone_moq) = max(0, 10, 0) = 10 + # Top Of Green (TOG) = TOY + green_zone_qty = 35 + 10 = 45 + orderpointA.adu_fixed = 2 + self.orderpointModel.cron_ddmrp_adu() + self.orderpointModel.cron_ddmrp() + + expected_value = 0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + orderpointA.buffer_profile_id.lead_time_id.factor = 1 + # Now we change the top of green. + # red base = dlt * adu * lead time factor = 10 * 2 * 1 = 20 + # red safety = red_base * variability factor = 20 * 0.5 = 10 + # red zone = red_base + red_safety = 20 + 10 = 30 + # Top Of Red (TOR) = red zone = 25 + # yellow zone = dlt * adu = 10 * 2 = 20 + # Top Of Yellow (TOY) = TOR + yellow zone = 30 + 20 = 50 + # green_zone_oc = order_cycle * adu = 0 * 4 = 0 + # green_zone_lt_factor = dlt * adu * lead time factor = 20 + # green_zone_moq = minimum_order_quantity = 0 + # green_zone_qty = max(green_zone_oc, green_zone_lt_factor, + # green_zone_moq) = max(0, 20, 0) = 20 + # Top Of Green (TOG) = TOY + green_zone_qty = 50 + 20 = 70 + expected_value = 0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + orderpointA.minimum_order_quantity = 40 + # Now we change the top of green. + # red base = dlt * adu * lead time factor = 10 * 2 * 1 = 20 + # red safety = red_base * variability factor = 20 * 0.5 = 10 + # red zone = red_base + red_safety = 20 + 10 = 30 + # Top Of Red (TOR) = red zone = 25 + # yellow zone = dlt * adu = 10 * 2 = 20 + # Top Of Yellow (TOY) = TOR + yellow zone = 30 + 20 = 50 + # green_zone_oc = order_cycle * adu = 0 * 4 = 0 + # green_zone_lt_factor = dlt * adu * lead time factor = 20 + # green_zone_moq = minimum_order_quantity = 0 + # green_zone_qty = max(green_zone_oc, green_zone_lt_factor, + # green_zone_moq) = max(0, 20, 40) = 40 + # Top Of Green (TOG) = TOY + green_zone_qty = 50 + 40 = 90 + expected_value = 0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + def test_buffer_zones_all(self): + method = self.env.ref('ddmrp.adu_calculation_method_fixed') + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': self.productA.id, + 'location_id': self.stock_location.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + 'qty_multiple': 0.0, + 'lead_days': 10, + 'adu_calculation_method': method.id, + 'adu_fixed': 4 + }) + self.orderpointModel.cron_ddmrp_adu() + # red base = dlt * adu * lead time factor = 10 * 4 * 0.5 = 20 + # red safety = red_base * variability factor = 20 * 0.5 = 10 + # red zone = red_base + red_safety = 20 + 10 = 30 + # Top Of Red (TOR) = red zone = 30 + self._check_red_zone(orderpointA, red_base_qty=20.0, + red_safety_qty=10.0, + red_zone_qty=30.0) + + # yellow zone = dlt * adu = 10 * 4 = 40 + # Top Of Yellow (TOY) = TOR + yellow zone = 30 + 40 = 70 + self._check_yellow_zone(orderpointA, yellow_zone_qty=40.0, + top_of_yellow=70.0) + + # green_zone_oc = order_cycle * adu = 0 * 4 = 0 + # green_zone_lt_factor = dlt * adu * lead time factor = 20 + # green_zone_moq = minimum_order_quantity = 0 + # green_zone_qty = max(green_zone_oc, green_zone_lt_factor, + # green_zone_moq) = max(0, 20, 0) = 20 + # Top Of Green (TOG) = TOY + green_zone_qty = 70 + 20 = 90 + self._check_green_zone(orderpointA, green_zone_oc=0.0, + green_zone_lt_factor=20.0, green_zone_moq=0.0, + green_zone_qty=20.0, top_of_green=90.0) + + self.orderpointModel.cron_ddmrp() + + # Net Flow Position = on hand + incoming - qualified demand = 200 + 0 + # - 0 = 200 + expected_value = 200.0 + self.assertEqual(orderpointA.net_flow_position, expected_value) + + # Net Flow Position Percent = (Net Flow Position / TOG)*100 = ( + # 200/90)*100 = 55.56 % + expected_value = 222.22 + self.assertEqual(orderpointA.net_flow_position_percent, expected_value) + + # Planning priority level + expected_value = '3_green' + self.assertEqual(orderpointA.planning_priority_level, expected_value) + + # On hand/TOR = (200 / 30) * 100 = 666.67 + expected_value = 666.67 + self.assertEqual(orderpointA.on_hand_percent, expected_value) + + # Execution priority level + expected_value = '3_green' + self.assertEqual(orderpointA.execution_priority_level, expected_value) + + # Procure recommended quantity = TOG - Net Flow Position if > 0 = 90 + # - 200 => 0.0 + expected_value = 0.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + # Now we prepare the shipment of 150 + date_move = datetime.today() + pickingOut = self.create_pickingoutA(date_move, 150) + pickingOut.action_confirm() + + self.orderpointModel.cron_ddmrp() + + # Net Flow Position = on hand + incoming - qualified demand = 200 + 0 + # - 150 = 50 + expected_value = 50.0 + self.assertEqual(orderpointA.net_flow_position, expected_value) + + # Net Flow Position Percent = (Net Flow Position / TOG)*100 = ( + # 50/90)*100 = 55.56 % + expected_value = 55.56 + self.assertEqual(orderpointA.net_flow_position_percent, expected_value) + + # Planning priority level + expected_value = '2_yellow' + self.assertEqual(orderpointA.planning_priority_level, expected_value) + + # On hand/TOR = (200 / 30) * 100 = 666.67 + expected_value = 666.67 + self.assertEqual(orderpointA.on_hand_percent, expected_value) + + # Execution priority level + expected_value = '3_green' + self.assertEqual(orderpointA.execution_priority_level, expected_value) + + # Now we confirm the shipment of the 150 + pickingOut.action_assign() + pickingOut.force_assign() + pickingOut.move_lines.quantity_done = 150 + pickingOut.action_done() + self.orderpointModel.cron_ddmrp() + + # On hand/TOR = (50 / 30) * 100 = 166.67 + expected_value = 166.67 + self.assertEqual(orderpointA.on_hand_percent, expected_value) + + # Execution priority level. Considering that the quantity available + # unrestricted is 50, and top of red is 30, we are in the green on + # hand zone. + expected_value = '3_green' + self.assertEqual(orderpointA.execution_priority_level, expected_value) + + # Procure recommended quantity = TOG - Net Flow Position if > 0 = 90 + # - 50 => 40.0 + expected_value = 40.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + # Now we ship them + pickingOut.action_done() + self.orderpointModel.cron_ddmrp() + + # Net Flow Position = on hand + incoming - qualified demand = 200 + 0 + # - 150 = 50 + expected_value = 50.0 + self.assertEqual(orderpointA.net_flow_position, expected_value) + + # Net Flow Position Percent = (Net Flow Position / TOG)*100 = ( + # 50/90)*100 = 55.56 % + expected_value = 55.56 + self.assertEqual(orderpointA.net_flow_position_percent, expected_value) + + # Planning priority level + expected_value = '2_yellow' + self.assertEqual(orderpointA.planning_priority_level, expected_value) + + # On hand/TOR = (50 / 30) * 100 = 166.67 + expected_value = 166.67 + self.assertEqual(orderpointA.on_hand_percent, expected_value) + + # Execution priority level + expected_value = '3_green' + self.assertEqual(orderpointA.execution_priority_level, expected_value) + + # Procure recommended quantity = TOG - Net Flow Position if > 0 = 90 + # - 50 => 40.0 + expected_value = 40.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + # Now we create a procurement order, based on the procurement + # recommendation + self.create_orderpoint_procurement(orderpointA) + # should have generated a manufacturing order + self.assertEqual(len(orderpointA.mrp_production_ids), 1) + self.assertEqual(orderpointA.mrp_production_ids.product_qty, 40.0) + + # We expect that the procurement recommendation is now 0 + expected_value = 0.0 + self.assertEqual(orderpointA.procure_recommended_qty, expected_value) + + def test_bom_orderpoint(self): + # Check if is_buffered and orderpoint_id are not set + self.assertEqual(self.bomA.is_buffered, False) + self.assertEqual(len(self.bomA.orderpoint_id), 0) + product = self.productA + orderpointA = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': product.id, + 'warehouse_id': self.warehouse.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + }) + # Create a new bom to trigger the compute functions + bomB = self.bomModel.create({ + 'product_id': product.id, + 'product_tmpl_id': product.product_tmpl_id.id, + }) + # Check if is_buffered and orderpoint_id are set + self.assertEqual(bomB.is_buffered, True) + self.assertEqual(bomB.orderpoint_id, orderpointA) + # Create another orderpoint with a location, then change the bom + # location + orderpointB = self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_pur.id, + 'product_id': product.id, + 'warehouse_id': self.warehouse.id, + 'location_id': self.supplier_location.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + }) + bomB.location_id = self.supplier_location.id + self.assertEqual(bomB.is_buffered, True) + self.assertEqual(bomB.orderpoint_id, orderpointB) + bomC = self.env['mrp.bom'].create({ + 'product_tmpl_id': product.product_tmpl_id.id + }) + self.assertEqual(bomC.is_buffered, True) + self.assertEqual(bomC.orderpoint_id, orderpointA) + + +class TestBomDLT(common.TransactionCase): + def setUp(self): + super(TestBomDLT, self).setUp() + self.orderpointModel = self.env['stock.warehouse.orderpoint'] + self.buffer_profile_mmm = self.env.ref( + 'ddmrp.stock_buffer_profile_replenish_manufactured_medium_medium') + self.warehouse = self.env.ref('stock.warehouse0') + self.stock_location = self.env.ref('stock.stock_location_stock') + + def test_01_bom_dlt_computation(self): + """Tests that DLT computation is correct adding/removing buffers.""" + bom_fp01 = self.env.ref('ddmrp.mrp_bom_fp01') + self.assertEqual(bom_fp01.dlt, 22) + # Remove RM-01 buffer: + orderpoint_rm01 = self.env.ref('ddmrp.stock_warehouse_orderpoint_rm01') + orderpoint_rm01.unlink() + self.assertEqual(bom_fp01.dlt, 33.0) + + def test_02_bom_dlt_computation(self): + # Add buffer on AS-01 + product_as01 = self.env.ref('ddmrp.product_product_as01') + self.orderpointModel.create({ + 'buffer_profile_id': self.buffer_profile_mmm.id, + 'product_id': product_as01.id, + 'warehouse_id': self.warehouse.id, + 'location_id': self.stock_location.id, + 'product_min_qty': 0.0, + 'product_max_qty': 0.0, + }) + bom_fp01 = self.env.ref('ddmrp.mrp_bom_fp01') + self.assertEqual(bom_fp01.dlt, 2.0) diff --git a/ddmrp/views/mrp_bom_view.xml b/ddmrp/views/mrp_bom_view.xml new file mode 100644 index 000000000..95c49f7cc --- /dev/null +++ b/ddmrp/views/mrp_bom_view.xml @@ -0,0 +1,26 @@ + + + + + + mrp.bom.form - mrp_bom_location + mrp.bom + + + + + + + + + + + + + + + + + + diff --git a/ddmrp/views/mrp_production_view.xml b/ddmrp/views/mrp_production_view.xml new file mode 100644 index 000000000..dae0d16cb --- /dev/null +++ b/ddmrp/views/mrp_production_view.xml @@ -0,0 +1,45 @@ + + + + + mrp.production.tree + mrp.production + + + + + + + + + + + mrp.production.select + mrp.production + + + + + + + + + + + + + + diff --git a/ddmrp/views/product_adu_calculation_method_view.xml b/ddmrp/views/product_adu_calculation_method_view.xml new file mode 100644 index 000000000..fe65e75b5 --- /dev/null +++ b/ddmrp/views/product_adu_calculation_method_view.xml @@ -0,0 +1,65 @@ + + + + + product.adu.calculation.method.tree + product.adu.calculation.method + + + + + + + + product.adu.calculation.method.form + product.adu.calculation.method + +
+ + + + + + + + + + +
+
+
+ + + product.adu.calculation.method.search + product.adu.calculation.method + + + + + + + + + ADU calculation methods + ir.actions.act_window + product.adu.calculation.method + form + tree,form + + + + + +
diff --git a/ddmrp/views/purchase_order_line_view.xml b/ddmrp/views/purchase_order_line_view.xml new file mode 100644 index 000000000..73270b948 --- /dev/null +++ b/ddmrp/views/purchase_order_line_view.xml @@ -0,0 +1,103 @@ + + + + + + purchase.order.form - ddmrp + purchase.order + + + + + + + + + + + + + + purchase.order.line.tree - ddmrp + purchase.order.line + + + + + + + + + + + + + + + + + purchase.order.line.search - ddmrp + purchase.order.line + + + + + + + + + + + + + + + request.quotation.select - ddmrp + purchase.order + + + + + + + + + + + + PO lines On-Hand Status + purchase.order.line + + [('orderpoint_ids','!=',False), ('execution_priority_level','!=',False)] + tree + + + + + diff --git a/ddmrp/views/report_mrpbomstructure.xml b/ddmrp/views/report_mrpbomstructure.xml new file mode 100644 index 000000000..9adfd6400 --- /dev/null +++ b/ddmrp/views/report_mrpbomstructure.xml @@ -0,0 +1,57 @@ + + + + diff --git a/ddmrp/views/stock_buffer_profile_lead_time_view.xml b/ddmrp/views/stock_buffer_profile_lead_time_view.xml new file mode 100644 index 000000000..256ab3fff --- /dev/null +++ b/ddmrp/views/stock_buffer_profile_lead_time_view.xml @@ -0,0 +1,77 @@ + + + + + stock.buffer.profile.lead.time.form + stock.buffer.profile.lead.time + +
+ +
+
+ + + + + + + + +
+
+
+
+ + + stock.buffer.profile.lead.time.tree + stock.buffer.profile.lead.time + + + + + + + + + + + stock.buffer.profile.lead.time.search + stock.buffer.profile.lead.time + + + + + + + + + + + + Buffer Profile Lead Time Factor + ir.actions.act_window + stock.buffer.profile.lead.time + form + tree,form + + +

+ Click to start a new buffer profile lead time factor +

+
+
+ + + +
diff --git a/ddmrp/views/stock_buffer_profile_variability_view.xml b/ddmrp/views/stock_buffer_profile_variability_view.xml new file mode 100644 index 000000000..21bb670cf --- /dev/null +++ b/ddmrp/views/stock_buffer_profile_variability_view.xml @@ -0,0 +1,77 @@ + + + + + stock.buffer.profile.variability.form + stock.buffer.profile.variability + +
+ +
+
+ + + + + + + + +
+
+
+
+ + + stock.buffer.profile.variability.tree + stock.buffer.profile.variability + + + + + + + + + + + stock.buffer.profile.variability.search + stock.buffer.profile.variability + + + + + + + + + + + + Buffer Profile Variability Factor + ir.actions.act_window + stock.buffer.profile.variability + form + tree,form + + +

+ Click to start a new buffer profile variability factor +

+
+
+ + + +
diff --git a/ddmrp/views/stock_buffer_profile_view.xml b/ddmrp/views/stock_buffer_profile_view.xml new file mode 100644 index 000000000..b6b23eea9 --- /dev/null +++ b/ddmrp/views/stock_buffer_profile_view.xml @@ -0,0 +1,93 @@ + + + + + stock.buffer.profile.form + stock.buffer.profile + +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + + stock.buffer.profile.tree + stock.buffer.profile + + + + + + + + + + + + + stock.buffer.profile.search + stock.buffer.profile + + + + + + + + + + + + + + + + Buffer Profiles + ir.actions.act_window + stock.buffer.profile + form + tree,form + + +

+ Click to start a new buffer profile +

+
+
+ + + + + +
diff --git a/ddmrp/views/stock_move_views.xml b/ddmrp/views/stock_move_views.xml new file mode 100644 index 000000000..05dd96070 --- /dev/null +++ b/ddmrp/views/stock_move_views.xml @@ -0,0 +1,30 @@ + + + + + + + + stock.move.tree + stock.move + + + + + + + + + + stock.move.form + stock.move + + + + False + + + + + diff --git a/ddmrp/views/stock_warehouse_orderpoint_view.xml b/ddmrp/views/stock_warehouse_orderpoint_view.xml new file mode 100644 index 000000000..beb86e865 --- /dev/null +++ b/ddmrp/views/stock_warehouse_orderpoint_view.xml @@ -0,0 +1,261 @@ + + + + + stock.warehouse.orderpoint.tree + stock.warehouse.orderpoint + + + + + + + + + + + + + + + + + + + + + + + + + stock.warehouse.orderpoint.search + stock.warehouse.orderpoint + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stock.warehouse.orderpoint.form + stock.warehouse.orderpoint + + + + + + + + + + + + +
+ +
+
+ + + + + + + + + +
+

+ The green zone determines the average order frequency and the order size. It + is determined as the maximum of the following three factors: Minimum Order Cycle, + Lead Time Factor and Minimum Order Quantity. +

+
+
+ + + + + + +
+

+ The yellow zone represents the + stock required to cover a full lead time. +

+
+
+ + + +
+

+ The red zone is the embedded safety in + the buffer. The larger the variability + associated with the product, the larger + the red zone will be. It is composed of + two sub-zones: Red base and red safety. +

+
+
+ + + + + + + + + + + + + + + + stock.warehouse.orderpoint.planner.tree + stock.warehouse.orderpoint + 50 + + + + + + + + + + + +