Compare commits
3 commits
b153fdc7af
...
2c52a9f36e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c52a9f36e | |||
| 65dd899963 | |||
| 3a35955d24 |
11 changed files with 422 additions and 48 deletions
|
|
@ -1 +1,2 @@
|
||||||
|
from . import models
|
||||||
from . import wizards
|
from . import wizards
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@
|
||||||
"author": "Criptomart",
|
"author": "Criptomart",
|
||||||
"website": "https://criptomart.net",
|
"website": "https://criptomart.net",
|
||||||
"depends": ["purchase_order_product_recommendation"],
|
"depends": ["purchase_order_product_recommendation"],
|
||||||
"data": ["wizards/purchase_order_recommendation.xml"],
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"wizards/purchase_order_recommendation.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,91 @@ 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
|
||||||
|
#: 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
|
#. 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
|
#: 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"
|
msgid "Average daily consumption during the previous period"
|
||||||
|
|
@ -101,9 +186,9 @@ msgid ""
|
||||||
"Indicate for how many days the new order should cover the stock. If not set,"
|
"Indicate for how many days the new order should cover the stock. If not set,"
|
||||||
" the default module behavior is kept."
|
" the default module behavior is kept."
|
||||||
msgstr ""
|
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 "
|
"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
|
#. module: purchase_order_product_recommendation_supermarket
|
||||||
#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__order_by_packages
|
#: model:ir.model.fields,help:purchase_order_product_recommendation_supermarket.field_purchase_order_recommendation__order_by_packages
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,91 @@ 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
|
||||||
|
#: 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
|
#. 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
|
#: 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"
|
msgid "Average daily consumption during the previous period"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import product_template
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
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.",
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_product_template_form_po_min_suggest" model="ir.ui.view">
|
||||||
|
<field name="name">product.template.form.po.min.suggest</field>
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<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>
|
||||||
|
</odoo>
|
||||||
|
|
@ -20,6 +20,11 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
|
||||||
default=False,
|
default=False,
|
||||||
help="If enabled, days when the product stock was 0 or less will not be considered in the daily sales calculation.",
|
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=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(
|
total_days = fields.Integer(
|
||||||
string="Total days",
|
string="Total days",
|
||||||
compute="_compute_total_days",
|
compute="_compute_total_days",
|
||||||
|
|
@ -176,6 +181,15 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
|
||||||
(self.order_days * res["units_avg_delivered"])
|
(self.order_days * res["units_avg_delivered"])
|
||||||
- res["units_virtual_available"],
|
- 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
|
res["units_included_original"] = qty_to_order
|
||||||
|
|
||||||
# Adjust qty_to_order to packaging multiples if order_by_packages is checked
|
# Adjust qty_to_order to packaging multiples if order_by_packages is checked
|
||||||
|
|
@ -193,36 +207,37 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
|
||||||
if len(found_scrapped):
|
if len(found_scrapped):
|
||||||
res["units_scrapped"] = found_scrapped[0]["qty_done"]
|
res["units_scrapped"] = found_scrapped[0]["qty_done"]
|
||||||
|
|
||||||
days = self._get_total_days()
|
if self.include_previous_period:
|
||||||
prev_date_end = self.date_begin - timedelta(days=1)
|
days = self._get_total_days()
|
||||||
prev_date_begin = self.date_begin - timedelta(days=days)
|
prev_date_end = self.date_begin - timedelta(days=1)
|
||||||
domain = self.with_context(
|
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,
|
"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(
|
)._get_move_line_domain(product_id, src="internal", dst="customer")
|
||||||
domain, ["product_id", "qty_done"], ["product_id"]
|
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 len(found_previous_period):
|
||||||
if self.ignore_zero_stock_days:
|
res["units_delivered_prev"] = found_previous_period[0]["qty_done"]
|
||||||
days_with_stock = days - self.with_context(
|
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,
|
"period_date_begin": prev_date_begin,
|
||||||
}
|
"period_date_end": prev_date_end,
|
||||||
)._get_days_out_of_stock(product_id)
|
}
|
||||||
res["units_avg_delivered_prev"] = (
|
)._get_days_out_of_stock(product_id)
|
||||||
res["units_delivered_prev"] / days_with_stock
|
res["units_avg_delivered_prev"] = (
|
||||||
if days_with_stock != 0
|
res["units_delivered_prev"] / days_with_stock
|
||||||
else 1
|
if days_with_stock != 0
|
||||||
)
|
else 1
|
||||||
else:
|
)
|
||||||
res["units_avg_delivered_prev"] = (
|
else:
|
||||||
found_previous_period[0]["qty_done"] / days if days != 0 else 1
|
res["units_avg_delivered_prev"] = (
|
||||||
)
|
found_previous_period[0]["qty_done"] / days if days != 0 else 1
|
||||||
|
)
|
||||||
seller = product_id._select_seller(
|
seller = product_id._select_seller(
|
||||||
partner_id=self.order_id.partner_id,
|
partner_id=self.order_id.partner_id,
|
||||||
date=fields.Date.today(),
|
date=fields.Date.today(),
|
||||||
|
|
@ -239,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."""
|
||||||
|
|
@ -273,6 +295,10 @@ class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
|
||||||
class PurchaseOrderRecommendationLine(models.TransientModel):
|
class PurchaseOrderRecommendationLine(models.TransientModel):
|
||||||
_inherit = "purchase.order.recommendation.line"
|
_inherit = "purchase.order.recommendation.line"
|
||||||
|
|
||||||
|
include_previous_period = fields.Boolean(
|
||||||
|
related="wizard_id.include_previous_period", store=False, readonly=True
|
||||||
|
)
|
||||||
|
|
||||||
packaging_id = fields.Many2one(
|
packaging_id = fields.Many2one(
|
||||||
comodel_name="product.packaging",
|
comodel_name="product.packaging",
|
||||||
string="Packaging",
|
string="Packaging",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
<field name="show_all_products" position="after">
|
<field name="show_all_products" position="after">
|
||||||
<field name="order_by_packages" />
|
<field name="order_by_packages" />
|
||||||
<field name="ignore_zero_stock_days" />
|
<field name="ignore_zero_stock_days" />
|
||||||
|
<field name="include_previous_period" />
|
||||||
<field name="last_order_total_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
<field name="last_order_total_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||||
<field name="order_total_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
<field name="order_total_amount" widget="monetary" options="{'currency_field': 'currency_id'}" />
|
||||||
<field name="currency_id" invisible="1"/>
|
<field name="currency_id" invisible="1"/>
|
||||||
|
|
@ -32,6 +33,15 @@
|
||||||
<field name="product_id" position="attributes">
|
<field name="product_id" position="attributes">
|
||||||
<attribute name="optional">hide</attribute>
|
<attribute name="optional">hide</attribute>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="price_unit" position="attributes">
|
||||||
|
<attribute name="optional">show</attribute>
|
||||||
|
</field>
|
||||||
|
<field name="units_available" position="attributes">
|
||||||
|
<attribute name="optional">show</attribute>
|
||||||
|
</field>
|
||||||
|
<field name="units_avg_delivered" position="attributes">
|
||||||
|
<attribute name="optional">show</attribute>
|
||||||
|
</field>
|
||||||
<field name="product_name" position="attributes">
|
<field name="product_name" position="attributes">
|
||||||
<attribute name="optional">show</attribute>
|
<attribute name="optional">show</attribute>
|
||||||
</field>
|
</field>
|
||||||
|
|
@ -39,8 +49,9 @@
|
||||||
<field name="units_scrapped" string="Qty scrapped" optional="hide" />
|
<field name="units_scrapped" string="Qty scrapped" optional="hide" />
|
||||||
<field name="stock_duration" string="Stock Duration" optional="hide" />
|
<field name="stock_duration" string="Stock Duration" optional="hide" />
|
||||||
<field name="days_without_stock" optional="hide" />
|
<field name="days_without_stock" optional="hide" />
|
||||||
<field name="units_delivered_prev" string="Prev Period" optional="hide" />
|
<field name="include_previous_period" invisible="1"/>
|
||||||
<field name="units_avg_delivered_prev" string="Avg Prev Period" optional="hide" />
|
<field name="units_delivered_prev" string="Prev Period" optional="hide" attrs="{'invisible': [('include_previous_period','=',False)]}" />
|
||||||
|
<field name="units_avg_delivered_prev" string="Avg Prev Period" optional="hide" attrs="{'invisible': [('include_previous_period','=',False)]}" />
|
||||||
<field name="packaging_id" optional="show" />
|
<field name="packaging_id" optional="show" />
|
||||||
<field name="packaging_contained_qty" string="Packaging Contained Qty" optional="hide" />
|
<field name="packaging_contained_qty" string="Packaging Contained Qty" optional="hide" />
|
||||||
<field name="subtotal_amount" string="Subtotal Amount" optional="hide" />
|
<field name="subtotal_amount" string="Subtotal Amount" optional="hide" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue