diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-04 19:32:26 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-04 19:32:26 +0200 |
| commit | 7a548569c665ae162510e7dc2ea48b16a041262a (patch) | |
| tree | 68782d726176c44c283e36006891090295758ef0 | |
| parent | ae50f2cbbba0a57b47039fcc8a8485dc857fa851 (diff) | |
| parent | 638eac9baea60ee9126043ee0088232a5dee3668 (diff) | |
[MERGE] forward port branch 12.0 up to 638eac9baea
21 files changed, 325 insertions, 91 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index b5f0c64a523..90651b8b0a3 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -132,8 +132,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) @@ -773,17 +775,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 @@ -807,8 +814,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]) 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 @@ -80,6 +80,12 @@ var CrossTabBus = Longpolling.extend({ this._callLocalStorage('setItem', 'channels', this._channels); }, /** + * @return {string} + */ + getTabId: function () { + return this._id; + }, + /** * Tells whether this bus is related to the master tab. * * @returns {boolean} diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 086f02daa32..c6b59683316 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -123,6 +123,14 @@ var LivechatButton = Widget.extend({ serverURL: this._serverURL, }); var message = new WebsiteLivechatMessage(this, data, options); + + var hasAlreadyMessage = _.some(this._messages, function (msg) { + return message.id === msg.id; + }); + if (hasAlreadyMessage) { + return; + } + if (this._livechat) { this._livechat.addMessage(message); } diff --git a/addons/l10n_ae/data/account_tax_template_data.xml b/addons/l10n_ae/data/account_tax_template_data.xml index b3cec92c1c5..2e1a98f7549 100644 --- a/addons/l10n_ae/data/account_tax_template_data.xml +++ b/addons/l10n_ae/data/account_tax_template_data.xml @@ -1,11 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <record id="uae_sale_tax_5_dubai" model="account.tax.template"> - <field name="name">TVA 5% (Dubai)</field> + <field name="name">VAT 5% (Dubai)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -36,11 +36,11 @@ ]"/> </record> <record id="uae_sale_tax_5_abu_dhabi" model="account.tax.template"> - <field name="name">TVA 5% (Abu Dhabi)</field> + <field name="name">VAT 5% (Abu Dhabi)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -71,11 +71,11 @@ ]"/> </record> <record id="uae_sale_tax_5_sharjah" model="account.tax.template"> - <field name="name">TVA 5% (Sharjah)</field> + <field name="name">VAT 5% (Sharjah)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -106,11 +106,11 @@ ]"/> </record> <record id="uae_sale_tax_5_ajman" model="account.tax.template"> - <field name="name">TVA 5% (Ajman)</field> + <field name="name">VAT 5% (Ajman)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -141,11 +141,11 @@ ]"/> </record> <record id="uae_sale_tax_5_umm_al_quwain" model="account.tax.template"> - <field name="name">TVA 5% (Umm Al Quwain)</field> + <field name="name">VAT 5% (Umm Al Quwain)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -176,11 +176,11 @@ ]"/> </record> <record id="uae_sale_tax_5_ras_al_khaima" model="account.tax.template"> - <field name="name">TVA 5% (Ras Al-Khaima)</field> + <field name="name">VAT 5% (Ras Al-Khaima)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -211,11 +211,11 @@ ]"/> </record> <record id="uae_sale_tax_5_fujairah" model="account.tax.template"> - <field name="name">TVA 5% (Fujairah)</field> + <field name="name">VAT 5% (Fujairah)</field> <field name="type_tax_use">sale</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -277,11 +277,11 @@ ]"/> </record> <record id="uae_sale_tax_0" model="account.tax.template"> - <field name="name">TVA 0%</field> + <field name="name">VAT 0%</field> <field name="type_tax_use">sale</field> <field name="amount">0</field> <field name="amount_type">percent</field> - <field name="description">TVA 0%</field> + <field name="description">VAT 0%</field> <field name="tax_group_id" ref="ae_tax_group_0"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -693,11 +693,11 @@ </record> <!-- purchase taxes --> <record id="uae_purchase_tax_5" model="account.tax.template"> - <field name="name">TVA 5%</field> + <field name="name">VAT 5%</field> <field name="type_tax_use">purchase</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -757,11 +757,11 @@ ]"/> </record> <record id="uae_purchase_tax_0" model="account.tax.template"> - <field name="name">TVA 0%</field> + <field name="name">VAT 0%</field> <field name="type_tax_use">purchase</field> <field name="amount">0</field> <field name="amount_type">percent</field> - <field name="description">TVA 0%</field> + <field name="description">VAT 0%</field> <field name="tax_group_id" ref="ae_tax_group_0"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -818,22 +818,22 @@ ]"/> </record> <record id="uae_purchase_tax_5_child_global" model="account.tax.template"> - <field name="name">TVA 5%</field> + <field name="name">VAT 5%</field> <field name="type_tax_use">none</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="account_id" ref="uae_account_3726"/> <field name="refund_account_id" ref="uae_account_3726"/> <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> </record> <record id="uae_purchase_tax_5_child_reverse" model="account.tax.template"> - <field name="name">Reverse Charge TVA +5%(Purchase)</field> + <field name="name">Reverse Charge VAT +5%(Purchase)</field> <field name="type_tax_use">none</field> <field name="amount">5</field> <field name="amount_type">percent</field> - <field name="description">TVA 5%</field> + <field name="description">VAT 5%</field> <field name="account_id" ref="uae_account_3726"/> <field name="refund_account_id" ref="uae_account_3726"/> <field name="tax_group_id" ref="ae_tax_group_5"/> 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 0ef02e5f57a..cb860b70beb 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 acf06461a04..6bb66b1b439 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 a832b0b0793..d8174da5a73 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. * @returns {Promise} */ openThreadWindow: function (threadID, options) { @@ -132,7 +134,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, + }); 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 86d06ba36c4..0748168a0cb 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', async function (assert) testUtils.mock.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); @@ -227,7 +227,7 @@ QUnit.test('post messages in a document thread window', async function (assert) }); QUnit.test('open, fold, unfold and close a document thread window', async function (assert) { - assert.expect(8); + assert.expect(24); var messagingMenu = new MessagingMenu(); testUtils.mock.addMockEnvironment(messagingMenu, { services: this.services, @@ -237,7 +237,13 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi await testUtils.nextTick(); testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) { if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') { - assert.step(JSON.stringify(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); await messagingMenu.appendTo($('#qunit-fixture')); @@ -263,10 +269,10 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi await testUtils.nextTick(); 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(); diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index da86bdfa0ac..015e1c24609 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -335,6 +335,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('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked') diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index b85d4525566..025bdeb26f1 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 f43374dad61..2d92b171a6c 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) 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 @@ -45,6 +45,19 @@ var AbstractStorageService = AbstractService.extend({ 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 * * @param {string} key 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; 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 @@ -42,11 +43,21 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, { 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}); }, 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 23adb8568de..cce5b693016 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -585,7 +585,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/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index ec793974609..05cc352098e 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -708,6 +708,7 @@ var ListController = BasicController.extend({ * @param {OdooEvent} ev */ _onToggleGroup: function (ev) { + event.stopPropagation(); var self = this; this.model .toggleGroup(ev.data.group.id) diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 282b1799dde..fd7e9eaf801 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -89,8 +89,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; @@ -107,6 +107,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 92600eed10e..63694e07781 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2579,8 +2579,32 @@ QUnit.module('basic_fields', { form.destroy(); }); - QUnit.test('date field should remove the date if the date is not valid', async function (assert) { - assert.expect(1); + QUnit.test('date field: set an invalid date when the field is already set', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners"><field name="date"/></form>', + 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', async function (assert) { + assert.expect(2); var form = await createView({ View: FormView, @@ -2588,13 +2612,18 @@ QUnit.module('basic_fields', { data: this.data, arch: '<form string="Partners"><field name="date"/></form>', res_id: 4, + viewOptions: { + mode: 'edit', + }, }); - // switch to edit mode - await testUtils.form.clickEdit(form); - // 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(); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index d89bffac31b..4c1265fae27 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -7882,6 +7882,46 @@ QUnit.module('Views', { form.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 = testUtils.makeTestPromise(); + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `<form> + <field name="foo"/> + <field name="bar" widget="toggle_button"/> + </form>`, + 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', async function(assert) { diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 41be0844c46..1e6fd7e5c53 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3797,6 +3797,57 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('grouped list with another grouped list parent, click unfold', async 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 = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top"><field name="foo"/><field name="m2o"/></tree>', + groupBy: ['bar'], + archs: { + 'bar,false,list': '<tree><field name="cornichon"/></tree>', + 'bar,false,search': '<search><filter context="{\'group_by\': \'cornichon\'}" string="cornichon"/></search>', + }, + }); + + list.update({groupBy: []}); + + await testUtils.dom.click(list.$('.o_data_cell:eq(0)')); + + await testUtils.dom.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'); + + await testUtils.dom.click($('body .modal-content button:contains(Group By)')); + + await testUtils.dom.click($('body .modal-content .o_menu_item a:contains(cornichon)')); + + await testUtils.dom.click($('body .modal-content .o_group_header')); + + assert.containsOnce($('body'), '.modal-content .o_group_open'); + + list.destroy(); + }); + QUnit.test('field values are escaped', async function (assert) { assert.expect(1); var value = '<script>throw Error();</script>'; diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js index f158c713af7..f523816995a 100644 --- a/addons/website_blog/static/src/js/website_blog.js +++ b/addons/website_blog/static/src/js/website_blog.js @@ -85,31 +85,34 @@ publicWidget.registry.websiteBlog = publicWidget.Widget.extend({ */ _onShareArticle: function (ev) { var url = ''; + var articleURL; ev.preventDefault(); var $element = $(ev.currentTarget); if ($element.is('*[class*="_complete"]')) { - var blogTitleComplete = $('#blog_post_name').html() || ''; + var blogTitleComplete = encodeURIComponent($('#blog_post_name').html() || ''); + articleURL = encodeURIComponent(window.location.href); if ($element.hasClass('o_twitter_complete')) { - url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blogTitleComplete + "! Check it live: " + window.location.href; + url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blogTitleComplete + "! Check it live: " + articleURL; } else if ($element.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 ($element.hasClass('o_linkedin_complete')) { - url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + window.location.href + '&title=' + blogTitleComplete; + url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + articleURL + '&title=' + blogTitleComplete; } else { - url = 'https://plus.google.com/share?url=' + window.location.href; + url = 'https://plus.google.com/share?url=' + articleURL; } } else { var blogPost = $element.parents('[name="blog_post"]'); - var blogPostTitle = blogPost.find('.o_blog_post_title').html() || ''; + var blogPostTitle = encodeURIComponent(blogPost.find('.o_blog_post_title').html() || ''); var blogArticleLink = blogPost.find('.o_blog_post_title').parent('a').attr('href'); + articleURL = encodeURIComponent(window.location.host + blogArticleLink); if ($element.hasClass('o_twitter')) { - url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blogPostTitle + "! " + window.location.host + blogArticleLink; + url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=Amazing blog article : ' + blogPostTitle + "! " + articleURL; } else if ($element.hasClass('o_facebook')) { - url = 'https://www.facebook.com/sharer/sharer.php?u=' + window.location.host + blogArticleLink; + url = 'https://www.facebook.com/sharer/sharer.php?u=' + articleURL; } else if ($element.hasClass('o_linkedin')) { - url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + window.location.host + blogArticleLink + '&title=' + blogPostTitle; + url = 'https://www.linkedin.com/shareArticle?mini=true&url=' + articleURL + '&title=' + blogPostTitle; } else if ($element.hasClass('o_google')) { - url = 'https://plus.google.com/share?url=' + window.location.host + blogArticleLink; + url = 'https://plus.google.com/share?url=' + articleURL; } } window.open(url, '', 'menubar=no, width=500, height=400'); |
