[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:
Vo Minh Thu 2013-03-27 17:11:29 +01:00
commit 23d672dfc3
20 changed files with 172 additions and 53 deletions

View File

@ -13,3 +13,4 @@ Modules
03_module_dev_04 03_module_dev_04
03_module_dev_05 03_module_dev_05
03_module_dev_06 03_module_dev_06
report-declaration

View File

@ -6,6 +6,9 @@ Changelog
`trunk` `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. - Removed support for the ``NET-RPC`` protocol.
- Added the :ref:`Long polling <longpolling-worker>` worker type. - Added the :ref:`Long polling <longpolling-worker>` worker type.
- Added :ref:`orm-workflows` to the ORM. - Added :ref:`orm-workflows` to the ORM.

View File

@ -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.

View File

@ -20,11 +20,13 @@
############################################################################## ##############################################################################
import logging import logging
import operator
import os import os
import re import re
from socket import gethostname from socket import gethostname
import time import time
import openerp
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp import netsvc, tools from openerp import netsvc, tools
from openerp.osv import fields, osv from openerp.osv import fields, osv
@ -85,26 +87,45 @@ class report_xml(osv.osv):
res[report.id] = False res[report.id] = False
return res return res
def register_all(self, cr): def _lookup_report(self, cr, name):
"""Report registration handler that may be overridden by subclasses to """
add their own kinds of report services. Look up a report definition.
Loads all reports with no manual loaders (auto==True) and
registers the appropriate services to implement them.
""" """
opj = os.path.join opj = os.path.join
cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
result = cr.dictfetchall() # First lookup in the deprecated place, because if the report definition
reports = openerp.report.interface.report_int._reports # has not been updated, it is more likely the correct definition is there.
for r in result: # Only reports with custom parser sepcified in Python are still there.
if reports.has_key('report.'+r['report_name']): if 'report.' + name in openerp.report.interface.report_int._reports:
continue new_report = openerp.report.interface.report_int._reports['report.' + name]
if r['report_rml'] or r['report_rml_content_data']: else:
report_sxw('report.'+r['report_name'], r['model'], cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
opj('addons',r['report_rml'] or '/'), header=r['header']) r = cr.dictfetchone()
if r['report_xsl']: if r:
report_rml('report.'+r['report_name'], r['model'], if r['report_rml'] or r['report_rml_content_data']:
opj('addons',r['report_xml']), if r['parser']:
r['report_xsl'] and opj('addons',r['report_xsl'])) 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' _name = 'ir.actions.report.xml'
_inherit = 'ir.actions.actions' _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_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'), 'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
'parser': fields.char('Parser Class'),
} }
_defaults = { _defaults = {
'type': 'ir.actions.report.xml', 'type': 'ir.actions.report.xml',

View File

@ -82,6 +82,7 @@
<group string="Miscellaneous"> <group string="Miscellaneous">
<field name="multi"/> <field name="multi"/>
<field name="auto"/> <field name="auto"/>
<field name="parser"/>
</group> </group>
</group> </group>
</page> </page>

View File

@ -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 track of those specific measures by providing variables that can be unset
by the user to check if her code is future proof. 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 # 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. # Change to False around 2013.02.
open_openerp_namespace = False 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: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -107,6 +107,7 @@
<rng:optional><rng:attribute name="sxw"/></rng:optional> <rng:optional><rng:attribute name="sxw"/></rng:optional>
<rng:optional><rng:attribute name="xml"/></rng:optional> <rng:optional><rng:attribute name="xml"/></rng:optional>
<rng:optional><rng:attribute name="xsl"/></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="auto" /> </rng:optional>
<rng:optional> <rng:attribute name="header" /> </rng:optional> <rng:optional> <rng:attribute name="header" /> </rng:optional>
<rng:optional> <rng:attribute name="webkit_header" /> </rng:optional> <rng:optional> <rng:attribute name="webkit_header" /> </rng:optional>

View File

@ -36,8 +36,6 @@ from openerp.tools.safe_eval import safe_eval as eval
import openerp.pooler as pooler import openerp.pooler as pooler
from openerp.tools.translate import _ from openerp.tools.translate import _
import openerp.netsvc as netsvc
import zipfile import zipfile
import openerp.release as release import openerp.release as release

View File

@ -36,8 +36,6 @@ from openerp.tools.safe_eval import safe_eval as eval
import openerp.pooler as pooler import openerp.pooler as pooler
from openerp.tools.translate import _ from openerp.tools.translate import _
import openerp.netsvc as netsvc
import zipfile import zipfile
import openerp.release as release import openerp.release as release

View File

@ -225,7 +225,6 @@ class RegistryManager(object):
try: try:
Registry.setup_multi_process_signaling(cr) Registry.setup_multi_process_signaling(cr)
registry.do_parent_store(cr) registry.do_parent_store(cr)
registry.get('ir.actions.report.xml').register_all(cr)
cr.commit() cr.commit()
finally: finally:
cr.close() cr.close()

View File

@ -21,11 +21,9 @@
############################################################################## ##############################################################################
import errno
import logging import logging
import logging.handlers import logging.handlers
import os import os
import platform
import release import release
import sys import sys
import threading import threading
@ -46,12 +44,30 @@ import openerp
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def LocalService(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': if name == 'workflow':
return openerp.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) 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 #The background is set with 40 plus the number of the color, and the foreground with 30

View File

@ -5155,6 +5155,15 @@ class BaseModel(object):
get_xml_id = get_external_id get_xml_id = get_external_id
_get_xml_ids = _get_external_ids _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 # Transience
def is_transient(self): def is_transient(self):
""" Return whether the model is transient. """ Return whether the model is transient.

View File

@ -19,6 +19,8 @@
# #
############################################################################## ##############################################################################
import openerp
import interface import interface
import print_xml import print_xml
import print_fnc import print_fnc
@ -30,6 +32,13 @@ import report_sxw
import printscreen 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: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -25,6 +25,7 @@ import re
from lxml import etree from lxml import etree
import openerp.pooler as pooler import openerp.pooler as pooler
import openerp
import openerp.tools as tools import openerp.tools as tools
import openerp.modules import openerp.modules
import print_xml import print_xml
@ -43,11 +44,16 @@ class report_int(object):
_reports = {} _reports = {}
def __init__(self, name): def __init__(self, name, register=True):
if not name.startswith('report.'): if register:
raise Exception('ConceptionError, bad report name, should start with "report."') assert openerp.conf.deprecation.allow_report_int_registration
assert name not in self._reports, 'The report "%s" already exists!' % name assert name.startswith('report.'), 'Report names should start with "report.".'
self._reports[name] = self 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
self.name = name self.name = name
@ -65,8 +71,8 @@ class report_rml(report_int):
XML -> DATAS -> RML -> PDF -> HTML XML -> DATAS -> RML -> PDF -> HTML
using a XSL:RML transformation using a XSL:RML transformation
""" """
def __init__(self, name, table, tmpl, xsl): def __init__(self, name, table, tmpl, xsl, register=True):
super(report_rml, self).__init__(name) super(report_rml, self).__init__(name, register=register)
self.table = table self.table = table
self.internal_header=False self.internal_header=False
self.tmpl = tmpl self.tmpl = tmpl

View File

@ -264,7 +264,7 @@ class document(object):
def parse_tree(self, ids, model, context=None): def parse_tree(self, ids, model, context=None):
if not context: if not context:
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) self.parse_node(self.dom, self.doc, browser)
def parse_string(self, xml, ids, model, context=None): def parse_string(self, xml, ids, model, context=None):

View File

@ -388,8 +388,19 @@ class rml_parse(object):
self.setCompany(objects[0].company_id) self.setCompany(objects[0].company_id)
class report_sxw(report_rml, preprocess.report): 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.name = name
self.parser = parser self.parser = parser
self.header = header self.header = header

View File

@ -5,8 +5,8 @@ import logging
import sys import sys
import threading import threading
import openerp.netsvc
import openerp.pooler import openerp.pooler
import openerp.report
from openerp import tools from openerp import tools
import security 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() cr = openerp.pooler.get_db(db).cursor()
try: try:
obj = openerp.netsvc.LocalService('report.'+object) result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
(result, format) = obj.create(cr, uid, ids, datas, context)
if not result: if not result:
tb = sys.exc_info() 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) 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): def go(id, uid, ids, datas, context):
cr = openerp.pooler.get_db(db).cursor() cr = openerp.pooler.get_db(db).cursor()
try: try:
obj = openerp.netsvc.LocalService('report.'+object) result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
(result, format) = obj.create(cr, uid, ids, datas, context)
if not result: if not result:
tb = sys.exc_info() 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) self_reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)

View File

@ -294,7 +294,8 @@ form: module.record_id""" % (xml_id,)
res[dest] = rec.get(f,'').encode('utf8') res[dest] = rec.get(f,'').encode('utf8')
assert res[dest], "Attribute %s of report is empty !" % (f,) 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'), 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): if rec.get(field):
res[dest] = rec.get(field).encode('utf8') res[dest] = rec.get(field).encode('utf8')
if rec.get('auto'): if rec.get('auto'):
@ -304,8 +305,6 @@ form: module.record_id""" % (xml_id,)
res['report_sxw_content'] = sxw_content res['report_sxw_content'] = sxw_content
if rec.get('header'): if rec.get('header'):
res['header'] = eval(rec.get('header','False')) 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')) res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))

View File

@ -25,7 +25,7 @@
through the code of yaml tests. through the code of yaml tests.
""" """
import openerp.netsvc as netsvc import openerp.report
import openerp.tools as tools import openerp.tools as tools
import logging import logging
import openerp.pooler as pooler 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:] rname_s = rname[7:]
else: else:
rname_s = rname rname_s = rname
_logger.log(netsvc.logging.TEST, " - Trying %s.create(%r)", rname, ids) _logger.log(logging.TEST, " - Trying %s.create(%r)", rname, ids)
res = netsvc.LocalService(rname).create(cr, uid, ids, data, context) res = openerp.report.render_report(cr, uid, ids, rname_s, data, context)
if not isinstance(res, tuple): if not isinstance(res, tuple):
raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \ raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
(rname, type(res))) (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) _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
return False return False
_logger.log(netsvc.logging.TEST, " + Report %s produced correctly.", rname) _logger.log(logging.TEST, " + Report %s produced correctly.", rname)
return True return True
def try_report_action(cr, uid, action_id, active_model=None, active_ids=None, 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) pool = pooler.get_pool(cr.dbname)
def log_test(msg, *args): def log_test(msg, *args):
_logger.log(netsvc.logging.TEST, " - " + msg, *args) _logger.log(logging.TEST, " - " + msg, *args)
datas = {} datas = {}
if active_model: if active_model:

View File

@ -5,6 +5,7 @@ import time # used to eval time.strftime expressions
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
import openerp
import openerp.pooler as pooler import openerp.pooler as pooler
import openerp.sql_db as sql_db import openerp.sql_db as sql_db
import misc import misc
@ -281,7 +282,6 @@ class YamlInterpreter(object):
return record_dict return record_dict
def process_record(self, node): def process_record(self, node):
import openerp.osv as osv
record, fields = node.items()[0] record, fields = node.items()[0]
model = self.get_model(record.model) model = self.get_model(record.model)
@ -543,7 +543,14 @@ class YamlInterpreter(object):
python, statements = node.items()[0] python, statements = node.items()[0]
model = self.get_model(python.model) model = self.get_model(python.model)
statements = statements.replace("\r\n", "\n") 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 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
try: try:
code_obj = compile(statements, self.filename, 'exec') code_obj = compile(statements, self.filename, 'exec')