424 lines
16 KiB
Python
424 lines
16 KiB
Python
|
# 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, tagged
|
||
|
from odoo.tools.misc import get_lang
|
||
|
from odoo.addons.web.controllers.export import ExportXlsxWriter, Export
|
||
|
from odoo.addons.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', groups='base.group_user,base.group_allow_export')
|
||
|
self.session = 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):
|
||
|
if isinstance(value, float):
|
||
|
decimal_places = style.num_format[::-1].find('.')
|
||
|
style_format = "{:." + str(decimal_places) + "f}"
|
||
|
self.worksheet[row, column] = style_format.format(value)
|
||
|
else:
|
||
|
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,
|
||
|
'type': self.model._fields[f].type,
|
||
|
} 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)),
|
||
|
'csrf_token': http.Request.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")
|
||
|
|
||
|
|
||
|
@tagged('-at_install', 'post_install')
|
||
|
class TestGroupedExport(XlsxCreatorCase):
|
||
|
model_name = 'export.group_operator'
|
||
|
# pylint: disable=bad-whitespace
|
||
|
|
||
|
def test_archived_groupped(self):
|
||
|
""" The decimal separator of the language used shouldn't impact the float representation in the exported xlsx """
|
||
|
get_lang(self.env).decimal_point = ','
|
||
|
get_lang(self.env).thousands_sep = '.'
|
||
|
|
||
|
values = [
|
||
|
{'int_sum': 1, 'active': False},
|
||
|
]
|
||
|
export = self.export(values, fields=['int_sum', 'active'], params={'groupby': ['int_sum']})
|
||
|
|
||
|
self.assertExportEqual(export, [
|
||
|
['Int Sum', 'Active'],
|
||
|
['1 (1)', ''],
|
||
|
])
|
||
|
|
||
|
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.00'],
|
||
|
[' 111.0 (1)','111.00'],
|
||
|
['10' ,'111.00'],
|
||
|
[' 222.0 (1)','222.00'],
|
||
|
['10' ,'222.00'],
|
||
|
['20 (1)' ,'333.00'],
|
||
|
[' 333.0 (1)','333.00'],
|
||
|
['20' ,'333.00'],
|
||
|
])
|
||
|
|
||
|
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.00'],
|
||
|
[' 100.0 (1)','100.00'],
|
||
|
['10' ,'100.00'],
|
||
|
[' 200.0 (1)','200.00'],
|
||
|
['10' ,'200.00'],
|
||
|
['20 (1)' ,'300.00'],
|
||
|
[' 300.0 (1)','300.00'],
|
||
|
['20' ,'300.00'],
|
||
|
])
|
||
|
|
||
|
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.00'],
|
||
|
[' 20 (1)' ,'600.00'],
|
||
|
[' 600.0 (1)','600.00'],
|
||
|
['10' ,'600.00'],
|
||
|
[' 30 (2)' ,'150.00'],
|
||
|
[' 100.0 (1)','100.00'],
|
||
|
['10' ,'100.00'],
|
||
|
[' 200.0 (1)','200.00'],
|
||
|
['10' ,'200.00'],
|
||
|
])
|
||
|
|
||
|
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.00'],
|
||
|
[' 20 (1)' ,'0.00'],
|
||
|
[' Undefined (1)','0.00'],
|
||
|
['10' ,'0.00'],
|
||
|
[' 30 (2)' ,'0.00'],
|
||
|
[' Undefined (2)','0.00'],
|
||
|
['10' ,'0.00'],
|
||
|
['10' ,'0.00'],
|
||
|
])
|
||
|
|
||
|
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' ,''],
|
||
|
])
|
||
|
|
||
|
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' ,''],
|
||
|
])
|
||
|
|
||
|
def test_float_representation(self):
|
||
|
currency = self.env['res.currency'].create({
|
||
|
'name': "bottlecap",
|
||
|
'symbol': "b",
|
||
|
'rounding': 0.001,
|
||
|
'decimal_places': 3,
|
||
|
})
|
||
|
|
||
|
values = [
|
||
|
{'int_sum': 1, 'currency_id': currency.id, 'float_monetary': 60739.2000000004},
|
||
|
{'int_sum': 2, 'currency_id': currency.id, 'float_monetary': 2.0},
|
||
|
{'int_sum': 3, 'currency_id': currency.id, 'float_monetary': 999.9995999},
|
||
|
]
|
||
|
export = self.export(values, fields=['int_sum', 'float_monetary'], params={'groupby': ['int_sum', 'float_monetary']})
|
||
|
|
||
|
self.assertExportEqual(export, [
|
||
|
['Int Sum', 'Float Monetary'],
|
||
|
['1 (1)', '60739.200'],
|
||
|
[' 60739.2 (1)', '60739.200'],
|
||
|
['1', '60739.20'],
|
||
|
['2 (1)', '2.000'],
|
||
|
[' 2.0 (1)', '2.000'],
|
||
|
['2', '2.00'],
|
||
|
['3 (1)', '1000.000'],
|
||
|
[' 1000.0 (1)', '1000.000'],
|
||
|
['3', '1000.00'],
|
||
|
])
|
||
|
|
||
|
def test_decimal_separator(self):
|
||
|
""" The decimal separator of the language used shouldn't impact the float representation in the exported xlsx """
|
||
|
get_lang(self.env).decimal_point = ','
|
||
|
get_lang(self.env).thousands_sep = '.'
|
||
|
|
||
|
values = [
|
||
|
{'int_sum': 1, 'float_min': 86420.864},
|
||
|
]
|
||
|
export = self.export(values, fields=['int_sum', 'float_min'], params={'groupby': ['int_sum', 'float_min']})
|
||
|
|
||
|
self.assertExportEqual(export, [
|
||
|
['Int Sum' ,'Float Min'],
|
||
|
['1 (1)' ,'86420.86'],
|
||
|
[' 86420.864 (1)','86420.86'],
|
||
|
['1' ,'86420.86'],
|
||
|
])
|
||
|
|
||
|
@tagged('-at_install', 'post_install')
|
||
|
class TestExport(common.HttpCase):
|
||
|
|
||
|
def test_properties_type_fields_not_selectable_with_import_compat(self):
|
||
|
with patch.object(Export, 'fields_get', return_value={
|
||
|
'id': {'string': 'ID', 'type': 'integer'},
|
||
|
'name': {'string': 'Name', 'type': 'char'},
|
||
|
'properties': {'string': 'Properties', 'type': 'properties'},
|
||
|
'properties_definition': {'string': 'Properties Definition', 'type': 'properties_definition'}
|
||
|
}):
|
||
|
fields = Export().get_fields("mock_model", import_compat=True)
|
||
|
field_names = [field['id'] for field in fields]
|
||
|
self.assertNotIn('properties', field_names)
|
||
|
self.assertNotIn('properties_definition', field_names)
|