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.

294 lines
10 KiB

8 years ago
8 years ago
8 years ago
8 years ago
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 XCG Consulting (http://odoo.consulting)
  3. # Copyright 2016 ACSONE SA/NV
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
  5. import base64
  6. from base64 import b64decode
  7. from cStringIO import StringIO
  8. import json
  9. import logging
  10. import os
  11. import pkg_resources
  12. import requests
  13. import sys
  14. from tempfile import NamedTemporaryFile
  15. import logging
  16. from zipfile import ZipFile, ZIP_DEFLATED
  17. from odoo.exceptions import UserError
  18. from openerp import api, fields, models, _
  19. from odoo.report.report_sxw import rml_parse
  20. logger = logging.getLogger(__name__)
  21. try:
  22. from py3o.template.helpers import Py3oConvertor
  23. from py3o.template import Template
  24. from py3o import formats
  25. except ImportError:
  26. logger.debug('Cannot import py3o.template')
  27. try:
  28. from py3o.formats import Formats
  29. except ImportError:
  30. logger.debug('Cannot import py3o.formats')
  31. _extender_functions = {}
  32. class TemplateNotFound(Exception):
  33. pass
  34. def py3o_report_extender(report_xml_id=None):
  35. """
  36. A decorator to define function to extend the context sent to a template.
  37. This will be called at the creation of the report.
  38. The following arguments will be passed to it:
  39. - ir_report: report instance
  40. - localcontext: The context that will be passed to the report engine
  41. If no report_xml_id is given the extender is registered for all py3o
  42. reports
  43. Idea copied from CampToCamp report_webkit module.
  44. :param report_xml_id: xml id of the report
  45. :return: a decorated class
  46. """
  47. global _extender_functions
  48. def fct1(fct):
  49. _extender_functions.setdefault(report_xml_id, []).append(fct)
  50. return fct
  51. return fct1
  52. @py3o_report_extender()
  53. def defautl_extend(report_xml, localcontext):
  54. # add the base64decode function to be able do decode binary fields into
  55. # the template
  56. localcontext['b64decode'] = b64decode
  57. localcontext['report_xml'] = report_xml
  58. class Py3oReport(models.TransientModel):
  59. _name = "py3o.report"
  60. _inherit = 'report'
  61. _description = "Report Py30"
  62. ir_actions_report_xml_id = fields.Many2one(
  63. comodel_name="ir.actions.report.xml",
  64. required=True
  65. )
  66. @api.multi
  67. def get_template(self):
  68. """private helper to fetch the template data either from the database
  69. or from the default template file provided by the implementer.
  70. ATM this method takes a report definition recordset
  71. to try and fetch the report template from database. If not found it
  72. will fallback to the template file referenced in the report definition.
  73. @returns: string or buffer containing the template data
  74. @raises: TemplateNotFound which is a subclass of
  75. odoo.exceptions.DeferredException
  76. """
  77. self.ensure_one()
  78. tmpl_data = None
  79. report_xml = self.ir_actions_report_xml_id
  80. if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
  81. # if a user gave a report template
  82. tmpl_data = b64decode(
  83. report_xml.py3o_template_id.py3o_template_data
  84. )
  85. elif report_xml.py3o_template_fallback:
  86. tmpl_name = report_xml.py3o_template_fallback
  87. flbk_filename = None
  88. if report_xml.module:
  89. # if the default is defined
  90. flbk_filename = pkg_resources.resource_filename(
  91. "odoo.addons.%s" % report_xml.module,
  92. tmpl_name,
  93. )
  94. elif os.path.isabs(tmpl_name):
  95. # It is an absolute path
  96. flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
  97. if flbk_filename and os.path.exists(flbk_filename):
  98. # and it exists on the fileystem
  99. with open(flbk_filename, 'r') as tmpl:
  100. tmpl_data = tmpl.read()
  101. if tmpl_data is None:
  102. # if for any reason the template is not found
  103. raise TemplateNotFound(
  104. _(u'No template found. Aborting.'),
  105. sys.exc_info(),
  106. )
  107. return tmpl_data
  108. @api.multi
  109. def _extend_parser_context(self, context_instance, report_xml):
  110. # add default extenders
  111. for fct in _extender_functions.get(None, []):
  112. fct(report_xml, context_instance.localcontext)
  113. # add extenders for registered on the template
  114. xml_id = report_xml.get_external_id().get(report_xml.id)
  115. if xml_id in _extender_functions:
  116. for fct in _extender_functions[xml_id]:
  117. fct(report_xml, context_instance.localcontext)
  118. @api.multi
  119. def _get_parser_context(self, model_instance, data):
  120. report_xml = self.ir_actions_report_xml_id
  121. context_instance = rml_parse(self.env.cr, self.env.uid,
  122. report_xml.name,
  123. context=self.env.context)
  124. context_instance.set_context(model_instance, data, model_instance.ids,
  125. report_xml.report_type)
  126. self._extend_parser_context(context_instance, report_xml)
  127. return context_instance.localcontext
  128. @api.multi
  129. def _postprocess_report(self, content, res_id, save_in_attachment):
  130. if save_in_attachment.get(res_id):
  131. attachment = {
  132. 'name': save_in_attachment.get(res_id),
  133. 'datas': base64.encodestring(content),
  134. 'datas_fname': save_in_attachment.get(res_id),
  135. 'res_model': save_in_attachment.get('model'),
  136. 'res_id': res_id,
  137. }
  138. return self.env['ir.attachment'].create(attachment)
  139. return False
  140. @api.multi
  141. def _create_single_report(self, model_instance, data, save_in_attachment):
  142. """ This function to generate our py3o report
  143. """
  144. self.ensure_one()
  145. report_xml = self.ir_actions_report_xml_id
  146. tmpl_data = self.get_template()
  147. in_stream = StringIO(tmpl_data)
  148. out_stream = StringIO()
  149. template = Template(in_stream, out_stream, escape_false=True)
  150. localcontext = self._get_parser_context(model_instance, data)
  151. if report_xml.py3o_is_local_fusion:
  152. template.render(localcontext)
  153. in_stream = out_stream
  154. datadict = {}
  155. else:
  156. expressions = template.get_all_user_python_expression()
  157. py_expression = template.convert_py3o_to_python_ast(expressions)
  158. convertor = Py3oConvertor()
  159. data_struct = convertor(py_expression)
  160. datadict = data_struct.render(localcontext)
  161. filetype = report_xml.py3o_filetype
  162. is_native = Formats().get_format(filetype).native
  163. if is_native:
  164. res = out_stream.getvalue()
  165. else: # Call py3o.server to render the template in the desired format
  166. in_stream.seek(0)
  167. files = {
  168. 'tmpl_file': in_stream,
  169. }
  170. fields = {
  171. "targetformat": filetype,
  172. "datadict": json.dumps(datadict),
  173. "image_mapping": "{}",
  174. }
  175. if report_xml.py3o_is_local_fusion:
  176. fields['skipfusion'] = '1'
  177. r = requests.post(
  178. report_xml.py3o_server_id.url, data=fields, files=files)
  179. if r.status_code != 200:
  180. # server says we have an issue... let's tell that to enduser
  181. raise UserError(
  182. _('Fusion server error %s') % r.text,
  183. )
  184. # Here is a little joke about Odoo
  185. # we do nice chunked reading from the network...
  186. chunk_size = 1024
  187. with NamedTemporaryFile(
  188. suffix=filetype,
  189. prefix='py3o-template-'
  190. ) as fd:
  191. for chunk in r.iter_content(chunk_size):
  192. fd.write(chunk)
  193. fd.seek(0)
  194. # ... but odoo wants the whole data in memory anyways :)
  195. res = fd.read()
  196. self._postprocess_report(
  197. res, model_instance.id, save_in_attachment)
  198. return res, "." + self.ir_actions_report_xml_id.py3o_filetype
  199. @api.multi
  200. def _get_or_create_single_report(self, model_instance, data,
  201. save_in_attachment):
  202. self.ensure_one()
  203. if save_in_attachment and save_in_attachment[
  204. 'loaded_documents'].get(model_instance.id):
  205. d = save_in_attachment[
  206. 'loaded_documents'].get(model_instance.id)
  207. return d, self.ir_actions_report_xml_id.py3o_filetype
  208. return self._create_single_report(
  209. model_instance, data, save_in_attachment)
  210. @api.multi
  211. def _zip_results(self, results):
  212. self.ensure_one()
  213. zfname_prefix = self.ir_actions_report_xml_id.name
  214. with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd:
  215. with ZipFile(fd, 'w', ZIP_DEFLATED) as zf:
  216. cpt = 0
  217. for r, ext in results:
  218. fname = "%s_%d.%s" % (zfname_prefix, cpt, ext)
  219. zf.writestr(fname, r)
  220. cpt += 1
  221. fd.seek(0)
  222. return fd.read(), 'zip'
  223. @api.multi
  224. def _merge_pdfs(self, results):
  225. from pyPdf import PdfFileWriter, PdfFileReader
  226. output = PdfFileWriter()
  227. for r in results:
  228. reader = PdfFileReader(StringIO(r[0]))
  229. for page in range(reader.getNumPages()):
  230. output.addPage(reader.getPage(page))
  231. s = StringIO()
  232. output.write(s)
  233. return s.getvalue(), formats.FORMAT_PDF
  234. @api.multi
  235. def _merge_results(self, results):
  236. self.ensure_one()
  237. if not results:
  238. return False, False
  239. if len(results) == 1:
  240. return results[0]
  241. filetype = self.ir_actions_report_xml_id.py3o_filetype
  242. if filetype == formats.FORMAT_PDF:
  243. return self._merge_pdfs(results)
  244. else:
  245. return self._zip_results(results)
  246. @api.multi
  247. def create_report(self, res_ids, data):
  248. """ Override this function to handle our py3o report
  249. """
  250. model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
  251. res_ids)
  252. save_in_attachment = self._check_attachment_use(
  253. model_instances, self.ir_actions_report_xml_id) or {}
  254. results = []
  255. for model_instance in model_instances:
  256. results.append(self._get_or_create_single_report(
  257. model_instance, data, save_in_attachment))
  258. return self._merge_results(results)