[MERGE] forward port branch 11.0 up to db0e84f3e3

This commit is contained in:
Christophe Simonis
2018-09-12 19:37:17 +02:00
10 changed files with 48 additions and 141 deletions
+20 -15
View File
@@ -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()
@@ -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
+1 -1
View File
@@ -932,7 +932,7 @@ class Lead(models.Model):
email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
sub_title = _('or send an email to %s') % (email_link)
return '<p class="o_view_nocontent_smiling_face">%s</p><p>%s</p>' % (help_title, sub_title)
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)
@api.multi
def log_meeting(self, meeting_subject, meeting_date, duration):
+2 -2
View File
@@ -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 = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % {
'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
}
return '<p class="oe_view_nocontent_smiling_face">%s</p><p>%s</p>' % (
return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (
_('Add a new expense,'),
_('or send receipts by email to %s.') % (link),)
return super(HrExpense, self).get_empty_list_help(help_message)
+3 -1
View File
@@ -339,7 +339,9 @@ class MailThread(models.AbstractModel):
'email_link': email_link
}
}
return "%(static_help)s<p>%(dyn_help)s</p>" % {
# do not add alias two times if it was added previously
if "oe_view_nocontent_alias" not in help:
return "%(static_help)s<p class="oe_view_nocontent_alias">%(dyn_help)s</p>" % {
'static_help': help,
'dyn_help': _("Create a new %(document)s by sending an email to %(email_link)s") % {
'document': document_name,
@@ -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:
@@ -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({
@@ -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))
@@ -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: '<kanban class="o_kanban_test" on_create="quick_create">' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="foo"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
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);
@@ -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: '<tree editable="top" default_order="int_field">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float"/>' +
'</tree>',
});
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);