From 1e21b79febaec0198899d5dfe38c3f2d54fb163b Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Thu, 17 May 2018 14:41:24 -0700 Subject: [PATCH 1/9] [FIX] website_quote: install sale_payment together with website_quote Stable version of 65a589eb54a1421baa71074701bea2873a83c75f (reverted at 935043dda123a030130571a2a4bb45b2b13f145c) that doesn't cause the following errors: odoo.modules.graph: module website_quote: Unmet dependencies: sale_payment on existing databases without sale_payment installed. A downside of this approach is that sale_payment can be uninstalled without uninstalling website_quote. Adding an uninstall_hook to sale_payment could solve that, but it wouldn't warn the user website_quote would be removed in the "Uninstall module" wizard. Keeping in mind this is only a temporary hack for stable, keeping sale_payment installed in this case shouldn't cause any issues. opw-1847977 --- addons/website_quote/__init__.py | 9 +++++++++ addons/website_quote/__manifest__.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/addons/website_quote/__init__.py b/addons/website_quote/__init__.py index 7d34c7c054a..ea016ede098 100644 --- a/addons/website_quote/__init__.py +++ b/addons/website_quote/__init__.py @@ -3,3 +3,12 @@ from . import controllers from . import models + +from odoo.api import Environment, SUPERUSER_ID + +def _install_sale_payment(cr, registry): + env = Environment(cr, SUPERUSER_ID, {}) + env['ir.module.module'].search([ + ('name', '=', 'sale_payment'), + ('state', '=', 'uninstalled'), + ]).button_install() diff --git a/addons/website_quote/__manifest__.py b/addons/website_quote/__manifest__.py index e6a0a1c274f..74d1de6dfcd 100644 --- a/addons/website_quote/__manifest__.py +++ b/addons/website_quote/__manifest__.py @@ -24,4 +24,8 @@ ], 'qweb': ['static/src/xml/*.xml'], 'installable': True, + + # needed because dependencies can't be changed in a stable version + # TODO in master: add sale_payment to depends and remove this + 'post_init_hook': '_install_sale_payment', } From a3ab33f2121c487920d370675d0e1a32dfc96c65 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 24 May 2018 09:46:36 +0200 Subject: [PATCH 2/9] [FIX] mass_mailing: unsubscribe not working in multi lang When installing the website with a lang different than the one set on the user, the button unsubscribe in the mass mailing snippets didn't work because the unsubscribe link contains the code of the language. The function send_get_email_dict in model mail.mail didn't expect this behavior and so couldn't set the right unsubscribe link in the mail. opw:1850696 --- addons/mass_mailing/models/mail_mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index f4611a3250c..f7ef42ff223 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -99,9 +99,9 @@ class MailMail(osv.Model): emails = tools.email_split(res.get('email_to')[0]) email_to = emails and emails[0] or False unsubscribe_url= self._get_unsubscribe_url(cr, uid, mail, email_to, context=context) - link_to_replace = base_url+'/unsubscribe_from_list' + regex_link_to_replace = re.escape(base_url) + '(/[a-z]{2}_[A-Z]{2})?' + '/unsubscribe_from_list' if link_to_replace in res['body']: - res['body'] = res['body'].replace(link_to_replace, unsubscribe_url if unsubscribe_url else '#') + res['body'] = res['body'].replace(regex_link_to_replace, unsubscribe_url if unsubscribe_url else '#') return res def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True): From 4c08e2a945fdc15ec1c8742062e2d626c767a188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Rahir?= Date: Wed, 23 May 2018 10:32:54 +0200 Subject: [PATCH 3/9] [FIX] iap: install web_settings_dashboard along with iap --- addons/iap/__init__.py | 9 +++++++++ addons/iap/__manifest__.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/addons/iap/__init__.py b/addons/iap/__init__.py index 01b7537fde3..612f61df949 100644 --- a/addons/iap/__init__.py +++ b/addons/iap/__init__.py @@ -3,3 +3,12 @@ from . import models from .models.iap import jsonrpc, charge, InsufficientCreditError + +from odoo.api import Environment, SUPERUSER_ID + +def _install_web_settings_dashboard(cr, registry): + env = Environment(cr, SUPERUSER_ID, {}) + env['ir.module.module'].search([ + ('name', '=', 'web_settings_dashboard'), + ('state', '=', 'uninstalled'), + ]).button_install() diff --git a/addons/iap/__manifest__.py b/addons/iap/__manifest__.py index 35124df76eb..65647061088 100644 --- a/addons/iap/__manifest__.py +++ b/addons/iap/__manifest__.py @@ -20,4 +20,7 @@ This module provides standard tools (account model, context manager and helpers) 'static/src/xml/iap_templates.xml', ], 'auto_install': True, + # needed because dependencies can't be changed in a stable version + # TODO in master: add web_settings_dashboard to depends and remove this + 'post_init_hook': '_install_web_settings_dashboard', } From 28179bc8cb9d98a60936634e50ca733beec0393f Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 24 May 2018 13:13:47 +0200 Subject: [PATCH 4/9] [IMP] base: Add support for private addresses This is a backport of commit https://github.com/odoo/odoo/commit/2f15a5fa647d55df36c9019df467802a3aa9b4e3 Purpose ======= Add the possibility to create private addresses, only accessible for a subset of users. Specification ============= - Add a new 'Private' partner type - Add a res.groups in base 'Access to Private Addresses' - Add ir.rules for the following behavior: - Every employees/internal users can read non-private addresses - Only users in group_private_addresses can access private addresses - Add in base a simplified form view for private addresses The following points won't be backported: - A HR Officer is automatically granted in group_private_addresses - Use the simplified form view to open the address_home_id form on employees That's because it requires to update 'base' to make it work. If a user only update 'hr', this will break his instance while 'base' isn't updated. But these modifications can be applied manually quite easily. --- openerp/addons/base/res/res_partner.py | 3 +- openerp/addons/base/res/res_partner_view.xml | 44 +++++++++++++++++++ .../addons/base/security/base_security.xml | 35 +++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 4995c13b975..5214d9384ec 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -212,7 +212,8 @@ class res_partner(osv.Model, format_address): [('contact', 'Contact'), ('invoice', 'Invoice address'), ('delivery', 'Shipping address'), - ('other', 'Other address')], 'Address Type', + ('other', 'Other address'), + ("private", "Private Address")], string='Address Type', help="Used to select automatically the right address according to the context in sales and purchases documents."), 'street': fields.char('Street'), 'street2': fields.char('Street2'), diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index e7e51b2f54b..201e0c8964a 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -330,6 +330,50 @@ + + + res.partner.view.form.private + res.partner + + +
+ + + +
+
+
+ res.partner.select res.partner diff --git a/openerp/addons/base/security/base_security.xml b/openerp/addons/base/security/base_security.xml index e3de88cf30b..aab7d358f8d 100644 --- a/openerp/addons/base/security/base_security.xml +++ b/openerp/addons/base/security/base_security.xml @@ -59,6 +59,10 @@ Contact Creation + + Access to Private Addresses + + + + res.partner.rule.private.employee + + + [('type', '!=', 'private')] + + + + + + + + + + res.partner.rule.private.group + + + [('type', '=', 'private')] + + + + + + + + From 919a1af93601f3873b78c755575cf0486da24e4e Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Thu, 24 May 2018 14:10:50 +0200 Subject: [PATCH 5/9] [FIX] purchase: correct product cost in reporting product.product inheritS from product.template, and they both define the 'standard_price' field, but implement it differently; - product: the field is a company dependent one (so non stored) - template: the field is a computed one based on tis variants For the first case, since the field is not stored in database, when doing SQL query, we have to get the value from the table ir_property. That is what purchase report does, but instead of searching on resource 'product.product', it does it on 'product.template'. There are obviously no entries in ir_property table for 'standard_price' field on product template. As consequence, the "product value" (cost) is always null in purchase reporting. This commit fixes that by modifying SQL query to get the good value from ir_property table. --- addons/purchase/report/purchase_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/report/purchase_report.py b/addons/purchase/report/purchase_report.py index a1a6cb4e3b2..b5ef4110c31 100644 --- a/addons/purchase/report/purchase_report.py +++ b/addons/purchase/report/purchase_report.py @@ -82,7 +82,7 @@ class purchase_report(osv.osv): join res_partner partner on s.partner_id = partner.id left join product_product p on (l.product_id=p.id) left join product_template t on (p.product_tmpl_id=t.id) - LEFT JOIN ir_property ip ON (ip.name='standard_price' AND ip.res_id=CONCAT('product.template,',t.id) AND ip.company_id=s.company_id) + LEFT JOIN ir_property ip ON (ip.name='standard_price' AND ip.res_id=CONCAT('product.product,',p.id) AND ip.company_id=s.company_id) left join product_uom u on (u.id=l.product_uom) left join product_uom u2 on (u2.id=t.uom_id) left join stock_picking_type spt on (spt.id=s.picking_type_id) From f9ce42c763389573ed6a19bbb1a4a9784d82a1bb Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 24 May 2018 15:24:17 +0200 Subject: [PATCH 6/9] [FIX] sale_timesheet: ZeroDivisionError To avoid a division by zero. Courtesy of @tbe-odoo opw:1850023 --- addons/sale_timesheet/controllers/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py index f1c72039eb4..d4c82e5ed10 100644 --- a/addons/sale_timesheet/controllers/main.py +++ b/addons/sale_timesheet/controllers/main.py @@ -56,8 +56,8 @@ class SaleTimesheetController(http.Controller): dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding) dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding) # rates - dashboard_values['rates'][billable_type] = round(data.get('unit_amount') / dashboard_total_hours * 100, 2) - dashboard_values['rates']['total'] += round(data.get('unit_amount') / dashboard_total_hours * 100, 2) + dashboard_values['rates'][billable_type] = dashboard_total_hours and round(data.get('unit_amount') / dashboard_total_hours * 100, 2) or 0 + dashboard_values['rates']['total'] += dashboard_total_hours and round(data.get('unit_amount') / dashboard_total_hours * 100, 2) or 0 # money_amount so_lines = values['timesheet_lines'].mapped('so_line') From 6e1b660f9c76a412dcd78d7f7de593ae318c00bd Mon Sep 17 00:00:00 2001 From: LofbergValentin Date: Thu, 24 May 2018 16:00:11 +0200 Subject: [PATCH 7/9] [FIX] hr_attendance: Keep the kiosk session alive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #1800442 Purpose ======= Odoo sessions are expired when no action has been triggered for the last 7 days. For kiosk mode, this doesn’t make sense. It means that once a week, a person with the rights to the employee under kiosk mode, has to come to the screen and log in again. Specification ============= Trigger an action to keep the Odoo session alive. --- addons/hr_attendance/static/src/js/kiosk_mode.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/addons/hr_attendance/static/src/js/kiosk_mode.js b/addons/hr_attendance/static/src/js/kiosk_mode.js index 51c78bd34b4..9a7e2898df1 100644 --- a/addons/hr_attendance/static/src/js/kiosk_mode.js +++ b/addons/hr_attendance/static/src/js/kiosk_mode.js @@ -36,6 +36,8 @@ var KioskMode = Widget.extend(BarcodeHandlerMixin, { self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self})); self.start_clock(); }); + // Make a RPC call every day to keep the session alive + self._interval = window.setInterval(this._callServer.bind(this), (60*60*1000*24)); return self._super.apply(this, arguments); }, @@ -60,8 +62,15 @@ var KioskMode = Widget.extend(BarcodeHandlerMixin, { destroy: function () { clearInterval(this.clock_start); + clearInterval(this._interval); this._super.apply(this, arguments); }, + + _callServer: function () { + // Make a call to the database to avoid the auto close of the session + return Session.rpc('/web/webclient/version_info', {}) + }, + }); core.action_registry.add('hr_attendance_kiosk_mode', KioskMode); From 34f567dd397a016e526c3923024ee1cf9234ca5d Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 24 May 2018 16:50:09 +0200 Subject: [PATCH 8/9] Revert "[FIX] mass_mailing: unsubscribe not working in multi lang" commit a3ab33f2121c487920d370675d0e1a32dfc96c65 introduced invalid code --- addons/mass_mailing/models/mail_mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index f7ef42ff223..f4611a3250c 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -99,9 +99,9 @@ class MailMail(osv.Model): emails = tools.email_split(res.get('email_to')[0]) email_to = emails and emails[0] or False unsubscribe_url= self._get_unsubscribe_url(cr, uid, mail, email_to, context=context) - regex_link_to_replace = re.escape(base_url) + '(/[a-z]{2}_[A-Z]{2})?' + '/unsubscribe_from_list' + link_to_replace = base_url+'/unsubscribe_from_list' if link_to_replace in res['body']: - res['body'] = res['body'].replace(regex_link_to_replace, unsubscribe_url if unsubscribe_url else '#') + res['body'] = res['body'].replace(link_to_replace, unsubscribe_url if unsubscribe_url else '#') return res def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True): From 04ba755f82c237f825cfdf331969e398a148b83b Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 24 May 2018 16:30:04 +0200 Subject: [PATCH 9/9] [FIX] web: AbstractController: handle concurrent reloads Let's assume the following scenario in a Kanban view with a default filter. The user removes the filter, and quickly adds a new one. On a slow network, it may happen that the second reload request returns before the first one. On odoo.com, this is easy to reproduce on the tasks Kanban view. Before this rev., when this occured, the result of the first request was displayed, whereas it should have been ignored as another request was done later on. --- .../src/js/views/abstract_controller.js | 8 +++- addons/web/static/tests/views/list_tests.js | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 6a3f9a3bbaa..2391fb1fa8c 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) { * reading localstorage, ...) has to go through the controller. */ +var concurrency = require('web.concurrency'); var Widget = require('web.Widget'); @@ -41,6 +42,9 @@ var AbstractController = Widget.extend({ this.handle = params.handle; this.activeActions = params.activeActions; this.initialState = params.initialState; + + // use a DropPrevious to correctly handle concurrent updates + this.dp = new concurrency.DropPrevious(); }, /** * Simply renders and updates the url. @@ -206,11 +210,11 @@ var AbstractController = Widget.extend({ var self = this; var shouldReload = (options && 'reload' in options) ? options.reload : true; var def = shouldReload ? this.model.reload(this.handle, params) : $.when(); - return def.then(function (handle) { + return this.dp.add(def).then(function (handle) { self.handle = handle || self.handle; // update handle if we reloaded var state = self.model.get(self.handle); var localState = self.renderer.getLocalState(); - return self.renderer.updateState(state, params).then(function () { + return self.dp.add(self.renderer.updateState(state, params)).then(function () { self.renderer.setLocalState(localState); self._update(state); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index f1c36aac0a7..f655151f995 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3310,7 +3310,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('basic support for widgets', function (assert) { assert.expect(1); @@ -3416,6 +3415,46 @@ QUnit.module('Views', { testUtils.unpatch(mixins.ParentedMixin); }); + QUnit.test('concurrent reloads finishing in inverse order', function (assert) { + assert.expect(3); + + var blockSearchRead = false; + var def = $.Deferred(); + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + mockRPC: function (route) { + var result = this._super.apply(this, arguments); + if (route === '/web/dataset/search_read' && blockSearchRead) { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4, + "list view should contain 4 records"); + + // reload with a domain (this request is blocked) + blockSearchRead = true; + list.reload({domain: [['foo', '=', 'yop']]}); + + assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4, + "list view should still contain 4 records (search_read being blocked)"); + + // reload without the domain + blockSearchRead = false; + list.reload({domain: []}); + + // unblock the RPC + def.resolve(); + assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4, + "list view should still contain 4 records"); + + list.destroy(); + }); }); });