[MERGE] forward port branch 12.0 up to 638eac9bae
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user