diff --git a/addons/l10n_ch/views/account_invoice_view.xml b/addons/l10n_ch/views/account_invoice_view.xml
index 52421816d1d..d86c8e8bece 100644
--- a/addons/l10n_ch/views/account_invoice_view.xml
+++ b/addons/l10n_ch/views/account_invoice_view.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml
index a56df3aa7b2..780cdb8810a 100644
--- a/addons/l10n_it_edi/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi/data/invoice_it_template.xml
@@ -112,6 +112,7 @@
+
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index de5ce88b742..ca926096c38 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -505,6 +505,7 @@ var Discuss = AbstractAction.extend({
*/
_initRender: function () {
var self = this;
+ this.$('.o_mail_discuss_loading').remove();
this._basicComposer = new BasicComposer(this, {
mentionPartnersRestricted: true,
showTyping: true,
@@ -1419,7 +1420,6 @@ var Discuss = AbstractAction.extend({
if (!this._isStarted) {
return;
}
- this.$('.o_mail_discuss_loading').remove();
this._initRender();
},
/**
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 8f96e8b754e..2b41f72e599 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -1,6 +1,7 @@
odoo.define('mail.discuss_test', function (require) {
"use strict";
+const Discuss = require('mail.Discuss');
var mailTestUtils = require('mail.testUtils');
var testUtils = require('web.test_utils');
@@ -191,6 +192,59 @@ QUnit.test('messaging not ready', async function (assert) {
discuss.destroy();
});
+QUnit.test('messaging initially ready', async function (assert) {
+ assert.expect(7);
+
+ const startDiscussProm = testUtils.makeTestPromise();
+
+ testUtils.mock.patch(Discuss, {
+ /**
+ * @override
+ */
+ async start() {
+ const _super = this._super.bind(this, ...arguments); // due to limitation of class.js
+ assert.step('discuss:starting');
+ await startDiscussProm;
+ assert.step('discuss:started');
+ return _super();
+ },
+ });
+
+ const discussProm = createDiscuss({
+ id: 1,
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ mockRPC(route) {
+ if (route === '/mail/init_messaging') {
+ assert.step(route);
+ }
+ return this._super(...arguments);
+ }
+ });
+ await testUtils.nextTick();
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'discuss:starting',
+ ]);
+
+ startDiscussProm.resolve();
+ await testUtils.nextTick();
+ assert.verifySteps(['discuss:started']);
+ const discuss = await discussProm;
+ assert.ok(
+ discuss.el,
+ "discuss should be rendered");
+ assert.containsNone(
+ discuss,
+ '.o_mail_discuss_loading',
+ "should not display sidebar or content is loading (messaging is ready)");
+
+ testUtils.mock.unpatch(Discuss);
+ discuss.destroy();
+});
+
QUnit.test('searchview options visibility', async function (assert) {
assert.expect(5);
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 6e0fdaa206e..1ba5110b1a9 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -572,7 +572,7 @@ class ProductProduct(models.Model):
'target': 'new'}
def _prepare_sellers(self, params):
- return self.seller_ids.sorted(lambda s: (s.sequence, -s.min_qty, s.price))
+ return self.seller_ids.filtered(lambda s: s.name.active).sorted(lambda s: (s.sequence, -s.min_qty, s.price))
def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False):
self.ensure_one()
@@ -710,7 +710,6 @@ class SupplierInfo(models.Model):
'res.partner', 'Vendor',
ondelete='cascade', required=True,
help="Vendor of this product", check_company=True)
- active = fields.Boolean(related='name.active', readonly=True)
product_name = fields.Char(
'Vendor Product Name',
help="This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one.")
diff --git a/addons/purchase_requisition/models/purchase.py b/addons/purchase_requisition/models/purchase.py
index 360c7633ff6..b1f783feb98 100644
--- a/addons/purchase_requisition/models/purchase.py
+++ b/addons/purchase_requisition/models/purchase.py
@@ -115,12 +115,11 @@ class PurchaseOrderLine(models.Model):
def _onchange_quantity(self):
res = super(PurchaseOrderLine, self)._onchange_quantity()
if self.order_id.requisition_id:
- for line in self.order_id.requisition_id.line_ids:
- if line.product_id == self.product_id:
- if line.product_uom_id != self.product_uom:
- self.price_unit = line.product_uom_id._compute_price(
- line.price_unit, self.product_uom)
- else:
- self.price_unit = line.price_unit
- break
+ for line in self.order_id.requisition_id.line_ids.filtered(lambda l: l.product_id == self.product_id):
+ if line.product_uom_id != self.product_uom:
+ self.price_unit = line.product_uom_id._compute_price(
+ line.price_unit, self.product_uom)
+ else:
+ self.price_unit = line.price_unit
+ break
return res
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index f3da4c34a37..602316f3f22 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -588,11 +588,10 @@ class ProductTemplate(models.Model):
outgoing_qty = fields.Float(
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
compute_sudo=False, digits='Product Unit of Measure')
- # The goal of these fields is not to be able to search a location_id/warehouse_id but
- # to properly make these fields "dummy": only used to put some keys in context from
- # the search view in order to influence computed field
- location_id = fields.Many2one('stock.location', 'Location', store=False, search=lambda operator, operand, vals: [])
- warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False, search=lambda operator, operand, vals: [])
+ # The goal of these fields is to be able to put some keys in context from search view in order
+ # to influence computed field.
+ location_id = fields.Many2one('stock.location', 'Location', store=False)
+ warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False)
route_ids = fields.Many2many(
'stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes',
domain=[('product_selectable', '=', True)],
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index 3f71cea2bda..1aef80c1abb 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -73,8 +73,8 @@
-
-
+
+
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 21ce85ce025..df1a03e0f42 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -404,10 +404,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
options = options || {};
return this.canBeDiscarded(recordID)
.then(function (needDiscard) {
- if (options.noAbandon || (options.readonlyIfRealDiscard && !needDiscard)) {
+ if (options.readonlyIfRealDiscard && !needDiscard) {
return;
}
self.model.discardChanges(recordID);
+ if (options.noAbandon) {
+ return;
+ }
if (self.model.canBeAbandoned(recordID)) {
self._abandonRecord(recordID);
return;
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 002a52f572f..b878e642b4f 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -4227,6 +4227,45 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+
+ QUnit.test('Call twice clearUncommittedChanges in a row does not display twice the discard warning', async function (assert) {
+ assert.expect(4);
+
+ var actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ intercepts: {
+ clear_uncommitted_changes: function () {
+ actionManager.clearUncommittedChanges();
+ },
+ },
+ });
+
+ // execute an action and edit existing record
+ await actionManager.doAction(3);
+
+ await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
+ assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
+
+ await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
+ assert.containsOnce(actionManager, '.o_form_view.o_form_editable');
+
+ await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val');
+ actionManager.trigger_up('clear_uncommitted_changes');
+ await testUtils.nextTick();
+
+ assert.containsOnce($('body'), '.modal'); // confirm discard dialog
+ // confirm discard changes
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ actionManager.trigger_up('clear_uncommitted_changes');
+ await testUtils.nextTick();
+
+ assert.containsNone($('body'), '.modal');
+
+ actionManager.destroy();
+ });
});
});
diff --git a/addons/website/controllers/backend.py b/addons/website/controllers/backend.py
index 86fe802d4f2..89355a74554 100644
--- a/addons/website/controllers/backend.py
+++ b/addons/website/controllers/backend.py
@@ -26,8 +26,10 @@ class WebsiteBackend(http.Controller):
current_website = website_id and Website.browse(website_id) or Website.get_current_website()
multi_website = request.env.user.has_group('website.group_multi_website')
- dashboard_data['websites'] = (multi_website and request.env['website'].search([]) or current_website).read(['id', 'name'])
- for website in dashboard_data['websites']:
+ websites = multi_website and request.env['website'].search([]) or current_website
+ dashboard_data['websites'] = websites.read(['id', 'name'])
+ for rec, website in zip(websites, dashboard_data['websites']):
+ website['domain'] = rec._get_http_domain()
if website['id'] == current_website.id:
website['selected'] = True
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 5d7a992dca1..070b885bc64 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -338,9 +338,9 @@ class View(models.Model):
cur = Website.get_current_website()
if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'):
- qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain}
+ qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur._get_http_domain()}
qcontext['multi_website_websites'] = [
- {'website_id': website.id, 'name': website.name, 'domain': website.domain}
+ {'website_id': website.id, 'name': website.name, 'domain': website._get_http_domain()}
for website in Website.search([]) if website != cur
]
diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js
index 28b9a55c8a4..f074bb0df84 100644
--- a/addons/website/static/src/js/backend/dashboard.js
+++ b/addons/website/static/src/js/backend/dashboard.js
@@ -133,6 +133,12 @@ var Dashboard = AbstractAction.extend({
}).open();
},
+ on_go_to_website: function (ev) {
+ ev.preventDefault();
+ var website = _.findWhere(this.websites, {selected: true});
+ window.location.href = $.param.querystring(website.domain + '/', {'fw': website.id});
+ },
+
on_save_ga_client_id: function(ga_client_id, ga_analytics_key) {
var self = this;
return this._rpc({
@@ -393,10 +399,14 @@ var Dashboard = AbstractAction.extend({
self.on_website_button($(ev.target).data('website-id'));
});
}
+
+ var $buttons = $(QWeb.render("website.GoToButtons"));
+ $buttons.on('click', this.on_go_to_website.bind(this));
+
this.updateControlPanel({
cp_content: {
$searchview: this.$searchview,
- $buttons: QWeb.render("website.GoToButtons"),
+ $buttons: $buttons,
},
});
},
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index a8e208f1408..70f8c5014f5 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -181,21 +181,14 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
* @param {Event} ev
*/
_onWebsiteSwitch: function (ev) {
- var websiteID = ev.currentTarget.getAttribute('website-id');
-
- // need to force in each case, even if domain is set
- // Website 1: localhost; Website 2: 0.0.0.0; website 3: -
- // when you switch 3 <--> 1, you need to force the website
-
+ var websiteId = ev.currentTarget.getAttribute('website-id');
var websiteDomain = ev.currentTarget.getAttribute('domain');
- var url = $.param.querystring(window.location.href, {fw: websiteID});
+ var url = window.location.href;
if (websiteDomain && window.location.hostname !== websiteDomain) {
- // if domain unchanged, this line will do a nop while we need to refresh
- // the page to load the new forced website.
- url = new URL(url);
- url.hostname = websiteDomain;
+ var path = window.location.pathname + window.location.search + window.location.hash;
+ url = websiteDomain + path;
}
- window.location.href = url;
+ window.location.href = $.param.querystring(url, {'fw': websiteId});
},
/**
* @private