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.

273 lines
12 KiB

Excel Import/Export/Report (#1522) * [ADD] v12 excel_import_export * Change from eval() to safe_evel() * Change variable to format to style, as fomat is a common python function :100644 100644 00ee3d9f... e9e48d87... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 01e5b9f5... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3... 17d3964d... M excel_import_export/__manifest__.py :100644 100644 00ee3d9f... 51c2572a... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 185a3330... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3... 933ce0dc... M excel_import_export/__manifest__.py :100644 100644 00ee3d9f... 51c2572a... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 185a3330... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 3b1217e8 M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 5b4d1fb1 M excel_import_export/models/styles.py :100644 100644 ace11a32 185a3330 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5 80490ce8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 5b4d1fb1 M excel_import_export/models/styles.py :100644 100644 ace11a32 185a3330 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5 80490ce8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 a7d6adc5 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 578a1fd8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 578a1fd8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 e3826e08 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 c8481487 A excel_import_export/tests/test_xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 ed8c9fc7 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 69aa6ea0 A excel_import_export/tests/test_xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 933d8614 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 1460473a M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 a2d035ef 9463f279 M excel_import_export_demo/__manifest__.py :100644 100644 475b5187 e7f1255b M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :000000 100644 00000000 79db62f7 A excel_import_export_demo/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export_demo/tests/sale_order.xlsx :000000 100644 00000000 c9733b95 A excel_import_export_demo/tests/test_common.py :000000 100644 00000000 9c943768 A excel_import_export_demo/tests/test_xlsx_import_export.py :000000 100644 00000000 730605c1 A excel_import_export_demo/tests/test_xlsx_template.py :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 933d8614 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 1460473a M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 a2d035ef 9463f279 M excel_import_export_demo/__manifest__.py :100644 100644 475b5187 e7f1255b M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :000000 100644 00000000 79db62f7 A excel_import_export_demo/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export_demo/tests/sale_order.xlsx :000000 100644 00000000 bb3ea32e A excel_import_export_demo/tests/test_common.py :000000 100644 00000000 9c943768 A excel_import_export_demo/tests/test_xlsx_import_export.py :000000 100644 00000000 730605c1 A excel_import_export_demo/tests/test_xlsx_template.py
6 years ago
  1. # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
  2. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
  3. import os
  4. import logging
  5. import base64
  6. from io import BytesIO
  7. import time
  8. from datetime import date, datetime as dt
  9. from odoo.tools.float_utils import float_compare
  10. from odoo import models, fields, api, _
  11. from odoo.tools.safe_eval import safe_eval
  12. from odoo.exceptions import ValidationError
  13. from . import common as co
  14. _logger = logging.getLogger(__name__)
  15. try:
  16. from openpyxl import load_workbook
  17. from openpyxl.utils.exceptions import IllegalCharacterError
  18. except ImportError:
  19. _logger.debug(
  20. 'Cannot import "openpyxl". Please make sure it is installed.')
  21. class XLSXExport(models.AbstractModel):
  22. _name = 'xlsx.export'
  23. _description = 'Excel Export AbstractModel'
  24. @api.model
  25. def get_eval_context(self, model, record, value):
  26. eval_context = {'float_compare': float_compare,
  27. 'time': time,
  28. 'datetime': dt,
  29. 'date': date,
  30. 'value': value,
  31. 'object': record,
  32. 'model': self.env[model],
  33. 'env': self.env,
  34. 'context': self._context,
  35. }
  36. return eval_context
  37. @api.model
  38. def _get_line_vals(self, record, line_field, fields):
  39. """ Get values of this field from record set and return as dict of vals
  40. - record: main object
  41. - line_field: rows object, i.e., line_ids
  42. - fields: fields in line_ids, i.e., partner_id.display_name
  43. """
  44. line_field, max_row = co.get_line_max(line_field)
  45. line_field = line_field.replace('_CONT_', '') # Remove _CONT_ if any
  46. lines = record[line_field]
  47. if max_row > 0 and len(lines) > max_row:
  48. raise Exception(
  49. _('Records in %s exceed max records allowed') % line_field)
  50. vals = dict([(field, []) for field in fields]) # value and do_style
  51. # Get field condition & aggre function
  52. field_cond_dict = {}
  53. aggre_func_dict = {}
  54. field_style_dict = {}
  55. style_cond_dict = {}
  56. pair_fields = [] # I.e., ('debit${value and . or .}@{sum}', 'debit')
  57. for field in fields:
  58. temp_field, eval_cond = co.get_field_condition(field)
  59. eval_cond = eval_cond or 'value or ""'
  60. temp_field, field_style = co.get_field_style(temp_field)
  61. temp_field, style_cond = co.get_field_style_cond(temp_field)
  62. raw_field, aggre_func = co.get_field_aggregation(temp_field)
  63. # Dict of all special conditions
  64. field_cond_dict.update({field: eval_cond})
  65. aggre_func_dict.update({field: aggre_func})
  66. field_style_dict.update({field: field_style})
  67. style_cond_dict.update({field: style_cond})
  68. # --
  69. pair_fields.append((field, raw_field))
  70. for line in lines:
  71. for field in pair_fields: # (field, raw_field)
  72. value = self._get_field_data(field[1], line)
  73. eval_cond = field_cond_dict[field[0]]
  74. eval_context = \
  75. self.get_eval_context(line._name, line, value)
  76. if eval_cond:
  77. value = safe_eval(eval_cond, eval_context)
  78. # style w/Cond takes priority
  79. style_cond = style_cond_dict[field[0]]
  80. style = self._eval_style_cond(line._name, line,
  81. value, style_cond)
  82. if style is None:
  83. style = False # No style
  84. elif style is False:
  85. style = field_style_dict[field[0]] # Use default style
  86. vals[field[0]].append((value, style))
  87. return (vals, aggre_func_dict,)
  88. @api.model
  89. def _eval_style_cond(self, model, record, value, style_cond):
  90. eval_context = self.get_eval_context(model, record, value)
  91. field = style_cond = style_cond or '#??'
  92. styles = {}
  93. for i in range(style_cond.count('#{')):
  94. i += 1
  95. field, style = co.get_field_style(field)
  96. styles.update({i: style})
  97. style_cond = style_cond.replace('#{%s}' % style, str(i))
  98. if not styles:
  99. return False
  100. res = safe_eval(style_cond, eval_context)
  101. if res is None or res is False:
  102. return res
  103. return styles[res]
  104. @api.model
  105. def _fill_workbook_data(self, workbook, record, data_dict):
  106. """ Fill data from record with style in data_dict to workbook """
  107. if not record or not data_dict:
  108. return
  109. try:
  110. for sheet_name in data_dict:
  111. ws = data_dict[sheet_name]
  112. st = False
  113. if isinstance(sheet_name, str):
  114. st = co.openpyxl_get_sheet_by_name(workbook, sheet_name)
  115. elif isinstance(sheet_name, int):
  116. if sheet_name > len(workbook.worksheets):
  117. raise Exception(_('Not enough worksheets'))
  118. st = workbook.worksheets[sheet_name - 1]
  119. if not st:
  120. raise ValidationError(
  121. _('Sheet %s not found') % sheet_name)
  122. # Fill data, header and rows
  123. self._fill_head(ws, st, record)
  124. self._fill_lines(ws, st, record)
  125. except KeyError as e:
  126. raise ValidationError(_('Key Error\n%s') % e)
  127. except IllegalCharacterError as e:
  128. raise ValidationError(
  129. _('IllegalCharacterError\n'
  130. 'Some exporting data contain special character\n%s') % e)
  131. except Exception as e:
  132. raise ValidationError(
  133. _('Error filling data into Excel sheets\n%s') % e)
  134. @api.model
  135. def _get_field_data(self, _field, _line):
  136. """ Get field data, and convert data type if needed """
  137. if not _field:
  138. return None
  139. line_copy = _line
  140. for f in _field.split('.'):
  141. line_copy = line_copy[f]
  142. if isinstance(line_copy, str):
  143. line_copy = line_copy.encode('utf-8')
  144. return line_copy
  145. @api.model
  146. def _fill_head(self, ws, st, record):
  147. for rc, field in ws.get('_HEAD_', {}).items():
  148. tmp_field, eval_cond = co.get_field_condition(field)
  149. eval_cond = eval_cond or 'value or ""'
  150. tmp_field, field_style = co.get_field_style(tmp_field)
  151. tmp_field, style_cond = co.get_field_style_cond(tmp_field)
  152. value = tmp_field and self._get_field_data(tmp_field, record)
  153. # Eval
  154. eval_context = self.get_eval_context(record._name,
  155. record, value)
  156. if eval_cond:
  157. value = safe_eval(eval_cond, eval_context)
  158. if value is not None:
  159. st[rc] = value
  160. fc = not style_cond and True or \
  161. safe_eval(style_cond, eval_context)
  162. if field_style and fc: # has style and pass style_cond
  163. styles = self.env['xlsx.styles'].get_openpyxl_styles()
  164. co.fill_cell_style(st[rc], field_style, styles)
  165. @api.model
  166. def _fill_lines(self, ws, st, record):
  167. line_fields = list(ws)
  168. if '_HEAD_' in line_fields:
  169. line_fields.remove('_HEAD_')
  170. cont_row = 0 # last data row to continue
  171. for line_field in line_fields:
  172. fields = ws.get(line_field, {}).values()
  173. vals, func = self._get_line_vals(record, line_field, fields)
  174. is_cont = '_CONT_' in line_field and True or False # continue row
  175. cont_set = 0
  176. rows_inserted = False # flag to insert row
  177. for rc, field in ws.get(line_field, {}).items():
  178. col, row = co.split_row_col(rc) # starting point
  179. # Case continue, start from the last data row
  180. if is_cont and not cont_set: # only once per line_field
  181. cont_set = cont_row + 1
  182. if is_cont:
  183. row = cont_set
  184. rc = '%s%s' % (col, cont_set)
  185. i = 0
  186. new_row = 0
  187. new_rc = False
  188. row_count = len(vals[field])
  189. # Insert rows to preserve total line
  190. if not rows_inserted:
  191. rows_inserted = True
  192. if row_count > 1:
  193. for _x in range(row_count-1):
  194. st.insert_rows(row+1)
  195. # --
  196. for (row_val, style) in vals[field]:
  197. new_row = row + i
  198. new_rc = '%s%s' % (col, new_row)
  199. row_val = co.adjust_cell_formula(row_val, i)
  200. if row_val not in ('None', None):
  201. st[new_rc] = co.str_to_number(row_val)
  202. if style:
  203. styles = self.env['xlsx.styles'].get_openpyxl_styles()
  204. co.fill_cell_style(st[new_rc], style, styles)
  205. i += 1
  206. # Add footer line if at least one field have sum
  207. f = func.get(field, False)
  208. if f and new_row > 0:
  209. new_row += 1
  210. f_rc = '%s%s' % (col, new_row)
  211. st[f_rc] = '=%s(%s:%s)' % (f, rc, new_rc)
  212. cont_row = cont_row < new_row and new_row or cont_row
  213. return
  214. @api.model
  215. def export_xlsx(self, template, res_model, res_id):
  216. if template.res_model != res_model:
  217. raise ValidationError(_("Template's model mismatch"))
  218. data_dict = co.literal_eval(template.instruction.strip())
  219. export_dict = data_dict.get('__EXPORT__', False)
  220. out_name = template.name
  221. if not export_dict: # If there is not __EXPORT__ formula, just export
  222. out_name = template.fname
  223. out_file = template.datas
  224. return (out_file, out_name)
  225. # Prepare temp file (from now, only xlsx file works for openpyxl)
  226. decoded_data = base64.decodestring(template.datas)
  227. ConfParam = self.env['ir.config_parameter']
  228. ptemp = ConfParam.get_param('path_temp_file') or '/tmp'
  229. stamp = dt.utcnow().strftime('%H%M%S%f')[:-3]
  230. ftemp = '%s/temp%s.xlsx' % (ptemp, stamp)
  231. f = open(ftemp, 'wb')
  232. f.write(decoded_data)
  233. f.seek(0)
  234. f.close()
  235. # Workbook created, temp fie removed
  236. wb = load_workbook(ftemp)
  237. os.remove(ftemp)
  238. # Start working with workbook
  239. record = res_model and self.env[res_model].browse(res_id) or False
  240. self._fill_workbook_data(wb, record, export_dict)
  241. # Return file as .xlsx
  242. content = BytesIO()
  243. wb.save(content)
  244. content.seek(0) # Set index to 0, and start reading
  245. out_file = base64.encodestring(content.read())
  246. if record and 'name' in record and record.name:
  247. out_name = record.name.replace(' ', '').replace('/', '')
  248. else:
  249. fname = out_name.replace(' ', '').replace('/', '')
  250. ts = fields.Datetime.context_timestamp(self, dt.now())
  251. out_name = '%s_%s' % (fname, ts.strftime('%Y%m%d_%H%M%S'))
  252. if not out_name or len(out_name) == 0:
  253. out_name = 'noname'
  254. out_ext = 'xlsx'
  255. # CSV (convert only on 1st sheet)
  256. if template.to_csv:
  257. delimiter = template.csv_delimiter
  258. out_file = co.csv_from_excel(out_file, delimiter,
  259. template.csv_quote)
  260. out_ext = template.csv_extension
  261. return (out_file, '%s.%s' % (out_name, out_ext))