[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 <juc@odoo.com> Signed-off-by: Guillaume Merlin (megu) <megu@odoo.com>
This commit is contained in:
@@ -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'],
|
||||
])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user