changeset 95fda395767f in modules/sale:default
details: https://hg.tryton.org/modules/sale?cmd=changeset&node=95fda395767f
description:
        Add reporting per customer category

        issue10488
        review331031003
diffstat:

 CHANGELOG                                                           |    2 +
 __init__.py                                                         |    3 +
 sale_reporting.py                                                   |  193 
++++++++++
 sale_reporting.xml                                                  |  166 
++++++++
 tests/scenario_sale_reporting.rst                                   |   52 ++
 view/sale_reporting_customer_category_list.xml                      |    9 +
 view/sale_reporting_customer_category_time_series_graph_number.xml  |   11 +
 view/sale_reporting_customer_category_time_series_graph_revenue.xml |   11 +
 view/sale_reporting_customer_category_time_series_list.xml          |    8 +
 view/sale_reporting_customer_category_tree.xml                      |    7 +
 10 files changed, 462 insertions(+), 0 deletions(-)

diffs (559 lines):

diff -r 5ce863742e26 -r 95fda395767f CHANGELOG
--- a/CHANGELOG Sun Jul 04 17:54:47 2021 +0200
+++ b/CHANGELOG Mon Jul 12 23:28:15 2021 +0200
@@ -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
diff -r 5ce863742e26 -r 95fda395767f __init__.py
--- a/__init__.py       Sun Jul 04 17:54:47 2021 +0200
+++ b/__init__.py       Mon Jul 12 23:28:15 2021 +0200
@@ -37,6 +37,9 @@
         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,
diff -r 5ce863742e26 -r 95fda395767f sale_reporting.py
--- a/sale_reporting.py Sun Jul 04 17:54:47 2021 +0200
+++ b/sale_reporting.py Mon Jul 12 23:28:15 2021 +0200
@@ -321,6 +321,199 @@
     __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(
diff -r 5ce863742e26 -r 95fda395767f sale_reporting.xml
--- a/sale_reporting.xml        Sun Jul 04 17:54:47 2021 +0200
+++ b/sale_reporting.xml        Mon Jul 12 23:28:15 2021 +0200
@@ -46,6 +46,10 @@
             <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 @@
             <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 @@
             <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">
diff -r 5ce863742e26 -r 95fda395767f tests/scenario_sale_reporting.rst
--- a/tests/scenario_sale_reporting.rst Sun Jul 04 17:54:47 2021 +0200
+++ b/tests/scenario_sale_reporting.rst Mon Jul 12 23:28:15 2021 +0200
@@ -51,15 +51,30 @@
     ...     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 @@
     ...     (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')
diff -r 5ce863742e26 -r 95fda395767f 
view/sale_reporting_customer_category_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_customer_category_list.xml    Mon Jul 12 23:28:15 
2021 +0200
@@ -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>
diff -r 5ce863742e26 -r 95fda395767f 
view/sale_reporting_customer_category_time_series_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_customer_category_time_series_graph_number.xml        
Mon Jul 12 23:28:15 2021 +0200
@@ -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>
diff -r 5ce863742e26 -r 95fda395767f 
view/sale_reporting_customer_category_time_series_graph_revenue.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_customer_category_time_series_graph_revenue.xml       
Mon Jul 12 23:28:15 2021 +0200
@@ -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>
diff -r 5ce863742e26 -r 95fda395767f 
view/sale_reporting_customer_category_time_series_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_customer_category_time_series_list.xml        Mon Jul 
12 23:28:15 2021 +0200
@@ -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>
diff -r 5ce863742e26 -r 95fda395767f 
view/sale_reporting_customer_category_tree.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_customer_category_tree.xml    Mon Jul 12 23:28:15 
2021 +0200
@@ -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>

Reply via email to