[MERGE] forward port branch 12.0 up to 638eac9bae

This commit is contained in:
Christophe Simonis
2019-09-04 19:32:26 +02:00
21 changed files with 325 additions and 91 deletions
+13 -8
View File
@@ -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])
+6
View File
@@ -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();
+1
View File
@@ -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')
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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');