diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index b97443b410f..d9969a3e205 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -537,7 +537,6 @@
-
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 26b1159bfb5..cf5b5cc4872 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -270,7 +270,7 @@ class StockRule(models.Model):
'partner_id': partner.id,
'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.user.company_id.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 24dfedf38a6..312b0bc4c0a 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1031,7 +1031,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 cd115e5bbab..b5dcb29f475 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 b6176f1d764..357a2773064 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -463,12 +463,11 @@
-
+
diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py
index 3eca680cce7..f5116ebf7e6 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):
@@ -64,6 +65,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 87c63386ca7..7a377af6abb 100644
--- a/addons/stock/security/stock_security.xml
+++ b/addons/stock/security/stock_security.xml
@@ -94,7 +94,7 @@
stock_move multi-company
- ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
+ ['|','|',('company_id','=',False),('company_id','child_of',[user.company_id.id]),('location_dest_id.company_id', '=', False)]
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index 3aea1854604..a4a4642eb7a 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -180,6 +180,7 @@
product.product.procurement
product.product
+
@@ -266,6 +267,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/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 8a60f15b388..a80047ee874 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -488,7 +488,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
- with self.assertQueryCount(__system__=61, emp=76): # com runbot: 61 - 76 // test_mail only: 60 - 72
+ with self.assertQueryCount(__system__=62, emp=76): # com runbot: 61 - 76 // test_mail only: 60 - 72
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)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 6d382c92333..7dfee54294b 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -198,11 +198,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 e576e094e30..c7624bd83a4 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2536,6 +2536,7 @@ var JournalDashboardGraph = AbstractField.extend({
* Called when the field is detached from the DOM.
*/
on_detach_callback: function () {
+ this.chart.tooltip.hidden(true);
this._isInDOM = false;
},
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 1e6bb9af0f0..46b89d2a859 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -795,6 +795,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/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index ee081aa3c5b..a0c7b9c1836 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -561,7 +561,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.debug) {
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 98baf231966..f103d2923fc 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -31,6 +31,9 @@
&:not(.o_column_sortable):hover {
cursor: default;
}
+ &.o_list_number_th {
+ text-align: right;
+ }
}
}
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 946ed9fe7f8..e81b6ebaf35 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 a5e0731e4b0..6d774b65073 100644
--- a/addons/website/static/src/js/widgets/theme.js
+++ b/addons/website/static/src/js/widgets/theme.js
@@ -529,6 +529,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 2aec4f03251..5373d9ec304 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 9e060b8d04e..b5c91d533e4 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
@@ -283,7 +283,7 @@
-
+
|