1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2010 Camptocamp SA (http://www.camptocamp.com)
7 # Author : Nicolas Bessi (Camptocamp)
8 # Contributor(s) : Florent Xicluna (Wingo SA)
10 # WARNING: This program as such is intended to be used by professional
11 # programmers who take the whole responsability of assessing all potential
12 # consequences resulting from its eventual inadequacies and bugs
13 # End users who are looking for a ready-to-use solution with commercial
14 # garantees and support are strongly adviced to contract a Free Software
17 # This program is Free Software; you can redistribute it and/or
18 # modify it under the terms of the GNU General Public License
19 # as published by the Free Software Foundation; either version 2
20 # of the License, or (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
31 ##############################################################################
36 from openerp import report
41 from report_helper import WebKitHelper
43 from openerp.modules.module import get_module_resource
44 from openerp.report.report_sxw import *
45 from openerp import tools
46 from openerp.tools.translate import _
47 from openerp.osv.osv import except_osv
48 from urllib import urlencode, quote as quote
50 _logger = logging.getLogger(__name__)
53 # We use a jinja2 sandboxed environment to render mako templates.
54 # Note that the rendering does not cover all the mako syntax, in particular
55 # arbitrary Python statements are not accepted, and not all expressions are
56 # allowed: only "public" attributes (not starting with '_') of objects may
58 # This is done on purpose: it prevents incidental or malicious execution of
59 # Python code that may break the security of the server.
60 from jinja2.sandbox import SandboxedEnvironment
61 mako_template_env = SandboxedEnvironment(
62 block_start_string="<%",
63 block_end_string="%>",
64 variable_start_string="${",
65 variable_end_string="}",
66 comment_start_string="<%doc>",
67 comment_end_string="</%doc>",
68 line_statement_prefix="%",
69 line_comment_prefix="##",
70 trim_blocks=True, # do not output newline after blocks
71 autoescape=True, # XML/HTML automatic escaping
73 mako_template_env.globals.update({
76 'urlencode': urlencode,
79 _logger.warning("jinja2 not available, templating features will not work!")
81 def mako_template(text):
82 """Build a Mako template.
84 This template uses UTF-8 encoding
87 return mako_template_env.from_string(text)
89 _extender_functions = {}
91 def webkit_report_extender(report_name):
93 A decorator to define functions to extend the context used in a template rendering.
94 report_name must be the xml id of the desired report (it is mandatory to indicate the
95 module in that xml id).
97 The given function will be called at the creation of the report. The following arguments
98 will be passed to it (in this order):
99 - pool The model pool.
102 - localcontext The context given to the template engine to render the templates for the
103 current report. This is the context that should be modified.
104 - context The OpenERP context.
107 lst = _extender_functions.get(report_name)
110 _extender_functions[report_name] = lst
115 class WebKitParser(report_sxw):
116 """Custom class that use webkit to render HTML reports
117 Code partially taken from report openoffice. Thanks guys :)
119 def __init__(self, name, table, rml=False, parser=rml_parse,
120 header=True, store=False, register=True):
121 self.parser_instance = False
122 self.localcontext = {}
123 report_sxw.__init__(self, name, table, rml, parser,
124 header, store, register=register)
126 def get_lib(self, cursor, uid):
127 """Return the lib wkhtml path"""
128 proxy = self.pool['ir.config_parameter']
129 webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
133 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
134 if hasattr(sys, 'frozen'):
135 defpath.append(os.getcwd())
136 if tools.config['root_path']:
137 defpath.append(os.path.dirname(tools.config['root_path']))
138 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
146 _('Wkhtmltopdf library path is not set'),
147 _('Please install executable on your system' \
148 ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
149 ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
150 ' path in the ir.config_parameter with the webkit_path key.' \
151 'Minimal version is 0.9.9')
154 def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
155 """Call webkit in order to generate pdf"""
156 if not webkit_header:
157 webkit_header = report_xml.webkit_header
158 tmp_dir = tempfile.gettempdir()
159 out_filename = tempfile.mktemp(suffix=".pdf", prefix="webkit.tmp.")
160 file_to_del = [out_filename]
162 command = [comm_path]
164 command = ['wkhtmltopdf']
166 command.append('--quiet')
167 # default to UTF-8 encoding. Use <meta charset="latin-1"> to override.
168 command.extend(['--encoding', 'utf-8'])
170 head_file = file( os.path.join(
172 str(time.time()) + '.head.html'
176 head_file.write(header.encode('utf-8'))
178 file_to_del.append(head_file.name)
179 command.extend(['--header-html', head_file.name])
181 foot_file = file( os.path.join(
183 str(time.time()) + '.foot.html'
187 foot_file.write(footer.encode('utf-8'))
189 file_to_del.append(foot_file.name)
190 command.extend(['--footer-html', foot_file.name])
192 if webkit_header.margin_top :
193 command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
194 if webkit_header.margin_bottom :
195 command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
196 if webkit_header.margin_left :
197 command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
198 if webkit_header.margin_right :
199 command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
200 if webkit_header.orientation :
201 command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
202 if webkit_header.format :
203 command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
205 for html in html_list :
206 html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
208 html_file.write(html.encode('utf-8'))
210 file_to_del.append(html_file.name)
211 command.append(html_file.name)
212 command.append(out_filename)
213 stderr_fd, stderr_path = tempfile.mkstemp(text=True)
214 file_to_del.append(stderr_path)
216 status = subprocess.call(command, stderr=stderr_fd)
217 os.close(stderr_fd) # ensure flush before reading
218 stderr_fd = None # avoid closing again in finally block
219 fobj = open(stderr_path, 'r')
220 error_message = fobj.read()
222 if not error_message:
223 error_message = _('No diagnosis message was provided')
225 error_message = _('The following diagnosis message was provided:\n') + error_message
227 raise except_osv(_('Webkit error' ),
228 _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
229 pdf_file = open(out_filename, 'rb')
230 pdf = pdf_file.read()
233 if stderr_fd is not None:
235 for f_to_del in file_to_del:
238 except (OSError, IOError), exc:
239 _logger.error('cannot remove file %s: %s', f_to_del, exc)
242 def translate_call(self, src):
243 """Translate String."""
244 ir_translation = self.pool['ir.translation']
245 name = self.tmpl and 'addons/' + self.tmpl or None
246 res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
247 name, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
249 # no translation defined, fallback on None (backward compatibility)
250 res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
251 None, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
256 # override needed to keep the attachments storing procedure
257 def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
258 """generate the PDF"""
260 # just try to find an xml id for the report
262 pool = openerp.registry(cr.dbname)
263 found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
264 ["res_id", "=", report_xml.id]], context=context)
267 xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
268 xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
273 if report_xml.report_type != 'webkit':
274 return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
276 self.parser_instance = self.parser(cursor,
282 objs = self.getObjects(cursor, uid, ids, context)
283 self.parser_instance.set_context(objs, data, ids, report_xml.report_type)
287 if report_xml.report_file :
288 path = get_module_resource(*report_xml.report_file.split('/'))
289 if path and os.path.exists(path) :
290 template = file(path).read()
291 if not template and report_xml.report_webkit_data :
292 template = report_xml.report_webkit_data
294 raise except_osv(_('Error!'), _('Webkit report template not found!'))
295 header = report_xml.webkit_header.html
296 footer = report_xml.webkit_header.footer_html
297 if not header and report_xml.header:
299 _('No header defined for this Webkit report!'),
300 _('Please set a header in company settings.')
302 if not report_xml.header :
304 default_head = get_module_resource('report_webkit', 'default_header.html')
305 with open(default_head,'r') as f:
307 css = report_xml.webkit_header.css
311 body_mako_tpl = mako_template(template)
312 helper = WebKitHelper(cursor, uid, report_xml.id, context)
313 self.parser_instance.localcontext['helper'] = helper
314 self.parser_instance.localcontext['css'] = css
315 self.parser_instance.localcontext['_'] = self.translate_call
317 # apply extender functions
319 if xml_id in _extender_functions:
320 for fct in _extender_functions[xml_id]:
321 fct(pool, cr, uid, self.parser_instance.localcontext, context)
323 if report_xml.precise_mode:
324 ctx = dict(self.parser_instance.localcontext)
325 for obj in self.parser_instance.localcontext['objects']:
326 ctx['objects'] = [obj]
328 html = body_mako_tpl.render(dict(ctx))
333 raise except_osv(_('Webkit render!'), msg)
336 html = body_mako_tpl.render(dict(self.parser_instance.localcontext))
341 raise except_osv(_('Webkit render!'), msg)
342 head_mako_tpl = mako_template(header)
344 head = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=False))
346 raise except_osv(_('Webkit render!'), u"%s" % e)
349 foot_mako_tpl = mako_template(footer)
351 foot = foot_mako_tpl.render(dict({},
352 **self.parser_instance.localcontext))
356 raise except_osv(_('Webkit render!'), msg)
357 if report_xml.webkit_debug :
359 deb = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
363 raise except_osv(_('Webkit render!'), msg)
365 bin = self.get_lib(cursor, uid)
366 pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
370 def create(self, cursor, uid, ids, data, context=None):
371 """We override the create function in order to handle generator
372 Code taken from report openoffice. Thanks guys :) """
373 pool = openerp.registry(cursor.dbname)
374 ir_obj = pool['ir.actions.report.xml']
375 report_xml_ids = ir_obj.search(cursor, uid,
376 [('report_name', '=', self.name[7:])], context=context)
379 report_xml = ir_obj.browse(cursor,
383 report_xml.report_rml = None
384 report_xml.report_rml_content = None
385 report_xml.report_sxw_content_data = None
386 report_xml.report_sxw_content = None
387 report_xml.report_sxw = None
389 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
390 if report_xml.report_type != 'webkit' :
391 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
392 result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
397 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: