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")