From 6fc0793c30705301190eb67558dab93da9d9424a Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 25 Oct 2017 09:48:00 +0200 Subject: [PATCH 01/23] [FIX] web: column aggregates should recompute when no records 1- When viewing for example journal items. 2- Now activate a filter that would display no record Before this commit, the column aggregates of case 2 were the same as case 1. After this commit, we recompute the aggregate, which ever case we are in. OPW 777660 closes #20452 --- addons/web/static/src/js/views/list/list_renderer.js | 5 +---- addons/web/static/tests/views/list_tests.js | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 4d4a47e7916..d9f420d665e 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -94,9 +94,6 @@ var ListRenderer = BasicRenderer.extend({ } else { data = this.state.data; } - if (data.length === 0) { - return; - } _.each(this.columns, this._computeColumnAggregates.bind(this, data)); }, @@ -138,7 +135,7 @@ var ListRenderer = BasicRenderer.extend({ } }); if (func === 'avg') { - aggregateValue = aggregateValue / count; + aggregateValue = count ? aggregateValue / count : aggregateValue; } column.aggregate = { help: attrs[func], diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 88bb0d30040..0bdb3187406 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -558,7 +558,7 @@ QUnit.module('Views', { }); QUnit.test('aggregates are computed correctly', function (assert) { - assert.expect(3); + assert.expect(4); var list = createView({ View: ListView, @@ -579,6 +579,11 @@ QUnit.module('Views', { $thead_selector.click(); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32 as all records are selected"); + + // Let's update the view to dislay NO records + list.update({domain: ['&', ['bar', '=', false], ['int_field', '>', 0]]}); + assert.strictEqual(list.$('tfoot td:nth(2)').text(), "0", "total should have been recomputed to 0"); + list.destroy(); }); From f13cab6aa6d3181336abbfbe52dbc6330dab6fc2 Mon Sep 17 00:00:00 2001 From: xmo-odoo Date: Wed, 25 Oct 2017 15:31:58 +0200 Subject: [PATCH 02/23] [FIX] website_mail_channel: unsubscription Fixes 27280fefa9e7cad0143aca688149f19ebdb684f6: * `partner_id` is an `int` not a recordset * qweb Response is lazy (the template is only looked up and rendered when we actually need to send the content over the wire) => the error would occur outside of the try..except block. Force rendering (`Response.flatten`) so we can handle the issue here * Uses the HttpCase interface to contact the server over a new socket (same as tours & al) because using `werkzeug.test.Client` would not use the correct cursor (?) and so the test data would not be visible --- .../website_mail_channel/controllers/main.py | 14 ++-- addons/website_mail_channel/tests/__init__.py | 2 + .../tests/test_unsubscribe.py | 71 +++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 addons/website_mail_channel/tests/__init__.py create mode 100644 addons/website_mail_channel/tests/test_unsubscribe.py diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py index 4a9e2c449a8..3aa9491bf54 100644 --- a/addons/website_mail_channel/controllers/main.py +++ b/addons/website_mail_channel/controllers/main.py @@ -228,15 +228,19 @@ class MailGroup(http.Controller): def confirm_unsubscribe(self, channel, partner_id, token, **kw): subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)]) if not subscriber: + partner = request.env['res.partner'].browse(partner_id).sudo().exists() # FIXME: remove try/except in master try: - return request.render( - 'website_mail_channel.not_subscribed', { - 'partner_id': partner_id - }) + response = request.render( + 'website_mail_channel.not_subscribed', + {'partner_id': partner}) + # make sure the rendering (and thus error if template is + # missing) happens inside the try block + response.flatten() + return response except ValueError: return _("The address %s is already unsubscribed or was never subscribed to any mailing list") % ( - partner_id.email + partner.email ) subscriber_token = channel._generate_action_token(partner_id, action='unsubscribe') diff --git a/addons/website_mail_channel/tests/__init__.py b/addons/website_mail_channel/tests/__init__.py new file mode 100644 index 00000000000..2e3364d701d --- /dev/null +++ b/addons/website_mail_channel/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_unsubscribe diff --git a/addons/website_mail_channel/tests/test_unsubscribe.py b/addons/website_mail_channel/tests/test_unsubscribe.py new file mode 100644 index 00000000000..44128c30b86 --- /dev/null +++ b/addons/website_mail_channel/tests/test_unsubscribe.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +import requests + +from odoo.api import Environment +from odoo.tests import common +from odoo.tools.misc import mute_logger + + +class TestConfirmUnsubscribe(common.HttpCase): + def setUp(self): + super(TestConfirmUnsubscribe, self).setUp() + + cr = self.registry.cursor() + # apparently HttpCase does not properly update self.env? + self.env2 = env = Environment(cr, self.uid, {}) + self.partner = env['res.partner'].create({ + 'name': 'Bob', + 'email': 'bob@bob.bob' + }) + self.mailing_list = env['mail.channel'].create({ + 'name': 'Test Mailing List', + 'public': 'public', + }) + self.token = self.mailing_list._generate_action_token(self.partner.id, action='unsubscribe') + + def test_not_subscribed(self): + """Test warning works""" + self.env2.cr.release() + + self._unsubscribe_check("The address %s is already unsubscribed" % self.partner.email) + + @mute_logger('odoo.addons.website.models.ir_ui_view') + def test_not_subscribed_no_template(self): + """ Test warning works on db without template (code update w/o module update) """ + self.env2.ref('website_mail_channel.not_subscribed').unlink() + self.assertEquals( + self.env2['ir.model.data'].search_count([ + ('module', '=', 'website_mail_channel'), + ('name', '=', 'not_subscribed'), + ]), 0, 'XID for template should have been deleted') + self.env2.cr.release() + + self._unsubscribe_check("The address %s is already unsubscribed or was never subscribed to any mailing list" % self.partner.email) + + def test_wrong_token(self): + self.mailing_list.sudo().write({ + 'channel_partner_ids': [(4, self.partner.id, False)] + }) + self.env2.cr.release() + self.token = 'XXX' + + self._unsubscribe_check("Invalid or expired confirmation link.") + + def test_successful_unsubscribe(self): + self.mailing_list.sudo().write({ + 'channel_partner_ids': [(4, self.partner.id, False)] + }) + self.env2.cr.release() + + self._unsubscribe_check("You have been correctly unsubscribed") + + def _unsubscribe_check(self, text): + url = "/groups/unsubscribe/{}/{}/{}".format( + self.mailing_list.id, self.partner.id, + self.token + ) + # FIXME: may need to be altered during 11.0 forward port + r = self.url_open(url) + body = r.read().decode('utf-8') + # normalize space to make matching simpler + self.assertIn(text, u' '.join(body.split())) From 505a7f853fd6b9574d41b2701480ed2f06e58e6d Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Tue, 24 Oct 2017 13:47:17 +0200 Subject: [PATCH 03/23] [ADD] account_tax_cash_basis: add hook to change the cash basis base amount account To be able to retrieve the base amount of cash basis taxes lines, an additional move is created. However, this additional move contains a several lines that are considered as pollution in reports. This hook allows to move these additional lines to an another account if needed. -task: https://www.odoo.com/web#id=35605&view_type=form&model=project.task&action=333&active_id=967&menu_id=4720 --- .../models/account_partial_reconcile.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/account_tax_cash_basis/models/account_partial_reconcile.py b/addons/account_tax_cash_basis/models/account_partial_reconcile.py index a95e355b68f..67d7351da08 100644 --- a/addons/account_tax_cash_basis/models/account_partial_reconcile.py +++ b/addons/account_tax_cash_basis/models/account_partial_reconcile.py @@ -9,6 +9,15 @@ from odoo.tools import float_is_zero class AccountPartialReconcileCashBasis(models.Model): _inherit = 'account.partial.reconcile' + def _get_tax_cash_basis_base_account(self, line, tax): + ''' Get the account of lines that will contain the base amount of taxes. + + :param line: An account.move.line record + :param tax: An account.tax record + :return: An account record + ''' + return line.account_id + def _get_tax_cash_basis_lines(self, value_before_reconciliation): # Search in account_move if we have any taxes account move lines tax_group = {} @@ -44,11 +53,12 @@ class AccountPartialReconcileCashBasis(models.Model): total_by_cash_basis_account[key] = amount if any([tax.use_cash_basis for tax in line.tax_ids]): for tax in line.tax_ids: + account_id = self._get_tax_cash_basis_base_account(line, tax) line_to_create.append((0, 0, { 'name': '/', 'debit': currency_id.round(line.debit_cash_basis - line.debit * matched_percentage), 'credit': currency_id.round(line.credit_cash_basis - line.credit * matched_percentage), - 'account_id': line.account_id.id, + 'account_id': account_id.id, 'tax_ids': [(6, 0, [tax.id])], 'tax_exigible': True, })) @@ -56,7 +66,7 @@ class AccountPartialReconcileCashBasis(models.Model): 'name': '/', 'credit': currency_id.round(line.debit_cash_basis - line.debit * matched_percentage), 'debit': currency_id.round(line.credit_cash_basis - line.credit * matched_percentage), - 'account_id': line.account_id.id, + 'account_id': account_id.id, 'tax_exigible': True, })) From f45edfbe6d03b15977392dda9561cd7a3e16ce34 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Mon, 23 Oct 2017 11:53:29 +0200 Subject: [PATCH 04/23] [FIX] web: image widget is dependent on its record's last_update As a way to optimize loading, images are not necessarily fetched in db. They have, in their url a "unique" parameter, which is the last_update date on **the record** and controls on the python-side whether it should get the image from a cache or from the db. Before this commit, this __last_update field wasn't present in the view, so it wasn't fetched, and writes on a model's image worked but did not refresh. The image displayed was the old one. After this commit, when the image field widget is present, we force the loading of the __last_update field of the record. Upon update, the image displayed is the new one. OPW 777552 closes #20457 --- addons/web/static/src/js/fields/abstract_field.js | 9 +++++++++ addons/web/static/src/js/fields/basic_fields.js | 7 ++++++- addons/web/static/src/js/services/data_manager.js | 14 ++++++++++++++ .../web/static/tests/fields/basic_fields_tests.js | 9 +++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 957d8233410..07918f8cbd0 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -40,6 +40,15 @@ var AbstractField = Widget.extend({ custom_events: { navigation_move: '_onNavigationMove', }, + + /** + * An object representing fields to be fetched by the model eventhough not present in the view + * This object contains "field name" as key and an object as value. + * That value object must contain the key "type" + * see FieldBinaryImage for an example. + */ + fieldDependencies: {}, + /** * If this flag is set to true, the field widget will be reset on every * change which is made in the view (if the view supports it). This is diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 7a7b3fa370f..ce1a305277f 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -21,6 +21,7 @@ var framework = require('web.framework'); var session = require('web.session'); var utils = require('web.utils'); var view_dialogs = require('web.view_dialogs'); +var field_utils = require('web.field_utils'); var qweb = core.qweb; var _t = core._t; @@ -1127,6 +1128,10 @@ var AbstractFieldBinary = AbstractField.extend({ }); var FieldBinaryImage = AbstractFieldBinary.extend({ + fieldDependencies: _.extend({}, AbstractFieldBinary.prototype.fieldDependencies, { + __last_update: {type: 'datetime'}, + }), + template: 'FieldBinaryImage', placeholder: "/web/static/src/img/placeholder.png", events: _.extend({}, AbstractFieldBinary.prototype.events, { @@ -1149,7 +1154,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ id: JSON.stringify(this.res_id), field: this.nodeOptions.preview_image || this.name, // unique forces a reload of the image when the record has been updated - unique: (this.recordData.__last_update || '').replace(/[^0-9]/g, ''), + unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''), }); } } diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 4bf1748fbab..7e938770ba3 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -377,6 +377,11 @@ return core.Class.extend({ } } } + + if (attrs.Widget.prototype.fieldDependencies) { + attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies; + } + return attrs; }, /** @@ -397,6 +402,15 @@ return core.Class.extend({ if (node.tag === 'field') { fieldsInfo[node.attrs.name] = self._processField(viewType, fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {}); + + if (fieldsInfo[node.attrs.name].fieldDependencies) { + var deps = fieldsInfo[node.attrs.name].fieldDependencies; + for (var dependency_name in deps) { + if (!(dependency_name in fieldsInfo)) { + fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type}; + } + } + } return false; } return node.tag !== 'arch'; diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index f05d7abf08e..1d1eccf207d 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1427,8 +1427,10 @@ QUnit.module('basic_fields', { QUnit.module('FieldImage'); QUnit.test('image fields are correctly rendered', function (assert) { - assert.expect(5); + assert.expect(6); + this.data.partner.fields.__last_update = {string: "Last updated", type: "datetime"}; + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; this.data.partner.records[0].document = 'myimage'; var form = createView({ @@ -1439,7 +1441,10 @@ QUnit.module('basic_fields', { ' ' + '', res_id: 1, - mockRPC: function (route) { + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/read') { + assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image"); + } if (route === 'data:image/png;base64,myimage') { assert.ok(true, "should called the correct route"); return $.when('wow'); From 0b41f21c165f231054488fd660c04f2ac3dc2560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 25 Oct 2017 14:58:00 +0200 Subject: [PATCH 05/23] [FIX] web: buttons with confirm attribute works properly Before this commit, buttons in form view with the confirm attributes had a peculiar behaviour: - < saas-16: they saved the record as soon as you clicked on them, so the warning was basically ignored - in saas-16+ (the new views): the button did not save the record before calling the method, at all. This can be a problem, obviously. Also, it does not seem consistent to have a different behaviour when the user clicks on OK in the confirm dialog, compared to a button without the confirm attribute. So, with this commit, we just make sure that such buttons save the record before calling the method, just like other buttons. --- .../src/js/views/form/form_controller.js | 31 ++++++------ addons/web/static/tests/views/form_tests.js | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index df5ce2cc2c1..cd6959938ae 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -365,20 +365,8 @@ var FormController = BasicController.extend({ this._disableButtons(); - var attrs = event.data.attrs; - if (attrs.confirm) { - var d = $.Deferred(); - Dialog.confirm(this, attrs.confirm, { confirm_callback: function () { - self._callButtonAction(attrs, event.data.record); - }}).on("closed", null, function () { - d.resolve(); - }); - def = d.promise(); - } else if (attrs.special) { - def = this._callButtonAction(attrs, event.data.record); - } else { - // save the record but don't switch to readonly mode - def = this.saveRecord(this.handle, { + function saveAndExecuteAction () { + return self.saveRecord(self.handle, { stayInEdit: true, }).then(function () { // we need to reget the record to make sure we have changes made @@ -388,6 +376,21 @@ var FormController = BasicController.extend({ return self._callButtonAction(attrs, record); }); } + var attrs = event.data.attrs; + if (attrs.confirm) { + var d = $.Deferred(); + Dialog.confirm(this, attrs.confirm, { + confirm_callback: saveAndExecuteAction, + }).on("closed", null, function () { + d.resolve(); + }); + def = d.promise(); + } else if (attrs.special) { + def = this._callButtonAction(attrs, event.data.record); + } else { + // save the record but don't switch to readonly mode + def = saveAndExecuteAction(); + } if (event.data.showWow) { def.then(this.trigger_up.bind(this, 'show_wow')); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index c6a825b04b1..5b5cb002412 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5317,6 +5317,53 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('buttons with "confirm" attribute save before calling the method', function (assert) { + assert.expect(9); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + intercepts: { + execute_action: function (event) { + assert.step('execute_action'); + }, + }, + }); + + // click on button, and cancel in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.ok(form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should be disabled'); + $('.modal .modal-footer button.btn-default').click(); + assert.ok(!form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should no longer be disabled'); + + assert.verifySteps(['default_get']); + + // click on button, and click on ok in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.verifySteps(['default_get']); + $('.modal .modal-footer button.btn-primary').click(); + + assert.verifySteps(['default_get', 'create', 'read', 'execute_action']); + + form.destroy(); + }); + QUnit.test('buttons are disabled until action is resolved (in dialogs)', function (assert) { assert.expect(3); From 7b8001a847e9a89dc7203c5cf2ead715b9bcd45b Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 25 Oct 2017 10:15:25 +0200 Subject: [PATCH 06/23] [FIX] hr_expense,web: explicit link_to command for o2m The ORM doesn't automatically link subrecords when it receives an update command for a one2many. However, it may be necessary when the default_get returns existing records for a one2many field. It only worked before this rev. for the hr_expense model because of an hack from 2016 (to reproduce: go to Expenses, check some expenses from the list view, in Actions, click on 'Submit to Manager', the new hr_expense_sheet contains the checked expenses (one2many), and the webclient only sent an update command by linked subrecord). This rev. fixes the problem properly: when the default_get returns existing subrecords for a one2many field, a link_to command (4) is generated alongside the update (1) command. --- addons/hr_expense/models/hr_expense.py | 16 ---------------- .../web/static/src/js/views/basic/basic_model.js | 12 ++++++++++-- .../tests/fields/relational_fields_tests.js | 4 ++++ 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index ca045d82da6..19fcdf9dc2e 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -500,22 +500,6 @@ class HrExpenseSheet(models.Model): ).compute(expense.total_amount, self.currency_id) self.total_amount = total_amount - # FIXME: A 4 command is missing to explicitly declare the one2many relation - # between the sheet and the lines when using 'default_expense_line_ids':[ids] - # in the context. A fix from chm-odoo should come since - # several saas versions but sadly I had to add this hack to avoid this - # issue - @api.model - def _add_missing_default_values(self, values): - values = super(HrExpenseSheet, self)._add_missing_default_values(values) - if self.env.context.get('default_expense_line_ids', False): - lines_to_add = [] - for line in values.get('expense_line_ids', []): - if line[0] == 1: - lines_to_add.append([4, line[1], False]) - values['expense_line_ids'] = lines_to_add + values['expense_line_ids'] - return values - @api.one def _compute_attachment_number(self): self.attachment_number = sum(self.expense_line_ids.mapped('attachment_number')) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index c450e2bedc7..1d8e68c3e62 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2634,13 +2634,21 @@ var BasicModel = AbstractModel.extend({ } commands[fieldName].push(command); } else if (_.contains(addedIds, list.res_ids[i])) { - // this is a new id + // this is a new id (maybe existing in DB, but new in JS) relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); changes = this._generateChanges(relRecord, options); if ('id' in changes) { + // the subrecord already exists in db delete changes.id; - commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); + if (this.isNew(record.id)) { + // if the main record is new, link the subrecord to it + commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id)); + } + if (!_.isEmpty(changes)) { + commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); + } } else { + // the subrecord is new, so create it commands[fieldName].push(x2ManyCommands.create(changes)); } } diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index ac812aeb734..91102d8dd90 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1068,7 +1068,9 @@ QUnit.module('relational_fields', { assert.deepEqual( obj.turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], "should have properly created the x2many command list"); @@ -1097,7 +1099,9 @@ QUnit.module('relational_fields', { // unchanged state with the command 1, but this seems more // difficult. assert.deepEqual(args.args[0].turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], 'should send proper commands to create method'); } From 6fa63eb15ca809fce2770f8ee75af592a4609c6a Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 25 Oct 2017 13:18:35 +0200 Subject: [PATCH 07/23] [FIX] web: default value for o2m inside o2m A traceback was raised when opening a new record for a model with a one2many displayed inside a one2many (e.g. simply displaying the number of records in the relation), and with an onchange setting a default value to the inner one2many (for example, linking it to existing records). As the inner o2m has no subviews, it has no fieldsInfo, and the code assumed that fieldsInfo was always set. This was for example reproducible from v11 as follows: - create a product (with MTO/Buy and call for tender (need purchase agreement module)) - create an SO with this product and confirm it - go to purchase order form view and add a move_dest_ids field in the tree definition of field order_line - go to menu purchase agreement and choose the PA generated by your SO - add a vendor, confirm the PA and click on 'New Quotation'. --- .../static/src/js/views/basic/basic_model.js | 3 +- .../tests/fields/relational_fields_tests.js | 41 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 1d8e68c3e62..2cf96815b5e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2881,7 +2881,8 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} */ _isFieldProtected: function (record, fieldName, viewType) { - var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo && + (record.fieldsInfo[viewType || record.viewType][fieldName]); if (fieldInfo) { var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); var modifiers = this._evalModifiers(record, rawModifiers); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 91102d8dd90..2f7253abba0 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -5990,6 +5990,46 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('default value for nested one2manys (coming from onchange)', function (assert) { + assert.expect(3); + + this.data.partner.onchanges.p = function (obj) { + obj.p = [ + [5], + [0, 0, {turtles: [[5], [4, 1]]}], // link record 1 by default + ]; + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.strictEqual(args.args[0].p[0][0], 0, + "should send a command 0 (CREATE) for p"); + assert.deepEqual(args.args[0].p[0][2], {turtles: [[4, 1, false]]}, + "should send the correct values"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_data_cell').text(), '1 record', + "should correctly display the value of the inner o2m"); + + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('display correct value after validation error', function (assert) { assert.expect(4); @@ -6079,7 +6119,6 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { From 0c0fb63b0d7789b02cd1d6a0985e624e561a1ae1 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 31 Oct 2017 09:54:36 -0400 Subject: [PATCH 08/23] [FIX] web: Calendar view of project tasks are displaying on the wrong day Tasks are showing up on Wednesday when they are Thursday. Checked the dates on the tasks and my timezone preferences are set to Americas-New York. fix: Do not apply timezone for all day mode. --- addons/web/static/src/js/views/calendar/calendar_model.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index b32374057bb..9900c4b9f29 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -623,8 +623,10 @@ return AbstractModel.extend({ date_stop = date_start.clone().add(date_delay,'hours'); } - date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); - date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + if (!all_day) { + date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); + date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + } if (this.mapping.all_day && evt[this.mapping.all_day]) { date_stop.add(1, 'days'); From ffa4646af7bb1f8263b0d7ee012e435a5855f2b0 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 31 Oct 2017 09:55:50 -0400 Subject: [PATCH 09/23] [FIX] web: calendar don't return to month mode in US When click on the mini calendar, the calendar does not return to month mode. --- .../static/src/js/views/calendar/calendar_controller.js | 2 +- addons/web/static/src/js/views/calendar/calendar_model.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index d46cb1c5241..94bd168ef0d 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -113,7 +113,7 @@ var CalendarController = AbstractController.extend({ */ _onChangeDate: function (event) { var modelData = this.model.get(); - if (modelData.target_date.isSame(event.data.date)) { + if (modelData.target_date.format('YYYY-MM-DD') === event.data.date.format('YYYY-MM-DD')) { // When clicking on same date, toggle between the two views switch (modelData.scale) { case 'month': this.model.setScale('week'); break; diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 9900c4b9f29..a0f6515b98c 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -255,6 +255,8 @@ return AbstractModel.extend({ */ setDate: function (start, highlight) { this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; + this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); + switch (this.data.scale) { case 'month': this.data.start_date = this.data.start_date.clone().startOf('month').startOf('week'); @@ -391,6 +393,12 @@ return AbstractModel.extend({ firstDay: moment().startOf('week').isoWeekday(), }; }, + /** + * Return a domain from the date range + * + * @private + * @returns {Array} + */ _getRangeDomain: function () { // Build OpenERP Domain to filter object by this.mapping.date_start field // between given start, end dates. From e2190860a6bbd79dbd4ec8139c04b4b9d64c90e5 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 25 Oct 2017 10:31:01 -0400 Subject: [PATCH 10/23] [FIX] web: calendar tests are good in US timezone Calendar tests did not go into US for a timezone problem, making it more difficult to correct errors. To do: pass all the tests js in US (with simplification for the calculations of the times) --- addons/web/static/tests/views/calendar_tests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 8d41fba39e3..31d30632da0 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -21,7 +21,8 @@ CalendarRenderer.include({ var createView = testUtils.createView; -var initialDate = new Date("2016-12-12T08:00:00Z"); +var initialDate = new Date(2016, 11, 12, 8, 0, 0); +initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000); QUnit.module('Views', { From 365362a4a0d86701d6f991b3917528d02923b22d Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Fri, 13 Oct 2017 17:02:52 +0200 Subject: [PATCH 11/23] [FIX] mrp: on rereserve of mo, we can unlink stock.move.lots On rereservation of an mo, it will remove the existing stock.move.lots if no quantity was done on them. We wanted to avoid however that it would remove the temporary stock.move.lots on the workorder. On the normal ones however, there is also a workorder_id. That way it would not unlink stock.move.lots when you had workorders. By checking the temporary or not flag (done_wo) instead of workorder_id, we solve the issue. Courtesy of blaggacao fixes #19422 --- addons/mrp/models/stock_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 183bd0dc1c6..dba5d5cd0d3 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -161,7 +161,7 @@ class StockMove(models.Model): def create_lots(self): lots = self.env['stock.move.lots'] for move in self: - unlink_move_lots = move.move_lot_ids.filtered(lambda x : (x.quantity_done == 0) and not x.workorder_id) + unlink_move_lots = move.move_lot_ids.filtered(lambda x : (x.quantity_done == 0) and x.done_wo) unlink_move_lots.sudo().unlink() group_new_quant = {} old_move_lot = {} From 6b767c8b1f43846812aec8a119782439a2086af3 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 25 Oct 2017 14:50:21 +0200 Subject: [PATCH 12/23] [FIX] web_editor: properly show error line when saving wrong XML When using the HTML editor, when attempting to save a syntax-invalid XML, the error line is supposed to be displayed in red. This was not the case anymore since saas-15. This was a simple mistake of comparison between a stringified integer ID and its integer equivalent. --- addons/web_editor/static/src/js/ace.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js index fed52c06f1d..45a8214c255 100644 --- a/addons/web_editor/static/src/js/ace.js +++ b/addons/web_editor/static/src/js/ace.js @@ -449,7 +449,8 @@ var ViewEditor = Widget.extend({ * @return the currently resource id (view ID or less file URL) */ selectedResource: function () { - return this.$lists[this.currentType].select2("val"); + var value = this.$lists[this.currentType].select2("val"); + return parseInt(value, 10) || value; }, /** * The displayResource method forces the view/less file identified by its ID/URL to be displayed in the editor. From 1c367e32d6ca49b0310aea2262e9cfc9fd1a83fb Mon Sep 17 00:00:00 2001 From: Cedric Snauwaert Date: Thu, 26 Oct 2017 10:35:24 +0200 Subject: [PATCH 13/23] [FIX] account: reconciliation: fix trouble with reconciliation in multi-currency This fixes several problems with the reconciliation widget when used with multi-currency. - company in EUR, create invoice 25 USD, statement of 25 USD (in journal USD), try to reconcile, the amount is not correctly converted, we see two different values, can't reconcile - company in EUR, create invoice in EUR, statement in USD journal, the amount displayed for the EUR invoice is not correct --- addons/account/models/account_move.py | 94 ++++++++++--------- .../js/reconciliation/reconciliation_model.js | 2 +- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 09086ccb15c..1cd55712dd8 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -753,54 +753,60 @@ class AccountMoveLine(models.Model): amount = abs(debit - credit) amount_currency = abs(line.amount_currency) - # Get right debit / credit: target_currency = target_currency or company_currency - amount_currency = debit - credit - amount_currency_str = "" - total_amount_currency_str = "" - if line_currency != company_currency and target_currency == line_currency: - # The payment currency is the invoice currency, but they are different than the company currency - # We use the `amount_currency` computed during the invoice validation, at the invoice date - # to avoid exchange gain/loss - # e.g. an invoice of 100€ must be paid with 100€, whatever the company currency and the exchange rates - total_amount = line.amount_currency - actual_debit = debit > 0 and amount_currency or 0.0 - actual_credit = credit > 0 and -amount_currency or 0.0 - currency = line_currency - else: - # Either: - # - the invoice, payment, company currencies are all the same, - # - the payment currency is the company currency, but the invoice currency is different, - # - the invoice currency is the company currency, but the payment currency is different, - # - the invoice, payment and company currencies are all different. - # For the two first cases, we can simply use the debit/credit of the invoice move line, which are always in the company currency, - # and this is what the target need. - # For the two last cases, we can use the debit/credit which are in the company currency, and then change them to the target currency - total_amount = abs(debit - credit) - actual_debit = debit > 0 and amount or 0.0 - actual_credit = credit > 0 and -amount or 0.0 - currency = company_currency + + ctx = context.copy() + ctx.update({'date': target_date or line.date}) + # Use case: + # Let's assume that company currency is in USD and that we have the 3 following move lines + # Debit Credit Amount currency Currency + # 1) 25 0 0 NULL + # 2) 17 0 25 EUR + # 3) 33 0 25 YEN + # + # If we ask to see the information in the reconciliation widget in company currency, we want to see + # The following informations + # 1) 25 USD (no currency information) + # 2) 17 USD [25 EUR] (show 25 euro in currency information, in the little bill) + # 3) 33 USD [25 YEN] (show 25 yen in currencu information) + # + # If we ask to see the information in another currency than the company let's say EUR + # 1) 35 EUR [25 USD] + # 2) 25 EUR (no currency information) + # 3) 50 EUR [25 YEN] + # In that case, we have to convert the debit-credit to the currency we want and we show next to it + # the value of the amount_currency or the debit-credit if no amount currency + if target_currency == company_currency: + if line_currency == target_currency: + amount = amount + amount_currency = "" + total_amount = debit - credit + total_amount_currency = "" + else: + amount = amount + amount_currency = amount_currency + total_amount = debit - credit + total_amount_currency = line.amount_currency - if line_currency != target_currency: - amount_currency = target_currency.compute(total_amount, line_currency) - amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=line_currency) - total_amount_currency_str = formatLang(self.env, target_currency.compute(total_amount, line_currency), currency_obj=line_currency) - if currency != target_currency: - ctx = context.copy() - ctx.update({'date': target_date or line.date}) - total_amount = currency.with_context(ctx).compute(total_amount, target_currency) - actual_debit = currency.with_context(ctx).compute(actual_debit, target_currency) - actual_credit = currency.with_context(ctx).compute(actual_credit, target_currency) - amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency) - total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency) + if target_currency != company_currency: + if line_currency == target_currency: + amount = amount_currency + amount_currency = "" + total_amount = line.amount_currency + total_amount_currency = "" + else: + amount_currency = line.currency_id and amount_currency or amount + amount = company_currency.with_context(ctx).compute(amount, target_currency) + total_amount = company_currency.with_context(ctx).compute((line.debit - line.credit), target_currency) + total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit) - ret_line['debit'] = abs(actual_debit) - ret_line['credit'] = abs(actual_credit) - ret_line['amount_str'] = amount_str - ret_line['total_amount_str'] = total_amount_str - ret_line['amount_currency_str'] = amount_currency_str + ret_line['debit'] = amount > 0 and amount or 0 + ret_line['credit'] = amount < 0 and -amount or 0 ret_line['amount_currency'] = amount_currency - ret_line['total_amount_currency_str'] = total_amount_currency_str + ret_line['amount_str'] = formatLang(self.env, abs(amount), currency_obj=target_currency) + ret_line['total_amount_str'] = formatLang(self.env, abs(total_amount), currency_obj=target_currency) + ret_line['amount_currency_str'] = amount_currency and formatLang(self.env, abs(amount_currency), currency_obj=line_currency) or "" + ret_line['total_amount_currency_str'] = total_amount_currency and formatLang(self.env, abs(total_amount_currency), currency_obj=line_currency) or "" ret.append(ret_line) return ret diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 2e6d81ae998..11116fab070 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -802,7 +802,7 @@ var StatementModel = BasicModel.extend({ if (!prop.invalid) { total -= prop.amount; if (isOtherCurrencyId) { - amount_currency -= prop.amount_currency; + amount_currency -= (prop.amount < 0 ? -1 : 1) * Math.abs(prop.amount_currency); } } }); From 4a3801bc58a38551fba59c9830b68a582e4857c1 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Thu, 26 Oct 2017 11:10:13 +0200 Subject: [PATCH 14/23] [FIX] google_calendar: allow to use the function number_of_week for synchro get_param return string, but timedelta wait a integer --- addons/google_calendar/google_calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 48950928199..39f4a075ba5 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -954,7 +954,7 @@ class google_calendar(osv.AbstractModel): def get_minTime(self, cr, uid, context=None): number_of_week = int(self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.week_synchro', default=13)) - return datetime.now() - timedelta(weeks=number_of_week) + return datetime.now() - timedelta(weeks=int(number_of_week)) def get_need_synchro_attendee(self, cr, uid, context=None): return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_synchro_attendee', default=True) From b80b13a572e478192e09715b68cdbe0404b196ee Mon Sep 17 00:00:00 2001 From: Pierre Rousseau Date: Thu, 26 Oct 2017 10:22:21 +0200 Subject: [PATCH 15/23] [FIX] l10n_fr_fec: change EcritureNum opw-777652 --- addons/l10n_fr_fec/wizard/fec.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/l10n_fr_fec/wizard/fec.py b/addons/l10n_fr_fec/wizard/fec.py index 1b1ddf20e9b..d56762b537c 100644 --- a/addons/l10n_fr_fec/wizard/fec.py +++ b/addons/l10n_fr_fec/wizard/fec.py @@ -5,6 +5,8 @@ from openerp import models, fields, api, _ from openerp.exceptions import Warning +from datetime import datetime +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT import base64 import StringIO import csv @@ -32,7 +34,7 @@ class AccountFrFec(models.TransientModel): SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'Balance initiale PL' AS EcritureNum, + 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, '120/129' AS CompteNum, 'Benefice (perte) reporte(e)' AS CompteLib, @@ -66,8 +68,10 @@ class AccountFrFec(models.TransientModel): ''' company = self.env.user.company_id formatted_date_from = self.date_from.replace('-', '') + date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + formatted_date_year = date_from.year self._cr.execute( - sql_query, (formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) + sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) listrow = [] row = self._cr.fetchone() listrow = list(row) @@ -129,7 +133,7 @@ class AccountFrFec(models.TransientModel): SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'Balance initiale ' || MIN(aa.name) AS EcritureNum, + 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN(aa.name), '|', '/') AS CompteLib, @@ -169,8 +173,10 @@ class AccountFrFec(models.TransientModel): HAVING sum(aml.balance) != 0 ''' formatted_date_from = self.date_from.replace('-', '') + date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + formatted_date_year = date_from.year self._cr.execute( - sql_query, (formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) + sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) From e653798d5c7e2de957ab35903e54b5998b0418b2 Mon Sep 17 00:00:00 2001 From: "Benjamin Willig (ACSONE)" Date: Thu, 26 Oct 2017 16:01:56 +0200 Subject: [PATCH 16/23] [FIX] hr_expense: allow inheritance when adding followers (#20319) --- addons/hr_expense/models/hr_expense.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index ee75dae5c52..f4cb1a69c9d 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -420,13 +420,7 @@ class HrExpenseSheet(models.Model): @api.model def create(self, vals): - # Add the followers at creation, so they can be notified - if vals.get('employee_id'): - employee = self.env['hr.employee'].browse(vals['employee_id']) - users = self._get_users_to_subscribe(employee=employee) - self.env.user - vals['message_follower_ids'] = [] - for partner in users.mapped('partner_id'): - vals['message_follower_ids'] += self.env['mail.followers']._add_follower_command(self._name, [], {partner.id: None}, {})[0] + self._create_set_followers(vals) sheet = super(HrExpenseSheet, self).create(vals) self.check_consistency() return sheet @@ -478,6 +472,20 @@ class HrExpenseSheet(models.Model): users = self._get_users_to_subscribe() self.message_subscribe_users(user_ids=users.ids) + @api.model + def _create_set_followers(self, values): + # Add the followers at creation, so they can be notified + employee_id = values.get('employee_id') + if not employee_id: + return + + employee = self.env['hr.employee'].browse(employee_id) + users = self._get_users_to_subscribe(employee=employee) - self.env.user + values['message_follower_ids'] = [] + MailFollowers = self.env['mail.followers'] + for partner in users.mapped('partner_id'): + values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0] + @api.onchange('employee_id') def _onchange_employee_id(self): self.address_id = self.employee_id.address_home_id From a44954b625132fea74e1bacd2ef6334f6645ca79 Mon Sep 17 00:00:00 2001 From: Denis Vermylen Date: Wed, 25 Oct 2017 16:14:46 +0200 Subject: [PATCH 17/23] [FIX] sale: pricelist recursion in without_discount Supplements https://github.com/odoo/odoo/commit/0d56dca When basing a pricelist on another pricelist in different currency conversion was not being made correctly, since commit https://github.com/odoo/odoo/commit/6b3a808 that changes the pricelist_item on which the currency_id is set in _get_real_price_currency. Also align the _get_display_price's base_price to the _onchange_discount's new_list_price in the case where the pricelist depends on other pricelists that are in without_discount mode as well. --- addons/sale/models/sale.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index a235937c3a0..6e68fdad57a 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -851,14 +851,11 @@ class SaleOrderLine(models.Model): if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id).price final_price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - pricelist_item = self.env['product.pricelist.item'].browse(rule_id) - if pricelist_item.base == 'pricelist': - base_price, rule_id = pricelist_item.base_pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - base_price = pricelist_item.base_pricelist_id.currency_id.compute(base_price, self.order_id.pricelist_id.currency_id) - else: - base_price = product[pricelist_item.base] if pricelist_item else product.lst_price - base_price = product.currency_id.compute(base_price, self.order_id.pricelist_id.currency_id) - # negative discounts (= surcharge) are included in the display price (= unit price) + context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order) + base_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + if currency_id != self.order_id.pricelist_id.currency_id.id: + base_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(base_price, self.order_id.pricelist_id.currency_id) + # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) @api.multi @@ -1004,8 +1001,8 @@ class SaleOrderLine(models.Model): new_list_price = self.env['account.tax']._fix_tax_included_price_company(new_list_price, self.product_id.taxes_id, self.tax_id, self.company_id) if new_list_price != 0: - if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: - # new_list_price is in company's currency while price in pricelist currency + if self.order_id.pricelist_id.currency_id.id != currency_id: + # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) discount = (new_list_price - price) / new_list_price * 100 if discount > 0: From 9dee7d1e58eb555eed1f3dbc5be396eed3ec2d7f Mon Sep 17 00:00:00 2001 From: Denis Vermylen Date: Wed, 25 Oct 2017 17:52:59 +0200 Subject: [PATCH 18/23] [FIX] sale: pricelist without_discount included taxes When having a product with included taxes that are changed/removed with a fiscal position, the price changes depending on if the pricelist's discount_policy is with_discount or without_discount. This commit aligns the without_discount behavior to the with_discount behavior. Note: the behaviour isn't 100% correct. The applied discount will be computed on the list_price, instead of the tax excluded list_price. If a price_surcharge is set, it will be wrong. --- addons/sale/models/sale.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 6e68fdad57a..40ce0e98088 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -998,7 +998,6 @@ class SaleOrderLine(models.Model): price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) - new_list_price = self.env['account.tax']._fix_tax_included_price_company(new_list_price, self.product_id.taxes_id, self.tax_id, self.company_id) if new_list_price != 0: if self.order_id.pricelist_id.currency_id.id != currency_id: From ab9b5a13e4a610ae74a633e989620d5c3da240be Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Oct 2017 17:23:14 +0200 Subject: [PATCH 19/23] [FIX] l10n_ca: default stock input account for products categories Since Odoo 8.0, the default stock input account for product categories in the Canadian localization is set to `214100 CANADA REVENUE AGENCY` This is the case since this commit: https://github.com/odoo/odoo/commit/13dacd11c10dac853def763432829b8976604a7d#diff-2e65e26a4efc4ab95e72dbe2033141ecL294 In which the account with the XML ID 2141_en 214100 Stock Received But Not Billed has been renamed 214100 CANADA REVENUE AGENCY In this very same commit, the account "Stock Received But Not Billed" has been moved to the account 217100, under the XML ID chart2171_en: https://github.com/odoo/odoo/commit/13dacd11c10dac853def763432829b8976604a7d#diff-2e65e26a4efc4ab95e72dbe2033141ecR447 While the default value for the products categories stock input account remained the same, the account with as code 2141: https://github.com/odoo/odoo/blob/8.0/addons/l10n_ca/account_chart_template_en.xml#L8 This is an oversight. It was not meant that way. The default stock input account for products should well be "Stock Received But Not Billed". In addition, a stock account is supposed to be of type assets, and not of type liabilities. I contacted @max3903, who was a contributor of the l10n_ca localization, and who is therefore a better expert than me regarding the Canadian localization. He confirmed me all the above findings. opw-775413 --- addons/l10n_ca/account_chart.xml | 2 +- addons/l10n_ca/account_chart_template_after.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/l10n_ca/account_chart.xml b/addons/l10n_ca/account_chart.xml index 0a463374290..8610079a78f 100644 --- a/addons/l10n_ca/account_chart.xml +++ b/addons/l10n_ca/account_chart.xml @@ -217,7 +217,7 @@ 2171 - + Stock Received But Not Billed diff --git a/addons/l10n_ca/account_chart_template_after.xml b/addons/l10n_ca/account_chart_template_after.xml index ccf130b9f0e..815fcea8b59 100644 --- a/addons/l10n_ca/account_chart_template_after.xml +++ b/addons/l10n_ca/account_chart_template_after.xml @@ -9,7 +9,7 @@ - + From 809aaf9e7b9de28383444d5059b19a67bb513f51 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Thu, 26 Oct 2017 17:02:14 +0200 Subject: [PATCH 20/23] [FIX] website_event: avoid double subscription Before this commit, you was able to double click on the button when are in registration flow. In this case, you subscribe 2 times and so take 2x more seats, what can be annoying when you have a limited room. In the same time, we fix the form in the form that generate strange behaviour like some events not bubbled correctly. The attendee form (into the modal) was inside the registration form. $'attendee_form).on('submit') obviously failed due to this bad dom. This commit closes opw-778191 --- addons/website/static/src/js/website.js | 2 +- addons/website_event/static/src/js/website_event.js | 4 +++- addons/website_event/views/website_event.xml | 4 ++-- .../views/website_event_questions_templates.xml | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 4e5b805fa8c..c353590510e 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -228,7 +228,7 @@ base.ready().then(function () { init_kanban(this); }); - $('.js_website_submit_form').on('submit', function() { + $('body').on('submit', '.js_website_submit_form', function() { var $buttons = $(this).find('button[type="submit"], a.a-submit'); _.each($buttons, function(btn) { $(btn).attr('data-loading-text', ' ' + $(btn).text()).button('loading'); diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js index 3b2248742a9..b5a31ae7950 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -9,6 +9,7 @@ $(document).ready(function () { .off('click') .removeClass('a-submit') .click(function (ev) { + $(this).attr('disabled', true); ev.preventDefault(); ev.stopPropagation(); var $form = $(ev.currentTarget).closest('form'); @@ -18,7 +19,8 @@ $(document).ready(function () { }); ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) { var $modal = $(modal); - $modal.appendTo($form).modal(); + $modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19 + $modal.after($form).modal(); $modal.on('click', '.js_goto_event', function () { $modal.modal('hide'); }); diff --git a/addons/website_event/views/website_event.xml b/addons/website_event/views/website_event.xml index 69701ce0056..a1c7e8e0588 100644 --- a/addons/website_event/views/website_event.xml +++ b/addons/website_event/views/website_event.xml @@ -441,7 +441,7 @@