diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 66dc46d7c3d..c86747b194f 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -43,6 +43,12 @@ def migrate_tags_on_taxes(cr, registry): if len(tax_id.ids) == 1: tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]}) +def preserve_existing_tags_on_taxes(cr, registry, module): + ''' This is a utility function used to preserve existing previous tags during upgrade of the module.''' + env = api.Environment(cr, SUPERUSER_ID, {}) + xml_records = env['ir.model.data'].search([('model', '=', 'account.account.tag'), ('module', 'like', module)]) + if xml_records: + cr.execute("update ir_model_data set noupdate = 't' where id in %s", [tuple(xml_records.ids)]) # --------------------------------------------------------------- # Account Templates: Account, Tax, Tax Code and chart. + Wizard diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 2a4432ace9e..8d4119659f1 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -478,6 +478,8 @@ class AccountAssetDepreciationLine(models.Model): created_moves = self.env['account.move'] prec = self.env['decimal.precision'].precision_get('Account') for line in self: + if line.move_id: + raise UserError(_('This depreciation is already linked to a journal entry! Please post or delete it.')) category_id = line.asset_id.category_id depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id diff --git a/addons/crm/wizard/base_partner_merge_views.xml b/addons/crm/wizard/base_partner_merge_views.xml index 0918d6ac3a4..259df710ee3 100644 --- a/addons/crm/wizard/base_partner_merge_views.xml +++ b/addons/crm/wizard/base_partner_merge_views.xml @@ -56,7 +56,7 @@ You can remove contacts from this list to avoid merging them.

- + diff --git a/addons/l10n_fr/__init__.py b/addons/l10n_fr/__init__.py index 422c6ad09a7..779ab9a056c 100644 --- a/addons/l10n_fr/__init__.py +++ b/addons/l10n_fr/__init__.py @@ -4,3 +4,8 @@ # Copyright (c) 2008 JAILLET Simon - CrysaLEAD - www.crysalead.fr from . import models + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_fr') diff --git a/addons/l10n_fr/__manifest__.py b/addons/l10n_fr/__manifest__.py index 3c8fc8c637c..25c75910a00 100644 --- a/addons/l10n_fr/__manifest__.py +++ b/addons/l10n_fr/__manifest__.py @@ -45,4 +45,5 @@ configuration of their taxes and fiscal positions manually. 'data/account_reconcile_model_template.xml', 'data/account_chart_template_data.yml', ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_lu/__init__.py b/addons/l10n_lu/__init__.py index 67dee8c60db..4ca483f7aff 100644 --- a/addons/l10n_lu/__init__.py +++ b/addons/l10n_lu/__init__.py @@ -1,2 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_lu') diff --git a/addons/l10n_lu/__manifest__.py b/addons/l10n_lu/__manifest__.py index f7f40c5848a..9fe7e6dfcc9 100644 --- a/addons/l10n_lu/__manifest__.py +++ b/addons/l10n_lu/__manifest__.py @@ -44,4 +44,5 @@ Notes: 'data/account.chart.template.csv', 'data/account_chart_template_data.yml', ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_no/__init__.py b/addons/l10n_no/__init__.py index 67dee8c60db..52c150be0f0 100644 --- a/addons/l10n_no/__init__.py +++ b/addons/l10n_no/__init__.py @@ -1,2 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_no') diff --git a/addons/l10n_no/__manifest__.py b/addons/l10n_no/__manifest__.py index ba4208b6b3c..670c1bf55d6 100644 --- a/addons/l10n_no/__manifest__.py +++ b/addons/l10n_no/__manifest__.py @@ -20,4 +20,5 @@ Updated for Odoo 9 by Bringsvor Consulting AS 'data/account_tax_data.xml', 'data/account_chart_template_data.yml'], "active": False, + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_pl/__init__.py b/addons/l10n_pl/__init__.py index 67dee8c60db..6575c176947 100644 --- a/addons/l10n_pl/__init__.py +++ b/addons/l10n_pl/__init__.py @@ -1,2 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_pl') diff --git a/addons/l10n_pl/__manifest__.py b/addons/l10n_pl/__manifest__.py index dab10c2a1f3..da203009525 100644 --- a/addons/l10n_pl/__manifest__.py +++ b/addons/l10n_pl/__manifest__.py @@ -33,4 +33,5 @@ Wewnętrzny numer wersji OpenGLOBE 1.02 'data/res_country_state_data.xml', 'data/account_chart_template_data.yml' ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_sg/__init__.py b/addons/l10n_sg/__init__.py index b286495ed4a..e2f7da260d0 100644 --- a/addons/l10n_sg/__init__.py +++ b/addons/l10n_sg/__init__.py @@ -2,3 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. # Copyright (C) 2014 Tech Receptives (). + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_sg') diff --git a/addons/l10n_sg/__manifest__.py b/addons/l10n_sg/__manifest__.py index 2df4ef0e928..d05a6c0958e 100644 --- a/addons/l10n_sg/__manifest__.py +++ b/addons/l10n_sg/__manifest__.py @@ -33,4 +33,5 @@ After installing this module, the Configuration wizard for accounting is launche 'data/account_tax_data.xml', 'data/account_chart_template_data.yml', ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_th/__init__.py b/addons/l10n_th/__init__.py index 67dee8c60db..1b8d7518cd3 100644 --- a/addons/l10n_th/__init__.py +++ b/addons/l10n_th/__init__.py @@ -1,2 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_th') diff --git a/addons/l10n_th/__manifest__.py b/addons/l10n_th/__manifest__.py index 548cc9dd626..e283dcfaace 100644 --- a/addons/l10n_th/__manifest__.py +++ b/addons/l10n_th/__manifest__.py @@ -19,4 +19,5 @@ Thai accounting chart and localization. 'data/l10n_th_chart_data.xml', 'data/account_chart_template_data.yml', ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/l10n_vn/__init__.py b/addons/l10n_vn/__init__.py index 34293fdfe8d..490754ae534 100644 --- a/addons/l10n_vn/__init__.py +++ b/addons/l10n_vn/__init__.py @@ -2,3 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. # This module is Copyright (c) 2009-2013 General Solutions (http://gscom.vn) All Rights Reserved. + + +def _preserve_tag_on_taxes(cr, registry): + from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes + preserve_existing_tags_on_taxes(cr, registry, 'l10n_vn') diff --git a/addons/l10n_vn/__manifest__.py b/addons/l10n_vn/__manifest__.py index c945d4726fa..7668c3c8dbe 100644 --- a/addons/l10n_vn/__manifest__.py +++ b/addons/l10n_vn/__manifest__.py @@ -28,5 +28,6 @@ with Chart of account under Circular No. 200/2014/TT-BTC 'data/account_data.xml', 'data/account_tax_data.xml', 'data/account_chart_template_data.yml', - ] + ], + 'post_init_hook': '_preserve_tag_on_taxes', } diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index 17adf4858bb..c94536371e0 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -18,8 +18,7 @@
Product:
- - +
Quantity:
diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 92c1316fcb7..88ca9e8e4fa 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -8,6 +8,7 @@ var models = require('point_of_sale.models'); var core = require('web.core'); var ajax = require('web.ajax'); var CrashManager = require('web.CrashManager'); +var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; var _t = core._t; @@ -606,10 +607,9 @@ var Chrome = PosBaseWidget.extend({ $(window).off(); $('html').off(); $('body').off(); - $(this.$el).parent().off(); - $('document').off(); - $('.oe_web_client').off(); - $('.openerp_webclient_container').off(); + this.$el.parent().off(); + // The above lines removed the bindings, but we really need them for the barcode + BarcodeEvents.start(); }, build_chrome: function() { diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index caf64ca72e4..e8ec348288b 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -235,10 +235,12 @@ var PosDB = core.Class.extend({ for(var i = 0, len = partners.length; i < len; i++){ partner = partners[i]; - if ( this.partner_write_date && + var local_partner_date = (this.partner_write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z'); + var dist_partner_date = (partner.write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z'); + if ( this.partner_write_date && this.partner_by_id[partner.id] && - new Date(this.partner_write_date).getTime() + 1000 >= - new Date(partner.write_date).getTime() ) { + new Date(local_partner_date).getTime() + 1000 >= + new Date(dist_partner_date).getTime() ) { // FIXME: The write_date is stored with milisec precision in the database // but the dates we get back are only precise to the second. This means when // you read partners modified strictly after time X, you get back partners that were diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 61704ad44f8..d0aed74bc66 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -542,7 +542,7 @@ class ProductProduct(models.Model): history = self.env['product.price.history'].search([ ('company_id', '=', company_id), ('product_id', 'in', self.ids), - ('datetime', '<=', date or fields.Datetime.now())], limit=1) + ('datetime', '<=', date or fields.Datetime.now())], order='datetime desc,id desc', limit=1) return history.cost or 0.0 class ProductPackaging(models.Model): diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index c2497b7f2ad..24b35183f68 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -803,7 +803,7 @@ class SaleOrderLine(models.Model): def create(self, values): values.update(self._prepare_add_missing_fields(values)) line = super(SaleOrderLine, self).create(values) - if line.state == 'sale': + if line.order_id.state == 'sale': msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) # create an analytic account if at least an expense product diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index f2cacb3327f..ae3080b3b8e 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -534,14 +534,17 @@ var FieldMany2One = AbstractField.extend({ * start/end of the input element. Stops any navigation move event if the * user is selecting text. * - * TODO this is a duplicate of InputField._onNavigationMove, maybe this - * should be done in a mixin or, better, the m2o field should be an - * InputField (but this requires some refactoring). - * * @private * @param {OdooEvent} ev */ - _onNavigationMove: basicFields.InputField.prototype._onNavigationMove, + _onNavigationMove: function (ev) { + // TODO Maybe this should be done in a mixin or, better, the m2o field + // should be an InputField (but this requires some refactoring). + basicFields.InputField.prototype._onNavigationMove.apply(this, arguments); + if (this.mode === 'edit' && $(this.$input.autocomplete('widget')).is(':visible')) { + ev.stopPropagation(); + } + }, /** * @private * @param {OdooEvent} event diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index f21db07f088..fe98eaaeab2 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -572,6 +572,38 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2one field and list navigation', function (assert) { + assert.expect(3); + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + }); + + // edit first input, to trigger autocomplete + list.$('.o_data_row .o_data_cell').first().click(); + list.$('.o_data_cell input').val('').trigger('input'); + + // press keydown, to select first choice + list.$('.o_data_cell input').focus().trigger($.Event('keydown', { + which: $.ui.keyCode.DOWN, + keyCode: $.ui.keyCode.DOWN, + })); + + // we now check that the dropdown is open (and that the focus did not go + // to the next line) + var $dropdown = list.$('.o_field_many2one input').autocomplete('widget'); + assert.ok($dropdown.is(':visible'), "dropdown should be visible"); + assert.ok(list.$('.o_data_row:eq(0)').hasClass('o_selected_row'), + 'first data row should still be selected'); + assert.ok(!list.$('.o_data_row:eq(1)').hasClass('o_selected_row'), + 'second data row should not be selected'); + + list.destroy(); + }); + QUnit.test('standalone many2one field', function (assert) { assert.expect(3); var done = assert.async(); diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index e75627f7ee2..714c2f6342d 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -241,6 +241,7 @@ diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml index bb90835df5e..9d5bee401ba 100644 --- a/addons/website_event_track/views/event_track_templates.xml +++ b/addons/website_event_track/views/event_track_templates.xml @@ -42,6 +42,7 @@ diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 6cc12e947d4..bdcc5c9f56f 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -77,12 +77,13 @@ class Website(models.Model): :param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo) :returns: pricelist recordset """ - website = request and request.website or None + website = request and hasattr(request, 'website') and request.website or None if not website: if self.env.context.get('website_id'): website = self.browse(self.env.context['website_id']) else: - website = self.search([], limit=1) + # In the weird case we are coming from the backend (https://github.com/odoo/odoo/issues/20245) + website = len(self) == 1 and self or self.search([], limit=1) isocountry = request and request.session.geoip and request.session.geoip.get('country_code') or False partner = self.env.user.partner_id order_pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id diff --git a/addons/website_slides/views/website_slides_embed.xml b/addons/website_slides/views/website_slides_embed.xml index c3c77fc676d..72614803325 100644 --- a/addons/website_slides/views/website_slides_embed.xml +++ b/addons/website_slides/views/website_slides_embed.xml @@ -59,7 +59,7 @@
@@ -133,7 +133,7 @@
- + diff --git a/doc/reference/views.rst b/doc/reference/views.rst index c2463245998..0b570bc3ee2 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -277,7 +277,7 @@ Possible children elements of the list view are: filled, and a cross if it is not ``handle`` for ``sequence`` fields, instead of displaying the field's value - just displays a dra&drop icon + just displays a drag&drop icon ``sum``, ``avg`` displays the corresponding aggregate at the bottom of the column. The aggregation is only computed on *currently displayed* records. The @@ -1170,7 +1170,7 @@ Possible children elements of the search view are: fields don't generate domains. .. note:: the domain and context are inclusive and both are generated - if if a ``context`` is specified. To only generate context + if a ``context`` is specified. To only generate context values, set ``filter_domain`` to an empty list: ``filter_domain="[]"`` ``groups`` diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py index d28b56584e5..920929a14d2 100644 --- a/odoo/addons/base/module/module.py +++ b/odoo/addons/base/module/module.py @@ -26,6 +26,7 @@ import odoo from odoo import api, fields, models, modules, tools, _ from odoo.exceptions import AccessDenied, UserError from odoo.tools.parse_version import parse_version +from odoo.tools.misc import topological_sort from odoo.http import request _logger = logging.getLogger(__name__) @@ -844,7 +845,13 @@ class Module(models.Model): filter_lang = [lang.code for lang in langs] elif not isinstance(filter_lang, (list, tuple)): filter_lang = [filter_lang] - mod_names = [mod.name for mod in self if mod.state in ('installed', 'to install', 'to upgrade')] + + update_mods = self.filtered(lambda r: r.state in ('installed', 'to install', 'to upgrade')) + mod_dict = { + mod.name: mod.dependencies_id.mapped('name') + for mod in update_mods + } + mod_names = topological_sort(mod_dict) self.env['ir.translation'].load_module_terms(mod_names, filter_lang) @api.multi diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py index 9c7311319e8..d5460d0dd5c 100644 --- a/odoo/addons/test_impex/tests/test_export.py +++ b/odoo/addons/test_impex/tests/test_export.py @@ -305,13 +305,12 @@ class test_m2o(CreatorCase): [[False]]) def test_basic(self): - """ Exported value is the name_get of the related object + """ Exported value is empty """ record = self.env['export.integer'].create({'value': 42}) - name = dict(record.name_get())[record.id] self.assertEqual( self.export(record.id), - [[name]]) + [['']]) def test_path(self): """ Can recursively export fields of m2o via path @@ -353,14 +352,13 @@ class test_o2m(CreatorCase): def test_single(self): self.assertEqual( self.export([(0, False, {'value': 42})]), - # name_get result - [[u'export.one2many.child:42']]) + [['']]) def test_single_subfield(self): self.assertEqual( self.export([(0, False, {'value': 42})], fields=['value', 'value/value']), - [[u'export.one2many.child:42', u'42']]) + [['', u'42']]) def test_integrate_one_in_parent(self): self.assertEqual( @@ -382,9 +380,13 @@ class test_o2m(CreatorCase): def test_multiple_records_name(self): self.assertEqual( self.export(self.commands, fields=['const', 'value']), - [[ - u'4', u','.join(self.names) - ]]) + [ + [u'4', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ]) def test_multiple_records_id(self): export = self.export(self.commands, fields=['const', 'value/.id']) @@ -402,14 +404,18 @@ class test_o2m(CreatorCase): def test_multiple_records_with_name_before(self): self.assertEqual( self.export(self.commands, fields=['const', 'value', 'value/value']), - [[ # exports sub-fields of very first o2m - u'4', u','.join(self.names), u'4' - ]]) + [ + [u'4', '', u'4'], + ['', '', u'42'], + ['', '', u'36'], + ['', '', u'4'], + ['', '', u'13'], + ]) def test_multiple_records_with_name_after(self): self.assertEqual( self.export(self.commands, fields=['const', 'value/value', 'value']), - [ # completely ignores name_get request + [ [u'4', u'4', ''], ['', u'42', ''], ['', u'36', ''], @@ -460,16 +466,16 @@ class test_o2m_multiple(CreatorCase): def test_single_per_side(self): self.assertEqual( self.export(child1=False, child2=[(0, False, {'value': 42})]), - [[False, u'export.one2many.child.2:42']]) + [[False, '']]) self.assertEqual( self.export(child1=[(0, False, {'value': 43})], child2=False), - [[u'export.one2many.child.1:43', False]]) + [['', False]]) self.assertEqual( self.export(child1=[(0, False, {'value': 43})], child2=[(0, False, {'value': 42})]), - [[u'export.one2many.child.1:43', u'export.one2many.child.2:42']]) + [['', '']]) def test_single_integrate_subfield(self): fields = ['const', 'child1/value', 'child2/value'] @@ -556,14 +562,13 @@ class test_m2m(CreatorCase): def test_single(self): self.assertEqual( self.export([(0, False, {'value': 42})]), - # name_get result - [[u'export.many2many.other:42']]) + [['']]) def test_single_subfield(self): self.assertEqual( self.export([(0, False, {'value': 42})], fields=['value', 'value/value']), - [[u'export.many2many.other:42', u'42']]) + [['', u'42']]) def test_integrate_one_in_parent(self): self.assertEqual( @@ -585,10 +590,13 @@ class test_m2m(CreatorCase): def test_multiple_records_name(self): self.assertEqual( self.export(self.commands, fields=['const', 'value']), - [[ # FIXME: hardcoded comma, import uses config.csv_internal_sep - # resolution: remove configurable csv_internal_sep - u'4', u','.join(self.names) - ]]) + [ + [u'4', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ]) # essentially same as o2m, so boring diff --git a/odoo/models.py b/odoo/models.py index dea095cd4c9..7ddd58451a4 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -697,14 +697,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): for j, val in enumerate(lines2[0]): if val or isinstance(val, bool): current[j] = val - # check value of current field - if not current[i] and not isinstance(current[i], bool): - # assign xml_ids, and forget about remaining lines - xml_ids = [item[1] for item in value.name_get()] - current[i] = ','.join(xml_ids) - else: - # append the other lines at the end - lines += lines2[1:] + # append the other lines at the end + lines += lines2[1:] else: current[i] = False