@@ -1814,17 +1842,9 @@
search
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -1836,7 +1856,7 @@
formThe configuration wizards are used to help you configure a new instance of OpenERP. They are launched during the installation of new modules, but you can choose to restart some wizards manually from this menu.
-
+
@@ -1851,50 +1871,6 @@
()
-
-
-
- ir.actions.todo.category.form
- ir.actions.todo.category
- form
-
-
-
-
-
-
-
-
-
-
-
-
- ir.actions.todo.category.tree
- ir.actions.todo.category
- tree
-
-
-
-
-
-
-
-
-
- Administration
- 1
-
-
-
- Sales Management
- 5
-
-
-
- Tools / Customization
- 5
-
-
ir.mail.server.form
@@ -1944,9 +1920,9 @@
search
-
-
-
+
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index fbe3f71e024..fc6ae9ea60e 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -210,10 +210,6 @@ class act_window(osv.osv):
res[act.id] = str(field_get)
return res
- def _get_help_status(self, cr, uid, ids, name, arg, context=None):
- activate_tips = self.pool.get('res.users').browse(cr, uid, uid).menu_tips
- return dict([(id, activate_tips) for id in ids])
-
_columns = {
'name': fields.char('Action Name', size=64, translate=True),
'type': fields.char('Action Type', size=32, required=True),
@@ -222,9 +218,10 @@ class act_window(osv.osv):
help="Optional domain filtering of the destination data, as a Python expression"),
'context': fields.char('Context Value', size=250, required=True,
help="Context dictionary as Python expression, empty by default (Default: {})"),
- 'res_model': fields.char('Object', size=64, required=True,
+ 'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
+ 'res_model': fields.char('Destination Model', size=64, required=True,
help="Model name of the object to open in the view window"),
- 'src_model': fields.char('Source Object', size=64,
+ 'src_model': fields.char('Source Model', size=64,
help="Optional model name of the objects on which this action should be visible"),
'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline')], 'Target Window'),
'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
@@ -250,8 +247,6 @@ class act_window(osv.osv):
'help': fields.text('Action description',
help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
translate=True),
- 'display_menu_tip':fields.function(_get_help_status, type='boolean', string='Display Menu Tips',
- help='It gives the status if the tip has to be displayed or not when a user executes an action'),
'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
}
@@ -754,20 +749,6 @@ class act_window_close(osv.osv):
}
act_window_close()
-class ir_actions_todo_category(osv.osv):
- """
- Category of Configuration Wizards
- """
-
- _name = 'ir.actions.todo.category'
- _description = "Configuration Wizard Category"
- _columns = {
- 'name':fields.char('Name', size=64, translate=True, required=True),
- 'sequence': fields.integer('Sequence'),
- 'wizards_ids': fields.one2many('ir.actions.todo', 'category_id', 'Configuration Wizards'),
- }
-ir_actions_todo_category()
-
# This model use to register action services.
TODO_STATES = [('open', 'To Do'),
('done', 'Done')]
@@ -781,8 +762,7 @@ class ir_actions_todo(osv.osv):
_description = "Configuration Wizards"
_columns={
'action_id': fields.many2one(
- 'ir.actions.act_window', 'Action', select=True, required=True,
- ondelete='cascade'),
+ 'ir.actions.actions', 'Action', select=True, required=True),
'sequence': fields.integer('Sequence'),
'state': fields.selection(TODO_STATES, string='State', required=True),
'name': fields.char('Name', size=64),
@@ -792,14 +772,13 @@ Automatic: Runs whenever the system is reconfigured.
Launch Manually Once: after hacing been launched manually, it sets automatically to Done."""),
'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
'note': fields.text('Text', translate=True),
- 'category_id': fields.many2one('ir.actions.todo.category','Category'),
}
_defaults={
'state': 'open',
'sequence': 10,
'type': 'manual',
}
- _order="sequence,name,id"
+ _order="sequence,id"
def action_launch(self, cr, uid, ids, context=None):
""" Launch Action of Wizard"""
@@ -809,7 +788,11 @@ Launch Manually Once: after hacing been launched manually, it sets automatically
wizard.write({'state': 'done'})
# Load action
- res = self.pool.get('ir.actions.act_window').read(cr, uid, wizard.action_id.id, [], context=context)
+ act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
+
+ res = self.pool.get(act_type['type']).read(cr, uid, wizard.action_id.id, [], context=context)
+ if act_type<>'ir.actions.act_window':
+ return res
res.setdefault('context','{}')
res['nodestroy'] = True
@@ -884,8 +867,10 @@ class act_client(osv.osv):
])
def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
- assert isinstance(field_value, dict), "params can only be dictionaries"
- self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
+ if isinstance(field_value, dict):
+ self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
+ else:
+ self.write(cr, uid, id, {'params_store': field_value}, context=context)
_columns = {
'tag': fields.char('Client action tag', size=64, required=True,
diff --git a/openerp/addons/base/ir/ir_config_parameter_view.xml b/openerp/addons/base/ir/ir_config_parameter_view.xml
index a3e2f7173d2..9254e043491 100644
--- a/openerp/addons/base/ir/ir_config_parameter_view.xml
+++ b/openerp/addons/base/ir/ir_config_parameter_view.xml
@@ -1,3 +1,4 @@
+
@@ -7,7 +8,7 @@
search
-
+
@@ -34,13 +35,12 @@
-
+
-
+
diff --git a/openerp/addons/base/ir/ir_filters.xml b/openerp/addons/base/ir/ir_filters.xml
index 3722b533e19..fec6b0a604e 100644
--- a/openerp/addons/base/ir/ir_filters.xml
+++ b/openerp/addons/base/ir/ir_filters.xml
@@ -53,16 +53,15 @@
search
+
-
-
+
+
-
-
-
+
diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py
index 04156411e48..6099c68a0fb 100644
--- a/openerp/addons/base/ir/ir_mail_server.py
+++ b/openerp/addons/base/ir/ir_mail_server.py
@@ -161,7 +161,7 @@ def encode_rfc2822_address_header(header_text):
class ir_mail_server(osv.osv):
- """Represents an SMTP server, able to send outgoing e-mails, with SSL and TLS capabilities."""
+ """Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
_name = "ir.mail_server"
_columns = {
@@ -396,7 +396,7 @@ class ir_mail_server(osv.osv):
MailDeliveryException and logs root cause.
"""
smtp_from = message['Return-Path'] or message['From']
- assert smtp_from, "The Return-Path or From header is required for any outbound e-mail"
+ assert smtp_from, "The Return-Path or From header is required for any outbound email"
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
from_rfc2822 = extract_rfc2822_addresses(smtp_from)
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index da5a1ec19fd..e777b390aa8 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -29,8 +29,7 @@ from openerp import netsvc, pooler, tools
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools import config
from openerp.tools.translate import _
-from openerp.osv.orm import except_orm, browse_record, EXT_ID_PREFIX_FK, \
- EXT_ID_PREFIX_M2M_TABLE, EXT_ID_PREFIX_CONSTRAINT
+from openerp.osv.orm import except_orm, browse_record
_logger = logging.getLogger(__name__)
@@ -97,9 +96,9 @@ class ir_model(osv.osv):
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
- 'osv_memory': fields.function(_is_osv_memory, string='In-Memory Model', type='boolean',
+ 'osv_memory': fields.function(_is_osv_memory, string='Transient Model', type='boolean',
fnct_search=_search_osv_memory,
- help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)"),
+ help="This field specifies whether the model is transient or not (i.e. if records are automatically deleted from the database or not)"),
'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the object is defined or inherited'),
'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
}
@@ -179,14 +178,16 @@ class ir_model(osv.osv):
def create(self, cr, user, vals, context=None):
if context is None:
context = {}
- if context and context.get('manual',False):
+ if context and context.get('manual'):
vals['state']='manual'
res = super(ir_model,self).create(cr, user, vals, context)
if vals.get('state','base')=='manual':
self.instanciate(cr, user, vals['model'], context)
self.pool.get(vals['model']).__init__(self.pool, cr)
- ctx = context.copy()
- ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0')})
+ ctx = dict(context,
+ field_name=vals['name'],
+ field_state='manual',
+ select=vals.get('select_level', '0'))
self.pool.get(vals['model'])._auto_init(cr, ctx)
#pooler.restart_pool(cr.dbname)
return res
@@ -226,7 +227,7 @@ class ir_model_fields(osv.osv):
'required': fields.boolean('Required'),
'readonly': fields.boolean('Readonly'),
'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
- 'translate': fields.boolean('Translate', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
+ 'translate': fields.boolean('Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
'size': fields.integer('Size'),
'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
@@ -336,8 +337,11 @@ class ir_model_fields(osv.osv):
if self.pool.get(vals['model']):
self.pool.get(vals['model']).__init__(self.pool, cr)
#Added context to _auto_init for special treatment to custom field for select_level
- ctx = context.copy()
- ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True})
+ ctx = dict(context,
+ field_name=vals['name'],
+ field_state='manual',
+ select=vals.get('select_level', '0'),
+ update_custom_fields=True)
self.pool.get(vals['model'])._auto_init(cr, ctx)
return res
@@ -447,8 +451,8 @@ class ir_model_fields(osv.osv):
# was called earlier, they will be in-sync before the _auto_init.
# Anything we don't update in _columns now will be reset from
# the model into ir.model.fields (db).
- ctx = context.copy()
- ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True})
+ ctx = dict(context, select=vals.get('select_level', '0'),
+ update_custom_fields=True)
for __, patch_struct in models_patch.items():
obj = patch_struct[0]
@@ -642,10 +646,37 @@ class ir_model_data(osv.osv):
"""
_name = 'ir.model.data'
_order = 'module,model,name'
+ def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+ result = {}
+ result2 = {}
+ for res in self.browse(cr, uid, ids, context=context):
+ if res.id:
+ result.setdefault(res.model, {})
+ result[res.model][res.res_id] = res.id
+ result2[res.id] = False
+
+ for model in result:
+ try:
+ r = dict(self.pool.get(model).name_get(cr, uid, result[model].keys(), context=context))
+ for key,val in result[model].items():
+ result2[val] = r.get(key, False)
+ except:
+ # some object have no valid name_get implemented, we accept this
+ pass
+ return result2
+
+ def _complete_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+ result = {}
+ for res in self.browse(cr, uid, ids, context=context):
+ result[res.id] = (res.module and (res.module + '.') or '')+res.name
+ return result
+
_columns = {
'name': fields.char('External Identifier', required=True, size=128, select=1,
help="External Key/Identifier that can be used for "
"data integration with third-party systems"),
+ 'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
+ 'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
'model': fields.char('Model Name', required=True, size=64, select=1),
'module': fields.char('Module', required=True, size=64, select=1),
'res_id': fields.integer('Record ID', select=1,
@@ -799,20 +830,16 @@ class ir_model_data(osv.osv):
'res_id': inherit_id.id,
'noupdate': noupdate,
},context=context)
- if xml_id:
- if res_id:
- self.loads[(module, xml_id)] = (model, res_id)
- if model_obj._inherits:
- for table in model_obj._inherits:
- inherit_field = model_obj._inherits[table]
- inherit_id = model_obj.read(cr, uid, res_id,
- [inherit_field])[inherit_field]
- self.loads[(module, xml_id + '_' + \
- table.replace('.', '_'))] = (table, inherit_id)
+ if xml_id and res_id:
+ self.loads[(module, xml_id)] = (model, res_id)
+ for table, inherit_field in model_obj._inherits.iteritems():
+ inherit_id = model_obj.read(cr, uid, res_id,
+ [inherit_field])[inherit_field]
+ self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
return res_id
def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
- if type(models[0])==type([]) or type(models[0])==type(()):
+ if isinstance(models[0], (list, tuple)):
model,res_id = models[0]
else:
res_id=None
@@ -832,12 +859,12 @@ class ir_model_data(osv.osv):
res = cr.fetchone()
if not res:
ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
- res = ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
+ ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
elif xml_id:
cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
return True
- def _module_data_uninstall(self, cr, uid, ids, context=None):
+ def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
"""Deletes all the records referenced by the ir.model.data entries
``ids`` along with their corresponding database backed (including
dropping tables, columns, FKs, etc, as long as there is no other
@@ -848,6 +875,8 @@ class ir_model_data(osv.osv):
This step is performed as part of the full uninstallation of a module.
"""
+ ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
+
if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
@@ -857,50 +886,11 @@ class ir_model_data(osv.osv):
ids_set = set(ids)
wkf_todo = []
to_unlink = []
- to_drop_table = []
ids.sort()
ids.reverse()
for data in self.browse(cr, uid, ids, context):
model = data.model
res_id = data.res_id
- model_obj = self.pool.get(model)
- name = tools.ustr(data.name)
-
- if name.startswith(EXT_ID_PREFIX_FK) or name.startswith(EXT_ID_PREFIX_M2M_TABLE)\
- or name.startswith(EXT_ID_PREFIX_CONSTRAINT):
- # double-check we are really going to delete all the owners of this schema element
- cr.execute("""SELECT id from ir_model_data where name = %s and res_id IS NULL""", (data.name,))
- external_ids = [x[0] for x in cr.fetchall()]
- if (set(external_ids)-ids_set):
- # as installed modules have defined this element we must not delete it!
- continue
-
- if name.startswith(EXT_ID_PREFIX_FK):
- name = name[len(EXT_ID_PREFIX_FK):]
- # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
- cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
- WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
- if cr.fetchone():
- cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
- _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
- continue
-
- if name.startswith(EXT_ID_PREFIX_M2M_TABLE):
- name = name[len(EXT_ID_PREFIX_M2M_TABLE):]
- cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
- if cr.fetchone() and not name in to_drop_table:
- to_drop_table.append(name)
- continue
-
- if name.startswith(EXT_ID_PREFIX_CONSTRAINT):
- name = name[len(EXT_ID_PREFIX_CONSTRAINT):]
- # test if constraint exists
- cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
- WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
- if cr.fetchone():
- cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
- _logger.info('Dropped CONSTRAINT %s@%s', name, model)
- continue
pair_to_unlink = (model, res_id)
if pair_to_unlink not in to_unlink:
@@ -918,24 +908,19 @@ class ir_model_data(osv.osv):
for model,res_id in wkf_todo:
try:
wf_service.trg_write(uid, model, res_id, cr)
- except:
- _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model)
-
- # drop m2m relation tables
- for table in to_drop_table:
- cr.execute('DROP TABLE %s CASCADE'% (table),)
- _logger.info('Dropped table %s', table)
+ except Exception:
+ _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
def unlink_if_refcount(to_unlink):
for model, res_id in to_unlink:
external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
- if (set(external_ids)-ids_set):
+ if set(external_ids)-ids_set:
# if other modules have defined this record, we must not delete it
- return
+ continue
_logger.info('Deleting %s@%s', res_id, model)
try:
self.pool.get(model).unlink(cr, uid, [res_id], context=context)
- except:
+ except Exception:
_logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
# Remove non-model records first, then model fields, and finish with models
@@ -943,11 +928,18 @@ class ir_model_data(osv.osv):
if model not in ('ir.model','ir.model.fields'))
unlink_if_refcount((model, res_id) for model, res_id in to_unlink
if model == 'ir.model.fields')
+
+ ir_model_relation = self.pool.get('ir.model.relation')
+ relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove)])
+ ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context)
+
unlink_if_refcount((model, res_id) for model, res_id in to_unlink
if model == 'ir.model')
cr.commit()
+ self.unlink(cr, uid, ids, context)
+
def _process_end(self, cr, uid, modules):
""" Clear records removed from updated module data.
This method is called at the end of the module loading process.
diff --git a/openerp/addons/base/ir/ir_model_constraint.py b/openerp/addons/base/ir/ir_model_constraint.py
new file mode 100644
index 00000000000..4a69bc2f0a7
--- /dev/null
+++ b/openerp/addons/base/ir/ir_model_constraint.py
@@ -0,0 +1,79 @@
+import logging
+
+import openerp
+from openerp import SUPERUSER_ID
+from openerp.osv import fields
+from openerp.osv.orm import Model
+
+_logger = logging.getLogger(__name__)
+
+class ir_model_constraint(Model):
+ """
+ This model tracks PostgreSQL foreign keys and constraints used by OpenERP
+ models.
+ """
+ _name = 'ir.model.constraint'
+ _columns = {
+ 'name': fields.char('Constraint', required=True, size=128, select=1,
+ help="PostgreSQL constraint or foreign key name."),
+ 'model': fields.many2one('ir.model', string='Model',
+ required=True, select=1),
+ 'module': fields.many2one('ir.module.module', string='Module',
+ required=True, select=1),
+ 'type': fields.char('Constraint Type', required=True, size=1, select=1,
+ help="Type of the constraint: `f` for a foreign key, "
+ "`u` for other constraints."),
+ 'date_update': fields.datetime('Update Date'),
+ 'date_init': fields.datetime('Initialization Date')
+ }
+
+ _sql_constraints = [
+ ('module_name_uniq', 'unique(name, module)',
+ 'Constraints with the same name are unique per module.'),
+ ]
+
+ def _module_data_uninstall(self, cr, uid, ids, context=None):
+ """
+ Delete PostgreSQL foreign keys and constraints tracked by this model.
+ """
+
+ if uid != SUPERUSER_ID and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
+ raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
+
+ context = dict(context or {})
+
+ ids_set = set(ids)
+ ids.sort()
+ ids.reverse()
+ to_unlink = []
+ for data in self.browse(cr, uid, ids, context):
+ model = data.model.model
+ model_obj = self.pool.get(model)
+ name = openerp.tools.ustr(data.name)
+ typ = data.type
+
+ # double-check we are really going to delete all the owners of this schema element
+ cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
+ external_ids = [x[0] for x in cr.fetchall()]
+ if (set(external_ids)-ids_set):
+ # as installed modules have defined this element we must not delete it!
+ continue
+
+ if typ == 'f':
+ # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
+ cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
+ WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
+ if cr.fetchone():
+ cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
+ _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
+
+ if typ == 'u':
+ # test if constraint exists
+ cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
+ WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
+ if cr.fetchone():
+ cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
+ _logger.info('Dropped CONSTRAINT %s@%s', name, model)
+
+ to_unlink.append(data.id)
+ self.unlink(cr, uid, to_unlink, context)
diff --git a/openerp/addons/base/ir/ir_model_relation.py b/openerp/addons/base/ir/ir_model_relation.py
new file mode 100644
index 00000000000..eede6eae44f
--- /dev/null
+++ b/openerp/addons/base/ir/ir_model_relation.py
@@ -0,0 +1,65 @@
+import logging
+
+import openerp
+from openerp import SUPERUSER_ID
+from openerp.osv import fields
+from openerp.osv.orm import Model
+
+_logger = logging.getLogger(__name__)
+
+class ir_model_relation(Model):
+ """
+ This model tracks PostgreSQL tables used to implement OpenERP many2many
+ relations.
+ """
+ _name = 'ir.model.relation'
+ _columns = {
+ 'name': fields.char('Relation Name', required=True, size=128, select=1,
+ help="PostgreSQL table name implementing a many2many relation."),
+ 'model': fields.many2one('ir.model', string='Model',
+ required=True, select=1),
+ 'module': fields.many2one('ir.module.module', string='Module',
+ required=True, select=1),
+ 'date_update': fields.datetime('Update Date'),
+ 'date_init': fields.datetime('Initialization Date')
+ }
+
+ def _module_data_uninstall(self, cr, uid, ids, context=None):
+ """
+ Delete PostgreSQL many2many relations tracked by this model.
+ """
+
+ if uid != SUPERUSER_ID and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
+ raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
+
+ ids_set = set(ids)
+ to_drop_table = []
+ ids.sort()
+ ids.reverse()
+ to_unlink = []
+ for data in self.browse(cr, uid, ids, context):
+ model = data.model
+ model_obj = self.pool.get(model)
+ name = openerp.tools.ustr(data.name)
+
+ # double-check we are really going to delete all the owners of this schema element
+ cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
+ external_ids = [x[0] for x in cr.fetchall()]
+ if (set(external_ids)-ids_set):
+ # as installed modules have defined this element we must not delete it!
+ continue
+
+ cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
+ if cr.fetchone() and not name in to_drop_table:
+ to_drop_table.append(name)
+
+ to_unlink.append(data.id)
+
+ self.unlink(cr, uid, to_unlink, context)
+
+ # drop m2m relation tables
+ for table in to_drop_table:
+ cr.execute('DROP TABLE %s CASCADE'% (table),)
+ _logger.info('Dropped table %s', table)
+
+ cr.commit()
diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py
index 75a6c6cb523..0f28c33ded9 100644
--- a/openerp/addons/base/ir/ir_needaction.py
+++ b/openerp/addons/base/ir/ir_needaction.py
@@ -24,16 +24,16 @@ from operator import itemgetter
from osv import osv, fields
from tools.translate import _
-class ir_needaction_users_rel(osv.osv):
- '''
- ir_needaction_users_rel holds data related to the needaction
- mechanism inside OpenERP. A row in this model is characterized by:
+class ir_needaction_users_rel(osv.Model):
+ ''' ir_needaction_users_rel holds data related to the needaction
+ mechanism inside OpenERP. A row in this model is characterized
+ by:
- res_model: model of the record requiring an action
- res_id: ID of the record requiring an action
- - user_id: foreign key to the res.users table, to the user that
- has to perform the action
- This model can be seen as a many2many, linking (res_model, res_id) to
- users (those whose attention is required on the record).'''
+ - user_id: foreign key to the res.users table, to the user that has to
+ perform the action
+ This model can be seen as a many2many, linking (res_model, res_id) to users
+ (those whose attention is required on the record). '''
_name = 'ir.needaction_users_rel'
_description = 'Needaction relationship table'
@@ -78,50 +78,47 @@ class ir_needaction_users_rel(osv.osv):
return True
-class ir_needaction_mixin(osv.osv):
+class ir_needaction_mixin(osv.Model):
'''Mixin class for objects using the need action feature.
- Need action feature can be used by objects willing to be able to
- signal that an action is required on a particular record. If in the
- business logic an action must be performed by somebody, for instance
- validation by a manager, this mechanism allows to set a list of
- users asked to perform an action.
+ Need action feature can be used by objects having to be able to
+ signal that an action is required on a particular record. If in
+ the business logic an action must be performed by somebody, for
+ instance validation by a manager, this mechanism allows to set a
+ list of users asked to perform an action.
- This class wraps a class (ir.ir_needaction_users_rel) that behaves
- like a many2many field. However, no field is added to the model
- inheriting from this mixin class. This class handles the low-level
- considerations of updating relationships. Every change made on the
- record calls a method that updates the relationships.
+ This class wraps a class (ir.ir_needaction_users_rel) that
+ behaves like a many2many field. This class handles the low-level
+ considerations of updating relationships. Every change made on
+ the record calls a method that updates the relationships.
- Objects using the 'need_action' feature should override the
- ``get_needaction_user_ids`` method. This methods returns a dictionary
- whose keys are record ids, and values a list of user ids, like
- in a many2many relationship. Therefore by defining only one method,
- you can specify if an action is required by defining the users
- that have to do it, in every possible situation.
+ Objects using the 'need_action' feature should override the
+ ``get_needaction_user_ids`` method. This methods returns a
+ dictionary whose keys are record ids, and values a list of user
+ ids, like in a many2many relationship. Therefore by defining
+ only one method, you can specify if an action is required by
+ defining the users that have to do it, in every possible
+ situation.
- This class also offers several global services,:
+ This class also offers several global services:
- ``needaction_get_record_ids``: for the current model and uid, get
- all record ids that ask this user to perform an action. This
- mechanism is used for instance to display the number of pending
- actions in menus, such as Leads (12)
+ all record ids that ask this user to perform an action. This
+ mechanism is used for instance to display the number of pending
+ actions in menus, such as Leads (12)
- ``needaction_get_action_count``: as ``needaction_get_record_ids``
- but returns only the number of action, not the ids (performs a
- search with count=True)
- - ``needaction_get_user_record_references``: for a given uid, get all
- the records that ask this user to perform an action. Records
- are given as references, a list of tuples (model_name, record_id)
+ but returns only the number of action, not the ids (performs a
+ search with count=True)
- The ``ir_needaction_mixin`` class adds a function field on models inheriting
- from the class. This field allows to state whether a given record has
- a needaction for the current user. This is usefull if you want to customize
- views according to the needaction feature. For example, you may want to
- set records in bold in a list view if the current user has an action to
- perform on the record. This makes the class not a pure abstract class,
- but allows to easily use the action information.'''
+ The ``ir_needaction_mixin`` class adds a calculated field
+ ``needaction_pending``. This function field allows to state
+ whether a given record has a needaction for the current user.
+ This is usefull if you want to customize views according to the
+ needaction feature. For example, you may want to set records in
+ bold in a list view if the current user has an action to perform
+ on the record. '''
_name = 'ir.needaction_mixin'
- _description = '"Need action" mixin'
+ _description = 'Need action mixin'
def get_needaction_pending(self, cr, uid, ids, name, arg, context=None):
res = {}
@@ -129,13 +126,19 @@ class ir_needaction_mixin(osv.osv):
for id in ids:
res[id] = uid in needaction_user_ids[id]
return res
+
+ def search_needaction_pending(self, cr, uid, self_again, field_name, criterion, context=None):
+ ids = self.needaction_get_record_ids(
+ cr, uid, uid, limit=1024, context=context)
+ return [('id', 'in', ids)]
_columns = {
- 'needaction_pending': fields.function(get_needaction_pending, type='boolean',
- string='Need action pending',
- help='If True, this field states that users have to perform an action. \
- This field comes from the needaction mechanism. Please refer \
- to the ir.needaction_mixin class.'),
+ 'needaction_pending': fields.function(
+ get_needaction_pending, type='boolean',
+ fnct_search=search_needaction_pending,
+ string='Need action pending',
+ help="If True, this field states that users have to perform an " \
+ "action This field comes from the ir.needaction_mixin class."),
}
#------------------------------------------------------
@@ -146,7 +149,7 @@ class ir_needaction_mixin(osv.osv):
""" Returns the user_ids that have to perform an action
:return: dict { record_id: [user_ids], }
"""
- return dict.fromkeys(ids, [])
+ return dict((id,list()) for id in ids)
def create(self, cr, uid, values, context=None):
rel_obj = self.pool.get('ir.needaction_users_rel')
@@ -191,14 +194,5 @@ class ir_needaction_mixin(osv.osv):
get the number of actions it has to perform"""
rel_obj = self.pool.get('ir.needaction_users_rel')
return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context)
-
- def needaction_get_record_references(self, cr, uid, user_id, offset=None, limit=None, order=None, context=None):
- """For a given user_id, get all the records that asks this user to
- perform an action. Records are given as references, a list of
- tuples (model_name, record_id).
- This method is trans-model."""
- rel_obj = self.pool.get('ir.needaction_users_rel')
- rel_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context)
- return map(itemgetter('res_model', 'res_id'), rel_obj.read(cr, uid, rel_ids, ['res_model', 'res_id'], context=context))
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/addons/base/ir/wizard/wizard_menu_view.xml b/openerp/addons/base/ir/wizard/wizard_menu_view.xml
index a9c841a6d69..9f5d912ed1a 100644
--- a/openerp/addons/base/ir/wizard/wizard_menu_view.xml
+++ b/openerp/addons/base/ir/wizard/wizard_menu_view.xml
@@ -7,14 +7,15 @@
form
-
-
-
-
+
diff --git a/openerp/addons/base/ir/workflow/workflow_view.xml b/openerp/addons/base/ir/workflow/workflow_view.xml
index 0ac7df78ce7..c20eeaed085 100644
--- a/openerp/addons/base/ir/workflow/workflow_view.xml
+++ b/openerp/addons/base/ir/workflow/workflow_view.xml
@@ -15,28 +15,25 @@
form
+
+
-
+
+ Inheritable view for installer objects
+ res.config.installer
+ form
+
+
+
+
+
+
+
+
+
diff --git a/openerp/addons/base/res/res_country_view.xml b/openerp/addons/base/res/res_country_view.xml
index 6b28f065272..efb3b9eca06 100644
--- a/openerp/addons/base/res/res_country_view.xml
+++ b/openerp/addons/base/res/res_country_view.xml
@@ -41,7 +41,7 @@
Display and manage the list of all countries that can be assigned to your partner records. You can create or delete countries to make sure the ones you are working on will be maintained.
-
+
diff --git a/openerp/addons/base/res/res_currency_view.xml b/openerp/addons/base/res/res_currency_view.xml
index e571ba70700..c0a66a12daa 100644
--- a/openerp/addons/base/res/res_currency_view.xml
+++ b/openerp/addons/base/res/res_currency_view.xml
@@ -8,7 +8,7 @@
search
-
+
@@ -78,7 +78,7 @@
-
+
+
+
-
+
res.partner.title.treeres.partner.title
@@ -31,7 +33,6 @@
-
@@ -70,11 +71,8 @@
-
+
+
res.partner.treeres.partner
@@ -101,46 +99,31 @@
@@ -333,12 +329,14 @@
kanban,tree,form{"search_default_customer":1}
-
- Click on "Create" to add a new contact in your address book.
- <p>
- A contact is either a person or a company; a person can be linked to a company as a contact of that company.
- <p>
- You will be able to follow documents and history of your contacts (invoices, meetings, projects, etc.)
+
+
+ Click to add a contact in your address book.
+
+ OpenERP helps you easily track all activities related to
+ a customer; discussions, history of business opportunities,
+ documents, etc.
+
+ OpenERP helps you easily track all activities related to
+ a customer: discussions, history of business opportunities,
+ documents, etc.
+
+
@@ -380,7 +388,15 @@
kanban,tree,form{'search_default_supplier': 1,'default_customer': 0,'default_supplier': 1}
- You can access all information regarding your suppliers from the supplier form: accounting data, history of emails, meetings, purchases, etc. You can uncheck the 'Suppliers' filter button in order to search in all your partners, including customers and prospects.
+
+
+ Click to add a contact in your address book.
+
+ OpenERP helps you easily track all activities related to
+ a supplier: discussions, history of purchases,
+ documents, etc.
+
+
@@ -421,11 +437,8 @@
form
-
+
+
Partner Categoriesres.partner.category
@@ -504,4 +517,3 @@
-
diff --git a/openerp/addons/base/res/res_request_view.xml b/openerp/addons/base/res/res_request_view.xml
index 0fecd3cd72d..88e9cc91932 100644
--- a/openerp/addons/base/res/res_request_view.xml
+++ b/openerp/addons/base/res/res_request_view.xml
@@ -34,8 +34,7 @@
search
-
-
+
diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py
index 8f424e534b0..97ca6ec32dc 100644
--- a/openerp/addons/base/res/res_users.py
+++ b/openerp/addons/base/res/res_users.py
@@ -226,12 +226,11 @@ class users(osv.osv):
'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
'context_lang': fields.selection(_lang_get, 'Language', required=True,
- help="The default language used in the graphical user interface, when translations are available. To add a new language, you can use the 'Load an Official Translation' wizard available from the 'Administration' menu."),
+ help="The default language used in the graphical user interface, when translations are available. To add a new language, you can use the 'Load a Translation' wizard available from the 'Administration' menu."),
'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
help="The user's timezone, used to output proper date and time values inside printed reports. "
"It is important to set a value for this field. You should use the same timezone "
"that is otherwise used to pick and render date and time values: your computer's timezone."),
- 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
'date': fields.datetime('Latest Connection', readonly=True),
}
@@ -336,11 +335,10 @@ class users(osv.osv):
'company_id': _get_company,
'company_ids': _get_companies,
'groups_id': _get_group,
- 'menu_tips': False
}
# User can write to a few of her own fields (but not her groups for example)
- SELF_WRITEABLE_FIELDS = ['menu_tips','password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'avatar', 'avatar_big']
+ SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'avatar', 'avatar_big']
def write(self, cr, uid, ids, values, context=None):
if not hasattr(ids, '__iter__'):
diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml
index 19a2ff6d950..12bdaf016ce 100644
--- a/openerp/addons/base/res/res_users_view.xml
+++ b/openerp/addons/base/res/res_users_view.xml
@@ -2,6 +2,16 @@
+
+ res.groups.search
+ res.groups
+ search
+
+
+
+
+
+ res.groups.formres.groups
@@ -58,7 +68,7 @@
-
+
@@ -81,19 +91,17 @@
diff --git a/openerp/addons/base/rng/view.rng b/openerp/addons/base/rng/view.rng
index 22d6ee12a88..c583040495b 100644
--- a/openerp/addons/base/rng/view.rng
+++ b/openerp/addons/base/rng/view.rng
@@ -677,6 +677,7 @@
+
diff --git a/openerp/addons/base/security/base_security.xml b/openerp/addons/base/security/base_security.xml
index 2fdd7f70ca9..cd0b7e999b3 100644
--- a/openerp/addons/base/security/base_security.xml
+++ b/openerp/addons/base/security/base_security.xml
@@ -23,6 +23,10 @@
Multi Companies
+
+
+ Multi Currencies
+ Technical Features
diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv
index cc382fcbd11..d49aa18db8c 100644
--- a/openerp/addons/base/security/ir.model.access.csv
+++ b/openerp/addons/base/security/ir.model.access.csv
@@ -117,7 +117,6 @@
"access_res_widget_user","res.widget.user","model_res_widget",,1,0,0,0
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
-"access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
"access_ir_needaction_users_rel","ir_needaction_users_rel","model_ir_needaction_users_rel",,1,1,1,1
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1
diff --git a/openerp/addons/base/static/src/css/modules.css b/openerp/addons/base/static/src/css/modules.css
index 6e6ee44ea0c..d13f932e4f1 100644
--- a/openerp/addons/base/static/src/css/modules.css
+++ b/openerp/addons/base/static/src/css/modules.css
@@ -1,5 +1,5 @@
.oe_module_vignette {
- padding: 14px;
+ padding: 8px;
}
.oe_module_icon, .oe_module_desc {
display: inline-block;
diff --git a/openerp/addons/base/tests/test_ir_values.py b/openerp/addons/base/tests/test_ir_values.py
index b7ea0107776..b306e1f0929 100644
--- a/openerp/addons/base/tests/test_ir_values.py
+++ b/openerp/addons/base/tests/test_ir_values.py
@@ -54,7 +54,10 @@ class test_ir_values(common.TransactionCase):
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True)
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True)
ir_values.set(self.cr, self.uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True)
- ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,45', isobject=True)
+ report_ids = self.registry('ir.actions.report.xml').search(self.cr, self.uid, [], {})
+ reports = self.registry('ir.actions.report.xml').browse(self.cr, self.uid, report_ids, {})
+ report_id = [report.id for report in reports if not report.groups_id][0] # assume at least one
+ ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,%s'%report_id, isobject=True)
ir_values.set(self.cr, self.uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True)
# Replace one action binding to set a new name.
@@ -86,7 +89,7 @@ class test_ir_values(common.TransactionCase):
assert len(actions) == 1, "Mismatching number of bound actions"
assert len(actions[0]) == 3, "Malformed action definition"
assert actions[0][1] == 'Nice Report', 'Bound action does not match definition'
- assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 45, 'Bound action does not match definition'
+ assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == report_id, 'Bound action does not match definition'
actions = ir_values.get(self.cr, self.uid, 'action', 'client_action_relate', ['unexisting_model'])
assert len(actions) == 1, "Mismatching number of bound actions"
@@ -96,4 +99,4 @@ class test_ir_values(common.TransactionCase):
if __name__ == '__main__':
- unittest2.main()
\ No newline at end of file
+ unittest2.main()
diff --git a/openerp/import_xml.rng b/openerp/import_xml.rng
index 13409f05aa1..bf5a9e1800e 100644
--- a/openerp/import_xml.rng
+++ b/openerp/import_xml.rng
@@ -19,10 +19,8 @@
-
-
-
-
+
+
diff --git a/openerp/modules/db.py b/openerp/modules/db.py
index 2449efc7fcd..7490631a3b9 100644
--- a/openerp/modules/db.py
+++ b/openerp/modules/db.py
@@ -77,8 +77,8 @@ def initialize(cr):
cr.execute('INSERT INTO ir_module_module \
(author, website, name, shortdesc, description, \
- category_id, auto_install, state, certificate, web, license, application, icon, sequence) \
- VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
+ category_id, auto_install, state, certificate, web, license, application, icon, sequence, summary) \
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
info['author'],
info['website'], i, info['name'],
info['description'], category_id,
@@ -86,7 +86,7 @@ def initialize(cr):
info['web'],
info['license'],
info['application'], info['icon'],
- info['sequence']))
+ info['sequence'], info['summary']))
id = cr.fetchone()[0]
cr.execute('INSERT INTO ir_model_data \
(name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
diff --git a/openerp/modules/module.py b/openerp/modules/module.py
index e226b69a6fc..b1d4de31bce 100644
--- a/openerp/modules/module.py
+++ b/openerp/modules/module.py
@@ -341,6 +341,7 @@ def load_information_from_description_file(module):
'web': False,
'website': '',
'sequence': 100,
+ 'summary': '',
}
info.update(itertools.izip(
'depends data demo test init_xml update_xml demo_xml'.split(),
@@ -493,7 +494,7 @@ def get_test_modules(module, submodule, explode):
# It seems the module has no `tests` sub-module, no problem.
pass
else:
- print 'Can not `import %s`.' % module
+ _logger.exception('Can not `import %s`.', module)
return []
# Discover available test sub-modules.
@@ -554,6 +555,7 @@ def run_unit_tests(module_name):
"""
import unittest2
ms = get_test_modules(module_name, '__fast_suite__', explode=False)
+ # TODO: No need to try again if the above call failed because of e.g. a syntax error.
ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
suite = unittest2.TestSuite()
for m in ms:
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index db700882555..e2a3391c8a4 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -122,6 +122,7 @@ start the server specifying the --unaccent flag.
"""
import logging
+import traceback
from openerp.tools import flatten, reverse_enumerate
import fields
@@ -426,6 +427,7 @@ class expression(object):
# If the field is _inherits'd, search for the working_table,
# and extract the field.
+ field = None
if field_path[0] in table._inherit_fields:
while True:
field = working_table._columns.get(field_path[0])
@@ -474,6 +476,13 @@ class expression(object):
# the function field doesn't provide a search function and doesn't store
# values in the database, so we must ignore it : we generate a dummy leaf
self.__exp[i] = TRUE_LEAF
+ _logger.error(
+ "The field '%s' (%s) can not be searched: non-stored "
+ "function field without fnct_search",
+ field.string, left)
+ # avoid compiling stack trace if not needed
+ if _logger.isEnabledFor(logging.DEBUG):
+ _logger.debug(''.join(traceback.format_stack()))
else:
subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
if not subexp:
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 2a6d0d6e3eb..fb13707d13d 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -49,7 +49,7 @@ import simplejson
_logger = logging.getLogger(__name__)
def _symbol_set(symb):
- if symb == None or symb == False:
+ if symb is None or symb == False:
return None
elif isinstance(symb, unicode):
return symb.encode('utf-8')
@@ -127,6 +127,23 @@ class _column(object):
res = obj.read(cr, uid, ids, [name], context=context)
return [x[name] for x in res]
+ def as_display_name(self, cr, uid, obj, value, context=None):
+ """Converts a field value to a suitable string representation for a record,
+ e.g. when this field is used as ``rec_name``.
+
+ :param obj: the ``BaseModel`` instance this column belongs to
+ :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
+ for this column
+ """
+ # delegated to class method, so a column type A can delegate
+ # to a column type B.
+ return self._as_display_name(self, cr, uid, obj, value, context=None)
+
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ # This needs to be a class method, in case a column type A as to delegate
+ # to a column type B.
+ return tools.ustr(value)
# ---------------------------------------------------------
# Simple fields
@@ -178,11 +195,22 @@ class reference(_column):
result[value['id']] = False
return result
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ if value:
+ # reference fields have a 'model,id'-like value, that we need to convert
+ # to a real name
+ model_name, res_id = value.split(',')
+ model = obj.pool.get(model_name)
+ if model and res_id:
+ return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
+ return tools.ustr(value)
+
class char(_column):
_type = 'char'
- def __init__(self, string, size, **args):
- _column.__init__(self, string=string, size=size, **args)
+ def __init__(self, string="unknown", size=None, **args):
+ _column.__init__(self, string=string, size=size or None, **args)
self._symbol_set = (self._symbol_c, self._symbol_set_char)
# takes a string (encoded in utf8) and returns a string (encoded in utf8)
@@ -191,7 +219,7 @@ class char(_column):
# * we need to remove the "symb==False" from the next line BUT
# for now too many things rely on this broken behavior
# * the symb==None test should be common to all data types
- if symb == None or symb == False:
+ if symb is None or symb == False:
return None
# we need to convert the string to a unicode object to be able
@@ -453,6 +481,11 @@ class many2one(_column):
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
+
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ return value[1] if isinstance(value, tuple) else tools.ustr(value)
+
class one2many(_column):
_classic_read = False
@@ -543,6 +576,10 @@ class one2many(_column):
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
+
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
#
# Values: (0, 0, { fields }) create
@@ -725,6 +762,10 @@ class many2many(_column):
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
+
def get_nice_size(value):
size = 0
@@ -1078,6 +1119,12 @@ class function(_column):
if self._fnct_inv:
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
+ @classmethod
+ def _as_display_name(cls, field, cr, uid, obj, value, context=None):
+ # Function fields are supposed to emulate a basic field type,
+ # so they can delegate to the basic type for record name rendering
+ return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
+
# ---------------------------------------------------------
# Related fields
# ---------------------------------------------------------
@@ -1211,7 +1258,6 @@ class related(function):
result[-1]['relation'] = f['relation']
self._relations = result
-
class sparse(function):
def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 9be2a1bed12..d8861a20c26 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -70,11 +70,6 @@ _schema = logging.getLogger(__name__ + '.schema')
# List of etree._Element subclasses that we choose to ignore when parsing XML.
from openerp.tools import SKIPPED_ELEMENT_TYPES
-# Prefixes for external IDs of schema elements
-EXT_ID_PREFIX_FK = "_foreign_key_"
-EXT_ID_PREFIX_M2M_TABLE = "_m2m_rel_table_"
-EXT_ID_PREFIX_CONSTRAINT = "_constraint_"
-
regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I)
regex_object_name = re.compile(r'^[a-z0-9_.]+$')
@@ -662,7 +657,7 @@ class BaseModel(object):
_columns = {}
_constraints = []
_defaults = {}
- _rec_name = 'name'
+ _rec_name = None
_parent_name = 'parent_id'
_parent_store = False
_parent_order = False
@@ -763,7 +758,7 @@ class BaseModel(object):
'model_id': model_id,
'model': self._name,
'name': k,
- 'field_description': f.string.replace("'", " "),
+ 'field_description': f.string,
'ttype': f._type,
'relation': f._obj or '',
'view_load': (f.view_load and 1) or 0,
@@ -772,7 +767,7 @@ class BaseModel(object):
'required': (f.required and 1) or 0,
'selectable': (f.selectable and 1) or 0,
'translate': (f.translate and 1) or 0,
- 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '',
+ 'relation_field': f._fields_id if isinstance(f, fields.one2many) else '',
'serialization_field_id': None,
}
if getattr(f, 'serialization_field', None):
@@ -1059,6 +1054,13 @@ class BaseModel(object):
assert self._log_access, "TransientModels must have log_access turned on, "\
"in order to implement their access rights policy"
+ # Validate rec_name
+ if self._rec_name is not None:
+ assert self._rec_name in self._columns.keys() + ['id'], "Invalid rec_name %s for model %s" % (self._rec_name, self._name)
+ else:
+ self._rec_name = 'name'
+
+
def __export_row(self, cr, uid, row, fields, context=None):
if context is None:
context = {}
@@ -1572,6 +1574,12 @@ class BaseModel(object):
res.extend(self.pool.get(parent).fields_get_keys(cr, user, context))
return res
+ def _rec_name_fallback(self, cr, uid, context=None):
+ rec_name = self._rec_name
+ if rec_name not in self._columns:
+ rec_name = self._columns.keys()[0] if len(self._columns.keys()) > 0 else "id"
+ return rec_name
+
#
# Overload this method if you need a window title which depends on the context
#
@@ -1742,6 +1750,15 @@ class BaseModel(object):
# translate view
if 'lang' in context:
+ if node.text and node.text.strip():
+ trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.text.strip())
+ if trans:
+ node.text = node.text.replace(node.text.strip(), trans)
+ if node.tail and node.tail.strip():
+ trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.tail.strip())
+ if trans:
+ node.tail = node.tail.replace(node.tail.strip(), trans)
+
if node.get('string') and not result:
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('string'))
if trans == node.get('string') and ('base_model_name' in context):
@@ -1750,18 +1767,13 @@ class BaseModel(object):
trans = self.pool.get('ir.translation')._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
if trans:
node.set('string', trans)
- if node.get('confirm'):
- trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('confirm'))
- if trans:
- node.set('confirm', trans)
- if node.get('sum'):
- trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('sum'))
- if trans:
- node.set('sum', trans)
- if node.get('help'):
- trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('help'))
- if trans:
- node.set('help', trans)
+
+ for attr_name in ('confirm', 'sum', 'help', 'placeholder'):
+ attr_value = node.get(attr_name)
+ if attr_value:
+ trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], attr_value)
+ if trans:
+ node.set(attr_name, trans)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
@@ -1860,8 +1872,7 @@ class BaseModel(object):
return view
def _get_default_search_view(self, cr, user, context=None):
- """ Generates a single-field tree view, using _rec_name if
- it's one of the columns or the first column it finds otherwise
+ """ Generates a single-field search view, based on _rec_name.
:param cr: database cursor
:param int user: user id
@@ -1869,17 +1880,12 @@ class BaseModel(object):
:returns: a tree view as an lxml document
:rtype: etree._Element
"""
- _rec_name = self._rec_name
- if _rec_name not in self._columns:
- _rec_name = self._columns.keys()[0] if len(self._columns.keys()) > 0 else "id"
-
view = etree.Element('search', string=self._description)
- etree.SubElement(view, 'field', name=_rec_name)
+ etree.SubElement(view, 'field', name=self._rec_name_fallback(cr, user, context))
return view
def _get_default_tree_view(self, cr, user, context=None):
- """ Generates a single-field tree view, using _rec_name if
- it's one of the columns or the first column it finds otherwise
+ """ Generates a single-field tree view, based on _rec_name.
:param cr: database cursor
:param int user: user id
@@ -1887,12 +1893,8 @@ class BaseModel(object):
:returns: a tree view as an lxml document
:rtype: etree._Element
"""
- _rec_name = self._rec_name
- if _rec_name not in self._columns:
- _rec_name = self._columns.keys()[0] if len(self._columns.keys()) > 0 else "id"
-
view = etree.Element('tree', string=self._description)
- etree.SubElement(view, 'field', name=_rec_name)
+ etree.SubElement(view, 'field', name=self._rec_name_fallback(cr, user, context))
return view
def _get_default_calendar_view(self, cr, user, context=None):
@@ -1919,7 +1921,7 @@ class BaseModel(object):
return False
view = etree.Element('calendar', string=self._description)
- etree.SubElement(view, 'field', name=self._rec_name)
+ etree.SubElement(view, 'field', self._rec_name_fallback(cr, user, context))
if (self._date_name not in self._columns):
date_found = False
@@ -2277,8 +2279,13 @@ class BaseModel(object):
return []
if isinstance(ids, (int, long)):
ids = [ids]
- return [(r['id'], tools.ustr(r[self._rec_name])) for r in self.read(cr, user, ids,
- [self._rec_name], context, load='_classic_write')]
+
+ if self._rec_name in self._all_columns:
+ rec_name_column = self._all_columns[self._rec_name].column
+ return [(r['id'], rec_name_column.as_display_name(cr, user, self, r[self._rec_name], context=context))
+ for r in self.read(cr, user, ids, [self._rec_name],
+ load='_classic_write', context=context)]
+ return [(id, "%s,%s" % (self._name, id)) for id in ids]
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
"""Search for records that have a display name matching the given ``name`` pattern if compared
@@ -2737,13 +2744,45 @@ class BaseModel(object):
_schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
self._table, column['attname'])
- # quick creation of ir.model.data entry to make uninstall of schema elements easier
- def _make_ext_id(self, cr, ext_id):
- cr.execute('SELECT 1 FROM ir_model_data WHERE name=%s AND module=%s', (ext_id, self._module))
+ def _save_constraint(self, cr, constraint_name, type):
+ """
+ Record the creation of a constraint for this model, to make it possible
+ to delete it later when the module is uninstalled. Type can be either
+ 'f' or 'u' depending on the constraing being a foreign key or not.
+ """
+ assert type in ('f', 'u')
+ cr.execute("""
+ SELECT 1 FROM ir_model_constraint, ir_module_module
+ WHERE ir_model_constraint.module=ir_module_module.id
+ AND ir_model_constraint.name=%s
+ AND ir_module_module.name=%s
+ """, (constraint_name, self._module))
if not cr.rowcount:
- cr.execute("""INSERT INTO ir_model_data (name,date_init,date_update,module,model)
- VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', %s, %s)""",
- (ext_id, self._module, self._name))
+ cr.execute("""
+ INSERT INTO ir_model_constraint
+ (name, date_init, date_update, module, model, type)
+ VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
+ (SELECT id FROM ir_module_module WHERE name=%s),
+ (SELECT id FROM ir_model WHERE model=%s), %s)""",
+ (constraint_name, self._module, self._name, type))
+
+ def _save_relation_table(self, cr, relation_table):
+ """
+ Record the creation of a many2many for this model, to make it possible
+ to delete it later when the module is uninstalled.
+ """
+ cr.execute("""
+ SELECT 1 FROM ir_model_relation, ir_module_module
+ WHERE ir_model_relation.module=ir_module_module.id
+ AND ir_model_relation.name=%s
+ AND ir_module_module.name=%s
+ """, (relation_table, self._module))
+ if not cr.rowcount:
+ cr.execute("""INSERT INTO ir_model_relation (name, date_init, date_update, module, model)
+ VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
+ (SELECT id FROM ir_module_module WHERE name=%s),
+ (SELECT id FROM ir_model WHERE model=%s))""",
+ (relation_table, self._module, self._name))
# checked version: for direct m2o starting from `self`
def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete):
@@ -3084,7 +3123,7 @@ class BaseModel(object):
""" Create the foreign keys recorded by _auto_init. """
for t, k, r, d in self._foreign_keys:
cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d))
- self._make_ext_id(cr, "%s%s_%s_fkey" % (EXT_ID_PREFIX_FK, t, k))
+ self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f')
cr.commit()
del self._foreign_keys
@@ -3173,7 +3212,7 @@ class BaseModel(object):
def _m2m_raise_or_create_relation(self, cr, f):
m2m_tbl, col1, col2 = f._sql_names(self)
- self._make_ext_id(cr, EXT_ID_PREFIX_M2M_TABLE + m2m_tbl)
+ self._save_relation_table(cr, m2m_tbl)
cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,))
if not cr.dictfetchall():
if not self.pool.get(f._obj):
@@ -3209,7 +3248,7 @@ class BaseModel(object):
for (key, con, _) in self._sql_constraints:
conname = '%s_%s' % (self._table, key)
- self._make_ext_id(cr, EXT_ID_PREFIX_CONSTRAINT + conname)
+ self._save_constraint(cr, conname, 'u')
cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,))
existing_constraints = cr.dictfetchall()
sql_actions = {
@@ -4121,6 +4160,12 @@ class BaseModel(object):
self.check_create(cr, user)
+ if self._log_access:
+ for f in LOG_ACCESS_COLUMNS:
+ if vals.pop(f, None) is not None:
+ _logger.warning(
+ 'Field `%s` is not allowed when creating the model `%s`.',
+ f, self._name)
vals = self._add_missing_default_values(cr, user, vals, context)
tocreate = {}
diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py
index fb4b937208e..f643b60c297 100644
--- a/openerp/tests/__init__.py
+++ b/openerp/tests/__init__.py
@@ -12,6 +12,7 @@ import test_expression
import test_ir_sequence
import test_orm
import test_view_validation
+import test_uninstall
fast_suite = [
test_ir_sequence,
diff --git a/openerp/tests/addons/test_exceptions/view.xml b/openerp/tests/addons/test_exceptions/view.xml
index 296f862aec4..34ec03f01de 100644
--- a/openerp/tests/addons/test_exceptions/view.xml
+++ b/openerp/tests/addons/test_exceptions/view.xml
@@ -38,11 +38,6 @@
-
-
-
-
-
diff --git a/openerp/tests/addons/test_uninstall/__init__.py b/openerp/tests/addons/test_uninstall/__init__.py
new file mode 100644
index 00000000000..fe4487156b1
--- /dev/null
+++ b/openerp/tests/addons/test_uninstall/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import models
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_uninstall/__openerp__.py b/openerp/tests/addons/test_uninstall/__openerp__.py
new file mode 100644
index 00000000000..d8776897df0
--- /dev/null
+++ b/openerp/tests/addons/test_uninstall/__openerp__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-uninstall',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to test the uninstall feature.""",
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base'],
+ 'data': [],
+ 'installable': True,
+ 'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_uninstall/models.py b/openerp/tests/addons/test_uninstall/models.py
new file mode 100644
index 00000000000..96a5800917d
--- /dev/null
+++ b/openerp/tests/addons/test_uninstall/models.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+import openerp
+from openerp.osv import fields
+from openerp.osv.orm import Model
+
+class test_uninstall_model(Model):
+ """
+ This model uses different types of columns to make it possible to test
+ the uninstall feature of OpenERP.
+ """
+ _name = 'test_uninstall.model'
+
+ _columns = {
+ 'name': fields.char('Name', size=64),
+ 'ref': fields.many2one('res.users', string='User'),
+ 'rel': fields.many2many('res.users', string='Users'),
+ }
+
+ _sql_constraints = [
+ ('name_uniq', 'unique (name)', 'Each name must be unique.')
+ ]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/test_orm.py b/openerp/tests/test_orm.py
index 0c4046df2a4..ea9b37e5f3b 100644
--- a/openerp/tests/test_orm.py
+++ b/openerp/tests/test_orm.py
@@ -20,7 +20,6 @@ class TestO2MSerialization(common.TransactionCase):
def setUp(self):
super(TestO2MSerialization, self).setUp()
self.partner = self.registry('res.partner')
- self.address = self.registry('res.partner.address')
def test_no_command(self):
" empty list of commands yields an empty list of records "
@@ -43,9 +42,9 @@ class TestO2MSerialization(common.TransactionCase):
def test_LINK_TO_command(self):
" reads the records from the database, records are returned with their ids. "
ids = [
- self.address.create(self.cr, UID, {'name': 'foo'}),
- self.address.create(self.cr, UID, {'name': 'bar'}),
- self.address.create(self.cr, UID, {'name': 'baz'})
+ self.partner.create(self.cr, UID, {'name': 'foo'}),
+ self.partner.create(self.cr, UID, {'name': 'bar'}),
+ self.partner.create(self.cr, UID, {'name': 'baz'})
]
commands = map(LINK_TO, ids)
@@ -61,9 +60,9 @@ class TestO2MSerialization(common.TransactionCase):
def test_bare_ids_command(self):
" same as the equivalent LINK_TO commands "
ids = [
- self.address.create(self.cr, UID, {'name': 'foo'}),
- self.address.create(self.cr, UID, {'name': 'bar'}),
- self.address.create(self.cr, UID, {'name': 'baz'})
+ self.partner.create(self.cr, UID, {'name': 'foo'}),
+ self.partner.create(self.cr, UID, {'name': 'bar'}),
+ self.partner.create(self.cr, UID, {'name': 'baz'})
]
results = self.partner.resolve_o2m_commands_to_record_dicts(
@@ -77,9 +76,9 @@ class TestO2MSerialization(common.TransactionCase):
def test_UPDATE_command(self):
" take the in-db records and merge the provided information in "
- id_foo = self.address.create(self.cr, UID, {'name': 'foo'})
- id_bar = self.address.create(self.cr, UID, {'name': 'bar'})
- id_baz = self.address.create(self.cr, UID, {'name': 'baz', 'city': 'tag'})
+ id_foo = self.partner.create(self.cr, UID, {'name': 'foo'})
+ id_bar = self.partner.create(self.cr, UID, {'name': 'bar'})
+ id_baz = self.partner.create(self.cr, UID, {'name': 'baz', 'city': 'tag'})
results = self.partner.resolve_o2m_commands_to_record_dicts(
self.cr, UID, 'address', [
@@ -96,7 +95,7 @@ class TestO2MSerialization(common.TransactionCase):
def test_mixed_commands(self):
ids = [
- self.address.create(self.cr, UID, {'name': name})
+ self.partner.create(self.cr, UID, {'name': name})
for name in ['NObar', 'baz', 'qux', 'NOquux', 'NOcorge', 'garply']
]
@@ -126,9 +125,9 @@ class TestO2MSerialization(common.TransactionCase):
def test_LINK_TO_pairs(self):
"LINK_TO commands can be written as pairs, instead of triplets"
ids = [
- self.address.create(self.cr, UID, {'name': 'foo'}),
- self.address.create(self.cr, UID, {'name': 'bar'}),
- self.address.create(self.cr, UID, {'name': 'baz'})
+ self.partner.create(self.cr, UID, {'name': 'foo'}),
+ self.partner.create(self.cr, UID, {'name': 'bar'}),
+ self.partner.create(self.cr, UID, {'name': 'baz'})
]
commands = map(lambda id: (4, id), ids)
diff --git a/openerp/tests/test_uninstall.py b/openerp/tests/test_uninstall.py
new file mode 100644
index 00000000000..1f74b660ce0
--- /dev/null
+++ b/openerp/tests/test_uninstall.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# This assumes an existing but uninitialized database.
+import psycopg2
+import unittest2
+
+import openerp
+from openerp import SUPERUSER_ID
+import common
+
+DB = common.DB
+ADMIN_USER_ID = common.ADMIN_USER_ID
+
+def registry(model):
+ return openerp.modules.registry.RegistryManager.get(DB)[model]
+
+def cursor():
+ return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+
+def get_module(module_name):
+ registry = openerp.modules.registry.RegistryManager.get(DB)
+ return registry.get(module_name)
+
+def reload_registry():
+ openerp.modules.registry.RegistryManager.new(
+ DB, update_module=True)
+
+def search_registry(model_name, domain):
+ cr = cursor()
+ model = registry(model_name)
+ record_ids = model.search(cr, SUPERUSER_ID, domain, {})
+ cr.close()
+ return record_ids
+
+def install_module(module_name):
+ ir_module_module = registry('ir.module.module')
+ cr = cursor()
+ module_ids = ir_module_module.search(cr, SUPERUSER_ID,
+ [('name', '=', module_name)], {})
+ assert len(module_ids) == 1
+ ir_module_module.button_install(cr, SUPERUSER_ID, module_ids, {})
+ cr.commit()
+ cr.close()
+ reload_registry()
+
+def uninstall_module(module_name):
+ ir_module_module = registry('ir.module.module')
+ cr = cursor()
+ module_ids = ir_module_module.search(cr, SUPERUSER_ID,
+ [('name', '=', module_name)], {})
+ assert len(module_ids) == 1
+ ir_module_module.button_uninstall(cr, SUPERUSER_ID, module_ids, {})
+ cr.commit()
+ cr.close()
+ reload_registry()
+
+class test_uninstall(unittest2.TestCase):
+ """
+ Test the install/uninstall of a test module. The module is available in
+ `openerp.tests` which should be present in the addons-path.
+ """
+
+ def test_01_install(self):
+ """ Check a few things showing the module is installed. """
+ install_module('test_uninstall')
+ assert get_module('test_uninstall.model')
+
+ assert search_registry('ir.model.data',
+ [('module', '=', 'test_uninstall')])
+
+ assert search_registry('ir.model.fields',
+ [('model', '=', 'test_uninstall.model')])
+
+ def test_02_uninstall(self):
+ """ Check a few things showing the module is uninstalled. """
+ uninstall_module('test_uninstall')
+ assert not get_module('test_uninstall.model')
+
+ assert not search_registry('ir.model.data',
+ [('module', '=', 'test_uninstall')])
+
+ assert not search_registry('ir.model.fields',
+ [('model', '=', 'test_uninstall.model')])
+
+
+
+if __name__ == '__main__':
+ unittest2.main()
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py
index a46a6663217..3130ee4446b 100644
--- a/openerp/tools/convert.py
+++ b/openerp/tools/convert.py
@@ -142,18 +142,20 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None):
except Exception:
_logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
return ""
+ def _process(s, idref):
+ m = re.findall('[^%]%\((.*?)\)[ds]', s)
+ for id in m:
+ if not id in idref:
+ idref[id]=self.id_get(cr, id)
+ return s % idref
if t == 'xml':
- def _process(s, idref):
- m = re.findall('[^%]%\((.*?)\)[ds]', s)
- for id in m:
- if not id in idref:
- idref[id]=self.id_get(cr, id)
- return s % idref
_fix_multiple_roots(node)
return '\n'\
+_process("".join([etree.tostring(n, encoding='utf-8')
- for n in node]),
- idref)
+ for n in node]), idref)
+ if t == 'html':
+ return _process("".join([etree.tostring(n, encoding='utf-8')
+ for n in node]), idref)
if t in ('char', 'int', 'float'):
d = node.text
if t == 'int':
diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py
index 440575b87dc..eb5dbe24077 100644
--- a/openerp/tools/translate.py
+++ b/openerp/tools/translate.py
@@ -501,6 +501,10 @@ def trans_parse_rml(de):
def trans_parse_view(de):
res = []
+ if de.text and de.text.strip():
+ res.append(de.text.strip().encode("utf8"))
+ if de.tail and de.tail.strip():
+ res.append(de.tail.strip().encode("utf8"))
if de.tag == 'attribute' and de.get("name") == 'string':
if de.text:
res.append(de.text.encode("utf8"))