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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
31 ##############################################################################
36 from openerp import report
41 from openerp import netsvc
42 from openerp import pooler
43 from report_helper import WebKitHelper
44 from openerp.report.report_sxw import *
45 from openerp import addons
46 from openerp import tools
47 from openerp.tools.translate import _
48 from openerp.osv.osv import except_osv
49 from urllib import urlencode, quote as quote
51 _logger = logging.getLogger(__name__)
54 # We use a jinja2 sandboxed environment to render mako templates.
55 # Note that the rendering does not cover all the mako syntax, in particular
56 # arbitrary Python statements are not accepted, and not all expressions are
57 # allowed: only "public" attributes (not starting with '_') of objects may
59 # This is done on purpose: it prevents incidental or malicious execution of
60 # Python code that may break the security of the server.
61 from jinja2.sandbox import SandboxedEnvironment
62 mako_template_env = SandboxedEnvironment(
63 block_start_string="<%",
64 block_end_string="%>",
65 variable_start_string="${",
66 variable_end_string="}",
67 comment_start_string="<%doc>",
68 comment_end_string="</%doc>",
69 line_statement_prefix="%",
70 line_comment_prefix="##",
71 trim_blocks=True, # do not output newline after blocks
72 autoescape=True, # XML/HTML automatic escaping
74 mako_template_env.globals.update({
77 'urlencode': urlencode,
80 _logger.warning("jinja2 not available, templating features will not work!")
82 def mako_template(text):
83 """Build a Mako template.
85 This template uses UTF-8 encoding
88 return mako_template_env.from_string(text)
90 _extender_functions = {}
92 def webkit_report_extender(report_name):
94 A decorator to define functions to extend the context used in a template rendering.
95 report_name must be the xml id of the desired report (it is mandatory to indicate the
96 module in that xml id).
98 The given function will be called at the creation of the report. The following arguments
99 will be passed to it (in this order):
100 - pool The model pool.
103 - localcontext The context given to the template engine to render the templates for the
104 current report. This is the context that should be modified.
105 - context The OpenERP context.
108 lst = _extender_functions.get(report_name)
111 _extender_functions[report_name] = lst
116 class WebKitParser(report_sxw):
117 """Custom class that use webkit to render HTML reports
118 Code partially taken from report openoffice. Thanks guys :)
120 def __init__(self, name, table, rml=False, parser=False,
121 header=True, store=False):
122 self.parser_instance = False
123 self.localcontext = {}
124 report_sxw.__init__(self, name, table, rml, parser,
127 def get_lib(self, cursor, uid):
128 """Return the lib wkhtml path"""
129 proxy = self.pool.get('ir.config_parameter')
130 webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
134 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
135 if hasattr(sys, 'frozen'):
136 defpath.append(os.getcwd())
137 if tools.config['root_path']:
138 defpath.append(os.path.dirname(tools.config['root_path']))
139 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
147 _('Wkhtmltopdf library path is not set'),
148 _('Please install executable on your system' \
149 ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
150 ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
151 ' path in the ir.config_parameter with the webkit_path key.' \
152 'Minimal version is 0.9.9')
155 def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
156 """Call webkit in order to generate pdf"""
157 if not webkit_header:
158 webkit_header = report_xml.webkit_header
159 tmp_dir = tempfile.gettempdir()
160 out_filename = tempfile.mktemp(suffix=".pdf", prefix="webkit.tmp.")
161 file_to_del = [out_filename]
163 command = [comm_path]
165 command = ['wkhtmltopdf']
167 command.append('--quiet')
168 # default to UTF-8 encoding. Use <meta charset="latin-1"> to override.
169 command.extend(['--encoding', 'utf-8'])
171 head_file = file( os.path.join(
173 str(time.time()) + '.head.html'
177 head_file.write(header)
179 file_to_del.append(head_file.name)
180 command.extend(['--header-html', head_file.name])
182 foot_file = file( os.path.join(
184 str(time.time()) + '.foot.html'
188 foot_file.write(footer)
190 file_to_del.append(foot_file.name)
191 command.extend(['--footer-html', foot_file.name])
193 if webkit_header.margin_top :
194 command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
195 if webkit_header.margin_bottom :
196 command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
197 if webkit_header.margin_left :
198 command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
199 if webkit_header.margin_right :
200 command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
201 if webkit_header.orientation :
202 command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
203 if webkit_header.format :
204 command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
206 for html in html_list :
207 html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
209 html_file.write(html)
211 file_to_del.append(html_file.name)
212 command.append(html_file.name)
213 command.append(out_filename)
214 stderr_fd, stderr_path = tempfile.mkstemp(text=True)
215 file_to_del.append(stderr_path)
217 status = subprocess.call(command, stderr=stderr_fd)
218 os.close(stderr_fd) # ensure flush before reading
219 stderr_fd = None # avoid closing again in finally block
220 fobj = open(stderr_path, 'r')
221 error_message = fobj.read()
223 if not error_message:
224 error_message = _('No diagnosis message was provided')
226 error_message = _('The following diagnosis message was provided:\n') + error_message
228 raise except_osv(_('Webkit error' ),
229 _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
230 pdf_file = open(out_filename, 'rb')
231 pdf = pdf_file.read()
234 if stderr_fd is not None:
236 for f_to_del in file_to_del:
239 except (OSError, IOError), exc:
240 _logger.error('cannot remove file %s: %s', f_to_del, exc)
243 def translate_call(self, src):
244 """Translate String."""
245 ir_translation = self.pool.get('ir.translation')
246 res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
247 None, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
252 # override needed to keep the attachments storing procedure
253 def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
254 """generate the PDF"""
256 # just try to find an xml id for the report
258 import openerp.pooler as pooler
259 pool = pooler.get_pool(cr.dbname)
260 found_xml_ids = pool.get("ir.model.data").search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
261 ["res_id", "=", report_xml.id]], context=context)
264 xml_id = pool.get("ir.model.data").read(cr, uid, found_xml_ids[0], ["module", "name"])
265 xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
270 if report_xml.report_type != 'webkit':
271 return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
273 self.parser_instance = self.parser(cursor,
278 self.pool = pooler.get_pool(cursor.dbname)
279 objs = self.getObjects(cursor, uid, ids, context)
280 self.parser_instance.set_context(objs, data, ids, report_xml.report_type)
284 if report_xml.report_file :
285 path = addons.get_module_resource(*report_xml.report_file.split(os.path.sep))
286 if path and os.path.exists(path) :
287 template = file(path).read()
288 if not template and report_xml.report_webkit_data :
289 template = report_xml.report_webkit_data
291 raise except_osv(_('Error!'), _('Webkit report template not found!'))
292 header = report_xml.webkit_header.html
293 footer = report_xml.webkit_header.footer_html
294 if not header and report_xml.header:
296 _('No header defined for this Webkit report!'),
297 _('Please set a header in company settings.')
299 if not report_xml.header :
301 default_head = addons.get_module_resource('report_webkit', 'default_header.html')
302 with open(default_head,'r') as f:
304 css = report_xml.webkit_header.css
308 body_mako_tpl = mako_template(template)
309 helper = WebKitHelper(cursor, uid, report_xml.id, context)
310 self.parser_instance.localcontext['helper'] = helper
311 self.parser_instance.localcontext['css'] = css
312 self.parser_instance.localcontext['_'] = self.translate_call
314 # apply extender functions
316 if xml_id in _extender_functions:
317 for fct in _extender_functions[xml_id]:
318 fct(pool, cr, uid, self.parser_instance.localcontext, context)
320 if report_xml.precise_mode:
321 ctx = dict(self.parser_instance.localcontext)
322 for obj in self.parser_instance.localcontext['objects']:
323 ctx['objects'] = [obj]
325 html = body_mako_tpl.render(dict(ctx))
330 raise except_osv(_('Webkit render!'), msg)
333 html = body_mako_tpl.render(dict(self.parser_instance.localcontext))
338 raise except_osv(_('Webkit render!'), msg)
339 head_mako_tpl = mako_template(header)
341 head = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=False))
343 raise except_osv(_('Webkit render!'), u"%s" % e)
346 foot_mako_tpl = mako_template(footer)
348 foot = foot_mako_tpl.render(dict({},
349 **self.parser_instance.localcontext))
353 raise except_osv(_('Webkit render!'), msg)
354 if report_xml.webkit_debug :
356 deb = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
360 raise except_osv(_('Webkit render!'), msg)
362 bin = self.get_lib(cursor, uid)
363 pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
367 def create(self, cursor, uid, ids, data, context=None):
368 """We override the create function in order to handle generator
369 Code taken from report openoffice. Thanks guys :) """
370 pool = pooler.get_pool(cursor.dbname)
371 ir_obj = pool.get('ir.actions.report.xml')
372 report_xml_ids = ir_obj.search(cursor, uid,
373 [('report_name', '=', self.name[7:])], context=context)
376 report_xml = ir_obj.browse(cursor,
380 report_xml.report_rml = None
381 report_xml.report_rml_content = None
382 report_xml.report_sxw_content_data = None
383 report_xml.report_sxw_content = None
384 report_xml.report_sxw = None
386 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
387 if report_xml.report_type != 'webkit' :
388 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
389 result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
394 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: