diff --git a/doc/03_module_dev.rst b/doc/03_module_dev.rst index 72f035be4d0..c5f965e4b93 100644 --- a/doc/03_module_dev.rst +++ b/doc/03_module_dev.rst @@ -13,3 +13,4 @@ Modules 03_module_dev_04 03_module_dev_05 03_module_dev_06 + report-declaration diff --git a/doc/changelog.rst b/doc/changelog.rst index c85bf64273f..59c4a6bced5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -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 ` worker type. - Added :ref:`orm-workflows` to the ORM. diff --git a/doc/report-declaration.rst b/doc/report-declaration.rst new file mode 100644 index 00000000000..2a40c96ec61 --- /dev/null +++ b/doc/report-declaration.rst @@ -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 ```` 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 +```` 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. + diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 3e29a0a9b97..cbdddb4401c 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -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', diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml index da118129932..82514692c06 100644 --- a/openerp/addons/base/ir/ir_actions.xml +++ b/openerp/addons/base/ir/ir_actions.xml @@ -82,6 +82,7 @@ + diff --git a/openerp/conf/deprecation.py b/openerp/conf/deprecation.py index 11399bef4fb..41bd4f971d9 100644 --- a/openerp/conf/deprecation.py +++ b/openerp/conf/deprecation.py @@ -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: diff --git a/openerp/import_xml.rng b/openerp/import_xml.rng index 97b2c4a13c0..5f4c40b65d3 100644 --- a/openerp/import_xml.rng +++ b/openerp/import_xml.rng @@ -107,6 +107,7 @@ + diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 56b17e3239a..0e14480d8ac 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -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 diff --git a/openerp/modules/migration.py b/openerp/modules/migration.py index e0faa77c3a4..16de56879b9 100644 --- a/openerp/modules/migration.py +++ b/openerp/modules/migration.py @@ -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 diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index b06c500a24c..89cb6143a59 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -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() diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 6b66830b75e..ef70fcca174 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -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 diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c6609592e00..69c7498c6b9 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -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. diff --git a/openerp/report/__init__.py b/openerp/report/__init__.py index 6b56f15b6b5..2cf01636806 100644 --- a/openerp/report/__init__.py +++ b/openerp/report/__init__.py @@ -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: diff --git a/openerp/report/interface.py b/openerp/report/interface.py index f0de9631800..39de2fc7a31 100644 --- a/openerp/report/interface.py +++ b/openerp/report/interface.py @@ -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 diff --git a/openerp/report/print_xml.py b/openerp/report/print_xml.py index c2af0984c2e..558f7dd3bd0 100644 --- a/openerp/report/print_xml.py +++ b/openerp/report/print_xml.py @@ -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): diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py index 325690205ef..b0fb70513bb 100644 --- a/openerp/report/report_sxw.py +++ b/openerp/report/report_sxw.py @@ -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 diff --git a/openerp/service/report.py b/openerp/service/report.py index c20c7f0d7ec..73d8d6a7e3d 100644 --- a/openerp/service/report.py +++ b/openerp/service/report.py @@ -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) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 55ca1c1ce9c..d07d2a16dc6 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -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')) diff --git a/openerp/tools/test_reports.py b/openerp/tools/test_reports.py index 9ec4dab6cc3..3fd1ba2fed4 100644 --- a/openerp/tools/test_reports.py +++ b/openerp/tools/test_reports.py @@ -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: diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index 3172caac1f4..662229541ad 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -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')