robinkeunen
7 years ago
committed by
robin.keunen
9 changed files with 308 additions and 0 deletions
-
3stock_coverage/README.md
-
1stock_coverage/__init__.py
-
23stock_coverage/__openerp__.py
-
14stock_coverage/data/cron.xml
-
1stock_coverage/models/__init__.py
-
124stock_coverage/models/product_template.py
-
1stock_coverage/tests/__init__.py
-
101stock_coverage/tests/test_stock_coverage.py
-
40stock_coverage/views/product_template_view.xml
@ -0,0 +1,3 @@ |
|||||
|
- initializing the computed fields |
||||
|
- daily cron |
||||
|
- manual trigger |
@ -0,0 +1 @@ |
|||||
|
from . import models |
@ -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', |
||||
|
], |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
<odoo> |
||||
|
<data noupdate="0"> |
||||
|
<record id="ir_cron_update_article_consumption" model="ir.cron"> |
||||
|
<field name="name">Stock Coverage - Update Article Consumption</field> |
||||
|
<field name="interval_number">24</field> |
||||
|
<field name="interval_type">hours</field> |
||||
|
<field name="numbercall">-1</field> |
||||
|
<field name="doall" eval="False" /> |
||||
|
<field name="model">product.template</field> |
||||
|
<field name="function">_batch_compute_total_consumption</field> |
||||
|
<field name="args">()</field> |
||||
|
</record> |
||||
|
</data> |
||||
|
</odoo> |
@ -0,0 +1 @@ |
|||||
|
from . import product_template |
@ -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) |
@ -0,0 +1 @@ |
|||||
|
from . import test_stock_coverage |
@ -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) |
@ -0,0 +1,40 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
|
||||
|
<odoo> |
||||
|
<record id="beesdoo_product_form" model="ir.ui.view"> |
||||
|
<field name="name">template.consumption.form</field> |
||||
|
<field name="model">product.template</field> |
||||
|
<field eval="7" name="priority"/> |
||||
|
<field name="inherit_id" ref="product.product_template_only_form_view"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<group name="stock_property" position="after"> |
||||
|
<group name="Consumption Figures"> |
||||
|
<field name="consumption_calculation_method"/> |
||||
|
<field name="calculation_range" /> |
||||
|
<field name="average_consumption"/> |
||||
|
<field name="total_consumption"/> |
||||
|
<field name="estimated_stock_coverage"/> |
||||
|
</group> |
||||
|
</group> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="beesdoo_product_tree" model="ir.ui.view"> |
||||
|
<field name="name">template.consumption.tree</field> |
||||
|
<field name="model">product.template</field> |
||||
|
<field eval="7" name="priority"/> |
||||
|
<field name="inherit_id" ref="product.product_template_tree_view"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
|
||||
|
<tree> |
||||
|
<field name="volume" invisible="1"/> |
||||
|
<field name="weight" invisible="1"/> |
||||
|
<field name="estimated_stock_coverage"/> |
||||
|
<field name="average_consumption"/> |
||||
|
<field name="total_consumption"/> |
||||
|
<field name="calculation_range"/> |
||||
|
</tree> |
||||
|
|
||||
|
</field> |
||||
|
</record> |
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue