From d3d3455e1798aad457b58da4cc49081b838d1321 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 11 Sep 2018 21:48:03 +0200 Subject: [PATCH 1/6] [FIX] web: lock handle when sorting on a different field This makes runbot Enterprise fail. Reverts until a proper solution is found. This reverts commit f4f1480a3e6c437c7d14929fadd0782a53171dd5. --- .../static/src/js/views/list/list_renderer.js | 3 +- addons/web/static/tests/views/list_tests.js | 70 ------------------- 2 files changed, 1 insertion(+), 72 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 b65b93f7fdc..2bb1dece89b 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -184,6 +184,7 @@ var ListRenderer = BasicRenderer.extend({ reject = columnInvisibleFields[c.attrs.name]; } if (!reject && c.attrs.widget === 'handle') { + self.hasHandle = true; self.handleField = c.attrs.name; } return reject; @@ -648,8 +649,6 @@ var ListRenderer = BasicRenderer.extend({ this._computeAggregates(); $table.toggleClass('o_list_view_grouped', is_grouped); $table.toggleClass('o_list_view_ungrouped', !is_grouped); - this.hasHandle = this.state.orderedBy.length === 0 || - this.state.orderedBy[0].name === this.handleField; if (is_grouped) { $table .append(this._renderHeader(true)) diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 9203cad8b99..eebf09c9365 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3175,76 +3175,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) { - assert.expect(6); - - // we need another sortable field to lock/unlock the handle - this.data.foo.fields.amount.sortable = true; - // resequence makes sense on a sequence field, not on arbitrary fields - this.data.foo.records[0].int_field = 0; - this.data.foo.records[1].int_field = 1; - this.data.foo.records[2].int_field = 2; - this.data.foo.records[3].int_field = 3; - - var list = createView({ - View: ListView, - model: 'foo', - data: this.data, - arch: '' + - '' + - '' + - '', - }); - - assert.strictEqual(list.$('tbody').text(), '1200.00500.00300.000.00', - "default should be sorted by int_field"); - - // Drag and drop the fourth line in second position - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - // Handle should be unlocked at this point - assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', - "drag and drop should have succeeded, as the handle is unlocked"); - - // Sorting by a field different for int_field should lock the handle - list.$('.o_column_sortable').eq(1).click(); - - assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', - "should have been sorted by amount"); - - // Drag and drop the fourth line in second position (not) - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', - "drag and drop should have failed as the handle is locked"); - - // Sorting by int_field should unlock the handle - list.$('.o_column_sortable').eq(0).click(); - - assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', - "records should be ordered as per the previous resequence"); - - // Drag and drop the fourth line in second position - testUtils.dragAndDrop( - list.$('.ui-sortable-handle').eq(3), - list.$('tbody tr').first(), - {position: 'bottom'} - ); - - assert.strictEqual(list.$('tbody').text(), '1200.00300.000.00500.00', - "drag and drop should have worked as the handle is unlocked"); - - list.destroy(); - }); - QUnit.test('editable list with handle widget with slow network', function (assert) { assert.expect(15); From a2acef42a0526ac023f3ca1c194a41d6c7ee5475 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 11 Sep 2018 14:21:04 +0200 Subject: [PATCH 2/6] [FIX] account: reconciliation models The reconciliation models are not available in the Manual Reconciliation widget. This is simply because they were not loaded. opw-1878944 --- .../src/js/reconciliation/reconciliation_model.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 9748f6c63e3..a975380d005 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -1171,7 +1171,15 @@ var ManualModel = StatementModel.extend({ self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code')); }); - return def_account.then(function () { + var def_reconcileModel = this._rpc({ + model: 'account.reconcile.model', + method: 'search_read', + }) + .then(function (reconcileModels) { + self.reconcileModels = reconcileModels; + }); + + return $.when(def_reconcileModel, def_account).then(function () { switch(context.mode) { case 'customers': case 'suppliers': @@ -1372,7 +1380,7 @@ var ManualModel = StatementModel.extend({ offset: 0, limitMoveLines: this.limitMoveLines, filter: "", - reconcileModels: [], + reconcileModels: this.reconcileModels, account_id: this._formatNameGet([data.account_id, data.account_name]), st_line: data, visible: true From 561ee39356408cb7edcffc694291dd029f236e3f Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 11 Sep 2018 16:26:23 +0200 Subject: [PATCH 3/6] [FIX] sale_timesheet: Validating an invoice with timesheets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce the bug: - Go to Timesheets > Settings and set Invoicing policy to “Approved Timesheets Only” - Create a expensable product P with Type "Service", Invoice based on “Timesheets on tasks” and Service Tracking “Create a new project but no Task” - Create a new SO using P and confirm it. A project PR will be created - Add a task TA to PR - Add two timesheets(T1, T2) to TA, one for the current date(T1) and one for a future date(+1 month, T2) - Go to timesheets and validate” the timesheet T1 - Go to the SO and now the delivered_qty is equal to 1, so create the invoice I - Validate I Bug: T1 and T2 were linked to I instead of just T1 (because invoice based on “Approved Timesheets Only”) opw:1879051 --- addons/sale_timesheet/models/account_invoice.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index b177ecb6311..0c9fa7de28e 100644 --- a/addons/sale_timesheet/models/account_invoice.py +++ b/addons/sale_timesheet/models/account_invoice.py @@ -51,15 +51,19 @@ class AccountInvoice(models.Model): self._compute_timesheet_revenue() return result + def _get_compute_timesheet_revenue_domain(self, so_line_ids): + return [ + ('so_line', 'in', so_line_ids), + ('project_id', '!=', False), + ('timesheet_invoice_id', '=', False), + ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed']) + ] + def _compute_timesheet_revenue(self): for invoice in self: for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)): - uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([ - ('so_line', 'in', invoice_line.sale_line_ids.ids), - ('project_id', '!=', False), - ('timesheet_invoice_id', '=', False), - ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed']) - ]) + domain = self._get_compute_timesheet_revenue_domain(invoice_line.sale_line_ids.ids) + uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search(domain) # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs) if uninvoiced_timesheet_lines: From 8e77632e5854477f802a3b2944b234dd45b46411 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Tue, 11 Sep 2018 16:53:23 +0200 Subject: [PATCH 4/6] [FIX] *mail_thread: don't modify modified help The 'help' of window action is often fiddled with, adding thing before, after or arround it. For example in CRM leads, we add at the beginning "Click to add a new opportunity" with an arrow towards the button, and after if there is a mail alias: "All email incoming to * will automatically create new...". But for crm.lead, hr.expense, sale.order this would not take into account that the fiddled "help" can be edited, so if we edit 2 times help in studio or backend action editing, we would get: Click to add a new opportunity Click to add a new opportunity Click to add a new opportunity [Original help content] All email incoming to * will automatically create new... All email incoming to * will automatically create new... All email incoming to * will automatically create new... And see several "arrows" towards the button (in enterprise the additional ones are on same color background). With this commit we do what is done in "mail.thread" by default which is not fiddling with the `help` if has been fiddled before (if it contains "oe_view_nocontent_create" class). note: this is the 11.0 version of #26912 opw-1877663 closes #26911 --- addons/crm/models/crm_lead.py | 6 ++++-- addons/hr_expense/models/hr_expense.py | 4 ++-- addons/im_livechat/models/mail_channel.py | 2 +- addons/mail/models/mail_thread.py | 4 +++- addons/sale/models/sale.py | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 026cb46fcb7..2ec7aa5f0bf 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -914,7 +914,7 @@ class Lead(models.Model): @api.model def get_empty_list_help(self, help): - if help: + if help and help.find("oe_view_nocontent_create") == -1: alias_record = self.env.ref("crm.mail_alias_lead_info", raise_if_not_found=False) if alias_record and alias_record.alias_domain and alias_record.alias_name: email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) @@ -922,7 +922,9 @@ class Lead(models.Model): dynamic_help = _("""All email incoming to %s will automatically create new opportunity. Update your business card, phone book, social media,... Send an email right now and see it here.""") % (email_link,) - return '

%s

%s

%s

' % (_('Click to add a new opportunity'), help, dynamic_help) + return '

%s

%s

%s

' % ( + _('Click to add a new opportunity'), help, dynamic_help + ) return super(Lead, self.with_context( empty_list_help_model='crm.team', empty_list_help_id=self._context.get('default_team_id', False), diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 7031aec788b..1cc6e2cbd45 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -331,14 +331,14 @@ class HrExpense(models.Model): @api.model def get_empty_list_help(self, help_message): - if help_message: + if help_message and help_message.find("oe_view_nocontent_create") == -1: use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway') alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False if alias_record and alias_record.alias_domain and alias_record.alias_name: link = "%(email)s" % { 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) } - return '

%s
%s

%s' % ( + return '

%s
%s

%s' % ( _('Click to add a new expense,'), _('or send receipts by email to %s.') % (link,), help_message) diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index c64bcd7108c..2e2dc5fee0a 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -104,7 +104,7 @@ class MailChannel(models.Model): @api.model def get_empty_list_help(self, help): if help: - return '%s

' % (help) + return '

%s

' % (help) return super(MailChannel, self).get_empty_list_help(help) def _define_command_history(self): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index fa819f038b1..7ffce440383 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -349,7 +349,9 @@ class MailThread(models.AbstractModel): 'email_link': email_link } } - return "%(static_help)s

%(dyn_help)s" % { + # do not add alias two times if it was added previously + if not help or help.find("oe_view_nocontent_alias") == -1: + return '%(static_help)s

%(dyn_help)s

' % { 'static_help': help or '', 'dyn_help': _("You could also add a new %(document)s by sending an email to: %(email_link)s.") % { 'document': document_name, diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 779ab3c809c..b70657578f5 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -89,8 +89,8 @@ class SaleOrder(models.Model): @api.model def get_empty_list_help(self, help): - if help: - return '

%s

' % (help) + if help and help.find("oe_view_nocontent_create") == -1: + return '

%s

' % (help) return super(SaleOrder, self).get_empty_list_help(help) def _get_default_access_token(self): From 8e15dc87a4b89182e36f5ed6e0f78121a0a8f43f Mon Sep 17 00:00:00 2001 From: Wolfgang Taferner Date: Sat, 25 Nov 2017 13:04:13 +0100 Subject: [PATCH 5/6] [IMP] account: extract_payment_vals + explicit communication hand-over --- addons/account/models/account_invoice.py | 35 ++++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 9ab256f4042..8735aa0fec0 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1341,18 +1341,7 @@ class AccountInvoice(models.Model): new_invoices += refund_invoice return new_invoices - @api.multi - def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None): - """ Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice. - - :param pay_journal: journal in which the payment entry will be created - :param pay_amount: amount of the payment to register, defaults to the residual of the invoice - :param date: payment date, defaults to fields.Date.context_today(self) - :param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid - """ - if isinstance(pay_journal, pycompat.integer_types): - pay_journal = self.env['account.journal'].browse([pay_journal]) - assert len(self) == 1, "Can only pay one invoice at a time." + def _prepare_payment_vals(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None, communication=None): payment_type = self.type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' if payment_type == 'inbound': payment_method = self.env.ref('account.account_payment_method_manual_in') @@ -1361,9 +1350,10 @@ class AccountInvoice(models.Model): payment_method = self.env.ref('account.account_payment_method_manual_out') journal_payment_methods = pay_journal.outbound_payment_method_ids - communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number - if self.origin: - communication = '%s (%s)' % (communication, self.origin) + if not communication: + communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number + if self.origin: + communication = '%s (%s)' % (communication, self.origin) payment_vals = { 'invoice_ids': [(6, 0, self.ids)], @@ -1378,7 +1368,22 @@ class AccountInvoice(models.Model): 'payment_difference_handling': writeoff_acc and 'reconcile' or 'open', 'writeoff_account_id': writeoff_acc and writeoff_acc.id or False, } + return payment_vals + @api.multi + def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None): + """ Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice. + + :param pay_journal: journal in which the payment entry will be created + :param pay_amount: amount of the payment to register, defaults to the residual of the invoice + :param date: payment date, defaults to fields.Date.context_today(self) + :param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid + """ + if isinstance(pay_journal, pycompat.integer_types): + pay_journal = self.env['account.journal'].browse([pay_journal]) + assert len(self) == 1, "Can only pay one invoice at a time." + + payment_vals = self._prepare_payment_vals(pay_journal, pay_amount=pay_amount, date=date, writeoff_acc=writeoff_acc) payment = self.env['account.payment'].create(payment_vals) payment.post() From db0e84f3e364919baf7bb32ba0d44f10a0f76187 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Wed, 12 Sep 2018 17:34:56 +0200 Subject: [PATCH 6/6] [FIX] web: revert ignore default_* on group creation This reverts commit 4cb585ab4f880147fbe35eb988c3c1da9728f624. When creating from the kanban view: - crm.stage - hr.recruitment.stage - project.task.type - helpdesk.type - mrp.eco.stage We go from a dashboard of eg. "GroupRecord" named "group_id" in the record. And when a stage is created, "default_group_id" is used. With the change the stage was not linked to the current "grouping". We could probably use "search_default_group_id" instead but the behavior may be present in existing customization. opw-1884217 closes #26943 --- .../src/js/views/kanban/kanban_model.js | 6 +-- addons/web/static/tests/views/kanban_tests.js | 37 ------------------- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 40c78ef9efc..7d47a081b0a 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -60,15 +60,11 @@ var KanbanModel = BasicModel.extend({ if (!groupByField || groupByField.type !== 'many2one') { return $.Deferred().reject(); // only supported when grouped on m2o } - // ignore action default which may be for another model - var context = _.pick(parent.context, function (value, key) { - return key.slice(0,8) !== 'default_'; - }); return this._rpc({ model: groupByField.relation, method: 'name_create', args: [name], - context: context, // todo: combine with view context + context: parent.context, // todo: combine with view context }) .then(function (result) { var newGroup = self._makeDataPoint({ diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 8ccb421eb89..921cf3655da 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -665,43 +665,6 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('m2o group record created does not use action defaults', function (assert) { - assert.expect(1); - - var kanban = createView({ - View: KanbanView, - model: 'partner', - data: this.data, - arch: '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', - groupBy: ['product_id'], - mockRPC: function (route, args) { - if (args.method === 'name_create') { - assert.notOk(_.has(args.kwargs.context, 'default_name'), - "default_* should be removed from context"); - } - return this._super.apply(this, arguments); - }, - viewOptions: { - context: { - default_name: 'default name partner', - }, - }, - }); - kanban.renderButtons(); - - kanban.$('.o_column_quick_create input').val('new product'); - kanban.$('.o_column_quick_create button.o_kanban_add').click(); - - kanban.destroy(); - }); - - QUnit.test('quick create fail in grouped', function (assert) { assert.expect(7);