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.

226 lines
7.8 KiB

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