Skip to content

Commit

Permalink
Add stock_reorder_forecast module with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gfcapalbo committed Jul 13, 2017
1 parent b72c1a7 commit fdad665
Show file tree
Hide file tree
Showing 30 changed files with 2,013 additions and 0 deletions.
171 changes: 171 additions & 0 deletions stock_reorder_forecast/README.rst
@@ -0,0 +1,171 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

======================
Stock Reorder Forecast
======================


Allows to predict date when stock levels will reach minimum by
analizying sales volume in a period and therefore to trigger RFQ's ahead
of time

Extends stock to calculate period turnover and ultimate date of order
(the date where we reach minimum stock)
This module allows to create RFQ's by checking the product form and
examining the ultimate purchase value.

The ultimate purchase value is the date we forecast this product will not be
available. It is obtained by calculating the average sales rate of this
product and predicting at this rate how long will it take for the
stock to reach 0 at this speed.

The period upon wich we calculate this average rate be personalized, default is:

TURNOVER_PERIOD = Amount of time to calculate average (default 365)
TURNOVER_AVERAGE = Average sale rate in that period.
Ultimate Purchase = Day in the future where stock should finish at current
rate.

All values are (period, average) are kept on (in order of importance):
* Supplier Info
* Partner
* Category

if the values are not set on supplier info it will default to values on
partner, if not set on supplier will default to category, if not set anywhere
will default to Hardcoded values (ir.config.parameters period=365 days).

THe user can also trigger orders from the supplier form. There is a wizard
that would allow to order:

* All the products provided by this partner (in required amounts
considering turnover_average, current stock and maximum_stock)
* All the products provided by this partner as primary supplier

This wizard also provides an overview of existing RFQ lines

The turnover average and the ultimate purchase derived by it are calculated in
bulk by a daily cron job.


Configuration
=============

To configure this module, you need to:
set turnover_period stock_period_max , stock_period_min on:

* Product
* Supplier Infos
* Partners
* Categories
* Default value

These values will be taken in this order of priority (highest to lowest)
if the values in product, Supplier Info, Partners, Categories are all not
set it will revert to Default Value, defined in installation data is a
company parameter.

Also set the frequency of turnover and purchase date calculation by setting
in Automatic Actions the execution of cron job "Purchase Proposal Refresh"
(by default set at once a day).


Usage
=====

Set on product and/or partner(supplier) and/or product category the values
of turnover period.

Make sure the cron job "Purchase Proposal Refresh" is activated, launch it
manually the first time in order to have all "ultimate dates" for products
calculated. Set the cron job time/date at a convenient time and frequency, this
job will refresh all stats used for forcasting ultimate purchase date. If you
have a high volume of sales and do frequent resupplies it is advisable to
launch it multiple times a day.


View products to see all products with an ultimate order date, for that
interface you can generate a RFQ to desired date.

You can also view from the partner/supplier form all products ordered by this
partner.

#. Go to ...

.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch}

.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt
.. branch is "9.0" for example
Known issues / Roadmap
======================
* Does not support Multicompany, calculation of stats will allways work on
cross-company products purchases and pickings. It will only calculate outgoing
moves , not internal moves. So the stats will be representative of all
companies global stats (all sales from all companies/turnover period of
product).

Implementing a full multicompany support will require additional support
datastrutures.

from a functional stand point, the global stats may be still usefull in some
multicompany configurations, not all.


* Another useful feature would be to trigger RFQ's automatically. Currently
the users receive stats and can press a button to make a RFQ based on their
decisions. we could make options to make an automatic RFQ when ultimate
purchase gets up to X days from now.


Bug Tracker
===========

Bugs are tracked on `GitHub Issues
<https://github.com/OCA/{project_repo}/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.

Images
------

* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.

Contributors
------------

* Giovanni Francesco Capalbo <giovanni@therp.nl>
* Holger Brunn <hbrunn@therp.nl>
* Hans Van Dijk <hvd400@gmail.com>
* Ronald Portier <rportier@therp.nl>

Funders
-------

The development of this module has been financially supported by:

* Therp B.V.

Maintainer
----------

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

This module is maintained by the OCA.

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.

To contribute to this module, please visit https://odoo-community.org.

.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
5 changes: 5 additions & 0 deletions stock_reorder_forecast/__init__.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models
from . import wizards
32 changes: 32 additions & 0 deletions stock_reorder_forecast/__openerp__.py
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Stock reorder forecast",
"version": "9.0.1.0.0",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Stock",
"summary": "Predict date stock levels will reach minimum and trigger RFQ",
"depends": [
'product',
'stock',
'sale',
'purchase'
],
"demo": [
'demo/data.xml',
],
"data": [
'data/ir_config_parameter.xml',
'wizards/purchase_wizard.xml',
'wizards/purchase_supplier_wizard.xml',
'views/product_product.xml',
"views/product_template.xml",
'views/product_supplierinfo.xml',
'views/product_category.xml',
'views/partner_view.xml',
'data/cron.xml',
],
"installable": True,
}
16 changes: 16 additions & 0 deletions stock_reorder_forecast/data/cron.xml
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_mail_scheduler_action" model="ir.cron">
<field name="name">Purchase Proposal Refresh</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'product.product'" name="model"/>
<field eval="'calc_purchase_date'" name="function"/>
<field eval="'()'" name="args"/>
</record>
</data>
</openerp>
24 changes: 24 additions & 0 deletions stock_reorder_forecast/data/ir_config_parameter.xml
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<openerp>
<data noupdate="1">
<record id="config_turnover_period" model="ir.config_parameter">
<field name="key">default_turnover_period</field>
<field name="value">365</field>
</record>

<record id="config_default_period_min" model="ir.config_parameter">
<field name="key">default_period_min</field>
<field name="value">91</field>
</record>

<record id="config_default_period_max" model="ir.config_parameter">
<field name="key">default_period_max</field>
<field name="value">185</field>
</record>

<record id="config_default_purchase_multiple" model="ir.config_parameter">
<field name="key">default_purchase_multiple</field>
<field name="value">1.0</field>
</record>
</data>
</openerp>
116 changes: 116 additions & 0 deletions stock_reorder_forecast/demo/data.xml
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="product_template1" model="product.template">
<field name="name">TMPL1</field>
<field
name="route_ids"
eval="[(6,0,[ref('purchase.route_warehouse0_buy'), ref('stock.route_warehouse0_mto')])]"></field>
<field name="categ_id" ref="product.product_category_5"></field>
</record>

<record id="product_template2" model="product.template">
<field name="name">TMPL2</field>
<field
name="route_ids"
eval="[(6,0,[ref('purchase.route_warehouse0_buy'), ref('stock.route_warehouse0_mto')])]"></field>
<field name="categ_id" ref="product.product_category_2"></field>
</record>

<record id="product_template3" model="product.template">
<field name="name">TMPL3</field>
<field
name="route_ids"
eval="[(6,0,[ref('purchase.route_warehouse0_buy'), ref('stock.route_warehouse0_mto')])]"></field>

<field name="categ_id" ref="product.product_category_3"></field>
</record>

<!-- product -->
<record id="product_noper" model="product.product">
<field name="name">product_noperiod</field>
<field name="product_tmpl_id" ref="product_template1"/>
<field name="type">product</field>
</record>

<record id="product_period90" model="product.product">
<field name="name">PERIOD90</field>
<field name="product_tmpl_id" ref="product_template2"/>
<field name="type">product</field>
<field name="turnover_period">90</field>

</record>

<record id="product_period180" model="product.product">
<field name="name">PERIOD180</field>
<field name="product_tmpl_id" ref="product_template3"/>
<field name="type">product</field>
<field name="turnover_period">180</field>
</record>


<!-- Suppliers -->

<record id="product_supplierinfo_1" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_template1"/>
<field name="product_id" ref="product_noper"/>
<field name="name" ref="base.res_partner_1"/>
<field name="delay">2</field>
<field name="min_qty">1</field>
</record>

<record id="product_supplierinfo_2" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_template2"/>
<field name="product_id" ref="product_period90"/>
<field name="name" ref="base.res_partner_4"/>
<field name="delay">1</field>
<field name="min_qty">1</field>
</record>

<record id="product_supplierinfo_3" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_template3"/>
<field name="product_id" ref="product_period180"/>
<field name="name" ref="base.res_partner_3"/>
<field name="delay">1</field>
<field name="min_qty">1</field>
</record>

<!-- new partner to be sure it isnt supplying anthing -->

<record id="partner_new_forsupply" model="res.partner">
<field name="name">New partner</field>
<field name="company_id" ref="base.main_company"/>
<field name="customer" eval="False"/>
<field name="email">demo@demo.com</field>
<field name="street">center street</field>
<field name="city">belleville</field>
<field name="zip">5431</field>
</record>

<record id="product_supplierinfo_new" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_template3"/>
<field name="product_id" ref="product_period180"/>
<field name="name" ref="partner_new_forsupply"/>
<field name="delay">1</field>
<field name="min_qty">1</field>
</record>

<record id="cat_no_period" model="product.category">
<field name="name">NOPERIOD</field>
</record>
<record id="cat_period_30" model="product.category">
<field name="name">PERIOD30</field>
<field name="turnover_period">30</field>
</record>
<record id="cat_period_180" model="product.category">
<field name="name">PERIOD180</field>
<field name="turnover_period">180</field>
</record>
<record id="cat_period_360" model="product.category">
<field name="name">PERIOD360</field>
<field name="turnover_period">360</field>
</record>

</data>
</openerp>

12 changes: 12 additions & 0 deletions stock_reorder_forecast/models/__init__.py
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import product_product
from . import product_category
from . import product_supplierinfo
from . import purchase_order
from . import purchase_order_line
from . import res_partner
from . import procurement_order
from . import product_template
15 changes: 15 additions & 0 deletions stock_reorder_forecast/models/procurement_order.py
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, models


class ProcurementOrder(models.Model):
_inherit = 'procurement.order'

@api.model
def _run(self, procurement):
if (procurement.rule_id and procurement.rule_id.action == 'buy'):
# disable making PO's from procurement orders
return True
return super(ProcurementOrder, self)._run(procurement)

0 comments on commit fdad665

Please sign in to comment.