summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-07-04 18:57:17 +0200
committerChristophe Simonis <chs@odoo.com>2019-07-04 18:57:17 +0200
commit7b74f4ca9a165a697853079d11d105eebc1f0cf9 (patch)
tree077737d05893b3e25dd6092dc914076e143f60d3 /odoo
parent789aa140b96226718bfecc52ec2fe33273e31d3f (diff)
parent1debaa89f14cf662c43744b5cefb476f10eb8a93 (diff)
[MERGE] forward port branch saas-11.3 up to 1debaa89f14
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/models/ir_mail_server.py5
-rw-r--r--odoo/addons/test_rpc/__init__.py2
-rw-r--r--odoo/addons/test_rpc/__manifest__.py11
-rw-r--r--odoo/addons/test_rpc/ir.model.access.csv3
-rw-r--r--odoo/addons/test_rpc/models.py20
-rw-r--r--odoo/addons/test_rpc/tests/__init__.py3
-rw-r--r--odoo/addons/test_rpc/tests/test_error.py67
-rw-r--r--odoo/service/model.py46
8 files changed, 143 insertions, 14 deletions
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index 58eb2afe5a5..3e64d982187 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -278,6 +278,11 @@ class IrMailServer(models.Model):
smtp_user = pycompat.to_native(ustr(smtp_user))
smtp_password = pycompat.to_native(ustr(smtp_password))
connection.login(smtp_user, smtp_password)
+
+ # Some methods of SMTP don't check whether EHLO/HELO was sent.
+ # Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent.
+ connection.ehlo_or_helo_if_needed()
+
return connection
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
diff --git a/odoo/addons/test_rpc/__init__.py b/odoo/addons/test_rpc/__init__.py
new file mode 100644
index 00000000000..a0fdc10fe11
--- /dev/null
+++ b/odoo/addons/test_rpc/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import models
diff --git a/odoo/addons/test_rpc/__manifest__.py b/odoo/addons/test_rpc/__manifest__.py
new file mode 100644
index 00000000000..6e8e68926f9
--- /dev/null
+++ b/odoo/addons/test_rpc/__manifest__.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Test RPC",
+ "version": "1.0",
+ "category": "Tests",
+ "description": """A module to test the RPC requests.""",
+ "depends": ["base", "web"],
+ "installable": True,
+ "auto_install": False,
+ "data": ["ir.model.access.csv"],
+}
diff --git a/odoo/addons/test_rpc/ir.model.access.csv b/odoo/addons/test_rpc/ir.model.access.csv
new file mode 100644
index 00000000000..5e48e2fedc6
--- /dev/null
+++ b/odoo/addons/test_rpc/ir.model.access.csv
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+access_test_rpc_model_a,access_test_rpc_model_a,model_test_rpc_model_a,,1,1,1,1
+access_test_rpc_model_b,access_test_rpc_model_b,model_test_rpc_model_b,,1,1,1,1
diff --git a/odoo/addons/test_rpc/models.py b/odoo/addons/test_rpc/models.py
new file mode 100644
index 00000000000..2d547a9947e
--- /dev/null
+++ b/odoo/addons/test_rpc/models.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ModelA(models.Model):
+ _name = "test_rpc.model_a"
+ _description = "Model A"
+
+ name = fields.Char(required=True)
+ field_b1 = fields.Many2one("test_rpc.model_b", string="required field", required=True)
+ field_b2 = fields.Many2one("test_rpc.model_b", string="restricted field", ondelete="restrict")
+
+
+class ModelB(models.Model):
+ _name = "test_rpc.model_b"
+ _description = "Model B"
+
+ name = fields.Char(required=True)
diff --git a/odoo/addons/test_rpc/tests/__init__.py b/odoo/addons/test_rpc/tests/__init__.py
new file mode 100644
index 00000000000..bf0757101fa
--- /dev/null
+++ b/odoo/addons/test_rpc/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import test_error
diff --git a/odoo/addons/test_rpc/tests/test_error.py b/odoo/addons/test_rpc/tests/test_error.py
new file mode 100644
index 00000000000..6896b1f180d
--- /dev/null
+++ b/odoo/addons/test_rpc/tests/test_error.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+from odoo.tests import common
+from odoo.tools.misc import mute_logger
+
+
+class TestError(common.HttpCase):
+ def setUp(self):
+ super(TestError, self).setUp()
+ self.o = self.xmlrpc_object
+ self.db_name = common.get_db_name()
+ # Reset the admin's lang to avoid breaking tests due to admin not in English
+ self.o.execute(self.db_name, 1, "admin", "res.users", "write", [1], {"lang": False})
+
+ def test_01_create(self):
+ """ Create: mandatory field not provided """
+ self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B1"})
+ try:
+ with mute_logger("odoo.sql_db"):
+ self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {})
+ raise
+ except Exception as e:
+ self.assertIn("The operation cannot be completed:", e.faultString)
+ self.assertIn("Create/update: a mandatory field is not set.", e.faultString)
+ self.assertIn(
+ "Delete: another model requires the record being deleted. If possible, archive it instead.",
+ e.faultString,
+ )
+ self.assertIn("Model: Model B (test_rpc.model_b), Field: Name (name)", e.faultString)
+
+ def test_02_delete(self):
+ """ Delete: NOT NULL and ON DELETE RESTRICT constraints """
+ b1 = self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B1"})
+ b2 = self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B2"})
+ self.o.execute(
+ self.db_name, 1, "admin", "test_rpc.model_a", "create", {"name": "A1", "field_b1": b1, "field_b2": b2}
+ )
+
+ # Unlink b1 => NOT NULL constraint raises
+ # FORWARD-PORT: in master, `required=True` implies `ondelete="restrict"`
+ # => the message should be the same than 'Unlink b2' case.
+ try:
+ with mute_logger("odoo.sql_db"):
+ self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "unlink", b1)
+ raise
+ except Exception as e:
+ self.assertIn("The operation cannot be completed:", e.faultString)
+ self.assertIn("Create/update: a mandatory field is not set.", e.faultString)
+ self.assertIn(
+ "Delete: another model requires the record being deleted. If possible, archive it instead.",
+ e.faultString,
+ )
+ self.assertIn("Model: Model A (test_rpc.model_a), Field: required field (field_b1)", e.faultString)
+
+ # Unlink b2 => ON DELETE RESTRICT constraint raises
+ try:
+ with mute_logger("odoo.sql_db"):
+ self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "unlink", b2)
+ raise
+ except Exception as e:
+ self.assertIn("The operation cannot be completed:", e.faultString)
+ self.assertIn(
+ " another model requires the record being deleted. If possible, archive it instead.", e.faultString
+ )
+ self.assertIn(
+ "Model: Model A (test_rpc.model_a), Constraint: test_rpc_model_a_field_b2_fkey", e.faultString
+ )
diff --git a/odoo/service/model.py b/odoo/service/model.py
index 61f63c68155..ca997d0f730 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -118,22 +118,40 @@ def check(f):
if key in inst.pgerror:
raise ValidationError(tr(registry._sql_error[key], 'sql_constraint') or inst.pgerror)
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
- msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
+ msg = _('The operation cannot be completed:')
_logger.debug("IntegrityError", exc_info=True)
try:
- errortxt = inst.pgerror.replace('«','"').replace('»','"')
- if '"public".' in errortxt:
- context = errortxt.split('"public".')[1]
- model_name = table = context.split('"')[1]
- else:
- last_quote_end = errortxt.rfind('"')
- last_quote_begin = errortxt.rfind('"', 0, last_quote_end)
- model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip()
- model = table.replace("_",".")
- if model in registry:
- model_class = registry[model]
- model_name = model_class._description or model_class._name
- msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
+ # Get corresponding model and field
+ model = field = None
+ for name, rclass in registry.items():
+ if inst.diag.table_name == rclass._table:
+ model = rclass
+ field = model._fields.get(inst.diag.column_name)
+ break
+ if inst.pgcode == errorcodes.NOT_NULL_VIOLATION:
+ # This is raised when a field is set with `required=True`. 2 cases:
+ # - Create/update: a mandatory field is not set.
+ # - Delete: another model has a not nullable using the deleted record.
+ msg += '\n'
+ msg += _(
+ '- Create/update: a mandatory field is not set.\n'
+ '- Delete: another model requires the record being deleted. If possible, archive it instead.'
+ )
+ if model:
+ msg += '\n\n{} {} ({}), {} {} ({})'.format(
+ _('Model:'), model._description, model._name,
+ _('Field:'), field.string if field else _('Unknown'), field.name if field else _('Unknown'),
+ )
+ elif inst.pgcode == errorcodes.FOREIGN_KEY_VIOLATION:
+ # This is raised when a field is set with `ondelete='restrict'`, at
+ # unlink only.
+ msg += _(' another model requires the record being deleted. If possible, archive it instead.')
+ constraint = inst.diag.constraint_name
+ if model or constraint:
+ msg += '\n\n{} {} ({}), {} {}'.format(
+ _('Model:'), model._description if model else _('Unknown'), model._name if model else _('Unknown'),
+ _('Constraint:'), constraint if constraint else _('Unknown'),
+ )
except Exception:
pass
raise ValidationError(msg)