diff --git a/report_xlsx/report/report_abstract_xlsx.py b/report_xlsx/report/report_abstract_xlsx.py index 244d88dc..9190dd5b 100644 --- a/report_xlsx/report/report_abstract_xlsx.py +++ b/report_xlsx/report/report_abstract_xlsx.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging +import re from io import BytesIO from odoo import models @@ -10,6 +11,56 @@ _logger = logging.getLogger(__name__) try: import xlsxwriter + + class PatchedXlsxWorkbook(xlsxwriter.Workbook): + def _check_sheetname(self, sheetname, is_chartsheet=False): + """We want to avoid duplicated sheet names exceptions the same following + the same philosophy that Odoo implements overriding the main library + to avoid the 31 characters limit triming the strings before sending them + to the library. + + In some cases, there's not much control over this as the reports send + automated data and the potential exception is hidden underneath making it + hard to debug the original issue. Even so, different names can become the + same one as their strings are trimmed to those 31 character limit. + + This way, once we come across with a duplicated, we set that final 3 + characters with a sequence that we evaluate on the fly. So for instance: + + - 'Sheet name' will be 'Sheet name~01' + - The next 'Sheet name' will try to rename to 'Sheet name~01' as well and + then that will give us 'Sheet name~02'. + - And the next 'Sheet name' will try to rename to 'Sheet name~01' and then + to 'Sheet name~02' and finally it will be able to 'Sheet name~03'. + - An so on as many times as duplicated sheet names come to the workbook up + to 100 for each sheet name. We set such limit as we don't want to truncate + the strings too much and keeping in mind that this issue don't usually + ocurrs. + """ + try: + return super()._check_sheetname(sheetname, is_chartsheet=is_chartsheet) + except xlsxwriter.exceptions.DuplicateWorksheetName: + pattern = re.compile(r"~[0-9]{2}$") + duplicated_secuence = ( + re.search(pattern, sheetname) and int(sheetname[-2:]) or 0 + ) + # Only up to 100 duplicates + deduplicated_secuence = "~{:02d}".format(duplicated_secuence + 1) + if duplicated_secuence > 99: + raise xlsxwriter.exceptions.DuplicateWorksheetName + if duplicated_secuence: + sheetname = re.sub(pattern, deduplicated_secuence, sheetname) + elif len(sheetname) <= 28: + sheetname += deduplicated_secuence + else: + sheetname = sheetname[:28] + deduplicated_secuence + # Refeed the method until we get an unduplicated name + return self._check_sheetname(sheetname, is_chartsheet=is_chartsheet) + + # "Short string" + + xlsxwriter.Workbook = PatchedXlsxWorkbook + except ImportError: _logger.debug("Can not import xlsxwriter`.")