[FIX] web: Correctly aggregate values in exported parent groups

When exporting a grouped list view with some nested groups, the aggregate value
of parent groups are not correct. It always sums aggregated values of children
whether the group operator is 'sum' or not (could be 'max', 'avg', ...).

This behavior is wrong and can even lead to a crash if the aggregated field is a
date field (e.g. with group_operator='max'). (Try two sum two dates...)

The quick fix 85cf47f was merged just before OXP to avoid any crash. This fix
limited the support of aggregates to only int and float fields.
This commit remove this limitation.

This commit correclty implements the aggregation for parent group for all
field types and all group_operator.

This commit also improves the export feature tests.

X-original-commit: 5e7e4fa98698967e3c4fd0903f4aa8e91981a6cd
This commit is contained in:
Lucas Lefèvre
2019-10-16 15:49:49 +00:00
committed by fw-bot
parent e76c65e8b4
commit 645ae69bb9
9 changed files with 486 additions and 190 deletions
+1
View File
@@ -0,0 +1 @@
from . import models
+12
View File
@@ -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,
}
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_export_group_operator access_export_group_operator model_export_group_operator 1 1 1 1
3 access_export_group_operator_one2many access_export_group_operator_one2many model_export_group_operator_one2many 1 1 1 1
+34
View File
@@ -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()
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_export
@@ -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' ,''],
])
+91 -13
View File
@@ -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
-1
View File
@@ -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
-176
View File
@@ -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")