OCA reporting engine fork for dev and update.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
4.7 KiB

  1. # Copyright 2015 ACSONE SA/NV (<http://acsone.eu>)
  2. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  3. import logging
  4. import re
  5. from io import BytesIO
  6. from odoo import models
  7. _logger = logging.getLogger(__name__)
  8. try:
  9. import xlsxwriter
  10. class PatchedXlsxWorkbook(xlsxwriter.Workbook):
  11. def _check_sheetname(self, sheetname, is_chartsheet=False):
  12. """We want to avoid duplicated sheet names exceptions the same following
  13. the same philosophy that Odoo implements overriding the main library
  14. to avoid the 31 characters limit triming the strings before sending them
  15. to the library.
  16. In some cases, there's not much control over this as the reports send
  17. automated data and the potential exception is hidden underneath making it
  18. hard to debug the original issue. Even so, different names can become the
  19. same one as their strings are trimmed to those 31 character limit.
  20. This way, once we come across with a duplicated, we set that final 3
  21. characters with a sequence that we evaluate on the fly. So for instance:
  22. - 'Sheet name' will be 'Sheet name~01'
  23. - The next 'Sheet name' will try to rename to 'Sheet name~01' as well and
  24. then that will give us 'Sheet name~02'.
  25. - And the next 'Sheet name' will try to rename to 'Sheet name~01' and then
  26. to 'Sheet name~02' and finally it will be able to 'Sheet name~03'.
  27. - An so on as many times as duplicated sheet names come to the workbook up
  28. to 100 for each sheet name. We set such limit as we don't want to truncate
  29. the strings too much and keeping in mind that this issue don't usually
  30. ocurrs.
  31. """
  32. try:
  33. return super()._check_sheetname(sheetname, is_chartsheet=is_chartsheet)
  34. except xlsxwriter.exceptions.DuplicateWorksheetName:
  35. pattern = re.compile(r"~[0-9]{2}$")
  36. duplicated_secuence = (
  37. re.search(pattern, sheetname) and int(sheetname[-2:]) or 0
  38. )
  39. # Only up to 100 duplicates
  40. deduplicated_secuence = "~{:02d}".format(duplicated_secuence + 1)
  41. if duplicated_secuence > 99:
  42. raise xlsxwriter.exceptions.DuplicateWorksheetName
  43. if duplicated_secuence:
  44. sheetname = re.sub(pattern, deduplicated_secuence, sheetname)
  45. elif len(sheetname) <= 28:
  46. sheetname += deduplicated_secuence
  47. else:
  48. sheetname = sheetname[:28] + deduplicated_secuence
  49. # Refeed the method until we get an unduplicated name
  50. return self._check_sheetname(sheetname, is_chartsheet=is_chartsheet)
  51. # "Short string"
  52. xlsxwriter.Workbook = PatchedXlsxWorkbook
  53. except ImportError:
  54. _logger.debug("Can not import xlsxwriter`.")
  55. class ReportXlsxAbstract(models.AbstractModel):
  56. _name = "report.report_xlsx.abstract"
  57. _description = "Abstract XLSX Report"
  58. def _get_objs_for_report(self, docids, data):
  59. """
  60. Returns objects for xlx report. From WebUI these
  61. are either as docids taken from context.active_ids or
  62. in the case of wizard are in data. Manual calls may rely
  63. on regular context, setting docids, or setting data.
  64. :param docids: list of integers, typically provided by
  65. qwebactionmanager for regular Models.
  66. :param data: dictionary of data, if present typically provided
  67. by qwebactionmanager for TransientModels.
  68. :param ids: list of integers, provided by overrides.
  69. :return: recordset of active model for ids.
  70. """
  71. if docids:
  72. ids = docids
  73. elif data and "context" in data:
  74. ids = data["context"].get("active_ids", [])
  75. else:
  76. ids = self.env.context.get("active_ids", [])
  77. return self.env[self.env.context.get("active_model")].browse(ids)
  78. def create_xlsx_report(self, docids, data):
  79. objs = self._get_objs_for_report(docids, data)
  80. file_data = BytesIO()
  81. workbook = xlsxwriter.Workbook(file_data, self.get_workbook_options())
  82. self.generate_xlsx_report(workbook, data, objs)
  83. workbook.close()
  84. file_data.seek(0)
  85. return file_data.read(), "xlsx"
  86. def get_workbook_options(self):
  87. """
  88. See https://xlsxwriter.readthedocs.io/workbook.html constructor options
  89. :return: A dictionary of options
  90. """
  91. return {}
  92. def generate_xlsx_report(self, workbook, data, objs):
  93. raise NotImplementedError()