diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index 82c5c2a4841..94998f98052 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -547,7 +547,6 @@
-
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 77d571b9171..f0be24f5fc4 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -277,7 +277,7 @@ class StockRule(models.Model):
'user_id': False,
'picking_type_id': self.picking_type_id.id,
'company_id': company_id.id,
- 'currency_id': partner.with_context(force_company=company_id.id).property_purchase_currency_id.id or self.env.company.currency_id.id,
+ 'currency_id': partner.with_context(force_company=company_id.id).property_purchase_currency_id.id or company_id.currency_id.id,
'dest_address_id': values.get('partner_id', False),
'origin': ', '.join(origins),
'payment_term_id': partner.with_context(force_company=company_id.id).property_supplier_payment_term_id.id,
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 2fe56bb5fd9..6c741044e78 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1017,7 +1017,8 @@ class SaleOrderLine(models.Model):
for line in self:
fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id
# If company_id is set, always filter taxes by the company
- taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
+ line_company_id = line.company_id or line.order_id.company_id
+ taxes = line.product_id.taxes_id.filtered(lambda r: not line_company_id or r.company_id == line_company_id)
line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes
@api.model
diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py
index f738d7c78b8..f694400d220 100644
--- a/addons/sale/tests/test_sale_order.py
+++ b/addons/sale/tests/test_sale_order.py
@@ -239,6 +239,59 @@ class TestSaleOrder(TestCommonSaleNoChart):
self.sale_order.amount_untaxed + self.sale_order.amount_tax,
'Taxes should be applied')
+ def test_so_create_multicompany(self):
+ # Preparing test Data
+ user_demo = self.env.ref('base.user_demo')
+ company_1 = self.env.ref('base.main_company')
+ company_2 = self.env['res.company'].create({
+ 'name': 'company 2',
+ 'parent_id': company_1.id,
+ })
+ user_demo.write({
+ 'groups_id': [(4, self.env.ref('sales_team.group_sale_manager').id, False)],
+ 'company_ids': [(6, False, [company_1.id])],
+ 'company_id': company_1.id,
+ })
+
+ so_partner = self.env.ref('base.res_partner_2')
+ so_partner.write({
+ 'property_account_position_id': False,
+ })
+
+ tax_company_1 = self.env['account.tax'].create({
+ 'name': 'T1',
+ 'amount': 90,
+ 'company_id': company_1.id,
+ })
+
+ tax_company_2 = self.env['account.tax'].create({
+ 'name': 'T2',
+ 'amount': 90,
+ 'company_id': company_2.id,
+ })
+
+ product_shared = self.env['product.template'].create({
+ 'name': 'shared product',
+ 'taxes_id': [(6, False, [tax_company_1.id, tax_company_2.id])],
+ })
+
+ # Use case
+ so_1 = self.env['sale.order'].sudo(user_demo.id).create({
+ 'partner_id': so_partner.id,
+ 'company_id': company_1.id,
+ })
+ so_1.invalidate_cache()
+
+ # This is what is done when importing the csv lines (on sale.order):
+ # id,order_line/product_id
+ # __export__.sale_order_37_1bb960ba,Product name
+ so_1.write({
+ 'order_line': [(0, False, {'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})],
+ })
+
+ self.assertEqual(set(so_1.order_line.tax_id.ids), set([tax_company_1.id]),
+ 'Only taxes from the right company are put by default')
+
def test_reconciliation_with_so(self):
# create SO
so = self.env['sale.order'].create({
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index 3f0889fa3b8..86635a61804 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -466,12 +466,11 @@
-
+
diff --git a/addons/sale_purchase/models/sale_order.py b/addons/sale_purchase/models/sale_order.py
index a055090c590..9a5388ab51b 100644
--- a/addons/sale_purchase/models/sale_order.py
+++ b/addons/sale_purchase/models/sale_order.py
@@ -114,8 +114,8 @@ class SaleOrderLine(models.Model):
decreased_values = {}
if 'product_uom_qty' in values:
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- increased_lines = self.sudo().filtered(lambda r: r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
- decreased_lines = self.sudo().filtered(lambda r: r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1)
+ increased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
+ decreased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1)
increased_values = {line.id: line.product_uom_qty for line in increased_lines}
decreased_values = {line.id: line.product_uom_qty for line in decreased_lines}
diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py
index c52262e9515..df325822e27 100644
--- a/addons/stock/models/res_config_settings.py
+++ b/addons/stock/models/res_config_settings.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
@@ -53,6 +54,13 @@ class ResConfigSettings(models.TransientModel):
def _onchange_group_stock_production_lot(self):
if not self.group_stock_production_lot:
self.group_lot_on_delivery_slip = False
+ tracked_products = self.env['product.template'].search([('tracking', 'in', ['lot', 'serial']),])
+ if not self.group_stock_production_lot and tracked_products:
+ names = ", ".join(tracked_products.mapped('display_name') if len(tracked_products) <= 10
+ else tracked_products[:10].mapped('display_name') + ["..."])
+ raise UserError(_("You should not remove the 'lots and serial numbers' "
+ "option while the following products are still tracked by lot "
+ "or serial number:\n %s") % names)
@api.onchange('group_stock_adv_location')
def onchange_adv_location(self):
diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml
index cf204581e53..7003796bd55 100644
--- a/addons/stock/security/stock_security.xml
+++ b/addons/stock/security/stock_security.xml
@@ -99,7 +99,7 @@
stock_move multi-company
- ['|',('company_id','=',False),('company_id', 'in', company_ids)]
+ ['|', '|', ('company_id','=',False), ('company_id', 'in', company_ids), ('location_dest_id.company_id', '=', False)]
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index e8e54d1b695..efbe443a689 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -193,6 +193,7 @@
product.product.procurement
product.product
+
@@ -283,6 +284,7 @@
product.template_procurement
product.template
+
diff --git a/addons/stock_dropshipping/__manifest__.py b/addons/stock_dropshipping/__manifest__.py
index de26d991087..9d092675cf2 100644
--- a/addons/stock_dropshipping/__manifest__.py
+++ b/addons/stock_dropshipping/__manifest__.py
@@ -21,7 +21,7 @@ internal transfer document is needed.
""",
'depends': ['sale_purchase', 'sale_stock'],
- 'data': ['data/stock_data.xml'],
+ 'data': ['data/stock_data.xml', 'views/sale_order_views.xml'],
'installable': True,
'auto_install': False,
}
diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py
index f70897f9eb8..9048fb2d009 100644
--- a/addons/stock_dropshipping/tests/test_dropship.py
+++ b/addons/stock_dropshipping/tests/test_dropship.py
@@ -45,7 +45,7 @@ class TestDropship(common.TransactionCase):
self.assertAlmostEqual(po_line.product_qty, 1.00)
# Update qty on SO and check PO
- so.order_line.product_uom_qty = 2.00
+ so.write({'order_line': [[1, so.order_line.id, {'product_uom_qty': 2.00}]]})
self.assertAlmostEqual(po_line.product_qty, 2.00)
# Create a new so line
diff --git a/addons/stock_dropshipping/views/sale_order_views.xml b/addons/stock_dropshipping/views/sale_order_views.xml
new file mode 100644
index 00000000000..b06b48ccab9
--- /dev/null
+++ b/addons/stock_dropshipping/views/sale_order_views.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ sale.order.form.sale.dropshipping
+ sale.order
+
+
+
+
+
+
+ {'readonly': ['|', ('product_updatable', '=', False), ('purchase_line_count', '>', 0)], 'required': [('display_type', '=', False)],}
+
+
+
+
+
+ {'readonly': ['|', ('product_updatable', '=', False), ('purchase_line_count', '>', 0)], 'required': [('display_type', '=', False)],}
+
+
+
+
+
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index 135af76deef..d03cd5cd89f 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -485,7 +485,7 @@ class TestHeavyMailPerformance(TransactionCase):
'user_id': self.env.uid,
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
- with self.assertQueryCount(__system__=55, emp=58): # com runbot: 55 - 58 // test_mail only: 55 - 58
+ with self.assertQueryCount(__system__=56, emp=58): # com runbot: 55 - 58 // test_mail only: 55 - 58
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
# write tracking message
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 860cf454dd4..18a36885f5f 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -201,11 +201,11 @@ def concat_xml(file_list):
:param list(str) file_list: list of files to check
:returns: (concatenation_result, checksum)
- :rtype: (str, str)
+ :rtype: (bytes, str)
"""
checksum = hashlib.new('sha1')
if not file_list:
- return '', checksum.hexdigest()
+ return b'', checksum.hexdigest()
root = None
for fname in file_list:
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 171a6da7735..160f445f1c4 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1603,8 +1603,8 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
}
this.$('> img').remove();
this.$el.prepend($img);
+
$img.one('error', function () {
- self._clearFile();
$img.attr('src', self.placeholder);
self.do_warn(_t("Image"), _t("Could not display the selected image."));
});
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index bc66594fee3..d3165420ef8 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -796,6 +796,7 @@ var BasicModel = AbstractModel.extend({
});
_.each(fields, function (field) {
var dataPoint;
+ record.data[field.name] = null;
if (field.type === 'many2one') {
if (field.value) {
var id = _.isArray(field.value) ? field.value[0] : field.value;
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index b3a04ec9a2b..bbfcb02def5 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -55,6 +55,7 @@ var CalendarController = AbstractController.extend({
this.readonlyFormViewId = params.readonlyFormViewId;
this.mapping = params.mapping;
this.context = params.context;
+ this.previousOpen = null;
// The quickCreating attribute ensures that we don't do several create
this.quickCreating = false;
},
@@ -271,7 +272,8 @@ var CalendarController = AbstractController.extend({
title += ': ' + this.renderer.arch.attrs.string;
}
if (this.eventOpenPopup) {
- new dialogs.FormViewDialog(self, {
+ if (this.previousOpen) { this.previousOpen.close(); }
+ this.previousOpen = new dialogs.FormViewDialog(self, {
res_model: this.modelName,
context: context,
title: title,
@@ -283,7 +285,8 @@ var CalendarController = AbstractController.extend({
}
self.reload();
},
- }).open();
+ });
+ this.previousOpen.open();
} else {
this.do_action({
type: 'ir.actions.act_window',
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js
index f2e28858ba3..0229daa41ee 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_model.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js
@@ -157,6 +157,9 @@ var ControlPanelModel = mvc.Model.extend({
if (filter.autoCompleteValues) {
filter.autoCompleteValues = [];
}
+ if (filter.currentOptionId) {
+ filter.currentOptionId = false;
+ }
});
// TODO: use toggleFilter here
group.activeFilterIds = [];
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
index b67bf9ee75d..fac67beb3f4 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
@@ -41,6 +41,7 @@ var GroupByMenu = DropdownMenu.extend({
*/
init: function (parent, groupBys, fields, options) {
var self = this;
+ options = options || {};
this._super(parent, groupBys);
this.fields = fields;
// determines when the 'Add custom groupby' submenu is open
@@ -65,9 +66,9 @@ var GroupByMenu = DropdownMenu.extend({
this.dropdownCategory = 'groupby';
this.dropdownTitle = _t('Group By');
this.dropdownIcon = 'fa fa-bars';
- this.dropdownSymbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : false;
+ this.dropdownSymbol = this.isMobile && !options.noSymbol ? 'fa fa-chevron-right float-right mt4' : false;
// the default style of the groupby menu can be changed here using the options key 'headerStyle'
- if (options && options.headerStyle === 'primary') {
+ if (options.headerStyle === 'primary') {
this.dropdownStyle = {
el: {class: 'btn-group o_group_by_menu o_dropdown', attrs: {'role': 'group'}},
mainButton: {class: 'btn btn-primary dropdown-toggle'},
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
index dc7a5dc3cc8..c9a17d6225e 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
@@ -36,6 +36,7 @@ var GroupByMenuMixin = {
* private
* @param {jQuery} $node
* @param {Object} groupableFields
+ * @param {Promise}
*/
_addGroupByMenu: function ($node, groupableFields) {
this.sortedFieldNames = _.sortBy(Object.keys(groupableFields), function (fieldName) {
@@ -43,8 +44,8 @@ var GroupByMenuMixin = {
});
this.groupableFields = groupableFields;
var groupBys = this._getGroupBys(this.model.get().groupBy);
- this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields);
- this.groupByMenu.insertAfter($node.find('div:first'));
+ this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields, {noSymbol: true});
+ return this.groupByMenu.insertAfter($node.find('div:first'));
},
/**
* This method puts the active groupBys in a convenient form.
diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js
index 9e2ebfc0a78..1c580a25bd6 100644
--- a/addons/web/static/src/js/views/graph/graph_controller.js
+++ b/addons/web/static/src/js/views/graph/graph_controller.js
@@ -96,8 +96,13 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
this._updateButtons();
this.$buttons.appendTo($node);
if (this.isEmbedded) {
- this._addGroupByMenu($node, this.groupableFields);
+ this._addGroupByMenu($node, this.groupableFields).then(function(){
+ var groupByButton = $node.find('.o_dropdown_toggler_btn');
+ groupByButton.removeClass("o_dropdown_toggler_btn btn btn-secondary dropdown-toggle");
+ groupByButton.addClass("btn dropdown-toggle btn-outline-secondary");
+ });
}
+
}
},
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 02fdb102358..9c70811ed18 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -215,6 +215,24 @@ return AbstractRenderer.extend({
}
return this.state.origins[dataPt.originIndex];
},
+ /**
+ * Returns an object used to style chart elements independently from the datasets.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getElementOptions: function () {
+ var elementOptions = {};
+ if (this.state.mode === 'bar') {
+ elementOptions.rectangle = {borderWidth: 1};
+ } else if (this.state.mode === 'line') {
+ elementOptions.line = {
+ tension: 0,
+ fill: false,
+ };
+ }
+ return elementOptions;
+ },
/**
* Returns a DateClasses instance used to manage equivalence of dates.
*
@@ -250,9 +268,8 @@ return AbstractRenderer.extend({
} else {
return dataPt.labels.slice(0, 1);
}
- } else if (i >= 0) {
+ } else if (i === 0) {
return Array.prototype.concat.apply([], [
- dataPt.labels.slice(0, i),
this.dateClasses.dateClass(dataPt.originIndex, dataPt.labels[i]),
dataPt.labels.slice(i+1)
]);
@@ -563,6 +580,7 @@ return AbstractRenderer.extend({
scales: this._getScaleOptions(),
legend: this._getLegendOptions(datasetsCount),
tooltips: this._getTooltipOptions(datasetsCount),
+ elements: this._getElementOptions(),
};
},
/**
@@ -583,7 +601,7 @@ return AbstractRenderer.extend({
if (_.contains(['bar', 'line'], this.state.mode) && i === 0) {
// here label is an array of length 1 and contains a number
return this.dateClasses.representative(label, originIndex) || '';
- } else if (this.state.mode === 'pie' && i >= 0) {
+ } else if (this.state.mode === 'pie' && i === 0) {
// here label is an array of length at least one containing string or numbers
var labelCopy = label.slice(0);
if (originIndex !== undefined) {
@@ -637,7 +655,7 @@ return AbstractRenderer.extend({
this.$el.append($canvasContainer);
var i = this.state.comparisonFieldIndex;
- if (i === 0 || (i > 0 && this.state.mode === 'pie')) {
+ if (i === 0) {
this.dateClasses = this._getDateClasses(dataPoints);
}
if (this.state.mode === 'bar') {
@@ -661,9 +679,6 @@ return AbstractRenderer.extend({
_renderBarChart: function (dataPoints) {
var self = this;
- // style rectangles
- Chart.defaults.global.elements.rectangle.borderWidth = 1;
-
// prepare data
var data = this._prepareData(dataPoints);
data.datasets.forEach(function (dataset, index) {
@@ -694,10 +709,6 @@ return AbstractRenderer.extend({
_renderLineChart: function (dataPoints) {
var self = this;
- // style lines
- Chart.defaults.global.elements.line.tension = 0;
- Chart.defaults.global.elements.line.fill = false;
-
// prepare data
var data = this._prepareData(dataPoints);
data.datasets.forEach(function (dataset, index) {
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 2a8e44df4a2..afa8f09a857 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -761,7 +761,7 @@ var ListRenderer = BasicRenderer.extend({
}
if (field.type === 'float' || field.type === 'integer' || field.type === 'monetary') {
- $th.css({ textAlign: 'right' });
+ $th.addClass('o_list_number_th');
}
if (config.isDebug()) {
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index a18d9f70083..21743a75751 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -23,6 +23,9 @@
cursor: default;
}
}
+ &.o_list_number_th {
+ text-align: right;
+ }
}
tbody > tr > td:not(.o_list_record_selector) {
@include o-text-overflow(table-cell);
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 16e304c14a6..8a5b8343208 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -5,6 +5,7 @@ var AbstractStorageService = require('web.AbstractStorageService');
var CalendarView = require('web.CalendarView');
var CalendarRenderer = require('web.CalendarRenderer');
var Dialog = require('web.Dialog');
+var ViewDialogs = require('web.view_dialogs');
var fieldUtils = require('web.field_utils');
var mixins = require('web.mixins');
var RamStorage = require('web.RamStorage');
@@ -482,6 +483,63 @@ QUnit.module('Views', {
calendar.destroy();
});
+ QUnit.test('open multiple event form at the same time', async function (assert) {
+ assert.expect(2);
+
+ var prom = testUtils.makeTestPromise();
+ var counter = 0;
+ testUtils.mock.patch(ViewDialogs.FormViewDialog, {
+ open: function () {
+ counter++;
+ this.options = _.omit(this.options, 'fields_view'); // force loadFieldView
+ return this._super.apply(this, arguments);
+ },
+ loadFieldView: function () {
+ var self = this;
+ var args = arguments;
+ var _super = this._super;
+ return prom.then(function () {
+ return _super.apply(self, args);
+ });
+ },
+ });
+
+ var event = $.Event();
+ var calendar = await createCalendarView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ ''+
+ ' '+
+ ' ',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
+ for (var i = 0; i < 5; i++) {
+ await testUtils.dom.triggerMouseEvent($cell, "mousedown");
+ await testUtils.dom.triggerMouseEvent($cell, "mouseup");
+ }
+ prom.resolve();
+ await testUtils.nextTick();
+ assert.equal(counter, 5, "there should had been 5 attemps to open a modal");
+ assert.containsOnce($('body'), '.modal', "there should be only one open modal");
+
+ calendar.destroy();
+ });
+
QUnit.test('create event with timezone in week mode European locale', async function (assert) {
assert.expect(5);
diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js
index 3fe15d661dd..ccf5bf5b50d 100644
--- a/addons/web/static/tests/views/search_view_tests.js
+++ b/addons/web/static/tests/views/search_view_tests.js
@@ -981,8 +981,8 @@ QUnit.module('Search View', {
QUnit.module('Autocompletion');
- QUnit.test('selection via autocompletion modifies appropriately submenus', async function (assert) {
- assert.expect(4);
+ QUnit.test('selection via autocompletion and deletion modifies appropriately submenus', async function (assert) {
+ assert.expect(6);
var actionManager = await createActionManager({
actions: this.actions,
@@ -1014,6 +1014,19 @@ QUnit.module('Search View', {
assert.strictEqual($('.o_group_by_menu .o_item_option a.selected').text().trim(), "Day",
"The item 'Day' should be selected in the groupby menu");
+ await testUtils.dom.click($('div .o_searchview_facet .o_facet_remove').eq(0));
+ await testUtils.dom.click($('div .o_searchview_facet .o_facet_remove').eq(0));
+
+ await testUtils.dom.click($('button .fa-filter'));
+ await testUtils.dom.click($('.o_filters_menu .o_menu_item').eq(0));
+ assert.containsNone($('.o_filters_menu .o_item_option a.selected'),
+ "The item 'This Month' should have unselected in the filters menu");
+
+ await testUtils.dom.click($('button .fa-bars'));
+ await testUtils.dom.click($('.o_group_by_menu .o_menu_item').eq(0));
+ assert.containsNone($('.o_group_by_menu .o_item_option a.selected'),
+ "The item 'Day' should have been unselected in the groupby menu");
+
actionManager.destroy();
});
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js
index b5c69102ff6..ef818762315 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -452,7 +452,7 @@ FieldHtml.include({
* @override
*/
_onLoadWysiwyg: function () {
- if (this.nodeOptions['style-inline']) {
+ if (this.nodeOptions['style-inline'] && this.isRendered) {
this._fromInline();
}
this._super();
diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js
index 40456961aeb..5ba630edc89 100644
--- a/addons/web_editor/static/src/js/backend/field_html.js
+++ b/addons/web_editor/static/src/js/backend/field_html.js
@@ -179,6 +179,8 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
* @returns {Object}
*/
_getWysiwygOptions: function () {
+ var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+
return {
recordInfo: {
context: this.record.getContext(this.recordParams),
@@ -197,6 +199,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
'SHIFT+TAB': null,
},
mac: {
+ 'ENTER': iPhone ? null : 'insertParagraph',
'TAB': null,
'SHIFT+TAB': null,
},
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
index 94ec9306f7d..9ac05220e45 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
@@ -9,18 +9,27 @@ dom.isAnchor = function (node) {
return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
!$(node).hasClass('fa') && !$(node).hasClass('o_image');
};
+var keycode = ({
+ '8': 'BACKSPACE',
+ '9': 'TAB',
+ '13': 'ENTER',
+ '46': 'DELETE',
+});
+
+var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
var KeyboardPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.keydown': '_onKeydown',
- 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
-
+ events: iPhone ?
+ {
+ 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
+ } :
+ {
+ 'summernote.keydown': '_onKeydown',
+ 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
+ 'keypress .note-editable': '_onKeyPress',
+ 'textInput .note-editable': '_onTextInput',
+ },
//--------------------------------------------------------------------------
// Private
@@ -818,7 +827,7 @@ var KeyboardPlugin = AbstractPlugin.extend({
}
},
/**
- * Remove the first unbreakable ancestor's next sibling if empty.
+ * Remove the first unbreakable ancestor's next sibling if not a block and empty.
*
* @private
* @param {Node} node
@@ -834,7 +843,7 @@ var KeyboardPlugin = AbstractPlugin.extend({
var isNextContainsOnlyInvisibleText = nextUnbreakable && _.all($(nextUnbreakable).contents(), function (n) {
return dom.isText(n) && !self.context.invoke('HelperPlugin.isVisibleText', n);
});
- if (isNextEmpty || isNextContainsOnlyInvisibleText) {
+ if (!dom.isBlock(nextUnbreakable) && (isNextEmpty || isNextContainsOnlyInvisibleText)) {
$(nextUnbreakable).remove();
}
},
@@ -1014,76 +1023,40 @@ var KeyboardPlugin = AbstractPlugin.extend({
* @returns {Boolean} true if case handled
*/
_onKeydown: function (se, e) {
- var self = this;
- var handled = false;
-
if (e.ctrlKey && e.key === 'a') {
e.preventDefault();
this._selectAll();
return;
}
-
- if (e.key &&
- (e.key.length === 1 || e.key === "Dead" || e.key === "Unidentified") &&
- !e.ctrlKey && !e.altKey && !e.metaKey) {
-
- if (e.key === "Dead" || e.key === "Unidentified") {
- this._accented = true;
- }
-
- // Record undo only if either:
- clearTimeout(this.lastCharIsVisibleTime);
- // e.key is punctuation or space
- var stopChars = [' ', ',', ';', ':', '?', '.', '!'];
- if (stopChars.indexOf(e.key) !== -1) {
- this.lastCharVisible = false;
- }
- // or not on top of history stack (record undo after undo)
- var history = this.context.invoke('HistoryPlugin.getHistoryStep');
- if (history && history.stack.length && history.stackOffset < history.stack.length - 1) {
- this.lastCharVisible = false;
- }
- // or no new char for 500ms
- this.lastCharIsVisibleTime = setTimeout(function () {
- self.lastCharIsVisible = false;
- }, 500);
- if (!this.lastCharIsVisible) {
- this.lastCharIsVisible = true;
- this.context.invoke('HistoryPlugin.recordUndo');
- }
-
- if (e.key !== "Dead") {
- this._onVisibleChar(e, this._accented);
- }
- } else {
- this.lastCharIsVisible = false;
- this.context.invoke('editor.clearTarget');
- this.context.invoke('MediaPlugin.hidePopovers');
- this.context.invoke('editor.beforeCommand');
- switch (e.keyCode) {
- case 8: // BACKSPACE
- handled = this._onBackspace(e);
- break;
- case 9: // TAB
- handled = this._onTab(e);
- break;
- case 13: // ENTER
- handled = this._onEnter(e);
- break;
- case 46: // DELETE
- handled = this._onDelete(e);
- break;
- }
- if (handled) {
- this._preventTextInEditableDiv();
- this.context.invoke('editor.saveRange');
- e.preventDefault();
- this.context.invoke('editor.afterCommand');
- }
+ if (!keycode[e.keyCode]) {
+ return;
}
- if (e.key !== "Dead") {
- this._accented = false;
+
+ this.context.invoke('editor.clearTarget');
+ this.context.invoke('MediaPlugin.hidePopovers');
+ this.context.invoke('editor.beforeCommand');
+ var handled = false;
+ switch (keycode[e.keyCode]) {
+ case 'BACKSPACE':
+ handled = this._onBackspace(e);
+ break;
+ case 'TAB':
+ handled = this._onTab(e);
+ break;
+ case 'ENTER':
+ handled = this._onEnter(e);
+ break;
+ case 'DELETE':
+ handled = this._onDelete(e);
+ break;
}
+ if (handled) {
+ e.preventDefault();
+ this._preventTextInEditableDiv();
+ this.context.invoke('editor.saveRange');
+ this.context.invoke('editor.afterCommand');
+ }
+ return handled;
},
/**
* Handle BACKSPACE keydown event.
@@ -1223,45 +1196,38 @@ var KeyboardPlugin = AbstractPlugin.extend({
return false;
},
/**
- * Handle visible char keydown event.
+ * Handle char input.
*
* @private
* @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
*/
- _onVisibleChar: function (e, accented) {
- var self = this;
- e.preventDefault();
- if (accented) {
- this.editable.normalize();
- var baseRange = this.context.invoke('editor.createRange');
-
- var $parent = $(baseRange.sc.parentNode);
- var parentContenteditable = $parent.attr('contenteditable');
- $parent.attr('contenteditable', false);
-
- var accentPlaceholder = this.document.createElement('span');
- $(baseRange.sc).after(accentPlaceholder);
- $(accentPlaceholder).attr('contenteditable', true);
-
- var range = this.context.invoke('editor.setRange', accentPlaceholder, 0);
- range.select();
-
- setTimeout(function () {
- var accentedChar = accentPlaceholder.innerHTML;
- $(accentPlaceholder).remove();
- if (parentContenteditable) {
- $parent.attr('contenteditable', parentContenteditable);
- } else {
- $parent.removeAttr('contenteditable');
- }
- baseRange.select();
- self.context.invoke('HelperPlugin.insertTextInline', accentedChar);
- });
- } else {
+ _onKeyPress: function (e) {
+ console.log(e.originalEvent);
+ if (!isSafari || iPhone) {
+ e.preventDefault();
this.context.invoke('HelperPlugin.insertTextInline', e.key);
}
- return true;
+ },
+ /**
+ * Handle char input.
+ *
+ * @private
+ * @param {jQueryEvent} e
+ */
+ _onTextInput: function (e) {
+ if (isSafari && !iPhone) {
+ return;
+ }
+ e.originalEvent.preventDefault();
+
+ var text = e.originalEvent.data;
+ if (!text) {
+ return;
+ }
+ if (text === 'Spacebar') {
+ text = ' ';
+ }
+ this.context.invoke('HelperPlugin.insertTextInline', text);
},
});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
index 71365e4348f..b8b4a7582fe 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
@@ -937,7 +937,7 @@ _.extend(wysiwygTranslation.image, {
dom.isVideo = function (node) {
node = node && !node.tagName ? node.parentNode : node;
return (node.tagName === "IFRAME" || node.tagName === "DIV") &&
- (node.parentNode.className && node.parentNode.className.indexOf('media_iframe_video') !== -1 ||
+ (node.parentNode && node.parentNode.className && node.parentNode.className.indexOf('media_iframe_video') !== -1 ||
node.className.indexOf('media_iframe_video') !== -1);
};
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js
index 5337661fa76..2b7d10d02fd 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js
@@ -766,6 +766,11 @@ var TextPlugin = AbstractPlugin.extend({
* @param {jQueryEvent} e
*/
_onPaste: function (se, e) {
+ var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+ var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ if (isSafari && !iPhone) {
+ return;
+ }
se.preventDefault();
se.stopImmediatePropagation();
e.preventDefault();
diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
index d13ddf63038..4193aef5332 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
@@ -678,6 +678,7 @@ var SnippetsMenu = Widget.extend({
// on text changes
this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
$(ev.target).selectContent();
+ $(ev.target).removeClass('o_default_snippet_text');
});
this.$document.on('keyup.snippets_menu', function () {
var range = Wysiwyg.getRange(this);
@@ -746,6 +747,7 @@ var SnippetsMenu = Widget.extend({
},
});
this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;
+ return this.cacheSnippetTemplate[this.options.snippets];
},
/**
* Sets the instance variables $editor, $body and selectorEditableArea.
@@ -1358,6 +1360,7 @@ var SnippetsMenu = Widget.extend({
if (!dropped) {
dropped = true;
$(this).first().after($toInsert).addClass('d-none');
+ $toInsert.removeClass('oe_snippet_body');
}
},
out: function () {
@@ -1366,6 +1369,7 @@ var SnippetsMenu = Widget.extend({
dropped = false;
$toInsert.detach();
$(this).removeClass('d-none');
+ $toInsert.addClass('oe_snippet_body');
}
},
});
diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js
index 99510bb3bae..c6d043a0722 100644
--- a/addons/web_editor/static/tests/test_utils.js
+++ b/addons/web_editor/static/tests/test_utils.js
@@ -508,9 +508,10 @@ var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
keypress.keyCode = keypress.keyCode;
var event = $.Event("keydown", keypress);
$target.trigger(event);
+
if (!event.isDefaultPrevented()) {
if (keypress.key.length === 1) {
- document.execCommand("insertText", 0, keypress.key);
+ textInput($target[0], keypress.key);
} else {
console.warn('Native "' + keypress.key + '" is not supported in test');
}
@@ -773,8 +774,40 @@ var keydown = function (key, $editable, options) {
var $target = $(target.tagName ? target : target.parentNode);
var event = $.Event("keydown", keyPress);
$target.trigger(event);
+
+ if (!event.isDefaultPrevented()) {
+ if (keyPress.key.length === 1) {
+ textInput($target[0], keyPress.key);
+ } else {
+ console.warn('Native "' + keyPress.key + '" is not supported in test');
+ }
+ }
};
+var textInput = function (target, char) {
+ var ev = new CustomEvent('textInput', {
+ bubbles: true,
+ cancelBubble: false,
+ cancelable: true,
+ composed: true,
+ data: char,
+ defaultPrevented: false,
+ detail: 0,
+ eventPhase: 3,
+ isTrusted: true,
+ returnValue: true,
+ sourceCapabilities: null,
+ type: "textInput",
+ which: 0,
+ });
+ ev.data = char;
+ target.dispatchEvent(ev);
+
+ if (!ev.defaultPrevented) {
+ document.execCommand("insertText", 0, ev.data);
+ }
+}
+
return {
wysiwygData: wysiwygData,
createWysiwyg: createWysiwyg,
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index a5e17c21628..a36bc717c7c 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -133,12 +133,8 @@ class View(models.Model):
('website_id', '!=', None),
])
for specific_parent_view in specific_parent_views:
- record.copy({
- # Set key to avoid copy() to generate an unique key as
- # we want the specific view to have the same key
- 'key': record.key,
+ record.with_context(website_id=specific_parent_view.website_id.id).write({
'inherit_id': specific_parent_view.id,
- 'website_id': specific_parent_view.website_id.id,
})
return records
diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py
index 202f6e19039..4afb9bd63e5 100644
--- a/addons/website/models/website_menu.py
+++ b/addons/website/models/website_menu.py
@@ -149,7 +149,12 @@ class Menu(models.Model):
if menu_id.page_id:
menu_id.page_id = None
else:
- page = self.env['website.page'].search(['|', ('url', '=', menu['url']), ('url', '=', '/' + menu['url'])], limit=1)
+ domain = self.env["website"].website_domain(website_id) + [
+ "|",
+ ("url", "=", menu["url"]),
+ ("url", "=", "/" + menu["url"]),
+ ]
+ page = self.env["website.page"].search(domain, limit=1)
if page:
menu['page_id'] = page.id
menu['url'] = page.url
diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js
index d5a2997031d..fcdc8c5ad3b 100644
--- a/addons/website/static/src/js/widgets/theme.js
+++ b/addons/website/static/src/js/widgets/theme.js
@@ -547,6 +547,34 @@ var ThemeCustomizeDialog = Dialog.extend({
_setActive: function () {
var self = this;
+ // First enforce that all input groups have only one element checked as
+ // it is supposed to be (it might not be the case on initialization, for
+ // exemple if we had data-xmlid="A" and data-xmlid="A,B" and if A and B
+ // are active, the 2 related inputs would be checked).
+ var $radioXMLInputs = this.$inputs.filter('[type="radio"][data-xmlid]');
+ var optionNames = _.uniq(_.map($radioXMLInputs, function (option) {
+ return option.name;
+ }));
+ _.each(optionNames, function (optionName) {
+ var $inputs = $radioXMLInputs.filter('[name="' + optionName + '"]:checked');
+ if ($inputs.length > 1) {
+ $inputs.prop('checked', false);
+
+ var maxNbXMLIDs = -1;
+ var $maxInput = null;
+ _.each($inputs, function (input) {
+ var $input = $(input);
+ var xmlID = $input.data('xmlid');
+ var nbXMLIDs = xmlID ? xmlID.split(',').length : 0;
+ if (nbXMLIDs >= maxNbXMLIDs) {
+ maxNbXMLIDs = nbXMLIDs;
+ $maxInput = $input;
+ }
+ });
+ $maxInput.prop('checked', true);
+ }
+ });
+
// Look at all options to see if they are enabled or disabled
var $enable = this.$inputs.filter(':checked');
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 5fb20b332c8..1c9792b91ab 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -534,8 +534,8 @@ table.table_desc tr td {
// Default
&.s_carousel_default {
// Controls - chevron
- .carousel-control-prev-icon:before { content: "\f053"; margin-left: 1.5rem; }
- .carousel-control-next-icon:before { content: "\f054"; margin-right: 1.5rem; }
+ .carousel-control-prev-icon:before { content: "\f053" #{"/*rtl:'\f054'*/"}; margin-left: 1.5rem; }
+ .carousel-control-next-icon:before { content: "\f054" #{"/*rtl:'\f053'*/"}; margin-right: 1.5rem; }
.carousel-control-prev-icon:before,
.carousel-control-next-icon:before {
background-color: rgba(0,0,0,0);
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index b8262bb11b8..38b3311ecbd 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -811,6 +811,43 @@ class TestCowViewSaving(common.TransactionCase):
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
+ def test_module_new_inherit_view_on_parent_already_forked_xpath_replace(self):
+ """ Deeper, more specific test of above behavior.
+ A module install should add/update the COW view (if allowed fields,
+ eg not modified or prohibited (website_id, inherit_id..)).
+ This test ensure it does not crash if the child view is a primary view.
+ """
+ View = self.env['ir.ui.view']
+
+ # Simulate layout views
+ base_view = View.create({
+ 'name': 'Main Frontend Layout',
+ 'type': 'qweb',
+ 'arch': ' ',
+ 'key': '_portal.frontend_layout',
+ }).with_context(load_all_views=True)
+
+ inherit_view = View.create({
+ 'name': 'Main layout',
+ 'mode': 'extension',
+ 'inherit_id': base_view.id,
+ 'arch': ' ',
+ 'key': '_website.layout',
+ })
+
+ # Trigger cow on website_sale hierarchy for website 1
+ base_view.with_context(website_id=1).write({'name': 'Main Frontend Layout (W1)'})
+
+ # Simulate website_sale_comparison install, that's the real test, it
+ # should not crash.
+ View._load_records([dict(xml_id='_website_forum.layout', values={
+ 'name': 'Forum Layout',
+ 'mode': 'primary',
+ 'inherit_id': inherit_view.id,
+ 'arch': ' ',
+ 'key': '_website_forum.layout',
+ })])
+
def test_multiple_inherit_level(self):
""" Test multi-level inheritance:
Base
diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
index 822678fa8d7..ff18d1f7bb5 100644
--- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
+++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
@@ -285,7 +285,7 @@
Description
-
+