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.

363 lines
16 KiB

6 years ago
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2009-2018 Noviat.
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. from datetime import datetime
  5. import re
  6. from types import CodeType
  7. from xlsxwriter.utility import xl_rowcol_to_cell
  8. from odoo import api, fields, _
  9. from odoo.addons.report_xlsx.report.report_xlsx import ReportXlsx
  10. from odoo.exceptions import UserError
  11. class AbstractReportXlsx(ReportXlsx):
  12. def create(self, cr, uid, ids, data, context=None):
  13. if context.get('xlsx_export'):
  14. self.env = api.Environment(cr, uid, context)
  15. return self.create_xlsx_report(ids, data, None)
  16. else:
  17. return super(AbstractReportXlsx, self).create(
  18. cr, uid, ids, data, context=context)
  19. def generate_xlsx_report(self, workbook, data, objects):
  20. self._define_formats(workbook)
  21. for ws_params in self._get_ws_params(workbook, data, objects):
  22. ws_name = ws_params.get('ws_name')
  23. ws_name = self._check_ws_name(ws_name)
  24. ws = workbook.add_worksheet(ws_name)
  25. generate_ws_method = getattr(
  26. self, ws_params['generate_ws_method'])
  27. generate_ws_method(workbook, ws, ws_params, data, objects)
  28. def _check_ws_name(self, name, sanitize=True):
  29. pattern = re.compile(r'[/\\*\[\]:?]') # invalid characters: /\*[]:?
  30. max_chars = 31
  31. if sanitize:
  32. # we could drop these two lines since a similar
  33. # sanitize is done in tools.misc PatchedXlsxWorkbook
  34. name = pattern.sub('', name)
  35. name = name[:max_chars]
  36. else:
  37. if len(name) > max_chars:
  38. raise UserError(_(
  39. "Programming Error."
  40. "\nExcel Sheet name '%s' should not exceed %s characters."
  41. ) % (name, max_chars))
  42. special_chars = pattern.findall(name)
  43. if special_chars:
  44. raise UserError(_(
  45. "Programming Error."
  46. "\nExcel Sheet name '%s' contains unsupported special "
  47. "characters: '%s'."
  48. ) % (name, special_chars))
  49. return name
  50. def _get_ws_params(self, workbook, data, objects):
  51. """
  52. Return list of dictionaries with parameters for the
  53. worksheets.
  54. Keywords:
  55. - 'generate_ws_method': mandatory
  56. - 'ws_name': name of the worksheet
  57. - 'title': title of the worksheet
  58. - 'wanted_list': list of column names
  59. - 'col_specs': cf. XXX
  60. The 'generate_ws_method' must be present in your report
  61. and contain the logic to generate the content of the worksheet.
  62. """
  63. return []
  64. def _define_formats(self, workbook):
  65. """
  66. This section contains a number of pre-defined formats.
  67. It is recommended to use these in order to have a
  68. consistent look & feel between your XLSX reports.
  69. """
  70. # predefined worksheet headers/footers
  71. hf_params = {
  72. 'font_size': 8,
  73. 'font_style': 'I', # B: Bold, I: Italic, U: Underline
  74. }
  75. self.xls_headers = {
  76. 'standard': ''
  77. }
  78. report_date = fields.Datetime.context_timestamp(
  79. self.env.user, datetime.now()).strftime('%Y-%m-%d %H:%M')
  80. self.xls_footers = {
  81. 'standard': (
  82. '&L&%(font_size)s&%(font_style)s' + report_date +
  83. '&R&%(font_size)s&%(font_style)s&P / &N'
  84. ) % hf_params,
  85. }
  86. border_grey = '#D3D3D3'
  87. border = {'border': True, 'border_color': border_grey}
  88. theader = dict(border, bold=True)
  89. bg_yellow = '#FFFFCC'
  90. bg_blue = '#CCFFFF'
  91. num_format = '#,##0.00'
  92. num_format_conditional = '{0};[Red]-{0};{0}'.format(num_format)
  93. pct_format = '#,##0.00%'
  94. pct_format_conditional = '{0};[Red]-{0};{0}'.format(pct_format)
  95. int_format = '#,##0'
  96. int_format_conditional = '{0};[Red]-{0};{0}'.format(int_format)
  97. date_format = 'YYYY-MM-DD'
  98. theader_yellow = dict(theader, bg_color=bg_yellow)
  99. theader_blue = dict(theader, bg_color=bg_blue)
  100. # format for worksheet title
  101. self.format_ws_title = workbook.add_format(
  102. {'bold': True, 'font_size': 14})
  103. # no border formats
  104. self.format_left = workbook.add_format({'align': 'left'})
  105. self.format_center = workbook.add_format({'align': 'center'})
  106. self.format_right = workbook.add_format({'align': 'right'})
  107. self.format_amount = workbook.add_format(
  108. {'align': 'right', 'num_format': num_format})
  109. self.format_amount_conditional = workbook.add_format(
  110. {'align': 'right', 'num_format': num_format_conditional})
  111. self.format_percent = workbook.add_format(
  112. {'align': 'right', 'num_format': pct_format})
  113. self.format_percent_conditional = workbook.add_format(
  114. {'align': 'right', 'num_format': pct_format_conditional})
  115. self.format_integer = workbook.add_format(
  116. {'align': 'right', 'num_format': int_format})
  117. self.format_integer_conditional = workbook.add_format(
  118. {'align': 'right', 'num_format': int_format_conditional})
  119. self.format_date = workbook.add_format(
  120. {'align': 'left', 'num_format': date_format})
  121. self.format_left_bold = workbook.add_format(
  122. {'align': 'left', 'bold': True})
  123. self.format_center_bold = workbook.add_format(
  124. {'align': 'center', 'bold': True})
  125. self.format_right_bold = workbook.add_format(
  126. {'align': 'right', 'bold': True})
  127. self.format_amount_bold = workbook.add_format(
  128. {'align': 'right', 'bold': True, 'num_format': num_format})
  129. self.format_amount_bold_conditional = workbook.add_format(
  130. {'align': 'right', 'bold': True,
  131. 'num_format': num_format_conditional})
  132. self.format_percent_bold = workbook.add_format(
  133. {'align': 'right', 'bold': True, 'num_format': pct_format})
  134. self.format_percent_bold_conditional = workbook.add_format(
  135. {'align': 'right', 'bold': True,
  136. 'num_format': pct_format_conditional})
  137. self.format_integer_bold = workbook.add_format(
  138. {'align': 'right', 'bold': True, 'num_format': int_format})
  139. self.format_integer_bold_conditional = workbook.add_format(
  140. {'align': 'right', 'bold': True,
  141. 'num_format': int_format_conditional})
  142. self.format_date_bold = workbook.add_format(
  143. {'align': 'left', 'bold': True, 'num_format': date_format})
  144. # formats for worksheet table column headers
  145. self.format_theader_yellow = workbook.add_format(theader_yellow)
  146. self.format_theader_yellow_center = workbook.add_format(
  147. dict(theader_yellow, align='center'))
  148. self.format_theader_yellow_right = workbook.add_format(
  149. dict(theader_yellow, align='right'))
  150. self.format_theader_yellow_amount = workbook.add_format(
  151. dict(theader_yellow, num_format=num_format))
  152. self.format_theader_yellow_amount_conditional = workbook.add_format(
  153. dict(theader_yellow, num_format=num_format_conditional))
  154. self.format_theader_yellow_percent = workbook.add_format(
  155. dict(theader_yellow, num_format=pct_format))
  156. self.format_theader_yellow_percent_conditional = workbook.add_format(
  157. dict(theader_yellow, num_format=pct_format_conditional))
  158. self.format_theader_yellow_integer = workbook.add_format(
  159. dict(theader_yellow, num_format=int_format))
  160. self.format_theader_yellow_integer_conditional = workbook.add_format(
  161. dict(theader_yellow, num_format=int_format_conditional))
  162. self.format_theader_blue = workbook.add_format(theader_blue)
  163. self.format_theader_blue_center = workbook.add_format(
  164. dict(theader_blue, align='center'))
  165. self.format_theader_blue_right = workbook.add_format(
  166. dict(theader_blue, align='right'))
  167. self.format_theader_blue_amount = workbook.add_format(
  168. dict(theader_blue, num_format=num_format))
  169. self.format_theader_blue_amount_conditional = workbook.add_format(
  170. dict(theader_blue, num_format=num_format_conditional))
  171. self.format_theader_blue_percent = workbook.add_format(
  172. dict(theader_blue, num_format=pct_format))
  173. self.format_theader_blue_percent_conditional = workbook.add_format(
  174. dict(theader_blue, num_format=pct_format_conditional))
  175. self.format_theader_blue_integer = workbook.add_format(
  176. dict(theader_blue, num_format=int_format))
  177. self.format_theader_blue_integer_conditional = workbook.add_format(
  178. dict(theader_blue, num_format=int_format_conditional))
  179. # formats for worksheet table cells
  180. self.format_tleft = workbook.add_format(
  181. dict(border, align='left'))
  182. self.format_tcenter = workbook.add_format(
  183. dict(border, align='center'))
  184. self.format_tright = workbook.add_format(
  185. dict(border, align='right'))
  186. self.format_tamount = workbook.add_format(
  187. dict(border, num_format=num_format))
  188. self.format_tamount_conditional = workbook.add_format(
  189. dict(border, num_format=num_format_conditional))
  190. self.format_tpercent = workbook.add_format(
  191. dict(border, num_format=pct_format))
  192. self.format_tpercent_conditional = workbook.add_format(
  193. dict(border, num_format=pct_format_conditional))
  194. self.format_tinteger = workbook.add_format(
  195. dict(border, num_format=int_format))
  196. self.format_tinteger_conditional = workbook.add_format(
  197. dict(border, num_format=int_format_conditional))
  198. self.format_tdate = workbook.add_format(
  199. dict(border, align='left', num_format=date_format))
  200. self.format_tleft_bold = workbook.add_format(
  201. dict(border, align='left', bold=True))
  202. self.format_tcenter_bold = workbook.add_format(
  203. dict(border, align='center', bold=True))
  204. self.format_tright_bold = workbook.add_format(
  205. dict(border, align='right', bold=True))
  206. self.format_tamount_bold = workbook.add_format(
  207. dict(border, bold=True, num_format=num_format))
  208. self.format_tamount_bold_conditional = workbook.add_format(
  209. dict(border, bold=True, num_format=num_format_conditional))
  210. self.format_tpercent_bold = workbook.add_format(
  211. dict(border, bold=True, num_format=pct_format))
  212. self.format_tpercent_bold_conditional = workbook.add_format(
  213. dict(border, bold=True, num_format=pct_format_conditional))
  214. self.format_tinteger_bold = workbook.add_format(
  215. dict(border, bold=True, num_format=int_format))
  216. self.format_tinteger_bold_conditional = workbook.add_format(
  217. dict(border, bold=True, num_format=int_format_conditional))
  218. self.format_tdate_bold = workbook.add_format(
  219. dict(border, align='left', bold=True, num_format=date_format))
  220. def _set_column_width(self, ws, ws_params):
  221. """
  222. Set width for all columns included in the 'wanted_list'.
  223. """
  224. col_specs = ws_params.get('col_specs')
  225. wl = ws_params.get('wanted_list') or []
  226. for pos, col in enumerate(wl):
  227. if col not in col_specs:
  228. raise UserError(_(
  229. "%s - Programming Error: "
  230. "the '%' column is not defined the worksheet "
  231. "column specifications.")
  232. % (__name__, col))
  233. ws.set_column(pos, pos, col_specs[col]['width'])
  234. def _write_ws_title(self, ws, row_pos, ws_params, merge_range=False):
  235. """
  236. Helper function to ensure consistent title formats
  237. troughout all worksheets.
  238. Requires 'title' keyword in ws_params.
  239. """
  240. title = ws_params.get('title')
  241. if not title:
  242. raise UserError(_(
  243. "%s - Programming Error: "
  244. "the 'title' parameter is mandatory "
  245. "when calling the '_write_ws_title' method.")
  246. % __name__)
  247. if merge_range:
  248. wl = ws_params.get('wanted_list')
  249. if wl and len(wl) > 1:
  250. ws.merge_range(
  251. row_pos, 0, row_pos, len(wl) - 1,
  252. title, self.format_ws_title)
  253. else:
  254. ws.write_string(row_pos, 0, title, self.format_ws_title)
  255. return row_pos + 2
  256. def _write_line(self, ws, row_pos, ws_params, col_specs_section=None,
  257. render_space=None, default_format=None):
  258. """
  259. Write a line with all columns included in the 'wanted_list'.
  260. Use the entry defined by the col_specs_section.
  261. An empty cell will be written if no col_specs_section entry
  262. for a column.
  263. """
  264. col_specs = ws_params.get('col_specs')
  265. wl = ws_params.get('wanted_list') or []
  266. pos = 0
  267. for col in wl:
  268. if col not in col_specs:
  269. raise UserError(_(
  270. "%s - Programming Error: "
  271. "the '%' column is not defined the worksheet "
  272. "column specifications.")
  273. % (__name__, col))
  274. colspan = col_specs[col].get('colspan') or 1
  275. cell_spec = col_specs[col].get(col_specs_section) or {}
  276. if not cell_spec:
  277. cell_value = None
  278. cell_type = 'blank'
  279. cell_format = default_format
  280. else:
  281. cell_value = cell_spec.get('value')
  282. if isinstance(cell_value, CodeType):
  283. cell_value = self._eval(cell_value, render_space)
  284. cell_type = cell_spec.get('type')
  285. cell_format = cell_spec.get('format') or default_format
  286. if not cell_type:
  287. if isinstance(cell_value, basestring):
  288. cell_type = 'string'
  289. elif isinstance(cell_value, (int, float)):
  290. cell_type = 'number'
  291. elif isinstance(cell_value, bool):
  292. cell_type = 'boolean'
  293. elif isinstance(cell_value, datetime):
  294. cell_type = 'datetime'
  295. else:
  296. if not cell_value:
  297. cell_type = 'blank'
  298. else:
  299. msg = _(
  300. "%s, _write_line : programming error "
  301. "detected while processing "
  302. "col_specs_section %s, column %s"
  303. ) % (__name__, col_specs_section, col)
  304. if cell_value:
  305. msg += _(", cellvalue %s")
  306. raise UserError(msg)
  307. colspan = cell_spec.get('colspan') or colspan
  308. args_pos = [row_pos, pos]
  309. args_data = [cell_value]
  310. if cell_format:
  311. args_data.append(cell_format)
  312. if colspan > 1:
  313. args_pos += [row_pos, pos + colspan - 1]
  314. args = args_pos + args_data
  315. ws.merge_range(*args)
  316. else:
  317. ws_method = getattr(ws, 'write_%s' % cell_type)
  318. args = args_pos + args_data
  319. ws_method(*args)
  320. pos += colspan
  321. return row_pos + 1
  322. @staticmethod
  323. def _render(code):
  324. return compile(code, '<string>', 'eval')
  325. @staticmethod
  326. def _eval(val, render_space):
  327. if not render_space:
  328. render_space = {}
  329. if 'datetime' not in render_space:
  330. render_space['datetime'] = datetime
  331. # the use of eval is not a security thread as long as the
  332. # col_specs template is defined in a python module
  333. return eval(val, render_space) # pylint: disable=W0123
  334. @staticmethod
  335. def _rowcol_to_cell(row, col, row_abs=False, col_abs=False):
  336. return xl_rowcol_to_cell(row, col, row_abs=row_abs, col_abs=col_abs)