summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/test_xlsx_export/__init__.py1
-rw-r--r--addons/test_xlsx_export/__manifest__.py12
-rw-r--r--addons/test_xlsx_export/ir.model.access.csv3
-rw-r--r--addons/test_xlsx_export/models.py34
-rw-r--r--addons/test_xlsx_export/tests/__init__.py4
-rw-r--r--addons/test_xlsx_export/tests/test_export.py341
-rw-r--r--addons/web/controllers/main.py104
-rw-r--r--addons/web/tests/__init__.py1
-rw-r--r--addons/web/tests/test_export.py176
9 files changed, 486 insertions, 190 deletions
diff --git a/addons/test_xlsx_export/__init__.py b/addons/test_xlsx_export/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/addons/test_xlsx_export/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/test_xlsx_export/__manifest__.py b/addons/test_xlsx_export/__manifest__.py
new file mode 100644
index 00000000000..1459d18a3dc
--- /dev/null
+++ b/addons/test_xlsx_export/__manifest__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'test xlsx export',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to test xlsx export.""",
+ 'depends': ['web', 'test_mail'],
+ 'data': ['ir.model.access.csv'],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/test_xlsx_export/ir.model.access.csv b/addons/test_xlsx_export/ir.model.access.csv
new file mode 100644
index 00000000000..c142abbcd12
--- /dev/null
+++ b/addons/test_xlsx_export/ir.model.access.csv
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+access_export_group_operator,access_export_group_operator,model_export_group_operator,,1,1,1,1
+access_export_group_operator_one2many,access_export_group_operator_one2many,model_export_group_operator_one2many,,1,1,1,1
diff --git a/addons/test_xlsx_export/models.py b/addons/test_xlsx_export/models.py
new file mode 100644
index 00000000000..99d96ba63cb
--- /dev/null
+++ b/addons/test_xlsx_export/models.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+class NewModel(models.Model):
+ _name = 'export.integer'
+ _description = 'Export: Integer'
+
+ value = fields.Integer(default=4)
+
+ def name_get(self):
+ return [(record.id, "%s:%s" % (self._name, record.value)) for record in self]
+
+class GroupOperator(models.Model):
+ _name = 'export.group_operator'
+ _description = 'Export Group Operator'
+
+ int_sum = fields.Integer(group_operator='sum')
+ int_max = fields.Integer(group_operator='max')
+ float_min = fields.Float(group_operator='min')
+ float_avg = fields.Float(group_operator='avg')
+ date_max = fields.Date(group_operator='max')
+ bool_and = fields.Boolean(group_operator='bool_and')
+ bool_or = fields.Boolean(group_operator='bool_or')
+ many2one = fields.Many2one('export.integer')
+ one2many = fields.One2many('export.group_operator.one2many', 'parent_id')
+
+class GroupOperatorO2M(models.Model):
+ _name = 'export.group_operator.one2many'
+ _description = 'Export Group Operator One2Many'
+
+ parent_id = fields.Many2one('export.group_operator')
+ value = fields.Integer()
diff --git a/addons/test_xlsx_export/tests/__init__.py b/addons/test_xlsx_export/tests/__init__.py
new file mode 100644
index 00000000000..ff41d3a8d95
--- /dev/null
+++ b/addons/test_xlsx_export/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import test_export
+
diff --git a/addons/test_xlsx_export/tests/test_export.py b/addons/test_xlsx_export/tests/test_export.py
new file mode 100644
index 00000000000..8aeeb45cbd0
--- /dev/null
+++ b/addons/test_xlsx_export/tests/test_export.py
@@ -0,0 +1,341 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import json
+from datetime import date
+from unittest.mock import patch
+
+from odoo import http
+from odoo.tests import common
+from odoo.addons.web.controllers.main import ExportXlsxWriter
+from odoo.addons.test_mail.tests.common import mail_new_test_user
+
+
+class XlsxCreatorCase(common.HttpCase):
+ model_name = False
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.model = None
+
+ def setUp(self):
+ super().setUp()
+ self.model = self.env[self.model_name]
+
+ mail_new_test_user(self.env, login='fof', password='123456789')
+ self.authenticate('fof', '123456789')
+
+ self.worksheet = {} # mock worksheet
+
+ self.default_params = {
+ 'domain': [],
+ 'fields': [{'name': field.name, 'label': field.string} for field in self.model._fields.values()],
+ 'groupby': [],
+ 'ids': False,
+ 'import_compat': False,
+ 'model': self.model._name,
+ }
+
+ def _mock_write(self, row, column, value, style=None):
+ self.worksheet[row, column] = str(value)
+
+ def make(self, values, context=None):
+ return self.model.with_context(**(context or {})).create(values)
+
+ def export(self, values, fields=[], params={}, context=None):
+ self.worksheet = {}
+ self.make(values, context=context)
+
+ if fields and 'fields' not in params:
+ params['fields'] = [{
+ 'name': self.model._fields[f].name,
+ 'label': self.model._fields[f].string,
+ } for f in fields]
+
+ with patch.object(ExportXlsxWriter, 'write', self._mock_write):
+ self.url_open('/web/export/xlsx', data={
+ 'data': json.dumps(dict(self.default_params, **params)),
+ 'token': 'dummy',
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ })
+ return self.worksheet
+
+ def assertExportEqual(self, value, expected):
+ for row in range(len(expected)):
+ for column in range(len(expected[row])):
+ cell_value = value.pop((row, column), '')
+ expected_value = expected[row][column]
+ self.assertEqual(cell_value, expected_value, "Cell %s, %s have a wrong value" % (row, column))
+ self.assertFalse(value, "There are unexpected cells in the export")
+
+
+class TestGroupedExport(XlsxCreatorCase):
+ model_name = 'export.group_operator'
+
+ def test_int_sum_max(self):
+ values = [
+ {'int_sum': 10, 'int_max': 20},
+ {'int_sum': 10, 'int_max': 50},
+ {'int_sum': 20,'int_max': 30},
+ ]
+ export = self.export(values, fields=['int_sum', 'int_max'], params={'groupby': ['int_sum', 'int_max']})
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Int Max'],
+ ['10 (2)' ,'50'],
+ [' 20 (1)' ,'20'],
+ ['10' ,'20'],
+ [' 50 (1)' ,'50'],
+ ['10' ,'50'],
+ ['20 (1)' ,'30'],
+ [' 30 (1)' ,'30'],
+ ['20' ,'30'],
+ ])
+
+ export = self.export([], fields=['int_max', 'int_sum'], params={'groupby': ['int_sum', 'int_max']})
+
+ self.assertExportEqual(export, [
+ ['Int Max' ,'Int Sum'],
+ ['10 (2)' ,'20'],
+ [' 20 (1)' ,'10'],
+ ['20' ,'10'],
+ [' 50 (1)' ,'10'],
+ ['50' ,'10'],
+ ['20 (1)' ,'20'],
+ [' 30 (1)' ,'20'],
+ ['30' ,'20'],
+ ])
+
+ def test_float_min(self):
+ values = [
+ {'int_sum': 10, 'float_min': 111.0},
+ {'int_sum': 10, 'float_min': 222.0},
+ {'int_sum': 20, 'float_min': 333.0},
+ ]
+ export = self.export(values, fields=['int_sum', 'float_min'], params={'groupby': ['int_sum', 'float_min']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Float Min'],
+ ['10 (2)' ,'111.0'],
+ [' 111.0 (1)','111.0'],
+ ['10' ,'111.0'],
+ [' 222.0 (1)','222.0'],
+ ['10' ,'222.0'],
+ ['20 (1)' ,'333.0'],
+ [' 333.0 (1)','333.0'],
+ ['20' ,'333.0'],
+ ])
+
+ def test_float_avg(self):
+ values = [
+ {'int_sum': 10, 'float_avg': 100.0},
+ {'int_sum': 10, 'float_avg': 200.0},
+ {'int_sum': 20, 'float_avg': 300.0},
+ ]
+ export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'float_avg']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Float Avg'],
+ ['10 (2)' ,'150.0'],
+ [' 100.0 (1)','100.0'],
+ ['10' ,'100.0'],
+ [' 200.0 (1)','200.0'],
+ ['10' ,'200.0'],
+ ['20 (1)' ,'300.0'],
+ [' 300.0 (1)','300.0'],
+ ['20' ,'300.0'],
+ ])
+
+ def test_float_avg_nested(self):
+ """ With more than one nested level (avg aggregation) """
+ values = [
+ {'int_sum': 10, 'int_max': 30, 'float_avg': 100.0},
+ {'int_sum': 10, 'int_max': 30, 'float_avg': 200.0},
+ {'int_sum': 10, 'int_max': 20, 'float_avg': 600.0},
+ ]
+ export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'int_max', 'float_avg']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Float Avg'],
+ ['10 (3)' ,'300.0'],
+ [' 20 (1)' ,'600.0'],
+ [' 600.0 (1)','600.0'],
+ ['10' ,'600.0'],
+ [' 30 (2)' ,'150.0'],
+ [' 100.0 (1)','100.0'],
+ ['10' ,'100.0'],
+ [' 200.0 (1)','200.0'],
+ ['10' ,'200.0'],
+ ])
+
+ def test_float_avg_nested_no_value(self):
+ """ With more than one nested level (avg aggregation is done on 0, not False) """
+ values = [
+ {'int_sum': 10, 'int_max': 20, 'float_avg': False},
+ {'int_sum': 10, 'int_max': 30, 'float_avg': False},
+ {'int_sum': 10, 'int_max': 30, 'float_avg': False},
+ ]
+ export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'int_max', 'float_avg']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Float Avg'],
+ ['10 (3)' ,'0.0'],
+ [' 20 (1)' ,'0.0'],
+ [' Undefined (1)','0.0'],
+ ['10' ,'0.0'],
+ [' 30 (2)' ,'0.0'],
+ [' Undefined (2)','0.0'],
+ ['10' ,'0.0'],
+ ['10' ,'0.0'],
+ ])
+
+ def test_date_max(self):
+ values = [
+ {'int_sum': 10, 'date_max': date(2019, 1, 1)},
+ {'int_sum': 10, 'date_max': date(2000, 1, 1)},
+ {'int_sum': 20, 'date_max': date(1980, 1, 1)},
+ ]
+ export = self.export(values, fields=['int_sum', 'date_max'], params={'groupby': ['int_sum', 'date_max:month']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Date Max'],
+ ['10 (2)' ,'2019-01-01'],
+ [' January 2000 (1)' ,'2000-01-01'],
+ ['10' ,'2000-01-01'],
+ [' January 2019 (1)' ,'2019-01-01'],
+ ['10' ,'2019-01-01'],
+ ['20 (1)' ,'1980-01-01'],
+ [' January 1980 (1)' ,'1980-01-01'],
+ ['20' ,'1980-01-01'],
+ ])
+
+ def test_bool_and(self):
+ values = [
+ {'int_sum': 10, 'bool_and': True},
+ {'int_sum': 10, 'bool_and': True},
+ {'int_sum': 20, 'bool_and': True},
+ {'int_sum': 20, 'bool_and': False},
+ ]
+ export = self.export(values, fields=['int_sum', 'bool_and'], params={'groupby': ['int_sum', 'bool_and']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Bool And'],
+ ['10 (2)' ,'True'],
+ [' True (2)' ,'True'],
+ ['10' ,'True'],
+ ['10' ,'True'],
+ ['20 (2)' ,'False'],
+ [' False (1)' ,'False'],
+ ['20' ,'False'],
+ [' True (1)' ,'True'],
+ ['20' ,'True'],
+ ])
+
+ def test_bool_or(self):
+ values = [
+ {'int_sum': 10, 'bool_or': True},
+ {'int_sum': 10, 'bool_or': False},
+ {'int_sum': 20, 'bool_or': False},
+ {'int_sum': 20, 'bool_or': False},
+ ]
+ export = self.export(values, fields=['int_sum', 'bool_or'], params={'groupby': ['int_sum', 'bool_or']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Bool Or'],
+ ['10 (2)' ,'True'],
+ [' False (1)' ,'False'],
+ ['10' ,'False'],
+ [' True (1)' ,'True'],
+ ['10' ,'True'],
+ ['20 (2)' ,'False'],
+ [' False (2)' ,'False'],
+ ['20' ,'False'],
+ ['20' ,'False'],
+ ])
+
+ def test_many2one(self):
+ values = [
+ {'int_sum': 10, 'many2one': self.env['export.integer'].create({}).id},
+ {'int_sum': 10},
+ ]
+ export = self.export(values, fields=['int_sum', 'many2one'], params={'groupby': ['int_sum', 'many2one']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Many2One'],
+ ['10 (2)' ,''],
+ [' export.integer:4 (1)' ,''],
+ ['10' ,'export.integer:4'],
+ [' Undefined (1)' ,''],
+ ['10' ,'False'],
+ ])
+
+ def test_nested_records(self):
+ """
+ aggregated values currently not supported for nested record export, but it should not crash
+ e.g. export 'many2one/const'
+ """
+ values = [{'int_sum': 10,
+ 'date_max': date(2019, 1, 1),
+ 'many2one': self.env['export.integer'].create({}).id,
+ }, {
+ 'int_sum': 10,
+ 'date_max': date(2000, 1, 1),
+ 'many2one': self.env['export.integer'].create({}).id,
+ },]
+ export = self.export(values,
+ params={
+ 'groupby': ['int_sum', 'date_max:month'],
+ 'fields': [
+ {'name': 'int_sum', 'label': 'Int Sum'},
+ {'name': 'date_max', 'label': 'Date Max'},
+ {'name': 'many2one/value', 'label': 'Many2One/Value'},
+ ]
+ })
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Date Max' ,'Many2One/Value'],
+ ['10 (2)' ,'2019-01-01' ,''],
+ [' January 2000 (1)' ,'2000-01-01' ,''],
+ ['10' ,'2000-01-01' ,'4'],
+ [' January 2019 (1)' ,'2019-01-01' ,''],
+ ['10' ,'2019-01-01' ,'4'],
+ ])
+
+ def test_one2many(self):
+ values = [{
+ 'int_sum': 10,
+ 'one2many': [
+ (0, 0, {'value': 8}),
+ (0, 0, {'value': 9}),
+ ],
+ }]
+ export = self.export(values,
+ params={
+ 'groupby': ['int_sum',],
+ 'fields': [
+ {'name': 'int_sum', 'label': 'Int Sum'},
+ {'name': 'one2many/value', 'label': 'One2many/Value'},
+ ]
+ })
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'One2many/Value'],
+ ['10 (1)' ,''],
+ ['10' ,'8'],
+ ['' ,'9'],
+ ])
+
+ def test_unset_date_values(self):
+ values = [
+ {'int_sum': 10, 'date_max': date(2019, 1, 1)},
+ {'int_sum': 10, 'date_max': False},
+ ]
+ # Group and aggregate by date, but date fields are not set for all records
+ export = self.export(values, fields=['int_sum', 'date_max'], params={'groupby': ['int_sum', 'date_max:month']})
+
+ self.assertExportEqual(export, [
+ ['Int Sum' ,'Date Max'],
+ ['10 (2)' ,'2019-01-01'],
+ [' January 2019 (1)' ,'2019-01-01'],
+ ['10' ,'2019-01-01'],
+ [' Undefined (1)' ,''],
+ ['10' ,''],
+ ])
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 165baa693b5..133421caa98 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -35,7 +35,7 @@ import odoo
import odoo.modules.registry
from odoo.api import call_kw, Environment
from odoo.modules import get_module_path, get_resource_path
-from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr, apply_inheritance_specs
+from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr, apply_inheritance_specs, lazy_property
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.translate import _
from odoo.tools.misc import str2bool, xlsxwriter, file_open
@@ -64,6 +64,37 @@ DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
COMMENT_PATTERN = r'Modified by [\s\w\-.]+ from [\s\w\-.]+'
+
+def none_values_filtered(func):
+ @functools.wraps(func)
+ def wrap(iterable):
+ return func(v for v in iterable if v is not None)
+ return wrap
+
+def allow_empty_iterable(func):
+ """
+ Some functions do not accept empty iterables (e.g. max, min with no default value)
+ This returns the function `func` such that it returns None if the iterable
+ is empty instead of raising a ValueError.
+ """
+ @functools.wraps(func)
+ def wrap(iterable):
+ iterator = iter(iterable)
+ try:
+ value = next(iterator)
+ return func(itertools.chain([value], iterator))
+ except StopIteration:
+ return None
+ return wrap
+
+OPERATOR_MAPPING = {
+ 'max': none_values_filtered(allow_empty_iterable(max)),
+ 'min': none_values_filtered(allow_empty_iterable(min)),
+ 'sum': sum,
+ 'bool_and': all,
+ 'bool_or': any,
+}
+
#----------------------------------------------------------
# Odoo Web helpers
#----------------------------------------------------------
@@ -591,12 +622,67 @@ class GroupsTreeNode:
self._groupby_type = groupby_type
self.count = 0 # Total number of records in the subtree
- self.aggregated_values = Counter() # Fields aggregated values {field_name: aggregated value}
self.children = OrderedDict()
self.data = [] # Only leaf nodes have data
+
if root:
self.insert_leaf(root)
+ def _get_aggregate(self, field_name, data, group_operator):
+ # When exporting one2many fields, multiple data lines might be exported for one record.
+ # Blank cells of additionnal lines are filled with an empty string. This could lead to '' being
+ # aggregated with an integer or float.
+ data = (value for value in data if value != '')
+
+ if group_operator == 'avg':
+ return self._get_avg_aggregate(field_name, data)
+
+ aggregate_func = OPERATOR_MAPPING.get(group_operator)
+ if not aggregate_func:
+ _logger.warning("Unsupported export of group_operator '%s' for field %s on model %s" % (group_operator, field_name, self._model._name))
+ return
+
+ if self.data:
+ return aggregate_func(data)
+ return aggregate_func((child.aggregated_values.get(field_name) for child in self.children.values()))
+
+ def _get_avg_aggregate(self, field_name, data):
+ aggregate_func = OPERATOR_MAPPING.get('sum')
+ if self.data:
+ return aggregate_func(data) / self.count
+ children_sums = (child.aggregated_values.get(field_name) * child.count for child in self.children.values())
+ return aggregate_func(children_sums) / self.count
+
+ def _get_aggregated_field_names(self):
+ """ Return field names of exported field having a group operator """
+ aggregated_field_names = []
+ for field_name in self._fields:
+ if '/' in field_name:
+ # Currently no support of aggregated value for nested record fields
+ # e.g. line_ids/analytic_line_ids/amount
+ continue
+ field = self._model._fields[field_name]
+ if field.group_operator:
+ aggregated_field_names.append(field_name)
+ return aggregated_field_names
+
+ # Lazy property to memoize aggregated values of children nodes to avoid useless recomputations
+ @lazy_property
+ def aggregated_values(self):
+
+ aggregated_values = {}
+
+ # Transpose the data matrix to group all values of each field in one iterable
+ field_values = zip(*self.data)
+ for field_name in self._fields:
+ field_data = self.data and next(field_values) or []
+
+ if field_name in self._get_aggregated_field_names():
+ field = self._model._fields[field_name]
+ aggregated_values[field_name] = self._get_aggregate(field_name, field_data, field.group_operator)
+
+ return aggregated_values
+
def child(self, key):
"""
Return the child identified by `key`.
@@ -614,17 +700,9 @@ class GroupsTreeNode:
Build a leaf from `group` and insert it in the tree.
:param group: dict as returned by `read_group(lazy=False)`
"""
- # Pop every known key in the group dict (__domain, __count and grouped values)
- # Remaining keys are aggregated fields.
- leaf_path = [group.pop(groupby_field) for groupby_field in self._groupby]
+ leaf_path = [group.get(groupby_field) for groupby_field in self._groupby]
domain = group.pop('__domain')
count = group.pop('__count')
- aggregated_values = group
-
- keys = list(aggregated_values)
- for key in keys:
- if not isinstance(aggregated_values[key], (int, float)):
- del aggregated_values[key]
records = self._model.search(domain, offset=0, limit=False, order=False)
@@ -637,7 +715,6 @@ class GroupsTreeNode:
node = node.child(node_key)
# Update count value and aggregated value.
node.count += count
- node.aggregated_values += Counter(aggregated_values)
node.data = records.export_data(self._fields).get('datas',[])
@@ -737,7 +814,8 @@ class GroupExportXlsxWriter(ExportXlsxWriter):
self.write(row, column, label, self.header_bold_style)
for field in self.fields[1:]: # No aggregates allowed in the first column because of the group title
column += 1
- self.write(row, column, aggregates.get(field['name'], ''), self.header_bold_style)
+ aggregated_value = aggregates.get(field['name'])
+ self.write(row, column, str(aggregated_value if aggregated_value is not None else ''), self.header_bold_style)
return row + 1, 0
diff --git a/addons/web/tests/__init__.py b/addons/web/tests/__init__.py
index b479a574e46..1ed329bafe1 100644
--- a/addons/web/tests/__init__.py
+++ b/addons/web/tests/__init__.py
@@ -6,4 +6,3 @@ from . import test_js
from . import test_menu
from . import test_serving_base
from . import test_click_everywhere
-from . import test_export
diff --git a/addons/web/tests/test_export.py b/addons/web/tests/test_export.py
deleted file mode 100644
index b0fed393e10..00000000000
--- a/addons/web/tests/test_export.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from unittest.mock import patch
-import json
-from odoo import http
-from odoo.tests import common, new_test_user
-from odoo.addons.web.controllers.main import ExportXlsxWriter
-
-
-class TestExport(common.HttpCase):
-
- def setUp(self):
- super().setUp()
-
- # Archive demo partners
- self.env['res.users'].search([]).active = False
- self.env['res.partner'].search([]).active = False
- new_test_user(self.env, login='fef', password='pwd')
- self.authenticate('fef', 'pwd')
-
- self.env['res.partner'].create([{
- 'name': 'partner 1',
- 'comment': 'yop',
- 'website': 'yop.com',
- 'credit_limit': 0,
- },{
- 'name': 'partner 2',
- 'comment': 'yop',
- 'website': 'blop.com',
- 'credit_limit': 3,
- },{
- 'name': 'partner 3',
- 'comment': 'yop',
- 'website': 'blop.com',
- 'credit_limit': 1,
- }, {
- 'name': 'partner 4',
- 'comment': 'blop',
- 'website': 'blop.com',
- 'credit_limit': 2,
- }])
-
- self.worksheet = {} # mock worksheet
-
- self.default_params = {
- 'domain': [],
- 'fields': [{
- 'label': "Partner name",
- 'name': 'name',
- }, {
- 'label': "Comment",
- 'name': "comment",
- }, {
- 'label': 'Website',
- 'name': 'website',
- }],
- 'groupby': [],
- 'ids': False,
- 'import_compat': False,
- 'model': 'res.partner',
- }
-
- def mock_write(self, row, column, value, style=None):
- self.worksheet[row, column] = value
-
- def test_grouped_list_export(self):
-
- with patch.object(ExportXlsxWriter, 'write', self.mock_write):
- self.default_params['groupby'] = ['comment', 'website']
- params = dict(self.default_params, **{
- 'groupby': ['comment', 'website'],
- 'fields': [*self.default_params['fields'], {'label': "Credit Limit", 'name': 'credit_limit'}]
- })
- self.url_open('/web/export/xlsx', data={
- 'data': json.dumps(params),
- 'token': 'dummy',
- 'csrf_token': http.WebRequest.csrf_token(self),
- })
-
- # Main header
- self.assertEqual(self.worksheet[0, 0], "Partner name", "It should display the list header")
- self.assertEqual(self.worksheet[0, 1], "Comment", "It should display the list header")
- self.assertEqual(self.worksheet[0, 2], "Website", "It should display the list header")
- self.assertEqual(self.worksheet[0, 3], "Credit Limit", "It should display the list header")
-
- # Header: Group Comment=blop
- self.assertEqual(self.worksheet[1, 0], "blop (1)", "It should display the group header")
- self.assertEqual(self.worksheet[1, 3], 2, "It should display the aggregated value")
-
- # Header: Group Comment=blop > Website=http://blop.com
- self.assertEqual(self.worksheet[2, 0], " http://blop.com (1)", "It should display the group header")
- self.assertEqual(self.worksheet[2, 3], 2, "It should display the aggregated value")
-
- # Data: Group Comment=blop > Website=http://blop.com
- self.assertEqual(self.worksheet[3, 0], "partner 4")
- self.assertEqual(self.worksheet[3, 1], "blop")
- self.assertEqual(self.worksheet[3, 2], "http://blop.com")
- self.assertEqual(self.worksheet[3, 3], 2)
-
- # Header: Group Comment=yop
- self.assertEqual(self.worksheet[4, 0], "yop (3)", "It should display the group header")
- self.assertEqual(self.worksheet[4, 3], 4, "It should display the aggregated value")
-
- # Header: Group Comment=yop > Website=http://blop.com
- self.assertEqual(self.worksheet[5, 0], " http://blop.com (2)", "It should display the group header")
- self.assertEqual(self.worksheet[5, 3], 4, "It should display the aggregated value")
-
- # Data: Group Comment=yop > Website=http://blop.com
- self.assertEqual(self.worksheet[6, 0], "partner 2")
- self.assertEqual(self.worksheet[6, 1], "yop")
- self.assertEqual(self.worksheet[6, 2], "http://blop.com")
- self.assertEqual(self.worksheet[6, 3], 3)
-
- self.assertEqual(self.worksheet[7, 0], "partner 3")
- self.assertEqual(self.worksheet[7, 1], "yop")
- self.assertEqual(self.worksheet[7, 2], "http://blop.com")
- self.assertEqual(self.worksheet[7, 3], 1)
-
- # Header: Group Comment=yop > Website=http://yop.com
- self.assertEqual(self.worksheet[8, 0], " http://yop.com (1)", "It should display the group header")
- self.assertEqual(self.worksheet[8, 3], '', "It should not have any aggregated value")
-
- # Data: Group Comment=yop > Website=http://yop.com
- self.assertEqual(self.worksheet[9, 0], "partner 1")
- self.assertEqual(self.worksheet[9, 1], "yop")
- self.assertEqual(self.worksheet[9, 2], "http://yop.com")
- self.assertEqual(self.worksheet[9, 3], 0)
-
- # Header: Group Comment=False
- self.assertEqual(self.worksheet[10, 0], "Undefined (1)", "It should display the group header")
- self.assertEqual(self.worksheet[10, 3], '', "It should not have any aggregated value")
-
- # Header: Group Comment=False > Website=False
- self.assertEqual(self.worksheet[11, 0], " Undefined (1)", "It should display the group header")
- self.assertEqual(self.worksheet[11, 3], '', "It should not have any aggregated value")
-
- # Data: Group Comment=False > Website=False
- self.assertEqual(self.worksheet[12, 0], "fef (base.group_user)")
- self.assertEqual(self.worksheet[12, 1], "")
- self.assertEqual(self.worksheet[12, 2], "")
- self.assertEqual(self.worksheet[12, 3], 0)
-
- def test_list_export(self):
-
- with patch.object(ExportXlsxWriter, 'write', self.mock_write):
- self.url_open('/web/export/xlsx', data={
- 'data': json.dumps(self.default_params),
- 'token': 'dummy',
- 'csrf_token': http.WebRequest.csrf_token(self),
- })
-
- # Main header
- self.assertEqual(self.worksheet[0, 0], "Partner name", "It should display the list header")
- self.assertEqual(self.worksheet[0, 1], "Comment", "It should display the list header")
- self.assertEqual(self.worksheet[0, 2], "Website", "It should display the list header")
-
- self.assertEqual(self.worksheet[1, 0], "fef (base.group_user)")
- self.assertEqual(self.worksheet[1, 1], "")
- self.assertEqual(self.worksheet[1, 2], "")
-
- self.assertEqual(self.worksheet[2, 0], "partner 1")
- self.assertEqual(self.worksheet[2, 1], "yop")
- self.assertEqual(self.worksheet[2, 2], "http://yop.com")
-
- self.assertEqual(self.worksheet[3, 0], "partner 2")
- self.assertEqual(self.worksheet[3, 1], "yop")
- self.assertEqual(self.worksheet[3, 2], "http://blop.com")
-
- self.assertEqual(self.worksheet[4, 0], "partner 3")
- self.assertEqual(self.worksheet[4, 1], "yop")
- self.assertEqual(self.worksheet[4, 2], "http://blop.com")
-
- self.assertEqual(self.worksheet[5, 0], "partner 4")
- self.assertEqual(self.worksheet[5, 1], "blop")
- self.assertEqual(self.worksheet[5, 2], "http://blop.com")