[MERGE] forward port branch 11.0 up to db0e84f3e3
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user