[16.0][ADD] stock_average_daily_sale

This commit is contained in:
Denis Roussel
2023-01-13 10:18:08 +01:00
committed by twalter-c2c
parent 0985bd7fb8
commit 35310da0bc
24 changed files with 1613 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
========================
Stock Average Daily Sale
========================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-reporting/tree/16.0/stock_average_daily_sale
:alt: OCA/stock-logistics-reporting
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-reporting-16-0/stock-logistics-reporting-16-0-stock_average_daily_sale
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/151/16.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to gather stock consumptions and build reporting for average daily
sales (aka stock consumptions). Technically, this has been done through a
materialized postgresql view in order to be as fast as possible (some other flow
modules can depend on this).
You can add several configurations depending on the window you want to analyze.
So, you can define criteria to filter data:
* The Warehouse
* The product ABC classification
* The location kind (Zone, Area, Bin)
* The amount of time to look backward (in days or weeks or months or years)
Moreover, you can define:
* A safety factor
* A standard deviation exclusion factor
**Table of contents**
.. contents::
:local:
Configuration
=============
#. To configure data analysis, you should go to Inventory > Configuration > Average daily sales computation parameters
#. You need to fill in the following informations:
* The product ABC classification you want - see product_abc_classification module
* The concerned Warehouse
* The stock location kind (Zone, Area, Bin) - see stock_location_zone module
* The period of time to analyze back (in days/weeks/months/years)
* A standard deviation exclusion factor
* A safety factor
#. Go to Configuration > Technical > Scheduled Actions > Refresh average daily sales materialized view
By default, the sceduled action is set to refresh data each 4 hours. You can change
that depending on your needs.
Known issues / Roadmap
======================
* Move the filter on saturday/sunday to configuration parameters
* An extensible data gathering query
Changelog
=========
16.0.1.0.0 (2023-01-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [16.0][ADD] stock_average_daily_sale
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-reporting/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_average_daily_sale%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu>
* Denis Roussel <denis.roussel@acsone.eu>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/stock-logistics-reporting <https://github.com/OCA/stock-logistics-reporting/tree/16.0/stock_average_daily_sale>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,30 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Stock Average Daily Sale",
"summary": """
Allows to gather delivered products average on daily basis""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-reporting",
"depends": [
"sale",
"stock_storage_type_putaway_abc",
"product_abc_classification",
"product_route_mto",
"stock_location_zone",
],
"data": [
"security/stock_average_daily_sale_config.xml",
"security/stock_average_daily_sale.xml",
"views/stock_average_daily_sale_config.xml",
"views/stock_average_daily_sale.xml",
"views/stock_warehouse.xml",
"data/ir_cron.xml",
],
"demo": [
"demo/stock_average_daily_sale_config.xml",
],
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record forcecreate="True" id="refresh_materialized_view" model="ir.cron">
<field name="name">Refresh average daily sales materialized view</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">4</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="model_id" ref="model_stock_average_daily_sale" />
<field name="code">model.refresh_view()</field>
<field name="state">code</field>
</record>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_a"
>
<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>
</record>
<record
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_b"
>
<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>
</record>
<record
model="stock.average.daily.sale.config"
id="stock_average_daily_sale_config_level_c"
>
<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>
</record>
</odoo>

View File

@@ -0,0 +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

View File

@@ -0,0 +1,330 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from psycopg2.errors import ObjectNotInPrerequisiteState
from psycopg2.extensions import AsIs
from odoo import _, api, fields, models
from odoo.addons.stock_storage_type_putaway_abc.models.stock_location import (
ABC_SELECTION,
)
_logger = logging.getLogger(__name__)
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_level = fields.Selection(
selection=ABC_SELECTION, required=True, readonly=True, index=True
)
average_daily_sales_count = fields.Float(
required=True, digits="Product Unit of Measure"
)
average_qty_by_sale = fields.Float(
required=True, digits="Product Unit of Measure", help="Average Daily Sales Qty"
)
average_daily_qty = fields.Float(
digits="Product Unit of Measure",
required=True,
help="The average daily qty sold",
)
config_id = fields.Many2one(
string="Stock Average Daily Sale Configuration",
comodel_name="stock.average.daily.sale.config",
required=True,
)
date_from = fields.Date(string="From", required=True)
date_to = fields.Date(string="To", required=True)
is_mto = fields.Boolean(
string="On Order",
readonly=True,
store=True,
index=True,
)
nbr_sales = fields.Integer(string="Number of Sales", required=True)
product_id = fields.Many2one(
comodel_name="product.product", string="Product", required=True, index=True
)
safety = fields.Float(
required=True,
help="daily standard deviation * safety factor * sqrt(nbr days into period "
"without saturday and sunday",
)
safety_bin_min_qty = fields.Float(
required=True,
digits="Product Unit of Measure",
help="Minimal safety qty into a bin location computed as: "
"average daily qty * number days in stock * safety",
)
safety_bin_min_qty_old = fields.Float(
required=True,
digits="Product Unit of Measure",
help="Minimal value for the safety qty. Computed as: "
"number days in stock * GREATEST(average daily sales count, 1) * "
"(average qty by sale + (stddev * safety factor))",
)
sale_ok = fields.Boolean(
string="Can be Sold",
readonly=True,
index=True,
help="Specify if the product can be selected in a sales order line.",
)
standard_deviation = fields.Float(string="Qty Standard Deviation", required=True)
daily_standard_deviation = fields.Float(
string="Daily Qty Standard Deviation", required=True
)
warehouse_id = fields.Many2one(comodel_name="stock.warehouse", required=True)
zone_location_id = fields.Many2one(
string="Location Zone", comodel_name="stock.location", index=True
)
qty_in_stock = fields.Float(
string="Quantity in stock",
digits="Product Unit of Measure",
help="All stock locations, reserved product included",
required=True,
)
@api.model
def _check_view(self):
try:
self.env.cr.execute("SELECT COUNT(1) FROM %s", (AsIs(self._table),))
return True
except ObjectNotInPrerequisiteState:
_logger.warning(
_("The materialized view has not been populated. Launch the cron.")
)
return False
except Exception as e:
raise e
# pylint: disable=redefined-outer-name
@api.model
def search_read(
self, domain=None, fields=None, offset=0, limit=None, order=None, **read_kwargs
):
if not self._check_view():
return self.browse()
return super().search_read(
domain=domain,
fields=fields,
offset=offset,
limit=limit,
order=order,
**read_kwargs
)
@api.model
def get_refresh_date(self):
return self.env["ir.config_parameter"].get_param(
"stock_average_daily_sale_refresh_date"
)
@api.model
def set_refresh_date(self, date=None):
if date is None:
date = fields.Datetime.now()
self.env["ir.config_parameter"].set_param(
"stock_average_daily_sale_refresh_date", date
)
@api.model
def refresh_view(self):
self.env.cr.execute("refresh materialized view %s", (AsIs(self._table),))
self.set_refresh_date()
def _create_materialized_view(self):
self.env.cr.execute(
"DROP MATERIALIZED VIEW IF EXISTS %s CASCADE", (AsIs(self._table),)
)
self.env.cr.execute(
"""
CREATE MATERIALIZED VIEW %(table)s AS (
-- Create a consolidated definition of parameters used into the average daily
-- sales computation. Parameters are specified by product ABC class
WITH cfg AS (
SELECT
*,
-- end of the analyzed period
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
(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
FROM
stock_average_daily_sale_config
),
-- Create a consolidated view of all the stock moves from internal locations
-- to customer location. The consolidation is done by including all the moves
-- with a date done into the period provided by the configuration for each
-- product according to its abc classification.
-- The consolidated view also include the standard deviation of the product qty
-- sold at once, and the lower and upper bounds to use to exclude qties
-- that diverge too much from the average qty by product. The factor applied
-- to the standard deviation to compute the lower and upper bounds is also
-- provided by the configuration according the product's abc classification
-- All the products without abc classification are linked to the 'C' class
deliveries_last AS (
SELECT
sm.product_id,
sm.product_uom_qty,
sl_src.warehouse_id,
sl_src.zone_location_id,
(avg(product_uom_qty) OVER pid
- (stddev_samp(product_uom_qty) OVER pid * cfg.standard_deviation_exclude_factor)
) as lower_bound,
(avg(product_uom_qty) OVER pid
+ ( 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.date_from,
cfg.date_to,
cfg.id as config_id,
sm.date
FROM stock_move sm
JOIN stock_location sl_src ON sm.location_id = sl_src.id
JOIN stock_location sl_dest ON sm.location_dest_id = sl_dest.id
JOIN product_product pp on pp.id = sm.product_id
JOIN product_template pt on pp.product_tmpl_id = pt.id
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 sm.priority > '0'
AND sm.date BETWEEN cfg.date_from AND cfg.date_to
AND sm.state = 'done'
WINDOW pid AS (PARTITION BY sm.product_id, sm.warehouse_id)
),
averages AS(
SELECT
concat(warehouse_id, product_id)::integer as id,
product_id,
warehouse_id,
zone_location_id,
(avg(product_uom_qty) FILTER
(WHERE product_uom_qty BETWEEN lower_bound AND upper_bound OR standard_deviation = 0)
)::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,
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
FROM deliveries_last
GROUP BY product_id, warehouse_id, zone_location_id, standard_deviation, nrb_days_without_sat_sun, date_from, date_to, config_id
),
-- Compute the stock by product in locations under stock
stock_qty AS (
SELECT sq.product_id AS pp_id,
sum(sq.quantity) AS qty_in_stock,
sl.warehouse_id AS warehouse_id
FROM stock_quant sq
JOIN stock_location sl ON sq.location_id = sl.id
JOIN stock_warehouse sw ON sl.warehouse_id = sw.id
WHERE sl.parent_path LIKE concat('%%/', sw.average_daily_sale_root_location_id, '/%%')
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,
product_id,
warehouse_id,
stddev_samp(daily_sales) as daily_standard_deviation
from (
SELECT
to_char(date_trunc('day', date), 'YYYY-MM-DD'),
concat(warehouse_id, product_id)::integer as id,
product_id,
warehouse_id,
(count(product_uom_qty) FILTER
(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'
GROUP BY product_id, warehouse_id, 1
) as averages_daily group by id, product_id, warehouse_id
)
-- Collect the data for the materialized view
SELECT
t.id,
t.product_id,
t.warehouse_id,
t.zone_location_id,
average_qty_by_sale,
average_daily_sales_count,
average_qty_by_sale * average_daily_sales_count as average_daily_qty,
nbr_sales,
standard_deviation,
date_from,
date_to,
config_id,
abc_classification_level,
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,
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)
) as safety_bin_min_qty
FROM averages t
JOIN daily_standard_deviation ds on ds.id= t.id
JOIN stock_average_daily_sale_config cfg on cfg.id = t.config_id
JOIN stock_qty sqty on sqty.pp_id = t.product_id AND t.warehouse_id = sqty.warehouse_id
JOIN product_product pp on pp.id = t.product_id
JOIN product_template pt on pt.id = pp.product_tmpl_id
ORDER BY product_id
) WITH NO DATA;""",
{
"table": AsIs(self._table),
},
)
self.env.cr.execute(
"CREATE UNIQUE INDEX pk_%s ON %s (id)",
(AsIs(self._table), AsIs(self._table)),
)
for name, field in self._fields.items():
if not field.index:
continue
self.env.cr.execute(
"CREATE INDEX %s_%s_idx ON %s (%s)",
(AsIs(self._table), AsIs(name), AsIs(self._table), AsIs(name)),
)
self.set_refresh_date(date=False)
cron = self.env.ref(
"stock_average_daily_sale.refresh_materialized_view",
# at install, won't exist yet
raise_if_not_found=False,
)
# refresh data asap, but not during the upgrade
if cron:
cron.nextcall = fields.Datetime.now()
def init(self):
self._create_materialized_view()

View File

@@ -0,0 +1,50 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
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"
abc_classification_level = fields.Selection(
selection=ABC_SELECTION, required=True, readonly=True
)
standard_deviation_exclude_factor = fields.Float(required=True, digits=(2, 2))
warehouse_id = fields.Many2one(
string="Warehouse",
comodel_name="stock.warehouse",
required=True,
ondelete="cascade",
default=lambda self: self.env["stock.warehouse"].search(
[("company_id", "=", self.env.company.id)], limit=1
),
readonly=True,
)
stock_location_kind = fields.Selection(
selection=lambda self: self.env["stock.location"]
._fields["location_kind"]
.selection,
default="zone",
)
period_name = fields.Selection(
string="Period analyzed unit",
selection=[
("year", "Years"),
("month", "Months"),
("week", "Weeks"),
("day", "Days"),
],
required=True,
)
period_value = fields.Integer("Period analyzed value", required=True)
number_days_qty_in_stock = fields.Integer(
string="Number of days of quantities in stock", required=True, default=2
)
safety_factor = fields.Float(digits=(2, 2), required=True)

View File

@@ -0,0 +1,29 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
average_daily_sale_root_location_id = fields.Many2one(
comodel_name="stock.location",
string="Average Daily Sale Root Location",
compute="_compute_average_daily_sale_root_location_id",
store=True,
readonly=False,
required=True,
precompute=True,
help="This is the root location for daily sale average stock computations",
)
@api.depends("lot_stock_id")
def _compute_average_daily_sale_root_location_id(self):
"""
Set a default root location from warehouse lot stock
"""
for warehouse in self.filtered(
lambda w: not w.average_daily_sale_root_location_id
):
warehouse.average_daily_sale_root_location_id = warehouse.lot_stock_id

View File

@@ -0,0 +1,21 @@
* To configure data analysis, you should go to Inventory > Configuration > Average daily sales computation parameters
* You need to fill in the following informations:
* The product ABC classification you want - see product_abc_classification module
* The concerned Warehouse
* The stock location kind (Zone, Area, Bin) - see stock_location_zone module
* The period of time to analyze back (in days/weeks/months/years)
* A standard deviation exclusion factor
* A safety factor
* Go to Configuration > Technical > Scheduled Actions > Refresh average daily sales materialized view
By default, the scheduled action is set to refresh data each 4 hours. You can change
that depending on your needs.
* By default, the root location where analysis is done is the Warehouse stock location,
but you can change it.
* Go to Inventory > Configuration > Warehouses
* Change the 'Average Daily Sale Root Location' field according your needs

View File

@@ -0,0 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Denis Roussel <denis.roussel@acsone.eu>

View File

@@ -0,0 +1,18 @@
This module allows to gather stock consumptions and build reporting for average daily
sales (aka stock consumptions). Technically, this has been done through a
materialized postgresql view in order to be as fast as possible (some other flow
modules can depend on this).
You can add several configurations depending on the window you want to analyze.
So, you can define criteria to filter data:
* The Warehouse
* The product ABC classification
* The location kind (Zone, Area, Bin)
* The amount of time to look backward (in days or weeks or months or years)
Moreover, you can define:
* A safety factor
* A standard deviation exclusion factor
* A different root location for analysis per Warehouse

View File

@@ -0,0 +1,4 @@
16.0.1.0.0 (2023-01-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [16.0][ADD] stock_average_daily_sale

View File

@@ -0,0 +1,2 @@
* Move the filter on saturday/sunday to configuration parameters
* An extensible data gathering query

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<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="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
</odoo>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<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="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<record model="ir.model.access" id="stock_average_daily_sale_config_access_manager">
<field name="name">stock_average_daily_sale_config access manager</field>
<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_write" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,481 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Stock Average Daily Sale</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="stock-average-daily-sale">
<h1 class="title">Stock Average Daily Sale</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/16.0/stock_average_daily_sale"><img alt="OCA/stock-logistics-reporting" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-reporting-16-0/stock-logistics-reporting-16-0-stock_average_daily_sale"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/151/16.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module allows to gather stock consumptions and build reporting for average daily
sales (aka stock consumptions). Technically, this has been done through a
materialized postgresql view in order to be as fast as possible (some other flow
modules can depend on this).</p>
<p>You can add several configurations depending on the window you want to analyze.
So, you can define criteria to filter data:</p>
<ul class="simple">
<li>The Warehouse</li>
<li>The product ABC classification</li>
<li>The location kind (Zone, Area, Bin)</li>
<li>The amount of time to look backward (in days or weeks or months or years)</li>
</ul>
<p>Moreover, you can define:</p>
<ul class="simple">
<li>A safety factor</li>
<li>A standard deviation exclusion factor</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#changelog" id="id4">Changelog</a><ul>
<li><a class="reference internal" href="#id1" id="id5">16.0.1.0.0 (2023-01-13)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="id6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id9">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id10">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<ol class="arabic simple">
<li>To configure data analysis, you should go to Inventory &gt; Configuration &gt; Average daily sales computation parameters</li>
<li>You need to fill in the following informations:</li>
</ol>
<blockquote>
<ul class="simple">
<li>The product ABC classification you want - see product_abc_classification module</li>
<li>The concerned Warehouse</li>
<li>The stock location kind (Zone, Area, Bin) - see stock_location_zone module</li>
<li>The period of time to analyze back (in days/weeks/months/years)</li>
<li>A standard deviation exclusion factor</li>
<li>A safety factor</li>
</ul>
</blockquote>
<ol class="arabic simple">
<li>Go to Configuration &gt; Technical &gt; Scheduled Actions &gt; Refresh average daily sales materialized view</li>
</ol>
<blockquote>
By default, the sceduled action is set to refresh data each 4 hours. You can change
that depending on your needs.</blockquote>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Move the filter on saturday/sunday to configuration parameters</li>
<li>An extensible data gathering query</li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#id4">Changelog</a></h1>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id5">16.0.1.0.0 (2023-01-13)</a></h2>
<ul class="simple">
<li>[16.0][ADD] stock_average_daily_sale</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_average_daily_sale%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id8">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id9">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Denis Roussel &lt;<a class="reference external" href="mailto:denis.roussel&#64;acsone.eu">denis.roussel&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id10">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/16.0/stock_average_daily_sale">OCA/stock-logistics-reporting</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_average_daily_sale

View File

@@ -0,0 +1,104 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
class CommonAverageSaleTest:
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.inventory_obj = cls.env["stock.quant"].with_context(inventory_mode=True)
cls.customers = cls.env.ref("stock.stock_location_customers")
cls.location_obj = cls.env["stock.location"]
cls.move_obj = cls.env["stock.move"]
cls.warehouse_0 = cls.env.ref("stock.warehouse0")
cls.average_sale_obj = cls.env["stock.average.daily.sale"]
cls.average_sale_obj._create_materialized_view()
cls.view_cron = cls.env.ref(
"stock_average_daily_sale.refresh_materialized_view"
)
# Create the following structure:
# [Stock]
# (...)
# # [Zone Location]
# # # [Area Location]
# # # # [Bin Location]
cls.location_zone = cls.location_obj.create(
{
"name": "Zone Location",
"is_zone": True,
"location_id": cls.warehouse_0.lot_stock_id.id,
}
)
cls.location_area = cls.location_obj.create(
{"name": "Area Location", "location_id": cls.location_zone.id}
)
cls.location_bin = cls.location_obj.create(
{"name": "Bin Location", "location_id": cls.location_area.id}
)
cls.location_bin_2 = cls.location_obj.create(
{"name": "Bin Location 2", "location_id": cls.location_area.id}
)
cls.scrap_location = cls.location_obj.create(
{
"name": "Scrap Location",
"usage": "inventory",
}
)
cls.stock_location = cls.env.ref("stock.warehouse0").lot_stock_id
cls._create_products()
@classmethod
def _create_inventory(cls):
cls.inventory_obj.create(
{
"product_id": cls.product_1.id,
"inventory_quantity": 50.0,
"location_id": cls.location_bin.id,
}
)._apply_inventory()
cls.inventory_obj.create(
{
"product_id": cls.product_2.id,
"inventory_quantity": 60.0,
"location_id": cls.location_bin_2.id,
}
)._apply_inventory()
@classmethod
def _create_products(cls):
cls.product_1 = cls.env["product.product"].create(
{
"name": "Product 1",
"type": "product",
}
)
cls.product_2 = cls.env["product.product"].create(
{
"name": "Product 2",
"type": "product",
}
)
@classmethod
def _create_move(cls, product, origin_location, qty):
move = cls.move_obj.create(
{
"product_id": product.id,
"name": product.name,
"location_id": origin_location.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
move.priority = "1"
return move
@classmethod
def _refresh(cls):
# Flush to allow materialized view to be correctly populated
cls.env.flush_all()
cls.env["stock.average.daily.sale"].refresh_view()

View File

@@ -0,0 +1,182 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.fields import Date
from odoo.tests.common import TransactionCase
from .common import CommonAverageSaleTest
class TestAverageSale(CommonAverageSaleTest, TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# As NOW() postgres function cannot easily mocked in python,
# We use now as basis for computations
cls.now = Date.today()
cls.inventory_date = Date.to_string(cls.now - relativedelta(cls.now, weeks=30))
with freeze_time(cls.inventory_date):
cls._create_inventory()
def test_average_sale(self):
# By default, products have abc_storage == 'b'
# So, the averages should correspond to 'b' one
move_1_date = Date.to_string(self.now - relativedelta(weeks=11))
with freeze_time(move_1_date):
move = self._create_move(self.product_1, self.location_bin, 10.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_2_date = Date.to_string(self.now - relativedelta(weeks=9))
with freeze_time(move_2_date):
move = self._create_move(self.product_2, self.location_bin_2, 12.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
self._refresh()
# self.env.flush_all()
avg_product_1 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_1.id)]
)
self.assertRecordValues(
avg_product_1,
[
{
"nbr_sales": 1.0,
"average_qty_by_sale": 10.0,
"qty_in_stock": 40.0,
"zone_location_id": self.location_zone.id,
}
],
)
avg_product_2 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_2.id)]
)
self.assertRecordValues(
avg_product_2,
[
{
"nbr_sales": 1.0,
"average_qty_by_sale": 12.0,
"qty_in_stock": 48.0,
"zone_location_id": self.location_zone.id,
}
],
)
def test_average_sale_multiple(self):
# By default, products have abc_storage == 'b'
# So, the averages should correspond to 'b' one
move_1_date = Date.to_string(self.now - relativedelta(weeks=11))
with freeze_time(move_1_date):
move = self._create_move(self.product_1, self.location_bin, 10.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_1_date = Date.to_string(self.now - relativedelta(weeks=10))
with freeze_time(move_1_date):
move = self._create_move(self.product_1, self.location_bin, 8.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_1_date = Date.to_string(self.now - relativedelta(weeks=9))
with freeze_time(move_1_date):
move = self._create_move(self.product_1, self.location_bin, 13.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_2_date = Date.to_string(self.now - relativedelta(weeks=9))
with freeze_time(move_2_date):
move = self._create_move(self.product_2, self.location_bin_2, 12.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_2_date = Date.to_string(self.now - relativedelta(weeks=8))
with freeze_time(move_2_date):
move = self._create_move(self.product_2, self.location_bin_2, 4.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
self._refresh()
avg_product_1 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_1.id)]
)
self.assertRecordValues(
avg_product_1,
[
{
"nbr_sales": 3.0,
"qty_in_stock": 19.0,
"zone_location_id": self.location_zone.id,
}
],
)
self.assertAlmostEqual(10.33, avg_product_1.average_qty_by_sale, places=2)
avg_product_2 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_2.id)]
)
self.assertRecordValues(
avg_product_2,
[
{
"nbr_sales": 2.0,
"average_qty_by_sale": 8.0,
"qty_in_stock": 44.0,
"zone_location_id": self.location_zone.id,
}
],
)
def test_average_sale_profile_a(self):
# Test with profile 'a'
# Check that no average daily is found
self.product_1.abc_storage = "a"
self.product_2.abc_storage = "a"
move_1_date = Date.to_string(self.now - relativedelta(weeks=11))
with freeze_time(move_1_date):
move = self._create_move(self.product_1, self.location_bin, 10.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
move_2_date = Date.to_string(self.now - relativedelta(weeks=9))
with freeze_time(move_2_date):
move = self._create_move(self.product_2, self.location_bin_2, 12.0)
move._action_confirm()
move._action_assign()
move.quantity_done = move.product_uom_qty
move._action_done()
self._refresh()
avg_product_1 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_1.id)]
)
self.assertFalse(avg_product_1)
avg_product_2 = self.env["stock.average.daily.sale"].search(
[("product_id", "=", self.product_2.id)]
)
self.assertFalse(avg_product_2)

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="stock_daily_sale_search_view">
<field name="name">stock.daily.sale.search (in stock_average_daily_sale)</field>
<field name="model">stock.average.daily.sale</field>
<field name="arch" type="xml">
<search string="Search Average Daily Sale">
<field name="product_id" />
<field name="zone_location_id" />
<separator />
<filter
string="Can be Sold"
name="filter_to_sell"
domain="[('sale_ok','=',1)]"
/>
<separator />
<filter
string="Normal product"
name="normal_product"
domain="[('is_mto','=',False)]"
/>
<filter
string="On Order"
name="on_order"
domain="[('is_mto','=',True)]"
/>
<group expand='0' string='Group by...'>
<filter
string="Zone"
name="group_by_zone"
domain="[]"
context="{'group_by': 'zone_location_id'}"
/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="stockdaily_sale_tree_view">
<field name="name">stock.daily.sale.tree (in stock_average_daily_sale)</field>
<field name="model">stock.average.daily.sale</field>
<field name="arch" type="xml">
<tree create="false" delete="false">
<field name="product_id" />
<field name="abc_classification_level" />
<field name="average_qty_by_sale" />
<field name="average_daily_sales_count" />
<field name="average_daily_qty" />
<field name="standard_deviation" />
<field name="daily_standard_deviation" />
<field name="safety" />
<field name="safety_bin_min_qty" />
<field name="nbr_sales" />
<field name="qty_in_stock" />
<field name="date_from" />
<field name="date_to" />
<field name="warehouse_id" />
<field name="zone_location_id" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="stock_average_daily_sale_act_window">
<field name="name">Average Daily Sales</field>
<field name="res_model">stock.average.daily.sale</field>
<field name="view_mode">tree</field>
<field name="domain">[]</field>
<field
name="context"
>{"search_default_filter_to_sell":1, "search_default_normal_product": 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data found.
You maybe need to launch the cron to refresh the average daily sale data.
</p>
</field>
</record>
<record model="ir.ui.menu" id="stock_average_daily_sale_menu">
<field name="name">Average Daily Sales</field>
<field name="parent_id" ref="stock.menu_warehouse_report" />
<field name="action" ref="stock_average_daily_sale_act_window" />
<field name="sequence" eval="99" />
</record>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="stock_average_daily_sale_config_tree_view">
<field
name="name"
>stock.average.daily.sale.config.tree (in stock_average_daily_sale)</field>
<field name="model">stock.average.daily.sale.config</field>
<field name="arch" type="xml">
<tree editable="top" create="false" delete="false">
<field name="abc_classification_level" />
<field name="period_value" />
<field name="period_name" />
<field name="number_days_qty_in_stock" />
<field name="standard_deviation_exclude_factor" />
<field name="safety_factor" />
<field name="warehouse_id" />
</tree>
</field>
</record>
<record
model="ir.actions.act_window"
id="stock_average_daily_sale_config_act_window"
>
<field name="name">Average daily sales computation parameters</field>
<field name="res_model">stock.average.daily.sale.config</field>
<field name="view_mode">tree</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="stock_average_daily_sale_config_menu">
<field name="name">Average daily sales computation parameters</field>
<field name="parent_id" ref="stock.menu_product_in_config_stock" />
<field name="action" ref="stock_average_daily_sale_config_act_window" />
<field name="sequence" eval="99" />
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_warehouse" model="ir.ui.view">
<field name="name">stock.warehouse</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse" />
<field name="arch" type="xml">
<xpath expr="//group[1]" position="after">
<group string="Average Sales" name="average_sale_reporting">
<field name="average_daily_sale_root_location_id" />
</group>
</xpath>
</field>
</record>
</odoo>