diff --git a/stock_coverage/README.md b/stock_coverage/README.md
new file mode 100644
index 0000000..778224d
--- /dev/null
+++ b/stock_coverage/README.md
@@ -0,0 +1,3 @@
+- initializing the computed fields
+ - daily cron
+ - manual trigger
diff --git a/stock_coverage/__init__.py b/stock_coverage/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/stock_coverage/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/stock_coverage/__openerp__.py b/stock_coverage/__openerp__.py
new file mode 100644
index 0000000..7348736
--- /dev/null
+++ b/stock_coverage/__openerp__.py
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+{
+ 'name': 'Product - Stock Coverage',
+ 'version': '9.0.1',
+ 'category': 'Product',
+ 'description': """
+Shows figures in the product form related to stock coverage
+There are settings in Inventory/settings to define the calculation range and
+the display range.
+ """,
+ 'author': 'coop it easy',
+ 'website': 'coopiteasy.be',
+ 'license': 'AGPL-3',
+ 'depends': [
+ 'product',
+ 'point_of_sale',
+ 'stock'
+ ],
+ 'data': [
+ 'views/product_template_view.xml',
+ 'data/cron.xml',
+ ],
+}
diff --git a/stock_coverage/data/cron.xml b/stock_coverage/data/cron.xml
new file mode 100644
index 0000000..12775ac
--- /dev/null
+++ b/stock_coverage/data/cron.xml
@@ -0,0 +1,14 @@
+
+
+
+ Stock Coverage - Update Article Consumption
+ 24
+ hours
+ -1
+
+ product.template
+ _batch_compute_total_consumption
+ ()
+
+
+
diff --git a/stock_coverage/models/__init__.py b/stock_coverage/models/__init__.py
new file mode 100644
index 0000000..e8fa8f6
--- /dev/null
+++ b/stock_coverage/models/__init__.py
@@ -0,0 +1 @@
+from . import product_template
diff --git a/stock_coverage/models/product_template.py b/stock_coverage/models/product_template.py
new file mode 100644
index 0000000..60be025
--- /dev/null
+++ b/stock_coverage/models/product_template.py
@@ -0,0 +1,124 @@
+# -*- encoding: utf-8 -*-
+from openerp import models, fields, api
+import datetime as dt
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ consumption_calculation_method = fields.Selection(
+ selection=[('sales_history', 'Sales History')],
+ string='Consumption Calculation Method',
+ default='sales_history',
+ )
+ calculation_range = fields.Integer(
+ 'Calculation range (days)',
+ default=365, # todo sensible defaults, 14, 28?
+ )
+
+ average_consumption = fields.Float(
+ string='Average Consumption',
+ compute='_compute_average_daily_consumption',
+ readonly=True,
+ digits=(100, 2),
+ )
+
+ total_consumption = fields.Float(
+ string='Total Consumption',
+ default=0,
+ readonly=True,
+ digits=(100, 2),
+ )
+
+ estimated_stock_coverage = fields.Float(
+ string='Estimated Stock Coverage (days)',
+ compute='_compute_estimated_stock_coverage',
+ default=0,
+ digits=(100, 2),
+ readonly=True,
+ )
+
+ @api.multi
+ @api.depends('calculation_range')
+ def _compute_average_daily_consumption(self):
+ for template in self:
+ if template.calculation_range > 0:
+ avg = template.total_consumption / template.calculation_range
+ else:
+ avg = 0
+ template.average_consumption = avg
+
+ return True
+
+ @api.multi
+ @api.onchange('calculation_range')
+ def _compute_total_consumption(self):
+ for template in self:
+ products = (
+ self.env['product.product']
+ .search([('product_tmpl_id', '=', template.id)]))
+
+ today = dt.date.today()
+ pol_date_limit = (
+ today - dt.timedelta(days=template.calculation_range))
+
+ order_lines = (
+ self.env['pos.order.line']
+ .search([
+ ('product_id', 'in', products.ids),
+ ('create_date', '>',
+ fields.Datetime.to_string(pol_date_limit))
+ ])
+ )
+
+ if order_lines:
+ order_lines = order_lines.filtered(
+ lambda oi: oi.order_id.state in ['done', 'invoiced', 'paid']) # noqa
+ res = sum(order_lines.mapped('qty'))
+ else:
+ res = 0
+ template.total_consumption = res
+ return True
+
+ @api.multi
+ @api.depends('calculation_range')
+ def _compute_estimated_stock_coverage(self):
+ for product_template in self:
+ qty = product_template.qty_available
+ avg = product_template.average_consumption
+ if avg > 0:
+ product_template.estimated_stock_coverage = qty / avg
+ else:
+ # todo what would be a good default value? (not float(inf))
+ product_template.estimated_stock_coverage = 9999
+
+ return True
+
+ @api.model
+ def _batch_compute_total_consumption(self):
+ products = (
+ self.env['product.template']
+ .search([('active', '=', True)])
+ )
+
+ query = """
+ select
+ template.id as product_template_id,
+ sum(pol.qty) as total_consumption
+ from pos_order_line pol
+ join pos_order po ON pol.order_id = po.id
+ join product_product product ON pol.product_id = product.id
+ join product_template template ON product.product_tmpl_id = template.id
+ where po.state in ('done', 'invoiced', 'paid')
+ and template.active
+ and pol.create_date
+ BETWEEN date_trunc('day', now()) - calculation_range * interval '1 days'
+ and date_trunc('day', now())
+ group by product_template_id
+ """
+
+ self.env.cr.execute(query)
+ results = {pid: qty for pid, qty in self.env.cr.fetchall()}
+
+ for product in products:
+ product.total_consumption = results.get(product.id, product.total_consumption)
diff --git a/stock_coverage/tests/__init__.py b/stock_coverage/tests/__init__.py
new file mode 100644
index 0000000..fc398f2
--- /dev/null
+++ b/stock_coverage/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_stock_coverage
diff --git a/stock_coverage/tests/test_stock_coverage.py b/stock_coverage/tests/test_stock_coverage.py
new file mode 100644
index 0000000..43777db
--- /dev/null
+++ b/stock_coverage/tests/test_stock_coverage.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+from collections import namedtuple
+
+from openerp.tests.common import TransactionCase
+from openerp.addons.stock.product import product_template as ProductTemplate
+from openerp.addons.stock.product import product_product as ProductProduct
+import datetime as dt
+
+# fixme setup tests based on demo data, test on a clean database
+
+_datetimes = map(
+ lambda d: d.strftime('%Y-%m-%d %H:%M:%S'),
+ (dt.datetime.now() - dt.timedelta(days=d) for d in range(0, 24, 2)))
+
+_quantities = [0.64, 6.45, 9.65, 1.76, 9.14, 3.99,
+ 6.92, 2.25, 6.91, 1.44, 6.52, 1.44]
+
+
+class TestProductTemplate(TransactionCase):
+
+ def setUp(self, *args, **kwargs):
+ result = super(TestProductTemplate, self).setUp(*args, **kwargs)
+
+ test_product_template = (
+ self.env['product.template']
+ .create({'name': 'test product template',
+ 'calculation_range': 14,
+ 'consumption_calculation_method': 'sales_history',
+ 'product_template_id': 0,
+ })
+ )
+
+ pid = (
+ self.env['product.product']
+ .search([('product_tmpl_id', '=', test_product_template.id)])
+ .ids
+ ).pop()
+
+ for date, qty in zip(_datetimes, _quantities):
+ (self.env['pos.order.line']
+ .create({'create_date': date,
+ 'qty': qty,
+ 'product_id': pid,
+ })
+ )
+
+ def _product_available(*args, **kwargs):
+ products = (
+ self.env['product.product']
+ .search([
+ ('product_tmpl_id', '=', test_product_template.id)])
+ )
+ mock_data = {
+ 'qty_available': 53.2,
+ 'incoming_qty': 14,
+ 'outgoing_qty': 4.1,
+ 'virtual_available': 53.2 + 14 - 4.1,
+ }
+ return {pid: mock_data for pid in products.ids}
+
+ # mock area
+ # ProductTemplate._product_available = _product_available
+ # ProductProduct._product_available = _product_available
+ # Order = namedtuple('Order', ['id', 'state'])
+ # PosOrderLine.order_id = Order('1', 'done')
+
+ test_product_template._compute_total_consumption()
+ self.product_template_id = test_product_template.id
+
+ return result
+
+ def test_create(self):
+ """Create a simple product template"""
+ Template = self.env['product.template']
+ product = Template.create({'name': 'Test create product'})
+ self.assertEqual(product.name, 'Test create product')
+
+ def test_compute_average_daily_consumption(self):
+ """Test computed field average_daily_consumption"""
+ ProductTemplate = self.env['product.template']
+ product_template = ProductTemplate.browse(self.product_template_id)
+
+ computed_value = product_template.average_consumption
+ expected_value = 4.08
+ self.assertAlmostEqual(computed_value, expected_value, 7)
+
+ def test_compute_total_consumption(self):
+ """Test total consumption was computed in setup"""
+ ProductTemplate = self.env['product.template']
+ product_template = ProductTemplate.browse(self.product_template_id)
+ computed_value = product_template.total_consumption
+ expected_value = 57.11
+ self.assertAlmostEqual(computed_value, expected_value)
+
+ # def test_compute_estimated_stock_coverage(self):
+ # """Test computed field estimated_stock_coverage"""
+ # ProductTemplate = self.env['product.template']
+ # product_template = ProductTemplate.browse(self.product_template_id)
+ # computed_value = product_template.estimated_stock_coverage
+ # expected_value = 13.04
+ # self.assertAlmostEqual(computed_value, expected_value)
diff --git a/stock_coverage/views/product_template_view.xml b/stock_coverage/views/product_template_view.xml
new file mode 100644
index 0000000..5eb634e
--- /dev/null
+++ b/stock_coverage/views/product_template_view.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ template.consumption.form
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ template.consumption.tree
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+