diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 55e506bbe8c..4c85e1d3a66 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1090,8 +1090,9 @@ class AccountMoveLine(models.Model): rec_move_ids += account_move_line.matched_credit_ids if self.env.context.get('invoice_id'): current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id']) + aml_to_keep = current_invoice.move_id.line_ids | current_invoice.move_id.line_ids.mapped('full_reconcile_id.exchange_move_id.line_ids') rec_move_ids = rec_move_ids.filtered( - lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids + lambda r: (r.debit_move_id + r.credit_move_id) & aml_to_keep ) return rec_move_ids.unlink() diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 8616fbb746f..af7abc9409b 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -656,3 +656,51 @@ class TestReconciliation(AccountingTestCase): credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile() self.assertAlmostEquals(inv1.residual, 10) self.assertAlmostEquals(inv2.residual, 20) + + def test_unreconcile_exchange(self): + # Use case: + # - Company currency in EUR + # - Create 2 rates for USD: + # 1.0 on 2018-01-01 + # 0.5 on 2018-02-01 + # - Create an invoice on 2018-01-02 of 111 USD + # - Register a payment on 2018-02-02 of 111 USD + # - Unreconcile the payment + + self.env['res.currency.rate'].create({ + 'name': time.strftime('%Y') + '-07-01', + 'rate': 1.0, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id + }) + self.env['res.currency.rate'].create({ + 'name': time.strftime('%Y') + '-08-01', + 'rate': 0.5, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id + }) + inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id) + payment = self.env['account.payment'].create({ + 'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 111, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_usd.id, + 'payment_date': time.strftime('%Y') + '-08-01', + }) + payment.post() + credit_aml = payment.move_line_ids.filtered('credit') + + # Check residual before assignation + self.assertAlmostEquals(inv.residual, 111) + + # Assign credit, check exchange move and residual + inv.assign_outstanding_credit(credit_aml.id) + self.assertEqual(len(payment.move_line_ids.mapped('full_reconcile_id').exchange_move_id), 1) + self.assertAlmostEquals(inv.residual, 0) + + # Unreconcile invoice and check residual + credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile() + self.assertAlmostEquals(inv.residual, 111) diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 42515a8e20b..52e8a8bd790 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -35,7 +35,7 @@ class AccountRegisterPayments(models.TransientModel): res = super(AccountRegisterPayments, self).get_payment_vals() if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'): res.update({ - 'check_manual_sequencing': self.check_manual_sequencing, + 'check_amount_in_words': self.check_amount_in_words, }) return res diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index a7447657742..81006c2f337 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -146,7 +146,16 @@ function make_message (data) { _.each(_.keys(emoji_substitutions), function (key) { var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|)", "g"); + var msg_bak = msg.body; msg.body = msg.body.replace(regexp, ' '+emoji_substitutions[key]+' '); + + // Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis, + // the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome). + // Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are + // already replaced by the unicode counterpart). + if (_.str.count(msg.body, 'o_mail_emoji') > 200) { + msg.body = msg_bak; + } }); function property_descr(channel) { diff --git a/addons/product/models/product.py b/addons/product/models/product.py index a5ce68d1773..09ded942500 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -452,7 +452,12 @@ class ProductProduct(models.Model): limit2 = (limit - len(products)) if limit else False products += self.search(args + [('name', operator, name), ('id', 'not in', products.ids)], limit=limit2) elif not products and operator in expression.NEGATIVE_TERM_OPERATORS: - products = self.search(args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit) + domain = expression.OR([ + ['&', ('default_code', operator, name), ('name', operator, name)], + ['&', ('default_code', '=', False), ('name', operator, name)], + ]) + domain = expression.AND([args, domain]) + products = self.search(domain, limit=limit) if not products and operator in positive_operators: ptrn = re.compile('(\[(.*?)\])') res = ptrn.search(name) diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py index 1a621d689cb..ce71b8532b7 100644 --- a/addons/product/tests/test_variants.py +++ b/addons/product/tests/test_variants.py @@ -40,6 +40,15 @@ class TestVariantsSearch(TransactionCase): self.assertIn(self.product_shirt_template, search_value, 'Shirt should be found searching L') + def test_name_search(self): + self.product_slip_template = self.env['product.template'].create({ + 'name': 'Slip', + }) + res = self.env['product.product'].name_search('Shirt', [], 'not ilike', None) + res_ids = [r[0] for r in res] + self.assertIn(self.product_slip_template.product_variant_ids.id, res_ids, + 'Slip should be found searching \'not ilike\'') + class TestVariants(common.TestProductCommon): diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index b0d0865af47..179e9c69415 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -988,7 +988,7 @@ class ProcurementOrder(models.Model): self.ensure_one() schedule_date = self._get_purchase_schedule_date() purchase_date = self._get_purchase_order_date(partner, schedule_date) - fpos = self.env['account.fiscal.position'].with_context(company_id=self.company_id.id).get_fiscal_position(partner.id) + fpos = self.env['account.fiscal.position'].with_context(force_company=self.company_id.id).get_fiscal_position(partner.id) gpo = self.rule_id.group_propagation_option group = (gpo == 'fixed' and self.rule_id.group_id.id) or \ diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js index dee6f4599b4..e74ae67dc04 100644 --- a/addons/web/static/src/js/framework/ajax.js +++ b/addons/web/static/src/js/framework/ajax.js @@ -252,8 +252,12 @@ function get_file(options) { if (options.error) { var body = this.contentDocument.body; var nodes = body.children.length === 0 ? body.childNodes : body.children; - var node = nodes[1] || nodes[0]; - options.error(JSON.parse(node.textContent)); + options.error({ + message: nodes.length > 1 ? nodes[1].textContent : '', + data: { + title: nodes.length > 0 ? nodes[0].textContent : '', + }, + }); } } finally { complete(); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js b/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js index 955b49437a8..4cd9088da75 100644 --- a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js +++ b/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js @@ -1,256 +1,41 @@ define([ - 'summernote/core/list', - 'summernote/core/dom', - 'summernote/core/key', - 'summernote/core/agent', - 'summernote/core/range' + 'summernote/core/list', + 'summernote/core/dom', + 'summernote/core/key', + 'summernote/core/agent', + 'summernote/core/range' ], function (list, dom, key, agent, range) { - var Clipboard = function (handler) { - var $paste; + // ODOO override: use 0.8.10 version of this, adapted for the old summernote + // version odoo is using + var Clipboard = function (handler) { + /** + * paste by clipboard event + * + * @param {Event} event + */ + var pasteByEvent = function (event) { + if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) { + // ODOO override: from old summernote version + return; + } - this.attach = function (layoutInfo) { - // [workaround] getting image from clipboard - // - IE11 and Firefox: CTRL+v hook - // - Webkit: event.clipboardData - if (agent.isMSIE && agent.browserVersion > 10) { - $paste = $('
').attr('contenteditable', true).css({ - position : 'absolute', - left : -100000, - opacity : 0 - }); + var clipboardData = event.originalEvent.clipboardData; + var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); + var $editable = layoutInfo.editable(); - layoutInfo.editable().on('keydown', function (e) { - if (e.ctrlKey && e.keyCode === key.code.V) { - handler.invoke('saveRange', layoutInfo.editable()); - $paste.focus(); + if (clipboardData && clipboardData.items && clipboardData.items.length) { + var item = list.head(clipboardData.items); + if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { + handler.insertImages(layoutInfo, [item.getAsFile()]); + } + handler.invoke('editor.afterCommand', $editable); + } + }; - setTimeout(function () { - pasteByHook(layoutInfo); - }, 0); - } - }); - - layoutInfo.editable().before($paste); - } else { - layoutInfo.editable().on('paste', pasteByEvent); - } + this.attach = function (layoutInfo) { + layoutInfo.editable().on('paste', pasteByEvent); + }; }; - var pasteByHook = function (layoutInfo) { - var $editable = layoutInfo.editable(); - var node = $paste[0].firstChild; - - if (dom.isImg(node)) { - var dataURI = node.src; - var decodedData = atob(dataURI.split(',')[1]); - var array = new Uint8Array(decodedData.length); - for (var i = 0; i < decodedData.length; i++) { - array[i] = decodedData.charCodeAt(i); - } - - var blob = new Blob([array], { type : 'image/png' }); - blob.name = 'clipboard.png'; - - handler.invoke('restoreRange', $editable); - handler.invoke('focus', $editable); - handler.insertImages(layoutInfo, [blob]); - } else { - var pasteContent = $('
').html($paste.html()).html(); - handler.invoke('restoreRange', $editable); - handler.invoke('focus', $editable); - - if (pasteContent) { - handler.invoke('pasteHTML', $editable, pasteContent); - } - } - - $paste.empty(); - }; - - /** - * paste by clipboard event - * - * @param {Event} event - */ - var pasteByEvent = function (event) { - var clipboardData = event.originalEvent.clipboardData; - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); - var $editable = layoutInfo.editable(); - - if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) { - return; - } - - if (clipboardData && clipboardData.items && clipboardData.items.length) { - var item = list.head(clipboardData.items); - if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { - handler.insertImages(layoutInfo, [item.getAsFile()]); - } - handler.invoke('editor.afterCommand', $editable); - } - - event.preventDefault(); - - var html = clipboardData.getData("text/html"); - var $node = $('
').html(html); - // if copying source did not provide html, default to plain text - if(!html) { - $node.text(clipboardData.getData("text/plain")).html(function(_, html){ - return html.replace(/\r?\n/g,'
'); - }); - } - pasteContent($node, layoutInfo, $editable); - }; - - /* - remove undesirable tag - filter classes and style attributes - remove undesirable attributes - */ - var filter_tag = function ($nodes, $editable) { - return $nodes.each(function() { - var $node = $(this); - - if ($node.attr('style')) { - var style = _.filter(_.compact($node.attr('style').split(/\s*;\s*/)), function (style) { - style = style.split(/\s*:\s*/); - return /width|height|color|background-color|font-weight|text-align|font-style|text-decoration/i.test(style[0]) && - !(style[1] === 'initial' || style[1] === 'inherit' || $node.css(style[0]) === $editable.css(style[0]) || - (style[0] === 'background-color' && style[1] === 'rgb(255, 255, 255)') || - (style[0] === 'color' && style[1] === 'rgb(0, 0, 0)')); - }).join(';'); - if (style.length) { - $node.attr('style', style); - } else { - $node.removeAttr('style'); - } - } - - if ($node.attr('class')) { - var classes = _.filter($node.attr('class').split(/\s+/), function (style) { - return /(^|\s)(fa|pull|text|bg)(\s|-|$)/.test(style); - }).join(' '); - if (classes.length) { - $node.attr('class', classes); - } else { - $node.removeAttr('class'); - } - } - }); - }; - - var pasteContent = function ($node, layoutInfo, $editable) { - $node.find('meta, script, style').remove(); - filter_tag($node.find('*'), $editable).removeAttr('title', 'alt', 'id', 'contenteditable'); - - /* - remove unless span and unwant font - */ - $node.find('span, font').filter(':not([class]):not([style])').each(function () { - $(this).replaceWith($(this).contents()); - }); - $node.find('span + span').each(function () { - - if (dom.isText(this.previousSibling)) { - if (dom.isVisibleText(this.previousSibling)) { - return; - } else { // keep space between 2 tags, but can merge the both tags - $(this).prev().append(this.previousSibling); - } - } - if ($(this).attr('class') === $(this).prev().attr('class') && $(this).attr('style') === $(this).prev().attr('style')) { - $(this).prev().append($(this).contents()); - $(this).remove(); - } - }); - - // remove empty table row and td - var $tdr; - while(($tdr = $node.find('tr:empty, td:empty, th:empty, tbody:empty, t-head:empty, table:empty')) && $tdr.length) { - $tdr.remove(); - } - - /* - reset architecture HTML node and add

tag - */ - var $arch = $('

'); - var $last = $arch; - $node.contents().each(function () { - if (dom.isBR(this)) { - $(this).remove(); - $last = $('

'); - $arch.append($last); - } else if (/h[0-9]+|li|table|p/i.test(this.tagName)) { - $last = $('

'); - $arch.append(this).append($last); - } else if ($arch.is(':empty') && dom.isText(this)) { - $last = $('

').append(this); - $arch.append($last); - } else if (this.nodeType !== Node.COMMENT_NODE) { - $last.append(this); - } - }); - $arch.find(':not([class]):not([style]):empty, p:empty').remove(); - - /* - history - */ - $editable.data('NoteHistory').recordUndo($editable, "paste"); - - /* - remove selected content - */ - var r = range.create(); - if (!r.isCollapsed()) { - r = r.deleteContents(); - r.select(); - } - - // If only pasting a

element in an unique

element, only paste - // the

element text - var $p = $arch.children('p'); - var onlyAP = ($p.length === 1 && $arch.children().length === 1); - if (onlyAP) { - var $p1 = $(r.sc).closest('p'); - var $p2 = $(r.ec).closest('p'); - if ($p1.length && $p2.length && $p1[0] === $p2[0]) { - $arch.html($p.text()); - } - } - - /* - insert content - */ - var $nodes = $(); - $editable.on('DOMNodeInserted', function (event) { - $nodes = $nodes.add(event.originalEvent.target); - }); - window.document.execCommand('insertHTML', false, $arch.html()); - $editable.off('DOMNodeInserted'); - - /* - clean insterted content - */ - var $span = $nodes.filter('span'); - $span = $span.first().add($span.last()); - $span = $span.add($span.prev('span')); - $span = $span.add($span.next('span')); - filter_tag($span, $editable); - $span.not('[span], [style]').each(function () { - _.each(this.childNodes, function (node) { - $(node.parentNode).after(node); - }); - $(this).remove(); - }); - r = range.create(); - if (!dom.isText(r.ec)) { - r = range.create(r.sc.childNodes[r.so], dom.nodeLength(r.sc.childNodes[r.so])); - } - r.clean().select(); - - $editable.trigger('content_changed'); - }; - }; - - return Clipboard; + return Clipboard; }); diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index b160e6a7e97..964ececa06b 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -194,8 +194,6 @@ var FieldTextHtml = widget.extend({ }, start: function () { var self = this; - // Do not Forward port in >= 11.0 - this.$formButtons = this.getParent().$buttons; this.callback = _.uniqueId('FieldTextHtml_'); window.odoo[this.callback+"_editor"] = function (EditorBar) { setTimeout(function () { @@ -293,13 +291,32 @@ var FieldTextHtml = widget.extend({ src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord)); return src; }, + _toggleFormButtons: function(enable) { + if (this.$formButtons) { + if (enable) { + this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false); + } else { + this.$formButtons.find('button').addClass('o_disabled').attr('disabled', true); + } + } + }, initialize_content: function () { var self = this; + + // Do not Forward port in >= 11.0 + function getModalButtons() { + var $modal = self.getParent().getParent(); + if ($modal && $modal.$footer) { + return $modal.$footer; + } + } + + this.$formButtons = this.getParent().$buttons || getModalButtons(); this.$el.closest('.modal-body').css('max-height', 'none'); this.$iframe = this.$el.find('iframe'); // deactivate any button to avoid saving a not ready iframe // Do not Forward port in >= 11.0 - this.$formButtons.find('button').addClass('o_disabled').attr('disabled', true); + this._toggleFormButtons(false); this.document = null; this.$body = $(); this.$content = $(); @@ -327,7 +344,7 @@ var FieldTextHtml = widget.extend({ this.render_value(); // reactivate all the buttons when the field's content (the iframe) is loaded // Do not Forward port in >= 11.0 - this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false); + this._toggleFormButtons(true); setTimeout(function () { self.add_button(); setTimeout(self.resize,0); @@ -426,7 +443,7 @@ var FieldTextHtml = widget.extend({ }, destroy: function () { // Do not Forward port in >= 11.0 - this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false); + this._toggleFormButtons(true); $(window).off('resize', this.resize); delete window.odoo[this.callback+"_editor"]; delete window.odoo[this.callback+"_content"]; diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 2f60b1333d4..6ba5ac6eeaa 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -676,7 +676,7 @@ function summernote_mousedown (event) { return; } var editables = $(".o_editable[contenteditable], .note-editable[contenteditable]"); - var r_editable = editables.has((r||{}).sc); + var r_editable = editables.has((r||{}).sc).addBack(editables.filter((r||{}).sc)); if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) { var saved_editable = editables.has((remember_selection||{}).sc); if($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) { diff --git a/odoo/addons/base/res/res_currency.py b/odoo/addons/base/res/res_currency.py index 538db04f455..0ee76e0567c 100644 --- a/odoo/addons/base/res/res_currency.py +++ b/odoo/addons/base/res/res_currency.py @@ -34,6 +34,7 @@ class Currency(models.Model): ] @api.multi + @api.depends('rate_ids.rate') def _compute_current_rate(self): date = self._context.get('date') or fields.Datetime.now() company_id = self._context.get('company_id') or self.env['res.users']._get_company().id