Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][IMP] stock_average_daily_sale: support multi-warehouses #336

Merged
merged 8 commits into from
Sep 24, 2024
3 changes: 0 additions & 3 deletions stock_average_daily_sale/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
"depends": [
"sale",
"stock_storage_type_putaway_abc",
"product_abc_classification",
"product_abc_classification_sale_stock",
"product_route_mto",
],
"data": [
Expand All @@ -22,7 +20,6 @@
"security/stock_average_daily_sale_demo.xml",
"views/stock_average_daily_sale_config.xml",
"views/stock_average_daily_sale.xml",
"views/abc_classification_profile.xml",
"views/stock_warehouse.xml",
"data/ir_cron.xml",
],
Expand Down
21 changes: 9 additions & 12 deletions stock_average_daily_sale/demo/stock_average_daily_sale_config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,42 @@
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_a"
>
<field
name="abc_classification_profile_id"
ref="product_abc_classification_sale_stock.abc_classification_profile_sale_stock"
/>
<field name="abc_classification_level">a</field>
<field name="period_value">2</field>
<field name="period_name">week</field>
<field name="standard_deviation_exclude_factor">3</field>
<field name="safety_factor">0.3</field>
<field name="number_days_qty_in_stock">2</field>
<field name="exclude_weekends">1</field>
<field name="warehouse_id" ref="stock.warehouse0" />
<field name="company_id" ref="base.main_company" />
</record>
<record
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_b"
>
<field
name="abc_classification_profile_id"
ref="product_abc_classification_sale_stock.abc_classification_profile_sale_stock"
/>
<field name="abc_classification_level">b</field>
<field name="period_value">13</field>
<field name="period_name">week</field>
<field name="standard_deviation_exclude_factor">3</field>
<field name="safety_factor">0.3</field>
<field name="number_days_qty_in_stock">2</field>
<field name="exclude_weekends">1</field>
<field name="warehouse_id" ref="stock.warehouse0" />
<field name="company_id" ref="base.main_company" />
</record>
<record
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_c"
>
<field
name="abc_classification_profile_id"
ref="product_abc_classification_sale_stock.abc_classification_profile_sale_stock"
/>
<field name="abc_classification_level">c</field>
<field name="period_value">26</field>
<field name="period_name">week</field>
<field name="standard_deviation_exclude_factor">3</field>
<field name="safety_factor">0.3</field>
<field name="number_days_qty_in_stock">2</field>
<field name="exclude_weekends">1</field>
<field name="warehouse_id" ref="stock.warehouse0" />
<field name="company_id" ref="base.main_company" />
</record>
</odoo>
1 change: 0 additions & 1 deletion stock_average_daily_sale/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from . import stock_warehouse # isort:skip
from . import stock_average_daily_sale_config # isort:skip
from . import stock_average_daily_sale # isort:skip
from . import abc_classification_profile
15 changes: 0 additions & 15 deletions stock_average_daily_sale/models/abc_classification_profile.py

This file was deleted.

36 changes: 16 additions & 20 deletions stock_average_daily_sale/models/stock_average_daily_sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,11 @@


class StockAverageDailySale(models.Model):

_name = "stock.average.daily.sale"
_auto = False
_order = "abc_classification_level ASC, product_id ASC"
_description = "Average Daily Sale for Products"

abc_classification_profile_id = fields.Many2one(
comodel_name="abc.classification.profile",
required=True,
index=True,
)
abc_classification_level = fields.Selection(
selection=ABC_SELECTION, required=True, readonly=True, index=True
)
Expand Down Expand Up @@ -182,14 +176,16 @@ def _create_materialized_view(self):
NOW()::date - '1 day'::interval as date_to,
-- start of the analyzed period computed from the original cfg
(NOW() - (period_value::TEXT || ' ' || period_name::TEXT)::INTERVAL):: date as date_from,
-- the number of business days between start and end computed by
-- removing saturday and sunday
-- the number of days between start and end computed by
-- removing saturday and sunday if weekends should be excluded
(SELECT count(1) from (select EXTRACT(DOW FROM s.d::date) as dd
FROM generate_series(
(NOW() - (period_value::TEXT || ' ' || period_name::TEXT)::INTERVAL):: date ,
(NOW()- '1 day'::interval)::date,
'1 day') AS s(d)) t
WHERE dd not in(0,6)) AS nrb_days_without_sat_sun
WHERE exclude_weekends = False
OR (exclude_weekends = True AND dd not in(0,6))
) AS nbr_days
FROM
stock_average_daily_sale_config
),
Expand All @@ -215,9 +211,10 @@ def _create_materialized_view(self):
+ ( stddev_samp(product_uom_qty) OVER pid * cfg.standard_deviation_exclude_factor)
) as upper_bound,
coalesce ((stddev_samp(product_uom_qty) OVER pid), 0) as standard_deviation,
cfg.nrb_days_without_sat_sun,
cfg.nbr_days,
cfg.date_from,
cfg.date_to,
cfg.exclude_weekends,
cfg.id as config_id,
sm.date
FROM stock_move sm
Expand All @@ -228,9 +225,10 @@ def _create_materialized_view(self):
JOIN cfg on cfg.abc_classification_level = coalesce(pt.abc_storage, 'c')
WHERE
sl_src.usage in ('view', 'internal')
AND sl_dest.usage = 'customer'
AND sl_dest.usage in ('customer', 'production')
AND sm.date BETWEEN cfg.date_from AND cfg.date_to
AND sm.state = 'done'
AND sm.warehouse_id = cfg.warehouse_id
WINDOW pid AS (PARTITION BY sm.product_id, sm.warehouse_id)
),

Expand All @@ -245,16 +243,16 @@ def _create_materialized_view(self):
)::numeric AS average_qty_by_sale,
(count(product_uom_qty) FILTER
(WHERE product_uom_qty BETWEEN lower_bound AND upper_bound OR standard_deviation = 0)
/ nrb_days_without_sat_sun::numeric) AS average_daily_sales_count,
/ nbr_days::numeric) AS average_daily_sales_count,
count(product_uom_qty) FILTER
(WHERE product_uom_qty BETWEEN lower_bound AND upper_bound OR standard_deviation = 0)::double precision as nbr_sales,
standard_deviation::numeric ,
date_from,
date_to,
config_id,
nrb_days_without_sat_sun
nbr_days
FROM deliveries_last
GROUP BY product_id, warehouse_id, standard_deviation, nrb_days_without_sat_sun, date_from, date_to, config_id
GROUP BY product_id, warehouse_id, standard_deviation, nbr_days, date_from, date_to, config_id
),
-- Compute the stock by product in locations under stock
stock_qty AS (
Expand All @@ -268,7 +266,6 @@ def _create_materialized_view(self):
GROUP BY sq.product_id, sl.warehouse_id
),
-- Compute the standard deviation of the average daily sales count
-- excluding saturday and sunday
daily_standard_deviation AS(
SELECT
id,
Expand All @@ -285,7 +282,7 @@ def _create_materialized_view(self):
(WHERE product_uom_qty BETWEEN lower_bound AND upper_bound OR standard_deviation = 0)
) as daily_sales
FROM deliveries_last
WHERE EXTRACT(DOW FROM date) <> '0' AND EXTRACT(DOW FROM date) <> '6'
WHERE exclude_weekends = False OR (EXTRACT(DOW FROM date) <> '0' AND EXTRACT(DOW FROM date) <> '6')
GROUP BY product_id, warehouse_id, 1
) as averages_daily group by id, product_id, warehouse_id

Expand All @@ -305,16 +302,15 @@ def _create_materialized_view(self):
date_to,
config_id,
abc_classification_level,
cfg.abc_classification_profile_id,
sale_ok,
is_mto,
sqty.qty_in_stock as qty_in_stock,
ds.daily_standard_deviation,
ds.daily_standard_deviation * cfg.safety_factor * sqrt(nrb_days_without_sat_sun) as safety,
(cfg.number_days_qty_in_stock * average_qty_by_sale * average_daily_sales_count) + (ds.daily_standard_deviation * cfg.safety_factor * sqrt(nrb_days_without_sat_sun)) as safety_bin_min_qty_new,
ds.daily_standard_deviation * cfg.safety_factor * sqrt(nbr_days) as safety,
(cfg.number_days_qty_in_stock * average_qty_by_sale * average_daily_sales_count) + (ds.daily_standard_deviation * cfg.safety_factor * sqrt(nbr_days)) as safety_bin_min_qty_new,
cfg.number_days_qty_in_stock * GREATEST(average_daily_sales_count, 1) * (average_qty_by_sale + (standard_deviation * cfg.safety_factor)) as safety_bin_min_qty_old,
GREATEST(
(cfg.number_days_qty_in_stock * average_qty_by_sale * average_daily_sales_count) + (ds.daily_standard_deviation * cfg.safety_factor * sqrt(nrb_days_without_sat_sun)),
(cfg.number_days_qty_in_stock * average_qty_by_sale * average_daily_sales_count) + (ds.daily_standard_deviation * cfg.safety_factor * sqrt(nbr_days)),
(cfg.number_days_qty_in_stock * average_qty_by_sale)
) as recommended_qty
FROM averages t
Expand Down
31 changes: 22 additions & 9 deletions stock_average_daily_sale/models/stock_average_daily_sale_config.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models
from odoo import _, fields, models

from odoo.addons.stock_storage_type_putaway_abc.models.stock_location import (
ABC_SELECTION,
)


class StockAverageDailySaleConfig(models.Model):

_name = "stock.average.daily.sale.config"
_description = "Average daily sales computation parameters"
check_company_auto = True

abc_classification_profile_id = fields.Many2one(
comodel_name="abc.classification.profile",
required=True,
ondelete="cascade",
)
abc_classification_level = fields.Selection(
selection=ABC_SELECTION, required=True, readonly=True
selection=ABC_SELECTION, required=True, default="b"
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
)
company_id = fields.Many2one(
string="Company",
comodel_name="res.company",
required=True,
default=lambda self: self.env.company,
)
standard_deviation_exclude_factor = fields.Float(required=True, digits=(2, 2))
warehouse_id = fields.Many2one(
Expand All @@ -30,7 +31,11 @@ class StockAverageDailySaleConfig(models.Model):
default=lambda self: self.env["stock.warehouse"].search(
[("company_id", "=", self.env.company.id)], limit=1
),
readonly=True,
)
exclude_weekends = fields.Boolean(
help="Set to True only if you do not expect any orders/deliveries during "
"the weekends. If set to True, stock moves done on weekends won't be "
"taken into account to calculate the average daily usage",
)
twalter-c2c marked this conversation as resolved.
Show resolved Hide resolved
period_name = fields.Selection(
string="Period analyzed unit",
Expand All @@ -47,3 +52,11 @@ class StockAverageDailySaleConfig(models.Model):
string="Number of days of quantities in stock", required=True, default=2
)
safety_factor = fields.Float(digits=(2, 2), required=True)

_sql_constraints = [
(
"abc_classification_level_unique",
"UNIQUE(abc_classification_level, warehouse_id)",
_("Abc Classification Level must be unique per warehouse"),
)
]
16 changes: 14 additions & 2 deletions stock_average_daily_sale/models/stock_warehouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


class StockWarehouse(models.Model):

_inherit = "stock.warehouse"

average_daily_sale_root_location_id = fields.Many2one(
Expand All @@ -13,8 +12,8 @@
compute="_compute_average_daily_sale_root_location_id",
store=True,
readonly=False,
required=True,
precompute=True,
check_company=True,
help="This is the root location for daily sale average stock computations",
)

Expand All @@ -26,4 +25,17 @@
for warehouse in self.filtered(
lambda w: not w.average_daily_sale_root_location_id
):
if not warehouse.lot_stock_id:
continue

Check warning on line 29 in stock_average_daily_sale/models/stock_warehouse.py

View check run for this annotation

Codecov / codecov/patch

stock_average_daily_sale/models/stock_warehouse.py#L29

Added line #L29 was not covered by tests
warehouse.average_daily_sale_root_location_id = warehouse.lot_stock_id

@api.model_create_multi
def create(self, vals_list):
# set the lot_stock_id of a newly created WH as an Average Daily Sale Root Location
warehouses = super().create(vals_list)

Check warning on line 35 in stock_average_daily_sale/models/stock_warehouse.py

View check run for this annotation

Codecov / codecov/patch

stock_average_daily_sale/models/stock_warehouse.py#L35

Added line #L35 was not covered by tests
for warehouse, vals in zip(warehouses, vals_list):
if vals.get("lot_stock_id") and not vals.get(
"average_daily_sale_root_location_id"
):
warehouse.average_daily_sale_root_location_id = vals["lot_stock_id"]
return warehouses

Check warning on line 41 in stock_average_daily_sale/models/stock_warehouse.py

View check run for this annotation

Codecov / codecov/patch

stock_average_daily_sale/models/stock_warehouse.py#L40-L41

Added lines #L40 - L41 were not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<record model="ir.model.access" id="stock_average_daily_sale_access_user">
<field name="name">stock.average.daily.sale access user</field>
<field name="model_id" ref="model_stock_average_daily_sale" />
<field name="group_id" ref="base.group_user" />
<field name="group_id" ref="stock.group_stock_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<record model="ir.model.access" id="stock_average_daily_sale_config_access_user">
<field name="name">stock_average_daily_sale_config access user</field>
<field name="model_id" ref="model_stock_average_daily_sale_config" />
<field name="group_id" ref="base.group_user" />
<field name="group_id" ref="stock.group_stock_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
Expand All @@ -16,8 +16,8 @@
<field name="model_id" ref="model_stock_average_daily_sale_config" />
<field name="group_id" ref="stock.group_stock_manager" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="0" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>
3 changes: 2 additions & 1 deletion stock_average_daily_sale/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ def _create_move(cls, product, origin_location, qty):
"product_id": product.id,
"name": product.name,
"location_id": origin_location.id,
"warehouse_id": origin_location.warehouse_id.id,
"location_dest_id": cls.customers.id,
"product_uom_qty": qty,
"priority": "1",
}
)
# TODO: Check why this is necessary - it's in materialzed view query
# TODO: Check why this is necessary - it's in materialized view query
move.priority = "1"
return move

Expand Down
3 changes: 2 additions & 1 deletion stock_average_daily_sale/tests/test_average_daily_sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ def test_average_sale_profile_a(self):
def test_view_refreshed(self):
self._refresh()
with self.assertNoLogs(
"odoo.addons.stock_average_daily_sale.models.stock_average_daily_sale"
"odoo.addons.stock_average_daily_sale.models.stock_average_daily_sale",
level="DEBUG",
):
self.env["stock.average.daily.sale"].search_read(
[("product_id", "=", self.product_1.id)]
Expand Down
22 changes: 0 additions & 22 deletions stock_average_daily_sale/views/abc_classification_profile.xml

This file was deleted.

Loading
Loading