diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index e3884d5c975..fe3d83bb0d2 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -270,7 +270,7 @@ class ResPartner(models.Model): RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated AND acc.company_id = %s - AND move.state == 'posted' + AND move.state = 'posted' GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand)) res = self._cr.fetchall() diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index 67de19e0ced..862d228fcdd 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -68,6 +68,28 @@ class AccountAnalyticDefault(models.Model): class AccountMoveLine(models.Model): _inherit = 'account.move.line' + @api.model + def default_get(self, fields_list): + defaults = super().default_get(fields_list) + if {'account_analytic_id', 'analytic_tag_ids'} & set(fields_list): + rec = self.env['account.analytic.default'].account_get( + product_id=self.product_id.id, + partner_id=self.move_id.commercial_partner_id.id, + user_id=self.move_id.user_id.id or self.env.uid, + date=fields.Date.today(), + company_id=self.company_id.id, + ) + if rec: + if 'account_analytic_id' in fields_list: + defaults.update({ + 'account_analytic_id': rec.analytic_id.id, + }) + if 'analytic_tag_ids' in fields_list: + defaults.update({ + 'analytic_tag_ids': rec.analytic_tag_ids.ids, + }) + return defaults + @api.onchange('product_id', 'account_id') def _onchange_product_id_account_id(self): rec = self.env['account.analytic.default'].account_get( diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 9fc3feff1bd..f266e4dcd2e 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -922,6 +922,7 @@ class Import(models.TransientModel): if dryrun: self._cr.execute('ROLLBACK TO SAVEPOINT import') # cancel all changes done to the registry/ormcache + self.pool.clear_caches() self.pool.reset_changes() else: self._cr.execute('RELEASE SAVEPOINT import') diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index f354e551212..84ac9aa1db0 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -398,7 +398,17 @@ class GoogleCalendar(models.AbstractModel): url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token()) headers = {'Content-type': 'application/json'} - data.update(recurringEventId=event_ori_google_id, originalStartTime=event_new.recurrent_id_date, sequence=self.get_sequence(instance_id)) + _originalStartTime = dict() + if event_new.allday: + _originalStartTime['date'] = event_new.recurrent_id_date.strftime("%Y-%m-%d") + else: + _originalStartTime['datetime'] = event_new.recurrent_id_date.strftime("%Y-%m-%dT%H:%M:%S.%fz") + + data.update( + recurringEventId=event_ori_google_id, + originalStartTime=_originalStartTime, + sequence=self.get_sequence(instance_id) + ) data_json = json.dumps(data) return self.env['google.service']._do_request(url, data_json, headers, type='PUT') @@ -665,8 +675,9 @@ class GoogleCalendar(models.AbstractModel): else: _logger.warning("Impossible to create event %s. [%s]", att.event_id.id, status) _logger.debug("Response : %s", response) - except: - pass + except Exception as e: + _logger.warning("Exception when updating recurrent event exclusions on google: %s", e) + return new_ids def update_events(self, lastSync=False): diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index be4b2fdcfea..f7e84bdddef 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -149,6 +149,7 @@ + @@ -156,7 +157,7 @@
- +
@@ -560,13 +561,14 @@ +
- +
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