diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 567c8504637..a256605c40b 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -346,7 +346,7 @@ class AccountBankStatement(models.Model): class AccountBankStatementLine(models.Model): _name = "account.bank.statement.line" _description = "Bank Statement Line" - _order = "statement_id desc, sequence, id desc" + _order = "statement_id desc, date desc, sequence, id desc" name = fields.Char(string='Label', required=True) date = fields.Date(required=True, default=lambda self: self._context.get('date', fields.Date.context_today(self))) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 5da37518d19..dacbc7e2df1 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -951,7 +951,7 @@ class AccountInvoice(models.Model): total_currency = 0 for line in invoice_move_lines: if self.currency_id != company_currency: - currency = self.currency_id.with_context(date=self.date or self.date_invoice or fields.Date.context_today(self)) + currency = self.currency_id.with_context(date=self._get_currency_rate_date() or fields.Date.context_today(self)) if not (line.get('currency_id') and line.get('amount_currency')): line['currency_id'] = currency.id line['amount_currency'] = currency.round(line['price']) @@ -1101,7 +1101,7 @@ class AccountInvoice(models.Model): if inv.payment_term_id: totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, inv.date_invoice)[0] res_amount_currency = total_currency - ctx['date'] = inv.date or inv.date_invoice + ctx['date'] = inv._get_currency_rate_date() for i, t in enumerate(totlines): if inv.currency_id != company_currency: amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id) @@ -1300,6 +1300,9 @@ class AccountInvoice(models.Model): copy_fields = ['company_id', 'user_id', 'fiscal_position_id'] return self._get_refund_common_fields() + self._get_refund_prepare_fields() + copy_fields + def _get_currency_rate_date(self): + return self.date or self.date_invoice + @api.model def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None): """ Prepare the dict of values to create the new credit note from the invoice. @@ -1460,7 +1463,7 @@ class AccountInvoiceLine(models.Model): @api.one @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id', - 'invoice_id.date_invoice') + 'invoice_id.date_invoice', 'invoice_id.date') def _compute_price(self): currency = self.invoice_id and self.invoice_id.currency_id or None price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) @@ -1470,7 +1473,7 @@ class AccountInvoiceLine(models.Model): self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price self.price_total = taxes['total_included'] if taxes else self.price_subtotal if self.invoice_id.currency_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id: - price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id.date_invoice).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id) + price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id._get_currency_rate_date()).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id) sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1 self.price_subtotal_signed = price_subtotal_signed * sign diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 10a6c97ca05..5018b1e28e2 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -110,7 +110,7 @@ class AccountMove(models.Model): tax_cash_basis_rec_id = fields.Many2one( 'account.partial.reconcile', string='Tax Cash Basis Entry of', - help="Technical field used to keep track of the tax cash basis reconciliation." + help="Technical field used to keep track of the tax cash basis reconciliation. " "This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.") auto_reverse = fields.Boolean(string='Reverse Automatically', default=False, help='If this checkbox is ticked, this entry will be automatically reversed at the reversal date you defined.') reverse_date = fields.Date(string='Reversal Date', help='Date of the reverse accounting entry.') @@ -174,6 +174,8 @@ class AccountMove(models.Model): if not move.journal_id.update_posted: raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) if self.ids: + self.check_access_rights('write') + self.check_access_rule('write') self._check_lock_date() self._cr.execute('UPDATE account_move '\ 'SET state=%s '\ @@ -544,9 +546,9 @@ class AccountMoveLine(models.Model): for line in self: amount = line.amount_currency if line.currency_id and line.currency_id != line.company_currency_id: - amount = self.currency_id.with_context(date=line.date).compute(amount, line.company_currency_id) - line.debit = amount > 0 and amount or 0.0 - line.credit = amount < 0 and -amount or 0.0 + amount = line.currency_id.with_context(date=line.date).compute(amount, line.company_currency_id) + line.debit = amount > 0 and amount or 0.0 + line.credit = amount < 0 and -amount or 0.0 #################################################### # Reconciliation interface methods diff --git a/addons/account/views/account_report.xml b/addons/account/views/account_report.xml index 6328d865c2c..06e077e7d8c 100644 --- a/addons/account/views/account_report.xml +++ b/addons/account/views/account_report.xml @@ -74,7 +74,7 @@ - + diff --git a/addons/account/wizard/account_report_print_journal_view.xml b/addons/account/wizard/account_report_print_journal_view.xml index f549e683934..5103f6aabea 100644 --- a/addons/account/wizard/account_report_print_journal_view.xml +++ b/addons/account/wizard/account_report_print_journal_view.xml @@ -2,7 +2,7 @@ - Sale/Purchase Journal + Journals Audit account.print.journal @@ -12,15 +12,12 @@ - - [('type', 'in', ('sale', 'purchase'))] - - Sale/Purchase Journal + Journals Audit ir.actions.act_window account.print.journal form @@ -31,7 +28,7 @@ - + @@ -111,7 +111,7 @@ - +
element so that it gets the correct // modifiers classes (for styling) - var modifiers = this._registerModifiers(node, record, $td); + var modifiers = this._registerModifiers(node, record, $td, _.pick(options, 'mode')); // If the invisible modifiers is true, the element is left empty. // Indeed, if the modifiers was to change the whole cell would be // rerendered anyway. diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js index b2295affb98..8a989dad35e 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -48,7 +48,7 @@ var ListView = BasicView.extend({ this.rendererParams.arch = arch; this.rendererParams.hasSelectors = 'hasSelectors' in params ? params.hasSelectors : true; - this.rendererParams.mode = mode; + this.rendererParams.editable = params.readonly ? false : arch.attrs.editable; this.loadParams.limit = this.loadParams.limit || 80; this.loadParams.type = 'list'; diff --git a/addons/web/static/src/less/search_view.less b/addons/web/static/src/less/search_view.less index a9d6d28e9e4..0261806631a 100644 --- a/addons/web/static/src/less/search_view.less +++ b/addons/web/static/src/less/search_view.less @@ -139,6 +139,9 @@ // Filters .o_filters_menu { .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .o_filter_condition { .o-search-options-dropdown-custom-li; @@ -175,6 +178,9 @@ // Group By .o_group_by_menu { .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .divider {display: none;} // hide the divider by default (no group) .o_add_group { // add button @@ -188,6 +194,9 @@ max-height: 75vh; overflow-y: auto; .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .divider {display: none;} // hide the divider by default (no favorite) .o-searchview-custom-private, .o-searchview-custom-public { diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml index 6cbc4d55942..7afc640ccfa 100644 --- a/addons/web/static/src/xml/kanban.xml +++ b/addons/web/static/src/xml/kanban.xml @@ -9,7 +9,7 @@ -
+
@@ -107,7 +107,7 @@
-
+
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 08e255777eb..6a67cc43736 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2457,6 +2457,63 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('monetary field with currency digits != 2', function (assert) { + assert.expect(5); + + this.data.partner.records = [{ + id: 1, + bar: false, + foo: "pouet", + int_field: 68, + qux: 99.1234, + currency_id: 1, + }]; + this.data.currency.records = [{ + id: 1, + display_name: "VEF", + symbol: "Bs.F", + position: "after", + digits: [16, 4], + }]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1234\u00a0Bs.F', + 'The value should be displayed properly.'); + + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('input').first().val(), '99.1234', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(form.$('input').parent().children().eq(1).text(), 'Bs.F', + 'The input should be followed by a span containing the currency symbol.'); + + form.$('input').first().val('99.111111111').trigger('input'); + assert.strictEqual(form.$('input').first().val(), '99.111111111', + 'The value should not be formated yet.'); + + form.$buttons.find('.o_form_button_save').click(); + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1111\u00a0Bs.F', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + QUnit.test('monetary field in editable list view', function (assert) { assert.expect(9); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index df9e7d4994b..84d095660cb 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2274,7 +2274,7 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should still have the 3 rows in the correct order"); - // Drag and drop the fourth line in second position + // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), @@ -2334,7 +2334,7 @@ QUnit.module('relational_fields', { form.$buttons.find('.o_form_button_edit').click(); - // Drag and drop the fourth line in second position + // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), @@ -2347,6 +2347,120 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange for embedded one2many with handle widget using same sequence', function (assert) { + assert.expect(4); + + this.data.turtle.records[0].turtle_int = 1; + this.data.turtle.records[1].turtle_int = 1; + this.data.turtle.records[2].turtle_int = 1; + this.data.partner.records[0].turtles = [1, 2, 3]; + var turtleOnchange = 0; + this.data.turtle.onchanges = { + turtle_int: function () { + turtleOnchange++; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1].turtles, [[1,1,{"turtle_int":2}],[4,2,false],[1,3,{"turtle_int":3}]], + "should change all lines that have changed (the first one doesn't change because it has the same sequence)"); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + + + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", + "should have the 3 rows in the correct order"); + + // Drag and drop the second line in first position + testUtils.dragAndDrop( + form.$('.ui-sortable-handle').eq(1), + form.$('tbody tr').first(), + {position: 'top'} + ); + + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", + "should still have the 3 rows in the correct order"); + assert.strictEqual(turtleOnchange, 3, "should update all lines"); + + form.$buttons.find('.o_form_button_save').click(); + form.destroy(); + }); + + QUnit.test('onchange (with command 5) for embedded one2many with handle widget', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1, 2, 3]; + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [[5]].concat(obj.turtles); + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", + "should have the 3 rows in the correct order"); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click(); + form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input'); + + // Drag and drop the third line in second position + testUtils.dragAndDrop( + form.$('.ui-sortable-handle').eq(2), + form.$('.o_field_one2many tbody tr').eq(1), + {position: 'top'} + ); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip", + "should display to record in 'turtle_int' order"); + + form.destroy(); + }); + QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { var done = assert.async(); assert.expect(20); @@ -4065,16 +4179,16 @@ QUnit.module('relational_fields', { assert.expect(5); var delta = 0; - var oldInit = AbstractField.prototype.init; - var oldDestroy = AbstractField.prototype.destroy; - AbstractField.prototype.init = function () { - delta++; - oldInit.apply(this, arguments); - }; - AbstractField.prototype.destroy = function () { - delta--; - oldDestroy.apply(this, arguments); - }; + testUtils.patch(AbstractField, { + init: function () { + delta++; + this._super.apply(this, arguments); + }, + destroy: function () { + delta--; + this._super.apply(this, arguments); + }, + }); var deactiveOnchange = true; @@ -4161,8 +4275,49 @@ QUnit.module('relational_fields', { "should correctly refresh the records after save"); form.destroy(); - AbstractField.prototype.init = oldInit; - AbstractField.prototype.destroy = oldDestroy; + testUtils.unpatch(AbstractField); + }); + + QUnit.test('editable one2many with sub widgets are rendered in readonly', function (assert) { + assert.expect(2); + + var editableWidgets = 0; + testUtils.patch(AbstractField, { + init: function () { + this._super.apply(this, arguments); + if (this.mode === 'edit') { + editableWidgets++; + } + }, + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(editableWidgets, 1, + "o2m is only widget in edit mode"); + form.$('tbody td.o_field_x2many_list_row_add a').click(); + + assert.strictEqual(editableWidgets, 3, + "3 widgets currently in edit mode"); + + form.destroy(); + testUtils.unpatch(AbstractField); }); QUnit.test('one2many editable list with onchange keeps the order', function (assert) { @@ -9538,7 +9693,7 @@ QUnit.module('relational_fields', { }); QUnit.test('one2many invisible depends on parent field', function (assert) { - assert.expect(2); + assert.expect(4); this.data.partner.records[0].p = [2]; var form = createView({ @@ -9547,12 +9702,15 @@ QUnit.module('relational_fields', { data: this.data, arch:'
' + '' + + '' + + '' + + '' + '' + '' + '' + '' + '' + - '' + + '' + '' + '' + '' + @@ -9565,6 +9723,13 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('th').length, 2, "should be 2 columns in the one2many"); form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_many2one[name="product_id"] input').click(); + $('li.ui-menu-item a:contains(xpad)').trigger('mouseenter').click(); + assert.strictEqual(form.$('th').length, 1, + "should be 1 column when the product_id is set"); + form.$('.o_field_many2one[name="product_id"] input').val('').trigger('keyup'); + assert.strictEqual(form.$('th').length, 2, + "should be 2 columns in the one2many when product_id is not set"); form.$('.o_field_boolean[name="bar"] input').click(); assert.strictEqual(form.$('th').length, 1, "should be 1 column after the value change"); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 37a98de1746..e049ba968b2 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -434,11 +434,16 @@ function dragAndDrop($el, $to, options) { elementCenter.top += $el.outerHeight()/2; var toOffset = $to.offset(); + toOffset.top += $to.outerHeight()/2; toOffset.left += $to.outerWidth()/2; - if (position === 'center') { - toOffset.top += $to.outerHeight()/2; + if (position === 'top') { + toOffset.top -= $to.outerHeight()/2; } else if (position === 'bottom') { - toOffset.top += $to.outerHeight(); + toOffset.top += $to.outerHeight()/2; + } else if (position === 'left') { + toOffset.left -= $to.outerWidth()/2; + } else if (position === 'right') { + toOffset.left += $to.outerWidth()/2; } $el.trigger($.Event("mousedown", { @@ -551,6 +556,51 @@ function removeSrcAttribute($el, widget) { }); } +var patches = {}; +/** + * Patches a given Class with the given properties. + * + * @param {Class} Klass + * @param {Object} props + */ +function patch (Klass, props) { + var patchID = _.uniqueId('patch_'); + Klass.__patchID = patchID; + patches[patchID] = { + Klass: Klass, + otherPatchedProps: [], + ownPatchedProps: [], + }; + _.each(props, function (value, key) { + if (Klass.prototype.hasOwnProperty(key)) { + patches[patchID].ownPatchedProps.push({ + key: key, + initialValue: Klass.prototype[key], + }); + } else { + patches[patchID].otherPatchedProps.push(key); + } + }); + Klass.include(props); +} +/** + * Unpatches a given Class. + * + * @param {Class} Klass + */ +function unpatch(Klass) { + var patchID = Klass.__patchID; + var patch = patches[patchID]; + _.each(patch.ownPatchedProps, function (p) { + Klass[p.key] = p.initialValue; + }); + _.each(patch.otherPatchedProps, function (key) { + delete Klass.prototype[key]; + }); + delete patches[patchID]; + delete Klass.__patchID; +} + // Loading static files cannot be properly simulated when their real content is // really needed. This is the case for static XML files so we load them here, // before starting the qunit test suite. @@ -566,18 +616,20 @@ return $.when( QUnit.start(); }, 0); return { - intercept: intercept, - observe: observe, - createView: createView, + addMockEnvironment: addMockEnvironment, createAsyncView: createAsyncView, createModel: createModel, createParent: createParent, - addMockEnvironment: addMockEnvironment, + createView: createView, dragAndDrop: dragAndDrop, + intercept: intercept, + observe: observe, + patch: patch, + removeSrcAttribute: removeSrcAttribute, + triggerKeypressEvent: triggerKeypressEvent, triggerMouseEvent: triggerMouseEvent, triggerPositionalMouseEvent: triggerPositionalMouseEvent, - triggerKeypressEvent: triggerKeypressEvent, - removeSrcAttribute: removeSrcAttribute, + unpatch: unpatch, }; }); diff --git a/addons/web/static/tests/views/form_benchmarks.js b/addons/web/static/tests/views/form_benchmarks.js new file mode 100644 index 00000000000..d664859ba2f --- /dev/null +++ b/addons/web/static/tests/views/form_benchmarks.js @@ -0,0 +1,95 @@ +odoo.define('web.list_benchmarks', function (require) { +"use strict"; + +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('Form View', { + beforeEach: function () { + this.data = { + foo: { + fields: { + many2many: { string: "bar", type: "many2many", relation: 'bar'}, + }, + records: [ + { id: 1, many2many: []}, + ], + onchanges: {} + }, + bar: { + fields: { + char: {string: "char", type: "char"}, + many2many: { string: "pokemon", type: "many2many", relation: 'pokemon'}, + }, + records: [], + onchanges: {} + }, + pokemon: { + fields: { + name: {string: "Name", type: "char"}, + }, + records: [], + onchanges: {} + }, + }; + this.arch = null; + this.run = function (assert, done, cb) { + var data = this.data; + var arch = this.arch; + var viewOptions = this.viewOptions; + new Benchmark.Suite({}) + .add('form', function () { + var list = createView({ + View: FormView, + model: 'foo', + data: data, + arch: arch, + res_id: 1, + }); + if (cb) { + cb(list); + } + list.destroy(); + }) + .on('cycle', function(event) { + assert.ok(true, String(event.target)); + }) + .on('complete', done) + .run({ 'async': true }); + }; + } +}, function () { + QUnit.test('x2many with 250 rows, 2 fields (with many2many_tags, and modifiers), onchanges, and edition', function (assert) { + var done = assert.async(); + assert.expect(1); + + this.data.foo.onchanges.many2many = function (obj) { + obj.many2many = [5].concat(obj.many2many); + }; + for (var i = 2; i < 500; i) { + this.data.bar.records.push({ + id: i, + char: "automated data", + }); + this.data.foo.records[0].many2many.push(i); + } + this.arch = + '' + '' + '' + '' + '' + '' + '' + ''; + this.run(assert, done, function (form) { + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_cell:first').click(); + form.$('input:first').val("tralala").trigger('input'); + }); + }); +}); + +}); \ No newline at end of file diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js index 07f7d060347..f7d22f27a64 100644 --- a/addons/web/static/tests/views/graph_tests.js +++ b/addons/web/static/tests/views/graph_tests.js @@ -90,7 +90,6 @@ QUnit.module('Views', { }); }); - QUnit.test('default type attribute', function (assert) { assert.expect(1); @@ -106,7 +105,6 @@ QUnit.module('Views', { graph.destroy(); }); - QUnit.test('switching mode', function (assert) { assert.expect(6); @@ -191,7 +189,6 @@ QUnit.module('Views', { }); }); - QUnit.test('no content helper', function (assert) { var done = assert.async(); assert.expect(4); @@ -442,6 +439,43 @@ QUnit.module('Views', { graph.destroy(); }); + + QUnit.test('initial groupby is kept when reloading', function (assert) { + var done = assert.async(); + assert.expect(4); + + var graph = createView({ + View: GraphView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (args.method === 'read_group') { + assert.deepEqual(args.kwargs.groupby, ['product_id'], + "should group by the correct field"); + } + return this._super.apply(this, arguments); + }, + }); + + + return concurrency.delay(0).then(function () { + assert.strictEqual(graph.$('.nv-groups rect').length, 2, + "should display two groups"); + + graph.reload({groupBy: []}); + return concurrency.delay(0).then(function () { + assert.strictEqual(graph.$('.nv-groups rect').length, 2, + "should still display two groups"); + + graph.destroy(); + done(); + }); + }); + }); }); }); diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js index d777f5bd87b..0ee35c2c9c3 100644 --- a/addons/web/static/tests/views/kanban_mobile_tests.js +++ b/addons/web/static/tests/views/kanban_mobile_tests.js @@ -26,6 +26,7 @@ QUnit.module('Views', { {id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]}, {id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]}, {id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []}, + {id: 5, bar: false, foo: "Hello \"World\"! #peace_n'_love", int_field: -9, qux: 10, state: "jkl", category_ids: []}, ] }, product: { @@ -55,7 +56,7 @@ QUnit.module('Views', { QUnit.module('KanbanView Mobile'); QUnit.test('mobile grouped rendering', function (assert) { - assert.expect(8); + assert.expect(9); var kanban = createView({ View: KanbanView, @@ -66,6 +67,7 @@ QUnit.module('Views', { '
' + '' + '', + domain: [['product_id', '!=', false]], groupBy: ['product_id'], }); @@ -95,6 +97,65 @@ QUnit.module('Views', { assert.ok(kanban.$('.o_kanban_group:nth(1) > div:nth(1)').hasClass('o_kanban_quick_create'), "clicking on create should open the quick_create in the second column"); + // kanban column should match kanban mobile tabs + var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get(); + var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get(); + assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + + kanban.destroy(); + }); + QUnit.test('mobile grouped with undefined column', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + groupBy: ['product_id'], + }); + + // first column should be undefined with framework unique identifier + assert.strictEqual(kanban.$('.o_kanban_group').length, 3, "should have 3 columns" ); + assert.strictEqual(kanban.$('.o_kanban_group:first-child[data-id^="partner_"]').length, 1, + "Undefined column should be first and have unique framework identifier as data-id") + + // kanban column should match kanban mobile tabs + var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get(); + var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get(); + assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + + kanban.destroy(); + }); + QUnit.test('mobile grouped on many2one rendering', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + groupBy: ['foo'], + }); + + // basic rendering tests + assert.strictEqual(kanban.$('.o_kanban_group').length, 4, "should have 4 columns" ); + assert.strictEqual(kanban.$('.o_kanban_group[data-id^="partner_"]').length, 4, + "all column should have framework unique identifiers"); + + // kanban column should match kanban mobile tabs + var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get(); + var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get(); + assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + kanban.destroy(); }); }); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 70e60b37bd5..9c8361e63ae 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -3,6 +3,7 @@ odoo.define('web.kanban_tests', function (require) { var core = require('web.core'); var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); +var KanbanRenderer = require('web.KanbanRenderer'); var KanbanView = require('web.KanbanView'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); @@ -1015,7 +1016,19 @@ QUnit.module('Views', { }); QUnit.test('delete a column in grouped on m2o', function (assert) { - assert.expect(26); + assert.expect(32); + + testUtils.patch(KanbanRenderer, { + _renderGrouped: function () { + this._super.apply(this, arguments); + // set delay and revert animation time to 0 so dummy drag and drop works + if (this.$el.sortable('instance')) { + this.$el.sortable('option', {delay: 0, revert: 0}); + } + }, + }); + + var resequencedIDs; var kanban = createView({ View: KanbanView, @@ -1029,6 +1042,12 @@ QUnit.module('Views', { '', groupBy: ['product_id'], mockRPC: function (route, args) { + if (route === '/web/dataset/resequence') { + resequencedIDs = args.ids; + assert.strictEqual(_.reject(args.ids, _.isNumber).length, 0, + "column resequenced should be existing records with IDs"); + return $.when(true); + } if (args.method) { assert.step(args.method); } @@ -1071,7 +1090,7 @@ QUnit.module('Views', { assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 3, 'last column should now be [3, "hello"]'); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should still have two columns"); - assert.ok(!kanban.$('.o_kanban_group:first').data('id'), + assert.ok(!_.isNumber(kanban.$('.o_kanban_group:first').data('id')), 'first column should have no id (Undefined column)'); // check available actions on 'Undefined' column assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length, @@ -1087,7 +1106,32 @@ QUnit.module('Views', { assert.verifySteps(['read_group', 'unlink', 'read_group']); assert.strictEqual(kanban.renderer.widgets.length, 2, "the old widgets should have been correctly deleted"); + + // test column drag and drop having an 'Undefined' column + testUtils.dragAndDrop( + kanban.$('.o_kanban_header_title:first'), + kanban.$('.o_kanban_header_title:last'), {position: 'right'} + ); + assert.strictEqual(resequencedIDs, undefined, + "resequencing require at least 2 not Undefined columns"); + kanban.$('.o_column_quick_create input').val('once third column'); + kanban.$('.o_column_quick_create button.o_kanban_add').click(); + var newColumnID = kanban.$('.o_kanban_group:last').data('id'); + testUtils.dragAndDrop( + kanban.$('.o_kanban_header_title:first'), + kanban.$('.o_kanban_header_title:last'), {position: 'right'} + ); + assert.deepEqual([3, newColumnID], resequencedIDs, + "moving the Undefined column should not affect order of other columns") + testUtils.dragAndDrop( + kanban.$('.o_kanban_header_title:first'), + kanban.$('.o_kanban_header_title:nth(1)'), {position: 'right'} + ); + assert.deepEqual([newColumnID, 3], resequencedIDs, + "moved column should be resequenced accordingly") + kanban.destroy(); + testUtils.unpatch(KanbanRenderer); }); QUnit.test('create a column, delete it and create another one', function (assert) { diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 9180015685e..2993e393a4f 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -415,11 +415,12 @@ QUnit.module('Views', { QUnit.test('editable list: add a line and discard', function (assert) { assert.expect(11); - var oldDestroy = basicFields.FieldChar.prototype.destroy; - basicFields.FieldChar.prototype.destroy = function () { - assert.step('destroy'); - oldDestroy.apply(this, arguments); - }; + testUtils.patch(basicFields.FieldChar, { + destroy: function () { + assert.step('destroy'); + this._super.apply(this, arguments); + }, + }); var list = createView({ View: ListView, @@ -456,7 +457,7 @@ QUnit.module('Views', { assert.verifySteps(['destroy'], "should have destroyed the widget of the removed line"); - basicFields.FieldChar.prototype.destroy = oldDestroy; + testUtils.unpatch(basicFields.FieldChar); list.destroy(); }); @@ -473,13 +474,13 @@ QUnit.module('Views', { var n = 0; testUtils.intercept(list, "field_changed", function () { - n = 1; + n += 1; }); $td.click(); $td.find('input').val('abc').trigger('input'); - assert.strictEqual(n, 1, "field_changed should not have been triggered"); - list.$('td:not(.o_list_record_selector)').eq(2).click(); assert.strictEqual(n, 1, "field_changed should have been triggered"); + list.$('td:not(.o_list_record_selector)').eq(2).click(); + assert.strictEqual(n, 1, "field_changed should not have been triggered"); list.destroy(); }); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index c3d4e4eceae..a02bbdbcf6a 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -587,6 +587,7 @@ +
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js index 95afaa90073..cbdb6cbb88b 100644 --- a/addons/web_editor/static/src/js/backend/fields.js +++ b/addons/web_editor/static/src/js/backend/fields.js @@ -221,7 +221,8 @@ var FieldTextHtml = AbstractField.extend({ start: function () { var self = this; - this.loaded = false; + this.editorLoadedDeferred = $.Deferred(); + this.contentLoadedDeferred = $.Deferred(); this.callback = _.uniqueId('FieldTextHtml_'); window.odoo[this.callback+"_editor"] = function (EditorBar) { setTimeout(function () { @@ -312,8 +313,6 @@ var FieldTextHtml = AbstractField.extend({ old_initialize_content: function () { this.$el.closest('.modal-body').css('max-height', 'none'); this.$iframe = this.$el.find('iframe'); - // deactivate any button to avoid saving a not ready iframe - $('.o_cp_buttons, .o_statusbar_buttons').find('button').addClass('o_disabled').attr('disabled', true); this.document = null; this.$body = $(); this.$content = $(); @@ -328,9 +327,7 @@ var FieldTextHtml = AbstractField.extend({ this.$content = this.$body.find("#editable_area"); this.render(); this.add_button(); - this.loaded = true; - // reactivate all the buttons when the field's content (the iframe) is loaded - $('.o_cp_buttons, .o_statusbar_buttons').find('button').removeClass('o_disabled').attr('disabled', false); + this.contentLoadedDeferred.resolve(); setTimeout(self.resize, 0); }, on_editor_loaded: function (EditorBar) { @@ -339,6 +336,7 @@ var FieldTextHtml = AbstractField.extend({ if (this.value && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) { this.render(); } + this.editorLoadedDeferred.resolve(); setTimeout(function () { setTimeout(self.resize,0); }, 0); @@ -395,20 +393,25 @@ var FieldTextHtml = AbstractField.extend({ //-------------------------------------------------------------------------- /** + * Set the value when the widget is fully loaded (content + editor). + * * @override */ commitChanges: function () { - if (!this.loaded || this.mode === 'readonly') { + var self = this; + var result = this._super.bind(this, arguments); + if (this.mode === 'readonly') { return; } - // switch to WYSIWYG mode if currently in code mode to get all changes - if (config.debug && this.mode === 'edit' && this.editor.rte) { - var layoutInfo = this.editor.rte.editable().data('layoutInfo'); - $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false); - } - this.editor.snippetsMenu && this.editor.snippetsMenu.cleanForSave(); - this._setValue(this.$content.html()); - return this._super.apply(this, arguments); + return $.when(this.contentLoadedDeferred, this.editorLoadedDeferred, result).then(function () { + // switch to WYSIWYG mode if currently in code mode to get all changes + if (config.debug && self.editor.rte) { + var layoutInfo = self.editor.rte.editable().data('layoutInfo'); + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false); + } + self.editor.snippetsMenu && self.editor.snippetsMenu.cleanForSave(); + self._setValue(self.$content.html()); + }); }, }); diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index 33bf1f38046..d6f9805599e 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -13,11 +13,64 @@ var range = $.summernote.core.range; var list = $.summernote.core.list; var key = $.summernote.core.key; var eventHandler = $.summernote.eventHandler; -// var editor = eventHandler.modules.editor; +var editor = eventHandler.modules.editor; var renderer = $.summernote.renderer; var options = $.summernote.options; +// Browser-unify execCommand +var oldJustify = {}; +_.each(['Left', 'Right', 'Full', 'Center'], function (align) { + oldJustify[align] = editor['justify' + align]; + editor['justify' + align] = function ($editable, value) { + // Before calling the standard function, check all elements which have + // an 'align' attribute and mark them with their value + var $align = $editable.find('[align]'); + _.each($align, function (el) { + var $el = $(el); + $el.data('__align', $el.attr('align')); + }); + + // Call the standard function + oldJustify[align].apply(this, arguments); + + // Then: + + // Remove the text-align of elements which lost the 'align' attribute + var $newAlign = $editable.find('[align]'); + $align.not($newAlign).css('text-align', ''); + + // Transform the 'align' attribute into the 'text-align' css + // property for elements which received the 'align' attribute or whose + // 'align' attribute changed + _.each($newAlign, function (el) { + var $el = $(el); + + var oldAlignValue = $align.data('__align'); + var alignValue = $el.attr('align'); + if (oldAlignValue === alignValue) { + // If the element already had an 'align' attribute and that it + // did not changed, do nothing (compatibility) + return; + } + + $el.removeAttr('align'); + $el.css('text-align', alignValue); + + // Note the first step (removing the text-align of elemnts which + // lost the 'align' attribute) is kinda the same as this one, but + // this one handles the elements which have been edited with chrome + // or with this new system + $el.find('*').css('text-align', ''); + }); + + // Unmark the elements + $align.removeData('__align'); + }; +}); + + // Add methods to summernote + dom.hasContentAfter = function (node) { var next; if (dom.isEditable(node)) return; @@ -2289,26 +2342,6 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) { } }; -// override summernote clipboard functionality -eventHandler.modules.clipboard.attach = function (layoutInfo) { - var $editable = layoutInfo.editable(); - $editable.on('paste', function (e) { - var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData); - // Change nothing if pasting html (copy from text editor / web / ...) or - // if clipboardData is not available (IE / ...) - if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") { - e.preventDefault(); - $editable.data('NoteHistory').recordUndo($editable); // FIXME - var pastedText = clipboardData.getData("text/plain"); - // Try removing linebreaks which are not really linebreaks (in a PDF, - // when a sentence goes over the next line, copying it considers it - // a linebreak for example). - var formattedText = pastedText.replace(/([\w-])\r?\n([\w-])/g, "$1 $2").trim(); - document.execCommand("insertText", false, formattedText); - } - }); -}; - var fn_attach = eventHandler.attach; eventHandler.attach = function (oLayoutInfo, options) { var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable'); diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js index 41ada113cba..3f46edde64c 100644 --- a/addons/web_editor/static/tests/web_editor_tests.js +++ b/addons/web_editor/static/tests/web_editor_tests.js @@ -4,6 +4,7 @@ odoo.define('web_editor.web_editor_tests', function (require) { var FormView = require('web.FormView'); var testUtils = require('web.test_utils'); var core = require('web.core'); +var web_editor = require('web_editor.editor'); var _t = core._t; @@ -191,7 +192,7 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode QUnit.test('html_frame does not crash when saving in readonly', function (assert) { // The 'Save' action may be triggered even in readonly (e.g. when clicking // on a button in the form view) - assert.expect(0); + assert.expect(2); var form = testUtils.createView({ View: FormView, @@ -203,10 +204,13 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert '' + '', res_id: 1, - mockRPC: function (route) { + mockRPC: function (route, args) { + if (args.method) { + assert.step(args.method); + } if (_.str.startsWith(route, '/test')) { // manually call the callback to simulate that the iframe has - // been correctly loaded + // been loaded (note: just the content, not the editor) window.odoo[$.deparam(route).callback + '_content'].call(); return $.when(); } @@ -215,7 +219,109 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert }); form.saveRecord(); // before the fix done in this commit, it crashed here + + assert.verifySteps(['read']); + form.destroy(); }); +QUnit.test('html_frame does not crash when saving in edit mode (editor not loaded)', function (assert) { + // The 'Save' action may be triggered when saving in edit mode very fast + // so that the editor may be not loaded, even though the content is! + assert.expect(2); + + var form = testUtils.createView({ + View: FormView, + model: 'mass.mailing', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method) { + assert.step(args.method); + } + if (_.str.startsWith(route, '/test')) { + // manually call the callback to simulate that the iframe has + // been partially loaded (just the content, not the editor) + window.odoo[$.deparam(route).callback + '_content'](); + return $.when(); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input').val('trululu').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); // crash without editor fully loaded + + assert.verifySteps(['read']); + + form.destroy(); +}); + +QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', function (assert) { + var done = assert.async(); + assert.expect(4); + + var editorBar = new web_editor.Class(); + var loadDeferred = $.Deferred(); + var writeDeferred = $.Deferred(); + + var form = testUtils.createView({ + View: FormView, + model: 'mass.mailing', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method) { + assert.step(args.method); + if (args.method === 'write') { + writeDeferred.resolve(); + } + } + if (_.str.startsWith(route, '/test')) { + // manually call the callback to simulate that the iframe has + // been fully loaded (content + editor) + var callback = $.deparam(route).callback; + return loadDeferred.then(function () { + var contentCallback = window.odoo[callback + '_content']; + var editorCallback = window.odoo[callback + '_editor']; + if (editorCallback && contentCallback) { + contentCallback(); + editorCallback(editorBar); + } + }); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input').val('trululu').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + + loadDeferred.resolve(); // simulate late loading of html frame + + assert.strictEqual(form.$('.o_field_char').val(), 'trululu', + "should have saved the char field text"); + + writeDeferred.then( function () { // html_frame is async with write + assert.verifySteps(['read', 'write']); + form.destroy(); + done(); + }); + +}); + }); diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js index 65553b46bf6..4b7dde7696b 100644 --- a/addons/web_tour/static/src/js/tour_manager.js +++ b/addons/web_tour/static/src/js/tour_manager.js @@ -170,14 +170,21 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { console.log('Tour Manager is ready. running_tour=' + this.running_tour); }, /** - * Registers a tour described by the following arguments (in order) - * @param [String] tour's name - * @param [Object] dict of options (optional), available options are: - * test [Boolean] true if the tour is dedicated to tests (it won't be enabled by default) - * skip_enabled [Boolean] true to add a link to consume the whole tour in its tips - * url [String] the url to load when manually running the tour - * rainbowMan [Bool] use to display the rainbow effect at the end of tour - * @param [Array] dict of steps, each step being a dict containing a tip description + * Registers a tour described by the following arguments *in order* + * + * @param [string] name - tour's name + * @param [Object] - options (optional), available options are: + * @param [boolean] - true if this is only for tests + * @param [boolean] + * true to add a link in its tips to consume the whole tour + * @param [string] + * the url to load when manually running the tour + * @param [boolean] + * whether or not the rainbowman must be shown at the end of the tour + * @param [Deferred] + * indicates when the tour can be started + * @param [Object] steps - steps' descriptions, each step being an object + * containing a tip description */ register: function() { var args = Array.prototype.slice.call(arguments); @@ -193,7 +200,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { name: name, steps: steps, url: options.url, - rainbowMan: options.rainbowMan || true, + rainbowMan: options.rainbowMan === undefined ? true : !!options.rainbowMan, test: options.test, wait_for: options.wait_for || $.when(), }; @@ -402,13 +409,14 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { _consume_tour: function (tour_name, error) { delete this.active_tooltips[tour_name]; //display rainbow at the end of any tour - if (this.rainbowMan && this.tours[tour_name].current_step === this.tours[tour_name].steps.length){ + if (this.tours[tour_name].rainbowMan && this.running_tour !== tour_name + && this.tours[tour_name].current_step === this.tours[tour_name].steps.length) { var $rainbow_message = $('' + 'Good job!' + ' You went through all steps of this tour.' + ''); new RainbowMan({message: $rainbow_message, click_close: false}).appendTo(this.$body); - }; + } this.tours[tour_name].current_step = 0; local_storage.removeItem(get_step_key(tour_name)); if (this.running_tour === tour_name) { diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 696f69e8ae3..65c514487a2 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -83,7 +83,7 @@ class Http(models.AbstractModel): super(Http, cls)._add_dispatch_parameters(func) if request.routing_iteration == 1: - request.website = request.website.with_context(context) + request.website = request.website.with_context(request.context) @classmethod def _get_languages(cls): diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml index e336df0e7f2..3af11508112 100644 --- a/addons/website_event_sale/views/event_templates.xml +++ b/addons/website_event_sale/views/event_templates.xml @@ -79,7 +79,7 @@ - event.event_ticket_ids + event.event_ticket_ids and not all([ticket.is_expired for ticket in event.event_ticket_ids]) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index fde2aecc25c..f978430f938 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -833,7 +833,7 @@ class WebsiteSale(http.Controller): return request.redirect('/shop/?error=invalid_token_id') # We retrieve the token the user want to use to pay - token = request.env['payment.token'].browse(pm_id) + token = request.env['payment.token'].sudo().browse(pm_id) if not token: return request.redirect('/shop/?error=token_not_found') diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml index 910a5f95cfd..81639c17cd0 100644 --- a/addons/website_sale/data/data.xml +++ b/addons/website_sale/data/data.xml @@ -31,6 +31,7 @@ sale.report week + diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 29f3652a185..88bbb95648c 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -168,7 +168,7 @@ odoo.define('website_sale.website_sale', function (require) { } else { $q.parents('li:first').addClass("hidden"); - $('a[href^="/shop/checkout"]').addClass("hidden"); + $('a[href*="/shop/checkout"]').addClass("hidden"); } $q.html(data.cart_quantity).hide().fadeIn(600); diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 9c8e10ad20f..de574739269 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -344,7 +344,7 @@
-
+