diff --git a/openerp/addons/base/i18n/cs.po b/openerp/addons/base/i18n/cs.po index 17f6ff64933..cef03afdd3c 100644 --- a/openerp/addons/base/i18n/cs.po +++ b/openerp/addons/base/i18n/cs.po @@ -1,20 +1,21 @@ # Translation of OpenERP Server. # This file contains the translation of the following modules: -# * base +# * base # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 5.0.4\n" +"Project-Id-Version: openobject-server\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2011-01-11 11:14+0000\n" -"PO-Revision-Date: 2010-06-06 04:19+0000\n" -"Last-Translator: Vladimír Burian \n" +"PO-Revision-Date: 2011-07-15 18:58+0000\n" +"Last-Translator: Chronos \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-02 04:55+0000\n" -"X-Generator: Launchpad (build 13168)\n" +"X-Launchpad-Export-Date: 2011-07-16 04:53+0000\n" +"X-Generator: Launchpad (build 13405)\n" +"X-Poedit-Language: Czech\n" #. module: base #: view:ir.filters:0 @@ -585,7 +586,7 @@ msgstr "Srbsko" #. module: base #: selection:ir.translation,type:0 msgid "Wizard View" -msgstr "" +msgstr "Zobrazení průvodce" #. module: base #: model:res.country,name:base.kh @@ -1181,7 +1182,7 @@ msgstr "res.request.link" #. module: base #: field:ir.actions.wizard,name:0 msgid "Wizard Info" -msgstr "" +msgstr "Informace průvodce" #. module: base #: view:base.language.export:0 @@ -1196,6 +1197,8 @@ msgid "" "Do not display this log if it belongs to the same object the user is working " "on" msgstr "" +"Nezobrazovat tento záznam, pokud patří ke stejnému objektu, se kterým " +"pracuje uživatel" #. module: base #: model:res.country,name:base.tp @@ -1253,7 +1256,7 @@ msgstr "Maledivy" #. module: base #: help:ir.values,res_id:0 msgid "Keep 0 if the action must appear on all resources." -msgstr "" +msgstr "Nechejte 0 pokud se musí akce objevit u všech zdrojů." #. module: base #: model:ir.model,name:base.model_ir_rule @@ -2501,7 +2504,7 @@ msgstr "Kontrola Ean" #. module: base #: field:res.partner,vat:0 msgid "VAT" -msgstr "DPH" +msgstr "DIČ" #. module: base #: view:res.lang:0 @@ -3588,7 +3591,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_translation_app msgid "Application Terms" -msgstr "" +msgstr "Podmínky aplikace" #. module: base #: help:res.config.users,context_tz:0 @@ -3818,7 +3821,7 @@ msgstr "Vlastník bankovního účtu" #. module: base #: model:ir.actions.act_window,name:base.act_values_form msgid "Client Actions Connections" -msgstr "" +msgstr "Nastavení akcí klienta" #. module: base #: field:ir.attachment,res_name:0 @@ -3880,7 +3883,7 @@ msgstr "Měsíc" #. module: base #: model:res.country,name:base.my msgid "Malaysia" -msgstr "" +msgstr "Malaysie" #. module: base #: view:base.language.install:0 @@ -4282,7 +4285,7 @@ msgstr "Zděděný pohled" #. module: base #: view:ir.translation:0 msgid "Source Term" -msgstr "" +msgstr "Podmínky zdrojového kódu" #. module: base #: model:ir.ui.menu,name:base.menu_main_pm @@ -7262,6 +7265,8 @@ msgid "" "Changing the type of a column is not yet supported. Please drop it and " "create it again!" msgstr "" +"Změna typu sloupce ještě není podporována. Prosíme smažte jej a opět " +"vytvořte!" #. module: base #: field:ir.ui.view_sc,user_id:0 @@ -8821,7 +8826,7 @@ msgstr "base.module.configuration" #: field:base.language.export,name:0 #: field:ir.attachment,datas_fname:0 msgid "Filename" -msgstr "" +msgstr "Jméno souboru" #. module: base #: field:ir.model,access_ids:0 @@ -8899,7 +8904,7 @@ msgstr "Bulharština / български език" #. module: base #: model:ir.ui.menu,name:base.menu_aftersale msgid "After-Sale Services" -msgstr "" +msgstr "Poprodejní služby" #. module: base #: view:ir.actions.todo:0 diff --git a/openerp/addons/base/i18n/es_CL.po b/openerp/addons/base/i18n/es_CL.po index 677954e0eb0..dca3c608a1a 100644 --- a/openerp/addons/base/i18n/es_CL.po +++ b/openerp/addons/base/i18n/es_CL.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2011-01-11 11:14+0000\n" -"PO-Revision-Date: 2011-06-06 17:53+0000\n" -"Last-Translator: Hector Rojas (doingIT.cl) \n" +"PO-Revision-Date: 2011-07-11 21:37+0000\n" +"Last-Translator: doingit.cl \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-02 05:03+0000\n" +"X-Launchpad-Export-Date: 2011-07-12 04:35+0000\n" "X-Generator: Launchpad (build 13168)\n" #. module: base @@ -47,6 +47,8 @@ msgid "" "The second argument of the many2many field %s must be a SQL table !You used " "%s, which is not a valid SQL table name." msgstr "" +"¡El segundo argumento del campo many2many %s debe ser una tabla SQL! Has " +"utilizado %s, que no es un nombre de tabla SQL válido." #. module: base #: view:ir.values:0 @@ -278,7 +280,7 @@ msgstr "Nombre de asistente" #: code:addons/orm.py:2160 #, python-format msgid "Invalid group_by" -msgstr "" +msgstr "group_by Invalido" #. module: base #: field:res.partner,credit_limit:0 diff --git a/openerp/addons/base/ir/ir_config_parameter.py b/openerp/addons/base/ir/ir_config_parameter.py index b1688b9a669..f8c0348e8f6 100644 --- a/openerp/addons/base/ir/ir_config_parameter.py +++ b/openerp/addons/base/ir/ir_config_parameter.py @@ -62,7 +62,7 @@ class ir_config_parameter(osv.osv): if not ids: self.set_param(cr, 1, key, func()) - def get_param(self, cr, uid, key, context=None): + def get_param(self, cr, uid, key, default=False, context=None): """ Get the value of a parameter. @param key: The key of the parameter. @@ -72,7 +72,7 @@ class ir_config_parameter(osv.osv): """ ids = self.search(cr, uid, [('key','=',key)], context=context) if not ids: - return False + return default param = self.browse(cr, uid, ids[0], context=context) value = param.value return value diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index d8a4edfe0df..d81248a735d 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -131,13 +131,6 @@ class res_currency(osv.osv): else: return (from_amount * rate) - def name_search(self, cr, uid, name, args=[], operator='ilike', context={}, limit=100): - args2 = args[:] - if name: - args += [('name', operator, name)] - ids = self.search(cr, uid, args, limit=limit) - res = self.name_get(cr, uid, ids, context) - return res res_currency() class res_currency_rate(osv.osv): diff --git a/openerp/addons/base/rng/view.rng b/openerp/addons/base/rng/view.rng index cd2aac5cb13..943b533e2e3 100644 --- a/openerp/addons/base/rng/view.rng +++ b/openerp/addons/base/rng/view.rng @@ -354,6 +354,7 @@ + diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 17ae2dce3ef..6bb45e77598 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -88,14 +88,14 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if new_query: cr.execute(new_query) - def load_init_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'init_xml') + def load_init_xml(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'init_xml') - def load_update_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'update_xml') + def load_update_xml(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'update_xml') - def load_demo_xml(cr, m, idref, mode): - _load_data(cr, m, idref, mode, 'demo_xml') + def load_demo_xml(cr, module_name, idref, mode): + _load_data(cr, module_name, idref, mode, 'demo_xml') def load_data(cr, module_name, idref, mode): _load_data(cr, module_name, idref, mode, 'data') @@ -152,38 +152,38 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= status = {} processed_modules = [] + loaded_modules = [] statusi = 0 pool = pooler.get_pool(cr.dbname) migrations = openerp.modules.migration.MigrationManager(cr, graph) logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph)) - # register, instanciate and initialize models for each modules + # register, instantiate and initialize models for each modules for package in graph: - if skip_modules and package.name in skip_modules: + module_name = package.name + module_id = package.id + + if skip_modules and module_name in skip_modules: continue + logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name) migrations.migrate_module(package, 'pre') register_module_classes(package.name) models = pool.instanciate(package.name, cr) + loaded_modules.append(package.name) if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): init_module_models(cr, package.name, models) - cr.commit() - # load data for each modules - modobj = pool.get('ir.module.module') - for package in graph: - status['progress'] = (float(statusi)+0.1) / len(graph) - m = package.name - mid = package.id + status['progress'] = float(statusi) / len(graph) - if skip_modules and m in skip_modules: - continue + # Can't put this line out of the loop: ir.module.module will be + # registered by init_module_models() above. + modobj = pool.get('ir.module.module') if perform_checks: - modobj.check(cr, 1, [mid]) + modobj.check(cr, 1, [module_id]) idref = {} - status['progress'] = (float(statusi)+0.4) / len(graph) mode = 'update' if hasattr(package, 'init') or package.state == 'to install': @@ -192,21 +192,21 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): if package.state=='to upgrade': # upgrading the module information - modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data)) - load_init_xml(cr, m, idref, mode) - load_update_xml(cr, m, idref, mode) - load_data(cr, m, idref, mode) + modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data)) + load_init_xml(cr, module_name, idref, mode) + load_update_xml(cr, module_name, idref, mode) + load_data(cr, module_name, idref, mode) if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): status['progress'] = (float(statusi)+0.75) / len(graph) - load_demo_xml(cr, m, idref, mode) - load_demo(cr, m, idref, mode) - cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) + load_demo_xml(cr, module_name, idref, mode) + load_demo(cr, module_name, idref, mode) + cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id)) # launch tests only in demo mode, as most tests will depend # on demo data. Other tests can be added into the regular # 'data' section, but should probably not alter the data, # as there is no rollback. - load_test(cr, m, idref, mode) + load_test(cr, module_name, idref, mode) processed_modules.append(package.name) @@ -214,22 +214,21 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= ver = release.major_version + '.' + package.data['version'] # Set new modules and dependencies - modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver}) - cr.commit() + modobj.write(cr, 1, [module_id], {'state': 'installed', 'latest_version': ver}) # Update translations for all installed languages - modobj.update_translations(cr, 1, [mid], None) - cr.commit() + modobj.update_translations(cr, 1, [module_id], None) package.state = 'installed' for kind in ('init', 'demo', 'update'): if hasattr(package, kind): delattr(package, kind) + cr.commit() statusi += 1 cr.commit() - return processed_modules + return loaded_modules, processed_modules def _check_module_names(cr, module_names): mod_names = set(module_names) @@ -245,11 +244,26 @@ def _check_module_names(cr, module_names): 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_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules): + """Loads modules marked with ``states``, adding them to ``graph`` and + ``loaded_modules`` and returns a list of installed/upgraded modules.""" + processed_modules = [] + while True: + cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),)) + module_list = [name for (name,) in cr.fetchall() if name not in graph] + new_modules_in_graph = graph.add_modules(cr, module_list, force) + logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) + loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules) + processed_modules.extend(processed) + loaded_modules.extend(loaded) + if not processed: break + return processed_modules + + def load_modules(db, force_demo=False, status=None, update_module=False): # TODO status['progress'] reporting is broken: used twice (and reset each # time to zero) in load_module_graph, not fine-grained enough. # It should be a method exposed by the pool. - initialize_sys_path() open_openerp_namespace() @@ -271,10 +285,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # This is a brand new pool, just created in pooler.get_db_and_pool() pool = pooler.get_pool(cr.dbname) - processed_modules = [] + processed_modules = [] # for cleanup step after install + loaded_modules = [] # to avoid double loading report = tools.assertion_report() - # NOTE: Try to also load the modules that have been marked as uninstallable previously... - STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable'] if 'base' in tools.config['update'] or 'all' in tools.config['update']: cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) @@ -284,7 +297,8 @@ def load_modules(db, force_demo=False, status=None, update_module=False): if not graph: logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) - processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)) + loaded, processed = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report) + processed_modules.extend(processed) if tools.config['load_language']: for lang in tools.config['load_language'].split(','): @@ -313,28 +327,19 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) - STATES_TO_LOAD += ['to install'] - # STEP 3: Load marked modules (skipping base which was done in STEP 1) - loop_guardrail = 0 - while True: - loop_guardrail += 1 - if loop_guardrail > 100: - raise ValueError('Possible recursive module tree detected, aborting.') - cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),)) - - module_list = [name for (name,) in cr.fetchall() if name not in graph] - if not module_list: - break - - new_modules_in_graph = graph.add_modules(cr, module_list, force) - if new_modules_in_graph == 0: - # nothing to load - break - - logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list))) - processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules)) + # IMPORTANT: this is done in two parts, first loading all installed or + # partially installed modules (i.e. installed/to upgrade), to + # offer a consistent system to the second part: installing + # newly selected modules. + states_to_load = ['installed', 'to upgrade'] + processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules) + processed_modules.extend(processed) + if update_module: + states_to_load = ['to install'] + processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules) + processed_modules.extend(processed) # load custom models cr.execute('select model from ir_model where state=%s', ('manual',)) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index a9fd20e2a65..056198a76d7 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1212,34 +1212,30 @@ class serialized(_column): self._symbol_get = self._deserialize_func 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): - return self._get_defaults(obj, cr, uid, [prop_name], context=None)[0][prop_name] + return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name] - def _get_defaults(self, obj, cr, uid, prop_name, context=None): + def _get_defaults(self, obj, cr, uid, prop_names, context=None): + """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False). + + :param list of string prop_names: list of name of property fields for those we want the default value + :return: map of property field names to their default value + :rtype: dict + """ 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 + res = {} + for prop_name in prop_names: + res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context) + return res 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 = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)] + 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 @@ -1274,48 +1270,48 @@ class property(function): }, context=context) return False + def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None): + prop = obj.pool.get('ir.property') + # get the default values (for res_id = False) for the property fields + default_val = self._get_defaults(obj, cr, uid, prop_names, context) - def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None): - properties = obj.pool.get('ir.property') - 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) - + # build the dictionary that will be returned res = {} for id in ids: 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'): - # check existence as root, as seeing the name of a related - # object depends on access right of source document, - # not target, so user may not have access. - record_exists = obj.pool.get(value._name).exists(cr, 1, value.id) - if record_exists: - replaces.setdefault(value._name, {}) - replaces[value._name][value.id] = True - else: - res[prop.res_id.id][prop.fields_id.name] = False - - for rep in replaces: - # search+name_get as root, as seeing the name of a related - # object depends on access right of source document, - # not target, so user may not have access. - nids = obj.pool.get(rep).search(cr, 1, [('id','in',replaces[rep].keys())], context=context) - replaces[rep] = dict(obj.pool.get(rep).name_get(cr, 1, nids, context=context)) - - for prop in prop_name: + for prop_name in prop_names: + property_field = obj._all_columns.get(prop_name).column + property_destination_obj = property_field._obj if property_field._type == 'many2one' else False + # If the property field is a m2o field, we will append the id of the value to name_get_ids + # in order to make a name_get in batch for all the ids needed. + name_get_ids = {} 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)) - + # get the result of ir.property.get() for this res_id and save it in res if it's existing + obj_reference = obj._name + ',' + str(id) + value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context) + if value: + res[id][prop_name] = value + # Check existence as root (as seeing the name of a related + # object depends on access right of source document, + # not target, so user may not have access) in order to avoid + # pointing on an unexisting record. + if property_destination_obj: + if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id): + name_get_ids[id] = res[id][prop_name].id + else: + res[id][prop_name] = False + if property_destination_obj: + # name_get as root (as seeing the name of a related + # object depends on access right of source document, + # not target, so user may not have access.) + name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context)) + # the property field is a m2o, we need to return a tuple with (id, name) + for k, v in name_get_ids.iteritems(): + if res[k][prop_name]: + res[k][prop_name] = (v , name_get_values.get(v)) return res - def _field_get(self, cr, uid, model_name, prop): if not self.field_id.get(cr.dbname): cr.execute('SELECT id \ diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c8df66c55da..ff0d17af47a 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -77,9 +77,21 @@ module_class_list = {} ROOT_USER_ID = 1 def transfer_field_to_modifiers(field, modifiers): - for a in ('invisible', 'readonly', 'required'): - if field.get(a): - modifiers[a] = bool(field.get(a)) + default_values = {} + state_exceptions = {} + for attr in ('invisible', 'readonly', 'required'): + state_exceptions[attr] = [] + default_values[attr] = bool(field.get(attr)) + for state, modifs in (field.get("states",{})).items(): + for modif in modifs: + if default_values[modif[0]] != modif[1]: + state_exceptions[modif[0]].append(state) + + for attr, default_value in default_values.items(): + if state_exceptions[attr]: + modifiers[attr] = [("state", "not in" if default_value else "in", state_exceptions[attr])] + else: + modifiers[attr] = default_value # Don't deal with groups, it is done by check_group(). @@ -2484,9 +2496,10 @@ class orm(orm_template): :param int offset: optional number of records to skip :param int limit: optional max number of records to return :param dict context: context arguments, like lang, time zone - :param order: optional ``order by`` specification, for overriding the natural - sort ordering of the groups, see also :py:meth:`~osv.osv.osv.search` - (supported only for many2one fields currently) + :param list orderby: optional ``order by`` specification, for + overriding the natural sort ordering of the + groups, see also :py:meth:`~osv.osv.osv.search` + (supported only for many2one fields currently) :return: list of dictionaries(one dictionary for each record) containing: * the values of fields grouped by the fields in ``groupby`` argument diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 43a508f9082..18efb62eb18 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -37,7 +37,8 @@ __all__ = ['db_connect', 'close_db'] from threading import currentThread import logging -from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE,\ + ISOLATION_LEVEL_REPEATABLE_READ from psycopg2.psycopg1 import cursor as psycopg1cursor from psycopg2.pool import PoolError @@ -79,12 +80,64 @@ re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$'); sql_counter = 0 class Cursor(object): - """ Cursor is an open transaction to Postgres, utilizing a TCP connection - - A lightweight wrapper around psycopg2's `psycopg1cursor` objects - - This is the object behind the `cr` variable used all over the OpenERP - code. + """Represents an open transaction to the PostgreSQL DB backend, + acting as a lightweight wrapper around psycopg2's + ``psycopg1cursor`` objects. + + ``Cursor`` is the object behind the ``cr`` variable used all + over the OpenERP code. + + .. rubric:: Transaction Isolation + + One very important property of database transactions is the + level of isolation between concurrent transactions. + The SQL standard defines four levels of transaction isolation, + ranging from the most strict *Serializable* level, to the least + strict *Read Uncommitted* level. These levels are defined in + terms of the phenomena that must not occur between concurrent + transactions, such as *dirty read*, etc. + In the context of a generic business data management software + such as OpenERP, we need the best guarantees that no data + corruption can ever be cause by simply running multiple + transactions in parallel. Therefore, the preferred level would + be the *serializable* level, which ensures that a set of + transactions is guaranteed to produce the same effect as + running them one at a time in some order. + + However, most database management systems implement a limited + serializable isolation in the form of + `snapshot isolation `_, + providing most of the same advantages as True Serializability, + with a fraction of the performance cost. + With PostgreSQL up to version 9.0, this snapshot isolation was + the implementation of both the ``REPEATABLE READ`` and + ``SERIALIZABLE`` levels of the SQL standard. + As of PostgreSQL 9.1, the previous snapshot isolation implementation + was kept for ``REPEATABLE READ``, while a new ``SERIALIZABLE`` + level was introduced, providing some additional heuristics to + detect a concurrent update by parallel transactions, and forcing + one of them to rollback. + + OpenERP implements its own level of locking protection + for transactions that are highly likely to provoke concurrent + updates, such as stock reservations or document sequences updates. + Therefore we mostly care about the properties of snapshot isolation, + but we don't really need additional heuristics to trigger transaction + rollbacks, as we are taking care of triggering instant rollbacks + ourselves when it matters (and we can save the additional performance + hit of these heuristics). + + As a result of the above, we have selected ``REPEATABLE READ`` as + the default transaction isolation level for OpenERP cursors, as + it will be mapped to the desired ``snapshot isolation`` level for + all supported PostgreSQL version (8.3 - 9.x). + + Note: up to psycopg2 v.2.4.2, psycopg2 itself remapped the repeatable + read level to serializable before sending it to the database, so it would + actually select the new serializable mode on PostgreSQL 9.1. Make + sure you use psycopg2 v2.4.2 or newer if you use PostgreSQL 9.1 and + the performance hit is a concern for you. + """ IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit __logger = None @@ -100,7 +153,7 @@ class Cursor(object): return f(self, *args, **kwargs) return wrapper - def __init__(self, pool, dbname, serialized=False): + def __init__(self, pool, dbname, serialized=True): if self.__class__.__logger is None: self.__class__.__logger = logging.getLogger('db.cursor') self.sql_from_log = {} @@ -115,7 +168,11 @@ class Cursor(object): # is raised by any of the following initialisations self._pool = pool self.dbname = dbname + + # Whether to enable snapshot isolation level for this cursor. + # see also the docstring of Cursor. self._serialized = serialized + self._cnx = pool.borrow(dsn(dbname)) self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor) self.__closed = False # real initialisation value @@ -227,9 +284,6 @@ class Cursor(object): self.__closer = frame_codeinfo(currentframe(),3) self.print_log() - if not self._serialized: - self.rollback() # Ensure we close the current transaction. - self._obj.close() # This force the cursor to be freed, and thus, available again. It is @@ -248,8 +302,23 @@ class Cursor(object): @check def autocommit(self, on): - offlevel = [ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE][bool(self._serialized)] - self._cnx.set_isolation_level([offlevel, ISOLATION_LEVEL_AUTOCOMMIT][bool(on)]) + if on: + isolation_level = ISOLATION_LEVEL_AUTOCOMMIT + else: + # If a serializable cursor was requested, we + # use the appropriate PotsgreSQL isolation level + # that maps to snaphsot isolation. + # For all supported PostgreSQL versions (8.3-9.x), + # this is currently the ISOLATION_REPEATABLE_READ. + # See also the docstring of this class. + # NOTE: up to psycopg 2.4.2, repeatable read + # is remapped to serializable before being + # sent to the database, so it is in fact + # unavailable for use with pg 9.1. + isolation_level = ISOLATION_LEVEL_REPEATABLE_READ \ + if self._serialized \ + else ISOLATION_LEVEL_READ_COMMITTED + self._cnx.set_isolation_level(isolation_level) @check def commit(self): @@ -383,13 +452,13 @@ class Connection(object): self.dbname = dbname self._pool = pool - def cursor(self, serialized=False): + def cursor(self, serialized=True): cursor_type = serialized and 'serialized ' or '' self.__logger.log(logging.DEBUG_SQL, 'create %scursor to %r', cursor_type, self.dbname) return Cursor(self._pool, self.dbname, serialized=serialized) - def serialized_cursor(self): - return self.cursor(True) + # serialized_cursor is deprecated - cursors are serialized by default + serialized_cursor = cursor def __nonzero__(self): """Check if connection is possible""" diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index dc66142fc4a..20f168f11ab 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1387,6 +1387,21 @@ def upload_data(email, data, type='SURVEY'): a.start() return True +def get_and_group_by_field(cr, uid, obj, ids, field, context=None): + """ Read the values of ``field´´ for the given ``ids´´ and group ids by value. + + :param string field: name of the field we want to read and group by + :return: mapping of field values to the list of ids that have it + :rtype: dict + """ + res = {} + for record in obj.read(cr, uid, ids, [field], context=context): + key = record[field] + res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id']) + return res + +def get_and_group_by_company(cr, uid, obj, ids, context=None): + return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context) # port of python 2.6's attrgetter with support for dotted notation def resolve_attr(obj, attr):