diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-07-04 18:57:17 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-07-04 18:57:17 +0200 |
| commit | 7b74f4ca9a165a697853079d11d105eebc1f0cf9 (patch) | |
| tree | 077737d05893b3e25dd6092dc914076e143f60d3 | |
| parent | 789aa140b96226718bfecc52ec2fe33273e31d3f (diff) | |
| parent | 1debaa89f14cf662c43744b5cefb476f10eb8a93 (diff) | |
[MERGE] forward port branch saas-11.3 up to 1debaa89f14
| -rw-r--r-- | addons/account/models/partner.py | 4 | ||||
| -rw-r--r-- | addons/l10n_fr_fec/wizard/account_fr_fec.py | 2 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/models.js | 7 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/summernote.js | 2 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/widgets/widgets.js | 5 | ||||
| -rw-r--r-- | addons/web_editor/static/src/xml/editor.xml | 6 | ||||
| -rw-r--r-- | doc/cla/corporate/vauxoo.md | 1 | ||||
| -rw-r--r-- | odoo/addons/base/models/ir_mail_server.py | 5 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/__init__.py | 2 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/__manifest__.py | 11 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/models.py | 20 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/tests/__init__.py | 3 | ||||
| -rw-r--r-- | odoo/addons/test_rpc/tests/test_error.py | 67 | ||||
| -rw-r--r-- | odoo/service/model.py | 46 |
15 files changed, 161 insertions, 23 deletions
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 85ec5f9bb66..3911a07ee6b 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -240,9 +240,9 @@ class ResPartner(models.Model): LEFT JOIN account_move_line aml ON aml.partner_id = partner.id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s - AND NOT acc.deprecated + AND NOT acc.deprecated AND acc.company_id = %s GROUP BY partner.id - HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand)) + HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand)) res = self._cr.fetchall() if not res: return [('id', '=', '0')] diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 68bb8a9c09b..cb743c57e76 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -307,7 +307,7 @@ class AccountFrFec(models.TransientModel): TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/' - ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib, + ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index caf09ced6c9..8afbf4a0920 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1709,8 +1709,11 @@ exports.Orderline = Backbone.Model.extend({ }, _compute_all: function(tax, base_amount, quantity) { if (tax.amount_type === 'fixed') { - var sign_base_amount = base_amount >= 0 ? 1 : -1; - return (Math.abs(tax.amount) * sign_base_amount) * quantity; + var sign_base_amount = Math.sign(base_amount) || 1; + // Since base amount has been computed with quantity + // we take the abs of quantity + // Same logic as bb72dea98de4dae8f59e397f232a0636411d37ce + return tax.amount * sign_base_amount * Math.abs(quantity); } if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){ return base_amount * tax.amount / 100; diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index 71130f54afb..bc7f5e7ff9f 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -2284,7 +2284,7 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC options.onCreateLink = function (sLinkUrl) { if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) { - // pass + sLinkUrl = sLinkUrl.replace(/^tel:([0-9]+)$/, 'tel://$1'); } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) { sLinkUrl = 'mailto:' + sLinkUrl; } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/' diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index af89b122732..f7d5e3f74c9 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -938,7 +938,7 @@ var VideoWidget = MediaWidget.extend({ return {errorCode: 0}; } - var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0'; + var autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0'; if (ytMatch && ytMatch[2].length === 11) { $video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay); @@ -967,7 +967,8 @@ var VideoWidget = MediaWidget.extend({ $video.attr('src', $video.attr('src') + '&rel=0'); } if (options.loop && (ytMatch || vimMatch)) { - $video.attr('src', $video.attr('src') + '&loop=1'); + var videoSrc = _.str.sprintf('%s&loop=1', $video.attr('src')); + $video.attr('src', ytMatch ? _.str.sprintf('%s&playlist=%s', videoSrc, ytMatch[2]) : videoSrc); } if (options.hide_controls && (ytMatch || dmMatch)) { $video.attr('src', $video.attr('src') + '&controls=0'); diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 0b8d3c8219d..a6877bdd5e0 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -204,7 +204,11 @@ <small class="text-muted">Options</small> <ul class="list-group"> <li class="list-group-item o_yt_option o_vim_option o_dm_option"> - <label class="o_switch mb0"><input id="o_video_autoplay" type="checkbox"/><span/>Autoplay</label> + <label class="o_switch mb0"> + <input id="o_video_autoplay" type="checkbox"/><span/> + <span style="margin-right: 8px;">Autoplay</span> + <span class="small text-muted" style="margin-left: auto;">Videos are muted when autoplay is enabled</span> + </label> </li> <li class="list-group-item o_yt_option o_vim_option"> <label class="o_switch mb0"><input id="o_video_loop" type="checkbox"/><span/>Loop</label> diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md index 2cce9509360..0978a479b11 100644 --- a/doc/cla/corporate/vauxoo.md +++ b/doc/cla/corporate/vauxoo.md @@ -46,3 +46,4 @@ Erick Birbe erick@vauxoo.com https://github.com/ebirbe Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro Carmen Liliana Miranda González carmen@vauxoo.com https://github.com/CarmenMiranda Arturo Flores arturo@vauxoo.com https://github.com/umiphos +Deivis Laya deivis@vauxoo.com https://github.com/deivislaya 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) |
