diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-25 13:43:26 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-25 13:43:26 +0200 |
| commit | 257a382b5cb306a7603139421c336eab71d10036 (patch) | |
| tree | 8d21c5394a255ccfe716d5fb4ff525b047bbb6b4 | |
| parent | 3c8f9a19731056e9c1a8d95a39f0b8d22e69d412 (diff) | |
| parent | dd627b9698d18fe8a0bbf56e07edcc30f8af0117 (diff) | |
[MERGE] forward port branch saas-12.3 up to dd627b9698d
20 files changed, 178 insertions, 43 deletions
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 83b5b7c9afc..eb7882a40e4 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -242,7 +242,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..f5dd829caa7 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( + self.product_id.id, + self.invoice_id.commercial_partner_id.id, + self.invoice_id.user_id.id or self.env.uid, + 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 dae5b383ca7..bdfdbe41ff4 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -909,6 +909,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 bf9010f3023..bf15b66ba10 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') @@ -666,8 +676,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/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 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='invoice_sent']" position="after"> <field name="l10n_ch_isr_sent" invisible="1"/> - <field name="l10n_ch_currency_name" invisible="1"/> + <field name="l10n_ch_currency_name" invisible="1" readonly="1"/> </xpath> <xpath expr="//button[@id='account_invoice_payment_btn']" position="before"> 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 @@ <IdCodice t-esc="get_vat_number(record.commercial_partner_id.vat)"/> </IdFiscaleIVA> <CodiceFiscale t-if="not record.commercial_partner_id.vat" t-esc="record.commercial_partner_id.l10n_it_codice_fiscale"/> + <CodiceFiscale t-if="not record.commercial_partner_id.vat and not record.commercial_partner_id.l10n_it_codice_fiscale" t-esc="99999999999"/> <Anagrafica> <Denominazione t-if="record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.display_name"/> <Nome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/> 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 1119ff2164f..3061a3d13f6 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'); @@ -164,6 +165,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 edd4d9ce82c..a25aeb97395 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -590,7 +590,7 @@ class ProductProduct(models.Model): 'target': 'new'} def _prepare_sellers(self, params): - return self.seller_ids + return self.seller_ids.filtered(lambda s: s.name.active) @api.multi def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False): @@ -761,7 +761,6 @@ class SupplierInfo(models.Model): 'res.partner', 'Vendor', domain=[('supplier', '=', True)], ondelete='cascade', required=True, help="Vendor of this product") - 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 70643757781..6d6a5aebc32 100644 --- a/addons/purchase_requisition/models/purchase.py +++ b/addons/purchase_requisition/models/purchase.py @@ -117,12 +117,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/i18n/fr.po b/addons/stock/i18n/fr.po index 2a7ab6f0cdc..aec452cb978 100644 --- a/addons/stock/i18n/fr.po +++ b/addons/stock/i18n/fr.po @@ -5933,7 +5933,9 @@ msgstr "" msgid "" "The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has" " been created." -msgstr "<a href=# data-oe-model=stock.picking data-oe-id=%d></a>" +msgstr "" +msgstr "Le reliquat <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> a été" +" créé." #. module: stock #: sql_constraint:stock.location:0 diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 6ad314d0e5c..91d317f35b8 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -517,11 +517,10 @@ class ProductTemplate(models.Model): outgoing_qty = fields.Float( 'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty', digits=dp.get_precision('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 efbe443a689..4e6a2e0c5fa 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -73,8 +73,8 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='pricelist_id']" position="after"> <separator/> - <field name="location_id" context="{'location': self}"/> - <field name="warehouse_id" context="{'warehouse': self}"/> + <field name="location_id" context="{'location': self}" filter_domain="[]"/> + <field name="warehouse_id" context="{'warehouse': self}" filter_domain="[]"/> <separator/> <filter name="real_stock_available" string="Available Products" domain="[('qty_available','>',0)]"/> <filter name="real_stock_negative" string="Negative Forecasted Quantity" domain="[('virtual_available','<',0)]"/> 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 fc9cd1c7f2f..e7e7c461b76 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -386,10 +386,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 b98a973695f..a25078ec1d1 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -4063,6 +4063,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 3d66d2feb06..3a262e2dbc9 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -331,9 +331,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 dda40102904..a1f0cf9ff58 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 612265124cd..3d9bf79ad6e 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 diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py index ccc34bd3dd1..f2f11742e2e 100644 --- a/odoo/addons/base/models/ir_cron.py +++ b/odoo/addons/base/models/ir_cron.py @@ -228,7 +228,7 @@ class ir_cron(models.Model): try: registry = odoo.registry(db_name) registry[cls._name]._process_job(job_cr, job, lock_cr) - _logger.info('Job `%s` done.', job['name']) + _logger.info('Job `%s` done.', job['cron_name']) except Exception: _logger.exception('Unexpected exception while processing cron job %r', job) finally: |
