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",
|
||||
"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": [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='bill']" position="before">
|
||||
<group string="Purchase Recommendation">
|
||||
<field name="po_min_suggest_if_forecast_le_zero"/>
|
||||
</group>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Purchase Recommendation">
|
||||
<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>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue