From 38fd292b5bd72442bfd5581c3597196fae8cf506 Mon Sep 17 00:00:00 2001
From: david
Date: Wed, 20 Nov 2019 14:30:39 +0100
Subject: [PATCH] [IMP] stock_orderpoint_generator: auto values
---
stock_orderpoint_generator/README.rst | 20 +
stock_orderpoint_generator/__manifest__.py | 5 +-
stock_orderpoint_generator/i18n/es.po | 197 +++++---
.../i18n/stock_orderpoint_generator.pot | 103 ++++-
.../models/orderpoint_template.py | 147 +++++-
stock_orderpoint_generator/models/product.py | 98 +++-
.../readme/CONTRIBUTORS.rst | 1 +
stock_orderpoint_generator/readme/USAGE.rst | 18 +
.../static/description/index.html | 21 +
.../tests/test_orderpoint_generator.py | 424 +++++++++++++++---
.../views/orderpoint_template_views.xml | 22 +-
.../wizard/orderpoint_generator.py | 20 +-
12 files changed, 917 insertions(+), 159 deletions(-)
diff --git a/stock_orderpoint_generator/README.rst b/stock_orderpoint_generator/README.rst
index 25e4ad3f2..134185358 100644
--- a/stock_orderpoint_generator/README.rst
+++ b/stock_orderpoint_generator/README.rst
@@ -51,6 +51,24 @@ you are able to select a list of products. Any change on the template will then
be replicated on the products Reordering Rules. The change is not immediate as
it is processed by a scheduled action.
+Aditionally, minimum and maximum quantity fields can be computed automatically
+if desired for the set of products and according to the desired criteria for
+the time range given. To do so:
+
+#. In an Orderpoint template, check "Auto minimum" or "Auto maximum" or both.
+#. The criteria fields for either one or another are visible now.
+#. Select a time range of moves to evalute. For every product a history of
+ the resulting stock for every move in such range and the location given
+ on the Orderpoint template will be obtained.
+#. Select a criteria method to compute the minimum o maximum quantity:
+
+ - Maximum: the maximum stock value for the given period.
+ - Most frequent: the median of the history of stock values for the specified
+ range. Useful when a large amount of history values are obtained, as it
+ tends to avoid deviation caused by extreme values in a common avarage.
+ - Average: Arithmetic mean of the stock history.
+ - Minimum: the minimum stock value for the given period.
+
Lastly, you can promptly create Reordering Rules for a product or a product
template using the "Reordering Rules Generator". Note that it will replace all
the existing rules for the product. You will usually not want to use this
@@ -73,6 +91,7 @@ Authors
~~~~~~~
* Camptocamp
+* Tecnativa
Contributors
~~~~~~~~~~~~
@@ -84,6 +103,7 @@ Contributors
* `Tecnativa `_:
* Vicent Cubells
+ * David Vidal
Maintainers
~~~~~~~~~~~
diff --git a/stock_orderpoint_generator/__manifest__.py b/stock_orderpoint_generator/__manifest__.py
index 183496798..99f6b6073 100644
--- a/stock_orderpoint_generator/__manifest__.py
+++ b/stock_orderpoint_generator/__manifest__.py
@@ -1,11 +1,14 @@
# Copyright 2012-2016 Camptocamp SA
+# Copyright 2019 Tecnativa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
'name': 'Order point generator',
'summary': 'Mass configuration of stock order points',
'version': '11.0.1.0.1',
- 'author': "Camptocamp, Odoo Community Association (OCA)",
+ 'author': "Camptocamp, "
+ "Tecnativa, "
+ "Odoo Community Association (OCA)",
'category': 'Warehouse',
'license': 'AGPL-3',
'website': "https://github.com/OCA/stock-logistics-warehouse",
diff --git a/stock_orderpoint_generator/i18n/es.po b/stock_orderpoint_generator/i18n/es.po
index 35aaf04c7..e3ec83eda 100644
--- a/stock_orderpoint_generator/i18n/es.po
+++ b/stock_orderpoint_generator/i18n/es.po
@@ -1,39 +1,70 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
-# * stock_orderpoint_generator
+# * stock_orderpoint_generator
#
-# Translators:
msgid ""
msgstr ""
-"Project-Id-Version: stock-logistics-warehouse (9.0)\n"
+"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-31 02:38+0000\n"
-"PO-Revision-Date: 2017-05-30 10:13+0000\n"
-"Last-Translator: OCA Transbot \n"
-"Language-Team: Spanish (http://www.transifex.com/oca/OCA-stock-logistics-"
-"warehouse-9-0/language/es/)\n"
-"Language: es\n"
+"POT-Creation-Date: 2019-11-21 15:42+0000\n"
+"PO-Revision-Date: 2019-11-21 15:42+0000\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: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: \n"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_product_ids
-msgid ""
-"A reordering rule will be automatically created by the scheduled action for "
-"every product in this list."
-msgstr ""
+msgid "A reordering rule will be automatically created by the scheduled action for every product in this list."
+msgstr "A reordering rule will be automatically created by the scheduled action for every product in this list."
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "Apply"
msgstr "Aplicar"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty
+msgid "Auto Maximum"
+msgstr "Auto Máximo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
+msgid "Auto Maximum Criteria"
+msgstr "Criterio Auto Máximo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty
+msgid "Auto Minimum"
+msgstr "Auto mínimo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
+msgid "Auto Minimum Criteria"
+msgstr "Criterio Auto Mínimo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty
+msgid "Auto compute maximum quantity per product for a given a date range"
+msgstr "Auto calcular cantidad máxima por producto para un rango de fechas dado"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty
+msgid "Auto compute minimum quantity per product for a given a date range"
+msgstr "Auto calcular cantidad mímina por producto para un rango de fechas dado"
+
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_product_product_auto_orderpoint_template_ids
msgid "Automatic Reordering Rules"
-msgstr ""
+msgstr "Reglas de Reabastecimiento Automáticas"
+
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Average"
+msgstr "Media"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
@@ -41,20 +72,20 @@ msgid "Cancel"
msgstr "Cancelar"
#. module: stock_orderpoint_generator
-#: code:addons/stock_orderpoint_generator/wizard/orderpoint_generator.py:39
+#: code:addons/stock_orderpoint_generator/wizard/orderpoint_generator.py:37
#, python-format
msgid "Cannot apply because some of selected products has multiple variants."
-msgstr ""
+msgstr "No es posible aplicar la plantilla ya que alguno de los productos seleccionados contiene múltiples variantes."
#. module: stock_orderpoint_generator
#: model:ir.actions.act_window,help:stock_orderpoint_generator.action_orderpoint_template
msgid "Click to add a reordering rule template."
-msgstr ""
+msgstr "Click para añadir una plantilla de regla de reabastecimiento."
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_generate
msgid "Create Rules Automatically"
-msgstr ""
+msgstr "Crear Reglas Automáticamente"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator_create_uid
@@ -68,10 +99,16 @@ msgstr "Creado por"
msgid "Created on"
msgstr "Creado en"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty_criteria
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty_criteria
+msgid "Criteria"
+msgstr "Criterio"
+
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_uom
msgid "Default Unit of Measure used for all stock operation."
-msgstr ""
+msgstr "Unidad de medida predeterminada utilizada para todas las operaciones de stock."
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator_display_name
@@ -79,10 +116,16 @@ msgstr ""
msgid "Display Name"
msgstr "Nombre mostrado"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_date_start
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_date_start
+msgid "From"
+msgstr "De"
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Generate Automatic Rules"
-msgstr ""
+msgstr "Generar Reglas Automáticas"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
@@ -93,24 +136,24 @@ msgstr "Agrupar por"
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator_id
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_id
msgid "ID"
-msgstr "ID"
+msgstr "ID (identificación)"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_last_generation
msgid "Last Automatic Generation"
-msgstr ""
+msgstr "Última Generación Automática"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator___last_update
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template___last_update
msgid "Last Modified on"
-msgstr "Última modificación el"
+msgstr "Última modificación en"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator_write_uid
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_write_uid
msgid "Last Updated by"
-msgstr "Última actualización por"
+msgstr "Última actualización de"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_generator_write_date
@@ -121,20 +164,45 @@ msgstr "Última actualización en"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
msgid "Location"
-msgstr "Localización"
+msgstr "Ubicación"
+
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Maximum"
+msgstr "Máximo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_max_qty
+msgid "Maximum Quantity"
+msgstr "Cantidad máxima"
+
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Minimum"
+msgstr "Mínimo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_min_qty
+msgid "Minimum Quantity"
+msgstr "Cantidad mínima"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Misc"
-msgstr "Misc."
+msgstr "Varios"
+
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Most frequent"
+msgstr "Más frecuente"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_group_id
-msgid ""
-"Moves created through this orderpoint will be put in this procurement group. "
-"If none is given, the moves generated by procurement rules will be grouped "
-"into one big picking."
-msgstr ""
+msgid "Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking."
+msgstr "Los movimientos creados por esta orden de abastecimiento serán colocados en este grupo de abastecimiento. Si no se proporciona ningún grupo, los movimientos generados por las reglas de abastecimiento serán agrupados en un gran albarán."
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_name
@@ -144,12 +212,12 @@ msgstr "Nombre"
#. module: stock_orderpoint_generator
#: model:ir.model,name:stock_orderpoint_generator.model_stock_warehouse_orderpoint_generator
msgid "Orderpoint Generator"
-msgstr ""
+msgstr "Generador de Reglas de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_group_id
msgid "Procurement Group"
-msgstr ""
+msgstr "Grupo de abastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.model,name:stock_orderpoint_generator.model_product_product
@@ -171,12 +239,12 @@ msgstr "Productos"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Quantity Multiple"
-msgstr ""
+msgstr "Múltiplo de la cantidad"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Reordering Rule Template"
-msgstr ""
+msgstr "Plantilla de Regla de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.actions.act_window,name:stock_orderpoint_generator.action_orderpoint_template
@@ -186,39 +254,47 @@ msgstr ""
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_tree
msgid "Reordering Rule Templates"
-msgstr ""
+msgstr "Plantillas de Regla de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.actions.server,name:stock_orderpoint_generator.ir_cron_auto_orderpoint_template_ir_actions_server
#: model:ir.cron,cron_name:stock_orderpoint_generator.ir_cron_auto_orderpoint_template
#: model:ir.cron,name:stock_orderpoint_generator.ir_cron_auto_orderpoint_template
msgid "Reordering Rule Templates Generator"
-msgstr ""
+msgstr "Generador de Plantillas de Regla de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
msgid "Reordering Rule Templates Search"
-msgstr ""
+msgstr "Búsqueda de Plantillas de Regla de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.actions.act_window,name:stock_orderpoint_generator.act_create_product_conf
#: model:ir.actions.act_window,name:stock_orderpoint_generator.act_create_product_template_conf
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "Reordering Rules Generator"
-msgstr ""
+msgstr "Generador de Reglas de Reabastecimiento"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
-msgid ""
-"Reordering rules will be created for the selected products. This is "
-"equivalent to the Scheduled Action action."
-msgstr ""
+msgid "Reordering rules will be created for the selected products. This is equivalent to the Scheduled Action action."
+msgstr "Se crearán reglas de reabastecimiento para los productos seleccionados. Es el equivalente a la acción programada"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Rules"
msgstr "Reglas"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty_criteria
+msgid "Select a criteria to auto compute the maximum"
+msgstr "Escoja un criterio para auto calcular el máximo"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty_criteria
+msgid "Select a criteria to auto compute the minimum"
+msgstr "Escoja un criterio para auto calcular el mínimo"
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "Templates"
@@ -227,7 +303,13 @@ msgstr "Plantillas"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "This wizard will apply the following orderpoint to selected product(s)"
-msgstr ""
+msgstr "Este asistente aplicará las siguientes plantillas de regla de reabastecimiento a los productos seleccionados"
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_date_end
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_date_end
+msgid "To"
+msgstr "Para"
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
@@ -236,21 +318,20 @@ msgstr "Almacén"
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_generate
-msgid ""
-"When checked, the 'Reordering Rule Templates Generator' scheduled action "
-"will automatically update the rules of a selection of products."
-msgstr ""
+msgid "When checked, the 'Reordering Rule Templates Generator' scheduled action will automatically update the rules of a selection of products."
+msgstr "La acción programada para 'Regenerar Plantillas de Reglas de Reabastecimiento' actualizará automáticamente las regls para la selección de productos."
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_product_product_auto_orderpoint_template_ids
-msgid ""
-"When one or several automatic reordering rule is selected, a Scheduled "
-"Action will automatically generate or update the reordering rules of the "
-"product."
-msgstr ""
+msgid "When one or several automatic reordering rule is selected, a Scheduled Action will automatically generate or update the reordering rules of the product."
+msgstr "Cuando una o más reglas de reabastecimiento son seleccionadas, una acción programada generará automáticamente o actualizará las reglas de reabastecimiento del producto."
-#~ msgid "Active"
-#~ msgstr "Activo"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_min_qty
+msgid "When the virtual stock goes below the Min Quantity specified for this field, Odoo generates a procurement to bring the forecasted quantity to the Max Quantity."
+msgstr "Cuando el stock virtual esté por debajo de la cantidad mínima especificada en este campo, Odoo generará un abastecimiento para llevar la cantidad prevista a la cantidad máxima."
-#~ msgid "Company"
-#~ msgstr "Compañía"
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_max_qty
+msgid "When the virtual stock goes below the Min Quantity, Odoo generates a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."
+msgstr "Cuando el stock virtual esté por debajo de la cantidad, Odoo generará un abastecimiento para llevar la cantidad prevista a la cantidad especificada como aquí como máxima."
diff --git a/stock_orderpoint_generator/i18n/stock_orderpoint_generator.pot b/stock_orderpoint_generator/i18n/stock_orderpoint_generator.pot
index 384e239f6..287722e52 100644
--- a/stock_orderpoint_generator/i18n/stock_orderpoint_generator.pot
+++ b/stock_orderpoint_generator/i18n/stock_orderpoint_generator.pot
@@ -23,18 +23,54 @@ msgstr ""
msgid "Apply"
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty
+msgid "Auto Maximum"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
+msgid "Auto Maximum Criteria"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty
+msgid "Auto Minimum"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
+msgid "Auto Minimum Criteria"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty
+msgid "Auto compute maximum quantity per product for a given a date range"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty
+msgid "Auto compute minimum quantity per product for a given a date range"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_product_product_auto_orderpoint_template_ids
msgid "Automatic Reordering Rules"
msgstr ""
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Average"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "Cancel"
msgstr ""
#. module: stock_orderpoint_generator
-#: code:addons/stock_orderpoint_generator/wizard/orderpoint_generator.py:39
+#: code:addons/stock_orderpoint_generator/wizard/orderpoint_generator.py:37
#, python-format
msgid "Cannot apply because some of selected products has multiple variants."
msgstr ""
@@ -61,6 +97,12 @@ msgstr ""
msgid "Created on"
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty_criteria
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty_criteria
+msgid "Criteria"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_uom
msgid "Default Unit of Measure used for all stock operation."
@@ -72,6 +114,12 @@ msgstr ""
msgid "Display Name"
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_date_start
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_date_start
+msgid "From"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Generate Automatic Rules"
@@ -116,11 +164,39 @@ msgstr ""
msgid "Location"
msgstr ""
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Maximum"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_max_qty
+msgid "Maximum Quantity"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Minimum"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_min_qty
+msgid "Minimum Quantity"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_form
msgid "Misc"
msgstr ""
+#. module: stock_orderpoint_generator
+#: selection:stock.warehouse.orderpoint.template,auto_max_qty_criteria:0
+#: selection:stock.warehouse.orderpoint.template,auto_min_qty_criteria:0
+msgid "Most frequent"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_group_id
msgid "Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking."
@@ -207,6 +283,16 @@ msgstr ""
msgid "Rules"
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_qty_criteria
+msgid "Select a criteria to auto compute the maximum"
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_qty_criteria
+msgid "Select a criteria to auto compute the minimum"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.orderpoint_generator_view
msgid "Templates"
@@ -217,6 +303,12 @@ msgstr ""
msgid "This wizard will apply the following orderpoint to selected product(s)"
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_max_date_end
+#: model:ir.model.fields,field_description:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_auto_min_date_end
+msgid "To"
+msgstr ""
+
#. module: stock_orderpoint_generator
#: model:ir.ui.view,arch_db:stock_orderpoint_generator.view_warehouse_orderpoint_template_search
msgid "Warehouse"
@@ -232,3 +324,12 @@ msgstr ""
msgid "When one or several automatic reordering rule is selected, a Scheduled Action will automatically generate or update the reordering rules of the product."
msgstr ""
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_min_qty
+msgid "When the virtual stock goes below the Min Quantity specified for this field, Odoo generates a procurement to bring the forecasted quantity to the Max Quantity."
+msgstr ""
+
+#. module: stock_orderpoint_generator
+#: model:ir.model.fields,help:stock_orderpoint_generator.field_stock_warehouse_orderpoint_template_product_max_qty
+msgid "When the virtual stock goes below the Min Quantity, Odoo generates a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."
+msgstr ""
diff --git a/stock_orderpoint_generator/models/orderpoint_template.py b/stock_orderpoint_generator/models/orderpoint_template.py
index 53440609d..b8be41337 100644
--- a/stock_orderpoint_generator/models/orderpoint_template.py
+++ b/stock_orderpoint_generator/models/orderpoint_template.py
@@ -1,8 +1,10 @@
# Copyright 2012-2016 Camptocamp SA
+# Copyright 2019 Tecnativa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
+from statistics import mean, median_high
class OrderpointTemplate(models.Model):
@@ -24,10 +26,54 @@ class OrderpointTemplate(models.Model):
name = fields.Char(copy=True)
group_id = fields.Many2one(copy=True)
-
product_id = fields.Many2one(required=False)
product_uom = fields.Many2one(required=False)
+ product_min_qty = fields.Float(required=False)
+ product_max_qty = fields.Float(required=False)
+ auto_min_qty = fields.Boolean(
+ string="Auto Minimum",
+ help="Auto compute minimum quantity "
+ "per product for a given a date range",
+ )
+ auto_min_date_start = fields.Datetime(
+ string="From",
+ )
+ auto_min_date_end = fields.Datetime(
+ string="To",
+ )
+ auto_min_qty_criteria = fields.Selection(
+ selection=[
+ ('max', 'Maximum'),
+ ('median', 'Most frequent'),
+ ('avg', 'Average'),
+ ('min', 'Minimum'),
+ ],
+ default='max',
+ string="Criteria",
+ help="Select a criteria to auto compute the minimum",
+ )
+ auto_max_qty = fields.Boolean(
+ string="Auto Maximum",
+ help="Auto compute maximum quantity "
+ "per product for a given a date range",
+ )
+ auto_max_qty_criteria = fields.Selection(
+ selection=[
+ ('max', 'Maximum'),
+ ('median', 'Most frequent'),
+ ('avg', 'Average'),
+ ('min', 'Minimum'),
+ ],
+ string="Criteria",
+ help="Select a criteria to auto compute the maximum",
+ )
+ auto_max_date_start = fields.Datetime(
+ string="From",
+ )
+ auto_max_date_end = fields.Datetime(
+ string="To",
+ )
auto_generate = fields.Boolean(
string='Create Rules Automatically',
help="When checked, the 'Reordering Rule Templates Generator' "
@@ -42,36 +88,101 @@ class OrderpointTemplate(models.Model):
)
auto_last_generation = fields.Datetime(string='Last Automatic Generation')
- def _disable_old_instances(self, product_ids):
- """ Clean old instance by setting those inactives
- """
+ def _template_fields_to_discard(self):
+ """In order to create every orderpoint we should pop this template
+ customization fields """
+ return [
+ 'auto_generate', 'auto_product_ids', 'auto_last_generation',
+ 'auto_min_qty', 'auto_min_date_start', 'auto_min_qty_criteria',
+ 'auto_min_date_end', 'auto_max_date_start', 'auto_max_date_end',
+ 'auto_max_qty_criteria', 'auto_max_qty',
+ ]
+
+ def _disable_old_instances(self, products):
+ """Clean old instance by setting those inactives"""
orderpoints = self.env['stock.warehouse.orderpoint'].search(
- [('product_id', 'in', product_ids)]
+ [('product_id', 'in', products.ids)]
)
orderpoints.write({'active': False})
+ @api.model
+ def _get_criteria_methods(self):
+ """Allows to extend methods with other statistical aproaches"""
+ return {
+ 'max': max,
+ 'median': median_high,
+ 'avg': mean,
+ 'min': min,
+ }
+
+ @api.model
+ def _get_product_qty_by_criteria(
+ self, products, location_id, from_date, to_date, criteria):
+ """Returns a dict with product ids as keys and the resulting
+ calculation of historic moves according to criteria"""
+ stock_qty_history = products._compute_historic_quantities_dict(
+ location_id=location_id,
+ from_date=from_date,
+ to_date=to_date)
+ criteria_methods = self._get_criteria_methods()
+ return {x: criteria_methods[criteria](y['stock_history'])
+ for x, y in stock_qty_history.items()}
+
def _create_instances(self, product_ids):
- """ Create instances of model using template inherited model
- """
+ """Create instances of model using template inherited model and
+ compute autovalues if needed"""
orderpoint_model = self.env['stock.warehouse.orderpoint']
for record in self:
+ # Flag equality so we compute the values just once
+ auto_same_values = (
+ record.auto_max_date_start == record.auto_min_date_start
+ ) and (
+ record.auto_max_date_end == record.auto_max_date_end
+ ) and (
+ record.auto_max_qty_criteria ==
+ record.auto_min_qty_criteria)
+ stock_min_qty = stock_max_qty = {}
+ if record.auto_min_qty:
+ stock_min_qty = (
+ self._get_product_qty_by_criteria(
+ product_ids,
+ location_id=record.location_id,
+ from_date=record.auto_min_date_start,
+ to_date=record.auto_min_date_end,
+ criteria=record.auto_min_qty_criteria,
+ ))
+ if auto_same_values:
+ stock_max_qty = stock_min_qty
+ if record.auto_max_qty and not stock_max_qty:
+ stock_max_qty = (
+ self._get_product_qty_by_criteria(
+ product_ids,
+ location_id=record.location_id,
+ from_date=record.auto_max_date_start,
+ to_date=record.auto_max_date_end,
+ criteria=record.auto_max_qty_criteria,
+ ))
for data in record.copy_data():
- data.pop('auto_generate')
- data.pop('auto_product_ids')
- data.pop('auto_last_generation')
+ for discard_field in self._template_fields_to_discard():
+ data.pop(discard_field)
for product_id in product_ids:
vals = data.copy()
- vals['product_id'] = product_id
+ vals['product_id'] = product_id.id
+ if record.auto_min_qty:
+ vals['product_min_qty'] = stock_min_qty.get(
+ product_id.id, 0)
+ if record.auto_max_qty:
+ vals['product_max_qty'] = stock_max_qty.get(
+ product_id.id, 0)
orderpoint_model.create(vals)
@api.multi
- def create_orderpoints(self, product_ids):
- """ Create orderpoint for *product_ids* based on these templates.
-
- :type product_ids: list of int
+ def create_orderpoints(self, products):
+ """ Create orderpoint for *products* based on these templates.
+ :type products: recordset of products
"""
- self._disable_old_instances(product_ids)
- self._create_instances(product_ids)
+ self._disable_old_instances(products)
+ self._create_instances(products)
@api.multi
def create_auto_orderpoints(self):
@@ -81,7 +192,7 @@ class OrderpointTemplate(models.Model):
if (not template.auto_last_generation or
template.write_date > template.auto_last_generation):
template.auto_last_generation = fields.Datetime.now()
- template.create_orderpoints(template.auto_product_ids.ids)
+ template.create_orderpoints(template.auto_product_ids)
@api.model
def _cron_create_auto_orderpoints(self):
diff --git a/stock_orderpoint_generator/models/product.py b/stock_orderpoint_generator/models/product.py
index 2db645931..66b42010b 100644
--- a/stock_orderpoint_generator/models/product.py
+++ b/stock_orderpoint_generator/models/product.py
@@ -1,7 +1,9 @@
# Copyright 2017 Camptocamp SA
+# Copyright 2019 Tecnativa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from odoo import api, fields, models
+from collections import OrderedDict
class ProductProduct(models.Model):
@@ -18,15 +20,103 @@ class ProductProduct(models.Model):
@api.model
def create(self, vals):
- record = super(ProductProduct, self).create(vals)
+ record = super().create(vals)
if vals.get('auto_orderpoint_template_ids'):
- record.auto_orderpoint_template_ids.create_orderpoints(record.ids)
+ record.auto_orderpoint_template_ids.create_orderpoints(record)
return record
@api.multi
def write(self, vals):
- result = super(ProductProduct, self).write(vals)
+ result = super().write(vals)
if vals.get('auto_orderpoint_template_ids'):
orderpoint_templates = self.mapped('auto_orderpoint_template_ids')
- orderpoint_templates.create_orderpoints(self.ids)
+ orderpoint_templates.create_orderpoints(self)
return result
+
+ def _compute_historic_quantities_dict(
+ self, location_id=False, from_date=False, to_date=False):
+ """Returns a dict of products with a dict of historic moves as for
+ a list of historic stock values resulting from those moves. If
+ a location_id is passed, we can restrict it to such location"""
+ location = location_id and location_id.id
+ domain_quant_loc, domain_move_in_loc, domain_move_out_loc = (
+ self.with_context(location=location)._get_domain_locations())
+ if not to_date:
+ to_date = fields.Datetime.now()
+ domain_move_in = domain_move_out = ([
+ ('product_id', 'in', self.ids),
+ ('state', '=', 'done'),
+ ] + domain_move_in_loc)
+ domain_move_out = ([
+ ('product_id', 'in', self.ids),
+ ('state', '=', 'done'),
+ ] + domain_move_out_loc)
+ if from_date:
+ domain_move_in += [('date', '>=', from_date)]
+ domain_move_out += [('date', '>=', from_date)]
+ domain_move_in += [('date', '<=', to_date)]
+ domain_move_out += [('date', '<=', to_date)]
+ move_obj = self.env['stock.move']
+ # Positive moves
+ moves_in = move_obj.search_read(
+ domain_move_in, ['product_id', 'product_qty', 'date'],
+ order='date asc')
+ # We'll convert to negative these quantities to operate with them
+ # to obtain the stock snapshot in every moment
+ moves_out = move_obj.search_read(
+ domain_move_out, ['product_id', 'product_qty', 'date'],
+ order='date asc')
+ for move in moves_out:
+ move['product_qty'] *= -1
+ # Merge both results and group them by product id as key
+ moves = moves_in + moves_out
+ # Obtain a dict with the stock snapshot for the relative date_from
+ # otherwise, the first move will counted as first stock value. We
+ # default the compute the stock value anyway to default the value
+ # for products with no moves for the given period
+ initial_stock = {}
+ initial_stock = self.with_context(
+ location=location)._compute_quantities_dict(
+ False, False, False, to_date=from_date or to_date)
+ product_moves_dict = {}
+ for move in moves:
+ product_moves_dict.setdefault(move['product_id'][0], {})
+ product_moves_dict[move['product_id'][0]].update({
+ move['date']: {
+ 'prod_qty': move['product_qty'],
+ }
+ })
+ for product in self.with_context(prefetch_fields=False):
+ # If no there are no moves for a product we default the stock
+ # to the one for the given period nevermind the dates
+ product_moves = product_moves_dict.get(product.id)
+ prod_initial_stock = initial_stock.get(product.id, {})
+ if not product_moves:
+ product_moves_dict[product.id] = {
+ to_date: {
+ 'prod_qty': 0,
+ 'stock': prod_initial_stock.get('qty_available', 0),
+ },
+ 'stock_history': [
+ prod_initial_stock.get('qty_available', 0)],
+ }
+ continue
+ # Now we'll sort the moves by date and assign an initial stock so
+ # we can compute the stock historical values from the moves
+ # sequence so we can exploit it statisticaly
+ product_moves = OrderedDict(sorted(product_moves.items()))
+ stock = False
+ first_item = product_moves[next(iter(product_moves))]
+ if from_date:
+ stock = prod_initial_stock.get('qty_available')
+ if not stock:
+ stock = first_item['prod_qty']
+ first_item['stock'] = stock
+ iter_moves = iter(product_moves)
+ next(iter_moves, None)
+ for date in iter_moves:
+ stock += product_moves[date]['prod_qty']
+ product_moves[date]['stock'] = stock
+ product_moves_dict[product.id]['stock_history'] = (
+ [v['stock'] for k, v in product_moves.items()])
+ return product_moves_dict
diff --git a/stock_orderpoint_generator/readme/CONTRIBUTORS.rst b/stock_orderpoint_generator/readme/CONTRIBUTORS.rst
index 308b5d3dc..3bfa9e4e4 100644
--- a/stock_orderpoint_generator/readme/CONTRIBUTORS.rst
+++ b/stock_orderpoint_generator/readme/CONTRIBUTORS.rst
@@ -5,3 +5,4 @@
* `Tecnativa `_:
* Vicent Cubells
+ * David Vidal
diff --git a/stock_orderpoint_generator/readme/USAGE.rst b/stock_orderpoint_generator/readme/USAGE.rst
index 5c74d298d..cc87fb0f7 100644
--- a/stock_orderpoint_generator/readme/USAGE.rst
+++ b/stock_orderpoint_generator/readme/USAGE.rst
@@ -3,6 +3,24 @@ you are able to select a list of products. Any change on the template will then
be replicated on the products Reordering Rules. The change is not immediate as
it is processed by a scheduled action.
+Aditionally, minimum and maximum quantity fields can be computed automatically
+if desired for the set of products and according to the desired criteria for
+the time range given. To do so:
+
+#. In an Orderpoint template, check "Auto minimum" or "Auto maximum" or both.
+#. The criteria fields for either one or another are visible now.
+#. Select a time range of moves to evalute. For every product a history of
+ the resulting stock for every move in such range and the location given
+ on the Orderpoint template will be obtained.
+#. Select a criteria method to compute the minimum o maximum quantity:
+
+ - Maximum: the maximum stock value for the given period.
+ - Most frequent: the median of the history of stock values for the specified
+ range. Useful when a large amount of history values are obtained, as it
+ tends to avoid deviation caused by extreme values in a common avarage.
+ - Average: Arithmetic mean of the stock history.
+ - Minimum: the minimum stock value for the given period.
+
Lastly, you can promptly create Reordering Rules for a product or a product
template using the "Reordering Rules Generator". Note that it will replace all
the existing rules for the product. You will usually not want to use this
diff --git a/stock_orderpoint_generator/static/description/index.html b/stock_orderpoint_generator/static/description/index.html
index 263cf6824..4203e2984 100644
--- a/stock_orderpoint_generator/static/description/index.html
+++ b/stock_orderpoint_generator/static/description/index.html
@@ -398,6 +398,25 @@ scheduled action is “Reordering Rule Templates Generator”.
you are able to select a list of products. Any change on the template will then
be replicated on the products Reordering Rules. The change is not immediate as
it is processed by a scheduled action.
+Aditionally, minimum and maximum quantity fields can be computed automatically
+if desired for the set of products and according to the desired criteria for
+the time range given. To do so:
+
+- In an Orderpoint template, check “Auto minimum” or “Auto maximum” or both.
+- The criteria fields for either one or another are visible now.
+- Select a time range of moves to evalute. For every product a history of
+the resulting stock for every move in such range and the location given
+on the Orderpoint template will be obtained.
+- Select a criteria method to compute the minimum o maximum quantity:
+- Maximum: the maximum stock value for the given period.
+- Most frequent: the median of the history of stock values for the specified
+range. Useful when a large amount of history values are obtained, as it
+tends to avoid deviation caused by extreme values in a common avarage.
+- Average: Arithmetic mean of the stock history.
+- Minimum: the minimum stock value for the given period.
+
+
+
Lastly, you can promptly create Reordering Rules for a product or a product
template using the “Reordering Rules Generator”. Note that it will replace all
the existing rules for the product. You will usually not want to use this
@@ -417,6 +436,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
@@ -428,6 +448,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
Guewen Baconnier <guewen.baconnier@camptocamp.com>
Tecnativa:
diff --git a/stock_orderpoint_generator/tests/test_orderpoint_generator.py b/stock_orderpoint_generator/tests/test_orderpoint_generator.py
index 495519819..a39d70a2d 100644
--- a/stock_orderpoint_generator/tests/test_orderpoint_generator.py
+++ b/stock_orderpoint_generator/tests/test_orderpoint_generator.py
@@ -1,90 +1,390 @@
# Copyright 2016 Cyril Gaudin (Camptocamp)
+# Copyright 2019 Tecnativa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+from odoo import models
from odoo.exceptions import UserError
-from odoo.tests.common import TransactionCase
+from odoo.tests.common import SavepointCase
-class TestOrderpointGenerator(TransactionCase):
+class TestOrderpointGenerator(SavepointCase):
- def setUp(self):
- super(TestOrderpointGenerator, self).setUp()
-
- self.wizard_model = self.env['stock.warehouse.orderpoint.generator']
-
- self.orderpoint_model = self.env['stock.warehouse.orderpoint']
-
- self.orderpoint_template_model = self.env[
- 'stock.warehouse.orderpoint.template'
- ]
-
- self.product_model = self.env['product.product']
- self.p1 = self.product_model.create({'name': 'Unittest P1'})
- self.p2 = self.product_model.create({'name': 'Unittest P2'})
-
- self.assertEqual(0, self.orderpoint_model.search_count([
- ('name', '=', 'OP/000445')
- ]))
-
- self.template = self.orderpoint_template_model.create({
- 'company_id': self.ref('base.main_company'),
- 'location_id': self.ref('stock.stock_location_stock'),
- 'name': 'OP/000445',
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.wizard_model = cls.env['stock.warehouse.orderpoint.generator']
+ cls.orderpoint_model = cls.env['stock.warehouse.orderpoint']
+ cls.orderpoint_template_model = (
+ cls.env['stock.warehouse.orderpoint.template'])
+ cls.product_model = cls.env['product.product']
+ cls.p1 = cls.product_model.create({
+ 'name': 'Unittest P1',
+ 'type': 'product',
+ })
+ cls.p2 = cls.product_model.create({
+ 'name': 'Unittest P2',
+ 'type': 'product',
+ })
+ cls.wh1 = cls.env['stock.warehouse'].create({
+ 'name': 'TEST WH1',
+ 'code': 'TST1',
+ })
+ location_obj = cls.env['stock.location']
+ cls.supplier_loc = location_obj.create({
+ 'name': 'Test supplier location',
+ 'usage': 'supplier',
+ })
+ cls.customer_loc = location_obj.create({
+ 'name': 'Test customer location',
+ 'usage': 'customer',
+ })
+ cls.orderpoint_fields_dict = {
+ 'warehouse_id': cls.wh1.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'name': 'TEST-ORDERPOINT-001',
'product_max_qty': 15.0,
'product_min_qty': 5.0,
'qty_multiple': 1,
- 'warehouse_id': self.ref('stock.warehouse0')
+ }
+ cls.template = cls.orderpoint_template_model.create(
+ cls.orderpoint_fields_dict)
+ # Create some moves for p1 and p2 so we can have a history to test
+ # p1 [100, 50, 45, 55, 52]
+ # t1 - p1 - stock.move location1 100 # 100
+ cls.p1m1 = cls.env['stock.move'].create({
+ 'name': cls.p1.name,
+ 'product_id': cls.p1.id,
+ 'product_uom_qty': 100,
+ 'product_uom': cls.p1.uom_id.id,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 01:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p1m1.id,
+ 'product_id': cls.p1.id,
+ 'qty_done': 100,
+ 'product_uom_id': cls.p1.uom_id.id,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 01:00:00',
+ })
+ # t2 - p1 - stock.move location1 -50 # 50
+ cls.p1m2 = cls.p1m1.copy({
+ 'product_uom_qty': 50,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 02:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p1m2.id,
+ 'product_id': cls.p1.id,
+ 'qty_done': 50,
+ 'product_uom_id': cls.p1.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 02:00:00',
+ })
+ # t3 - p1 - stock.move location1 -5 # 45
+ cls.p1m3 = cls.p1m1.copy({
+ 'name': cls.p1.name,
+ 'product_id': cls.p1.id,
+ 'product_uom_qty': 5,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 03:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p1m3.id,
+ 'product_id': cls.p1.id,
+ 'qty_done': 5,
+ 'product_uom_id': cls.p1.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 03:00:00',
+ })
+ # t4 - p1 - stock.move location1 10 # 55
+ cls.p1m4 = cls.p1m1.copy({
+ 'product_uom_qty': 10,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 04:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p1m4.id,
+ 'product_id': cls.p1.id,
+ 'qty_done': 10,
+ 'product_uom_id': cls.p1.uom_id.id,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 04:00:00',
+ })
+ # t5 - p1 - stock.move location1 -3 # 52
+ cls.p1m5 = cls.p1m1.copy({
+ 'product_uom_qty': 3,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 05:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p1m5.id,
+ 'product_id': cls.p1.id,
+ 'qty_done': 3,
+ 'product_uom_id': cls.p1.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 05:00:00',
+ })
+ # p2
+ # t1 - p2 - stock.move location1 1000 # 1000
+ cls.p2m1 = cls.env['stock.move'].create({
+ 'name': cls.p2.name,
+ 'product_id': cls.p2.id,
+ 'product_uom': cls.p2.uom_id.id,
+ 'product_uom_qty': 1000,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 01:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p2m1.id,
+ 'product_id': cls.p2.id,
+ 'qty_done': 1000,
+ 'product_uom_id': cls.p2.uom_id.id,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 01:00:00',
+ })
+ # t2 - p2 - stock.move location1 -50 # 950
+ cls.p2m2 = cls.p2m1.copy({
+ 'product_uom_qty': 50,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 02:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p2m2.id,
+ 'product_id': cls.p2.id,
+ 'qty_done': 50,
+ 'product_uom_id': cls.p2.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 02:00:00',
+ })
+ # t3 - p2 - stock.move location1 -7 # 943
+ cls.p2m3 = cls.p2m1.copy({
+ 'product_uom_qty': 7,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 03:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p2m3.id,
+ 'product_id': cls.p2.id,
+ 'qty_done': 7,
+ 'product_uom_id': cls.p2.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 03:00:00',
+ })
+ # t4 - p2 - stock.move location1 100 # 1043
+ cls.p2m4 = cls.p2m1.copy({
+ 'product_uom_qty': 100,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 04:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p2m4.id,
+ 'product_id': cls.p2.id,
+ 'qty_done': 100,
+ 'product_uom_id': cls.p2.uom_id.id,
+ 'location_id': cls.supplier_loc.id,
+ 'location_dest_id': cls.wh1.lot_stock_id.id,
+ 'state': 'done',
+ 'date': '2019-01-01 04:00:00',
+ })
+ # t5 - p2 - stock.move location1 -3 # 1040
+ cls.p2m5 = cls.p2m1.copy({
+ 'product_uom_qty': 3,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 05:00:00',
+ })
+ cls.env['stock.move.line'].create({
+ 'move_id': cls.p2m5.id,
+ 'product_id': cls.p2.id,
+ 'qty_done': 3,
+ 'product_uom_id': cls.p2.uom_id.id,
+ 'location_id': cls.wh1.lot_stock_id.id,
+ 'location_dest_id': cls.customer_loc.id,
+ 'state': 'done',
+ 'date': '2019-01-01 05:00:00',
})
- def check_orderpoint(self):
+ def check_orderpoint(self, products, template, fields_dict):
orderpoints = self.orderpoint_model.search([
- ('name', '=', 'OP/000445')
+ ('name', '=', template.name)
], order='product_id')
-
- self.assertEqual(2, len(orderpoints))
-
- self.assertEqual(self.p1, orderpoints[0].product_id)
- self.assertEqual(self.p2, orderpoints[1].product_id)
-
+ self.assertEqual(len(products), len(orderpoints))
+ for i, product in enumerate(products):
+ self.assertEqual(product, orderpoints[i].product_id)
for orderpoint in orderpoints:
- for field in ('company_id', 'location_id', 'product_max_qty',
- 'product_min_qty', 'qty_multiple', 'warehouse_id'):
- self.assertEqual(orderpoint[field], self.template[field])
+ for field in fields_dict.keys():
+ op_field_value = orderpoint[field]
+ if isinstance(orderpoint[field], models.Model):
+ op_field_value = orderpoint[field].id
+ self.assertEqual(op_field_value, fields_dict[field])
+ return orderpoints
+
+ def wizard_over_products(self, product, template):
+ return self.wizard_model.with_context(
+ active_model=product._name,
+ active_ids=product.ids,
+ ).create({
+ 'orderpoint_template_id': [(6, 0, template.ids)]
+ })
def test_product_orderpoint(self):
-
- wizard = self.wizard_model.with_context(
- active_ids=[self.p1.id, self.p2.id]
- ).create({
- 'orderpoint_template_id': [(6, 0, [self.template.id])]
- })
+ products = self.p1 + self.p2
+ wizard = self.wizard_over_products(products, self.template)
wizard.action_configure()
-
- self.check_orderpoint()
+ self.check_orderpoint(
+ products, self.template, self.orderpoint_fields_dict)
def test_template_orderpoint(self):
-
- wizard = self.wizard_model.with_context(
- active_model='product.template',
- active_ids=[self.p1.product_tmpl_id.id, self.p2.product_tmpl_id.id]
- ).create({
- 'orderpoint_template_id': [(6, 0, [self.template.id])]
- })
+ prod_tmpl = self.p1.product_tmpl_id + self.p2.product_tmpl_id
+ wizard = self.wizard_over_products(prod_tmpl, self.template)
wizard.action_configure()
-
- self.check_orderpoint()
+ products = self.p1 + self.p2
+ self.check_orderpoint(
+ products, self.template, self.orderpoint_fields_dict)
def test_template_variants_orderpoint(self):
-
self.product_model.create({
'product_tmpl_id': self.p1.product_tmpl_id.id,
'name': 'Unittest P1 variant'
})
-
- wizard = self.wizard_model.with_context(
- active_model='product.template',
- active_ids=[self.p1.product_tmpl_id.id]
- ).create({
- 'orderpoint_template_id': [(6, 0, [self.template.id])]
- })
+ wizard = self.wizard_over_products(
+ self.p1.product_tmpl_id, self.template)
with self.assertRaises(UserError):
wizard.action_configure()
+
+ def test_auto_qty(self):
+ """Compute min and max qty according to criteria"""
+ # Max stock for p1: 100
+ self.template.write({
+ 'auto_min_qty': True,
+ 'auto_min_date_start': '2019-01-01 00:00:00',
+ 'auto_min_date_end': '2019-02-01 00:00:00',
+ 'auto_min_qty_criteria': 'max',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict = self.orderpoint_fields_dict.copy()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 100.0,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # Min stock for p1: 45
+ self.template.write({
+ 'auto_min_qty_criteria': 'min',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 45.0,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # Median of stock for p1: 52
+ self.template.write({
+ 'auto_min_qty_criteria': 'median',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 52.0,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # Average of stock for p1: 60.4
+ self.template.write({
+ 'auto_min_qty_criteria': 'avg',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 60.4,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # Set auto values for min and max: 60.4 (avg) 100 (max)
+ self.template.write({
+ 'auto_max_qty': True,
+ 'auto_max_date_start': '2019-01-01 00:00:00',
+ 'auto_max_date_end': '2019-02-01 00:00:00',
+ 'auto_max_qty_criteria': 'max',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_max_qty': 100,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # If they have the same values, only one is computed:
+ self.template.write({
+ 'auto_min_qty_criteria': 'max',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 100,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+ # Auto min max over a shorter period
+ self.template.write({
+ 'auto_max_date_start': '2019-01-01 02:00:00',
+ 'auto_max_date_end': '2019-01-01 03:00:00',
+ 'auto_min_date_start': '2019-01-01 04:00:00',
+ 'auto_min_date_end': '2019-01-01 06:00:00',
+ })
+ wizard = self.wizard_over_products(self.p1, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict.update({
+ 'product_min_qty': 55,
+ 'product_max_qty': 50,
+ })
+ self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict)
+
+ def test_auto_qty_multi_products(self):
+ """Each product has a different history"""
+ products = self.p1 + self.p2
+ self.template.write({
+ 'auto_min_qty': True,
+ 'auto_min_date_start': '2019-01-01 00:00:00',
+ 'auto_min_date_end': '2019-02-01 00:00:00',
+ 'auto_min_qty_criteria': 'max',
+ })
+ wizard = self.wizard_over_products(products, self.template)
+ wizard.action_configure()
+ orderpoint_auto_dict = self.orderpoint_fields_dict.copy()
+ del orderpoint_auto_dict['product_min_qty']
+ orderpoints = self.check_orderpoint(
+ products, self.template, orderpoint_auto_dict)
+ self.assertEqual(orderpoints[0].product_min_qty, 100)
+ self.assertEqual(orderpoints[1].product_min_qty, 1043)
diff --git a/stock_orderpoint_generator/views/orderpoint_template_views.xml b/stock_orderpoint_generator/views/orderpoint_template_views.xml
index ed0342fd0..dd7bf9996 100644
--- a/stock_orderpoint_generator/views/orderpoint_template_views.xml
+++ b/stock_orderpoint_generator/views/orderpoint_template_views.xml
@@ -10,8 +10,10 @@
-
-
+
+
+
+
@@ -59,8 +61,10 @@
-
-
+
+
+
+
@@ -72,6 +76,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/stock_orderpoint_generator/wizard/orderpoint_generator.py b/stock_orderpoint_generator/wizard/orderpoint_generator.py
index a8cdb709e..b83eaf8aa 100644
--- a/stock_orderpoint_generator/wizard/orderpoint_generator.py
+++ b/stock_orderpoint_generator/wizard/orderpoint_generator.py
@@ -25,20 +25,18 @@ class OrderpointGenerator(models.TransientModel):
@api.multi
def action_configure(self):
- """ Action to retrieve wizard data and launch creation of items.
- """
+ """Action to retrieve wizard data and launch creation of items."""
self.ensure_one()
-
- product_ids = self.env.context.get('active_ids')
- assert product_ids and isinstance(product_ids, list)
-
+ model_obj = self.env[self.env.context.get('active_model')]
+ record_ids = model_obj.browse(self.env.context.get('active_ids'))
+ if not record_ids:
+ return model_obj
if self.env.context.get('active_model') == 'product.template':
- templates = self.env['product.template'].browse(product_ids)
- product_ids = templates.mapped('product_variant_ids.id')
- if len(product_ids) != len(templates):
+ product_ids = record_ids.mapped('product_variant_ids')
+ if len(product_ids) != len(record_ids):
raise UserError(_(
'Cannot apply because some of selected '
'products has multiple variants.'
))
-
- self.orderpoint_template_id.create_orderpoints(product_ids)
+ record_ids = product_ids
+ self.orderpoint_template_id.create_orderpoints(record_ids)