Add reporting per customer category

issue10488
review331031003
This commit is contained in:
Cédric Krier
2021-07-12 23:28:15 +02:00
parent 6a24efe5d9
commit 25ffaa04ab
10 changed files with 462 additions and 0 deletions
+2
View File
@@ -1,3 +1,5 @@
* Add reporting per customer category
Version 6.0.0 - 2021-05-03
* Bug fixes (see mercurial logs for details)
* Add default lead time for products
+3
View File
@@ -37,6 +37,9 @@ def register():
sale_reporting.Context,
sale_reporting.Customer,
sale_reporting.CustomerTimeseries,
sale_reporting.CustomerCategory,
sale_reporting.CustomerCategoryTimeseries,
sale_reporting.CustomerCategoryTree,
sale_reporting.Product,
sale_reporting.ProductTimeseries,
sale_reporting.ProductCategory,
+193
View File
@@ -321,6 +321,199 @@ class CustomerTimeseries(CustomerMixin, AbstractTimeseries, ModelView):
__name__ = 'sale.reporting.customer.time_series'
class CustomerCategoryMixin:
__slots__ = ()
category = fields.Many2One('party.category', "Category")
@classmethod
def _joins(cls):
pool = Pool()
PartyCategory = pool.get('party.party-party.category')
from_item, tables, withs = super()._joins()
if 'line.sale.party.party_category' not in tables:
party_category = PartyCategory.__table__()
tables['line.sale.party.party_category'] = party_category
sale = tables['line.sale']
from_item = (from_item
.join(party_category,
condition=sale.party == party_category.party))
return from_item, tables, withs
@classmethod
def _columns(cls, tables, withs):
party_category = tables['line.sale.party.party_category']
return super()._columns(tables, withs) + [
party_category.category.as_('category')]
@classmethod
def _column_id(cls, tables, withs):
pool = Pool()
Category = pool.get('party.category')
category = Category.__table__()
line = tables['line']
party_category = tables['line.sale.party.party_category']
# Get a stable number of categories over time
# by using a number one order bigger.
nb_category = category.select(
Power(10, (Ceil(Log(Max(category.id))) + Literal(1))))
return Min(line.id * nb_category + party_category.id)
@classmethod
def _group_by(cls, tables, withs):
party_category = tables['line.sale.party.party_category']
return super()._group_by(tables, withs) + [party_category.category]
@classmethod
def _where(cls, tables, withs):
party_category = tables['line.sale.party.party_category']
where = super()._where(tables, withs)
where &= party_category.category != Null
return where
def get_rec_name(self, name):
return self.category.rec_name if self.category else None
class CustomerCategory(CustomerCategoryMixin, Abstract, ModelView):
"Sale Reporting per Customer Category"
__name__ = 'sale.reporting.customer.category'
time_series = fields.One2Many(
'sale.reporting.customer.category.time_series', 'category',
"Time Series")
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('category', 'ASC'))
@classmethod
def _column_id(cls, tables, withs):
party_category = tables['line.sale.party.party_category']
return party_category.category
class CustomerCategoryTimeseries(
CustomerCategoryMixin, AbstractTimeseries, ModelView):
"Sale Reporting per Customer Category"
__name__ = 'sale.reporting.customer.category.time_series'
class CustomerCategoryTree(ModelSQL, ModelView):
"Sale Reporting per Customer Category"
__name__ = 'sale.reporting.customer.category.tree'
name = fields.Function(fields.Char("Name"), 'get_name')
parent = fields.Many2One('sale.reporting.customer.category.tree', "Parent")
children = fields.One2Many(
'sale.reporting.customer.category.tree', 'parent', "Children")
revenue = fields.Function(
fields.Numeric("Revenue", digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits']), 'get_total')
currency_digits = fields.Function(
fields.Integer("Currency Digits"), 'get_currency_digits')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('name', 'ASC'))
@classmethod
def table_query(cls):
pool = Pool()
Category = pool.get('party.category')
return Category.__table__()
@classmethod
def get_name(cls, categories, name):
pool = Pool()
Category = pool.get('party.category')
categories = Category.browse(categories)
return {c.id: c.name for c in categories}
@classmethod
def order_name(cls, tables):
pool = Pool()
Category = pool.get('party.category')
table, _ = tables[None]
if 'category' not in tables:
category = Category.__table__()
tables['category'] = {
None: (category, table.id == category.id),
}
return Category.name.convert_order(
'name', tables['category'], Category)
def time_series_all(self):
return []
@classmethod
def get_total(cls, categories, names):
pool = Pool()
ReportingCustomerCategory = pool.get(
'sale.reporting.customer.category')
table = cls.__table__()
reporting_product_category = ReportingCustomerCategory.__table__()
cursor = Transaction().connection.cursor()
categories = cls.search([
('parent', 'child_of', [c.id for c in categories]),
])
ids = [c.id for c in categories]
parents = {}
reporting_customer_categories = []
for sub_ids in grouped_slice(ids):
sub_ids = list(sub_ids)
where = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id, table.parent, where=where))
parents.update(cursor)
where = reduce_ids(reporting_product_category.id, sub_ids)
cursor.execute(
*reporting_product_category.select(
reporting_product_category.id, where=where))
reporting_customer_categories.extend(r for r, in cursor)
result = {}
reporting_product_categories = ReportingCustomerCategory.browse(
reporting_customer_categories)
for name in names:
values = dict.fromkeys(ids, 0)
values.update(
(c.id, getattr(c, name)) for c in reporting_product_categories)
result[name] = cls._sum_tree(categories, values, parents)
return result
@classmethod
def _sum_tree(cls, categories, values, parents):
result = values.copy()
categories = set((c.id for c in categories))
leafs = categories - set(parents.values())
while leafs:
for category in leafs:
categories.remove(category)
parent = parents.get(category)
if parent in result:
result[parent] += result[category]
next_leafs = set(categories)
for category in categories:
parent = parents.get(category)
if not parent:
continue
if parent in next_leafs and parent in categories:
next_leafs.remove(parent)
leafs = next_leafs
return result
def get_currency_digits(self, name):
pool = Pool()
Company = pool.get('company.company')
company = Transaction().context.get('company')
if company:
return Company(company).currency.digits
class ProductMixin(object):
__slots__ = ()
product = fields.Many2One(
+166
View File
@@ -46,6 +46,10 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Sales per Customer</field>
<field name="res_model">sale.reporting.customer</field>
<field name="context_model">sale.reporting.context</field>
<field
name="domain"
eval="If(Eval('active_model') == 'sale.reporting.customer.category.tree', [('customer', 'where', [('categories', 'child_of', Eval('active_ids', []), 'parent')])], [])"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_view1">
<field name="sequence" eval="10"/>
@@ -67,6 +71,11 @@ this repository contains the full copyright notices and license terms. -->
<field name="model" eval="'ir.ui.menu,%s' % ref('menu_reporting_sale')"/>
<field name="action" ref="act_reporting_customer"/>
</record>
<record model="ir.action.keyword" id="act_reporting_customer_keyword2">
<field name="keyword">tree_open</field>
<field name="model">sale.reporting.customer.category.tree,-1</field>
<field name="action" ref="act_reporting_customer"/>
</record>
<record model="ir.rule.group" id="rule_group_reporting_customer_companies">
<field name="name">User in companies</field>
@@ -173,6 +182,163 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="False"/>
</record>
<!-- Customer Category -->
<record model="ir.ui.view" id="reporting_customer_category_view_tree">
<field name="model">sale.reporting.customer.category.tree</field>
<field name="type">tree</field>
<field name="field_childs">children</field>
<field name="name">sale_reporting_customer_category_tree</field>
</record>
<record model="ir.action.act_window" id="act_reporting_customer_category_tree">
<field name="name">Sales per Customer Category</field>
<field name="res_model">sale.reporting.customer.category.tree</field>
<field name="context_model">sale.reporting.context</field>
<field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_category_tree_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="reporting_customer_category_view_tree"/>
<field name="act_window" ref="act_reporting_customer_category_tree"/>
</record>
<record model="ir.action.keyword" id="act_reporting_customer_category_tree_keyword1">
<field name="keyword">tree_open</field>
<field name="model" eval="'ir.ui.menu,%s' % ref('menu_reporting_sale')"/>
<field name="action" ref="act_reporting_customer_category_tree"/>
</record>
<record model="ir.ui.view" id="reporting_customer_category_view_list">
<field name="model">sale.reporting.customer.category</field>
<field name="type">tree</field>
<field name="name">sale_reporting_customer_category_list</field>
</record>
<record model="ir.action.act_window" id="act_reporting_customer_category">
<field name="name">Sales per Customer Category</field>
<field name="res_model">sale.reporting.customer.category</field>
<field name="context_model">sale.reporting.context</field>
<field
name="domain"
eval="[('category', 'child_of', Eval('active_ids', []), 'parent')]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_category_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="reporting_customer_category_view_list"/>
<field name="act_window" ref="act_reporting_customer_category"/>
</record>
<record model="ir.action.keyword" id="act_reporting_customer_category_keyword1">
<field name="keyword">tree_open</field>
<field name="model">sale.reporting.customer.category.tree,-1</field>
<field name="action" ref="act_reporting_customer_category"/>
</record>
<record model="ir.rule.group" id="rule_group_reporting_customer_category_companies">
<field name="name">User in companies</field>
<field name="model" search="[('model', '=', 'sale.reporting.customer.category')]"/>
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_reporting_customer_category_companies">
<field name="domain"
eval="[('company', 'in', Eval('companies', []))]"
pyson="1"/>
<field name="rule_group" ref="rule_group_reporting_customer_category_companies"/>
</record>
<record model="ir.model.access" id="access_reporting_customer_category">
<field name="model" search="[('model', '=', 'sale.reporting.customer.category')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_reporting_customer_category_sale">
<field name="model" search="[('model', '=', 'sale.reporting.customer.category')]"/>
<field name="group" ref="group_sale"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.ui.view" id="reporting_customer_category_time_series_view_list">
<field name="model">sale.reporting.customer.category.time_series</field>
<field name="type">tree</field>
<field name="name">sale_reporting_customer_category_time_series_list</field>
</record>
<record model="ir.ui.view" id="reporting_customer_category_time_series_view_graph_revenue">
<field name="model">sale.reporting.customer.category.time_series</field>
<field name="type">graph</field>
<field name="name">sale_reporting_customer_category_time_series_graph_revenue</field>
</record>
<record model="ir.ui.view" id="reporting_customer_category_time_series_view_graph_number">
<field name="model">sale.reporting.customer.category.time_series</field>
<field name="type">graph</field>
<field name="name">sale_reporting_customer_category_time_series_graph_number</field>
</record>
<record model="ir.action.act_window" id="act_reporting_customer_category_time_series">
<field name="name">Sales per Party Category</field>
<field name="res_model">sale.reporting.customer.category.time_series</field>
<field name="context_model">sale.reporting.context</field>
<field
name="domain"
eval="[('category', '=', Eval('active_id', -1))]"
pyson="1"/>
<field name="order" eval="[('date', 'DESC')]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_category_time_series_list_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="reporting_customer_category_time_series_view_list"/>
<field name="act_window" ref="act_reporting_customer_category_time_series"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_category_time_series_list_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="reporting_customer_category_time_series_view_graph_revenue"/>
<field name="act_window" ref="act_reporting_customer_category_time_series"/>
</record>
<record model="ir.action.act_window.view" id="act_reporting_customer_category_time_series_list_view3">
<field name="sequence" eval="30"/>
<field name="view" ref="reporting_customer_category_time_series_view_graph_number"/>
<field name="act_window" ref="act_reporting_customer_category_time_series"/>
</record>
<record model="ir.action.keyword" id="act_reporting_customer_category_time_series_list_keyword1">
<field name="keyword">tree_open</field>
<field name="model">sale.reporting.customer.category,-1</field>
<field name="action" ref="act_reporting_customer_category_time_series"/>
</record>
<record model="ir.rule.group" id="rule_group_reporting_customer_category_time_series_companies">
<field name="name">User in companies</field>
<field name="model" search="[('model', '=', 'sale.reporting.customer.category.time_series')]"/>
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_reporting_customer_category_time_series_companies">
<field name="domain"
eval="[('company', 'in', Eval('companies', []))]"
pyson="1"/>
<field name="rule_group" ref="rule_group_reporting_customer_category_time_series_companies"/>
</record>
<record model="ir.model.access" id="access_reporting_customer_category_time_series">
<field name="model" search="[('model', '=', 'sale.reporting.customer.category.time_series')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_reporting_customer_category_time_series_sale">
<field name="model" search="[('model', '=', 'sale.reporting.customer.category.time_series')]"/>
<field name="group" ref="group_sale"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<!-- Product -->
<record model="ir.ui.view" id="reporting_product_view_list">
+52
View File
@@ -51,15 +51,30 @@ Create countries::
... name="New York", code="US-NY", type='state', country=country_us)
>>> new_york.save()
Create party categories::
>>> PartyCategory = Model.get('party.category')
>>> party_category_root1 = PartyCategory(name="Root1")
>>> party_category_root1.save()
>>> party_category_child1 = PartyCategory(name="Child1", parent=party_category_root1)
>>> party_category_child1.save()
>>> party_category_child2 = PartyCategory(name="Child2", parent=party_category_root1)
>>> party_category_child2.save()
>>> party_category_root2 = PartyCategory(name="Root2")
>>> party_category_root2.save()
Create parties::
>>> Party = Model.get('party.party')
>>> customer1 = Party(name='Customer1')
>>> customer1.categories.append(PartyCategory(party_category_child1.id))
>>> customer1.categories.append(PartyCategory(party_category_root2.id))
>>> address, = customer1.addresses
>>> address.country = country_us
>>> address.subdivision = california
>>> customer1.save()
>>> customer2 = Party(name='Customer2')
>>> customer2.categories.append(PartyCategory(party_category_child2.id))
>>> address, = customer2.addresses
>>> address.country = country_us
>>> address.subdivision = new_york
@@ -159,6 +174,43 @@ Check sale reporting per customer::
... (customer2.id, sale2.sale_date.replace(day=1), 1, Decimal('10'))])
True
Check sale reporting per customer categories::
>>> CustomerCategory = Model.get('sale.reporting.customer.category')
>>> CustomerCategoryTimeseries = Model.get(
... 'sale.reporting.customer.category.time_series')
>>> CustomerCategoryTree = Model.get('sale.reporting.customer.category.tree')
>>> with config.set_context(context=context):
... reports = CustomerCategory.find([])
... time_series = CustomerCategoryTimeseries.find([])
... tree = CustomerCategoryTree.find([])
>>> len(reports)
3
>>> with config.set_context(context=context):
... sorted((r.category.id, r.number, r.revenue) for r in reports) == \
... sorted([(party_category_child1.id, 1, Decimal('30')),
... (party_category_root2.id, 1, Decimal('30')),
... (party_category_child2.id, 1, Decimal('10'))])
True
>>> len(time_series)
3
>>> with config.set_context(context=context):
... sorted((r.category.id, r.date, r.number, r.revenue)
... for r in time_series) == sorted(
... [(party_category_child1.id, sale1.sale_date.replace(day=1), 1, Decimal('30')),
... (party_category_root2.id, sale1.sale_date.replace(day=1), 1, Decimal('30')),
... (party_category_child2.id, sale2.sale_date.replace(day=1), 1, Decimal('10'))])
True
>>> len(tree)
4
>>> with config.set_context(context=context):
... sorted((r.name, r.revenue) for r in tree) == sorted([
... ('Root1', Decimal('40')),
... ('Child1', Decimal('30')),
... ('Child2', Decimal('10')),
... ('Root2', Decimal('30'))])
True
Check sale reporting per product::
>>> Product = Model.get('sale.reporting.product')
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree keyword_open="1">
<field name="category" expand="1"/>
<field name="number"/>
<field name="revenue" symbol="currency"/>
<field name="revenue_trend" expand="1"/>
</tree>
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<graph>
<x>
<field name="date"/>
</x>
<y>
<field name="number"/>
</y>
</graph>
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<graph>
<x>
<field name="date"/>
</x>
<y>
<field name="revenue"/>
</y>
</graph>
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="date"/>
<field name="number"/>
<field name="revenue" symbol="currency"/>
</tree>
@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree keyword_open="1">
<field name="name" expand="1"/>
<field name="revenue"/>
</tree>