diff --git a/stock_average_daily_sale/__manifest__.py b/stock_average_daily_sale/__manifest__.py index a76c0145b..fefa73c24 100644 --- a/stock_average_daily_sale/__manifest__.py +++ b/stock_average_daily_sale/__manifest__.py @@ -5,15 +5,13 @@ "name": "Stock Average Daily Sale", "summary": """ Allows to gather delivered products average on daily basis""", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "license": "AGPL-3", "author": "ACSONE SA/NV,BCIM,Odoo Community Association (OCA)", "website": "https://github.com/OCA/stock-logistics-reporting", "depends": [ "sale", "stock_storage_type_putaway_abc", - "product_abc_classification", - "product_abc_classification_sale_stock", "product_route_mto", ], "data": [ @@ -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", ], diff --git a/stock_average_daily_sale/demo/stock_average_daily_sale_config.xml b/stock_average_daily_sale/demo/stock_average_daily_sale_config.xml index 2b90e8449..0963cea62 100644 --- a/stock_average_daily_sale/demo/stock_average_daily_sale_config.xml +++ b/stock_average_daily_sale/demo/stock_average_daily_sale_config.xml @@ -6,45 +6,42 @@ model="stock.average.daily.sale.config" id="stock_average_daily_sale_config_level_a" > - a 2 week 3 0.3 2 + 1 + + - b 13 week 3 0.3 2 + 1 + + - c 26 week 3 0.3 2 + 1 + + diff --git a/stock_average_daily_sale/migrations/16.0.1.1.0/pre-migrate.py b/stock_average_daily_sale/migrations/16.0.1.1.0/pre-migrate.py new file mode 100644 index 000000000..bd49c7e83 --- /dev/null +++ b/stock_average_daily_sale/migrations/16.0.1.1.0/pre-migrate.py @@ -0,0 +1,20 @@ +# Copyright 2024 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo.tools.sql import column_exists, create_column + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + if not column_exists(cr, "stock_average_daily_sale_config", "exclude_weekends"): + _logger.info("Create stock_average_daily_sale_config column exclude_weekends") + create_column( + cr, "stock_average_daily_sale_config", "exclude_weekends", "boolean" + ) + cr.execute("UPDATE stock_average_daily_sale_config SET exclude_weekends = True") diff --git a/stock_average_daily_sale/models/__init__.py b/stock_average_daily_sale/models/__init__.py index 756ee7b45..f6a34f5ed 100644 --- a/stock_average_daily_sale/models/__init__.py +++ b/stock_average_daily_sale/models/__init__.py @@ -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 diff --git a/stock_average_daily_sale/models/abc_classification_profile.py b/stock_average_daily_sale/models/abc_classification_profile.py deleted file mode 100644 index e6275aba5..000000000 --- a/stock_average_daily_sale/models/abc_classification_profile.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2023 ACSONE SA/NV -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class AbcClassificationProfile(models.Model): - - _inherit = "abc.classification.profile" - - stock_average_daily_sale_config_ids = fields.One2many( - comodel_name="stock.average.daily.sale.config", - inverse_name="abc_classification_profile_id", - string="Average Daily Sale Configurations", - ) diff --git a/stock_average_daily_sale/models/stock_average_daily_sale.py b/stock_average_daily_sale/models/stock_average_daily_sale.py index bdc1d90bd..144100716 100644 --- a/stock_average_daily_sale/models/stock_average_daily_sale.py +++ b/stock_average_daily_sale/models/stock_average_daily_sale.py @@ -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 ) @@ -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 ), @@ -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 @@ -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) ), @@ -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 ( @@ -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, @@ -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 @@ -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 diff --git a/stock_average_daily_sale/models/stock_average_daily_sale_config.py b/stock_average_daily_sale/models/stock_average_daily_sale_config.py index 55f5ec552..59ae09f31 100644 --- a/stock_average_daily_sale/models/stock_average_daily_sale_config.py +++ b/stock_average_daily_sale/models/stock_average_daily_sale_config.py @@ -1,7 +1,7 @@ # 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, @@ -9,17 +9,18 @@ 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" + ) + 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( @@ -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", ) period_name = fields.Selection( string="Period analyzed unit", @@ -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"), + ) + ] diff --git a/stock_average_daily_sale/models/stock_warehouse.py b/stock_average_daily_sale/models/stock_warehouse.py index 2d34710fa..26d8dec3b 100644 --- a/stock_average_daily_sale/models/stock_warehouse.py +++ b/stock_average_daily_sale/models/stock_warehouse.py @@ -4,7 +4,6 @@ class StockWarehouse(models.Model): - _inherit = "stock.warehouse" average_daily_sale_root_location_id = fields.Many2one( @@ -13,8 +12,8 @@ class StockWarehouse(models.Model): 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", ) @@ -26,4 +25,17 @@ def _compute_average_daily_sale_root_location_id(self): for warehouse in self.filtered( lambda w: not w.average_daily_sale_root_location_id ): + if not warehouse.lot_stock_id: + continue 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) + 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 diff --git a/stock_average_daily_sale/security/stock_average_daily_sale.xml b/stock_average_daily_sale/security/stock_average_daily_sale.xml index 2fad7c29e..f7e41a5e0 100644 --- a/stock_average_daily_sale/security/stock_average_daily_sale.xml +++ b/stock_average_daily_sale/security/stock_average_daily_sale.xml @@ -5,7 +5,7 @@ stock.average.daily.sale access user - + diff --git a/stock_average_daily_sale/security/stock_average_daily_sale_config.xml b/stock_average_daily_sale/security/stock_average_daily_sale_config.xml index c9b326ab8..7f4cfbbaa 100644 --- a/stock_average_daily_sale/security/stock_average_daily_sale_config.xml +++ b/stock_average_daily_sale/security/stock_average_daily_sale_config.xml @@ -5,7 +5,7 @@ stock_average_daily_sale_config access user - + @@ -16,8 +16,8 @@ - + - + diff --git a/stock_average_daily_sale/tests/common.py b/stock_average_daily_sale/tests/common.py index 0cb17286e..0dcb7102d 100644 --- a/stock_average_daily_sale/tests/common.py +++ b/stock_average_daily_sale/tests/common.py @@ -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 diff --git a/stock_average_daily_sale/tests/test_average_daily_sale.py b/stock_average_daily_sale/tests/test_average_daily_sale.py index 5a2d42f26..8bb672563 100644 --- a/stock_average_daily_sale/tests/test_average_daily_sale.py +++ b/stock_average_daily_sale/tests/test_average_daily_sale.py @@ -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)] diff --git a/stock_average_daily_sale/views/abc_classification_profile.xml b/stock_average_daily_sale/views/abc_classification_profile.xml deleted file mode 100644 index e958473a7..000000000 --- a/stock_average_daily_sale/views/abc_classification_profile.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - abc.classification.profile.form (in stock_average_daily_sale) - abc.classification.profile - - - - - - - - - - diff --git a/stock_average_daily_sale/views/stock_average_daily_sale_config.xml b/stock_average_daily_sale/views/stock_average_daily_sale_config.xml index ccda26f94..2c21b5822 100644 --- a/stock_average_daily_sale/views/stock_average_daily_sale_config.xml +++ b/stock_average_daily_sale/views/stock_average_daily_sale_config.xml @@ -2,37 +2,69 @@ + - stock.average.daily.sale.config.tree (in stock_average_daily_sale) + stock.average.daily.sale.config.tree stock.average.daily.sale.config - + + + + - + + + stock.average.daily.sale.config.form + stock.average.daily.sale.config + +
+ + + + + + + + + + + + + + + + + +
+
+
+ Average daily sales computation parameters + ir.actions.act_window stock.average.daily.sale.config - tree + tree,form + [] {} + Average daily sales computation parameters +
diff --git a/stock_average_daily_sale/views/stock_warehouse.xml b/stock_average_daily_sale/views/stock_warehouse.xml index 9715f0123..dc70ab57a 100644 --- a/stock_average_daily_sale/views/stock_warehouse.xml +++ b/stock_average_daily_sale/views/stock_warehouse.xml @@ -10,7 +10,11 @@ - + + diff --git a/stock_average_daily_sale/wizards/stock_average_daily_sale_demo.py b/stock_average_daily_sale/wizards/stock_average_daily_sale_demo.py index b720073de..b9e79fcce 100644 --- a/stock_average_daily_sale/wizards/stock_average_daily_sale_demo.py +++ b/stock_average_daily_sale/wizards/stock_average_daily_sale_demo.py @@ -26,6 +26,7 @@ def _create_move(self, product, origin_location, qty): "product_id": product.id, "name": product.name, "location_id": suppliers.id, + "warehouse_id": suppliers.warehouse_id.id, "location_dest_id": customers.id, "product_uom_qty": qty, } @@ -41,6 +42,7 @@ def _create_move(self, 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": customers.id, "product_uom_qty": qty, "priority": "1",