diff --git a/ddmrp_report_part_flow_index/README.rst b/ddmrp_report_part_flow_index/README.rst new file mode 100644 index 000000000..06b3e401a --- /dev/null +++ b/ddmrp_report_part_flow_index/README.rst @@ -0,0 +1,139 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================ +DDMRP Report Part Flow Index +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:33d753fbb5ac790cf572c9e97a54a10c723452eb9b22b995f3cfdde935e7766f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fddmrp-lightgray.png?logo=github + :target: https://github.com/OCA/ddmrp/tree/19.0/ddmrp_report_part_flow_index + :alt: OCA/ddmrp +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/ddmrp-19-0/ddmrp-19-0-ddmrp_report_part_flow_index + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/ddmrp&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Classifies DDMRP stock buffers by their order frequency ("flow"), into +user-defined Flow Index Groups. + +Two complementary views are provided: + +- **Plan Flow Index** -- the *planned* order frequency derived from the + buffer sizing (Green Zone / ADU). +- **Actual Flow Index** -- the *observed* order frequency derived from + the real completed replenishments over the past horizon. + +Comparing planned vs actual flow highlights buffers whose real ordering +behaviour diverges from their sizing. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +- Go to *Inventory > Configuration > DDMRP > Flow Index Group* to define + the groups (a name and an order-frequency range each). An unset bound + is treated as open-ended. + +Buffers are assigned to a group automatically -- the **planned** and +**actual** Flow Index Group are computed from the buffer's order +frequency against those ranges. The planned value updates whenever the +buffer's green zone or ADU change; the actual value is refreshed +together with the buffer (the scheduled *Buffer refresh* action, or the +**Refresh Buffer** button). Changes to a group's range therefore take +effect on the next buffer refresh. + +Usage +===== + +- Go to *Inventory > Reporting > Parts Plan Flow Index* to classify + buffers by their **planned** order frequency (Green Zone / ADU). +- Go to *Inventory > Reporting > Parts Actual Flow Index* to classify + buffers by their **actual** order frequency (completed replenishments + observed over the past horizon). + +Both reports group buffers by Flow Index Group and expose the planned +and actual figures side by side, so a divergence between planned and +actual flow flags a buffer that may be mis-sized. + +Changelog +========= + +11.0.1.0.0 (2018-08-01) +----------------------- + +- Start of the history + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ForgeFlow + +Contributors +------------ + +- Jordi Ballester Alomar +- Lois Rilo Antelo + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-JordiBForgeFlow| image:: https://github.com/JordiBForgeFlow.png?size=40px + :target: https://github.com/JordiBForgeFlow + :alt: JordiBForgeFlow +.. |maintainer-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px + :target: https://github.com/LoisRForgeFlow + :alt: LoisRForgeFlow + +Current `maintainers `__: + +|maintainer-JordiBForgeFlow| |maintainer-LoisRForgeFlow| + +This module is part of the `OCA/ddmrp `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ddmrp_report_part_flow_index/__init__.py b/ddmrp_report_part_flow_index/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/ddmrp_report_part_flow_index/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/ddmrp_report_part_flow_index/__manifest__.py b/ddmrp_report_part_flow_index/__manifest__.py new file mode 100644 index 000000000..9d417ada7 --- /dev/null +++ b/ddmrp_report_part_flow_index/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2017-24 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "DDMRP Report Part Flow Index", + "summary": "Provides the DDMRP Parts Flow Index Report", + "version": "19.0.2.0.0", + "development_status": "Beta", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "maintainers": ["JordiBForgeFlow", "LoisRForgeFlow"], + "website": "https://github.com/OCA/ddmrp", + "category": "Warehouse Management", + "depends": [ + "ddmrp", + ], + "data": [ + "security/ir.model.access.csv", + "reports/report_ddmrp_part_plan_flow_index_views.xml", + "views/ddmrp_flow_index_group_views.xml", + "views/stock_buffer_view.xml", + ], + "license": "LGPL-3", + "installable": True, +} diff --git a/ddmrp_report_part_flow_index/i18n/ddmrp_report_part_flow_index.pot b/ddmrp_report_part_flow_index/i18n/ddmrp_report_part_flow_index.pot new file mode 100644 index 000000000..3375ea43c --- /dev/null +++ b/ddmrp_report_part_flow_index/i18n/ddmrp_report_part_flow_index.pot @@ -0,0 +1,174 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ddmrp_report_part_flow_index +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__active +msgid "Active" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_form +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_search +msgid "Archived" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__adu +msgid "Average Daily Usage (ADU)" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__buffer_id +msgid "Buffer" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__company_id +msgid "Company" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__create_uid +msgid "Created by" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__create_date +msgid "Created on" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.actions.act_window,name:ddmrp_report_part_flow_index.action_ddmrp_flow_index_group +#: model:ir.model,name:ddmrp_report_part_flow_index.model_ddmrp_flow_index_group +msgid "DDMRP Flow Index Group" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__display_name +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__display_name +msgid "Display Name" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__flow_index_group_id +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_stock_buffer__flow_index_group_id +#: model:ir.ui.menu,name:ddmrp_report_part_flow_index.menu_ddmrp_flow_index_group +msgid "Flow Index Group" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__green_zone_qty +msgid "Green Zone Qty" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__id +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__id +msgid "ID" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__location_id +msgid "Location" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_form +msgid "Log a description..." +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__lower_range +msgid "Lower Range" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,help:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__lower_range +msgid "Lower range used to assign in stock buffer" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__name +msgid "Name" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency +msgid "Order Frequency" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency_group +msgid "Order Frequency Group" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency_group_count +msgid "Order Frequency Group Count" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.actions.act_window,name:ddmrp_report_part_flow_index.action_report_part_plan_flow_index_template +#: model:ir.ui.menu,name:ddmrp_report_part_flow_index.menu_report_part_plan_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_filter +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_graph +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_pivot +msgid "Parts Plan Flow Index" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__product_id +msgid "Product" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model,name:ddmrp_report_part_flow_index.model_report_ddmrp_part_plan_flow_index +msgid "Report DDMRP Plan Flow Index" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__sequence +msgid "Sequence" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model,name:ddmrp_report_part_flow_index.model_stock_buffer +msgid "Stock Buffer" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__summary +msgid "Summary" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__upper_range +msgid "Upper Range" +msgstr "" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,help:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__upper_range +msgid "Upper range used to assign in stock buffer" +msgstr "" diff --git a/ddmrp_report_part_flow_index/i18n/it.po b/ddmrp_report_part_flow_index/i18n/it.po new file mode 100644 index 000000000..5be413e20 --- /dev/null +++ b/ddmrp_report_part_flow_index/i18n/it.po @@ -0,0 +1,186 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ddmrp_report_part_flow_index +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-01-15 11:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__active +msgid "Active" +msgstr "Attivo" + +#. module: ddmrp_report_part_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_form +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_search +msgid "Archived" +msgstr "In archivio" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__adu +msgid "Average Daily Usage (ADU)" +msgstr "Utilizzo medio giornaliero (UMG)" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__buffer_id +msgid "Buffer" +msgstr "Buffer" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__company_id +msgid "Company" +msgstr "Azienda" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: ddmrp_report_part_flow_index +#: model:ir.actions.act_window,name:ddmrp_report_part_flow_index.action_ddmrp_flow_index_group +#: model:ir.model,name:ddmrp_report_part_flow_index.model_ddmrp_flow_index_group +msgid "DDMRP Flow Index Group" +msgstr "Gruppo indice flusso DDMRP" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__display_name +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__flow_index_group_id +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_stock_buffer__flow_index_group_id +#: model:ir.ui.menu,name:ddmrp_report_part_flow_index.menu_ddmrp_flow_index_group +msgid "Flow Index Group" +msgstr "Gruppo indice flusso" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__green_zone_qty +msgid "Green Zone Qty" +msgstr "Q.tà zona verde" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__id +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__id +msgid "ID" +msgstr "ID" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__location_id +msgid "Location" +msgstr "Ubicazione" + +#. module: ddmrp_report_part_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_ddmrp_flow_index_group_form +msgid "Log a description..." +msgstr "Registra una descrizione..." + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__lower_range +msgid "Lower Range" +msgstr "Intervallo inferiore" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,help:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__lower_range +msgid "Lower range used to assign in stock buffer" +msgstr "Intervallo inferiore utilizzato per assegnare buffer di magazzino" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__name +msgid "Name" +msgstr "Nome" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency +msgid "Order Frequency" +msgstr "Frequenza ordine" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency_group +msgid "Order Frequency Group" +msgstr "Gruppo frequenza ordine" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__order_frequency_group_count +msgid "Order Frequency Group Count" +msgstr "Conteggio gruppo frequenza ordine" + +#. module: ddmrp_report_part_flow_index +#: model:ir.actions.act_window,name:ddmrp_report_part_flow_index.action_report_part_plan_flow_index_template +#: model:ir.ui.menu,name:ddmrp_report_part_flow_index.menu_report_part_plan_flow_index +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_filter +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_graph +#: model_terms:ir.ui.view,arch_db:ddmrp_report_part_flow_index.view_part_plan_flow_index_pivot +msgid "Parts Plan Flow Index" +msgstr "Indice flusso piano parti" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_report_ddmrp_part_plan_flow_index__product_id +msgid "Product" +msgstr "Prodotto" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model,name:ddmrp_report_part_flow_index.model_report_ddmrp_part_plan_flow_index +msgid "Report DDMRP Plan Flow Index" +msgstr "Resoconto DDMRP indice flusso piano" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model,name:ddmrp_report_part_flow_index.model_stock_buffer +msgid "Stock Buffer" +msgstr "Buffer di materiale" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__summary +msgid "Summary" +msgstr "Riepilogo" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,field_description:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__upper_range +msgid "Upper Range" +msgstr "Intervallo superiore" + +#. module: ddmrp_report_part_flow_index +#: model:ir.model.fields,help:ddmrp_report_part_flow_index.field_ddmrp_flow_index_group__upper_range +msgid "Upper range used to assign in stock buffer" +msgstr "Intervallo superiore utilizzato per assegnare buffer di magazzino" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#~ msgid "ddmrp.flow.index.group" +#~ msgstr "ddmrp.flow.index.group" + +#~ msgid "report.ddmrp.part.plan.flow.index" +#~ msgstr "report.ddmrp.part.plan.flow.index" diff --git a/ddmrp_report_part_flow_index/models/__init__.py b/ddmrp_report_part_flow_index/models/__init__.py new file mode 100644 index 000000000..3d20cc617 --- /dev/null +++ b/ddmrp_report_part_flow_index/models/__init__.py @@ -0,0 +1,2 @@ +from . import ddmrp_flow_index_group +from . import stock_buffer diff --git a/ddmrp_report_part_flow_index/models/ddmrp_flow_index_group.py b/ddmrp_report_part_flow_index/models/ddmrp_flow_index_group.py new file mode 100644 index 000000000..bdb81477a --- /dev/null +++ b/ddmrp_report_part_flow_index/models/ddmrp_flow_index_group.py @@ -0,0 +1,34 @@ +# Copyright 2017-24 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models + + +class DdmrpFlowIndexGroup(models.Model): + _name = "ddmrp.flow.index.group" + _order = "sequence, id" + _description = "DDMRP Flow Index Group" + + name = fields.Char(required=True) + summary = fields.Text() + active = fields.Boolean(default=True) + lower_range = fields.Float(help="Lower range used to assign in stock buffer") + upper_range = fields.Float(help="Upper range used to assign in stock buffer") + sequence = fields.Integer(required=True) + + def _match(self, value): + """Return the first group (by sequence) in ``self`` whose range contains + ``value``. ``self`` is expected to be pre-ordered by sequence. An unset + bound (0.0) is treated as open-ended on that side.""" + for group in self: + lower, upper = group.lower_range, group.upper_range + if lower and upper: + if lower <= value <= upper: + return group + elif upper: + if value <= upper: + return group + elif lower: + if value >= lower: + return group + return self.browse() diff --git a/ddmrp_report_part_flow_index/models/stock_buffer.py b/ddmrp_report_part_flow_index/models/stock_buffer.py new file mode 100644 index 000000000..1b6856b2e --- /dev/null +++ b/ddmrp_report_part_flow_index/models/stock_buffer.py @@ -0,0 +1,118 @@ +# Copyright 2017-24 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from datetime import timedelta + +from odoo import api, fields, models + +# Fallback observation window (working days) for the actual flow index when the +# buffer's ADU method defines no past horizon (e.g. fixed ADU). +DEFAULT_ACTUAL_HORIZON = 365 + + +def _round_half_up(value): + """Match PostgreSQL ``round()`` (half away from zero) for non-negative values, + instead of Python's banker's rounding.""" + return int(value + 0.5) + + +class StockBuffer(models.Model): + _inherit = "stock.buffer" + + # --- Planned flow index (derived from buffer sizing, kept up to date by the + # ORM whenever the green zone or ADU change) --- + order_frequency = fields.Float( + compute="_compute_order_frequency", + store=True, + digits="Product Unit of Measure", + help="Planned order cycle, in days of ADU: Green Zone / ADU.", + ) + order_frequency_group = fields.Integer( + compute="_compute_order_frequency", + store=True, + help="Rounded planned order cycle, used to assign the Flow Index Group.", + ) + flow_index_group_id = fields.Many2one( + "ddmrp.flow.index.group", + string="Flow Index Group", + compute="_compute_flow_index_group_id", + store=True, + readonly=True, + ) + + # --- Actual flow index (derived from real replenishment history; refreshed by + # cron_actions, like the ADU) --- + actual_order_count = fields.Integer( + string="Actual Replenishments", + readonly=True, + help="Completed replenishments received over the observation window.", + ) + actual_order_frequency = fields.Float( + readonly=True, + digits="Product Unit of Measure", + help="Observed order cycle, in days: observation window / completed " + "replenishments.", + ) + actual_order_frequency_group = fields.Integer(readonly=True) + actual_flow_index_group_id = fields.Many2one( + "ddmrp.flow.index.group", + string="Actual Flow Index Group", + readonly=True, + ) + + def _flow_index_groups(self): + """Flow index groups ordered by sequence, ready for range matching.""" + return self.env["ddmrp.flow.index.group"].search([], order="sequence, id") # pylint: disable=no-search-all + + @api.depends("green_zone_qty", "adu") + def _compute_order_frequency(self): + for rec in self: + freq = rec.green_zone_qty / rec.adu if rec.adu else 0.0 + rec.order_frequency = freq + rec.order_frequency_group = _round_half_up(freq) + + @api.depends("order_frequency_group") + def _compute_flow_index_group_id(self): + groups = self._flow_index_groups() + for rec in self: + rec.flow_index_group_id = groups._match(rec.order_frequency_group) + + def _past_incoming_moves_domain(self, date_from, date_to, locations): + """Completed replenishment moves into the buffer locations (the supply-side + mirror of ``_past_moves_domain``).""" + self.ensure_one() + return [ + ("state", "=", "done"), + ("location_dest_id", "in", locations.ids), + ("location_id", "not in", locations.ids), + ("product_id", "=", self.product_id.id), + ("date", ">=", date_from), + ("date", "<=", date_to), + ] + + def _calc_actual_flow_index(self): + groups = self._flow_index_groups() + today = fields.Date.context_today(self) + for rec in self: + horizon = rec._get_horizon_adu_past_demand() or DEFAULT_ACTUAL_HORIZON + date_to = today - timedelta(days=1) + date_from = today - timedelta(days=horizon) + locations = ( + rec.env["stock.location"] + .with_context(active_test=False) + .search([("id", "child_of", rec.location_id.ids)]) + ) + count = rec.env["stock.move"].search_count( + rec._past_incoming_moves_domain(date_from, date_to, locations) + ) + freq = (horizon / count) if count else 0.0 + group = _round_half_up(freq) + rec.actual_order_count = count + rec.actual_order_frequency = freq + rec.actual_order_frequency_group = group + rec.actual_flow_index_group_id = groups._match(group) if count else False + + def cron_actions(self, only_nfp=False): + res = super().cron_actions(only_nfp=only_nfp) + self._calc_actual_flow_index() + return res diff --git a/ddmrp_report_part_flow_index/pyproject.toml b/ddmrp_report_part_flow_index/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/ddmrp_report_part_flow_index/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/ddmrp_report_part_flow_index/readme/CONFIGURE.md b/ddmrp_report_part_flow_index/readme/CONFIGURE.md new file mode 100644 index 000000000..313d72c35 --- /dev/null +++ b/ddmrp_report_part_flow_index/readme/CONFIGURE.md @@ -0,0 +1,10 @@ +- Go to *Inventory \> Configuration \> DDMRP \> Flow Index Group* to define + the groups (a name and an order-frequency range each). An unset bound is + treated as open-ended. + +Buffers are assigned to a group automatically -- the **planned** and **actual** +Flow Index Group are computed from the buffer's order frequency against those +ranges. The planned value updates whenever the buffer's green zone or ADU +change; the actual value is refreshed together with the buffer (the scheduled +*Buffer refresh* action, or the **Refresh Buffer** button). Changes to a group's +range therefore take effect on the next buffer refresh. diff --git a/ddmrp_report_part_flow_index/readme/CONTRIBUTORS.md b/ddmrp_report_part_flow_index/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..b73022aef --- /dev/null +++ b/ddmrp_report_part_flow_index/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Jordi Ballester Alomar \<\> +- Lois Rilo Antelo \<\> diff --git a/ddmrp_report_part_flow_index/readme/DESCRIPTION.md b/ddmrp_report_part_flow_index/readme/DESCRIPTION.md new file mode 100644 index 000000000..8b1352d79 --- /dev/null +++ b/ddmrp_report_part_flow_index/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +Classifies DDMRP stock buffers by their order frequency ("flow"), into +user-defined Flow Index Groups. + +Two complementary views are provided: + +- **Plan Flow Index** -- the *planned* order frequency derived from the + buffer sizing (Green Zone / ADU). +- **Actual Flow Index** -- the *observed* order frequency derived from the + real completed replenishments over the past horizon. + +Comparing planned vs actual flow highlights buffers whose real ordering +behaviour diverges from their sizing. diff --git a/ddmrp_report_part_flow_index/readme/HISTORY.md b/ddmrp_report_part_flow_index/readme/HISTORY.md new file mode 100644 index 000000000..00f3607e5 --- /dev/null +++ b/ddmrp_report_part_flow_index/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 11.0.1.0.0 (2018-08-01) + +- Start of the history diff --git a/ddmrp_report_part_flow_index/readme/USAGE.md b/ddmrp_report_part_flow_index/readme/USAGE.md new file mode 100644 index 000000000..f16d2b885 --- /dev/null +++ b/ddmrp_report_part_flow_index/readme/USAGE.md @@ -0,0 +1,9 @@ +- Go to *Inventory \> Reporting \> Parts Plan Flow Index* to classify + buffers by their **planned** order frequency (Green Zone / ADU). +- Go to *Inventory \> Reporting \> Parts Actual Flow Index* to classify + buffers by their **actual** order frequency (completed replenishments + observed over the past horizon). + +Both reports group buffers by Flow Index Group and expose the planned and +actual figures side by side, so a divergence between planned and actual +flow flags a buffer that may be mis-sized. diff --git a/ddmrp_report_part_flow_index/reports/report_ddmrp_part_plan_flow_index_views.xml b/ddmrp_report_part_flow_index/reports/report_ddmrp_part_plan_flow_index_views.xml new file mode 100644 index 000000000..a5826a84b --- /dev/null +++ b/ddmrp_report_part_flow_index/reports/report_ddmrp_part_plan_flow_index_views.xml @@ -0,0 +1,141 @@ + + + + + stock.buffer.flow.index.list + stock.buffer + 99 + + + + + + + + + + + + + + + + + + + + + stock.buffer.flow.index.pivot + stock.buffer + 99 + + + + + + + + + + + stock.buffer.flow.index.graph + stock.buffer + 99 + + + + + + + + + stock.buffer.flow.index.search + stock.buffer + 99 + + + + + + + + + + + + + + + + + + Parts Plan Flow Index + stock.buffer + list,pivot,graph + + {'search_default_group_flow_index': 1} + + + + + + + + Parts Actual Flow Index + stock.buffer + list,pivot,graph + + {'search_default_group_actual_flow_index': 1} + + + + + diff --git a/ddmrp_report_part_flow_index/security/ir.model.access.csv b/ddmrp_report_part_flow_index/security/ir.model.access.csv new file mode 100644 index 000000000..caf0bfc20 --- /dev/null +++ b/ddmrp_report_part_flow_index/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ddmrp_flow_index_group,ddmrp.flow.index.group,model_ddmrp_flow_index_group,ddmrp.group_stock_buffer_maintainer,1,0,0,0 +access_ddmrp_flow_index_group_manager,ddmrp.flow.index.group,model_ddmrp_flow_index_group,ddmrp.group_ddmrp_manager,1,1,1,1 diff --git a/ddmrp_report_part_flow_index/static/description/icon.png b/ddmrp_report_part_flow_index/static/description/icon.png new file mode 100644 index 000000000..90c92e98d Binary files /dev/null and b/ddmrp_report_part_flow_index/static/description/icon.png differ diff --git a/ddmrp_report_part_flow_index/static/description/index.html b/ddmrp_report_part_flow_index/static/description/index.html new file mode 100644 index 000000000..70826271e --- /dev/null +++ b/ddmrp_report_part_flow_index/static/description/index.html @@ -0,0 +1,485 @@ + + + + + +DDMRP Report Part Flow Index + + + +
+ + + +Odoo Community Association + +
+

DDMRP Report Part Flow Index

+ +

Beta License: LGPL-3 OCA/ddmrp Translate me on Weblate Try me on Runboat

+

Classifies DDMRP stock buffers by their order frequency (“flow”), into +user-defined Flow Index Groups.

+

Two complementary views are provided:

+
    +
  • Plan Flow Index – the planned order frequency derived from the +buffer sizing (Green Zone / ADU).
  • +
  • Actual Flow Index – the observed order frequency derived from +the real completed replenishments over the past horizon.
  • +
+

Comparing planned vs actual flow highlights buffers whose real ordering +behaviour diverges from their sizing.

+

Table of contents

+ +
+

Configuration

+
    +
  • Go to Inventory > Configuration > DDMRP > Flow Index Group to define +the groups (a name and an order-frequency range each). An unset bound +is treated as open-ended.
  • +
+

Buffers are assigned to a group automatically – the planned and +actual Flow Index Group are computed from the buffer’s order +frequency against those ranges. The planned value updates whenever the +buffer’s green zone or ADU change; the actual value is refreshed +together with the buffer (the scheduled Buffer refresh action, or the +Refresh Buffer button). Changes to a group’s range therefore take +effect on the next buffer refresh.

+
+
+

Usage

+
    +
  • Go to Inventory > Reporting > Parts Plan Flow Index to classify +buffers by their planned order frequency (Green Zone / ADU).
  • +
  • Go to Inventory > Reporting > Parts Actual Flow Index to classify +buffers by their actual order frequency (completed replenishments +observed over the past horizon).
  • +
+

Both reports group buffers by Flow Index Group and expose the planned +and actual figures side by side, so a divergence between planned and +actual flow flags a buffer that may be mis-sized.

+
+
+

Changelog

+
+

11.0.1.0.0 (2018-08-01)

+
    +
  • Start of the history
  • +
+
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainers:

+

JordiBForgeFlow LoisRForgeFlow

+

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

+

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

+
+
+
+
+ + diff --git a/ddmrp_report_part_flow_index/tests/__init__.py b/ddmrp_report_part_flow_index/tests/__init__.py new file mode 100644 index 000000000..a687aa8cf --- /dev/null +++ b/ddmrp_report_part_flow_index/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ddmrp_report_part_flow_index diff --git a/ddmrp_report_part_flow_index/tests/test_ddmrp_report_part_flow_index.py b/ddmrp_report_part_flow_index/tests/test_ddmrp_report_part_flow_index.py new file mode 100644 index 000000000..78e4223e4 --- /dev/null +++ b/ddmrp_report_part_flow_index/tests/test_ddmrp_report_part_flow_index.py @@ -0,0 +1,116 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from datetime import timedelta + +from odoo import fields +from odoo.tests import TransactionCase + + +class TestDDMRPReportPartFlowIndex(TransactionCase): + def setUp(self): + super().setUp() + self.env = self.env(context=dict(self.env.context, tracking_disable=True)) + self.buffer_profile_mmm = self.env.ref( + "ddmrp.stock_buffer_profile_replenish_manufactured_medium_medium" + ) + self.stock_location = self.env.ref("stock.stock_location_stock") + self.supplier_location = self.env.ref("stock.stock_location_suppliers") + self.warehouse = self.env.ref("stock.warehouse0") + self.adu_fixed = self.env.ref("ddmrp.adu_calculation_method_fixed") + self.product = self.env["product.product"].create( + { + "name": "product test", + "is_storable": True, + } + ) + self.buffer = self.env["stock.buffer"].create( + { + "buffer_profile_id": self.buffer_profile_mmm.id, + "product_id": self.product.id, + "location_id": self.stock_location.id, + "warehouse_id": self.warehouse.id, + "adu_calculation_method": self.adu_fixed.id, + "adu_fixed": 4.0, + "order_cycle": 5, + } + ) + self.flow_index_group_1 = self.env["ddmrp.flow.index.group"].create( + {"name": "Group 1", "lower_range": 1, "upper_range": 10, "sequence": 0} + ) + self.flow_index_group_2 = self.env["ddmrp.flow.index.group"].create( + {"name": "Group 2", "lower_range": 11, "upper_range": 30, "sequence": 1} + ) + self.flow_index_group_3 = self.env["ddmrp.flow.index.group"].create( + {"name": "Group 3", "lower_range": 31, "upper_range": 100000, "sequence": 2} + ) + + def _create_done_receipt(self, days_ago): + """Create a completed replenishment move into the buffer location.""" + move = self.env["stock.move"].create( + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "product_uom_qty": 10.0, + "location_id": self.supplier_location.id, + "location_dest_id": self.stock_location.id, + "company_id": self.warehouse.company_id.id, + } + ) + date = fields.Datetime.now() - timedelta(days=days_ago) + move.write({"state": "done", "date": date}) + return move + + def test_01_calc_flow_index_group_id(self): + """The planned flow index group follows the buffer's order frequency.""" + self.buffer.cron_actions() + self.assertEqual(self.buffer.order_frequency_group, 5) + self.assertEqual(self.buffer.flow_index_group_id, self.flow_index_group_1) + # Growing the order cycle grows the green zone -> order frequency -> group. + self.buffer.order_cycle = 20 + self.buffer.cron_actions() + self.assertEqual(self.buffer.order_frequency_group, 20) + self.assertEqual(self.buffer.flow_index_group_id, self.flow_index_group_2) + + def test_02_flow_index_is_reactive(self): + """flow_index_group_id is recomputed by the ORM on green zone / ADU change, + without going through cron_actions.""" + self.buffer.cron_actions() + self.assertEqual(self.buffer.flow_index_group_id, self.flow_index_group_1) + # A direct write of the inputs re-triggers the stored computes. + self.buffer.green_zone_qty = 80.0 + self.assertEqual(self.buffer.order_frequency_group, 20) + self.assertEqual(self.buffer.flow_index_group_id, self.flow_index_group_2) + + def test_03_actual_flow_index(self): + """The actual flow index reflects real completed replenishments.""" + self._create_done_receipt(days_ago=30) + self._create_done_receipt(days_ago=120) + self.buffer.cron_actions() + self.assertEqual(self.buffer.actual_order_count, 2) + # 365-day default horizon / 2 receipts -> ~183 day actual cycle. + self.assertEqual(self.buffer.actual_order_frequency_group, 183) + self.assertEqual( + self.buffer.actual_flow_index_group_id, self.flow_index_group_3 + ) + + def test_04_actual_flow_index_no_history(self): + """No completed replenishments -> no actual flow index group.""" + self.buffer.cron_actions() + self.assertEqual(self.buffer.actual_order_count, 0) + self.assertFalse(self.buffer.actual_flow_index_group_id) + + def test_05_match_open_ended_and_gaps(self): + """_match handles open-ended bounds (0 = unbounded) and unmatched gaps.""" + Group = self.env["ddmrp.flow.index.group"] + Group.search([]).unlink() + fast = Group.create( + {"name": "Fast", "sequence": 1, "lower_range": 0, "upper_range": 5} + ) + slow = Group.create( + {"name": "Slow", "sequence": 2, "lower_range": 31, "upper_range": 0} + ) + groups = Group.search([], order="sequence, id") + self.assertEqual(groups._match(3), fast, "upper-only bound") + self.assertEqual(groups._match(50), slow, "lower-only bound") + self.assertFalse(groups._match(20), "value in the gap -> no group") diff --git a/ddmrp_report_part_flow_index/views/ddmrp_flow_index_group_views.xml b/ddmrp_report_part_flow_index/views/ddmrp_flow_index_group_views.xml new file mode 100644 index 000000000..9ca374b93 --- /dev/null +++ b/ddmrp_report_part_flow_index/views/ddmrp_flow_index_group_views.xml @@ -0,0 +1,72 @@ + + + + + ddmrp.flow.index.group.tree + ddmrp.flow.index.group + + + + + + + + + + + + ddmrp.flow.index.group.form + ddmrp.flow.index.group + +
+ + + + + + + + + + + + +
+
+
+ + + ddmrp.flow.index.group.search + ddmrp.flow.index.group + + + + + + + + + + + DDMRP Flow Index Group + ddmrp.flow.index.group + list,form + + + +
diff --git a/ddmrp_report_part_flow_index/views/stock_buffer_view.xml b/ddmrp_report_part_flow_index/views/stock_buffer_view.xml new file mode 100644 index 000000000..48541037b --- /dev/null +++ b/ddmrp_report_part_flow_index/views/stock_buffer_view.xml @@ -0,0 +1,28 @@ + + + + + stock.buffer.form - Part Flow Index + stock.buffer + + + + + + + + + + + + + + + + + + + + +