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