diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index b0afefb527a..4baac419a5f 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1357,18 +1357,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') @@ -1377,9 +1366,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)], @@ -1394,7 +1384,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() diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 5d0dc13105a..62da2abe007 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -1199,7 +1199,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': @@ -1400,7 +1408,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 diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 8afdeb7613e..2f97a786f9e 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -932,7 +932,7 @@ class Lead(models.Model): email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) email_link = "%s" % (email, email) sub_title = _('or send an email to %s') % (email_link) - return '

%s

%s

' % (help_title, sub_title) + return '

%s

%s

' % (help_title, sub_title) @api.multi def log_meeting(self, meeting_subject, meeting_date, duration): diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 41b6d534d57..60fe5487083 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -130,14 +130,14 @@ class HrExpense(models.Model): @api.model def get_empty_list_help(self, help_message): - if help_message: + if help_message and "oe_view_nocontent_smiling_face" not in help_message: 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

' % ( + return '

%s

%s

' % ( _('Add a new expense,'), _('or send receipts by email to %s.') % (link),) return super(HrExpense, self).get_empty_list_help(help_message) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 49ef44e35ae..350d4c77fb8 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -339,7 +339,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 "oe_view_nocontent_alias" not in help: + return "%(static_help)s

%(dyn_help)s

" % { 'static_help': help, 'dyn_help': _("Create a new %(document)s by sending an email to %(email_link)s") % { 'document': document_name, diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index d2104cf8163..37aa03093a2 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: 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 5d606324023..0328ff1e0e6 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -61,15 +61,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/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 2388b3a3707..a72cd7008eb 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -193,6 +193,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; @@ -658,8 +659,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/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 49d65629c00..101f484bdd1 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1110,43 +1110,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 record fail in grouped', function (assert) { assert.expect(7); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 980b94e644c..79efc03f52e 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3178,76 +3178,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);