diff --git a/bin/addons/__init__.py b/bin/addons/__init__.py
index cc3bdc2ce7b..0c591f9ca4f 100644
--- a/bin/addons/__init__.py
+++ b/bin/addons/__init__.py
@@ -757,16 +757,21 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
return has_updates
+def _check_module_names(cr, module_names):
+ mod_names = set(module_names)
+ if 'base' in mod_names:
+ # ignore dummy 'all' module
+ if 'all' in mod_names:
+ mod_names.remove('all')
+ if mod_names:
+ cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
+ if cr.dictfetchone()['count'] != len(mod_names):
+ # find out what module name(s) are incorrect:
+ cr.execute("SELECT name FROM ir_module_module")
+ incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
+ logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
+
def load_modules(db, force_demo=False, status=None, update_module=False):
-
- def check_module_name(cr, mods, state):
- for mod in mods:
- id = modobj.search(cr, 1, ['&', ('state', '=', state), ('name', '=', mod)])
- if id:
- getattr(modobj, states[state])(cr, 1, id)
- elif mod != 'all':
- logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: invalid module name!' % (mod))
-
if not status:
status = {}
cr = db.cursor()
@@ -795,16 +800,23 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if update_module:
modobj = pool.get('ir.module.module')
- states = {'installed': 'button_upgrade', 'uninstalled': 'button_install'}
logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
if ('base' in tools.config['init']) or ('base' in tools.config['update']):
modobj.update_list(cr, 1)
+ _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
+
mods = [k for k in tools.config['init'] if tools.config['init'][k]]
- check_module_name(cr, mods, 'uninstalled')
+ if mods:
+ ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
+ if ids:
+ modobj.button_install(cr, 1, ids)
mods = [k for k in tools.config['update'] if tools.config['update'][k]]
- check_module_name(cr, mods, 'installed')
+ if mods:
+ ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
+ if ids:
+ modobj.button_upgrade(cr, 1, ids)
cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
diff --git a/bin/addons/base/__openerp__.py b/bin/addons/base/__openerp__.py
index 5f7fade6309..47a36987b48 100644
--- a/bin/addons/base/__openerp__.py
+++ b/bin/addons/base/__openerp__.py
@@ -52,7 +52,7 @@
'module/wizard/base_language_install_view.xml',
'module/wizard/base_import_language_view.xml',
'module/wizard/base_module_upgrade_view.xml',
- #TODO: 'module/wizard/base_module_configuration_view.xml',
+ 'module/wizard/base_module_configuration_view.xml',
'module/wizard/base_export_language_view.xml',
'module/wizard/base_update_translations_view.xml',
'res/res_request_view.xml',
diff --git a/bin/addons/base/base_data.xml b/bin/addons/base/base_data.xml
index 2866c1ea31b..5082adfbfe0 100644
--- a/bin/addons/base/base_data.xml
+++ b/bin/addons/base/base_data.xml
@@ -1335,8 +1335,8 @@
- INR
- Rs
+ Rs
+ INRRs0.014
diff --git a/bin/addons/base/ir/ir.xml b/bin/addons/base/ir/ir.xml
index eac674f3e77..e76af79ea01 100644
--- a/bin/addons/base/ir/ir.xml
+++ b/bin/addons/base/ir/ir.xml
@@ -1906,11 +1906,10 @@
+
-
-
+
+
@@ -1930,10 +1929,8 @@
-
-
+
+
@@ -1961,14 +1958,8 @@
-
-
-
diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py
index 7ba775c1797..a20d780c671 100644
--- a/bin/addons/base/ir/ir_actions.py
+++ b/bin/addons/base/ir/ir_actions.py
@@ -512,14 +512,21 @@ class actions_server(osv.osv):
return obj
- def merge_message(self, cr, uid, keystr, action, context):
+ def merge_message(self, cr, uid, keystr, action, context=None):
+ if context is None:
+ context = {}
logger = netsvc.Logger()
def merge(match):
obj_pool = self.pool.get(action.model_id.model)
id = context.get('active_id')
obj = obj_pool.browse(cr, uid, id)
exp = str(match.group()[2:-2]).strip()
- result = eval(exp, {'object':obj, 'context': context,'time':time})
+ result = eval(exp,
+ {
+ 'object': obj,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'time': time,
+ })
if result in (None, False):
return str("--------")
return tools.ustr(result)
@@ -536,13 +543,16 @@ class actions_server(osv.osv):
# False : Finnished correctly
# ACTION_ID : Action to launch
- def run(self, cr, uid, ids, context={}):
+ # FIXME: refactor all the eval() calls in run()!
+ def run(self, cr, uid, ids, context=None):
logger = netsvc.Logger()
+ if context is None:
+ context = {}
for action in self.browse(cr, uid, ids, context):
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
- 'context':context,
+ 'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time':time,
'cr': cr,
@@ -560,26 +570,19 @@ class actions_server(osv.osv):
.read(cr, uid, action.action_id.id, context=context)
if action.state=='code':
- if config['server_actions_allow_code']:
- localdict = {
- 'self': self.pool.get(action.model_id.model),
- 'context': context,
- 'time': time,
- 'ids': ids,
- 'cr': cr,
- 'uid': uid,
- 'object':obj,
- 'obj': obj,
- }
- eval(action.code, localdict, mode="exec")
- if 'action' in localdict:
- return localdict['action']
- else:
- netsvc.Logger().notifyChannel(
- self._name, netsvc.LOG_ERROR,
- "%s is a `code` server action, but "
- "it isn't allowed in this configuration.\n\n"
- "See server options to enable it"%action)
+ localdict = {
+ 'self': self.pool.get(action.model_id.model),
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'time': time,
+ 'ids': ids,
+ 'cr': cr,
+ 'uid': uid,
+ 'object':obj,
+ 'obj': obj,
+ }
+ eval(action.code, localdict, mode="exec", nocopy=True) # nocopy allows to return 'action'
+ if 'action' in localdict:
+ return localdict['action']
if action.state == 'email':
user = config['email_from']
@@ -631,9 +634,9 @@ class actions_server(osv.osv):
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
- 'context':context,
+ 'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
- 'time':time,
+ 'time': time,
'cr': cr,
'pool' : self.pool,
'uid' : uid
@@ -651,7 +654,11 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- cxt = {'context':context, 'object': obj, 'time':time}
+ cxt = {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ }
expr = eval(euq, cxt)
else:
expr = exp.value
@@ -687,7 +694,12 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- expr = eval(euq, {'context':context, 'object': obj, 'time':time})
+ expr = eval(euq,
+ {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ })
else:
expr = exp.value
res[exp.col1.name] = expr
@@ -706,7 +718,12 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- expr = eval(euq, {'context':context, 'object': obj, 'time':time})
+ expr = eval(euq,
+ {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ })
else:
expr = exp.value
res[exp.col1.name] = expr
@@ -736,7 +753,7 @@ act_window_close()
TODO_STATES = [('open', 'To Do'),
('done', 'Done'),
('skip','Skipped'),
- ('cancel','Cancel')]
+ ('cancel','Cancelled')]
class ir_actions_todo(osv.osv):
_name = 'ir.actions.todo'
@@ -752,9 +769,9 @@ class ir_actions_todo(osv.osv):
'note':fields.text('Text', translate=True),
}
_defaults={
- 'state': lambda *a: 'open',
- 'sequence': lambda *a: 10,
- 'restart': lambda *a: 'always',
+ 'state': 'open',
+ 'sequence': 10,
+ 'restart': 'onskip',
}
_order="sequence,id"
diff --git a/bin/addons/base/ir/ir_attachment.py b/bin/addons/base/ir/ir_attachment.py
index abba9222d25..173632c4411 100644
--- a/bin/addons/base/ir/ir_attachment.py
+++ b/bin/addons/base/ir/ir_attachment.py
@@ -105,8 +105,8 @@ class ir_attachment(osv.osv):
'datas': fields.binary('Data'),
'datas_fname': fields.char('Filename',size=256),
'description': fields.text('Description'),
- 'res_name': fields.function(_name_get_resname, type='char',
- string='Resource Name', method=True),
+ 'res_name': fields.function(_name_get_resname, type='char', size=128,
+ string='Resource Name', method=True, store=True),
'res_model': fields.char('Resource Object',size=64, readonly=True,
help="The database object this attachment will be attached to"),
'res_id': fields.integer('Resource ID', readonly=True,
diff --git a/bin/addons/base/ir/ir_report_custom.py b/bin/addons/base/ir/ir_report_custom.py
index 036469fb417..3c7daa09d34 100644
--- a/bin/addons/base/ir/ir_report_custom.py
+++ b/bin/addons/base/ir/ir_report_custom.py
@@ -88,7 +88,7 @@ class report_custom(osv.osv):
c_f[i] = []
tmp = report.fields_child0[i]
for j in range(3):
- c_f[i].append((not isinstance(eval('tmp.field_child'+str(j)), browse_null) and eval('tmp.field_child'+str(j)+'.ttype')) or None)
+ c_f[i].append((not isinstance(eval('tmp.field_child'+str(j),{},{'tmp':tmp}), browse_null) and eval('tmp.field_child'+str(j)+'.ttype',{},{'tmp':tmp})) or None)
if not reduce(lambda x,y : x or y, map(lambda x: x in ['integer', 'float'], c_f[1])):
raise osv.except_osv(_('Invalid operation'), _('Second field should be figures'))
@@ -101,7 +101,7 @@ class report_custom(osv.osv):
c_f[i] = []
tmp = report.fields_child0[i]
for j in range(3):
- c_f[i].append((not isinstance(eval('tmp.field_child'+str(j)), browse_null) and eval('tmp.field_child'+str(j)+'.ttype')) or None)
+ c_f[i].append((not isinstance(eval('tmp.field_child'+str(j),{},{'tmp':tmp}), browse_null) and eval('tmp.field_child'+str(j)+'.ttype',{},{'tmp':tmp})) or None)
if i == 0:
pass
diff --git a/bin/addons/base/maintenance/maintenance_view.xml b/bin/addons/base/maintenance/maintenance_view.xml
index e61348abca5..8b4021347d4 100644
--- a/bin/addons/base/maintenance/maintenance_view.xml
+++ b/bin/addons/base/maintenance/maintenance_view.xml
@@ -38,7 +38,7 @@
-
+
maintenance.contract.searchmaintenance.contract
@@ -51,7 +51,7 @@
-
+
maintenance.contract.calendarmaintenance.contract
@@ -106,9 +106,9 @@
-
+
-
+
diff --git a/bin/addons/base/module/module.py b/bin/addons/base/module/module.py
index 89a059eb9c5..f4086f9edc3 100644
--- a/bin/addons/base/module/module.py
+++ b/bin/addons/base/module/module.py
@@ -130,8 +130,8 @@ class module(osv.osv):
return res
_columns = {
- 'name': fields.char("Name", size=128, readonly=True, required=True),
- 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True),
+ 'name': fields.char("Name", size=128, readonly=True, required=True, select=True),
+ 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
'description': fields.text("Description", readonly=True, translate=True),
'author': fields.char("Author", size=128, readonly=True),
@@ -160,7 +160,7 @@ class module(osv.osv):
('to upgrade','To be upgraded'),
('to remove','To be removed'),
('to install','To be installed')
- ], string='State', readonly=True),
+ ], string='State', readonly=True, select=True),
'demo': fields.boolean('Demo data'),
'license': fields.selection([
('GPL-2', 'GPL Version 2'),
@@ -510,7 +510,7 @@ class web_module_dependency(osv.osv):
_columns = {
- 'name': fields.char('Name', size=128),
+ 'name': fields.char('Name', size=128, select=True),
'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
'web_module_id': fields.many2one('ir.module.web', 'Web Module', select=True, ondelete='cascade'),
'state': fields.function(_state, method=True, type='selection', selection=[
@@ -521,7 +521,7 @@ class web_module_dependency(osv.osv):
('to remove','To be removed'),
('to install','To be installed'),
('unknown', 'Unknown'),
- ], string='State', readonly=True),
+ ], string='State', readonly=True, select=True),
}
web_module_dependency()
@@ -542,7 +542,7 @@ class module_dependency(osv.osv):
return result
_columns = {
- 'name': fields.char('Name', size=128),
+ 'name': fields.char('Name', size=128, select=True),
'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
'state': fields.function(_state, method=True, type='selection', selection=[
('uninstallable','Uninstallable'),
@@ -552,7 +552,7 @@ class module_dependency(osv.osv):
('to remove','To be removed'),
('to install','To be installed'),
('unknown', 'Unknown'),
- ], string='State', readonly=True),
+ ], string='State', readonly=True, select=True),
}
module_dependency()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/module/module_view.xml b/bin/addons/base/module/module_view.xml
index 6f2155b7c7f..bf5164d8276 100644
--- a/bin/addons/base/module/module_view.xml
+++ b/bin/addons/base/module/module_view.xml
@@ -162,9 +162,9 @@
-
+
-
+
diff --git a/bin/addons/base/module/wizard/__init__.py b/bin/addons/base/module/wizard/__init__.py
index 76e7b329331..a4133a50113 100644
--- a/bin/addons/base/module/wizard/__init__.py
+++ b/bin/addons/base/module/wizard/__init__.py
@@ -19,9 +19,6 @@
#
##############################################################################
-#Old Wizard
-import wizard_configuration
-
import base_module_import
import base_module_update
import base_language_install
diff --git a/bin/addons/base/module/wizard/base_export_language_view.xml b/bin/addons/base/module/wizard/base_export_language_view.xml
index 389a97d0954..cae515425d5 100644
--- a/bin/addons/base/module/wizard/base_export_language_view.xml
+++ b/bin/addons/base/module/wizard/base_export_language_view.xml
@@ -27,10 +27,10 @@
-
+
-
-
+
+
diff --git a/bin/addons/base/module/wizard/base_import_language_view.xml b/bin/addons/base/module/wizard/base_import_language_view.xml
index 031969543f7..eb73e8d4370 100644
--- a/bin/addons/base/module/wizard/base_import_language_view.xml
+++ b/bin/addons/base/module/wizard/base_import_language_view.xml
@@ -20,7 +20,7 @@
-
+
@@ -32,7 +32,7 @@
-
+
diff --git a/bin/addons/base/module/wizard/base_language_install_view.xml b/bin/addons/base/module/wizard/base_language_install_view.xml
index 3bec1c19005..794ea6bed7d 100644
--- a/bin/addons/base/module/wizard/base_language_install_view.xml
+++ b/bin/addons/base/module/wizard/base_language_install_view.xml
@@ -3,27 +3,24 @@
- Install Language
+ Load Official Translationbase.language.installform
-
+
- Install Language
+ Load Official Translationir.actions.act_windowbase.language.installform
@@ -38,23 +35,21 @@
parent="menu_translation"/>
- Install Language Message
+ Official Translation Loadedbase.module.import.msgform
-
- Install Language Message
+ Official Translation Loadedbase.module.import.msgformform
diff --git a/bin/addons/base/module/wizard/base_module_configuration.py b/bin/addons/base/module/wizard/base_module_configuration.py
index 2473136501d..909637c1d21 100644
--- a/bin/addons/base/module/wizard/base_module_configuration.py
+++ b/bin/addons/base/module/wizard/base_module_configuration.py
@@ -19,15 +19,33 @@
#
##############################################################################
-import pooler
-from osv import osv, fields
+from osv import osv
+from tools.translate import _
class base_module_configuration(osv.osv_memory):
- _name = "base.module.configuration"
+ _name = "base.module.configuration"
- def config(self, cr, uid, data, context=None):
- return self.pool.get('res.config').next(cr, uid, [], context=context)
+ def start(self, cr, uid, ids, context=None):
+ todo_ids = self.pool.get('ir.actions.todo').search(cr, uid, ['|', ('state', '=', 'open'), ('restart', '=', 'always')])
+ if not todo_ids:
+ # When there is no wizard todo it will display message
+ data_obj = self.pool.get('ir.model.data')
+ result = data_obj._get_id(cr, uid, 'base', 'view_base_module_configuration_form')
+ view_id = data_obj.browse(cr, uid, result).res_id
+ value = {
+ 'name': _('System Configuration done'),
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'base.module.configuration',
+ 'view_id': [view_id],
+ 'type': 'ir.actions.act_window',
+ 'target': 'new'
+ }
+ return value
+ # Run the config wizards
+ config_pool = self.pool.get('res.config')
+ return config_pool.start(cr, uid, ids, context=context)
base_module_configuration()
diff --git a/bin/addons/base/module/wizard/base_module_configuration_view.xml b/bin/addons/base/module/wizard/base_module_configuration_view.xml
index 05190133801..559273be8a9 100644
--- a/bin/addons/base/module/wizard/base_module_configuration_view.xml
+++ b/bin/addons/base/module/wizard/base_module_configuration_view.xml
@@ -2,35 +2,42 @@
-
+ Module Configurationbase.module.configurationform
-
+
-
- Module Configuration
- ir.actions.act_window
- base.module.configuration
- form
- form
- new
+
+
+ Start Configuration
+
+ code
+ action = obj.start([])
-
+
-
+
+
+
+ Start Configuration
+ client_action_multi
+
+ action
+ ir.actions.todo
+
+
-
\ No newline at end of file
+
diff --git a/bin/addons/base/module/wizard/base_module_import_view.xml b/bin/addons/base/module/wizard/base_module_import_view.xml
index f46c7ebf293..8cb974167d9 100644
--- a/bin/addons/base/module/wizard/base_module_import_view.xml
+++ b/bin/addons/base/module/wizard/base_module_import_view.xml
@@ -14,7 +14,7 @@
-
+
@@ -47,7 +47,7 @@
-
+
diff --git a/bin/addons/base/module/wizard/base_module_update_view.xml b/bin/addons/base/module/wizard/base_module_update_view.xml
index 8ef4b0860e6..08576006ba5 100644
--- a/bin/addons/base/module/wizard/base_module_update_view.xml
+++ b/bin/addons/base/module/wizard/base_module_update_view.xml
@@ -12,7 +12,7 @@
-
+
@@ -47,7 +47,7 @@
-
+
diff --git a/bin/addons/base/module/wizard/base_module_upgrade_view.xml b/bin/addons/base/module/wizard/base_module_upgrade_view.xml
index a01146c0d37..ce7d2e6b659 100644
--- a/bin/addons/base/module/wizard/base_module_upgrade_view.xml
+++ b/bin/addons/base/module/wizard/base_module_upgrade_view.xml
@@ -14,7 +14,7 @@
-
+
@@ -53,7 +53,7 @@
-
+
diff --git a/bin/addons/base/module/wizard/base_update_translations_view.xml b/bin/addons/base/module/wizard/base_update_translations_view.xml
index 9663406c030..781f3d142d8 100644
--- a/bin/addons/base/module/wizard/base_update_translations_view.xml
+++ b/bin/addons/base/module/wizard/base_update_translations_view.xml
@@ -11,7 +11,7 @@
-
+
diff --git a/bin/addons/base/module/wizard/wizard_configuration.py b/bin/addons/base/module/wizard/wizard_configuration.py
deleted file mode 100644
index e720ba419b0..00000000000
--- a/bin/addons/base/module/wizard/wizard_configuration.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# 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 .
-#
-##############################################################################
-
-import wizard
-import pooler
-
-class wizard_configuration(wizard.interface):
-
- def _config(self, cr, uid, data, context=None):
- return pooler.get_pool(cr.dbname).get('res.config')\
- .next(cr, uid, [], context=context)
-
- states = {
- 'init':{
- 'result': {
- 'type': 'action',
- 'action': _config,
- 'state': 'end',
- },
- }
- }
-wizard_configuration('module.configuration')
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/res/ir_property.py b/bin/addons/base/res/ir_property.py
index 8dcd01c4897..27a3223f18c 100644
--- a/bin/addons/base/res/ir_property.py
+++ b/bin/addons/base/res/ir_property.py
@@ -31,7 +31,6 @@ class ir_property(osv.osv):
def _models_field_get(self, cr, uid, field_key, field_value, context=None):
get = attrgetter(field_key, field_value)
-
obj = self.pool.get('ir.model.fields')
ids = obj.search(cr, uid, [('view_load','=',1)], context=context)
res = set()
@@ -40,19 +39,19 @@ class ir_property(osv.osv):
return res
def _models_get(self, cr, uid, context=None):
- return self._models_field_get(cr, uid, 'model', 'name', context)
+ return self._models_field_get(cr, uid, 'model', 'model_id.name', context)
def _models_get2(self, cr, uid, context=None):
return self._models_field_get(cr, uid, 'relation', 'relation', context)
_columns = {
- 'name': fields.char('Name', size=128),
+ 'name': fields.char('Name', size=128, select=1),
'res_id': fields.reference('Resource', selection=_models_get, size=128,
- help="If not set, act as default property"),
- 'company_id': fields.many2one('res.company', 'Company'),
- 'fields_id': fields.many2one('ir.model.fields', 'Fields', ondelete='cascade', required=True),
+ help="If not set, acts as a default value for new resources", select=1),
+ 'company_id': fields.many2one('res.company', 'Company', select=1),
+ 'fields_id': fields.many2one('ir.model.fields', 'Field', ondelete='cascade', required=True, select=1),
'value_float' : fields.float('Value'),
'value_integer' : fields.integer_big('Value'), # will contain (int, bigint)
@@ -135,15 +134,7 @@ class ir_property(osv.osv):
def create(self, cr, uid, values, context=None):
return super(ir_property, self).create(cr, uid, self._update_values(cr, uid, None, values), context=context)
- def get_by_id(self, cr, uid, record_ids, context=None):
- if isinstance(record_ids, (int, long)):
- record_ids = [record_ids]
-
- if not record_ids:
- return False
-
- record = self.browse(cr, uid, record_ids[0], context=context)
-
+ def get_by_record(self, cr, uid, record, context=None):
if record.type in ('char', 'text'):
return record.value_text
elif record.type == 'float':
@@ -162,7 +153,6 @@ class ir_property(osv.osv):
if not record.value_datetime:
return False
return time.strftime('%Y-%m-%d', time.strptime(record.value_datetime, '%Y-%m-%d %H:%M:%S'))
-
return False
def get(self, cr, uid, name, model, res_id=False, context={}):
@@ -170,7 +160,9 @@ class ir_property(osv.osv):
if domain is not None:
domain = [('res_id', '=', res_id)] + domain
nid = self.search(cr, uid, domain, context=context)
- return self.get_by_id(cr, uid, nid, context=context)
+ if not nid: return False
+ record = self.browse(cr, uid, nid[0], context=context)
+ return self.get_by_record(cr, uid, record, context=context)
return False
def _get_domain_default(self, cr, uid, prop_name, model, context=None):
diff --git a/bin/addons/base/res/res_request_view.xml b/bin/addons/base/res/res_request_view.xml
index 6780c7adefd..3e83f6c3e96 100644
--- a/bin/addons/base/res/res_request_view.xml
+++ b/bin/addons/base/res/res_request_view.xml
@@ -6,7 +6,7 @@
res.requesttree
-
+
@@ -87,7 +87,7 @@
-
+ res.request.calendarres.request
@@ -109,7 +109,7 @@
{"search_default_act_to":uid}
-
+
res.request.link.formres.request.link
@@ -144,7 +144,7 @@
-
+
References Linkir.actions.act_window
@@ -154,8 +154,8 @@
-
-
+
+
res.request.history.treeres.request.history
@@ -169,7 +169,7 @@
-
+
res.request.history.formres.request.history
diff --git a/bin/addons/base/rng/view.rng b/bin/addons/base/rng/view.rng
index a21ed037705..c634ede8d5f 100644
--- a/bin/addons/base/rng/view.rng
+++ b/bin/addons/base/rng/view.rng
@@ -411,6 +411,7 @@
+
diff --git a/bin/netsvc.py b/bin/netsvc.py
index 52825c48f82..ca81372a8fe 100644
--- a/bin/netsvc.py
+++ b/bin/netsvc.py
@@ -35,6 +35,7 @@ import time
import release
from pprint import pformat
import warnings
+import heapq
class Service(object):
""" Base class for *Local* services
@@ -132,6 +133,7 @@ class ExportService(object):
LOG_NOTSET = 'notset'
LOG_DEBUG_SQL = 'debug_sql'
LOG_DEBUG_RPC = 'debug_rpc'
+LOG_DEBUG_RPC_ANSWER = 'debug_rpc_answer'
LOG_DEBUG = 'debug'
LOG_TEST = 'test'
LOG_INFO = 'info'
@@ -139,9 +141,11 @@ LOG_WARNING = 'warn'
LOG_ERROR = 'error'
LOG_CRITICAL = 'critical'
+logging.DEBUG_RPC_ANSWER = logging.DEBUG - 4
+logging.addLevelName(logging.DEBUG_RPC_ANSWER, 'DEBUG_RPC_ANSWER')
logging.DEBUG_RPC = logging.DEBUG - 2
logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
-logging.DEBUG_SQL = logging.DEBUG_RPC - 2
+logging.DEBUG_SQL = logging.DEBUG_RPC - 3
logging.addLevelName(logging.DEBUG_SQL, 'DEBUG_SQL')
logging.TEST = logging.INFO - 5
@@ -157,6 +161,7 @@ COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
LEVEL_COLOR_MAPPING = {
logging.DEBUG_SQL: (WHITE, MAGENTA),
logging.DEBUG_RPC: (BLUE, WHITE),
+ logging.DEBUG_RPC_ANSWER: (BLUE, WHITE),
logging.DEBUG: (BLUE, DEFAULT),
logging.INFO: (GREEN, DEFAULT),
logging.TEST: (WHITE, BLUE),
@@ -288,36 +293,76 @@ import tools
init_logger()
class Agent(object):
- _timers = {}
+ """Singleton that keeps track of cancellable tasks to run at a given
+ timestamp.
+ The tasks are caracterised by:
+ * a timestamp
+ * the database on which the task run
+ * the function to call
+ * the arguments and keyword arguments to pass to the function
+
+ Implementation details:
+ Tasks are stored as list, allowing the cancellation by setting
+ the timestamp to 0.
+ A heapq is used to store tasks, so we don't need to sort
+ tasks ourself.
+ """
+ __tasks = []
+ __tasks_by_db = {}
_logger = Logger()
- __logger = logging.getLogger('timer')
-
- def setAlarm(self, fn, dt, db_name, *args, **kwargs):
- wait = dt - time.time()
- if wait > 0:
- self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
- timer = threading.Timer(wait, fn, args, kwargs)
- timer.start()
- self._timers.setdefault(db_name, []).append(timer)
-
- for db in self._timers:
- for timer in self._timers[db]:
- if not timer.isAlive():
- self._timers[db].remove(timer)
+ @classmethod
+ def setAlarm(cls, function, timestamp, db_name, *args, **kwargs):
+ task = [timestamp, db_name, function, args, kwargs]
+ heapq.heappush(cls.__tasks, task)
+ cls.__tasks_by_db.setdefault(db_name, []).append(task)
@classmethod
def cancel(cls, db_name):
- """Cancel all timers for a given database. If None passed, all timers are cancelled"""
- for db in cls._timers:
- if db_name is None or db == db_name:
- for timer in cls._timers[db]:
- timer.cancel()
+ """Cancel all tasks for a given database. If None is passed, all tasks are cancelled"""
+ if db_name is None:
+ cls.__tasks, cls.__tasks_by_db = [], {}
+ else:
+ if db_name in cls.__tasks_by_db:
+ for task in cls.__tasks_by_db[db_name]:
+ task[0] = 0
@classmethod
def quit(cls):
cls.cancel(None)
+ @classmethod
+ def runner(cls):
+ """Neverending function (intended to be ran in a dedicated thread) that
+ checks every 60 seconds tasks to run.
+ """
+ current_thread = threading.currentThread()
+ while True:
+ while cls.__tasks and cls.__tasks[0][0] < time.time():
+ task = heapq.heappop(cls.__tasks)
+ timestamp, dbname, function, args, kwargs = task
+ cls.__tasks_by_db[dbname].remove(task)
+ if not timestamp:
+ # null timestamp -> cancelled task
+ continue
+ current_thread.dbname = dbname # hack hack
+ cls._logger.notifyChannel('timers', LOG_DEBUG, "Run %s.%s(*%r, **%r)" % (function.im_class.__name__, function.func_name, args, kwargs))
+ delattr(current_thread, 'dbname')
+ task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs)
+ # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
+ task_thread.daemon = False
+ task_thread.start()
+ time.sleep(1)
+ time.sleep(60)
+
+agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner")
+# the agent runner is a typical daemon thread, that will never quit and must be
+# terminated when the main process exits - with no consequence (the processing
+# threads it spawns are not marked daemon)
+agent_runner.daemon = True
+agent_runner.start()
+
+
import traceback
class Server:
@@ -420,11 +465,11 @@ class OpenERPDispatcherException(Exception):
self.traceback = traceback
class OpenERPDispatcher:
- def log(self, title, msg):
+ def log(self, title, msg, channel=logging.DEBUG_RPC, depth=2):
logger = logging.getLogger(title)
- if logger.isEnabledFor(logging.DEBUG_RPC):
- for line in pformat(msg).split('\n'):
- logger.log(logging.DEBUG_RPC, line)
+ if logger.isEnabledFor(channel):
+ for line in pformat(msg, depth=depth).split('\n'):
+ logger.log(channel, line)
def dispatch(self, service_name, method, params):
try:
@@ -433,10 +478,8 @@ class OpenERPDispatcher:
self.log('params', params)
auth = getattr(self, 'auth_provider', None)
result = ExportService.getService(service_name).dispatch(method, auth, params)
- self.log('result', result)
- # We shouldn't marshall None,
- if result == None:
- result = False
+ logger = logging.getLogger('result')
+ self.log('result', result, channel=logging.DEBUG_RPC_ANSWER, depth=(logger.isEnabledFor(logging.DEBUG_SQL) and 1 or None))
return result
except Exception, e:
self.log('exception', tools.exception_to_unicode(e))
diff --git a/bin/openerp-server.py b/bin/openerp-server.py
index 92e945acda0..9061b0ae3c8 100755
--- a/bin/openerp-server.py
+++ b/bin/openerp-server.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -16,7 +16,7 @@
# 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 .
+# along with this program. If not, see .
#
##############################################################################
@@ -27,16 +27,19 @@ OpenERP is an ERP+CRM program for small and medium businesses.
The whole source code is distributed under the terms of the
GNU Public Licence.
-(c) 2003-TODAY, Fabien Pinckaers - Tiny sprl
+(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
"""
#----------------------------------------------------------
# python imports
#----------------------------------------------------------
-import sys
+import logging
import os
-import signal
import pwd
+import signal
+import sys
+import threading
+import traceback
import release
__author__ = release.author
@@ -52,23 +55,22 @@ if pwd.getpwuid(os.getuid())[0] == 'root' :
# get logger
#----------------------------------------------------------
import netsvc
-logger = netsvc.Logger()
+logger = logging.getLogger('server')
#-----------------------------------------------------------------------
# import the tools module so that the commandline parameters are parsed
#-----------------------------------------------------------------------
import tools
-
-logger.notifyChannel("server", netsvc.LOG_INFO, "version - %s" % release.version )
+logger.info("OpenERP version - %s", release.version)
for name, value in [('addons_path', tools.config['addons_path']),
('database hostname', tools.config['db_host'] or 'localhost'),
('database port', tools.config['db_port'] or '5432'),
('database user', tools.config['db_user'])]:
- logger.notifyChannel("server", netsvc.LOG_INFO, "%s - %s" % ( name, value ))
+ logger.info("%s - %s", name, value)
# Don't allow if the connection to PostgreSQL done by postgres user
if tools.config['db_user'] == 'postgres':
- logger.notifyChannel("server", netsvc.LOG_ERROR, "%s" % ("Attempted to connect database with postgres user. This is a security flaws, aborting."))
+ logger.error("Connecting to the database as 'postgres' user is forbidden, as it present major security issues. Shutting down.")
sys.exit(1)
import time
@@ -76,7 +78,7 @@ import time
#----------------------------------------------------------
# init net service
#----------------------------------------------------------
-logger.notifyChannel("objects", netsvc.LOG_INFO, 'initialising distributed objects services')
+logger.info('initialising distributed objects services')
#---------------------------------------------------------------
# connect to the database and initialize it with base if needed
@@ -117,8 +119,7 @@ if tools.config['db_name']:
for dbname in tools.config['db_name'].split(','):
db,pool = pooler.get_db_and_pool(dbname, update_module=tools.config['init'] or tools.config['update'], pooljobs=False)
if tools.config["test_file"]:
- logger.notifyChannel("init", netsvc.LOG_INFO,
- 'loading test file %s' % (tools.config["test_file"],))
+ logger.info('loading test file %s', tools.config["test_file"])
cr = db.cursor()
tools.convert_yaml_import(cr, 'base', file(tools.config["test_file"]), {}, 'test', True)
cr.rollback()
@@ -134,16 +135,14 @@ if tools.config["translate_out"]:
msg = "language %s" % (tools.config["language"],)
else:
msg = "new language"
- logger.notifyChannel("init", netsvc.LOG_INFO,
- 'writing translation file for %s to %s' % (msg,
- tools.config["translate_out"]))
+ logger.info('writing translation file for %s to %s', msg, tools.config["translate_out"])
fileformat = os.path.splitext(tools.config["translate_out"])[-1][1:].lower()
buf = file(tools.config["translate_out"], "w")
tools.trans_export(tools.config["language"], tools.config["translate_modules"], buf, fileformat)
buf.close()
- logger.notifyChannel("init", netsvc.LOG_INFO, 'translation file written successfully')
+ logger.info('translation file written successfully')
sys.exit(0)
if tools.config["translate_in"]:
@@ -164,30 +163,64 @@ if tools.config["stop_after_init"]:
#----------------------------------------------------------
LST_SIGNALS = ['SIGINT', 'SIGTERM']
-if os.name == 'posix':
- LST_SIGNALS.extend(['SIGUSR1','SIGQUIT'])
-
SIGNALS = dict(
[(getattr(signal, sign), sign) for sign in LST_SIGNALS]
)
-def handler(signum, _):
+netsvc.quit_signals_received = 0
+
+def handler(signum, frame):
"""
:param signum: the signal number
- :param _:
+ :param frame: the interrupted stack frame or None
"""
+ netsvc.quit_signals_received += 1
+ if netsvc.quit_signals_received > 1:
+ sys.stderr.write("Forced shutdown.\n")
+ os._exit(0)
+
+def dumpstacks(signum, frame):
+ # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
+
+ id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
+ code = []
+ for threadId, stack in sys._current_frames().items():
+ code.append("\n# Thread: %s(%d)" % (id2name[threadId], threadId))
+ for filename, lineno, name, line in traceback.extract_stack(stack):
+ code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
+ if line:
+ code.append(" %s" % (line.strip()))
+
+ logging.getLogger('dumpstacks').info("\n".join(code))
+
+for signum in SIGNALS:
+ signal.signal(signum, handler)
+
+if os.name == 'posix':
+ signal.signal(signal.SIGQUIT, dumpstacks)
+
+def quit():
netsvc.Agent.quit()
netsvc.Server.quitAll()
if tools.config['pidfile']:
os.unlink(tools.config['pidfile'])
- logger.notifyChannel('shutdown', netsvc.LOG_INFO,
- "Shutdown Server! - %s" % ( SIGNALS[signum], ))
- logger.shutdown()
- sys.exit(0)
+ logger = logging.getLogger('shutdown')
+ logger.info("Initiating OpenERP Server shutdown")
+ logger.info("Hit CTRL-C again or send a second signal to immediately terminate the server...")
+ logging.shutdown()
-for signum in SIGNALS:
- signal.signal(signum, handler)
+ # manually join() all threads before calling sys.exit() to allow a second signal
+ # to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
+ # threading.Thread.join() should not mask signals (at least in python 2.5)
+ for thread in threading.enumerate():
+ if thread != threading.currentThread() and not thread.daemon:
+ while thread.isAlive():
+ # need a busyloop here as thread.join() masks signals
+ # and would present the forced shutdown
+ thread.join(0.05)
+ time.sleep(0.05)
+ sys.exit(0)
if tools.config['pidfile']:
fd = open(tools.config['pidfile'], 'w')
@@ -195,13 +228,13 @@ if tools.config['pidfile']:
fd.write(pidtext)
fd.close()
-
netsvc.Server.startAll()
-logger.notifyChannel("web-services", netsvc.LOG_INFO,
- 'the server is running, waiting for connections...')
+logger.info('OpenERP server is running, waiting for connections...')
-while True:
+while netsvc.quit_signals_received == 0:
time.sleep(60)
+quit()
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/osv/fields.py b/bin/osv/fields.py
index 97a6f130f44..64432ba15d7 100644
--- a/bin/osv/fields.py
+++ b/bin/osv/fields.py
@@ -303,10 +303,9 @@ class many2one(_column):
return result
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
- if not context:
- context = {}
- if not values:
- values = {}
+ context = context or {}
+ values = values or {}
+
res = {}
for r in values:
res[r['id']] = r[name]
@@ -315,21 +314,14 @@ class many2one(_column):
obj = obj.pool.get(self._obj)
# build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
- from orm import except_orm
- names = {}
- for record in list(set(filter(None, res.values()))):
- try:
- record_name = dict(obj.name_get(cr, user, [record], context))
- except except_orm:
- record_name = {}
- record_name[record] = '// Access Denied //'
- names.update(record_name)
-
- for r in res.keys():
- if res[r] and res[r] in names:
- res[r] = (res[r], names[res[r]])
+ # we use uid=1 because the visibility of a many2one field value (just id and name)
+ # must be the access right of the parent form and not the linked object itself.
+ records = dict(obj.name_get(cr, 1, list(set(filter(None, res.values()))), context=context))
+ for id in res:
+ if res[id] in records:
+ res[id] = (res[id], records[res[id]])
else:
- res[r] = False
+ res[id] = False
return res
def set(self, cr, obj_src, id, field, values, user=None, context=None):
@@ -785,7 +777,7 @@ class related(function):
if not ids: return {}
relation = obj._name
if self._type in ('one2many', 'many2many'):
- res = {}.fromkeys(ids, [])
+ res = dict([(i, []) for i in ids])
else:
res = {}.fromkeys(ids, False)
@@ -881,38 +873,44 @@ class serialized(_column):
super(serialized, self).__init__(string=string, **args)
+# TODO: review completly this class for speed improvement
class property(function):
def _get_default(self, obj, cr, uid, prop_name, context=None):
- from orm import browse_record
- prop = obj.pool.get('ir.property')
- domain = prop._get_domain_default(cr, uid, prop_name, obj._name, context)
- ids = prop.search(cr, uid, domain, order='company_id', context=context)
- if not ids:
- return False
+ return self._get_defaults(obj, cr, uid, [prop_name], context=None)[0][prop_name]
- default_value = prop.get_by_id(cr, uid, ids, context=context)
- if isinstance(default_value, browse_record):
- return default_value.id
- return default_value or False
+ def _get_defaults(self, obj, cr, uid, prop_name, context=None):
+ prop = obj.pool.get('ir.property')
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name), ('res_id','=',False)]
+ ids = prop.search(cr, uid, domain, context=context)
+ replaces = {}
+ default_value = {}.fromkeys(prop_name, False)
+ for prop_rec in prop.browse(cr, uid, ids, context=context):
+ if default_value.get(prop_rec.fields_id.name, False):
+ continue
+ value = prop.get_by_record(cr, uid, prop_rec, context=context) or False
+ default_value[prop_rec.fields_id.name] = value
+ if value and (prop_rec.type == 'many2one'):
+ replaces.setdefault(value._name, {})
+ replaces[value._name][value.id] = True
+ return default_value, replaces
def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
prop = obj.pool.get('ir.property')
vids = [obj._name + ',' + str(oid) for oid in ids]
- domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
- if domain is not None:
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
+ #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
+ if vids:
domain = [('res_id', 'in', vids)] + domain
- return prop.search(cr, uid, domain, context=context)
- else:
- return []
-
+ return prop.search(cr, uid, domain, context=context)
+ # TODO: to rewrite more clean
def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
if context is None:
context = {}
- nids = self._get_by_id(obj, cr, uid, prop_name, [id], context)
+ nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
if nids:
cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
@@ -933,29 +931,37 @@ class property(function):
'company_id': cid,
'fields_id': def_id,
'type': self._type,
- }, context=context)
+ }, context=context)
return False
def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None):
- from orm import browse_record
properties = obj.pool.get('ir.property')
-
- default_val = self._get_default(obj, cr, uid, prop_name, context)
-
- nids = self._get_by_id(obj, cr, uid, prop_name, ids, context)
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
+ domain += [('res_id','in', [obj._name + ',' + str(oid) for oid in ids])]
+ nids = properties.search(cr, uid, domain, context=context)
+ default_val,replaces = self._get_defaults(obj, cr, uid, prop_name, context)
res = {}
for id in ids:
- res[id] = default_val
- for prop in properties.browse(cr, uid, nids):
- value = prop.get_by_id(context=context)
- if isinstance(value, browse_record):
- if not value.exists():
- cr.execute('DELETE FROM ir_property WHERE id=%s', (prop.id,))
- continue
- value = value.id
- res[prop.res_id.id] = value or False
+ res[id] = default_val.copy()
+
+ brs = properties.browse(cr, uid, nids, context=context)
+ for prop in brs:
+ value = properties.get_by_record(cr, uid, prop, context=context)
+ res[prop.res_id.id][prop.fields_id.name] = value or False
+ if value and (prop.type == 'many2one'):
+ replaces.setdefault(value._name, {})
+ replaces[value._name][value.id] = True
+
+ for rep in replaces:
+ replaces[rep] = dict(obj.pool.get(rep).name_get(cr, uid, replaces[rep].keys(), context=context))
+
+ for prop in prop_name:
+ for id in ids:
+ if res[id][prop] and hasattr(res[id][prop], '_name'):
+ res[id][prop] = (res[id][prop].id , replaces[res[id][prop]._name].get(res[id][prop].id, False))
+
return res
@@ -972,7 +978,7 @@ class property(function):
# TODO remove obj_prop parameter (use many2one type)
self.field_id = {}
function.__init__(self, self._fnct_read, False, self._fnct_write,
- obj_prop, **args)
+ obj_prop, multi='properties', **args)
def restart(self):
self.field_id = {}
diff --git a/bin/osv/orm.py b/bin/osv/orm.py
index 50006603bcf..64b118f3e15 100644
--- a/bin/osv/orm.py
+++ b/bin/osv/orm.py
@@ -1541,6 +1541,14 @@ class orm_template(object):
else:
for n in node.getiterator(node2.tag):
res = True
+ if node2.tag == 'field':
+ # only compare field names, a field can be only once in a given view
+ # at a given level (and for multilevel expressions, we should use xpath
+ # inheritance spec anyway)
+ if node2.get('name') == n.get('name'):
+ return n
+ else:
+ continue
for attr in node2.attrib:
if attr == 'position':
continue
@@ -1809,10 +1817,10 @@ class orm_template(object):
def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
"""
- Private implementation of search() method, allowing specifying the uid to use for the access right check.
+ Private implementation of search() method, allowing specifying the uid to use for the access right check.
This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors,
by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules!
-
+
:param access_rights_uid: optional user ID to use when checking access rights
(not for ir.rules, this is only for ir.model.access)
"""
@@ -1841,7 +1849,7 @@ class orm_template(object):
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
"""
- Search for records and their display names according to a search domain.
+ Search for records and their display names according to a search domain.
:param cr: database cursor
:param user: current user id
@@ -2207,7 +2215,7 @@ class orm(orm_template):
previous_tables = list(tables)
if self._apply_ir_rules(cr, uid, where_clause, where_clause_params, tables, 'read', model_name=inherited_model, context=context):
# if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
- # list of table in case the inherited table was not in the list before (as that means the corresponding
+ # list of table in case the inherited table was not in the list before (as that means the corresponding
# JOIN(s) was(were) not present)
self._inherits_join_add(inherited_model, previous_tables, where_clause)
tables = list(set(tables).union(set(previous_tables)))
@@ -2215,7 +2223,7 @@ class orm(orm_template):
# Take care of adding join(s) if groupby is an '_inherits'ed field
groupby_list = groupby
if groupby:
- if groupby and isinstance(groupby, list):
+ if isinstance(groupby, list):
groupby = groupby[0]
tables, where_clause, qfield = self._inherits_join_calc(groupby, tables, where_clause)
@@ -2234,14 +2242,14 @@ class orm(orm_template):
flist = ''
group_by = groupby
if groupby:
- if fget.get(groupby, False):
+ if fget.get(groupby):
if fget[groupby]['type'] in ('date', 'datetime'):
flist = "to_char(%s,'yyyy-mm') as %s " % (groupby, groupby)
groupby = "to_char(%s,'yyyy-mm')" % (groupby)
else:
flist = groupby
else:
- # Don't allow arbitrary values, as this would be a SQL injection vector!
+ # Don't allow arbitrary values, as this would be a SQL injection vector!
raise except_orm(_('Invalid group_by'),
_('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(groupby,))
@@ -2256,10 +2264,7 @@ class orm(orm_template):
flist += ','
flist += operator+'('+f+') as '+f
- if groupby:
- gb = ' group by '+groupby
- else:
- gb = ''
+ gb = groupby and (' group by '+groupby) or ''
cr.execute('select min(%s.id) as id,' % self._table + flist + ' from ' + ','.join(tables) + where_clause + gb + limit_str + offset_str, where_clause_params)
alldata = {}
groupby = group_by
@@ -2268,7 +2273,18 @@ class orm(orm_template):
if val == None: r[fld] = False
alldata[r['id']] = r
del r['id']
- data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context)
+ if groupby and fget[groupby]['type'] == 'many2one':
+ data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=groupby, context=context)
+ # the IDS of the records that has groupby field value = False or ''
+ # should be added too
+ data_ids += filter(lambda x:x not in data_ids, alldata.keys())
+ data = self.read(cr, uid, data_ids, groupby and [groupby] or ['id'], context=context)
+ # restore order of the search as read() uses the default _order (this is only for groups, so the size of data_read shoud be small):
+ data.sort(lambda x,y: cmp(data_ids.index(x['id']), data_ids.index(y['id'])))
+ else:
+ data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context)
+ if groupby:
+ data.sort(lambda x,y:cmp(x[groupby],y[groupby]))
for d in data:
if groupby:
d['__domain'] = [(groupby, '=', alldata[d['id']][groupby] or False)] + domain
@@ -2314,9 +2330,9 @@ class orm(orm_template):
:param tables: list of table._table names enclosed in double quotes as returned
by _where_calc()
:param where_clause: current list of WHERE clause params
- :return: (table, where_clause, qualified_field) where ``table`` and ``where_clause`` are the updated
+ :return: (tables, where_clause, qualified_field) where ``tables`` and ``where_clause`` are the updated
versions of the parameters, and ``qualified_field`` is the qualified name of ``field``
- in the form ``table.field``, to be referenced in queries.
+ in the form ``table.field``, to be referenced in queries.
"""
current_table = self
while field in current_table._inherit_fields and not field in current_table._columns:
@@ -2988,19 +3004,20 @@ class orm(orm_template):
column = self._inherit_fields[key][2]
else:
continue
- if v and column._type == 'reference':
- model_name, ref_id = v.split(',', 1)
- model = self.pool.get(model_name)
- if not model:
- reset = True
- else:
- cr.execute('SELECT count(1) FROM "%s" WHERE id=%%s' % (model._table,), (ref_id,))
- reset = not cr.fetchone()[0]
- if reset:
- if column._classic_write:
- query = 'UPDATE "%s" SET "%s"=NULL WHERE id=%%s' % (self._table, key)
- cr.execute(query, (r['id'],))
- r[key] = False
+# TODO: removed this, it's too slow
+# if v and column._type == 'reference':
+# model_name, ref_id = v.split(',', 1)
+# model = self.pool.get(model_name)
+# if not model:
+# reset = True
+# else:
+# cr.execute('SELECT count(1) FROM "%s" WHERE id=%%s' % (model._table,), (ref_id,))
+# reset = not cr.fetchone()[0]
+# if reset:
+# if column._classic_write:
+# query = 'UPDATE "%s" SET "%s"=NULL WHERE id=%%s' % (self._table, key)
+# cr.execute(query, (r['id'],))
+# r[key] = False
if isinstance(ids, (int, long, dict)):
return result and result[0] or False
@@ -3888,13 +3905,13 @@ class orm(orm_template):
:param args: the domain to compute
:type args: list
:param active_test: whether the default filtering of records with ``active``
- field set to ``False`` should be applied.
+ field set to ``False`` should be applied.
:return: tuple with 3 elements: (where_clause, where_clause_params, tables) where
``where_clause`` contains a list of where clause elements (to be joined with 'AND'),
``where_clause_params`` is a list of parameters to be passed to the db layer
for the where_clause expansion, and ``tables`` is the list of double-quoted
- table names that need to be included in the FROM clause.
- :rtype: tuple
+ table names that need to be included in the FROM clause.
+ :rtype: tuple
"""
if not context:
context = {}
@@ -3926,7 +3943,7 @@ class orm(orm_template):
def _check_qorder(self, word):
if not regex_order.match(word):
- raise except_orm(_('AccessError'), _('Bad query.'))
+ raise except_orm(_('AccessError'), _('Invalid "order" specified. A valid "order" specification is a comma-separated list of valid field names (optionally followed by asc/desc for the direction)'))
return True
def _apply_ir_rules(self, cr, uid, where_clause, where_clause_params, tables, mode='read', model_name=None, context=None):
@@ -3939,7 +3956,7 @@ class orm(orm_template):
in ``where_clause``
:param model_name: optional name of the model whose ir.rules should be applied (default:``self._name``)
This could be useful for inheritance for example, but there is no provision to include
- the appropriate JOIN for linking the current model to the one referenced in model_name.
+ the appropriate JOIN for linking the current model to the one referenced in model_name.
:return: True if additional clauses where applied.
"""
added_clause, added_params, added_tables = self.pool.get('ir.rule').domain_get(cr, uid, model_name or self._name, mode, context=context)
@@ -3952,13 +3969,86 @@ class orm(orm_template):
return True
return False
+ def _generate_m2o_order_by(self, order_field, tables, where_clause):
+ """
+ Add possibly missing JOIN and generate the ORDER BY clause for m2o fields,
+ either native m2o fields or function/related fields that are stored, including
+ intermediate JOINs for inheritance if required.
+ """
+ if order_field not in self._columns and order_field in self._inherit_fields:
+ # also add missing joins for reaching the table containing the m2o field
+ tables, where_clause, qualified_field = self._inherits_join_calc(order_field, tables, where_clause)
+ order_field_column = self._inherit_fields[order_field][2]
+ else:
+ qualified_field = '"%s"."%s"' % (self._table, order_field)
+ order_field_column = self._columns[order_field]
+
+ assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()'
+ assert order_field_column._classic_write or getattr(order_field_column, 'store', False), "Many2one function/related fields must be stored to be used as ordering fields"
+
+ # figure out the applicable order_by for the m2o
+ dest_model = self.pool.get(order_field_column._obj)
+ m2o_order = dest_model._order
+ if not regex_order.match(m2o_order):
+ # _order is complex, can't use it here, so we default to _rec_name
+ m2o_order = dest_model._rec_name
+ else:
+ # extract the first field name, to be able to qualify it and add desc/asc
+ m2o_order = m2o_order.split(",",1)[0].strip().split(" ",1)[0]
+
+ # the perhaps missing join:
+ quoted_model_table = '"%s"' % dest_model._table
+ if quoted_model_table not in tables:
+ tables.append(quoted_model_table)
+ where_clause.append('%s = %s.id' % (qualified_field, quoted_model_table))
+
+ return ('%s.%s' % (quoted_model_table, m2o_order), tables, where_clause)
+
+
+ def _generate_order_by(self, order_spec, tables, where_clause):
+ """
+ Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be
+ a comma-separated list of valid field names, optionally followed by an ASC or DESC direction.
+
+ :raise" except_orm in case order_spec is malformed
+ """
+ order_by_clause = self._order
+ if order_spec:
+ order_by_elements = []
+ self._check_qorder(order_spec)
+ for order_part in order_spec.split(','):
+ order_split = order_part.strip().split(' ')
+ order_field = order_split[0].strip()
+ order_direction = order_split[1].strip() if len(order_split) == 2 else ''
+ if order_field in self._columns:
+ order_column = self._columns[order_field]
+ if order_column._classic_read:
+ order_by_clause = '"%s"."%s"' % (self._table, order_field)
+ elif order_column._type == 'many2one':
+ order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ else:
+ continue # ignore non-readable or "non-joignable" fields
+ elif order_field in self._inherit_fields:
+ parent_obj = self.pool.get(self._inherit_fields[order_field][0])
+ order_column = parent_obj._columns[order_field]
+ if order_column._classic_read:
+ tables, where_clause, order_by_clause = self._inherits_join_calc(order_field, tables, where_clause)
+ elif order_column._type == 'many2one':
+ order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ else:
+ continue # ignore non-readable or "non-joignable" fields
+ order_by_elements.append("%s %s" % (order_by_clause, order_direction))
+ order_by_clause = ",".join(order_by_elements)
+
+ return order_by_clause and (' ORDER BY %s ' % order_by_clause) or ''
+
def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
"""
- Private implementation of search() method, allowing specifying the uid to use for the access right check.
+ Private implementation of search() method, allowing specifying the uid to use for the access right check.
This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors,
by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules!
This is ok at the security level because this method is private and not callable through XML-RPC.
-
+
:param access_rights_uid: optional user ID to use when checking access rights
(not for ir.rules, this is only for ir.model.access)
"""
@@ -3982,35 +4072,17 @@ class orm(orm_template):
tables = list(set(tables).union(set(previous_tables)))
where = where_clause
-
- order_by = self._order
- if order:
- self._check_qorder(order)
- o = order.split(' ')[0]
- if (o in self._columns):
- # we can only do efficient sort if the fields is stored in database
- if getattr(self._columns[o], '_classic_read'):
- order_by = order
- elif (o in self._inherit_fields):
- parent_obj = self.pool.get(self._inherit_fields[o][0])
- if getattr(parent_obj._columns[o], '_classic_read'):
- # Allowing inherits'ed field for server side sorting, if they can be sorted by the dbms
- inherited_tables, inherit_join, order_by = self._inherits_join_calc(o, tables, where_clause)
-
+ order_by = self._generate_order_by(order, tables, where_clause)
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
-
- if where:
- where_str = " WHERE %s" % " AND ".join(where)
- else:
- where_str = ""
+ where_str = where and (" WHERE %s" % " AND ".join(where)) or ''
if count:
cr.execute('select count(%s.id) from ' % self._table +
','.join(tables) + where_str + limit_str + offset_str, where_clause_params)
res = cr.fetchall()
return res[0][0]
- cr.execute('select %s.id from ' % self._table + ','.join(tables) + where_str +' order by '+order_by+limit_str+offset_str, where_clause_params)
+ cr.execute('select %s.id from ' % self._table + ','.join(tables) + where_str + order_by + limit_str+offset_str, where_clause_params)
res = cr.fetchall()
return [x[0] for x in res]
diff --git a/bin/report/custom.py b/bin/report/custom.py
index b53be34cfdb..4b02901bdb2 100644
--- a/bin/report/custom.py
+++ b/bin/report/custom.py
@@ -73,7 +73,7 @@ class report_custom(report_int):
for cond in conditions:
if cond and cond[0]:
c = cond[0]
- temp = c[0](eval('obj.'+c[1]))
+ temp = c[0](eval('obj.'+c[1],{'obj': obj}))
if not eval('\''+temp+'\''+' '+c[2]+' '+'\''+str(c[3])+'\''):
tobreak = True
if tobreak:
@@ -87,7 +87,7 @@ class report_custom(report_int):
row_canvas[i]=False
elif len(fields[i])==1:
if not isinstance(obj, browse_null):
- row.append(str(eval('obj.'+fields[i][0])))
+ row.append(str(eval('obj.'+fields[i][0],{'obj': obj})))
else:
row.append(None)
else:
@@ -107,7 +107,7 @@ class report_custom(report_int):
else:
key = levels.keys()
for l in key:
- objs = eval('obj.'+l)
+ objs = eval('obj.'+l,{'obj': obj})
if not isinstance(objs, browse_record_list) and type(objs) <> type([]):
objs = [objs]
field_new = []
@@ -192,7 +192,7 @@ class report_custom(report_int):
def build_tree(obj, level, depth):
res = self._row_get(cr, uid,[obj], new_fields, new_cond)
level.append(depth)
- new_obj = eval('obj.'+report['field_parent'][1])
+ new_obj = eval('obj.'+report['field_parent'][1],{'obj': obj})
if not isinstance(new_obj, list) :
new_obj = [new_obj]
for o in new_obj:
diff --git a/bin/service/http_server.py b/bin/service/http_server.py
index 7b62dda6c05..86e5943f170 100644
--- a/bin/service/http_server.py
+++ b/bin/service/http_server.py
@@ -137,7 +137,7 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server):
_RealProto = '??'
def __init__(self, interface, port, handler):
- threading.Thread.__init__(self)
+ threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
netsvc.Server.__init__(self)
self.__port = port
self.__interface = interface
diff --git a/bin/service/netrpc_server.py b/bin/service/netrpc_server.py
index 80cb968c8e9..9d25c98083a 100644
--- a/bin/service/netrpc_server.py
+++ b/bin/service/netrpc_server.py
@@ -102,7 +102,7 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
class TinySocketServerThread(threading.Thread,netsvc.Server):
def __init__(self, interface, port, secure=False):
- threading.Thread.__init__(self, name="Net-RPC socket")
+ threading.Thread.__init__(self, name="NetRPCDaemon-%d"%port)
netsvc.Server.__init__(self)
self.__port = port
self.__interface = interface
diff --git a/bin/sql_db.py b/bin/sql_db.py
index 193a709dd8d..6c2c17beeb3 100644
--- a/bin/sql_db.py
+++ b/bin/sql_db.py
@@ -113,8 +113,6 @@ class Cursor(object):
self.__logger.warn(query)
self.__logger.warn("SQL queries cannot contain %d or %f anymore. "
"Use only %s")
- if params:
- query = query.replace('%d', '%s').replace('%f', '%s')
if self.sql_log:
now = mdt.now()
@@ -167,6 +165,7 @@ class Cursor(object):
sqllogitems = sqllogs[type].items()
sqllogitems.sort(key=lambda k: k[1][1])
self.__logger.log(logging.DEBUG_SQL, "SQL LOG %s:", type)
+ sqllogitems.sort(lambda x,y: cmp(x[1][0], y[1][0]))
for r in sqllogitems:
delay = timedelta(microseconds=r[1][1])
self.__logger.log(logging.DEBUG_SQL, "table: %s: %s/%s",
diff --git a/bin/tools/config.py b/bin/tools/config.py
index 17959ec133b..2964870d427 100644
--- a/bin/tools/config.py
+++ b/bin/tools/config.py
@@ -98,7 +98,7 @@ class configmanager(object):
self.has_ssl = check_ssl()
self._LOGLEVELS = dict([(getattr(netsvc, 'LOG_%s' % x), getattr(logging, x))
- for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'TEST', 'DEBUG', 'DEBUG_RPC', 'DEBUG_SQL', 'NOTSET')])
+ for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'TEST', 'DEBUG', 'DEBUG_RPC', 'DEBUG_SQL', 'DEBUG_RPC_ANSWER','NOTSET')])
version = "%s %s" % (release.description, release.version)
self.parser = parser = optparse.OptionParser(version=version)
@@ -221,9 +221,6 @@ class configmanager(object):
security = optparse.OptionGroup(parser, 'Security-related options')
security.add_option('--no-database-list', action="store_false", dest='list_db', help="disable the ability to return the list of databases")
- security.add_option('--enable-code-actions', action='store_true',
- dest='server_actions_allow_code', default=False,
- help='Enables server actions of state "code". Warning, this is a security risk.')
parser.add_option_group(security)
def parse_config(self):
@@ -287,7 +284,7 @@ class configmanager(object):
keys = [
'language', 'translate_out', 'translate_in', 'debug_mode', 'smtp_ssl',
'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog',
- 'list_db', 'server_actions_allow_code', 'xmlrpcs',
+ 'list_db', 'xmlrpcs',
'test_file', 'test_disable', 'test_commit', 'test_report_directory'
]
diff --git a/bin/tools/misc.py b/bin/tools/misc.py
index 2baf7c0c227..ad422411d29 100644
--- a/bin/tools/misc.py
+++ b/bin/tools/misc.py
@@ -62,9 +62,7 @@ _logger = logging.getLogger('tools')
def init_db(cr):
import addons
f = addons.get_module_resource('base', 'base.sql')
- for line in file_open(f).read().split(';'):
- if (len(line)>0) and (not line.isspace()):
- cr.execute(line)
+ cr.execute(file_open(f).read())
cr.commit()
for i in addons.get_modules():
@@ -421,6 +419,13 @@ def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False,
:return: True if the mail was delivered successfully to the smtp,
else False (+ exception logged)
"""
+ class WriteToLogger(object):
+ def __init__(self):
+ self.logger = netsvc.Logger()
+
+ def write(self, s):
+ self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
+
if openobject_id:
message['Message-Id'] = generate_tracking_message_id(openobject_id)
@@ -536,13 +541,6 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
msg.attach(part)
- class WriteToLogger(object):
- def __init__(self):
- self.logger = netsvc.Logger()
-
- def write(self, s):
- self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
-
return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
#----------------------------------------------------------
diff --git a/bin/tools/yaml_import.py b/bin/tools/yaml_import.py
index 87fd2ff432c..814779b3f36 100644
--- a/bin/tools/yaml_import.py
+++ b/bin/tools/yaml_import.py
@@ -70,14 +70,16 @@ def is_url(node):
def is_eval(node):
return isinstance(node, yaml_tag.Eval)
-
+
def is_ref(node):
return isinstance(node, yaml_tag.Ref) \
or _is_yaml_mapping(node, yaml_tag.Ref)
-
+
def is_ir_set(node):
return _is_yaml_mapping(node, yaml_tag.IrSet)
+def is_string(node):
+ return isinstance(node, basestring)
class TestReport(object):
def __init__(self):
@@ -374,6 +376,14 @@ class YamlInterpreter(object):
elif column._type == "many2many":
ids = [self.get_id(xml_id) for xml_id in expression]
value = [(6, 0, ids)]
+ elif column._type == "date" and is_string(expression):
+ # enforce ISO format for string date values, to be locale-agnostic during tests
+ time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
+ value = expression
+ elif column._type == "datetime" and is_string(expression):
+ # enforce ISO format for string datetime values, to be locale-agnostic during tests
+ time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
+ value = expression
else: # scalar field
if is_eval(expression):
value = self.process_eval(expression)
diff --git a/setup.py b/setup.py
index 28d8e8d516d..8831e6f8da6 100755
--- a/setup.py
+++ b/setup.py
@@ -51,6 +51,38 @@ if 'bdist_rpm' in sys.argv:
# get python short version
py_short_version = '%s.%s' % sys.version_info[:2]
+# backports os.walk with followlinks from python 2.6
+def walk_followlinks(top, topdown=True, onerror=None, followlinks=False):
+ from os.path import join, isdir, islink
+ from os import listdir, error
+
+ try:
+ names = listdir(top)
+ except error, err:
+ if onerror is not None:
+ onerror(err)
+ return
+
+ dirs, nondirs = [], []
+ for name in names:
+ if isdir(join(top, name)):
+ dirs.append(name)
+ else:
+ nondirs.append(name)
+
+ if topdown:
+ yield top, dirs, nondirs
+ for name in dirs:
+ path = join(top, name)
+ if followlinks or not islink(path):
+ for x in walk_followlinks(path, topdown, onerror, followlinks):
+ yield x
+ if not topdown:
+ yield top, dirs, nondirs
+
+if sys.version_info < (2, 6):
+ os.walk = walk_followlinks
+
def find_addons():
for root, _, names in os.walk(join('bin', 'addons'), followlinks=True):
if '__openerp__.py' in names or '__terp__.py' in names:
@@ -64,6 +96,8 @@ def data_files():
files.append((root, [join(root, name) for name in names]))
for root, _, names in os.walk('doc'):
files.append((root, [join(root, name) for name in names]))
+ for root, _, names in os.walk('pixmaps'):
+ files.append((root, [join(root, name) for name in names]))
files.append(('.', [join('bin', 'import_xml.rng'),
join('bin', 'server.pkey'),
join('bin', 'server.cert')]))
@@ -139,7 +173,7 @@ options = {
"encodings", "dateutil", "wizard", "pychart", "PIL", "pyparsing",
"pydot", "asyncore","asynchat", "reportlab", "vobject",
"HTMLParser", "select", "mako", "poplib",
- "imaplib", "smtplib", "email", "yaml","pywebdav",
+ "imaplib", "smtplib", "email", "yaml", "DAV",
],
"excludes" : ["Tkconstants","Tkinter","tcl"],
}