[REF][IMP] report module: merge the four report's routes into one; extra report arguments are passed json encoded (therefore removed the _eval_params method); added an abstract_report model to wrap old-style report without touching them; Removed formatLang method from report as it is embeded in the old-style report localcontext; moved the save_in_attachment logic in a method for readability; adapted the action manager to encode data and context of action if needed; fixed the post install test to test the generic report

bzr revid: sle@openerp.com-20140402162344-3lrako0jepmhasvl
This commit is contained in:
Simon Lejeune
2014-04-02 18:23:44 +02:00
parent 8bf8e8a01c
commit 2c90fad7ac
6 changed files with 227 additions and 319 deletions
+40 -75
View File
@@ -22,48 +22,43 @@
from openerp.addons.web.http import Controller, route, request from openerp.addons.web.http import Controller, route, request
import simplejson import simplejson
import urlparse from werkzeug import exceptions, url_decode
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 from reportlab.graphics.barcode import createBarcodeDrawing
class ReportController(Controller): class ReportController(Controller):
#------------------------------------------------------ #------------------------------------------------------
# Generic reports controller # Report controllers
#------------------------------------------------------ #------------------------------------------------------
@route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True) @route([
def report_html(self, reportname, docids): '/report/<path:converter>/<reportname>',
cr, uid, context = request.cr, request.uid, request.context '/report/<path:converter>/<reportname>/<docids>',
docids = self._eval_params(docids) ], type='http', auth='user', website=True, multilang=True)
return request.registry['report'].get_html(cr, uid, docids, reportname, context=context) def report_routes(self, reportname, docids=None, converter=None, **data):
@route('/report/pdf/report/<reportname>/<docids>', 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)
#------------------------------------------------------
# Particular reports controller
#------------------------------------------------------
@route('/report/<reportname>', 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'] report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing
return report_obj.get_html(cr, uid, [], reportname, data=data, context=context)
@route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_pdf_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing if docids:
pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context) docids = [int(i) for i in docids.split(',')]
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] options_data = None
return request.make_response(pdf, headers=pdfhttpheaders) if data.get('options'):
options_data = simplejson.loads(data['options'])
if data.get('context'):
context.update(simplejson.loads(data['context']))
if converter == 'html':
html = report_obj.get_html(cr, uid, docids, reportname, data=options_data, context=context)
return request.make_response(html)
elif converter == 'pdf':
pdf = report_obj.get_pdf(cr, uid, docids, reportname, data=options_data, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
else:
raise exceptions.HTTPException(description='Converter %s not implemented.' % converter)
#------------------------------------------------------ #------------------------------------------------------
# Misc. route utils # Misc. route utils
@@ -94,7 +89,7 @@ class ReportController(Controller):
@route(['/report/download'], type='http', auth="user", website=True) @route(['/report/download'], type='http', auth="user", website=True)
def report_download(self, data, token): def report_download(self, data, token):
"""This function is used by 'qwebactionmanager.js' in order to trigger the download of """This function is used by 'qwebactionmanager.js' in order to trigger the download of
a pdf report. a pdf/controller report.
:param data: a javascript array JSON.stringified containg report internal url ([0]) and :param data: a javascript array JSON.stringified containg report internal url ([0]) and
type [1] type [1]
@@ -102,26 +97,26 @@ class ReportController(Controller):
""" """
requestcontent = simplejson.loads(data) requestcontent = simplejson.loads(data)
url, type = requestcontent[0], requestcontent[1] url, type = requestcontent[0], requestcontent[1]
if type == 'qweb-pdf':
reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0]
if '?' not in url: if type == 'qweb-pdf':
reportname = url.split('/report/pdf/')[1].split('?')[0]
docids = None
if '/' in reportname:
reportname, docids = reportname.split('/')
if docids:
# Generic report: # Generic report:
docids = url.split('/')[-1] response = self.report_routes(reportname, docids=docids, converter='pdf')
response = self.report_pdf(reportname, docids)
else: else:
# Particular report: # Particular report:
querystring = url.split('?')[1] data = url_decode(url.split('?')[1]).items() # decoding the args represented in JSON
querystring = dict(urlparse.parse_qsl(querystring)) response = self.report_routes(reportname, converter='pdf', **dict(data))
response = self.report_pdf_particular(reportname, **querystring)
response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname) response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname)
response.set_cookie('fileToken', token) response.set_cookie('fileToken', token)
return response return response
elif type =='controller': elif type =='controller':
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
reqheaders = Headers(request.httprequest.headers) reqheaders = Headers(request.httprequest.headers)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True) response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True)
response.set_cookie('fileToken', token) response.set_cookie('fileToken', token)
@@ -132,33 +127,3 @@ class ReportController(Controller):
@route(['/report/check_wkhtmltopdf'], type='json', auth="user") @route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self): def check_wkhtmltopdf(self):
return request.registry['report']._check_wkhtmltopdf() return request.registry['report']._check_wkhtmltopdf()
def _eval_params(self, param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
"""
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:
try:
param[key] = [int(i) for i in value.split(',')]
except ValueError:
param[key] = value.split(',')
if len(param[key]) == 1:
param[key] = value
else:
try:
param[key] = int(value)
except (ValueError, TypeError):
continue
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
+1
View File
@@ -1,2 +1,3 @@
import report import report
import report_paperformat import report_paperformat
import abstract_report
+60
View File
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2014-Today OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import osv
class AbstractReport(osv.AbstractModel):
"""Model used to embed old style reports"""
_name = 'report.abstract_report'
_template = None
_wrapped_report_class = None
def render_html(self, cr, uid, ids, data=None, context=None):
if context is None:
context = {}
# If the key 'landscape' is present in data['form'], passing it into the context
if data and data.get('form', {}).get('landscape'):
context['landscape'] = True
if context and context.get('active_ids'):
# Browse the selected objects via their reference in context
model = context.get('active_model') or context.get('model')
objects_model = self.pool[model]
objects = objects_model.browse(cr, uid, context['active_ids'], context=context)
else:
# If no context is set (for instance, during test execution), build one
model = self.pool['report']._get_report_from_name(cr, uid, self._template).model
objects_model = self.pool[model]
objects = objects_model.browse(cr, uid, ids, context=context)
context['active_model'] = model
context['active_ids'] = ids
# Generate the old style report
wrapped_report = self._wrapped_report_class(cr, uid, '', context=context)
wrapped_report.set_context(objects, data, ids)
# Rendering self._template with the wrapped report instance localcontext as
# rendering environment
docargs = wrapped_report.localcontext
docargs['docs'] = docargs.get('objects')
return self.pool['report'].render(cr, uid, [], self._template, docargs, context=context)
+76 -192
View File
@@ -20,9 +20,8 @@
############################################################################## ##############################################################################
from openerp.osv import osv from openerp.osv import osv
from openerp.tools import config
from openerp.tools.translate import _ 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 os
import time import time
@@ -34,8 +33,6 @@ import tempfile
import lxml.html import lxml.html
import cStringIO import cStringIO
import subprocess import subprocess
from datetime import datetime
from functools import partial
from distutils.version import LooseVersion from distutils.version import LooseVersion
try: try:
from pyPdf import PdfFileWriter, PdfFileReader from pyPdf import PdfFileWriter, PdfFileReader
@@ -46,7 +43,7 @@ except ImportError:
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
"""Check the presence of wkhtmltopdf and return its version.""" """Check the presence of wkhtmltopdf and return its version at OpnerERP start-up."""
wkhtmltopdf_state = 'install' wkhtmltopdf_state = 'install'
try: try:
process = subprocess.Popen( process = subprocess.Popen(
@@ -70,106 +67,26 @@ class Report(osv.Model):
public_user = None public_user = None
MINIMAL_HTML_PAGE = """
<base href="{base_url}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{css}</style>
{subst}
</head>
<body class="container" onload="subst()">
{body}
</body>
</html>"""
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
# Extension of ir_ui_view.render with arguments frequently used in reports # Extension of ir_ui_view.render with arguments frequently used in reports
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
def _get_digits(self, cr=None, uid=None, obj=None, f=None, dp=None):
d = DEFAULT_DIGITS = 2
if dp:
decimal_precision_obj = self.pool['decimal.precision']
ids = decimal_precision_obj.search(cr, uid, [('name', '=', dp)])
if ids:
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(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, cr, uid):
pool_lang = self.pool['res.lang']
lang = self.localcontext.get('lang', 'en_US') or 'en_US'
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,
'time_format': lang_obj.time_format
}
self.lang_dict.update(lang_dict)
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, cr=None, uid=None):
"""
Assuming 'Account' decimal.precision=3:
formatLang(value) -> digits=2 (default)
formatLang(value, digits=4) -> digits=4
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))
# In case we use formatLang on the model (and not in the rendering environment).
if not hasattr(self, 'land_ditct'):
self.localcontext = {}
self.lang_dict = {}
self.default_lang = {}
self._get_lang_dict(cr, uid)
self.lang_dict_called = True
if digits is None:
if dp:
digits = self._get_digits(cr, uid, dp=dp)
else:
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(cr, uid)
self.lang_dict_called = True
if date or date_time:
if not str(value):
return ''
date_format = self.lang_dict['date_format']
parse_format = DEFAULT_SERVER_DATE_FORMAT
if date_time:
value = value.split('.')[0]
date_format = date_format + " " + self.lang_dict['time_format']
parse_format = DEFAULT_SERVER_DATETIME_FORMAT
if isinstance(value, basestring):
# FIXME: the trimming is probably unreliable if format includes day/month names
# and those would need to be translated anyway.
date = datetime.strptime(value[:get_date_length(parse_format)], parse_format)
elif isinstance(value, time.struct_time):
date = datetime(*value[:6])
else:
date = datetime(*value.timetuple()[:6])
if date_time:
# Convert datetime values to the expected client/context timezone
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)
if currency_obj:
if currency_obj.position == 'after':
res = '%s %s' % (res, currency_obj.symbol)
elif currency_obj and currency_obj.position == 'before':
res = '%s %s' % (currency_obj.symbol, res)
return res
def render(self, cr, uid, ids, template, values=None, context=None): def render(self, cr, uid, ids, template, values=None, context=None):
"""Allow to render a QWeb template python-side. This function returns the 'ir.ui.view' """Allow to render a QWeb template python-side. This function returns the 'ir.ui.view'
render but embellish it with some variables/methods used in reports. render but embellish it with some variables/methods used in reports.
@@ -183,15 +100,6 @@ class Report(osv.Model):
if context is None: if context is None:
context = {} context = {}
self.lang_dict = self.default_lang = {}
self.lang_dict_called = False
self.localcontext = {
'lang': context.get('lang'),
'tz': context.get('tz'),
'uid': context.get('uid'),
}
self._get_lang_dict(cr, uid)
view_obj = self.pool['ir.ui.view'] view_obj = self.pool['ir.ui.view']
def render_doc(doc_id, model, template): def render_doc(doc_id, model, template):
@@ -217,22 +125,20 @@ class Report(osv.Model):
qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx)
return view_obj.render(cr, uid, template, qcontext, context=ctx) return view_obj.render(cr, uid, template, qcontext, context=ctx)
user = self.pool['res.users'].browse(cr, uid, uid)
values.update({ values.update({
'time': time, 'time': time,
'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'get_digits': partial(self._get_digits, cr=cr, uid=uid),
'render_doc': render_doc, 'render_doc': render_doc,
'editable': True, # Will active inherit_branding 'editable': True, # Will active inherit_branding
'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id, 'user': user,
'res_company': user.company_id,
'website': False, # Will be overidden by ir.ui.view if the request has website enabled '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) return view_obj.render(cr, uid, template, values, context=context)
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
# Main reports methods # Main report methods
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
def get_html(self, cr, uid, ids, report_name, data=None, context=None): def get_html(self, cr, uid, ids, report_name, data=None, context=None):
"""This method generates and returns html version of a report. """This method generates and returns html version of a report.
""" """
@@ -241,7 +147,7 @@ class Report(osv.Model):
try: try:
report_model_name = 'report.%s' % report_name report_model_name = 'report.%s' % report_name
particularreport_obj = self.pool[report_model_name] particularreport_obj = self.pool[report_model_name]
return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context) return particularreport_obj.render_html(cr, uid, ids, data=data, context=context)
except KeyError: except KeyError:
report = self._get_report_from_name(cr, uid, report_name) report = self._get_report_from_name(cr, uid, report_name)
report_obj = self.pool[report.model] report_obj = self.pool[report.model]
@@ -262,41 +168,12 @@ class Report(osv.Model):
if html is None: if html is None:
html = self.get_html(cr, uid, ids, report_name, data=data, context=context) html = self.get_html(cr, uid, ids, report_name, data=data, context=context)
html = html.decode('utf-8') html = html.decode('utf-8') # Ensure the current document is utf-8 encoded.
# Get the ir.actions.report.xml record we are working on. # Get the ir.actions.report.xml record we are working on.
report = self._get_report_from_name(cr, uid, report_name) report = self._get_report_from_name(cr, uid, report_name)
# Check if we have to save the report or if we have to get one from the db.
# Check attachment_use field. If set to true and an existing pdf is already saved, load save_in_attachment = self._check_attachment_use(cr, uid, ids, report)
# 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. # Get the paperformat associated to the report, otherwise fallback on the company one.
if not report.paperformat_id: if not report.paperformat_id:
user = self.pool['res.users'].browse(cr, uid, uid) user = self.pool['res.users'].browse(cr, uid, uid)
@@ -305,34 +182,15 @@ class Report(osv.Model):
paperformat = report.paperformat_id paperformat = report.paperformat_id
# Preparing the minimal html pages # Preparing the minimal html pages
#subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
subst = "<script src='/report/static/src/js/subst.js'></script> " subst = "<script src='/report/static/src/js/subst.js'></script> "
css = '' # Will contain local css css = '' # Will contain local css
headerhtml = [] headerhtml = []
contenthtml = [] contenthtml = []
footerhtml = [] footerhtml = []
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """ # The received html report must be simplified. We convert it in a xml tree
<base href="{base_url}"> # in order to extract headers, bodies and footers.
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{css}</style>
{subst}
</head>
<body class="container" onload='subst()'>
{body}
</body>
</html>"""
# 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: try:
root = lxml.html.fromstring(html) root = lxml.html.fromstring(html)
@@ -341,12 +199,12 @@ class Report(osv.Model):
for node in root.xpath("//div[@class='header']"): for node in root.xpath("//div[@class='header']"):
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
header = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url) header = self.MINIMAL_HTML_PAGE.format(css=css, subst=subst, body=body, base_url=base_url)
headerhtml.append(header) headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"): for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url) footer = self.MINIMAL_HTML_PAGE.format(css=css, subst=subst, body=body, base_url=base_url)
footerhtml.append(footer) footerhtml.append(footer)
for node in root.xpath("//div[@class='page']"): for node in root.xpath("//div[@class='page']"):
@@ -363,7 +221,7 @@ class Report(osv.Model):
reportid = False reportid = False
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url) reportcontent = self.MINIMAL_HTML_PAGE.format(css=css, subst='', body=body, base_url=base_url)
contenthtml.append(tuple([reportid, reportcontent])) contenthtml.append(tuple([reportid, reportcontent]))
except lxml.etree.XMLSyntaxError: except lxml.etree.XMLSyntaxError:
@@ -390,37 +248,60 @@ class Report(osv.Model):
:param report_name: Name of the template to generate an action for :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: if context is None:
context = {} context = {}
if data is None: report_obj = self.pool['ir.actions.report.xml']
data = {}
report_obj = self.pool.get('ir.actions.report.xml')
idreport = report_obj.search(cr, uid, [('report_name', '=', report_name)], context=context) idreport = report_obj.search(cr, uid, [('report_name', '=', report_name)], context=context)
try: try:
report = report_obj.browse(cr, uid, idreport[0], context=context) report = report_obj.browse(cr, uid, idreport[0], context=context)
except IndexError: except IndexError:
raise osv.except_osv(_('Bad Report'), raise osv.except_osv(_('Bad Report'), _('This report is not loaded into the database.'))
_('This report is not loaded into the database.'))
action = { action = {
'context': context,
'data': data,
'type': 'ir.actions.report.xml', 'type': 'ir.actions.report.xml',
'report_name': report.report_name, 'report_name': report.report_name,
'report_type': report.report_type, 'report_type': report.report_type,
'report_file': report.report_file, 'report_file': report.report_file,
} }
if data:
action['datas'] = data
return action return action
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
# Report generation helpers # Report generation helpers
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
def _check_attachment_use(self, cr, uid, ids, report):
""" 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[record_id] = filename
return save_in_attachment
def _check_wkhtmltopdf(self): def _check_wkhtmltopdf(self):
return wkhtmltopdf_state return wkhtmltopdf_state
@@ -445,7 +326,8 @@ class Report(osv.Model):
# Passing the cookie to wkhtmltopdf in order to resolve URL. # Passing the cookie to wkhtmltopdf in order to resolve URL.
try: try:
from openerp.addons.web.http import request from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.session.sid]) if request:
command_args.extend(['--cookie', 'session_id', request.session.sid])
except AttributeError: except AttributeError:
pass pass
@@ -453,6 +335,8 @@ class Report(osv.Model):
if paperformat: if paperformat:
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args)) command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
command_args.extend(['--load-error-handling', 'ignore'])
if landscape and '--orientation' in command_args: if landscape and '--orientation' in command_args:
command_args_copy = list(command_args) command_args_copy = list(command_args)
for index, elem in enumerate(command_args_copy): for index, elem in enumerate(command_args_copy):
@@ -472,7 +356,7 @@ class Report(osv.Model):
# Directly load the document if we have it # Directly load the document if we have it
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]): if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0])) pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
pdfreport.flush() pdfreport.seek(0)
pdfdocuments.append(pdfreport) pdfdocuments.append(pdfreport)
continue continue
@@ -481,7 +365,7 @@ class Report(osv.Model):
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.', head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
dir=tmp_dir, mode='w+') dir=tmp_dir, mode='w+')
head_file.write(headers[index]) head_file.write(headers[index])
head_file.flush() head_file.seek(0)
command_arg_local.extend(['--header-html', head_file.name]) command_arg_local.extend(['--header-html', head_file.name])
# Footer stuff # Footer stuff
@@ -489,14 +373,14 @@ class Report(osv.Model):
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.', foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
dir=tmp_dir, mode='w+') dir=tmp_dir, mode='w+')
foot_file.write(footers[index]) foot_file.write(footers[index])
foot_file.flush() foot_file.seek(0)
command_arg_local.extend(['--footer-html', foot_file.name]) command_arg_local.extend(['--footer-html', foot_file.name])
# Body stuff # Body stuff
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.', content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
dir=tmp_dir, mode='w+') dir=tmp_dir, mode='w+')
content_file.write(reporthtml[1]) content_file.write(reporthtml[1])
content_file.flush() content_file.seek(0)
try: try:
# If the server is running with only one worker, ask to create a secund to be able # If the server is running with only one worker, ask to create a secund to be able
@@ -533,7 +417,7 @@ class Report(osv.Model):
_logger.info('The PDF document %s is now saved in the ' _logger.info('The PDF document %s is now saved in the '
'database' % attachment['name']) 'database' % attachment['name'])
pdfreport.flush() pdfreport.seek(0)
pdfdocuments.append(pdfreport) pdfdocuments.append(pdfreport)
if headers: if headers:
@@ -6,8 +6,6 @@ openerp.report = function(instance) {
var self = this; var self = this;
instance.web.blockUI(); instance.web.blockUI();
action = _.clone(action); action = _.clone(action);
var eval_contexts = ([instance.session.user_context] || []).concat([action.context]);
action.context = instance.web.pyeval.eval('contexts',eval_contexts);
_t = instance.web._t; _t = instance.web._t;
// QWeb reports // QWeb reports
@@ -15,43 +13,33 @@ openerp.report = function(instance) {
var report_url = ''; var report_url = '';
switch (action.report_type) { switch (action.report_type) {
case 'qweb-html': case 'qweb-html':
report_url = '/report/' + action.report_name; report_url = '/report/html/' + action.report_name;
break; break;
case 'qweb-pdf': case 'qweb-pdf':
report_url = '/report/pdf/report/' + action.report_name; report_url = '/report/pdf/' + action.report_name;
break; break;
case 'controller': case 'controller':
report_url = action.report_file; report_url = action.report_file;
break; break;
default: default:
report_url = '/report/' + action.report_name; report_url = '/report/html/' + action.report_name;
break; break;
} }
// single/multiple id(s): no query string // generic report: no query string
// wizard: query string of action.datas.form // particular: query string of action.data.form and context
if (!('datas' in action)) { if (!('data' in action) || !(action.data)) {
if ('active_ids' in action.context) { if ('active_ids' in action.context) {
report_url += "/" + action.context.active_ids.join(','); report_url += "/" + action.context.active_ids.join(',');
} }
} else { } else {
_.each(action.datas.form, function(value, key) { report_url += "?options=" + encodeURI(JSON.stringify(action.data));
// will be erased when all wizards are rewritten report_url += "&context=" + encodeURI(JSON.stringify(action.context));
if (key.substring(0, 12) === 'used_context') {
delete action.datas.form[key];
}
if ($.type(value) === 'array') {
action.datas.form[key] = value.join(',');
}
});
report_url += "?" + $.param(action.datas.form);
} }
if (action.report_type == 'qweb-html') { if (action.report_type == 'qweb-html') {
// Open the html report in a popup
window.open(report_url, '_blank', 'height=900,width=1280'); window.open(report_url, '_blank', 'height=900,width=1280');
instance.web.unblockUI(); instance.web.unblockUI();
return;
} else { } else {
// Trigger the download of the pdf/controller report // Trigger the download of the pdf/controller report
var c = openerp.webclient.crashmanager; var c = openerp.webclient.crashmanager;
@@ -68,6 +56,7 @@ openerp.report = function(instance) {
wkhtmltopdf.org</a>'), true); wkhtmltopdf.org</a>'), true);
window.open(report_url.substring(12), '_blank', 'height=768,width=1024'); window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
instance.web.unblockUI(); instance.web.unblockUI();
return;
} else { } else {
if (presence == 'upgrade') { if (presence == 'upgrade') {
self.do_notify(_t('Report'), _t('You should upgrade your version of\ self.do_notify(_t('Report'), _t('You should upgrade your version of\
@@ -75,22 +64,15 @@ openerp.report = function(instance) {
support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \ support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \
target="_blank">wkhtmltopdf.org</a>'), true); target="_blank">wkhtmltopdf.org</a>'), true);
} }
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({ self.session.get_file({
url: '/report/download', url: '/report/download',
data: {data: JSON.stringify(response)}, data: {data: JSON.stringify(response)},
complete: openerp.web.unblockUI, complete: openerp.web.unblockUI,
error: c.rpc_error.bind(c) error: c.rpc_error.bind(c)
}); });
}
} }
} else { } else {
return self._super(action, options); return self._super(action, options);
+33 -17
View File
@@ -1,5 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2014-Today OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging import logging
import openerp import openerp
@@ -11,26 +29,24 @@ _logger = logging.getLogger(__name__)
@openerp.tests.common.post_install(True) @openerp.tests.common.post_install(True)
class TestReports(openerp.tests.TransactionCase): class TestReports(openerp.tests.TransactionCase):
def test_reports(self): def test_reports(self):
return # commented out until post_install tests are working
registry, cr, uid = self.registry, self.cr, self.uid registry, cr, uid = self.registry, self.cr, self.uid
r_model = registry('ir.actions.report.xml') 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)): for r in r_model.browse(cr, uid, r_model.search(cr, uid, domain)):
report_model = 'report.%s' % r.report_name report_model = 'report.%s' % r.report_name
particular_model = registry('ir.model').search(cr, uid, [('model', '=', report_model)]) try:
registry(report_model)
except KeyError:
# Only test the generic reports here # Only test the generic reports here
if particular_model: _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)
if not r.multi:
report_model_ids = report_model_ids[:1]
# Test report generation
registry('report').get_html(cr, uid, report_model_ids, r.report_name)
else:
continue 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)
if not r.multi:
report_model_ids = report_model_ids[:1]
# Test report generation
registry('report').get_html(cr, uid, report_model_ids, r.report_name)