From 3a35955d24a5c71c841d64295d5e4bb648a25022 Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 6 Oct 2025 12:10:46 +0200 Subject: [PATCH 1/3] purchase_order_product_recommendation_supermarket: make prevous period data optional --- .../wizards/purchase_order_recommendation.py | 70 +++++++++++-------- .../wizards/purchase_order_recommendation.xml | 15 +++- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py index 95355aa..6731a91 100644 --- a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py +++ b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py @@ -20,6 +20,11 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): default=False, help="If enabled, days when the product stock was 0 or less will not be considered in the daily sales calculation.", ) + include_previous_period = fields.Boolean( + string="Include previous period stats", + default=True, + help="If disabled, the previous period (same number of days immediately preceding) is NOT queried. This saves a read_group on stock move lines and skips zero-stock day analysis for that window.", + ) total_days = fields.Integer( string="Total days", compute="_compute_total_days", @@ -193,36 +198,37 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): if len(found_scrapped): res["units_scrapped"] = found_scrapped[0]["qty_done"] - days = self._get_total_days() - prev_date_end = self.date_begin - timedelta(days=1) - prev_date_begin = self.date_begin - timedelta(days=days) - domain = self.with_context( - { - "period_date_begin": prev_date_begin, - "period_date_end": prev_date_end, - } - )._get_move_line_domain(product_id, src="internal", dst="customer") - found_previous_period = self.env["stock.move.line"].read_group( - domain, ["product_id", "qty_done"], ["product_id"] - ) - if len(found_previous_period): - res["units_delivered_prev"] = found_previous_period[0]["qty_done"] - if self.ignore_zero_stock_days: - days_with_stock = days - self.with_context( - { - "period_date_begin": prev_date_begin, - "period_date_end": prev_date_end, - } - )._get_days_out_of_stock(product_id) - res["units_avg_delivered_prev"] = ( - res["units_delivered_prev"] / days_with_stock - if days_with_stock != 0 - else 1 - ) - else: - res["units_avg_delivered_prev"] = ( - found_previous_period[0]["qty_done"] / days if days != 0 else 1 - ) + if self.include_previous_period: + days = self._get_total_days() + prev_date_end = self.date_begin - timedelta(days=1) + prev_date_begin = self.date_begin - timedelta(days=days) + domain = self.with_context( + { + "period_date_begin": prev_date_begin, + "period_date_end": prev_date_end, + } + )._get_move_line_domain(product_id, src="internal", dst="customer") + found_previous_period = self.env["stock.move.line"].read_group( + domain, ["product_id", "qty_done"], ["product_id"] + ) + if len(found_previous_period): + res["units_delivered_prev"] = found_previous_period[0]["qty_done"] + if self.ignore_zero_stock_days: + days_with_stock = days - self.with_context( + { + "period_date_begin": prev_date_begin, + "period_date_end": prev_date_end, + } + )._get_days_out_of_stock(product_id) + res["units_avg_delivered_prev"] = ( + res["units_delivered_prev"] / days_with_stock + if days_with_stock != 0 + else 1 + ) + else: + res["units_avg_delivered_prev"] = ( + found_previous_period[0]["qty_done"] / days if days != 0 else 1 + ) seller = product_id._select_seller( partner_id=self.order_id.partner_id, date=fields.Date.today(), @@ -273,6 +279,10 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): class PurchaseOrderRecommendationLine(models.TransientModel): _inherit = "purchase.order.recommendation.line" + include_previous_period = fields.Boolean( + related="wizard_id.include_previous_period", store=False, readonly=True + ) + packaging_id = fields.Many2one( comodel_name="product.packaging", string="Packaging", diff --git a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.xml b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.xml index 27d67ed..f62749b 100644 --- a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.xml +++ b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.xml @@ -25,6 +25,7 @@ + @@ -32,6 +33,15 @@ hide + + show + + + show + + + show + show @@ -39,8 +49,9 @@ - - + + + From 65dd899963cf2bc2d07d26c11c70c1e8e22b0539 Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 6 Oct 2025 12:51:58 +0200 Subject: [PATCH 2/3] purchase_order_product_recommendation_supermarket: add a setting in products to suggest a minimum purchase when forecast <= 0 --- .../__init__.py | 1 + .../__manifest__.py | 5 +++- .../i18n/es.po | 29 +++++++++++++++++-- ...der_product_recommendation_supermarket.pot | 25 ++++++++++++++++ .../models/__init__.py | 1 + .../models/product_template.py | 10 +++++++ .../views/product_template_view.xml | 15 ++++++++++ .../wizards/purchase_order_recommendation.py | 11 ++++++- 8 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 purchase_order_product_recommendation_supermarket/models/__init__.py create mode 100644 purchase_order_product_recommendation_supermarket/models/product_template.py create mode 100644 purchase_order_product_recommendation_supermarket/views/product_template_view.xml diff --git a/purchase_order_product_recommendation_supermarket/__init__.py b/purchase_order_product_recommendation_supermarket/__init__.py index 5cb1c49..aee8895 100644 --- a/purchase_order_product_recommendation_supermarket/__init__.py +++ b/purchase_order_product_recommendation_supermarket/__init__.py @@ -1 +1,2 @@ +from . import models from . import wizards diff --git a/purchase_order_product_recommendation_supermarket/__manifest__.py b/purchase_order_product_recommendation_supermarket/__manifest__.py index 51393df..6797bee 100644 --- a/purchase_order_product_recommendation_supermarket/__manifest__.py +++ b/purchase_order_product_recommendation_supermarket/__manifest__.py @@ -9,6 +9,9 @@ "author": "Criptomart", "website": "https://criptomart.net", "depends": ["purchase_order_product_recommendation"], - "data": ["wizards/purchase_order_recommendation.xml"], + "data": [ + "wizards/purchase_order_recommendation.xml", + "views/product_template_view.xml", + ], "demo": [], } diff --git a/purchase_order_product_recommendation_supermarket/i18n/es.po b/purchase_order_product_recommendation_supermarket/i18n/es.po index 1ba61da..937d6f4 100644 --- a/purchase_order_product_recommendation_supermarket/i18n/es.po +++ b/purchase_order_product_recommendation_supermarket/i18n/es.po @@ -15,6 +15,31 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period +msgid "Include previous period stats" +msgstr "Incluir estadísticas del periodo previo" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period +msgid "If disabled, the previous period (same number of days immediately preceding) is NOT queried. This saves a read_group on stock move lines and skips zero-stock day analysis for that window." +msgstr "Si se desactiva, no se consulta el periodo previo (mismo número de días inmediatamente anteriores). Ahorra tiempo en el cálculo al no consultar movimientos de stock y omite el análisis de días sin stock para esa ventana de tiempo." + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_template__po_min_suggest_if_forecast_le_zero +msgid "Suggest minimum purchase when forecast <= 0" +msgstr "Sugerir compra mínima cuando el stock previsto <= 0" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_template__po_min_suggest_if_forecast_le_zero +msgid "If enabled, the purchase recommendation wizard will always propose at least 1 unit (or 1 full package when ordering by packages) when the forecasted stock is less or equal to 0." +msgstr "Si se activa, el asistente de recomendación de compras propondrá siempre al menos 1 unidad (o 1 paquete completo al pedir por paquetes) cuando el stock previsto sea menor o igual que 0." + +#. module: purchase_order_product_recommendation_supermarket +#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation_supermarket.view_product_template_form_po_min_suggest +msgid "Purchase Recommendation" +msgstr "Recomendación de compra" + #. module: purchase_order_product_recommendation_supermarket #: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation_line__units_avg_delivered_prev msgid "Average daily consumption during the previous period" @@ -101,9 +126,9 @@ msgid "" "Indicate for how many days the new order should cover the stock. If not set," " the default module behavior is kept." msgstr "" -"Indica durante cuantos días el nuevo pedido debe cubrir el stock. Sino se " +"Indica durante cuantos días el nuevo pedido debe cubrir el stock. Si no se " "rellena este campo las recomendaciones mostradas se calcularán para cubrir " -"un periodo equivalente al mostrado en días totales." +"un periodo equivalente al mostrado en días analizados." #. module: purchase_order_product_recommendation_supermarket #: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__order_by_packages diff --git a/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot b/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot index 33e561d..0f5845f 100644 --- a/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot +++ b/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot @@ -15,6 +15,31 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period +msgid "Include previous period stats" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period +msgid "If disabled, the previous period (same number of days immediately preceding) is NOT queried. This saves a read_group on stock move lines and skips zero-stock day analysis for that window." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_template__po_min_suggest_if_forecast_le_zero +msgid "Suggest minimum purchase when forecast <= 0" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_template__po_min_suggest_if_forecast_le_zero +msgid "If enabled, the purchase recommendation wizard will always propose at least 1 unit (or 1 full package when ordering by packages) when the forecasted stock is less or equal to 0." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation_supermarket.view_product_template_form_po_min_suggest +msgid "Purchase Recommendation" +msgstr "" + #. module: purchase_order_product_recommendation_supermarket #: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation_line__units_avg_delivered_prev msgid "Average daily consumption during the previous period" diff --git a/purchase_order_product_recommendation_supermarket/models/__init__.py b/purchase_order_product_recommendation_supermarket/models/__init__.py new file mode 100644 index 0000000..e8fa8f6 --- /dev/null +++ b/purchase_order_product_recommendation_supermarket/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/purchase_order_product_recommendation_supermarket/models/product_template.py b/purchase_order_product_recommendation_supermarket/models/product_template.py new file mode 100644 index 0000000..e705777 --- /dev/null +++ b/purchase_order_product_recommendation_supermarket/models/product_template.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + po_min_suggest_if_forecast_le_zero = fields.Boolean( + string='Suggest minimum purchase when forecast <= 0', + help='If enabled, the purchase recommendation wizard will always propose at least 1 unit (or 1 full package when ordering by packages) when the forecasted stock is less or equal to 0.' + ) diff --git a/purchase_order_product_recommendation_supermarket/views/product_template_view.xml b/purchase_order_product_recommendation_supermarket/views/product_template_view.xml new file mode 100644 index 0000000..4d241d2 --- /dev/null +++ b/purchase_order_product_recommendation_supermarket/views/product_template_view.xml @@ -0,0 +1,15 @@ + + + + product.template.form.po.min.suggest + product.template + + + + + + + + + + diff --git a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py index 6731a91..d8fd3c2 100644 --- a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py +++ b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py @@ -22,7 +22,7 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): ) include_previous_period = fields.Boolean( string="Include previous period stats", - default=True, + default=False, help="If disabled, the previous period (same number of days immediately preceding) is NOT queried. This saves a read_group on stock move lines and skips zero-stock day analysis for that window.", ) total_days = fields.Integer( @@ -181,6 +181,15 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): (self.order_days * res["units_avg_delivered"]) - res["units_virtual_available"], ) + + # Force a minimum suggested quantity when forecast <= 0 and product configured + # We apply this BEFORE packaging adjustment so packages logic can upscale it. + if ( + product_id.po_min_suggest_if_forecast_le_zero + and res["units_virtual_available"] <= 0 + ): + # If ordering by packages we later bump to one full package. + qty_to_order = max(qty_to_order, 1) res["units_included_original"] = qty_to_order # Adjust qty_to_order to packaging multiples if order_by_packages is checked From 2c52a9f36e1f0a592b08b6c839417746dbfc72d4 Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 6 Oct 2025 14:19:28 +0200 Subject: [PATCH 3/3] CriptoMart/red-supermercados-coop#1 purchase_order_product_recommendation_supermarket: add cron to store periods out of stock in products. compute days without stock based on this new model --- .../__manifest__.py | 2 + .../data/stock_out_cron.xml | 15 +++ .../i18n/es.po | 60 ++++++++++ ...der_product_recommendation_supermarket.pot | 60 ++++++++++ .../models/product_template.py | 105 +++++++++++++++++- .../security/ir.model.access.csv | 2 + .../views/product_template_view.xml | 29 ++++- .../wizards/purchase_order_recommendation.py | 33 +++--- 8 files changed, 285 insertions(+), 21 deletions(-) create mode 100644 purchase_order_product_recommendation_supermarket/data/stock_out_cron.xml create mode 100644 purchase_order_product_recommendation_supermarket/security/ir.model.access.csv diff --git a/purchase_order_product_recommendation_supermarket/__manifest__.py b/purchase_order_product_recommendation_supermarket/__manifest__.py index 6797bee..27a1a9b 100644 --- a/purchase_order_product_recommendation_supermarket/__manifest__.py +++ b/purchase_order_product_recommendation_supermarket/__manifest__.py @@ -10,8 +10,10 @@ "website": "https://criptomart.net", "depends": ["purchase_order_product_recommendation"], "data": [ + "security/ir.model.access.csv", "wizards/purchase_order_recommendation.xml", "views/product_template_view.xml", + "data/stock_out_cron.xml", ], "demo": [], } diff --git a/purchase_order_product_recommendation_supermarket/data/stock_out_cron.xml b/purchase_order_product_recommendation_supermarket/data/stock_out_cron.xml new file mode 100644 index 0000000..72843b6 --- /dev/null +++ b/purchase_order_product_recommendation_supermarket/data/stock_out_cron.xml @@ -0,0 +1,15 @@ + + + + Update Stock-out Periods + + code + model.cron_update_stock_out_periods() + + True + 1 + days + -1 + False + + diff --git a/purchase_order_product_recommendation_supermarket/i18n/es.po b/purchase_order_product_recommendation_supermarket/i18n/es.po index 937d6f4..b05b268 100644 --- a/purchase_order_product_recommendation_supermarket/i18n/es.po +++ b/purchase_order_product_recommendation_supermarket/i18n/es.po @@ -15,6 +15,66 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: purchase_order_product_recommendation_supermarket +#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation_supermarket.view_product_template_form_po_min_suggest +msgid "Stock-out Periods" +msgstr "Periodos sin stock" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_template__stock_out_period_ids +msgid "Stock-out Periods" +msgstr "Periodos sin stock" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__start_date +msgid "Start Date" +msgstr "Fecha inicio" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__end_date +msgid "End Date" +msgstr "Fecha fin" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__start_date +msgid "First calendar day when the product had zero stock in this period." +msgstr "Primer día (calendario) en el que el producto estuvo sin stock en este periodo." + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__end_date +msgid "Last calendar day (inclusive) the product remained without stock in this period." +msgstr "Último día (incluido) en el que el producto siguió sin stock en este periodo." + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__is_open +msgid "Open" +msgstr "Abierto" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__days_out +msgid "Days without Stock" +msgstr "Días sin stock" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__product_tmpl_id +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__product_tmpl_id +msgid "Product template this stock-out period belongs to." +msgstr "Plantilla de producto a la que pertenece este periodo sin stock." + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__is_open +msgid "Indicates the period is still ongoing (stock not yet restored)." +msgstr "Indica que el periodo sigue en curso (el stock todavía no se ha recuperado)." + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__days_out +msgid "Total number of calendar days (inclusive) without stock for this period." +msgstr "Número total de días naturales (incluidos) sin stock durante este periodo." + #. module: purchase_order_product_recommendation_supermarket #: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period msgid "Include previous period stats" diff --git a/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot b/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot index 0f5845f..c2dd1d3 100644 --- a/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot +++ b/purchase_order_product_recommendation_supermarket/i18n/purchase_order_product_recommendation_supermarket.pot @@ -15,6 +15,66 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: purchase_order_product_recommendation_supermarket +#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation_supermarket.view_product_template_form_po_min_suggest +msgid "Stock-out Periods" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_template__stock_out_period_ids +msgid "Stock-out Periods" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__start_date +msgid "Start Date" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__end_date +msgid "End Date" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__start_date +msgid "First calendar day when the product had zero stock in this period." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__end_date +msgid "Last calendar day (inclusive) the product remained without stock in this period." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__is_open +msgid "Open" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__days_out +msgid "Days without Stock" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__product_tmpl_id +msgid "Product Template" +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__product_tmpl_id +msgid "Product template this stock-out period belongs to." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__is_open +msgid "Indicates the period is still ongoing (stock not yet restored)." +msgstr "" + +#. module: purchase_order_product_recommendation_supermarket +#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_product_stock_out_period__days_out +msgid "Total number of calendar days (inclusive) without stock for this period." +msgstr "" + #. module: purchase_order_product_recommendation_supermarket #: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period msgid "Include previous period stats" diff --git a/purchase_order_product_recommendation_supermarket/models/product_template.py b/purchase_order_product_recommendation_supermarket/models/product_template.py index e705777..f97150d 100644 --- a/purchase_order_product_recommendation_supermarket/models/product_template.py +++ b/purchase_order_product_recommendation_supermarket/models/product_template.py @@ -1,10 +1,107 @@ -from odoo import models, fields +from odoo import models, fields, api +from datetime import date class ProductTemplate(models.Model): - _inherit = 'product.template' + _inherit = "product.template" po_min_suggest_if_forecast_le_zero = fields.Boolean( - string='Suggest minimum purchase when forecast <= 0', - help='If enabled, the purchase recommendation wizard will always propose at least 1 unit (or 1 full package when ordering by packages) when the forecasted stock is less or equal to 0.' + string="Suggest minimum purchase when forecast <= 0", + help="If enabled, the purchase recommendation wizard will always propose at least 1 unit (or 1 full package when ordering by packages) when the forecasted stock is less or equal to 0.", ) + + stock_out_period_ids = fields.One2many( + "product.stock.out.period", + "product_tmpl_id", + string="Stock-out Periods", + help="Historical periods where this product had zero stock (qty_available <= 0).", + ) + + def cron_update_stock_out_periods(self): + """Scheduled every night: detect products currently without stock and + update (or create) an open stock-out period. While a period is open: + - end_date is kept updated each day with today's date + - days_out reflects (end_date - start_date).days + When stock returns the period is marked closed (is_open=False) but end_date + keeps the last day without stock (previous day if we consider stock restored today). + """ + # Consider only storable products + products = self.search([("type", "=", "product")]) + today = date.today() + # Prefetch qty_available efficiently (framework batches reads) + for product in products: + qty = product.qty_available + open_period = product.stock_out_period_ids.filtered(lambda p: p.is_open)[:1] + if qty <= 0: + if open_period: + # Refresh end_date to today (still open) + if open_period.end_date != today: + open_period.end_date = today + open_period._compute_days_out() + else: + self.env["product.stock.out.period"].create( + { + "product_tmpl_id": product.id, + "start_date": today, + "end_date": today, + "is_open": True, + } + ) + else: + if open_period: + # Close period: stock restored today -> last day without stock was yesterday + # We set end_date to max(start_date, today - 1) to avoid negative spans. + from datetime import timedelta + + closed_end = max(open_period.start_date, today - timedelta(days=1)) + if open_period.end_date != closed_end: + open_period.end_date = closed_end + open_period.is_open = False + open_period._compute_days_out() + + +class ProductStockOutPeriod(models.Model): + _name = "product.stock.out.period" + _description = "Product Stock-out Period" + _order = "start_date desc" + + product_tmpl_id = fields.Many2one( + "product.template", + required=True, + ondelete="cascade", + index=True, + string="Product Template", + help="Product template this stock-out period belongs to.", + ) + start_date = fields.Date( + required=True, + index=True, + string="Start Date", + help="First calendar day when the product had zero stock in this period.", + ) + end_date = fields.Date( + index=True, + string="End Date", + help="Last calendar day (inclusive) the product remained without stock in this period.", + ) + is_open = fields.Boolean( + string="Open", + default=False, + index=True, + help="Indicates the period is still ongoing (stock not yet restored).", + ) + days_out = fields.Integer( + string="Days without Stock", + compute="_compute_days_out", + store=True, + help="Total number of calendar days (inclusive) without stock for this period.", + ) + + @api.depends("start_date", "end_date", "is_open") + def _compute_days_out(self): + for rec in self: + if rec.start_date and rec.end_date: + # Inclusive count of days (both start and end) + rec.days_out = (rec.end_date - rec.start_date).days + 1 + else: + rec.days_out = 0 diff --git a/purchase_order_product_recommendation_supermarket/security/ir.model.access.csv b/purchase_order_product_recommendation_supermarket/security/ir.model.access.csv new file mode 100644 index 0000000..15f0899 --- /dev/null +++ b/purchase_order_product_recommendation_supermarket/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_stock_out_period_user,access.product.stock.out.period.user,model_product_stock_out_period,base.group_user,1,0,0,0 diff --git a/purchase_order_product_recommendation_supermarket/views/product_template_view.xml b/purchase_order_product_recommendation_supermarket/views/product_template_view.xml index 4d241d2..09d2982 100644 --- a/purchase_order_product_recommendation_supermarket/views/product_template_view.xml +++ b/purchase_order_product_recommendation_supermarket/views/product_template_view.xml @@ -5,10 +5,31 @@ product.template - - - - + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+
diff --git a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py index d8fd3c2..cc65484 100644 --- a/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py +++ b/purchase_order_product_recommendation_supermarket/wizards/purchase_order_recommendation.py @@ -254,24 +254,31 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel): line.unlink() def _get_days_out_of_stock(self, product): + """Compute days without stock using precomputed periods (product.stock.out.period). + + A day is counted if it belongs to any stored stock-out period overlapping + the target window [date_from, date_to]. The period model stores end_date daily + (or closure day). """ - Returns the number of days between date_begin and date_end - where the given product had zero or negative stock (qty_available). - """ - days_out_of_stock = 0 date_from = self.env.context.get("period_date_begin", self.date_begin) date_to = self.env.context.get("period_date_end", self.date_end) if not date_from or not date_to: return 0 - # Loop through each day in the range - for n in range((date_to - date_from).days + 1): - day = date_from + timedelta(days=n) - qty = product.with_context( - to_date=datetime.combine(day, time(23, 59, 59)) - ).qty_available - if qty <= 0: - days_out_of_stock += 1 - return days_out_of_stock + # Access template periods (product may be product.product or product.template) + product_tmpl = ( + product.product_tmpl_id if hasattr(product, "product_tmpl_id") else product + ) + periods = product_tmpl.stock_out_period_ids + total = 0 + for p in periods: + if not p.start_date or not p.end_date: + continue + # Overlap window + start = max(p.start_date, date_from) + end = min(p.end_date, date_to) + if start <= end: + total += (end - start).days + return total def _get_products(self): """Overwrite because filter by cateogory is not working in the base method."""