diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index b4972946e7c..8d938a79d22 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -265,12 +265,12 @@ class AccountBankStatement(models.Model): """ Changes statement state to Running.""" for statement in self: if not statement.name: - context = {'ir_sequence_date', statement.date} + context = {'ir_sequence_date': statement.date} if statement.journal_id.sequence_id: - st_number = statement.journal_id.sequence_id.with_context(context).next_by_id() + st_number = statement.journal_id.sequence_id.with_context(**context).next_by_id() else: SequenceObj = self.env['ir.sequence'] - st_number = SequenceObj.with_context(context).next_by_code('account.bank.statement') + st_number = SequenceObj.with_context(**context).next_by_code('account.bank.statement') statement.name = st_number statement.state = 'open' diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index eb458812a57..68e45762593 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -181,6 +181,8 @@ var DashBoard = form_common.FormWidget.extend({ action = result, view_mode = action_attrs.view_mode; + if (!action) { return; } + // evaluate action_attrs context and domain action_attrs.context_string = action_attrs.context; action_attrs.context = pyeval.eval( diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 4381da63995..b213c7770a1 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -20,6 +20,8 @@ _logger = logging.getLogger(__name__) def format_date(env, date, pattern=False): + if not date: + return '' date = datetime.datetime.strptime(date[:10], tools.DEFAULT_SERVER_DATE_FORMAT) lang_code = env.context.get('lang') or 'en_US' if not pattern: diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 9589bab1278..d94b263449e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1368,9 +1368,9 @@ class MailThread(models.AbstractModel): to_remove.append(node) if node.tag == 'img' and node.get('src', '').startswith('cid:'): cid = node.get('src').split(':', 1)[1] - related_attachment = [attach for attach in attachments if len(attach) == 2 and attach[2] == cid] + related_attachment = [attach for attach in attachments if attach[2] and attach[2].get('cid') == cid] if related_attachment: - node.set('data-filename', related_attachment[0]) + node.set('data-filename', related_attachment[0][0]) postprocessed = True for node in to_remove: diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index 2c30a82701e..3bc730d15e8 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -59,8 +59,9 @@ function _parse_and_transform(nodes, transform_function) { }).join(""); } -// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611) -var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi; +// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url +// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match. +var url_regexp = /\b(?:https?:\/\/|(www\.))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi; function linkify(text, attrs) { attrs = attrs || {}; if (attrs.target === undefined) { diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index b8513df98bc..40a182fe6bd 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -10,3 +10,4 @@ access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base access_mail_mail_statistics_user,mail.mail.statistics,model_mail_mail_statistics,base.group_user,1,1,1,1 access_mail_mail_statistics_mass_mailing_user,mail.mail.statistics,model_mail_mail_statistics,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mail_statistics_report,mail.statistics.report,model_mail_statistics_report,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_utm_source,access_utm_source,utm.model_utm_source,mass_mailing.group_mass_mailing_user,1,1,1,0 \ No newline at end of file diff --git a/addons/pos_mercury/static/src/xml/pos_mercury.xml b/addons/pos_mercury/static/src/xml/pos_mercury.xml index 02f25588a3c..acf830e2543 100644 --- a/addons/pos_mercury/static/src/xml/pos_mercury.xml +++ b/addons/pos_mercury/static/src/xml/pos_mercury.xml @@ -25,16 +25,16 @@ - this.removeAttr("class"); - this.attr('t-attf-class', 'paymentline selected #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + this.removeAttr('class'); + this.attr('t-attf-class', 'paymentline selected #{line.mercury_swipe_pending ? \'o_pos_mercury_swipe_pending\' : \'\'}'); - this.removeAttr("class"); - this.attr('t-attf-class', 'paymentline #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + this.removeAttr('class'); + this.attr('t-attf-class', 'paymentline #{line.mercury_swipe_pending ? \'o_pos_mercury_swipe_pending\' : \'\'}'); - this.removeAttr("class"); - this.attr('t-attf-class', 'col-tendered edit #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + this.removeAttr('class'); + this.attr('t-attf-class', 'col-tendered edit #{line.mercury_swipe_pending ? \'o_pos_mercury_swipe_pending\' : \'\'}'); diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 597e8aed1f6..dad6be47932 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -112,8 +112,9 @@ class SaleOrderLine(models.Model): def _compute_qty_delivered_updateable(self): for line in self: if line.product_id.type not in ('consu', 'product'): - return super(SaleOrderLine, self)._compute_qty_delivered_updateable() - line.qty_delivered_updateable = False + super(SaleOrderLine, line)._compute_qty_delivered_updateable() + else: + line.qty_delivered_updateable = False @api.onchange('product_id') def _onchange_product_id_set_customer_lead(self): diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index 2dd5e80def4..0330c5d95cb 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -639,11 +639,11 @@ var FormView = View.extend(common.FieldManagerMixin, { } }, disable_button: function () { - this.$('.oe_form_buttons').add(this.$buttons).find('button').addClass('o_disabled').prop('disabled', true); + this.$('.oe_form_buttons,.o_statusbar_buttons').add(this.$buttons).find('button').addClass('o_disabled').prop('disabled', true); this.is_disabled = true; }, enable_button: function () { - this.$('.oe_form_buttons').add(this.$buttons).find('button.o_disabled').removeClass('o_disabled').prop('disabled', false); + this.$('.oe_form_buttons,.o_statusbar_buttons').add(this.$buttons).find('button.o_disabled').removeClass('o_disabled').prop('disabled', false); this.is_disabled = false; }, on_button_save: function() { @@ -658,8 +658,10 @@ var FormView = View.extend(common.FieldManagerMixin, { self.to_view_mode(); core.bus.trigger('do_reload_needaction'); core.bus.trigger('form_view_saved', self); + }).always(function() { + self.enable_button(); }); - }).always(function(){ + }).fail(function(){ self.enable_button(); }); }, diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 5e0f8b1e7a2..0ef57e418ff 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -38,7 +38,7 @@ class WebsiteEventTrackController(http.Controller): for loc in locations.keys(): if locations[loc] and (locations[loc][-1][2] > start_date): locations[loc][-1][3] += 1 - elif not locations[loc] or locations[loc][-1][2] < start_date: + elif not locations[loc] or locations[loc][-1][2] <= start_date: locations[loc].append([False, locations[loc] and locations[loc][-1][2] or dates[0][0], start_date, 1]) dates[-1][1][loc] = locations[loc][-1] forcetr = not bool(location) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 52ffbccc303..03bad9cce6d 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -306,6 +306,13 @@ actual arch. if view.type == 'qweb' and view.groups_id: raise ValidationError(_("Qweb view cannot have 'Groups' define on the record. Use 'groups' attributes inside the view definition")) + @api.constrains('inherit_id') + def _check_000_inheritance(self): + # NOTE: constraints methods are check alphabetically. Always ensure this method will be + # called before other constraint metheods to avoid infinite loop in `read_combined`. + if not self._check_recursion(parent='inherit_id'): + raise ValidationError(_('You cannot create recursive inherited views.')) + _sql_constraints = [ ('inheritance_mode', "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)", diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 7790f2d11db..5d6608457f2 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -8,6 +8,7 @@ from lxml import etree from lxml.builder import E from psycopg2 import IntegrityError +from odoo.exceptions import ValidationError from odoo.tests import common from odoo.tools import mute_logger @@ -231,6 +232,24 @@ class TestViewInheritance(ViewCase): self.assertFalse(self.View.default_view(model='does.not.exist', view_type='form')) self.assertFalse(self.View.default_view(model=self.model, view_type='graph')) + def test_no_recursion(self): + r1 = self.makeView('R1') + with self.assertRaises(ValidationError), self.cr.savepoint(): + r1.write({'inherit_id': r1.id}) + + r2 = self.makeView('R2', r1.id) + r3 = self.makeView('R3', r2.id) + with self.assertRaises(ValidationError), self.cr.savepoint(): + r2.write({'inherit_id': r3.id}) + + with self.assertRaises(ValidationError), self.cr.savepoint(): + r1.write({'inherit_id': r3.id}) + + with self.assertRaises(ValidationError), self.cr.savepoint(): + r1.write({ + 'inherit_id': r1.id, + 'arch': self.arch_for('itself', parent=True), + }) class TestApplyInheritanceSpecs(ViewCase): """ Applies a sequence of inheritance specification nodes to a base @@ -1249,4 +1268,4 @@ class TestQWebRender(ViewCase): content1 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id]).render('base.dummy') content3 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id]).render('base.dummy_primary_ext') - self.assertNotEqual(content1, content3) \ No newline at end of file + self.assertNotEqual(content1, content3)