From 171bbf8ad43435fc9bcca1c3823400da3b269b04 Mon Sep 17 00:00:00 2001 From: robinkeunen Date: Thu, 29 Mar 2018 13:50:01 +0200 Subject: [PATCH] refactor module name --- stock_coverage/README.md | 3 + stock_coverage/__init__.py | 1 + stock_coverage/__openerp__.py | 23 ++++ stock_coverage/data/cron.xml | 14 ++ stock_coverage/models/__init__.py | 1 + stock_coverage/models/product_template.py | 124 ++++++++++++++++++ stock_coverage/tests/__init__.py | 1 + stock_coverage/tests/test_stock_coverage.py | 101 ++++++++++++++ .../views/product_template_view.xml | 40 ++++++ 9 files changed, 308 insertions(+) create mode 100644 stock_coverage/README.md create mode 100644 stock_coverage/__init__.py create mode 100644 stock_coverage/__openerp__.py create mode 100644 stock_coverage/data/cron.xml create mode 100644 stock_coverage/models/__init__.py create mode 100644 stock_coverage/models/product_template.py create mode 100644 stock_coverage/tests/__init__.py create mode 100644 stock_coverage/tests/test_stock_coverage.py create mode 100644 stock_coverage/views/product_template_view.xml 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 + + + + + + + + + + + + + + + +