diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql
index eb9966bddcd..f2cb77709ed 100644
--- a/openerp/addons/base/base.sql
+++ b/openerp/addons/base/base.sql
@@ -347,6 +347,39 @@ CREATE TABLE ir_model_data (
res_id integer, primary key(id)
);
+-- Records foreign keys and constraints installed by a module (so they can be
+-- removed when the module is uninstalled):
+-- - for a foreign key: type is 'f',
+-- - for a constraint: type is 'u' (this is the convention PostgreSQL uses).
+CREATE TABLE ir_model_constraint (
+ id serial NOT NULL,
+ create_uid integer,
+ create_date timestamp without time zone,
+ write_date timestamp without time zone,
+ write_uid integer,
+ date_init timestamp without time zone,
+ date_update timestamp without time zone,
+ module integer NOT NULL references ir_module_module on delete restrict,
+ model integer NOT NULL references ir_model on delete restrict,
+ type character varying(1) NOT NULL,
+ name character varying(128) NOT NULL
+);
+
+-- Records relation tables (i.e. implementing many2many) installed by a module
+-- (so they can be removed when the module is uninstalled).
+CREATE TABLE ir_model_relation (
+ id serial NOT NULL,
+ create_uid integer,
+ create_date timestamp without time zone,
+ write_date timestamp without time zone,
+ write_uid integer,
+ date_init timestamp without time zone,
+ date_update timestamp without time zone,
+ module integer NOT NULL references ir_module_module on delete restrict,
+ model integer NOT NULL references ir_model on delete restrict,
+ name character varying(128) NOT NULL
+);
+
---------------------------------
-- Users
---------------------------------
diff --git a/openerp/addons/base/i18n/base.pot b/openerp/addons/base/i18n/base.pot
index 83c982e76b9..b4d28052ba8 100644
--- a/openerp/addons/base/i18n/base.pot
+++ b/openerp/addons/base/i18n/base.pot
@@ -467,7 +467,7 @@ msgstr ""
#. module: base
#: field:res.bank,email:0
#: field:res.partner.address,email:0
-msgid "E-Mail"
+msgid "Email
msgstr ""
#. module: base
@@ -544,16 +544,16 @@ msgid "\n"
"==========================================================================\n"
"\n"
"You can define your multiple levels of recall through the menu:\n"
-" Accounting/Configuration/Miscellaneous/Follow-Ups\n"
+" Accounting/Configuration/Miscellaneous/Follow-ups\n"
"\n"
"Once it is defined, you can automatically print recalls every day through simply clicking on the menu:\n"
-" Accounting/Periodical Processing/Billing/Send followups\n"
+" Accounting/Periodical Processing/Billing/Send follow-ups\n"
"\n"
"It will generate a PDF with all the letters according to the the\n"
"different levels of recall defined. You can define different policies\n"
"for different companies. You can also send mail to the customer.\n"
"\n"
-"Note that if you want to check the followup level for a given partner/account entry, you can do from in the menu:\n"
+"Note that if you want to check the follow-up level for a given partner/account entry, you can do from in the menu:\n"
" Accounting/Reporting/Generic Reporting/Partners/Follow-ups Sent\n"
"\n"
""
@@ -7592,7 +7592,7 @@ msgstr ""
#. module: base
#: model:ir.module.module,shortdesc:base.module_email_template
-msgid "E-Mail Templates"
+msgid "Email Templates
msgstr ""
#. module: base
@@ -9455,7 +9455,7 @@ msgstr ""
#. module: base
#: field:res.partner,email:0
-msgid "E-mail"
+msgid "Email
msgstr ""
#. module: base
@@ -12688,7 +12688,7 @@ msgstr ""
#. module: base
#: model:ir.module.module,shortdesc:base.module_account_followup
-msgid "Followup Management"
+msgid "Follow-up Management"
msgstr ""
#. module: base
diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py
index a2a4b999ed8..bc4f3eecfbf 100644
--- a/openerp/addons/base/ir/__init__.py
+++ b/openerp/addons/base/ir/__init__.py
@@ -20,6 +20,8 @@
##############################################################################
import ir_model
+import ir_model_constraint
+import ir_model_relation
import ir_sequence
import ir_needaction
import ir_ui_menu
diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml
index 6d5da0fc47a..392bb60c53e 100644
--- a/openerp/addons/base/ir/ir.xml
+++ b/openerp/addons/base/ir/ir.xml
@@ -1127,6 +1127,65 @@
+
+ ir.model.constraint.form
+ ir.model.constraint
+ form
+
+
+
+
+
+
+ ir.model.constraint.list
+ ir.model.constraint
+ tree
+
+
+
+
+
+
+
+
+
+
+
+ ir.model.relation.form
+ ir.model.relation
+ form
+
+
+
+
+
+
+ ir.model.relation.list
+ ir.model.relation
+ tree
+
+
+
+
+
+
+
+
Models
@@ -1156,6 +1215,24 @@
+
+ Model Constraints
+ ir.model.constraint
+ form
+
+
+
+
+
+ ManyToMany Relations
+ ir.model.relation
+ form
+
+
+
+
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 b366d1dfb1c..beed30a2178 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__)
@@ -864,7 +863,7 @@ class ir_model_data(osv.osv):
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
@@ -875,6 +874,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')))
@@ -884,7 +885,6 @@ 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):
@@ -893,42 +893,6 @@ class ir_model_data(osv.osv):
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:
to_unlink.append(pair_to_unlink)
@@ -948,17 +912,12 @@ class ir_model_data(osv.osv):
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)
-
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 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)
@@ -970,11 +929,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/module/module.py b/openerp/addons/base/module/module.py
index d75f330afa2..392850f7b77 100644
--- a/openerp/addons/base/module/module.py
+++ b/openerp/addons/base/module/module.py
@@ -378,10 +378,11 @@ class module(osv.osv):
including the deletion of all database structures created by the module:
tables, columns, constraints, etc."""
ir_model_data = self.pool.get('ir.model.data')
+ ir_model_constraint = self.pool.get('ir.model.constraint')
modules_to_remove = [m.name for m in self.browse(cr, uid, ids, context)]
- data_ids = ir_model_data.search(cr, uid, [('module', 'in', modules_to_remove)])
- ir_model_data._module_data_uninstall(cr, uid, data_ids, context)
- ir_model_data.unlink(cr, uid, data_ids, context)
+ constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove)])
+ ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
+ ir_model_data._module_data_uninstall(cr, uid, modules_to_remove, context)
self.write(cr, uid, ids, {'state': 'uninstalled'})
return True
diff --git a/openerp/addons/base/res/res_bank.py b/openerp/addons/base/res/res_bank.py
index 48b5973cf64..c9010b64319 100644
--- a/openerp/addons/base/res/res_bank.py
+++ b/openerp/addons/base/res/res_bank.py
@@ -35,7 +35,7 @@ class Bank(osv.osv):
'state': fields.many2one("res.country.state", 'Fed. State',
domain="[('country_id', '=', country)]"),
'country': fields.many2one('res.country', 'Country'),
- 'email': fields.char('E-Mail', size=64),
+ 'email': fields.char('Email', size=64),
'phone': fields.char('Phone', size=64),
'fax': fields.char('Fax', size=64),
'active': fields.boolean('Active'),
diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py
index 08cf19731c6..a38d434de88 100644
--- a/openerp/addons/base/res/res_partner.py
+++ b/openerp/addons/base/res/res_partner.py
@@ -89,7 +89,7 @@ class res_partner_category(osv.osv):
'active' : fields.boolean('Active', help="The active field allows you to hide the category without removing it."),
'parent_left' : fields.integer('Left parent', select=True),
'parent_right' : fields.integer('Right parent', select=True),
- 'partner_ids': fields.many2many('res.partner', 'res_partner_category_rel', 'category_id', 'partner_id', 'Partners'),
+ 'partner_ids': fields.many2many('res.partner', id1='category_id', id2='partner_id', string='Partners'),
}
_constraints = [
(osv.osv._check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
@@ -171,7 +171,7 @@ class res_partner(osv.osv):
'website': fields.char('Website',size=64, help="Website of Partner or Company"),
'comment': fields.text('Notes'),
'address': fields.one2many('res.partner.address', 'partner_id', 'Contacts'), # should be removed in version 7, but kept until then for backward compatibility
- 'category_id': fields.many2many('res.partner.category', 'res_partner_category_rel', 'partner_id', 'category_id', 'Tags'),
+ 'category_id': fields.many2many('res.partner.category', id1='partner_id', id2='category_id', string='Tags'),
'credit_limit': fields.float(string='Credit Limit'),
'ean13': fields.char('EAN13', size=13),
'active': fields.boolean('Active'),
@@ -190,7 +190,7 @@ class res_partner(osv.osv):
'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"),
'country_id': fields.many2one('res.country', 'Country'),
'country': fields.related('country_id', type='many2one', relation='res.country', string='Country'), # for backward compatibility
- 'email': fields.char('E-Mail', size=240),
+ 'email': fields.char('Email', size=240),
'phone': fields.char('Phone', size=64),
'fax': fields.char('Fax', size=64),
'mobile': fields.char('Mobile', size=64),
@@ -495,7 +495,7 @@ class res_partner_address(osv.osv):
'city': fields.char('City', size=128),
'state_id': fields.many2one("res.country.state", 'Fed. State', domain="[('country_id','=',country_id)]"),
'country_id': fields.many2one('res.country', 'Country'),
- 'email': fields.char('E-Mail', size=240),
+ 'email': fields.char('Email', size=240),
'phone': fields.char('Phone', size=64),
'fax': fields.char('Fax', size=64),
'mobile': fields.char('Mobile', size=64),
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/modules/module.py b/openerp/modules/module.py
index e226b69a6fc..265a23190e5 100644
--- a/openerp/modules/module.py
+++ b/openerp/modules/module.py
@@ -493,7 +493,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 +554,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/orm.py b/openerp/osv/orm.py
index 9be2a1bed12..4284f77d1d7 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_.]+$')
@@ -1762,6 +1757,10 @@ class BaseModel(object):
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('help'))
if trans:
node.set('help', trans)
+ if node.get('placeholder'):
+ trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('placeholder'))
+ if trans:
+ node.set('placeholder', trans)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
@@ -2737,13 +2736,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 +3115,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 +3204,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 +3240,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 = {
diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py
index 5fccb07a082..913c5c759c9 100644
--- a/openerp/tests/__init__.py
+++ b/openerp/tests/__init__.py
@@ -11,6 +11,7 @@ See the :ref:`test-framework` section in the :ref:`features` list.
import test_expression
import test_ir_sequence
import test_orm
+import test_uninstall
fast_suite = [
test_ir_sequence,
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_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: