[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_05
03_module_dev_06
report-declaration

View File

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

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 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',

View File

@ -82,6 +82,7 @@
<group string="Miscellaneous">
<field name="multi"/>
<field name="auto"/>
<field name="parser"/>
</group>
</group>
</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
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:

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@ -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):

View File

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

View File

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

View File

@ -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'))

View File

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

View File

@ -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')