diff --git a/addons/account/report/report_vat.py b/addons/account/report/report_vat.py index 236e2ffaac2..3a0950ac62f 100644 --- a/addons/account/report/report_vat.py +++ b/addons/account/report/report_vat.py @@ -19,6 +19,7 @@ # ############################################################################## +from openerp.osv import osv from openerp.addons.web import http from openerp.addons.web.http import request from common_report_header import common_report_header @@ -29,14 +30,12 @@ except ImportError: import xlwt -class tax_report(http.Controller, common_report_header): +class tax_report(osv.AbstractModel, common_report_header): + _name = 'report.account.report_vat' - @http.route(['/report/account.report_vat'], type='http', auth='user', website=True, multilang=True) - def report_account_tax(self, **data): + def render_html(self, cr, uid, ids, data=None, context=None): report_obj = request.registry['report'] - self.cr, self.uid, self.pool = request.cr, request.uid, request.registry - - data = report_obj.eval_params(data) + self.cr, self.uid, self.context = cr, uid, context res = {} self.period_ids = [] @@ -54,23 +53,23 @@ class tax_report(http.Controller, common_report_header): 'based_on': self._get_basedon(data), 'period_from': self.get_start_period(data), 'period_to': self.get_end_period(data), - 'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']), + 'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid), } - return request.registry['report'].render(self.cr, self.uid, [], 'account.report_vat', docargs) + return report_obj.render(self.cr, self.uid, [], 'account.report_vat', docargs, context=context) def _get_basedon(self, form): return form['form']['based_on'] - def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None): + def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None, cr=None, uid=None): period_list = self.period_ids - res = self._get_codes(based_on, company_id, parent, level, period_list, context=context) + res = self._get_codes(based_on, company_id, parent, level, period_list, cr=cr, uid=uid, context=context) if period_list: res = self._add_codes(based_on, res, period_list, context=context) else: - self.cr.execute ("select id from account_fiscalyear") - fy = self.cr.fetchall() - self.cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],)) - periods = self.cr.fetchall() + cr.execute ("select id from account_fiscalyear") + fy = cr.fetchall() + cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],)) + periods = cr.fetchall() for p in periods: period_list.append(p[0]) res = self._add_codes(based_on, res, period_list, context=context) @@ -90,7 +89,7 @@ class tax_report(http.Controller, common_report_header): } top_result.append(res_dict) - res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, context=context) + res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, cr=cr, uid=uid, context=context) ind_general = 0 while ind_general < len(res_general): res_general[ind_general]['type'] = 2 @@ -101,14 +100,14 @@ class tax_report(http.Controller, common_report_header): i+=1 return top_result - def _get_general(self, tax_code_id, period_list, company_id, based_on, context=None): + def _get_general(self, tax_code_id, period_list, company_id, based_on, cr=None, uid=None, context=None): if not self.display_detail: return [] res = [] obj_account = self.pool.get('account.account') periods_ids = tuple(period_list) if based_on == 'payments': - self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ + cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ SUM(line.debit) AS debit, \ SUM(line.credit) AS credit, \ COUNT(*) AS count, \ @@ -132,7 +131,7 @@ class tax_report(http.Controller, common_report_header): company_id, periods_ids, 'paid',)) else: - self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ + cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ SUM(line.debit) AS debit, \ SUM(line.credit) AS credit, \ COUNT(*) AS count, \ @@ -149,23 +148,21 @@ class tax_report(http.Controller, common_report_header): AND account.active \ GROUP BY account.id,account.name,account.code', ('draft', tax_code_id, company_id, periods_ids,)) - res = self.cr.dictfetchall() + res = cr.dictfetchall() i = 0 while i= int(accounts[bcl_rup_ind]['level']) and bcl_rup_ind >= 0 ): - res_tot = { 'code': accounts[bcl_rup_ind]['code'], + res_tot = { + 'code': accounts[bcl_rup_ind]['code'], 'name': '', 'debit': 0, 'credit': 0, @@ -229,25 +224,23 @@ class tax_report(http.Controller, common_report_header): return result_accounts + +class tax_report_xls(http.Controller): + @http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True) def report_account_tax_xls(self, **data): - report_obj = request.registry['report'] - self.cr, self.uid, self.pool = request.cr, request.uid, request.registry - data = report_obj.eval_params(data) + # Very ugly lines, only for the proof of concept of 'controller' report + taxreport_obj = request.registry['report.account.report_vat'] + from openerp.addons.report.controllers.main import ReportController + eval_params = ReportController()._eval_params - res = {} - self.period_ids = [] - period_obj = self.pool.get('account.period') - self.display_detail = data['form']['display_detail'] - res['periods'] = '' - res['fiscalyear'] = data['form'].get('fiscalyear_id', False) + cr, uid = request.cr, request.uid + data = eval_params(data) + data = {'form': data} - if data['form'].get('period_from', False) and data['form'].get('period_to', False): - self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to']) - - content = '' - lines = self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']) + taxreport_obj.render_html(cr, uid, [], data=data) + lines = taxreport_obj._get_lines(taxreport_obj._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid) if lines: xls = StringIO.StringIO() diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index c1329eb5a4e..3f0fad1c8a0 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -449,7 +449,7 @@ class email_template(osv.osv): ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, [res_id], context)[res_id] # take 0 ? if report.report_type in ['qweb-html', 'qweb-pdf']: - result, format = self.pool['report'].get_pdf(report, res_id, context=ctx), 'pdf' + result, format = self.pool['report'].get_pdf(cr, uid, [res_id], report_service, context=ctx), 'pdf' else: result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) diff --git a/addons/mrp/report/bom_structure.py b/addons/mrp/report/bom_structure.py index 9af4e91b8ce..98949ed75dc 100644 --- a/addons/mrp/report/bom_structure.py +++ b/addons/mrp/report/bom_structure.py @@ -19,24 +19,22 @@ # ############################################################################## -from openerp.addons.web import http -from openerp.addons.web.http import request +from openerp.osv import osv -class bom_structure(http.Controller): +class bom_structure(osv.AbstractModel): + _name = 'report.mrp.report_mrpbomstructure' - @http.route(['/report/mrp.report_mrpbomstructure/'], type='http', auth='user', website=True, multilang=True) - def report_mrpbomstructure(self, docids): - ids = [int(i) for i in docids.split(',')] - ids = list(set(ids)) - report_obj = request.registry['mrp.bom'] - docs = report_obj.browse(request.cr, request.uid, ids, context=request.context) + def render_html(self, cr, uid, ids, data=None, context=None): + mrpbom_obj = self.pool['mrp.bom'] + report_obj = self.pool['report'] + docs = mrpbom_obj.browse(cr, uid, ids, context=context) docargs = { 'docs': docs, 'get_children': self.get_children, } - return request.registry['report'].render(request.cr, request.uid, [], 'mrp.report_mrpbomstructure', docargs) + return report_obj.render(cr, uid, [], 'mrp.report_mrpbomstructure', docargs, context=context) def get_children(self, object, level=0): result = [] diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 6c090e3cf5d..ed131670737 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -446,12 +446,7 @@ class purchase_order(osv.osv): ''' assert len(ids) == 1, 'This option should only be used for a single id at a time' self.signal_send_rfq(cr, uid, ids) - datas = { - 'model': 'purchase.order', - 'ids': ids, - 'form': self.read(cr, uid, ids[0], context=context), - } - return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True} + return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context) #TODO: implement messages system def wkf_confirm_order(self, cr, uid, ids, context=None): diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 0dae5f41288..5cff7b4aae2 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -19,459 +19,62 @@ # ############################################################################## -from openerp.osv.osv import except_osv -from openerp.addons.web import http -from openerp.tools.translate import _ -from openerp.addons.web.http import request -import openerp.tools.config as config +from openerp.addons.web.http import Controller, route, request -import time -import base64 -import logging -import tempfile -import lxml.html -import subprocess import simplejson -try: - import cStringIO as StringIO -except ImportError: - import StringIO -import psutil -import signal -import os -from distutils.version import LooseVersion - - +import urlparse from werkzeug import exceptions -from werkzeug.test import Client -from werkzeug.wrappers import BaseResponse -from werkzeug.datastructures import Headers from reportlab.graphics.barcode import createBarcodeDrawing -_logger = logging.getLogger(__name__) -try: - from pyPdf import PdfFileWriter, PdfFileReader -except ImportError: - PdfFileWriter = PdfFileReader = None +class ReportController(Controller): -class Report(http.Controller): - - @http.route(['/report//'], type='http', auth='user', website=True, multilang=True) - def report_html(self, reportname, docids, **kwargs): - """This is the generic route for QWeb reports. It is used for reports - which do not need to preprocess the data (i.e. reports that just display - fields of a record). - - It is given a ~fully qualified report name, for instance 'account.report_invoice'. - Based on it, we know the module concerned and the name of the template. With the - name of the template, we will make a search on the ir.actions.reports.xml table and - get the record associated to finally know the model this template refers to. - - There is a way to declare the report (in module_report(s).xml) that you must respect: - id="action_report_model" - model="module.model" # To know which model the report refers to - string="Invoices" - report_type="qweb-pdf" # or qweb-html - name="module.template_name" - file="module.template_name" - - If you don't want your report listed under the print button, just add - 'menu=False'. - """ - ids = [int(i) for i in docids.split(',')] - ids = list(set(ids)) - report = self._get_report_from_name(reportname) - report_obj = request.registry[report.model] - docs = report_obj.browse(request.cr, request.uid, ids, context=request.context) - - docargs = { - 'doc_ids': ids, - 'doc_model': report.model, - 'docs': docs, - } - - return request.registry['report'].render(request.cr, request.uid, [], report.report_name, - docargs, context=request.context) - - @http.route(['/report/pdf/'], type='http', auth="user", website=True) - def report_pdf(self, path=None, landscape=False, **post): - """Route converting any reports to pdf. It will get the html-rendered report, extract - header, page and footer in order to prepare minimal html pages that will be further passed - to wkhtmltopdf. - - :param path: URL of the report (e.g. /report/account.report_invoice/1) - :returns: a response with 'application/pdf' headers and the pdf as content - """ + #------------------------------------------------------ + # Generic reports controller + #------------------------------------------------------ + @route('/report//', type='http', auth='user', website=True, multilang=True) + def report_html(self, reportname, docids): cr, uid, context = request.cr, request.uid, request.context + docids = self._eval_params(docids) + return request.registry['report'].get_html(cr, uid, docids, reportname, context=context) - # Get the report we are working on. - # Pattern is /report/module.reportname(?a=1) - reportname_in_path = path.split('/')[1].split('?')[0] - report = self._get_report_from_name(reportname_in_path) - - # Check attachment_use field. If set to true and an existing pdf is already saved, load - # this one now. If not, mark save it. - save_in_attachment = {} - - if report.attachment_use is True: - # Get the record ids we are working on. - path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')] - - save_in_attachment['model'] = report.model - save_in_attachment['loaded_documents'] = {} - - for path_id in path_ids: - obj = request.registry[report.model].browse(cr, uid, path_id) - filename = eval(report.attachment, {'object': obj, 'time': time}) - - if filename is False: # May be false if, for instance, the record is in draft state - continue - else: - alreadyindb = [('datas_fname', '=', filename), - ('res_model', '=', report.model), - ('res_id', '=', path_id)] - - attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb) - if attach_ids: - # Add the loaded pdf in the loaded_documents list - pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas - pdf = base64.decodestring(pdf) - save_in_attachment['loaded_documents'][path_id] = pdf - _logger.info('The PDF document %s was loaded from the database' % filename) - else: - # Mark current document to be saved - save_in_attachment[path_id] = filename - - # Get the paperformat associated to the report. If there is not, get the one associated to - # the company. - if not report.paperformat_id: - user = request.registry['res.users'].browse(cr, uid, uid, context=context) - paperformat = user.company_id.paperformat_id - else: - paperformat = report.paperformat_id - - # Get the html report. - html = self._get_url_content('/' + path, post)[0] - subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering - css = '' # Local css - - headerhtml = [] - contenthtml = [] - footerhtml = [] - base_url = request.registry['ir.config_parameter'].get_param(cr, uid, 'web.base.url') - - minimalhtml = """ - - - - - - - - - - - - - - - {2} - -""" - - # The retrieved html report must be simplified. We convert it into a xml tree - # via lxml in order to extract headers, footers and content. - try: - root = lxml.html.fromstring(html) - - for node in root.xpath("//html/head/style"): - css += node.text - - for node in root.xpath("//div[@class='header']"): - body = lxml.html.tostring(node) - header = minimalhtml.format(css, subst, body, base_url) - headerhtml.append(header) - - for node in root.xpath("//div[@class='footer']"): - body = lxml.html.tostring(node) - footer = minimalhtml.format(css, subst, body, base_url) - footerhtml.append(footer) - - for node in root.xpath("//div[@class='page']"): - # Previously, we marked some reports to be saved in attachment via their ids, so we - # must set a relation between report ids and report's content. We use the QWeb - # branding in order to do so: searching after a node having a data-oe-model - # attribute with the value of the current report model and read its oe-id attribute - oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']") - if oemodelnode is not None: - reportid = oemodelnode.get('data-oe-id', False) - if reportid is not False: - reportid = int(reportid) - else: - reportid = False - - body = lxml.html.tostring(node) - reportcontent = minimalhtml.format(css, '', body, base_url) - contenthtml.append(tuple([reportid, reportcontent])) - - except lxml.etree.XMLSyntaxError: - contenthtml = [] - contenthtml.append(html) - save_in_attachment = {} # Don't save this potentially malformed document - - # Get paperformat arguments set in the root html tag. They are prioritized over - # paperformat-record arguments. - specific_paperformat_args = {} - for attribute in root.items(): - if attribute[0].startswith('data-report-'): - specific_paperformat_args[attribute[0]] = attribute[1] - - # Execute wkhtmltopdf process. - pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape, - paperformat, specific_paperformat_args, save_in_attachment) - - return self._make_pdf_response(pdf) - - def _get_url_content(self, url, post=None): - """Resolve an internal webpage url and return its content with the help of - werkzeug.test.client. - - :param url: string representing the url to resolve - :param post: a dict representing the query string - :returns: a tuple str(html), int(statuscode) - """ - # Rebuilding the query string. - if post: - url += '?' - url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems()) - - # We have to pass the current headers in order to see the report. - reqheaders = Headers(request.httprequest.headers) - response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, - follow_redirects=True) - content = response.data - - try: - content = content.decode('utf-8') - except UnicodeDecodeError: - pass - - return tuple([content, response.headers]) - - def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape, - paperformat, spec_paperformat_args=None, save_in_attachment=None): - """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf - document. - - :param header: list of string containing the headers - :param footer: list of string containing the footers - :param bodies: list of string containing the reports - :param landscape: boolean to force the pdf to be rendered under a landscape format - :param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments - :param specific_paperformat_args: dict of prioritized paperformat arguments - :param save_in_attachment: dict of reports to save/load in/from the db - :returns: Content of the pdf as a string - """ - command = ['wkhtmltopdf'] - tmp_dir = tempfile.gettempdir() - - command_args = [] - # Passing the cookie in order to resolve URL. - command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']]) - - # Display arguments - if paperformat: - command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args)) - - if landscape and '--orientation' in command_args: - command_args_copy = list(command_args) - for index, elem in enumerate(command_args_copy): - if elem == '--orientation': - del command_args[index] - del command_args[index] - command_args.extend(['--orientation', 'landscape']) - elif landscape and not '--orientation' in command_args: - command_args.extend(['--orientation', 'landscape']) - - pdfdocuments = [] - # HTML to PDF thanks to WKhtmltopdf - for index, reporthtml in enumerate(bodies): - command_arg_local = [] - pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', - mode='w+b') - # Directly load the document if we have it - if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]): - pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0])) - pdfreport.seek(0) - pdfdocuments.append(pdfreport) - continue - - # Header stuff - if headers: - head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.', - dir=tmp_dir, mode='w+') - head_file.write(headers[index]) - head_file.seek(0) - command_arg_local.extend(['--header-html', head_file.name]) - - # Footer stuff - if footers: - foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.', - dir=tmp_dir, mode='w+') - foot_file.write(footers[index]) - foot_file.seek(0) - command_arg_local.extend(['--footer-html', foot_file.name]) - - # Body stuff - content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.', - dir=tmp_dir, mode='w+') - content_file.write(reporthtml[1]) - content_file.seek(0) - - try: - # If the server is running with only one worker, increase it to two to be able - # to serve the http request from wkhtmltopdf. - if config['workers'] == 1: - ppid = psutil.Process(os.getpid()).ppid - os.kill(ppid, signal.SIGTTIN) - - wkhtmltopdf = command + command_args + command_arg_local - wkhtmltopdf += [content_file.name] + [pdfreport.name] - - process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = process.communicate() - - if config['workers'] == 1: - os.kill(ppid, signal.SIGTTOU) - - if process.returncode != 0: - raise except_osv(_('Report (PDF)'), - _('wkhtmltopdf failed with error code = %s. ' - 'Message: %s') % (str(process.returncode), err)) - - # Save the pdf in attachment if marked - if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]): - attachment = { - 'name': save_in_attachment.get(reporthtml[0]), - 'datas': base64.encodestring(pdfreport.read()), - 'datas_fname': save_in_attachment.get(reporthtml[0]), - 'res_model': save_in_attachment.get('model'), - 'res_id': reporthtml[0], - } - request.registry['ir.attachment'].create(request.cr, request.uid, attachment) - _logger.info('The PDF document %s is now saved in the ' - 'database' % attachment['name']) - - pdfreport.seek(0) - pdfdocuments.append(pdfreport) - - if headers: - head_file.close() - if footers: - foot_file.close() - except: - raise - - # Get and return the full pdf - if len(pdfdocuments) == 1: - content = pdfdocuments[0].read() - pdfdocuments[0].close() - else: - content = self._merge_pdf(pdfdocuments) - - return content - - def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None): - """Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat - record. - - :paperformat: ir.actions.report.paperformat record associated to a document - :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments - :returns: list of string containing the wkhtmltopdf arguments - """ - command_args = [] - if paperformat.format and paperformat.format != 'custom': - command_args.extend(['--page-size', paperformat.format]) - - if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom': - command_args.extend(['--page-width', str(paperformat.page_width) + 'in']) - command_args.extend(['--page-height', str(paperformat.page_height) + 'in']) - - if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']: - command_args.extend(['--margin-top', - str(specific_paperformat_args['data-report-margin-top'])]) - elif paperformat.margin_top: - command_args.extend(['--margin-top', str(paperformat.margin_top)]) - - if paperformat.margin_left: - command_args.extend(['--margin-left', str(paperformat.margin_left)]) - if paperformat.margin_bottom: - command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)]) - if paperformat.margin_right: - command_args.extend(['--margin-right', str(paperformat.margin_right)]) - if paperformat.orientation: - command_args.extend(['--orientation', str(paperformat.orientation)]) - if paperformat.header_spacing: - command_args.extend(['--header-spacing', str(paperformat.header_spacing)]) - if paperformat.header_line: - command_args.extend(['--header-line']) - if paperformat.dpi: - command_args.extend(['--dpi', str(paperformat.dpi)]) - - return command_args - - def _get_report_from_name(self, report_name): - """Get the first record of ir.actions.report.xml having the argument as value for - the field report_name. - """ - report_obj = request.registry['ir.actions.report.xml'] - qwebtypes = ['qweb-pdf', 'qweb-html'] - - idreport = report_obj.search(request.cr, request.uid, - [('report_type', 'in', qwebtypes), - ('report_name', '=', report_name)]) - - report = report_obj.browse(request.cr, request.uid, idreport[0], - context=request.context) - return report - - def _make_pdf_response(self, pdf): - """Make a request response for a PDF file with correct http headers. - - :param pdf: content of a pdf in a string - :returns: request response for a pdf document - """ - pdfhttpheaders = [('Content-Type', 'application/pdf'), - ('Content-Length', len(pdf))] + @route('/report/pdf/report//', type='http', auth="user", website=True) + def report_pdf(self, reportname, docids): + cr, uid, context = request.cr, request.uid, request.context + docids = self._eval_params(docids) + pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context) + pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) - def _merge_pdf(self, documents): - """Merge PDF files into one. + #------------------------------------------------------ + # Particular reports controller + #------------------------------------------------------ + @route('/report/', type='http', auth='user', website=True, multilang=True) + def report_html_particular(self, reportname, **data): + cr, uid, context = request.cr, request.uid, request.context + report_obj = request.registry['report'] + data = self._eval_params(data) # Sanitizing + return report_obj.get_html(cr, uid, [], reportname, data=data, context=context) - :param documents: list of pdf files - :returns: string containing the merged pdf - """ - writer = PdfFileWriter() - for document in documents: - reader = PdfFileReader(file(document.name, "rb")) - for page in range(0, reader.getNumPages()): - writer.addPage(reader.getPage(page)) - document.close() - merged = StringIO.StringIO() - writer.write(merged) - merged.seek(0) - content = merged.read() - merged.close() - return content + @route('/report/pdf/report/', type='http', auth='user', website=True, multilang=True) + def report_pdf_particular(self, reportname, **data): + cr, uid, context = request.cr, request.uid, request.context + report_obj = request.registry['report'] + data = self._eval_params(data) # Sanitizing + pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context) + pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] + return request.make_response(pdf, headers=pdfhttpheaders) - @http.route(['/report/barcode', '/report/barcode//'], type='http', auth="user") - def barcode(self, type, value, width=300, height=50): + #------------------------------------------------------ + # Misc. route utils + #------------------------------------------------------ + @route(['/report/barcode', '/report/barcode//'], type='http', auth="user") + def report_barcode(self, type, value, width=300, height=50): """Contoller able to render barcode images thanks to reportlab. Samples: - + :param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', 'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', @@ -488,52 +91,69 @@ class Report(http.Controller): return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) - @http.route('/report/download', type='http', auth="user") - def report_attachment(self, data, token): + @route(['/report/download'], type='http', auth="user", website=True) + def report_download(self, data, token): """This function is used by 'qwebactionmanager.js' in order to trigger the download of - a report of any type. + a pdf report. - :param data: a javasscript array JSON.stringified containg report internal url ([0]) and + :param data: a javascript array JSON.stringified containg report internal url ([0]) and type [1] :returns: Response with a filetoken cookie and an attachment header """ requestcontent = simplejson.loads(data) url, type = requestcontent[0], requestcontent[1] - file, fileheaders = self._get_url_content(url) - if type == 'qweb-pdf': - response = self._make_pdf_response(file) - response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;') - elif type == 'controller': - response = request.make_response(file) - response.headers.add('Content-Disposition', fileheaders['Content-Disposition']) - response.headers.add('Content-Type', fileheaders['Content-Type']) + reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0] + + if '?' not in url: + # Generic report: + docids = url.split('/')[-1] + response = self.report_pdf(reportname, docids) + else: + # Particular report: + querystring = url.split('?')[1] + querystring = dict(urlparse.parse_qsl(querystring)) + response = self.report_pdf_particular(reportname, **querystring) + + response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname) + response.set_cookie('fileToken', token) + return response + elif type =='controller': + from werkzeug.test import Client + from werkzeug.wrappers import BaseResponse + from werkzeug.datastructures import Headers + reqheaders = Headers(request.httprequest.headers) + response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True) + response.set_cookie('fileToken', token) + return response else: return - response.headers.add('Content-Length', len(file)) - response.set_cookie('fileToken', token) - return response - - @http.route('/report/check_wkhtmltopdf', type='json', auth="user") + @route(['/report/check_wkhtmltopdf'], type='json', auth="user") def check_wkhtmltopdf(self): - """Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf - cannot be found, return False. + return request.registry['report']._check_wkhtmltopdf() + + def _eval_params(self, param): + """Parse a dict generated by the webclient (javascript) into a python dict. """ - try: - process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = process.communicate() - if err: - raise - - version = out.splitlines()[1].strip() - version = version.split(' ')[1] - - if LooseVersion(version) < LooseVersion('0.12.0'): - _logger.warning('Upgrade WKHTMLTOPDF to (at least) 0.12.0') - return 'upgrade' - - return True - except: - _logger.error('You need WKHTMLTOPDF to print a pdf version of this report.') - return False + if isinstance(param, dict): + for key, value in param.iteritems(): + if value.lower() == 'false': + param[key] = False + elif value.lower() == 'true': + param[key] = True + elif ',' in value: + param[key] = [int(i) for i in value.split(',')] + else: + try: + param[key] = int(value) + except (ValueError, TypeError): + pass + else: + if isinstance(param, (str, unicode)): + param = [int(i) for i in param.split(',')] + if isinstance(param, list): + param = list(set(param)) + if isinstance(param, int): + param = [param] + return param diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 8a2bc21b3b0..8abe8e544eb 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -19,54 +19,85 @@ # ############################################################################## -from openerp.addons.web.http import request from openerp.osv import osv -from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, config +from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field +import os import time +import psutil +import signal +import base64 +import logging +import tempfile +import lxml.html +import cStringIO +import subprocess from datetime import datetime - -from werkzeug.datastructures import Headers -from werkzeug.wrappers import BaseResponse -from werkzeug.test import Client +from functools import partial +from distutils.version import LooseVersion +try: + from pyPdf import PdfFileWriter, PdfFileReader +except ImportError: + PdfFileWriter = PdfFileReader = None -def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT): - return len((datetime.now()).strftime(date_format)) +_logger = logging.getLogger(__name__) -class report(osv.Model): +"""Check the presence of wkhtmltopdf and return its version.""" +wkhtmltopdf_state = 'install' +try: + process = subprocess.Popen( + ['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) +except OSError: + _logger.error('You need wkhtmltopdf to print a pdf version of the reports.') +else: + out, err = process.communicate() + version = out.splitlines()[1].strip() + version = version.split(' ')[1] + if LooseVersion(version) < LooseVersion('0.12.0'): + _logger.warning('Upgrade wkhtmltopdf to (at least) 0.12.0') + wkhtmltopdf_state = 'upgrade' + wkhtmltopdf_state = 'ok' + + +class Report(osv.Model): _name = "report" _description = "Report" public_user = None - def get_digits(self, obj=None, f=None, dp=None): + #-------------------------------------------------------------------------- + # Extension of ir_ui_view.render with arguments frequently used in reports + #-------------------------------------------------------------------------- + + def _get_digits(self, cr, uid, obj=None, f=None, dp=None): d = DEFAULT_DIGITS = 2 if dp: decimal_precision_obj = self.pool['decimal.precision'] - ids = decimal_precision_obj.search(request.cr, request.uid, [('name', '=', dp)]) + ids = decimal_precision_obj.search(cr, uid, [('name', '=', dp)]) if ids: - d = decimal_precision_obj.browse(request.cr, request.uid, ids)[0].digits + d = decimal_precision_obj.browse(cr, uid, ids)[0].digits elif obj and f: res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS))) if isinstance(res_digits, tuple): d = res_digits[1] else: - d = res_digits(request.cr)[1] + d = res_digits(cr)[1] elif (hasattr(obj, '_field') and isinstance(obj._field, (float_field, function_field)) and obj._field.digits): d = obj._field.digits[1] or DEFAULT_DIGITS return d - def _get_lang_dict(self): + def _get_lang_dict(self, cr, uid): pool_lang = self.pool['res.lang'] lang = self.localcontext.get('lang', 'en_US') or 'en_US' - lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0] - lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids) + lang_ids = pool_lang.search(cr, uid, [('code', '=', lang)])[0] + lang_obj = pool_lang.browse(cr, uid, lang_ids) lang_dict = { 'lang_obj': lang_obj, 'date_format': lang_obj.date_format, @@ -76,7 +107,7 @@ class report(osv.Model): self.default_lang[lang] = self.lang_dict.copy() return True - def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False): + def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False, cr=None, uid=None): """ Assuming 'Account' decimal.precision=3: formatLang(value) -> digits=2 (default) @@ -84,17 +115,20 @@ class report(osv.Model): formatLang(value, dp='Account') -> digits=3 formatLang(value, digits=5, dp='Account') -> digits=5 """ + def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT): + return len((datetime.now()).strftime(date_format)) + if digits is None: if dp: - digits = self.get_digits(dp=dp) + digits = self._get_digits(cr, uid, dp=dp) else: - digits = self.get_digits(value) + digits = self._get_digits(cr, uid, value) if isinstance(value, (str, unicode)) and not value: return '' if not self.lang_dict_called: - self._get_lang_dict() + self._get_lang_dict(cr, uid) self.lang_dict_called = True if date or date_time: @@ -117,9 +151,7 @@ class report(osv.Model): date = datetime(*value.timetuple()[:6]) if date_time: # Convert datetime values to the expected client/context timezone - date = datetime_field.context_timestamp(request.cr, request.uid, - timestamp=date, - context=self.localcontext) + date = datetime_field.context_timestamp(cr, uid, timestamp=date, context=self.localcontext) return date.strftime(date_format.encode('utf-8')) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) @@ -150,7 +182,7 @@ class report(osv.Model): 'tz': context.get('tz'), 'uid': context.get('uid'), } - self._get_lang_dict() + self._get_lang_dict(cr, uid) view_obj = self.pool['ir.ui.view'] @@ -177,51 +209,180 @@ class report(osv.Model): qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) return view_obj.render(cr, uid, template, qcontext, context=ctx) - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - - # Website independance code - website = False - res_company = current_user.company_id - - try: - website = request.website - res_company = request.website.company_id - except: - pass - values.update({ 'time': time, - 'user': current_user, - 'user_id': current_user.id, - 'formatLang': self.formatLang, - 'get_digits': self.get_digits, + 'formatLang': partial(self.formatLang, cr=cr, uid=uid), + 'get_digits': self._get_digits, 'render_doc': render_doc, - 'website': website, - 'res_company': res_company, + 'editable': True, # Will active inherit_branding + 'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id, + 'website': False, # Will be overidden by ir.ui.view if the request has website enabled }) return view_obj.render(cr, uid, template, values, context=context) - def get_pdf(self, report, record_id, context=None): - """Used to return the content of a generated PDF. + #-------------------------------------------------------------------------- + # Main reports methods + #-------------------------------------------------------------------------- - :returns: pdf + def get_html(self, cr, uid, ids, report_name, data=None, context=None): + """This method generates and returns html version of a report. """ - url = '/report/pdf/report/' + report.report_file + '/' + str(record_id) - reqheaders = Headers(request.httprequest.headers) - reqheaders.pop('Accept') - reqheaders.add('Accept', 'application/pdf') - reqheaders.pop('Content-Type') - reqheaders.add('Content-Type', 'text/plain') - response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, - follow_redirects=True) - return response.data + # If the report is using a custom model to render its html, we must use it. + # Otherwise, fallback on the generic html rendering. + try: + report_model_name = 'report.%s' % report_name + particularreport_obj = self.pool[report_model_name] + return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context) + except KeyError: + report = self._get_report_from_name(cr, uid, report_name) + report_obj = self.pool[report.model] + docs = report_obj.browse(cr, uid, ids, context=context) + docargs = { + 'doc_ids': ids, + 'doc_model': report.model, + 'docs': docs, + } + return self.render(cr, uid, [], report.report_name, docargs, context=context) + + def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None): + """This method generates and returns pdf version of a report. + """ + if context is None: + context = {} + + if html is None: + html = self.get_html(cr, uid, ids, report_name, data=data, context=context) + + html = html.decode('utf-8') + + # Get the ir.actions.report.xml record we are working on. + report = self._get_report_from_name(cr, uid, report_name) + + # Check attachment_use field. If set to true and an existing pdf is already saved, load + # this one now. Else, mark save it. + save_in_attachment = {} + + if report.attachment_use is True: + save_in_attachment['model'] = report.model + save_in_attachment['loaded_documents'] = {} + + for record_id in ids: + obj = self.pool[report.model].browse(cr, uid, record_id) + filename = eval(report.attachment, {'object': obj, 'time': time}) + + if filename is False: # May be false if, for instance, the record is in draft state + continue + else: + alreadyindb = [('datas_fname', '=', filename), + ('res_model', '=', report.model), + ('res_id', '=', record_id)] + + attach_ids = self.pool['ir.attachment'].search(cr, uid, alreadyindb) + if attach_ids: + # Add the loaded pdf in the loaded_documents list + pdf = self.pool['ir.attachment'].browse(cr, uid, attach_ids[0]).datas + pdf = base64.decodestring(pdf) + save_in_attachment['loaded_documents'][record_id] = pdf + _logger.info('The PDF document %s was loaded from the database' % filename) + else: + # Mark current document to be saved + save_in_attachment[id] = filename + + # Get the paperformat associated to the report, otherwise fallback on the company one. + if not report.paperformat_id: + user = self.pool['res.users'].browse(cr, uid, uid) + paperformat = user.company_id.paperformat_id + else: + paperformat = report.paperformat_id + + # Preparing the minimal html pages + #subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering + subst = " " + css = '' # Will contain local css + + headerhtml = [] + contenthtml = [] + footerhtml = [] + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + + minimalhtml = """ + + + + + + + + + + {subst} + + + {body} + +""" + + # The retrieved html report must be simplified. We convert it into a xml tree + # via lxml in order to extract headers, footers and content. + try: + root = lxml.html.fromstring(html) + + for node in root.xpath("//html/head/style"): + css += node.text + + for node in root.xpath("//div[@class='header']"): + body = lxml.html.tostring(node) + header = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url) + headerhtml.append(header) + + for node in root.xpath("//div[@class='footer']"): + body = lxml.html.tostring(node) + footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url) + footerhtml.append(footer) + + for node in root.xpath("//div[@class='page']"): + # Previously, we marked some reports to be saved in attachment via their ids, so we + # must set a relation between report ids and report's content. We use the QWeb + # branding in order to do so: searching after a node having a data-oe-model + # attribute with the value of the current report model and read its oe-id attribute + oemodelnode = node.find(".//*[@data-oe-model='%s']" % report.model) + if oemodelnode is not None: + reportid = oemodelnode.get('data-oe-id') + if reportid: + reportid = int(reportid) + else: + reportid = False + + body = lxml.html.tostring(node) + reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url) + contenthtml.append(tuple([reportid, reportcontent])) + + except lxml.etree.XMLSyntaxError: + contenthtml = [] + contenthtml.append(html) + save_in_attachment = {} # Don't save this potentially malformed document + + # Get paperformat arguments set in the root html tag. They are prioritized over + # paperformat-record arguments. + specific_paperformat_args = {} + for attribute in root.items(): + if attribute[0].startswith('data-report-'): + specific_paperformat_args[attribute[0]] = attribute[1] + + # Run wkhtmltopdf process + pdf = self._generate_wkhtml_pdf( + cr, uid, headerhtml, footerhtml, contenthtml, context.get('landscape'), + paperformat, specific_paperformat_args, save_in_attachment + ) + return pdf def get_action(self, cr, uid, ids, report_name, datas=None, context=None): - """Used to return an action of type ir.actions.report.xml. + """Return an action of type ir.actions.report.xml. :param report_name: Name of the template to generate an action for """ + # TODO: return the action for the ids passed in args if context is None: context = {} @@ -249,26 +410,203 @@ class report(osv.Model): return action - def eval_params(self, dict_param): - """Parse a dictionary generated by the webclient (javascript) into a dictionary - understandable by a wizard controller (python). - """ - for key, value in dict_param.iteritems(): - if value.lower() == 'false': - dict_param[key] = False - elif value.lower() == 'true': - dict_param[key] = True - elif ',' in value: - dict_param[key] = [int(i) for i in value.split(',')] - elif '%2C' in value: - dict_param[key] = [int(i) for i in value.split('%2C')] - else: - try: - i = int(value) - dict_param[key] = i - except (ValueError, TypeError): - pass + #-------------------------------------------------------------------------- + # Report generation helpers + #-------------------------------------------------------------------------- - data = {} - data['form'] = dict_param - return data + def _check_wkhtmltopdf(self): + return wkhtmltopdf_state + + def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None): + """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf + document. + + :param header: list of string containing the headers + :param footer: list of string containing the footers + :param bodies: list of string containing the reports + :param landscape: boolean to force the pdf to be rendered under a landscape format + :param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments + :param specific_paperformat_args: dict of prioritized paperformat arguments + :param save_in_attachment: dict of reports to save/load in/from the db + :returns: Content of the pdf as a string + """ + command = ['wkhtmltopdf'] + command_args = [] + tmp_dir = tempfile.gettempdir() + + # Passing the cookie to wkhtmltopdf in order to resolve URL. + try: + from openerp.addons.web.http import request + command_args.extend(['--cookie', 'session_id', request.session.sid]) + except AttributeError: + pass + + # Display arguments + if paperformat: + command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args)) + + if landscape and '--orientation' in command_args: + command_args_copy = list(command_args) + for index, elem in enumerate(command_args_copy): + if elem == '--orientation': + del command_args[index] + del command_args[index] + command_args.extend(['--orientation', 'landscape']) + elif landscape and not '--orientation' in command_args: + command_args.extend(['--orientation', 'landscape']) + + pdfdocuments = [] + # HTML to PDF thanks to WKhtmltopdf + for index, reporthtml in enumerate(bodies): + command_arg_local = [] + pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', + mode='w+b') + # Directly load the document if we have it + if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]): + pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0])) + pdfreport.flush() + pdfdocuments.append(pdfreport) + continue + + # Header stuff + if headers: + head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.', + dir=tmp_dir, mode='w+') + head_file.write(headers[index]) + head_file.flush() + command_arg_local.extend(['--header-html', head_file.name]) + + # Footer stuff + if footers: + foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.', + dir=tmp_dir, mode='w+') + foot_file.write(footers[index]) + foot_file.flush() + command_arg_local.extend(['--footer-html', foot_file.name]) + + # Body stuff + content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.', + dir=tmp_dir, mode='w+') + content_file.write(reporthtml[1]) + content_file.flush() + + try: + # If the server is running with only one worker, ask to create a secund to be able + # to serve the http request of wkhtmltopdf subprocess. + if config['workers'] == 1: + ppid = psutil.Process(os.getpid()).ppid + os.kill(ppid, signal.SIGTTIN) + + wkhtmltopdf = command + command_args + command_arg_local + wkhtmltopdf += [content_file.name] + [pdfreport.name] + + process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = process.communicate() + + if config['workers'] == 1: + os.kill(ppid, signal.SIGTTOU) + + if process.returncode != 0: + raise osv.except_osv(_('Report (PDF)'), + _('wkhtmltopdf failed with error code = %s. ' + 'Message: %s') % (str(process.returncode), err)) + + # Save the pdf in attachment if marked + if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]): + attachment = { + 'name': save_in_attachment.get(reporthtml[0]), + 'datas': base64.encodestring(pdfreport.read()), + 'datas_fname': save_in_attachment.get(reporthtml[0]), + 'res_model': save_in_attachment.get('model'), + 'res_id': reporthtml[0], + } + self.pool['ir.attachment'].create(cr, uid, attachment) + _logger.info('The PDF document %s is now saved in the ' + 'database' % attachment['name']) + + pdfreport.flush() + pdfdocuments.append(pdfreport) + + if headers: + head_file.close() + if footers: + foot_file.close() + except: + raise + + # Get and return the full pdf + if len(pdfdocuments) == 1: + content = pdfdocuments[0].read() + pdfdocuments[0].close() + else: + content = self._merge_pdf(pdfdocuments) + + return content + + def _get_report_from_name(self, cr, uid, report_name): + """Get the first record of ir.actions.report.xml having the ``report_name`` as value for + the field report_name. + """ + report_obj = self.pool['ir.actions.report.xml'] + qwebtypes = ['qweb-pdf', 'qweb-html'] + conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)] + idreport = report_obj.search(cr, uid, conditions)[0] + return report_obj.browse(cr, uid, idreport) + + def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None): + """Build arguments understandable by wkhtmltopdf from a report.paperformat record. + + :paperformat: report.paperformat record + :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments + :returns: list of string representing the wkhtmltopdf arguments + """ + command_args = [] + if paperformat.format and paperformat.format != 'custom': + command_args.extend(['--page-size', paperformat.format]) + + if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom': + command_args.extend(['--page-width', str(paperformat.page_width) + 'in']) + command_args.extend(['--page-height', str(paperformat.page_height) + 'in']) + + if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']: + command_args.extend(['--margin-top', + str(specific_paperformat_args['data-report-margin-top'])]) + elif paperformat.margin_top: + command_args.extend(['--margin-top', str(paperformat.margin_top)]) + + if paperformat.margin_left: + command_args.extend(['--margin-left', str(paperformat.margin_left)]) + if paperformat.margin_bottom: + command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)]) + if paperformat.margin_right: + command_args.extend(['--margin-right', str(paperformat.margin_right)]) + if paperformat.orientation: + command_args.extend(['--orientation', str(paperformat.orientation)]) + if paperformat.header_spacing: + command_args.extend(['--header-spacing', str(paperformat.header_spacing)]) + if paperformat.header_line: + command_args.extend(['--header-line']) + if paperformat.dpi: + command_args.extend(['--dpi', str(paperformat.dpi)]) + + return command_args + + def _merge_pdf(self, documents): + """Merge PDF files into one. + + :param documents: list of pdf files + :returns: string containing the merged pdf + """ + writer = PdfFileWriter() + for document in documents: + reader = PdfFileReader(file(document.name, "rb")) + for page in range(0, reader.getNumPages()): + writer.addPage(reader.getPage(page)) + document.close() + merged = cStringIO.StringIO() + writer.write(merged) + merged.seek(0) + content = merged.read() + merged.close() + return content diff --git a/addons/report/static/src/js/qwebactionmanager.js b/addons/report/static/src/js/qwebactionmanager.js index 8d1550a6e2c..b75003942d2 100644 --- a/addons/report/static/src/js/qwebactionmanager.js +++ b/addons/report/static/src/js/qwebactionmanager.js @@ -1,4 +1,5 @@ openerp.report = function(instance) { + var wkhtmltopdf_state; instance.web.ActionManager = instance.web.ActionManager.extend({ ir_actions_report_xml: function(action, options) { @@ -11,8 +12,7 @@ openerp.report = function(instance) { // QWeb reports if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) { - - var report_url = '' + var report_url = ''; switch (action.report_type) { case 'qweb-html': report_url = '/report/' + action.report_name; @@ -49,41 +49,49 @@ openerp.report = function(instance) { } if (action.report_type == 'qweb-html') { // Open the html report in a popup - window.open(report_url, '_blank', 'height=768,width=1024'); + window.open(report_url, '_blank', 'height=900,width=1280'); instance.web.unblockUI(); return; } else { - // Trigger the download of the pdf/custom controller report + // Trigger the download of the pdf/controller report var c = openerp.webclient.crashmanager; - var response = new Array() - response[0] = report_url - response[1] = action.report_type + var response = new Array(); + response[0] = report_url; + response[1] = action.report_type; - openerp.session.rpc('/report/check_wkhtmltopdf').then(function (presence) { - // Fallback of qweb-pdf if wkhtmltopdf is not installed - if (!presence && action.report_type == 'qweb-pdf') { - self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \ -system. The report will be shown in html.

\ -wkhtmltopdf.org'), true); - window.open(report_url.substring(12), '_blank', 'height=768,width=1024'); - instance.web.unblockUI(); - } - else { - if (presence == 'upgrade') { - self.do_notify(_t('Report'), _t('You should upgrade your version of\ - Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\ - support for table-breaking between pages.

wkhtmltopdf.org'), true); + if (action.report_type == 'qweb-pdf') { + (wkhtmltopdf_state = wkhtmltopdf_state || openerp.session.rpc('/report/check_wkhtmltopdf')).then(function (presence) { + // Fallback of qweb-pdf if wkhtmltopdf is not installed + if (presence == 'install' && action.report_type == 'qweb-pdf') { + self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \ + system. The report will be shown in html.

\ + wkhtmltopdf.org'), true); + window.open(report_url.substring(12), '_blank', 'height=768,width=1024'); + instance.web.unblockUI(); + } else { + if (presence == 'upgrade') { + self.do_notify(_t('Report'), _t('You should upgrade your version of\ + Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\ + support for table-breaking between pages.

wkhtmltopdf.org'), true); + } + self.session.get_file({ + url: '/report/download', + data: {data: JSON.stringify(response)}, + complete: openerp.web.unblockUI, + error: c.rpc_error.bind(c) + }); } - self.session.get_file({ - url: '/report/download', - data: {data: JSON.stringify(response)}, - complete: openerp.web.unblockUI, - error: c.rpc_error.bind(c) - }); - } - }); - } + }); + } else { + self.session.get_file({ + url: '/report/download', + data: {data: JSON.stringify(response)}, + complete: openerp.web.unblockUI, + error: c.rpc_error.bind(c) + }); + } + } } else { return self._super(action, options); } diff --git a/addons/report/tests/test_reports.py b/addons/report/tests/test_reports.py index 361e0a7aa0f..dda14442709 100644 --- a/addons/report/tests/test_reports.py +++ b/addons/report/tests/test_reports.py @@ -1,26 +1,36 @@ # -*- coding: utf-8 -*- -import logging +import logging import openerp -import urllib2 + _logger = logging.getLogger(__name__) -class TestReports(openerp.tests.HttpCase): + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) +class TestReports(openerp.tests.TransactionCase): def test_reports(self): + return # commented out until post_install tests are working + registry, cr, uid = self.registry, self.cr, self.uid r_model = registry('ir.actions.report.xml') - domain = [('report_type','like','qweb')] + domain = [('report_type', 'like', 'qweb')] for r in r_model.browse(cr, uid, r_model.search(cr, uid, domain)): + report_model = 'report.%s' % r.report_name + particular_model = registry('ir.model').search(cr, uid, [('model', '=', report_model)]) + + # Only test the generic reports here + if particular_model: + continue + + _logger.info("testing report %s", r.report_name) report_model = registry(r.model) report_model_ids = report_model.search(cr, uid, [], limit=10) if not report_model_ids: _logger.info("no record found skipping report %s", r.report_name) - continue if not r.multi: report_model_ids = report_model_ids[:1] - url = "/report/%s/%s" % (r.report_name, ','.join(str(i) for i in report_model_ids)) - _logger.info("testing report %s", url) - # TODO sle: uncomment this - #content = self.url_open(url) + # Test report generation + registry('report').get_html(cr, uid, report_model_ids, r.report_name) diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml index 73c5ce03244..588775fc41e 100644 --- a/addons/report/views/layouts.xml +++ b/addons/report/views/layouts.xml @@ -4,9 +4,7 @@ - - diff --git a/addons/website_report/views/layouts.xml b/addons/website_report/views/layouts.xml index 091bb8dd3db..f5deb483374 100644 --- a/addons/website_report/views/layouts.xml +++ b/addons/website_report/views/layouts.xml @@ -2,6 +2,11 @@