summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-04 19:32:26 +0200
committerChristophe Simonis <chs@odoo.com>2019-09-04 19:32:26 +0200
commit7a548569c665ae162510e7dc2ea48b16a041262a (patch)
tree68782d726176c44c283e36006891090295758ef0
parentae50f2cbbba0a57b47039fcc8a8485dc857fa851 (diff)
parent638eac9baea60ee9126043ee0088232a5dee3668 (diff)
[MERGE] forward port branch 12.0 up to 638eac9baea
-rw-r--r--addons/account/models/account_invoice.py21
-rw-r--r--addons/bus/static/src/js/crosstab_bus.js6
-rw-r--r--addons/im_livechat/static/src/js/im_livechat.js8
-rw-r--r--addons/l10n_ae/data/account_tax_template_data.xml48
-rw-r--r--addons/mail/static/src/js/models/threads/document_thread.js27
-rw-r--r--addons/mail/static/src/js/services/mail_document_thread_manager.js73
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js6
-rw-r--r--addons/mail/static/tests/document_thread_window_tests.js20
-rw-r--r--addons/mrp/models/mrp_production.py1
-rw-r--r--addons/sale_stock/models/stock.py2
-rw-r--r--addons/stock/models/stock_quant.py4
-rw-r--r--addons/web/static/src/js/core/abstract_storage_service.js13
-rw-r--r--addons/web/static/src/js/core/local_storage.js10
-rw-r--r--addons/web/static/src/js/core/ram_storage.js14
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js4
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js1
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js5
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js39
-rw-r--r--addons/web/static/tests/views/form_tests.js40
-rw-r--r--addons/web/static/tests/views/list_tests.js51
-rw-r--r--addons/website_blog/static/src/js/website_blog.js23
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');