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."""