[MERGE] reports: make it possible to use only XML declaration, even with custom parsers.
This makes report registration more uniform, and report rendering be possible through the ORM (and thus the model service, instead of the report service). To register a report, always use the database, i.e. a <report> tag within an XML file. The custom parser, if any, will be specified in the database. Previously specify a custom parser, the report was declared in Python. Each model exposes a print_report() method, which will take the report name (as many report can be defined on a single model) in argument. bzr revid: vmt@openerp.com-20130327161129-6e7jz7l3lx7z1t18
This commit is contained in:
commit
23d672dfc3
|
@ -13,3 +13,4 @@ Modules
|
|||
03_module_dev_04
|
||||
03_module_dev_05
|
||||
03_module_dev_06
|
||||
report-declaration
|
||||
|
|
|
@ -6,6 +6,9 @@ Changelog
|
|||
`trunk`
|
||||
-------
|
||||
|
||||
- Almost removed ``LocalService()``. For reports,
|
||||
``openerp.osv.orm.Model.print_report()`` can be used. For workflows, see
|
||||
:ref:`orm-workflows`.
|
||||
- Removed support for the ``NET-RPC`` protocol.
|
||||
- Added the :ref:`Long polling <longpolling-worker>` worker type.
|
||||
- Added :ref:`orm-workflows` to the ORM.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
.. _report-declaration:
|
||||
|
||||
Report declaration
|
||||
==================
|
||||
|
||||
.. versionadded:: 7.1
|
||||
|
||||
Before version 7.1, report declaration could be done in two different ways:
|
||||
either via a ``<report>`` tag in XML, or via such a tag and a class
|
||||
instanciation in a Python module. Instanciating a class in a Python module was
|
||||
necessary when a custom parser was used.
|
||||
|
||||
In version 7.1, the recommended way to register a report is to use only the
|
||||
``<report>`` XML tag. The tag can now support an additional ``parser``
|
||||
attribute. The value for that attibute must be a fully-qualified class name,
|
||||
without the leading ``openerp.addons.`` namespace.
|
||||
|
||||
.. note::
|
||||
The rational to deprecate the manual class instanciation is to make all
|
||||
reports visible in the database, have a unique way to declare reports
|
||||
instead of two, and remove the need to maintain a registry of reports in
|
||||
memory.
|
||||
|
|
@ -20,11 +20,13 @@
|
|||
##############################################################################
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
from socket import gethostname
|
||||
import time
|
||||
|
||||
import openerp
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import netsvc, tools
|
||||
from openerp.osv import fields, osv
|
||||
|
@ -85,26 +87,45 @@ class report_xml(osv.osv):
|
|||
res[report.id] = False
|
||||
return res
|
||||
|
||||
def register_all(self, cr):
|
||||
"""Report registration handler that may be overridden by subclasses to
|
||||
add their own kinds of report services.
|
||||
Loads all reports with no manual loaders (auto==True) and
|
||||
registers the appropriate services to implement them.
|
||||
def _lookup_report(self, cr, name):
|
||||
"""
|
||||
Look up a report definition.
|
||||
"""
|
||||
opj = os.path.join
|
||||
cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
|
||||
result = cr.dictfetchall()
|
||||
reports = openerp.report.interface.report_int._reports
|
||||
for r in result:
|
||||
if reports.has_key('report.'+r['report_name']):
|
||||
continue
|
||||
if r['report_rml'] or r['report_rml_content_data']:
|
||||
report_sxw('report.'+r['report_name'], r['model'],
|
||||
opj('addons',r['report_rml'] or '/'), header=r['header'])
|
||||
if r['report_xsl']:
|
||||
report_rml('report.'+r['report_name'], r['model'],
|
||||
opj('addons',r['report_xml']),
|
||||
r['report_xsl'] and opj('addons',r['report_xsl']))
|
||||
|
||||
# First lookup in the deprecated place, because if the report definition
|
||||
# has not been updated, it is more likely the correct definition is there.
|
||||
# Only reports with custom parser sepcified in Python are still there.
|
||||
if 'report.' + name in openerp.report.interface.report_int._reports:
|
||||
new_report = openerp.report.interface.report_int._reports['report.' + name]
|
||||
else:
|
||||
cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
|
||||
r = cr.dictfetchone()
|
||||
if r:
|
||||
if r['report_rml'] or r['report_rml_content_data']:
|
||||
if r['parser']:
|
||||
kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
|
||||
else:
|
||||
kwargs = {}
|
||||
new_report = report_sxw('report.'+r['report_name'], r['model'],
|
||||
opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs)
|
||||
elif r['report_xsl']:
|
||||
new_report = report_rml('report.'+r['report_name'], r['model'],
|
||||
opj('addons',r['report_xml']),
|
||||
r['report_xsl'] and opj('addons',r['report_xsl']), register=False)
|
||||
else:
|
||||
raise Exception, "Unhandled report type: %s" % r
|
||||
else:
|
||||
raise Exception, "Required report does not exist: %s" % r
|
||||
|
||||
return new_report
|
||||
|
||||
def render_report(self, cr, uid, res_ids, name, data, context=None):
|
||||
"""
|
||||
Look up a report definition and render the report for the provided IDs.
|
||||
"""
|
||||
new_report = self._lookup_report(cr, name)
|
||||
return new_report.create(cr, uid, res_ids, data, context)
|
||||
|
||||
_name = 'ir.actions.report.xml'
|
||||
_inherit = 'ir.actions.actions'
|
||||
|
@ -140,6 +161,7 @@ class report_xml(osv.osv):
|
|||
'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
|
||||
'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
|
||||
|
||||
'parser': fields.char('Parser Class'),
|
||||
}
|
||||
_defaults = {
|
||||
'type': 'ir.actions.report.xml',
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
<group string="Miscellaneous">
|
||||
<field name="multi"/>
|
||||
<field name="auto"/>
|
||||
<field name="parser"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
|
|
@ -26,6 +26,8 @@ additional code is needed throughout the core library. This module keeps
|
|||
track of those specific measures by providing variables that can be unset
|
||||
by the user to check if her code is future proof.
|
||||
|
||||
In a perfect world, all these variables are set to False, the corresponding
|
||||
code removed, and thus these variables made unnecessary.
|
||||
"""
|
||||
|
||||
# If True, the Python modules inside the openerp namespace are made available
|
||||
|
@ -35,4 +37,20 @@ by the user to check if her code is future proof.
|
|||
# Change to False around 2013.02.
|
||||
open_openerp_namespace = False
|
||||
|
||||
# If True, openerp.netsvc.LocalService() can be used to lookup reports or to
|
||||
# access openerp.workflow.
|
||||
# Introduced around 2013.03.
|
||||
# Among the related code:
|
||||
# - The openerp.netsvc.LocalService() function.
|
||||
# - The openerp.report.interface.report_int._reports dictionary.
|
||||
# - The register attribute in openerp.report.interface.report_int (and in its
|
||||
# - auto column in ir.actions.report.xml.
|
||||
# inheriting classes).
|
||||
allow_local_service = True
|
||||
|
||||
# Applies for the register attribute in openerp.report.interface.report_int.
|
||||
# See comments for allow_local_service above.
|
||||
# Introduced around 2013.03.
|
||||
allow_report_int_registration = True
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -107,6 +107,7 @@
|
|||
<rng:optional><rng:attribute name="sxw"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="xml"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="xsl"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="parser"/></rng:optional>
|
||||
<rng:optional> <rng:attribute name="auto" /> </rng:optional>
|
||||
<rng:optional> <rng:attribute name="header" /> </rng:optional>
|
||||
<rng:optional> <rng:attribute name="webkit_header" /> </rng:optional>
|
||||
|
|
|
@ -36,8 +36,6 @@ from openerp.tools.safe_eval import safe_eval as eval
|
|||
import openerp.pooler as pooler
|
||||
from openerp.tools.translate import _
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
|
||||
import zipfile
|
||||
import openerp.release as release
|
||||
|
||||
|
|
|
@ -36,8 +36,6 @@ from openerp.tools.safe_eval import safe_eval as eval
|
|||
import openerp.pooler as pooler
|
||||
from openerp.tools.translate import _
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
|
||||
import zipfile
|
||||
import openerp.release as release
|
||||
|
||||
|
|
|
@ -225,7 +225,6 @@ class RegistryManager(object):
|
|||
try:
|
||||
Registry.setup_multi_process_signaling(cr)
|
||||
registry.do_parent_store(cr)
|
||||
registry.get('ir.actions.report.xml').register_all(cr)
|
||||
cr.commit()
|
||||
finally:
|
||||
cr.close()
|
||||
|
|
|
@ -21,11 +21,9 @@
|
|||
##############################################################################
|
||||
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import platform
|
||||
import release
|
||||
import sys
|
||||
import threading
|
||||
|
@ -46,12 +44,30 @@ import openerp
|
|||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def LocalService(name):
|
||||
# Special case for addons support, will be removed in a few days when addons
|
||||
# are updated to directly use openerp.osv.osv.service.
|
||||
"""
|
||||
The openerp.netsvc.LocalService() function is deprecated. It still works
|
||||
in two cases: workflows and reports. For workflows, instead of using
|
||||
LocalService('workflow'), openerp.workflow should be used (better yet,
|
||||
methods on openerp.osv.orm.Model should be used). For reports,
|
||||
openerp.report.render_report() should be used (methods on the Model should
|
||||
be provided too in the future).
|
||||
"""
|
||||
assert openerp.conf.deprecation.allow_local_service
|
||||
_logger.warning("LocalService() is deprecated since march 2013 (it was called with '%s')." % name)
|
||||
|
||||
if name == 'workflow':
|
||||
return openerp.workflow
|
||||
|
||||
return openerp.report.interface.report_int._reports[name]
|
||||
if name.startswith('report.'):
|
||||
report = openerp.report.interface.report_int._reports.get(name)
|
||||
if report:
|
||||
return report
|
||||
else:
|
||||
dbname = getattr(threading.currentThread(), 'dbname', None)
|
||||
if dbname:
|
||||
registry = openerp.modules.registry.RegistryManager.get(dbname)
|
||||
with registry.cursor() as cr:
|
||||
return registry['ir.actions.report.xml']._lookup_report(cr, name[len('report.'):])
|
||||
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
|
||||
#The background is set with 40 plus the number of the color, and the foreground with 30
|
||||
|
|
|
@ -5155,6 +5155,15 @@ class BaseModel(object):
|
|||
get_xml_id = get_external_id
|
||||
_get_xml_ids = _get_external_ids
|
||||
|
||||
def print_report(self, cr, uid, ids, name, data, context=None):
|
||||
"""
|
||||
Render the report `name` for the given IDs. The report must be defined
|
||||
for this model, not another.
|
||||
"""
|
||||
report = self.pool['ir.actions.report.xml']._lookup_report(cr, name)
|
||||
assert self._name == report.table
|
||||
return report.create(cr, uid, ids, data, context)
|
||||
|
||||
# Transience
|
||||
def is_transient(self):
|
||||
""" Return whether the model is transient.
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import openerp
|
||||
|
||||
import interface
|
||||
import print_xml
|
||||
import print_fnc
|
||||
|
@ -30,6 +32,13 @@ import report_sxw
|
|||
|
||||
import printscreen
|
||||
|
||||
def render_report(cr, uid, ids, name, data, context=None):
|
||||
"""
|
||||
Helper to call ``ir.actions.report.xml.render_report()``.
|
||||
"""
|
||||
registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
|
||||
return registry['ir.actions.report.xml'].render_report(cr, uid, ids, name, data, context)
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import re
|
|||
from lxml import etree
|
||||
import openerp.pooler as pooler
|
||||
|
||||
import openerp
|
||||
import openerp.tools as tools
|
||||
import openerp.modules
|
||||
import print_xml
|
||||
|
@ -43,11 +44,16 @@ class report_int(object):
|
|||
|
||||
_reports = {}
|
||||
|
||||
def __init__(self, name):
|
||||
if not name.startswith('report.'):
|
||||
raise Exception('ConceptionError, bad report name, should start with "report."')
|
||||
assert name not in self._reports, 'The report "%s" already exists!' % name
|
||||
self._reports[name] = self
|
||||
def __init__(self, name, register=True):
|
||||
if register:
|
||||
assert openerp.conf.deprecation.allow_report_int_registration
|
||||
assert name.startswith('report.'), 'Report names should start with "report.".'
|
||||
assert name not in self._reports, 'The report "%s" already exists.' % name
|
||||
self._reports[name] = self
|
||||
else:
|
||||
# The report is instanciated at each use site, which is ok.
|
||||
pass
|
||||
|
||||
self.__name = name
|
||||
|
||||
self.name = name
|
||||
|
@ -65,8 +71,8 @@ class report_rml(report_int):
|
|||
XML -> DATAS -> RML -> PDF -> HTML
|
||||
using a XSL:RML transformation
|
||||
"""
|
||||
def __init__(self, name, table, tmpl, xsl):
|
||||
super(report_rml, self).__init__(name)
|
||||
def __init__(self, name, table, tmpl, xsl, register=True):
|
||||
super(report_rml, self).__init__(name, register=register)
|
||||
self.table = table
|
||||
self.internal_header=False
|
||||
self.tmpl = tmpl
|
||||
|
|
|
@ -264,7 +264,7 @@ class document(object):
|
|||
def parse_tree(self, ids, model, context=None):
|
||||
if not context:
|
||||
context={}
|
||||
browser = self.pool.get(model).browse(self.cr, self.uid, ids, context)
|
||||
browser = self.pool[model].browse(self.cr, self.uid, ids, context)
|
||||
self.parse_node(self.dom, self.doc, browser)
|
||||
|
||||
def parse_string(self, xml, ids, model, context=None):
|
||||
|
|
|
@ -388,8 +388,19 @@ class rml_parse(object):
|
|||
self.setCompany(objects[0].company_id)
|
||||
|
||||
class report_sxw(report_rml, preprocess.report):
|
||||
def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False):
|
||||
report_rml.__init__(self, name, table, rml, '')
|
||||
"""
|
||||
The register=True kwarg has been added to help remove the
|
||||
openerp.netsvc.LocalService() indirection and the related
|
||||
openerp.report.interface.report_int._reports dictionary:
|
||||
report_sxw registered in XML with auto=False are also registered in Python.
|
||||
In that case, they are registered in the above dictionary. Since
|
||||
registration is automatically done upon instanciation, and that
|
||||
instanciation is needed before rendering, a way was needed to
|
||||
instanciate-without-register a report. In the future, no report
|
||||
should be registered in the above dictionary and it will be dropped.
|
||||
"""
|
||||
def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False, register=True):
|
||||
report_rml.__init__(self, name, table, rml, '', register=register)
|
||||
self.name = name
|
||||
self.parser = parser
|
||||
self.header = header
|
||||
|
|
|
@ -5,8 +5,8 @@ import logging
|
|||
import sys
|
||||
import threading
|
||||
|
||||
import openerp.netsvc
|
||||
import openerp.pooler
|
||||
import openerp.report
|
||||
from openerp import tools
|
||||
|
||||
import security
|
||||
|
@ -51,8 +51,7 @@ def exp_render_report(db, uid, object, ids, datas=None, context=None):
|
|||
|
||||
cr = openerp.pooler.get_db(db).cursor()
|
||||
try:
|
||||
obj = openerp.netsvc.LocalService('report.'+object)
|
||||
(result, format) = obj.create(cr, uid, ids, datas, context)
|
||||
result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
|
||||
if not result:
|
||||
tb = sys.exc_info()
|
||||
self_reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
|
||||
|
@ -90,8 +89,7 @@ def exp_report(db, uid, object, ids, datas=None, context=None):
|
|||
def go(id, uid, ids, datas, context):
|
||||
cr = openerp.pooler.get_db(db).cursor()
|
||||
try:
|
||||
obj = openerp.netsvc.LocalService('report.'+object)
|
||||
(result, format) = obj.create(cr, uid, ids, datas, context)
|
||||
result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
|
||||
if not result:
|
||||
tb = sys.exc_info()
|
||||
self_reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
|
||||
|
|
|
@ -294,7 +294,8 @@ form: module.record_id""" % (xml_id,)
|
|||
res[dest] = rec.get(f,'').encode('utf8')
|
||||
assert res[dest], "Attribute %s of report is empty !" % (f,)
|
||||
for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
|
||||
('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage')):
|
||||
('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
|
||||
('report_type', 'report_type'), ('parser', 'parser')):
|
||||
if rec.get(field):
|
||||
res[dest] = rec.get(field).encode('utf8')
|
||||
if rec.get('auto'):
|
||||
|
@ -304,8 +305,6 @@ form: module.record_id""" % (xml_id,)
|
|||
res['report_sxw_content'] = sxw_content
|
||||
if rec.get('header'):
|
||||
res['header'] = eval(rec.get('header','False'))
|
||||
if rec.get('report_type'):
|
||||
res['report_type'] = rec.get('report_type')
|
||||
|
||||
res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
through the code of yaml tests.
|
||||
"""
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
import openerp.report
|
||||
import openerp.tools as tools
|
||||
import logging
|
||||
import openerp.pooler as pooler
|
||||
|
@ -49,8 +49,8 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
|
|||
rname_s = rname[7:]
|
||||
else:
|
||||
rname_s = rname
|
||||
_logger.log(netsvc.logging.TEST, " - Trying %s.create(%r)", rname, ids)
|
||||
res = netsvc.LocalService(rname).create(cr, uid, ids, data, context)
|
||||
_logger.log(logging.TEST, " - Trying %s.create(%r)", rname, ids)
|
||||
res = openerp.report.render_report(cr, uid, ids, rname_s, data, context)
|
||||
if not isinstance(res, tuple):
|
||||
raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
|
||||
(rname, type(res)))
|
||||
|
@ -92,7 +92,7 @@ def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
|
|||
_logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
|
||||
return False
|
||||
|
||||
_logger.log(netsvc.logging.TEST, " + Report %s produced correctly.", rname)
|
||||
_logger.log(logging.TEST, " + Report %s produced correctly.", rname)
|
||||
return True
|
||||
|
||||
def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
|
||||
|
@ -126,7 +126,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
|
|||
pool = pooler.get_pool(cr.dbname)
|
||||
|
||||
def log_test(msg, *args):
|
||||
_logger.log(netsvc.logging.TEST, " - " + msg, *args)
|
||||
_logger.log(logging.TEST, " - " + msg, *args)
|
||||
|
||||
datas = {}
|
||||
if active_model:
|
||||
|
|
|
@ -5,6 +5,7 @@ import time # used to eval time.strftime expressions
|
|||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
import openerp
|
||||
import openerp.pooler as pooler
|
||||
import openerp.sql_db as sql_db
|
||||
import misc
|
||||
|
@ -281,7 +282,6 @@ class YamlInterpreter(object):
|
|||
return record_dict
|
||||
|
||||
def process_record(self, node):
|
||||
import openerp.osv as osv
|
||||
record, fields = node.items()[0]
|
||||
model = self.get_model(record.model)
|
||||
|
||||
|
@ -543,7 +543,14 @@ class YamlInterpreter(object):
|
|||
python, statements = node.items()[0]
|
||||
model = self.get_model(python.model)
|
||||
statements = statements.replace("\r\n", "\n")
|
||||
code_context = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
|
||||
code_context = {
|
||||
'model': model,
|
||||
'cr': self.cr,
|
||||
'uid': self.uid,
|
||||
'log': self._log,
|
||||
'context': self.context,
|
||||
'openerp': openerp,
|
||||
}
|
||||
code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
|
||||
try:
|
||||
code_obj = compile(statements, self.filename, 'exec')
|
||||
|
|
Loading…
Reference in New Issue