From 15f6170bed1cf64537339d698fb78b21b98baeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 29 Aug 2019 12:39:00 +0000 Subject: [PATCH 01/14] [FIX] bus: background slave tabs should not loose notifications Before this commit, some notifications may be lost while forwarding them between browser tabs by means of the local storage (i.e. from the master tab to slave tabs). This happened when some slave tabs are in the background and are throttled by the browser: the master tab writes twice in the local storage (to notify slave tabs of new notifications), and slave tabs drops the 1st notification. This is caused by the mismatch between the new value in the local storage event and the actual value in the local storage. For example: ``` t(0) - Master tab receives `message` notification; t(1) - Master tab writes notifications (= `message`) in local storage; t(2) - Master tab receives `message_seen` notification; t(3) - Master tab writes notifications (= `message_seen`) in local storage; t(4) - Slave tab detects new notifications (= `message`) from local storage event, but drops it since it does not match actual local storage value (= `message_seen`); t(5) - Slave tab detects new notifications (= `message_seen`) from local storage event, and handles it since it matches actual local storage value (= `message_seen`); => Notifications received + handled: - master tab: `message`, `message_seen` - slave tab: `message_seen` => Slave tab lost "message" notification. ``` This commit fixes the issue by letting all tabs handle any local storage events, regardless of the actual value in the local storage. In the example above, this means the slave tab will not drop the notification `message` at t(4). Task-ID 2044231 Closes #36177 --- addons/web/static/src/js/core/local_storage.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/core/local_storage.js b/addons/web/static/src/js/core/local_storage.js index 0ae98498b31..ecc0cb7b78f 100644 --- a/addons/web/static/src/js/core/local_storage.js +++ b/addons/web/static/src/js/core/local_storage.js @@ -35,12 +35,10 @@ try { var newValue = e.originalEvent.newValue; try { JSON.parse(newValue); - if (localStorage.getItem(key) === newValue) { - storage.trigger('storage', { - key: key, - newValue: newValue, - }); - } + storage.trigger('storage', { + key: key, + newValue: newValue, + }); } catch (error) {} }); return storage; From 7bdcef9c1c4d42f39302403e1c5e56682788022c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 29 Aug 2019 16:27:47 +0000 Subject: [PATCH 02/14] [FIX] web: add support for `localStorage.key()` and `.length` These operations on local storage are necessary in order to loop on all local storage keys. Closes #36177 --- .../static/src/js/core/abstract_storage_service.js | 13 +++++++++++++ addons/web/static/src/js/core/ram_storage.js | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js index ef828b5dd5c..5d724f87911 100644 --- a/addons/web/static/src/js/core/abstract_storage_service.js +++ b/addons/web/static/src/js/core/abstract_storage_service.js @@ -44,6 +44,19 @@ var AbstractStorageService = AbstractService.extend({ var val = this.storage.getItem(key); return val ? JSON.parse(val) : defaultValue; }, + /** + * @param {integer} index + * @return {string} + */ + key: function (index) { + return this.storage.key(index); + }, + /** + * @return {integer} + */ + length: function () { + return this.storage.length; + }, /** * Removes the given key from the storage * diff --git a/addons/web/static/src/js/core/ram_storage.js b/addons/web/static/src/js/core/ram_storage.js index 9bf0fc29f93..abad86c4538 100644 --- a/addons/web/static/src/js/core/ram_storage.js +++ b/addons/web/static/src/js/core/ram_storage.js @@ -31,6 +31,7 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, { */ clear: function () { this.storage = Object.create(null); + this.length = 0; }, /** * Returns the value associated with a given key in the storage @@ -41,12 +42,22 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, { getItem: function (key) { return this.storage[key]; }, + /** + * @param {integer} index + * @return {string} + */ + key: function (index) { + return _.keys(this.storage)[index]; + }, /** * Removes the given key from the storage * * @param {string} key */ removeItem: function (key) { + if (key in this.storage) { + this.length--; + } delete this.storage[key]; this.trigger('storage', {key: key, newValue: null}); }, @@ -58,6 +69,9 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, { * @param {string} value */ setItem: function (key, value) { + if (!(key in this.storage)) { + this.length++; + } this.storage[key] = value; this.trigger('storage', {key: key, newValue: value}); }, From 40b57ca824e967a9221f7834e4c9b3db7a4849d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 29 Aug 2019 16:28:07 +0000 Subject: [PATCH 03/14] [FIX] bus, mail: correctly synchronize document chat windows Before this commit, there could be an infinite loop when changing the chat window state of a document chat window. This is caused when mutiple tabs overwrite the chat window state with a different value everytime, resulting to an infinite loop. Accross multiple tabs, `setItem` and `onStorage` are not synchronous. That's why tabs should communicate through local storage events instead of actual value in the local storage. To illustrate the issue, suppose there are 2 tabs (T1 and T2) with one document chat windows (C) and the following user interactions: - T1 and T2 both have C open - on T1, user folds C - on T2, user folds C Now let's add an example of local storage (LS) logics that could apply from above example: - T1 and T2 both have C open - on T1, user folds C [1] T1 writes 'C folded' in LS - on T2, user folds C [2] T2 writes 'C folded' in LS [3] T2 detects 'C folded' in LS (from [1]) => T2 writes 'C folded' in LS [4] T1 detects 'C folded' in LS (from [2]) => T1 writes 'C folded' in LS [5] T1 detects 'C folded' in LS (from [3]) => T1 writes 'C folded' in LS [6] T2 detects 'C folded' in LS (from [4]) => T2 writes 'C folded' in LS etc... This scenario could have been easily prevented by not writing on local storage when the chat window state has not changed. However, this wouldn't fix this scenario that also introduces an infinite loop: - T1 and T2 both have C open - on T1, user folds C [1] T1 writes 'C folded' in LS - on T2, user folds C [2] T2 writes 'C folded' in LS - on T2, user unfolds C [3] T2 writes 'C unfolded' in LS [4] T2 detects 'C folded' in LS (from [1]) => T2 writes 'C folded' in LS [5] T1 detects 'C unfolded' in LS (from [3]) => T1 writes 'C unfolded' in LS [6] T2 detects 'C unfolded' in LS (from [5]) => T2 writes 'C unfolded' in LS [7] T1 detects 'C folded' in LS (from [4]) => T1 writes 'C folded' in LS [8] T2 detects 'C folded' in LS (from [7]) => T2 writes 'C folded' in LS [9] T1 detects 'C unfolded' in LS (from [6]) => T1 writes 'C folded' in LS etc... This commit fixes the infinite loop issue: - by turning local storage read during cross-tab communication into local storage event read instead. This should prevent race conditions based on read/write operations in local, since their order is not guaranteed accros tabs. - by isolating document chat window state to their own local storage entry. This should prevent a tab from notifying a chat window state that it didn't change, which may also result in an infinite loop. Some tests were adapted from the changes above. Also, some tests did pass thanks to local storage being mocked by a ram storage: the handler `_onStorage` should have been called, but it didn't because it was registered on the actual local storage instead of the ram storage. In order to make tests pass again, the following additional changes were required: - Cross-tab bus service now exports the unique tab ID with `getTabId`. - document thread entries in local storage now track the tab ID, so that self-tab ignore their own local storage writes. Task-Id 2034997 Closes #36177 --- addons/bus/static/src/js/crosstab_bus.js | 6 ++ .../src/js/models/threads/document_thread.js | 27 ++++++- .../services/mail_document_thread_manager.js | 73 +++++++++++++------ .../src/js/services/mail_window_manager.js | 6 +- .../tests/document_thread_window_tests.js | 20 +++-- 5 files changed, 99 insertions(+), 33 deletions(-) diff --git a/addons/bus/static/src/js/crosstab_bus.js b/addons/bus/static/src/js/crosstab_bus.js index 46643cad0de..9d0e413ab94 100644 --- a/addons/bus/static/src/js/crosstab_bus.js +++ b/addons/bus/static/src/js/crosstab_bus.js @@ -79,6 +79,12 @@ var CrossTabBus = Longpolling.extend({ this._super.apply(this, arguments); this._callLocalStorage('setItem', 'channels', this._channels); }, + /** + * @return {string} + */ + getTabId: function () { + return this._id; + }, /** * Tells whether this bus is related to the master tab. * diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js index 18f1e4fb762..3d91b786cef 100644 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ b/addons/mail/static/src/js/models/threads/document_thread.js @@ -62,9 +62,16 @@ var DocumentThread = Thread.extend({ * shared between tabs, and restored on F5. * * @override + * @param {Object} [options] + * @param {boolean} [options.skipCrossTabSync=false] if set, it should + * not notify other tabs from updating document thread state. This happens + * in case the `close` operation comes from local storage event. */ - close: function () { + close: function (options) { this._super.apply(this, arguments); + if (options && options.skipCrossTabSync) { + return; + } this.call('mail_service', 'updateDocumentThreadState', this._id, { name: this.getName(), windowState: 'closed', @@ -75,9 +82,16 @@ var DocumentThread = Thread.extend({ * shared between tabs, and restored on F5. * * @override + * @param {Object} [options] + * @param {boolean} [options.skipCrossTabSync=false] if set, it should + * not notify other tabs from updating document thread state. This happens + * in case the `detach` operation comes from local storage event. */ - detach: function () { + detach: function (options) { this._super.apply(this, arguments); + if (options && options.skipCrossTabSync) { + return; + } var windowState = this._folded ? 'folded' : 'open'; this.call('mail_service', 'updateDocumentThreadState', this._id, { name: this.getName(), @@ -105,9 +119,16 @@ var DocumentThread = Thread.extend({ * shared between tabs, and restored on F5. * * @override + * @param {Object} [options] + * @param {boolean} [options.skipCrossTabSync=false] if set, it should + * not notify other tabs from updating document thread state. This happens + * in case the `detach` operation comes from local storage event. */ - fold: function () { + fold: function (folded, options) { this._super.apply(this, arguments); + if (options && options.skipCrossTabSync) { + return; + } var windowState = this._folded ? 'folded' : 'open'; this.call('mail_service', 'updateDocumentThreadState', this._id, { name: this.getName(), diff --git a/addons/mail/static/src/js/services/mail_document_thread_manager.js b/addons/mail/static/src/js/services/mail_document_thread_manager.js index 000a1c15fa5..1af3fc337a3 100644 --- a/addons/mail/static/src/js/services/mail_document_thread_manager.js +++ b/addons/mail/static/src/js/services/mail_document_thread_manager.js @@ -21,17 +21,23 @@ MailManager.include({ start: function () { this._super.apply(this, arguments); - + this._documentThreadWindowStates = {}; + this._tabId = this.call('bus_service', 'getTabId'); // retrieve the open DocumentThreads from the localStorage - var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY); - if (!state) { - this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {}); - } else { - this.isReady().then(this._updateDocumentThreadWindows.bind(this, state)); + var localStorageLength = this.call('local_storage', 'length'); + for (var i = 0; i < localStorageLength; i++) { + var key = this.call('local_storage', 'key', i); + if (key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) { + // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId + var documentThreadId = key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1); + this._documentThreadWindowStates[documentThreadId] = + this.call('local_storage', 'getItem', key); + } } + this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates)); // listen to localStorage changes to synchronize DocumentThread's // windows between tabs - window.addEventListener('storage', this._onStorage.bind(this)); + this.call('local_storage', 'onStorage', this, this._onStorage); }, //-------------------------------------------------------------------------- @@ -60,7 +66,10 @@ MailManager.include({ options.postedFromDocumentThread ) { var key = this.DOCUMENT_THREAD_MESSAGE_KEY; - this.call('local_storage', 'setItem', key, data); + this.call('local_storage', 'setItem', key, { + messageData: data, + tabId: this._tabId, + }); } return this._super.apply(this, arguments); }, @@ -144,12 +153,11 @@ MailManager.include({ * @param {string} state.windowState ('closed', 'folded' or 'open') */ updateDocumentThreadState: function (threadID, state) { - var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY); - states = _.omit(states, function (state) { - return state.windowState === 'closed'; + this._documentThreadWindowStates[threadID] = state; + this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY + '/' + threadID, { + state: state, + tabId: this._tabId, }); - states[threadID] = state; - this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states); }, //-------------------------------------------------------------------------- @@ -194,10 +202,17 @@ MailManager.include({ resModel: info[0], }); if (state.windowState === 'closed') { - documentThread.close(); + documentThread.close({ + skipCrossTabSync: true, + }); } else { - documentThread.fold(state.windowState === 'folded'); - self.openThreadWindow(documentThread.getID(), { keepFoldState: true }); + documentThread.fold(state.windowState === 'folded', { + skipCrossTabSync: true, + }); + self.openThreadWindow(documentThread.getID(), { + keepFoldState: true, + skipCrossTabSync: true, + }); } }); }, @@ -213,13 +228,27 @@ MailManager.include({ * @param {StorageEvent} ev */ _onStorage: function (ev) { - if (ev.key === this.DOCUMENT_THREAD_STATE_KEY) { - var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY); - this._updateDocumentThreadWindows(state); + var value; + try { + value = JSON.parse(ev.newValue); + } catch (err) { + return; + } + if (ev.key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) { + if (value.tabId === this._tabId) { + return; + } + // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId + var documentThreadId = ev.key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1); + var param = {}; + param[documentThreadId] = value.state; + this._updateDocumentThreadWindows(param); } else if (ev.key === this.DOCUMENT_THREAD_MESSAGE_KEY) { - var message = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_MESSAGE_KEY); - if (message) { - this.addMessage(message); + if (value.tabId === this._tabId) { + return; + } + if (value.messageData) { + this.addMessage(value.messageData); } } }, diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index ebb6a0ff2e7..c5b9f2566c5 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -94,6 +94,8 @@ MailManager.include({ * is not open yet, and do nothing otherwise. * @param {boolean} [options.keepFoldState=false] if set to true, keep the * fold state of the thread + * @param {boolean} [options.skipCrossTabSync=false] if set, thread + * should not notify other tabs from new document thread chat window state. */ openThreadWindow: function (threadID, options) { var self = this; @@ -130,7 +132,9 @@ MailManager.include({ // thread window could not be open, which may happen due to // access error while fetching messages to the document. // abort opening the thread window in this case. - thread.close(); + thread.close({ + skipCrossTabSync: true, + }); }).always(function () { thread.isCreatingWindow = false; }); diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js index bdab46e91ef..fe2b31b2811 100644 --- a/addons/mail/static/tests/document_thread_window_tests.js +++ b/addons/mail/static/tests/document_thread_window_tests.js @@ -201,7 +201,7 @@ QUnit.test('post messages in a document thread window', function (assert) { testUtils.intercept(messagingMenu, 'call_service', function (ev) { if (ev.data.service === 'local_storage' && ev.data.method === 'setItem' && ev.data.args[0] === 'mail.document_threads_last_message') { - assert.deepEqual(ev.data.args[1], newMessage, + assert.deepEqual(ev.data.args[1].messageData, newMessage, "should write sent message in local storage, to share info with other tabs"); } }, true); @@ -229,7 +229,7 @@ QUnit.test('post messages in a document thread window', function (assert) { }); QUnit.test('open, fold, unfold and close a document thread window', function (assert) { - assert.expect(8); + assert.expect(24); var messagingMenu = new MessagingMenu(); testUtils.addMockEnvironment(messagingMenu, { @@ -239,7 +239,13 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as }); testUtils.intercept(messagingMenu, 'call_service', function (ev) { if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') { - assert.step(ev.data.args); + const key = ev.data.args[0]; + const state = ev.data.args[1].state; + assert.strictEqual(key, 'mail.document_threads_state/some.res.model_1'); + assert.ok(state); + assert.ok(state.name); + assert.ok(state.windowState); + assert.step(`${key}: { name: "${state.name}", windowState: '${state.windowState}' }`); } }, true); messagingMenu.appendTo($('#qunit-fixture')); @@ -262,10 +268,10 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as $('.o_thread_window .o_thread_window_close').click(); assert.verifySteps([ - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "folded"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "closed"}}], + `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`, + `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'folded' }`, + `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`, + `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'closed' }`, ]); messagingMenu.destroy(); From fbbfa6ddcaf5cb8ef75672a73d1a928704484850 Mon Sep 17 00:00:00 2001 From: Harpritsinh Sisodiya Date: Wed, 28 Aug 2019 11:47:37 +0000 Subject: [PATCH 04/14] [FIX] web: datepicker: crash on invalid date There is an open issue on the tempusdominus lib: https://github.com/tempusdominus/bootstrap-4/issues/223 It occurs when there is a valid value in a date(time) field, and the user unsets it by setting an invalid one. This rev. fixes the issue in Odoo by preventing to reach the buggy piece of code in the lib. In a few words, when the user sets an invalid date, we reset the previous valid date (which is what the lib tries to do anyway). Task 2057009 closes odoo/odoo#36211 Signed-off-by: Aaron Bohy (aab) Co-authored-by: Aaron Bohy --- .../web/static/src/js/widgets/date_picker.js | 5 ++- .../static/tests/fields/basic_fields_tests.js | 39 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index df9601b3a04..90e77aafb41 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -84,8 +84,8 @@ var DateWidget = Widget.extend({ * set datetime value */ changeDatetime: function () { + var oldValue = this.getValue(); if (this.isValid()) { - var oldValue = this.getValue(); this._setValueFromUi(); var newValue = this.getValue(); var hasChanged = !oldValue !== !newValue; @@ -102,6 +102,9 @@ var DateWidget = Widget.extend({ } this.trigger("datetime_changed"); } + } else { + var formattedValue = oldValue ? this._formatClient(oldValue) : null; + this.$input.val(formattedValue); } }, /** diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7fae0f7963a..84a5844b8c0 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2696,8 +2696,32 @@ QUnit.module('basic_fields', { form.destroy(); }); - QUnit.test('date field should remove the date if the date is not valid', function (assert) { - assert.expect(1); + QUnit.test('date field: set an invalid date when the field is already set', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + var $input = form.$('.o_field_widget[name=date] input'); + + assert.strictEqual($input.val(), "02/03/2017"); + + $input.val('mmmh').trigger('change'); + assert.strictEqual($input.val(), "02/03/2017", "should have reset the original value"); + + form.destroy(); + }); + + QUnit.test('date field: set an invalid date when the field is not set yet', function (assert) { + assert.expect(2); var form = createView({ View: FormView, @@ -2705,13 +2729,18 @@ QUnit.module('basic_fields', { data: this.data, arch: '
', res_id: 4, + viewOptions: { + mode: 'edit', + }, }); - // switch to edit mode - form.$buttons.find('.o_form_button_edit').click(); - // set an invalid date + var $input = form.$('.o_field_widget[name=date] input'); + + assert.strictEqual($input.text(), ""); + $input.val('mmmh').trigger('change'); assert.strictEqual($input.text(), "", "The date field should be empty"); + form.destroy(); }); From 75460e697a20505aa22b3a273d3056e0bdc75633 Mon Sep 17 00:00:00 2001 From: Nans Lefebvre Date: Wed, 14 Aug 2019 13:56:31 +0000 Subject: [PATCH 05/14] [FIX] mrp: block partial post inventory of MO with workorders and tracking Enable work orders, routings, and track components by lot numbers or serial. Create an MO for a certain X quantity of finished products and plan work orders. Only complete a partial amount Y of the overall X amount. Specify the lots for the consumed tracked materials equal to the Y amount. Then, return to the MO and click "Post Inventory". An error is raised because the lot is missing. This is because when processing workorders temporary lines are created without lot/serial for tracked component products. This is a limitation that require a task to be removed; in the meantime we remove the button, in true bugfix fashion. opw 2049806 closes odoo/odoo#35731 Signed-off-by: Nans Lefebvre (len) --- addons/mrp/models/mrp_production.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 693cfca5b16..608742a4103 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -277,6 +277,7 @@ class MrpProduction(models.Model): order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids) else: order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids) + order.post_visible &= all(wo.state in ['done', 'cancel'] for wo in order.workorder_ids) or all(m.product_id.tracking == 'none' for m in order.move_raw_ids) @api.multi @api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty') From 0159d71ec16de2ce40ac84bb16e35a19c857dcea Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 27 Aug 2019 11:20:07 +0000 Subject: [PATCH 06/14] [FIX] website_blog: encode URL for social sharing Like in other places where we have social sharing buttons, when we pass a URL or title as a parameter it needs to be properly encoded. closes odoo/odoo#36197 Signed-off-by: Quentin Smetz (qsm) --- .../static/src/js/website_blog.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js index 7985e0ebecc..d0f93a7a473 100644 --- a/addons/website_blog/static/src/js/website_blog.js +++ b/addons/website_blog/static/src/js/website_blog.js @@ -27,30 +27,33 @@ function animate(event) { function shareArticle(event){ var url = ''; + var articleURL; if ($(this).is('*[class*="_complete"]')) { - var blog_title_complete = $('#blog_post_name').html() || ''; + var blog_title_complete = encodeURIComponent($('#blog_post_name').html() || ''); + articleURL = encodeURIComponent(window.location.href); if ($(this).hasClass('o_twitter_complete')){ - url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : '+blog_title_complete+"! Check it live: "+window.location.href; + url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blog_title_complete + "! Check it live: " + articleURL; } else if ($(this).hasClass('o_facebook_complete')){ - url = 'https://www.facebook.com/sharer/sharer.php?u='+window.location.href; + url = 'https://www.facebook.com/sharer/sharer.php?u=' + articleURL; } else if ($(this).hasClass('o_linkedin_complete')){ - url = 'https://www.linkedin.com/shareArticle?mini=true&url='+window.location.href+'&title='+blog_title_complete; + url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + articleURL + '&title=' + blog_title_complete; } else { - url = 'https://plus.google.com/share?url='+window.location.href; + url = 'https://plus.google.com/share?url=' + articleURL; } } else { var blog_post = $(this).parents("[name='blog_post']"); - var blog_post_title = blog_post.find('.o_blog_post_title').html() || ''; + var blog_post_title = encodeURIComponent(blog_post.find('.o_blog_post_title').html() || ''); var blog_article_link = blog_post.find('.o_blog_post_title').parent('a').attr('href'); + articleURL = encodeURIComponent(window.location.host + blog_article_link); if ($(this).hasClass('o_twitter')) { - url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : '+blog_post_title+"! "+window.location.host+blog_article_link; + url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blog_post_title + "! " + articleURL; } else if ($(this).hasClass('o_facebook')){ - url = 'https://www.facebook.com/sharer/sharer.php?u='+window.location.host+blog_article_link; + url = 'https://www.facebook.com/sharer/sharer.php?u=' + articleURL; } else if ($(this).hasClass('o_linkedin')){ - url = 'https://www.linkedin.com/shareArticle?mini=true&url='+window.location.host+blog_article_link+'&title='+blog_post_title; + url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + articleURL + '&title=' + blog_post_title; } else if ($(this).hasClass('o_google')){ - url = 'https://plus.google.com/share?url='+window.location.host+blog_article_link; + url = 'https://plus.google.com/share?url=' + articleURL; } } window.open(url, "", "menubar=no, width=500, height=400"); From 923fb7aa05f0e9e0e67440073993ec36bb0f5f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Tue, 3 Sep 2019 10:55:30 +0000 Subject: [PATCH 07/14] [FIX] im_livechat: no duplicated messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, when using website livechat with multiple tabs, it may duplicate messages in the chat window. This happens due to registering messages twice: - once, from `/mail/chat_history` - second, from receiving notification from master tab, via local storage. This commit fixes the issue by not registering messages that have already been registered (i.e. unique ID). opw-2052960 closes odoo/odoo#36370 Signed-off-by: Alexandre Kühn (aku) --- addons/im_livechat/static/src/js/im_livechat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 7de6629f24e..f357e987e57 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -232,7 +232,12 @@ var LivechatButton = Widget.extend({ is_note: data.is_note, customer_email_data: [] }; - + var hasAlreadyMessage = _.some(this.messages, function (message) { + return message.id === msg.id; + }); + if (hasAlreadyMessage) { + return; + } // Compute displayed author name or email msg.displayed_author = msg.author_id && msg.author_id[1] || this.options.default_username; From b7e3469931f69091e47b4c6c2ce8a83669fa7313 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 3 Sep 2019 13:30:04 +0000 Subject: [PATCH 08/14] [FIX] web: grouped list as grouped list as child In grouped editable list, get rid of the groups Edit one record in the editable list In a M2O, click on search more In search's list, group by a field Expand a group in the search's list Before this commit, it crashes because the event was propagated onto the first main list After this commit, it doesn't crash and the group is expanded OPW 2064156 closes odoo/odoo#36383 Signed-off-by: Lucas Perais (lpe) --- .../src/js/views/list/list_controller.js | 1 + addons/web/static/tests/views/list_tests.js | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 524bd2034db..9fb765f8ae2 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -579,6 +579,7 @@ var ListController = BasicController.extend({ * @param {OdooEvent} event */ _onToggleGroup: function (event) { + event.stopPropagation(); this.model .toggleGroup(event.data.group.id) .then(this.update.bind(this, {}, {keepSelection: true, reload: false})); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 223aea85a93..031e691388b 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3268,6 +3268,57 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('grouped list with another grouped list parent, click unfold', function (assert) { + assert.expect(3); + this.data.bar.fields = { + cornichon: {string: 'cornichon', type: 'char'}, + }; + + var rec = this.data.bar.records[0]; + // create records to have the search more button + var newRecs = []; + for (var i=0; i<8; i++) { + var newRec = _.extend({}, rec); + newRec.id = 10 + i; + newRec.cornichon = 'extra fin'; + newRecs.push(newRec) + } + this.data.bar.records = newRecs; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + groupBy: ['bar'], + archs: { + 'bar,false,list': '', + 'bar,false,search': '', + }, + }); + + list.update({groupBy: []}); + + testUtilsDom.click(list.$('.o_data_cell:eq(0)')); + + testUtilsDom.click(list.$('.o_selected_row .o_data_cell .o_field_many2one input')); + $('.ui-autocomplete .ui-menu-item:contains("Search More")').mouseenter().click(); + + assert.containsOnce($('body'), '.modal-content'); + + assert.containsNone($('body'), '.modal-content .o_group_name', 'list in modal not grouped'); + + testUtilsDom.click($('body .modal-content button:contains(Group By)')); + + testUtilsDom.click($('body .modal-content .o_menu_item a:contains(cornichon)')); + + testUtilsDom.click($('body .modal-content .o_group_header')); + + assert.containsOnce($('body'), '.modal-content .o_group_open'); + + list.destroy(); + }); + QUnit.test('field values are escaped', function (assert) { assert.expect(1); var value = ''; From 0b146aa253bdd8f5f420511253c644e64e850543 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 4 Sep 2019 06:41:04 +0000 Subject: [PATCH 09/14] [FIX] web: form: race condition when editing in readonly ... and switching to 'edit' mode before the write RPC returns. For instance, on a project task form view, in readonly, change the state by clicking on the bullet, and directly switch to 'edit' mode: when the RPC returns, the view is switched back in readonly. Task 2065443 closes odoo/odoo#36400 Signed-off-by: VincentSchippefilt --- .../src/js/views/form/form_controller.js | 4 +- addons/web/static/tests/views/form_tests.js | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index e3085b1379d..aa8d86e6cc2 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -561,7 +561,9 @@ var FormController = BasicController.extend({ * @private */ _onEdit: function () { - this._setMode('edit'); + // wait for potential pending changes to be saved (done with widgets + // allowing to edit in readonly) + this.mutex.getUnlockedDef().then(this._setMode.bind(this, 'edit')); }, /** * This method is called when someone tries to freeze the order, most likely diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index f37df9baf45..9dea12481cf 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -7755,6 +7755,46 @@ QUnit.module('Views', { actionManager.destroy(); }); + QUnit.test('edit a record in readonly and switch to edit before it is actually saved', async function (assert) { + assert.expect(3); + + const prom = $.Deferred(); // testUtils.makeTestPromise(); + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `
+ + + `, + mockRPC: function (route, args) { + const result = this._super.apply(this, arguments); + if (args.method === 'write') { // delay the write RPC + assert.deepEqual(args.args[1], {bar: false}); + return prom.then(_.constant(result)); + } + return result; + }, + res_id: 1, + }); + + // edit the record (in readonly) with toogle_button widget (and delay the write RPC) + await testUtils.dom.click(form.$('.o_field_widget[name=bar]')); + + // switch to edit mode + await testUtils.form.clickEdit(form); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); // should wait for the RPC to return + + // make write RPC return + prom.resolve(); + // await testUtils.nextTick(); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + + form.destroy(); + }); + QUnit.module('FormViewTABMainButtons'); QUnit.test('using tab in an empty required string field should not move to the next field',function(assert) { From 450269b26805b21428b16bd6deab30d4a8d9dd16 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 3 Sep 2019 12:33:11 +0000 Subject: [PATCH 10/14] [FIX] account: Wrong amount due when reconciling with refund MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce the bug: - Let's consider the following currency rates: 1.00 for USD and 0.50 for EUR - Create invoice I with the same currency of your company (USD) and amount due = 100$ - Validate I - Create refund R with a diffenrent currency of your company (EUR) and amount due = 1€ - Validate R - Reconcile R with I Bug: The amount due of I was 196$ instead of 98$ PS: The root of this issue was coming from function assign_outstanding_credit that was called when reconciling R with I. This function changes the currency_id and the amount_currency of the account move line. So the function _amount_residual defined on model account.move.line was triggered as the currency_id was changed. In _amount_residual, the residual_amount_currency was computed. opw:2062103 closes odoo/odoo#36379 Signed-off-by: Simon Goffin (sig) --- addons/account/models/account_invoice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ec7cca76b2b..5c71e8263d1 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -127,8 +127,10 @@ class AccountInvoice(models.Model): if line.currency_id == self.currency_id: residual += line.amount_residual_currency if line.currency_id else line.amount_residual else: - from_currency = line.currency_id or line.company_id.currency_id - residual += from_currency._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) + if line.currency_id: + residual += line.currency_id._convert(line.amount_residual_currency, self.currency_id, line.company_id, line.date or fields.Date.today()) + else: + residual += line.company_id.currency_id._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) self.residual_company_signed = abs(residual_company_signed) * sign self.residual_signed = abs(residual) * sign self.residual = abs(residual) From 116dda06a767598dfdf1147e0e6baee105f5c860 Mon Sep 17 00:00:00 2001 From: Nans Lefebvre Date: Wed, 4 Sep 2019 06:19:33 +0000 Subject: [PATCH 11/14] [FIX] account: do not sent user as vendor when sending bill by email On purchase journals, there is the possibility to set up an alias to automatically create vendor bills from incoming email. Three types of emails can send the bill: - the vendor, with email matching a partner in database - an internal user, possibly forwarding the bill he received from the vendor - an unknown address In the third case, the address will be ignored. In the first, we found the vendor, so he can be set as partner_id of the bill. The tricky one is the second case, where we try to guess the vendor. Before, the code would take only the first email found in the body; if it found a matching partner, then it used that partner anyway, even if that partner belonged to a user and was skipped before. After the fix, we get the partners for all emails and take arbitrarily the first vendor, then backup on the first non-user. Note also that email_addresses would duplicate every address read in the body; we make a set to avoid duplicate processing. opw 1885070 closes odoo/odoo#36399 Signed-off-by: Nans Lefebvre (len) --- addons/account/models/account_invoice.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 5c71e8263d1..509e2393ce1 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -693,17 +693,22 @@ class AccountInvoice(models.Model): email_from = email_escape_char(email_split(email_from)[0]) partner_id = self._search_on_partner(email_from, extra_domain=[('supplier', '=', True)]) + is_internal = lambda p: (p.user_ids and + all(p.user_ids.mapped(lambda u: u.has_group('base.group_user')))) # 2) otherwise, if the email sender is from odoo internal users then it is likely that the vendor sent the bill # by mail to the internal user who, inturn, forwarded that email to the alias to automatically generate the bill # on behalf of the vendor. if not partner_id: user_partner_id = self._search_on_user(email_from) if user_partner_id and user_partner_id in self.env.ref('base.group_user').users.mapped('partner_id').ids: - # In this case, we will look for the vendor's email address in email's body and assume if will come first - email_addresses = email_re.findall(msg_dict.get('body')) + # In this case, we will look for the vendor's email address in email's body + email_addresses = set(email_re.findall(msg_dict.get('body'))) if email_addresses: - partner_ids = [pid for pid in self._find_partner_from_emails([email_addresses[0]], force_create=False) if pid] - partner_id = partner_ids and partner_ids[0] + pids_list = [self._find_partner_from_emails([email], force_create=False) for email in email_addresses] + partner_ids = set(pid for pids in pids_list for pid in pids if pid) + potential_vendors = self.env['res.partner'].browse(partner_ids).filtered(lambda x: not is_internal(x)) + partner_id = ((potential_vendors.filtered('supplier') and potential_vendors.filtered('supplier')[0].id) + or (potential_vendors and potential_vendors[0].id)) # otherwise, there's no fallback on the partner_id found for the regular author of the mail.message as we want # the partner_id to stay empty @@ -727,8 +732,6 @@ class AccountInvoice(models.Model): # Subscribe internal users on the newly created bill partners = self.env['res.partner'].browse(seen_partner_ids) - is_internal = lambda p: (p.user_ids and - all(p.user_ids.mapped(lambda u: u.has_group('base.group_user')))) partners_to_subscribe = partners.filtered(is_internal) if partners_to_subscribe: invoice.message_subscribe([p.id for p in partners_to_subscribe]) From dde5227546099d7eea99a44c6b12661d48c4645a Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 4 Sep 2019 09:37:59 +0000 Subject: [PATCH 12/14] [FIX] l10n_ae: Wrong translation TVA in english is VAT opw:2065413 closes odoo/odoo#36410 Signed-off-by: Simon Goffin (sig) --- .../data/account_tax_template_data.xml | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/addons/l10n_ae/data/account_tax_template_data.xml b/addons/l10n_ae/data/account_tax_template_data.xml index 3e9bdac6df7..edb22792eed 100644 --- a/addons/l10n_ae/data/account_tax_template_data.xml +++ b/addons/l10n_ae/data/account_tax_template_data.xml @@ -57,11 +57,11 @@ taxes - Reverse Charge TVA -5%(sales) + Reverse Charge VAT -5%(sales) taxes - Reverse Charge TVA -5%(purchase) + Reverse Charge VAT -5%(purchase) taxes @@ -71,11 +71,11 @@ - TVA 5% (Dubai) + VAT 5% (Dubai) sale 5 percent - TVA 5% + VAT 5% @@ -83,11 +83,11 @@ - TVA 5% (Abu Dhabi) + VAT 5% (Abu Dhabi) sale 5 percent - TVA 5% + VAT 5% @@ -95,11 +95,11 @@ - TVA 5% (Sharjah) + VAT 5% (Sharjah) sale 5 percent - TVA 5% + VAT 5% @@ -107,11 +107,11 @@ - TVA 5% (Ajman) + VAT 5% (Ajman) sale 5 percent - TVA 5% + VAT 5% @@ -119,11 +119,11 @@ - TVA 5% (Umm Al Quwain) + VAT 5% (Umm Al Quwain) sale 5 percent - TVA 5% + VAT 5% @@ -131,11 +131,11 @@ - TVA 5% (Ras Al-Khaima) + VAT 5% (Ras Al-Khaima) sale 5 percent - TVA 5% + VAT 5% @@ -143,11 +143,11 @@ - TVA 5% (Fujairah) + VAT 5% (Fujairah) sale 5 percent - TVA 5% + VAT 5% @@ -167,11 +167,11 @@ - TVA 0% + VAT 0% sale 0 percent - TVA 0% + VAT 0% @@ -188,11 +188,11 @@ - TVA 5% (Dubai) + VAT 5% (Dubai) none 5 percent - TVA 5% + VAT 5% @@ -200,7 +200,7 @@ - Reverse Charge TVA -5%(Sale) (Dubai) + Reverse Charge VAT -5%(Sale) (Dubai) none -5.0 percent @@ -219,11 +219,11 @@ - TVA 5% (Abu Dhabi) + VAT 5% (Abu Dhabi) none 5 percent - TVA 5% + VAT 5% @@ -231,7 +231,7 @@ - Reverse Charge TVA -5%(Sale) (Abu Dhabi) + Reverse Charge VAT -5%(Sale) (Abu Dhabi) none -5.0 percent @@ -250,11 +250,11 @@ - TVA 5% (Sharjah) + VAT 5% (Sharjah) none 5 percent - TVA 5% + VAT 5% @@ -262,7 +262,7 @@ - Reverse Charge TVA -5%(Sale) (Sharjah) + Reverse Charge VAT -5%(Sale) (Sharjah) none -5.0 percent @@ -281,11 +281,11 @@ - TVA 5% (Ajman) + VAT 5% (Ajman) none 5 percent - TVA 5% + VAT 5% @@ -293,7 +293,7 @@ - Reverse Charge TVA -5%(Sale) (Ajman) + Reverse Charge VAT -5%(Sale) (Ajman) none -5.0 percent @@ -312,11 +312,11 @@ - TVA 5% (Umm Al Quwain) + VAT 5% (Umm Al Quwain) none 5 percent - TVA 5% + VAT 5% @@ -324,7 +324,7 @@ - Reverse Charge TVA -5%(Sale) (Umm Al Quwain) + Reverse Charge VAT -5%(Sale) (Umm Al Quwain) none -5.0 percent @@ -343,11 +343,11 @@ - TVA 5% (Ras Al-Khaima) + VAT 5% (Ras Al-Khaima) none 5 percent - TVA 5% + VAT 5% @@ -355,7 +355,7 @@ - Reverse Charge TVA -5%(Sale) (Ras Al-Khaima) + Reverse Charge VAT -5%(Sale) (Ras Al-Khaima) none -5.0 percent @@ -374,11 +374,11 @@ - TVA 5% (Fujairah) + VAT 5% (Fujairah) none 5 percent - TVA 5% + VAT 5% @@ -386,7 +386,7 @@ - Reverse Charge TVA -5%(Sale) (Fujairah) + Reverse Charge VAT -5%(Sale) (Fujairah) none -5.0 percent @@ -417,11 +417,11 @@ - TVA 5% + VAT 5% purchase 5 percent - TVA 5% + VAT 5% @@ -441,11 +441,11 @@ - TVA 0% + VAT 0% purchase 0 percent - TVA 0% + VAT 0% @@ -462,7 +462,7 @@ - Reverse Charge TVA -5%(Purchase) + Reverse Charge VAT -5%(Purchase) none -5.0 percent @@ -472,11 +472,11 @@ - TVA 5% + VAT 5% none 5 percent - TVA 5% + VAT 5% @@ -484,11 +484,11 @@ - Reverse Charge TVA +5%(Purchase) + Reverse Charge VAT +5%(Purchase) none 5 percent - TVA 5% + VAT 5% From cd1b56b41fb6e5b28d22e33867bea31343475fe8 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 4 Sep 2019 09:58:06 +0000 Subject: [PATCH 13/14] [FIX] account: Wrong amount due when reconciling with refund MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce the bug: - Let's consider the following currency rates: 1.00 for USD and 0.50 for EUR - Create invoice I with the same currency of your company (USD) and amount due = 100$ - Validate I - Create refund R with a diffenrent currency of your company (EUR) and amount due = 1€ - Validate R - Reconcile R with I Bug: The amount due of I was 196$ instead of 98$ PS: The root of this issue was coming from function assign_outstanding_credit that was called when reconciling R with I. This function changes the currency_id and the amount_currency of the account move line. So the function _amount_residual defined on model account.move.line was triggered as the currency_id was changed. In _amount_residual, the residual_amount_currency was computed. Forward-port up to saas-11.3 Closes #36252 opw:2062103 closes odoo/odoo#36412 Signed-off-by: Simon Goffin (sig) --- addons/account/models/account_invoice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index c22a2beb56b..e545d5d0507 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -114,8 +114,10 @@ class AccountInvoice(models.Model): if line.currency_id == self.currency_id: residual += line.amount_residual_currency if line.currency_id else line.amount_residual else: - from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date) - residual += from_currency.compute(line.amount_residual, self.currency_id) + if line.currency_id: + residual += line.currency_id.with_context(date=line.date).compute(line.amount_residual_currency, self.currency_id) + else: + residual += line.company_id.currency_id.with_context(date=line.date).compute(line.amount_residual, self.currency_id) self.residual_company_signed = abs(residual_company_signed) * sign self.residual_signed = abs(residual) * sign self.residual = abs(residual) From 2761393b06bb396cf08799948e6fda3091ff1f35 Mon Sep 17 00:00:00 2001 From: jev-odoo Date: Tue, 27 Aug 2019 09:26:51 +0000 Subject: [PATCH 14/14] [FIX] stock,sale_stock: Speed up sale_order confirmation In case there is a lot of stock_moves and/or lot of stock_quants, the function _get_qty_procurement takes a lot of time because there is no index defined on most accessed fields. I.E. OPW-2027183 closes odoo/odoo#36113 Signed-off-by: Richard Mathot (rim) --- addons/sale_stock/models/stock.py | 2 +- addons/stock/models/stock_quant.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index a346b815d20..b28a3e4c2ef 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -11,7 +11,7 @@ class StockLocationRoute(models.Model): class StockMove(models.Model): _inherit = "stock.move" - sale_line_id = fields.Many2one('sale.order.line', 'Sale Line') + sale_line_id = fields.Many2one('sale.order.line', 'Sale Line', index=True) @api.model def _prepare_merge_moves_distinct_fields(self): diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 80bd07e9a80..20a4bd640c6 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -20,7 +20,7 @@ class StockQuant(models.Model): product_id = fields.Many2one( 'product.product', 'Product', - ondelete='restrict', readonly=True, required=True) + ondelete='restrict', readonly=True, required=True, index=True) # so user can filter on template in webclient product_tmpl_id = fields.Many2one( 'product.template', string='Product Template', @@ -32,7 +32,7 @@ class StockQuant(models.Model): string='Company', store=True, readonly=True) location_id = fields.Many2one( 'stock.location', 'Location', - auto_join=True, ondelete='restrict', readonly=True, required=True) + auto_join=True, ondelete='restrict', readonly=True, required=True, index=True) lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', ondelete='restrict', readonly=True)