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.

217 lines
7.7 KiB

8 years ago
8 years ago
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 XCG Consulting (http://odoo.consulting)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
  4. from cStringIO import StringIO
  5. import json
  6. import pkg_resources
  7. import os
  8. import sys
  9. from base64 import b64decode
  10. import requests
  11. from tempfile import NamedTemporaryFile
  12. from py3o.template.helpers import Py3oConvertor
  13. from py3o.template import Template
  14. from py3o.formats import Formats
  15. from openerp import _
  16. from openerp import exceptions
  17. from openerp.report.report_sxw import report_sxw
  18. from openerp import registry
  19. _extender_functions = {}
  20. class TemplateNotFound(Exception):
  21. pass
  22. def py3o_report_extender(report_xml_id=None):
  23. """
  24. A decorator to define function to extend the context sent to a template.
  25. This will be called at the creation of the report.
  26. The following arguments will be passed to it:
  27. - ir_report: report instance
  28. - localcontext: The context that will be passed to the report engine
  29. If no report_xml_id is given the extender is registered for all py3o
  30. reports
  31. Idea copied from CampToCamp report_webkit module.
  32. :param report_xml_id: xml id of the report
  33. :return: a decorated class
  34. """
  35. global _extender_functions
  36. def fct1(fct):
  37. _extender_functions.setdefault(report_xml_id, []).append(fct)
  38. return fct
  39. return fct1
  40. @py3o_report_extender()
  41. def defautl_extend(report_xml, localcontext):
  42. # add the base64decode function to be able do decode binary fields into
  43. # the template
  44. localcontext['b64decode'] = b64decode
  45. class Py3oParser(report_sxw):
  46. """Custom class that use Py3o to render libroffice reports.
  47. Code partially taken from CampToCamp's webkit_report."""
  48. def get_template(self, report_obj):
  49. """private helper to fetch the template data either from the database
  50. or from the default template file provided by the implementer.
  51. ATM this method takes a report definition recordset
  52. to try and fetch the report template from database. If not found it
  53. will fallback to the template file referenced in the report definition.
  54. @param report_obj: a recordset representing the report defintion
  55. @type report_obj: openerp.model.recordset instance
  56. @returns: string or buffer containing the template data
  57. @raises: TemplateNotFound which is a subclass of
  58. openerp.exceptions.DeferredException
  59. """
  60. tmpl_data = None
  61. if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
  62. # if a user gave a report template
  63. tmpl_data = b64decode(
  64. report_obj.py3o_template_id.py3o_template_data
  65. )
  66. elif report_obj.py3o_template_fallback:
  67. tmpl_name = report_obj.py3o_template_fallback
  68. flbk_filename = None
  69. if report_obj.module:
  70. # if the default is defined
  71. flbk_filename = pkg_resources.resource_filename(
  72. "openerp.addons.%s" % report_obj.module,
  73. tmpl_name,
  74. )
  75. elif os.path.isabs(tmpl_name):
  76. # It is an absolute path
  77. flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
  78. if flbk_filename and os.path.exists(flbk_filename):
  79. # and it exists on the fileystem
  80. with open(flbk_filename, 'r') as tmpl:
  81. tmpl_data = tmpl.read()
  82. if tmpl_data is None:
  83. # if for any reason the template is not found
  84. raise TemplateNotFound(
  85. _(u'No template found. Aborting.'),
  86. sys.exc_info(),
  87. )
  88. return tmpl_data
  89. def _extend_parser_context(self, parser_instance, report_xml):
  90. # add default extenders
  91. for fct in _extender_functions.get(None, []):
  92. fct(report_xml, parser_instance.localcontext)
  93. # add extenders for registered on the template
  94. xml_id = report_xml.get_external_id().get(report_xml.id)
  95. if xml_id in _extender_functions:
  96. for fct in _extender_functions[xml_id]:
  97. fct(report_xml, parser_instance.localcontext)
  98. def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
  99. """ Overide this function to generate our py3o report
  100. """
  101. if report_xml.report_type != 'py3o':
  102. return super(Py3oParser, self).create_single_pdf(
  103. cr, uid, ids, data, report_xml, context=context
  104. )
  105. parser_instance = self.parser(cr, uid, self.name2, context=context)
  106. parser_instance.set_context(
  107. self.getObjects(cr, uid, ids, context),
  108. data, ids, report_xml.report_type
  109. )
  110. self._extend_parser_context(parser_instance, report_xml)
  111. tmpl_data = self.get_template(report_xml)
  112. in_stream = StringIO(tmpl_data)
  113. out_stream = StringIO()
  114. template = Template(in_stream, out_stream)
  115. localcontext = parser_instance.localcontext
  116. if report_xml.py3o_is_local_fusion:
  117. template.render(localcontext)
  118. in_stream = out_stream
  119. datadict = {}
  120. else:
  121. expressions = template.get_all_user_python_expression()
  122. py_expression = template.convert_py3o_to_python_ast(expressions)
  123. convertor = Py3oConvertor()
  124. data_struct = convertor(py_expression)
  125. datadict = data_struct.render(localcontext)
  126. filetype = report_xml.py3o_filetype
  127. is_native = Formats().get_format(filetype).native
  128. if is_native:
  129. res = out_stream.getvalue()
  130. else: # Call py3o.server to render the template in the desired format
  131. in_stream.seek(0)
  132. files = {
  133. 'tmpl_file': in_stream,
  134. }
  135. fields = {
  136. "targetformat": filetype,
  137. "datadict": json.dumps(datadict),
  138. "image_mapping": "{}",
  139. }
  140. if report_xml.py3o_is_local_fusion:
  141. fields['skipfusion'] = '1'
  142. r = requests.post(
  143. report_xml.py3o_server_id.url, data=fields, files=files)
  144. if r.status_code != 200:
  145. # server says we have an issue... let's tell that to enduser
  146. raise exceptions.Warning(
  147. _('Fusion server error %s') % r.text,
  148. )
  149. # Here is a little joke about Odoo
  150. # we do nice chunked reading from the network...
  151. chunk_size = 1024
  152. with NamedTemporaryFile(
  153. suffix=filetype.human_ext,
  154. prefix='py3o-template-'
  155. ) as fd:
  156. for chunk in r.iter_content(chunk_size):
  157. fd.write(chunk)
  158. fd.seek(0)
  159. # ... but odoo wants the whole data in memory anyways :)
  160. res = fd.read()
  161. return res, "." + filetype
  162. def create(self, cr, uid, ids, data, context=None):
  163. """ Override this function to handle our py3o report
  164. """
  165. pool = registry(cr.dbname)
  166. ir_action_report_obj = pool['ir.actions.report.xml']
  167. report_xml_ids = ir_action_report_obj.search(
  168. cr, uid, [('report_name', '=', self.name[7:])], context=context
  169. )
  170. if not report_xml_ids:
  171. return super(Py3oParser, self).create(
  172. cr, uid, ids, data, context=context
  173. )
  174. report_xml = ir_action_report_obj.browse(
  175. cr, uid, report_xml_ids[0], context=context
  176. )
  177. result = self.create_source_pdf(
  178. cr, uid, ids, data, report_xml, context
  179. )
  180. if not result:
  181. return False, False
  182. return result