From 719e0286401d8b2947058028a1a7a03663464867 Mon Sep 17 00:00:00 2001 From: MerlinGuillaume Date: Fri, 24 Jun 2022 11:21:11 +0000 Subject: [PATCH] [FIX] web: number format in exported group header for float values Exporting data incorrectly formats the float values of group headers Steps to reproduce: 1. Install Planning 2. Open Planning and trigger the list view 3. Remove the default filter and add a group_by on employees 4. Export the data 5. The file produced doesn't have the same format for Allocated Hours in the group headers and in the line details Solution: Create formats for float and monetary values using the user's preferences in decimal separator and decimal precision. For monetary format, we use the biggest decimal precision used in the company currencies. opw-2864273 closes odoo/odoo#98813 X-original-commit: 718e8eea7862ad307479190336baf5b5e7092ae4 Signed-off-by: Julien Castiaux Signed-off-by: Guillaume Merlin (megu) --- addons/test_xlsx_export/tests/test_export.py | 38 +++++++++++--------- addons/web/controllers/export.py | 35 +++++++++--------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/addons/test_xlsx_export/tests/test_export.py b/addons/test_xlsx_export/tests/test_export.py index 85f1e8e7e26..d8a3c4eaebd 100644 --- a/addons/test_xlsx_export/tests/test_export.py +++ b/addons/test_xlsx_export/tests/test_export.py @@ -36,7 +36,12 @@ class XlsxCreatorCase(common.HttpCase): } def _mock_write(self, row, column, value, style=None): - self.worksheet[row, column] = str(value) + 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) @@ -71,6 +76,7 @@ class XlsxCreatorCase(common.HttpCase): @tagged('-at_install', 'post_install') class TestGroupedExport(XlsxCreatorCase): model_name = 'export.group_operator' + # pylint: disable=bad-whitespace def test_int_sum_max(self): values = [ @@ -117,12 +123,12 @@ class TestGroupedExport(XlsxCreatorCase): ['Int Sum' ,'Float Min'], ['10 (2)' ,'111.00'], [' 111.0 (1)','111.00'], - ['10' ,'111.0'], + ['10' ,'111.00'], [' 222.0 (1)','222.00'], - ['10' ,'222.0'], + ['10' ,'222.00'], ['20 (1)' ,'333.00'], [' 333.0 (1)','333.00'], - ['20' ,'333.0'], + ['20' ,'333.00'], ]) def test_float_avg(self): @@ -137,12 +143,12 @@ class TestGroupedExport(XlsxCreatorCase): ['Int Sum' ,'Float Avg'], ['10 (2)' ,'150.00'], [' 100.0 (1)','100.00'], - ['10' ,'100.0'], + ['10' ,'100.00'], [' 200.0 (1)','200.00'], - ['10' ,'200.0'], + ['10' ,'200.00'], ['20 (1)' ,'300.00'], [' 300.0 (1)','300.00'], - ['20' ,'300.0'], + ['20' ,'300.00'], ]) def test_float_avg_nested(self): @@ -159,12 +165,12 @@ class TestGroupedExport(XlsxCreatorCase): ['10 (3)' ,'300.00'], [' 20 (1)' ,'600.00'], [' 600.0 (1)','600.00'], - ['10' ,'600.0'], + ['10' ,'600.00'], [' 30 (2)' ,'150.00'], [' 100.0 (1)','100.00'], - ['10' ,'100.0'], + ['10' ,'100.00'], [' 200.0 (1)','200.00'], - ['10' ,'200.0'], + ['10' ,'200.00'], ]) def test_float_avg_nested_no_value(self): @@ -181,11 +187,11 @@ class TestGroupedExport(XlsxCreatorCase): ['10 (3)' ,'0.00'], [' 20 (1)' ,'0.00'], [' Undefined (1)','0.00'], - ['10' ,'0.0'], + ['10' ,'0.00'], [' 30 (2)' ,'0.00'], [' Undefined (2)','0.00'], - ['10' ,'0.0'], - ['10' ,'0.0'], + ['10' ,'0.00'], + ['10' ,'0.00'], ]) def test_date_max(self): @@ -359,11 +365,11 @@ class TestGroupedExport(XlsxCreatorCase): ['Int Sum', 'Float Monetary'], ['1 (1)', '60739.200'], [' 60739.2 (1)', '60739.200'], - ['1', '60739.2'], + ['1', '60739.20'], ['2 (1)', '2.000'], [' 2.0 (1)', '2.000'], - ['2', '2.0'], + ['2', '2.00'], ['3 (1)', '1000.000'], [' 1000.0 (1)', '1000.000'], - ['3', '1000.0'], + ['3', '1000.00'], ]) diff --git a/addons/web/controllers/export.py b/addons/web/controllers/export.py index 2de0e7f1cd1..93eaba4ad6e 100644 --- a/addons/web/controllers/export.py +++ b/addons/web/controllers/export.py @@ -16,8 +16,8 @@ import odoo.modules.registry from odoo import http from odoo.exceptions import UserError from odoo.http import content_disposition, request -from odoo.tools import float_repr, lazy_property, osutil, pycompat -from odoo.tools.misc import xlsxwriter +from odoo.tools import lazy_property, osutil, pycompat +from odoo.tools.misc import xlsxwriter, get_lang from odoo.tools.translate import _ @@ -183,6 +183,11 @@ class ExportXlsxWriter: self.datetime_style = self.workbook.add_format({'text_wrap': True, 'num_format': 'yyyy-mm-dd hh:mm:ss'}) self.worksheet = self.workbook.add_worksheet() self.value = False + decimal_separator = get_lang(request.env).decimal_point + self.float_format = f'0{decimal_separator}00' + decimal_places = [res['decimal_places'] for res in + request.env['res.currency'].search_read([], ['decimal_places'])] + self.monetary_format = f'0{decimal_separator}{max(decimal_places or [2]) * "0"}' if row_count > self.worksheet.xls_rowmax: raise UserError(_('There are too many rows (%s rows, limit: %s) to export as Excel 2007-2013 (.xlsx) format. Consider splitting the export.') % (row_count, self.worksheet.xls_rowmax)) @@ -230,6 +235,8 @@ class ExportXlsxWriter: cell_style = self.datetime_style elif isinstance(cell_value, datetime.date): cell_style = self.date_style + elif isinstance(cell_value, float): + cell_style.set_num_format(self.float_format) self.write(row, column, cell_value, cell_style) @@ -264,26 +271,16 @@ class GroupExportXlsxWriter(ExportXlsxWriter): label = '%s%s (%s)' % (' ' * group_depth, label, group.count) self.write(row, column, label, self.header_bold_style) - if any(f.get('type') == 'monetary' for f in self.fields[1:]): - - decimal_places = [res['decimal_places'] for res in group._model.env['res.currency'].search_read([], ['decimal_places'])] - decimal_places = max(decimal_places) if decimal_places else 2 for field in self.fields[1:]: # No aggregates allowed in the first column because of the group title column += 1 aggregated_value = aggregates.get(field['name']) - # Float fields may not be displayed properly because of float - # representation issue with non stored fields or with values - # that, even stored, cannot be rounded properly and it is not - # acceptable to display useless digits (i.e. monetary) - # - # non stored field -> we force 2 digits - # stored monetary -> we force max digits of installed currencies - if isinstance(aggregated_value, float): - if field.get('type') == 'monetary': - aggregated_value = float_repr(aggregated_value, decimal_places) - elif not field.get('store'): - aggregated_value = float_repr(aggregated_value, 2) - self.write(row, column, str(aggregated_value if aggregated_value is not None else ''), self.header_bold_style) + if field.get('type') == 'monetary': + self.header_bold_style.set_num_format(self.monetary_format) + elif field.get('type') == 'float': + self.header_bold_style.set_num_format(self.float_format) + else: + aggregated_value = str(aggregated_value if aggregated_value is not None else '') + self.write(row, column, aggregated_value, self.header_bold_style) return row + 1, 0