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
This commit is contained in:
parent
65dd899963
commit
2c52a9f36e
8 changed files with 285 additions and 21 deletions
|
|
@ -10,8 +10,10 @@
|
||||||
"website": "https://criptomart.net",
|
"website": "https://criptomart.net",
|
||||||
"depends": ["purchase_order_product_recommendation"],
|
"depends": ["purchase_order_product_recommendation"],
|
||||||
"data": [
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
"wizards/purchase_order_recommendation.xml",
|
"wizards/purchase_order_recommendation.xml",
|
||||||
"views/product_template_view.xml",
|
"views/product_template_view.xml",
|
||||||
|
"data/stock_out_cron.xml",
|
||||||
],
|
],
|
||||||
"demo": [],
|
"demo": [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="ir_cron_update_stock_out_periods" model="ir.cron">
|
||||||
|
<field name="name">Update Stock-out Periods</field>
|
||||||
|
<field name="model_id" ref="product.model_product_template"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.cron_update_stock_out_periods()</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="active">True</field>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="doall">False</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -15,6 +15,66 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: \n"
|
"Content-Transfer-Encoding: \n"
|
||||||
"Plural-Forms: \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
|
#. module: purchase_order_product_recommendation_supermarket
|
||||||
#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period
|
#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period
|
||||||
msgid "Include previous period stats"
|
msgid "Include previous period stats"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,66 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: \n"
|
"Content-Transfer-Encoding: \n"
|
||||||
"Plural-Forms: \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
|
#. module: purchase_order_product_recommendation_supermarket
|
||||||
#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period
|
#: model:ir.model.fields,field_description:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__include_previous_period
|
||||||
msgid "Include previous period stats"
|
msgid "Include previous period stats"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,107 @@
|
||||||
from odoo import models, fields
|
from odoo import models, fields, api
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
class ProductTemplate(models.Model):
|
class ProductTemplate(models.Model):
|
||||||
_inherit = 'product.template'
|
_inherit = "product.template"
|
||||||
|
|
||||||
po_min_suggest_if_forecast_le_zero = fields.Boolean(
|
po_min_suggest_if_forecast_le_zero = fields.Boolean(
|
||||||
string='Suggest minimum purchase when forecast <= 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.'
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
@ -5,10 +5,31 @@
|
||||||
<field name="model">product.template</field>
|
<field name="model">product.template</field>
|
||||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//group[@name='bill']" position="before">
|
<xpath expr="//notebook" position="inside">
|
||||||
<group string="Purchase Recommendation">
|
<page string="Purchase Recommendation">
|
||||||
<field name="po_min_suggest_if_forecast_le_zero"/>
|
<group>
|
||||||
</group>
|
<field name="po_min_suggest_if_forecast_le_zero"/>
|
||||||
|
</group>
|
||||||
|
<group string="Stock-out Periods">
|
||||||
|
<field name="stock_out_period_ids" context="{'default_product_tmpl_id': active_id}">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="days_out"/>
|
||||||
|
<field name="is_open"/>
|
||||||
|
</tree>
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="product_tmpl_id"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="days_out"/>
|
||||||
|
<field name="is_open"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -254,24 +254,31 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
|
||||||
line.unlink()
|
line.unlink()
|
||||||
|
|
||||||
def _get_days_out_of_stock(self, product):
|
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_from = self.env.context.get("period_date_begin", self.date_begin)
|
||||||
date_to = self.env.context.get("period_date_end", self.date_end)
|
date_to = self.env.context.get("period_date_end", self.date_end)
|
||||||
if not date_from or not date_to:
|
if not date_from or not date_to:
|
||||||
return 0
|
return 0
|
||||||
# Loop through each day in the range
|
# Access template periods (product may be product.product or product.template)
|
||||||
for n in range((date_to - date_from).days + 1):
|
product_tmpl = (
|
||||||
day = date_from + timedelta(days=n)
|
product.product_tmpl_id if hasattr(product, "product_tmpl_id") else product
|
||||||
qty = product.with_context(
|
)
|
||||||
to_date=datetime.combine(day, time(23, 59, 59))
|
periods = product_tmpl.stock_out_period_ids
|
||||||
).qty_available
|
total = 0
|
||||||
if qty <= 0:
|
for p in periods:
|
||||||
days_out_of_stock += 1
|
if not p.start_date or not p.end_date:
|
||||||
return days_out_of_stock
|
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):
|
def _get_products(self):
|
||||||
"""Overwrite because filter by cateogory is not working in the base method."""
|
"""Overwrite because filter by cateogory is not working in the base method."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue