changeset 3144fd7d8485 in modules/sale:default
details: https://hg.tryton.org/modules/sale?cmd=changeset&node=3144fd7d8485
description:
        Add sale reporting per region

        issue11669
        review423711003
diffstat:

 CHANGELOG                            |    1 +
 __init__.py                          |    6 +-
 doc/design.rst                       |    1 +
 sale_reporting.py                    |  151 ++++++++++++++++++++++++++++++++--
 sale_reporting.xml                   |   78 ++++++++++++-----
 tests/scenario_sale_reporting.rst    |   20 +++-
 view/sale_reporting_country_tree.xml |    9 ++
 view/sale_reporting_region_tree.xml  |    4 +-
 8 files changed, 226 insertions(+), 44 deletions(-)

diffs (429 lines):

diff -r abb45176fb28 -r 3144fd7d8485 CHANGELOG
--- a/CHANGELOG Sun Sep 11 01:29:22 2022 +0200
+++ b/CHANGELOG Mon Sep 12 21:31:32 2022 +0200
@@ -1,3 +1,4 @@
+* Add sale reporting per region
 * Support warehouse pickup
 * Use MultiValue for product lead time
 * Remove CompanyValueMixin for default lead time
diff -r abb45176fb28 -r 3144fd7d8485 __init__.py
--- a/__init__.py       Sun Sep 11 01:29:22 2022 +0200
+++ b/__init__.py       Mon Sep 12 21:31:32 2022 +0200
@@ -47,11 +47,12 @@
         sale_reporting.ProductCategory,
         sale_reporting.ProductCategoryTimeseries,
         sale_reporting.ProductCategoryTree,
+        sale_reporting.RegionTree,
         sale_reporting.Country,
         sale_reporting.CountryTimeseries,
         sale_reporting.Subdivision,
         sale_reporting.SubdivisionTimeseries,
-        sale_reporting.Region,
+        sale_reporting.CountryTree,
         invoice.Invoice,
         invoice.Line,
         module='sale', type_='model')
@@ -63,7 +64,8 @@
         sale.ModifyHeader,
         party.Replace,
         party.Erase,
-        sale_reporting.OpenRegion,
+        sale_reporting.OpenRegionTree,
+        sale_reporting.OpenCountryTree,
         module='sale', type_='wizard')
     Pool.register(
         sale.SaleReport,
diff -r abb45176fb28 -r 3144fd7d8485 doc/design.rst
--- a/doc/design.rst    Sun Sep 11 01:29:22 2022 +0200
+++ b/doc/design.rst    Mon Sep 12 21:31:32 2022 +0200
@@ -209,6 +209,7 @@
 The sales reporting that is done *By Region* shows sales based on where the
 customer is located.
 This is done by combining together in a tree structure the sales
+`By Regions <country:country.region>` which can be opened to the sales
 `By Country <model-sale.reporting.country>` below which are the sales
 `By Subdivision <model-sale.reporting.country.subdivision>`.
 
diff -r abb45176fb28 -r 3144fd7d8485 sale_reporting.py
--- a/sale_reporting.py Sun Sep 11 01:29:22 2022 +0200
+++ b/sale_reporting.py Mon Sep 12 21:31:32 2022 +0200
@@ -866,6 +866,138 @@
         return where
 
 
+class RegionTree(ModelSQL, ModelView):
+    "Sale Reporting per Region"
+    __name__ = 'sale.reporting.region.tree'
+
+    name = fields.Function(fields.Char("Name"), 'get_name')
+    parent = fields.Many2One('sale.reporting.region.tree', "Parent")
+    subregions = fields.One2Many(
+        'sale.reporting.region.tree', 'parent', "Subregions")
+
+    revenue = fields.Function(Monetary(
+            "Revenue", digits='currency'), 'get_total')
+
+    currency = fields.Function(fields.Many2One(
+            'currency.currency', "Currency"), 'get_currency')
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order.insert(0, ('name', 'ASC'))
+
+    @classmethod
+    def table_query(cls):
+        pool = Pool()
+        Region = pool.get('country.region')
+        return Region.__table__()
+
+    @classmethod
+    def get_name(cls, regions, name):
+        pool = Pool()
+        Region = pool.get('country.region')
+        regions = Region.browse(regions)
+        return {r.id: r.name for r in regions}
+
+    @classmethod
+    def order_name(cls, tables):
+        pool = Pool()
+        Region = pool.get('country.region')
+        table, _ = tables[None]
+        if 'region' not in tables:
+            region = Region.__table__()
+            tables['region'] = {
+                None: (region, table.id == region.id),
+                }
+        return Region.name.convert_order(
+                'name', tables['region'], Region)
+
+    @classmethod
+    def get_total(cls, regions, names):
+        pool = Pool()
+        ReportingCountry = pool.get('sale.reporting.country')
+        table = cls.__table__()
+        cursor = Transaction().connection.cursor()
+
+        regions = cls.search([
+                ('parent', 'child_of', [r.id for r in regions]),
+                ])
+        ids = [c.id for c in regions]
+        parents = {}
+        reporting_countries = []
+        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)
+
+        result = {}
+        reporting_countries = ReportingCountry.search([
+                ('country.region', 'in', [r.id for r in regions]),
+                ])
+        for name in names:
+            values = dict.fromkeys(ids, 0)
+            for reporting_country in reporting_countries:
+                values[reporting_country.country.region.id] += (
+                    getattr(reporting_country, name))
+            result[name] = cls._sum_tree(regions, values, parents)
+        return result
+
+    @classmethod
+    def _sum_tree(cls, regions, values, parents):
+        result = values.copy()
+        regions = set((c.id for c in regions))
+        leafs = regions - set(parents.values())
+        while leafs:
+            for region in leafs:
+                regions.remove(region)
+                parent = parents.get(region)
+                if parent in result:
+                    result[parent] += result[region]
+            next_leafs = set(regions)
+            for region in regions:
+                parent = parents.get(region)
+                if not parent:
+                    continue
+                if parent in next_leafs and parent in regions:
+                    next_leafs.remove(parent)
+            leafs = next_leafs
+        return result
+
+    def get_currency(self, name):
+        pool = Pool()
+        Company = pool.get('company.company')
+        company = Transaction().context.get('company')
+        if company:
+            return Company(company).currency.id
+
+
+class OpenRegionTree(Wizard):
+    "Open Region"
+    __name__ = 'sale.reporting.region.tree.open'
+
+    start = StateAction('sale.act_reporting_country_tree')
+
+    def do_start(self, action):
+        pool = Pool()
+        Country = pool.get('country.country')
+        CountryTree = pool.get('sale.reporting.country.tree')
+        countries = Country.search([
+                ('region', 'child_of', [r.id for r in self.records], 'parent'),
+                ])
+        ids = [
+            CountryTree.union_shard(c.id, 'sale.reporting.country')
+            for c in countries]
+        data = {
+            'ids': ids,
+            }
+        name_suffix = ', '.join(r.rec_name for r in self.records[:5])
+        if len(self.records) > 5:
+            name_suffix += ',...'
+        action['name'] += ' (%s)' % name_suffix
+        return action, data
+
+
 class Country(CountryMixin, Abstract):
     "Sale Reporting per Country"
     __name__ = 'sale.reporting.country'
@@ -944,13 +1076,14 @@
     __name__ = 'sale.reporting.country.subdivision.time_series'
 
 
-class Region(UnionMixin, Abstract, ModelView):
-    "Sale Reporting per Region"
-    __name__ = 'sale.reporting.region'
+class CountryTree(UnionMixin, Abstract, ModelView):
+    "Sale Reporting per Country"
+    __name__ = 'sale.reporting.country.tree'
 
     region = fields.Function(fields.Char("Region"), 'get_rec_name')
-    parent = fields.Many2One('sale.reporting.region', "Parent")
-    children = fields.One2Many('sale.reporting.region', 'parent', "Children")
+    parent = fields.Many2One('sale.reporting.country.tree', "Parent")
+    children = fields.One2Many(
+        'sale.reporting.country.tree', 'parent', "Children")
 
     @classmethod
     def union_models(cls):
@@ -958,7 +1091,7 @@
 
     @classmethod
     def union_column(cls, name, field, table, Model):
-        column = super(Region, cls).union_column(
+        column = super().union_column(
             name, field, table, Model)
         if (name == 'parent'
                 and Model.__name__ == 'sale.reporting.country.subdivision'):
@@ -985,9 +1118,9 @@
         return record.time_series
 
 
-class OpenRegion(Wizard):
-    "Open Region"
-    __name__ = 'sale.reporting.region.open'
+class OpenCountryTree(Wizard):
+    "Open Country"
+    __name__ = 'sale.reporting.country.tree.open'
 
     start = StateTransition()
     country = StateAction('sale.act_reporting_country_time_series')
diff -r abb45176fb28 -r 3144fd7d8485 sale_reporting.xml
--- a/sale_reporting.xml        Sun Sep 11 01:29:22 2022 +0200
+++ b/sale_reporting.xml        Mon Sep 12 21:31:32 2022 +0200
@@ -802,51 +802,81 @@
 
         <!-- Region -->
 
-        <record model="ir.ui.view" id="reporting_region_view_tree">
-            <field name="model">sale.reporting.region</field>
+        <record model="ir.ui.view" id="reporting_region_tree_view_tree">
+            <field name="model">sale.reporting.region.tree</field>
             <field name="type">tree</field>
+            <field name="field_childs">subregions</field>
             <field name="name">sale_reporting_region_tree</field>
-            <field name="field_childs">children</field>
         </record>
 
-        <record model="ir.action.act_window" id="act_reporting_region">
+        <record model="ir.action.act_window" id="act_reporting_region_tree">
             <field name="name">Sales per Region</field>
-            <field name="res_model">sale.reporting.region</field>
+            <field name="res_model">sale.reporting.region.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_region_view1">
+        <record model="ir.action.act_window.view" 
id="act_reporting_region_tree_view1">
             <field name="sequence" eval="10"/>
-            <field name="view" ref="reporting_region_view_tree"/>
-            <field name="act_window" ref="act_reporting_region"/>
+            <field name="view" ref="reporting_region_tree_view_tree"/>
+            <field name="act_window" ref="act_reporting_region_tree"/>
         </record>
-        <record model="ir.action.keyword" id="act_reporting_country_keyword1">
+        <record model="ir.action.keyword" 
id="act_reporting_region_tree_keyword1">
             <field name="keyword">tree_open</field>
             <field name="model" ref="menu_reporting_sale"/>
-            <field name="action" ref="act_reporting_region"/>
+            <field name="action" ref="act_reporting_region_tree"/>
+        </record>
+
+        <record model="ir.action.wizard" 
id="wizard_reporting_region_tree_open">
+            <field name="name">Open Region</field>
+            <field name="wiz_name">sale.reporting.region.tree.open</field>
+            <field name="model">sale.reporting.region.tree</field>
+        </record>
+        <record model="ir.action.keyword" 
id="wizard_reporting_region_tree_open_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">sale.reporting.region.tree,-1</field>
+            <field name="action" ref="wizard_reporting_region_tree_open"/>
         </record>
 
-        <record model="ir.rule.group" 
id="rule_group_reporting_region_companies">
+        <record model="ir.ui.view" id="reporting_country_tree_view_tree">
+            <field name="model">sale.reporting.country.tree</field>
+            <field name="type">tree</field>
+            <field name="name">sale_reporting_country_tree</field>
+            <field name="field_childs">children</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_country_tree">
+            <field name="name">Sales per Country</field>
+            <field name="res_model">sale.reporting.country.tree</field>
+            <field name="context_model">sale.reporting.context</field>
+            <field name="domain" eval="[('parent', '=', None), ('id', 'in', 
Eval('active_ids', []))]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_country_tree_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_country_tree_view_tree"/>
+            <field name="act_window" ref="act_reporting_country_tree"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_country_tree_companies">
             <field name="name">User in companies</field>
-            <field name="model" search="[('model', '=', 
'sale.reporting.region')]"/>
+            <field name="model" search="[('model', '=', 
'sale.reporting.country.tree')]"/>
             <field name="global_p" eval="True"/>
         </record>
-        <record model="ir.rule" id="rule_reporting_region_companies">
+        <record model="ir.rule" id="rule_reporting_country_tree_companies">
             <field name="domain"
                 eval="[('company', 'in', Eval('companies', []))]"
                 pyson="1"/>
-            <field name="rule_group" 
ref="rule_group_reporting_region_companies"/>
+            <field name="rule_group" 
ref="rule_group_reporting_country_tree_companies"/>
         </record>
 
-        <record model="ir.model.access" id="access_reporting_region">
-            <field name="model" search="[('model', '=', 
'sale.reporting.region')]"/>
+        <record model="ir.model.access" id="access_reporting_country_tree">
+            <field name="model" search="[('model', '=', 
'sale.reporting.country.tree')]"/>
             <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_region_sale">
-            <field name="model" search="[('model', '=', 
'sale.reporting.region')]"/>
+        <record model="ir.model.access" 
id="access_reporting_country_tree_sale">
+            <field name="model" search="[('model', '=', 
'sale.reporting.country.tree')]"/>
             <field name="group" ref="group_sale"/>
             <field name="perm_read" eval="True"/>
             <field name="perm_write" eval="False"/>
@@ -997,15 +1027,15 @@
             <field name="perm_delete" eval="False"/>
         </record>
 
-        <record model="ir.action.wizard" id="wizard_reporting_region_open">
+        <record model="ir.action.wizard" 
id="wizard_reporting_country_tree_open">
             <field name="name">Open Region</field>
-            <field name="wiz_name">sale.reporting.region.open</field>
-            <field name="model">sale.reporting.region</field>
+            <field name="wiz_name">sale.reporting.country.tree.open</field>
+            <field name="model">sale.reporting.country.tree</field>
         </record>
-        <record model="ir.action.keyword" 
id="wizard_reporting_region_open_keyword1">
+        <record model="ir.action.keyword" 
id="wizard_reporting_country_tree_open_keyword1">
             <field name="keyword">tree_open</field>
-            <field name="model">sale.reporting.region,-1</field>
-            <field name="action" ref="wizard_reporting_region_open"/>
+            <field name="model">sale.reporting.country.tree,-1</field>
+            <field name="action" ref="wizard_reporting_country_tree_open"/>
         </record>
 
     </data>
diff -r abb45176fb28 -r 3144fd7d8485 tests/scenario_sale_reporting.rst
--- a/tests/scenario_sale_reporting.rst Sun Sep 11 01:29:22 2022 +0200
+++ b/tests/scenario_sale_reporting.rst Mon Sep 12 21:31:32 2022 +0200
@@ -39,9 +39,11 @@
 
 Create countries::
 
+    >>> Region = Model.get('country.region')
     >>> Country = Model.get('country.country')
     >>> Subdivision = Model.get('country.subdivision')
-    >>> country_us = Country(name="United States")
+    >>> north_america, = Region.find([('code_numeric', '=', '021')])
+    >>> country_us = Country(name="United States", region=north_america)
     >>> country_us.save()
     >>> california = Subdivision(
     ...     name="California", type='state', country=country_us)
@@ -279,20 +281,26 @@
     ...         ('Account Category', Decimal('40'))])
     True
 
-Check sale reporting per regions::
+Check sale reporting per countries::
 
-    >>> Region = Model.get('sale.reporting.region')
+    >>> RegionTree = Model.get('sale.reporting.region.tree')
+    >>> CountryTree = Model.get('sale.reporting.country.tree')
     >>> CountryTimeseries = Model.get('sale.reporting.country.time_series')
     >>> SubdivisionTimeseries = Model.get(
     ...     'sale.reporting.country.subdivision.time_series')
     >>> with config.set_context(context=context):
-    ...     reports = Region.find([])
+    ...     region = RegionTree(north_america.id)
+    ...     countries = CountryTree.find([])
     ...     country_time_series = CountryTimeseries.find([])
     ...     subdivision_time_series = SubdivisionTimeseries.find([])
-    >>> len(reports)
+    >>> region.revenue == Decimal('40')
+    True
+    >>> region.parent.revenue == Decimal('40')
+    True
+    >>> len(countries)
     3
     >>> with config.set_context(context=context):
-    ...     sorted((r.region, r.number, r.revenue) for r in reports) == \
+    ...     sorted((c.region, c.number, c.revenue) for c in countries) == \
     ...     sorted([('United States', 2, Decimal('40')),
     ...         ('California', 1, Decimal('30')),
     ...         ('New York', 1, Decimal('10'))])
diff -r abb45176fb28 -r 3144fd7d8485 view/sale_reporting_country_tree.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/sale_reporting_country_tree.xml      Mon Sep 12 21:31:32 2022 +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="region" expand="1"/>
+    <field name="number"/>
+    <field name="revenue"/>
+    <field name="revenue_trend" expand="1"/>
+</tree>
diff -r abb45176fb28 -r 3144fd7d8485 view/sale_reporting_region_tree.xml
--- a/view/sale_reporting_region_tree.xml       Sun Sep 11 01:29:22 2022 +0200
+++ b/view/sale_reporting_region_tree.xml       Mon Sep 12 21:31:32 2022 +0200
@@ -2,8 +2,6 @@
 <!-- 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="region" expand="1"/>
-    <field name="number"/>
+    <field name="name" expand="1"/>
     <field name="revenue"/>
-    <field name="revenue_trend" expand="1"/>
 </tree>

Reply via email to