Finalise the wizard of selection of move lines to pay

Add button "Add to payment/debit order" on invoice form view
Started to integrate payment transfer in account_payment_order (not finished at all though)
Various fixes/changes/improvements/...
This commit is contained in:
Alexis de Lattre
2016-05-06 01:16:20 +02:00
parent 3c8c2e9e70
commit abb052d894
28 changed files with 446 additions and 330 deletions

View File

@@ -3,50 +3,22 @@
# © 2014 Serv. Tecnol. Avanzados - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, models, _
from lxml import etree
from openerp import models, fields, api, _
from openerp.exceptions import UserError
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
payment_order_ok = fields.Boolean(
related='payment_mode_id.payment_order_ok', readonly=True)
@api.model
def _get_reference_type(self):
rt = super(AccountInvoice, self)._get_reference_type()
rt.append(('structured', _('Structured Reference')))
return rt
@api.model
def fields_view_get(self, view_id=None, view_type=False, toolbar=False,
submenu=False):
"""This adds the field 'reference_type' only if the view doesn't
contain this field (this is for customer invoice and with
l10n_be_invoice_bba not installed).
"""
res = super(AccountInvoice, self).fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar,
submenu=submenu)
if view_type != 'form':
return res
field_name = 'reference_type'
doc = etree.XML(res['arch'])
if not doc.xpath("//field[@name='%s']" % field_name):
nodes = doc.xpath("//field[@name='origin']")
if nodes:
field = self.fields_get([field_name])[field_name]
field_xml = etree.Element(
'field', {'name': field_name,
'widget': 'selection',
'states': str(field['states']),
'selection': str(field['selection']),
'required': '1' if field['required'] else '0',
'string': field['string'],
'nolabel': '0'})
nodes[0].addnext(field_xml)
res['arch'] = etree.tostring(doc)
res['fields'][field_name] = field
return res
@api.model
def line_get_convert(self, line, part):
"""Copy supplier bank account from invoice to account move line"""
@@ -56,3 +28,72 @@ class AccountInvoice(models.Model):
if invoice.type in ('in_invoice', 'in_refund'):
res['partner_bank_id'] = invoice.partner_bank_id.id or False
return res
@api.multi
def _prepare_new_payment_order(self):
self.ensure_one()
vals = {
'payment_mode_id': self.payment_mode_id.id,
'payment_type': self.payment_mode_id.payment_type,
}
if self.payment_mode_id.bank_account_link == 'fixed':
vals['journal_id'] = self.payment_mode_id.fixed_journal_id.id
# TODO : else: no filter on allowed bank accounts, because onchange not played ??
return vals
@api.multi
def create_account_payment_line(self):
apoo = self.env['account.payment.order']
aplo = self.env['account.payment.line']
action = {}
for inv in self:
if not inv.payment_mode_id:
raise UserError(_(
"No Payment Mode on invoice %s") % inv.number)
if not inv.move_id:
raise UserError(_(
"No Journal Entry on invoice %s") % inv.number)
payorders = apoo.search([
('payment_mode_id', '=', inv.payment_mode_id.id),
('state', '=', 'draft')])
if payorders:
payorder = payorders[0]
new_payorder = False
else:
payorder = apoo.create(inv._prepare_new_payment_order())
new_payorder = True
count = 0
for line in inv.move_id.line_ids:
if line.account_id == inv.account_id and not line.reconciled:
paylines = aplo.search([
('move_line_id', '=', line.id),
('state', '!=', 'cancel')])
if paylines:
continue
line.create_payment_line_from_move_line(payorder)
count += 1
if count:
if new_payorder:
inv.message_post(_(
'%d payment lines added to the new draft payment '
'order %s which has been automatically created.')
% (count, payorder.name))
else:
inv.message_post(_(
'%d payment lines added to the existing draft '
'payment order %s.')
% (count, payorder.name))
else:
raise UserError(_(
'No Payment Line created for invoice %s because '
'it already exists or because this invoice is '
'already paid.') % inv.number)
action = self.env['ir.actions.act_window'].for_xml_id(
'account_payment_order',
'account_payment_order_%s_action' % payorder.payment_type)
action.update({
'view_mode': 'form,tree,pivot,graph',
'res_id': payorder.id,
'views': False,
})
return action

View File

@@ -45,17 +45,22 @@ class AccountMoveLine(models.Model):
self.ensure_one()
assert payment_order, 'Missing payment order'
aplo = self.env['account.payment.line']
# default values for communication_type and communication
communication_type = 'normal'
communication = self.move_id.name or '-'
ref2comm_type = aplo.invoice_reference_type2communication_type()
if (
self.invoice_id and
self.invoice_id.type in ('in_invoice', 'in_refund') and
self.invoice_id.reference):
communication = self.invoice_id.reference
if self.invoice_id.reference_type:
# change these default values if move line is linked to an invoice
if self.invoice_id:
if self.invoice_id.reference_type != 'none':
communication = self.invoice_id.reference
ref2comm_type =\
aplo.invoice_reference_type2communication_type()
communication_type =\
ref2comm_type[self.invoice_id.reference_type]
else:
if (
self.invoice_id.type in ('in_invoice', 'in_refund')
and self.invoice_id.reference):
communication = self.invoice_id.reference
if self.currency_id:
currency_id = self.currency_id.id
amount_currency = self.amount_residual_currency
@@ -66,7 +71,6 @@ class AccountMoveLine(models.Model):
precision = self.env['decimal.precision'].precision_get('Account')
if payment_order.payment_type == 'outbound':
amount_currency *= -1
# TODO : inherit for mandate
vals = {
'order_id': payment_order.id,
'partner_bank_id': self.partner_bank_id.id,

View File

@@ -20,6 +20,9 @@ class AccountPaymentLine(models.Model):
related='order_id.company_currency_id', store=True, readonly=True)
payment_type = fields.Selection(
related='order_id.payment_type', store=True, readonly=True)
state = fields.Selection(
related='order_id.state', string='State',
readonly=True, store=True)
move_line_id = fields.Many2one(
'account.move.line', string='Journal Item')
ml_maturity_date = fields.Date(
@@ -83,6 +86,11 @@ class AccountPaymentLine(models.Model):
values = []
for field in bplo.same_fields_payment_line_and_bank_payment_line():
values.append(unicode(self[field]))
# Don't group the payment lines that are attached to the same supplier
# but to move lines with different accounts (very unlikely),
# for easier generation/comprehension of the transfer move
# TODO Alexis : but this is for ????
values.append(unicode(self.move_line_id.account_id or False))
hashcode = '-'.join(values)
return hashcode

View File

@@ -5,7 +5,8 @@
# © 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, fields, api
from openerp import models, fields, api, _
from openerp.exceptions import ValidationError
class AccountPaymentMode(models.Model):
@@ -42,6 +43,38 @@ class AccountPaymentMode(models.Model):
"* Payment Date\n"
"(other modules can set additional fields to restrict the "
"grouping.)")
transfer_move = fields.Boolean(
'Generate Accounting Entries On File Upload')
transfer_account_id = fields.Many2one(
'account.account', string='Transfer Account',
domain=[('internal_type', '=', 'other'), ('reconcile', '=', True)],
help="Pay off lines in 'file uploaded' payment orders with a move on "
"this account. You can only select accounts of type regular "
"that are marked for reconciliation")
transfer_journal_id = fields.Many2one(
'account.journal', string='Transfer Journal',
help='Journal to write payment entries when confirming '
'payment/debit orders of this mode')
transfer_move_option = fields.Selection([
('date', 'One move per payment date'),
('line', 'One move per payment line'),
], string='Transfer Move Option', default='date')
@api.multi
@api.constrains(
'transfer_move', 'transfer_account_id', 'transfer_journal_id',
'transfer_move_option')
def transfer_move_constrains(self):
for mode in self:
if mode.transfer_move and (
not mode.transfer_account_id or
not mode.transfer_journal_id or
not mode.transfer_move_option):
raise ValidationError(_(
"The option 'Generate Accounting Entries On File Upload' "
"is active on payment mode '%s', so the three parameters "
"'Transfer Account', 'Transfer Journal' and "
"'Transfer Move Option' must be set.") % mode.name)
@api.onchange('payment_method_id')
def payment_method_id_change(self):

View File

@@ -35,7 +35,9 @@ class AccountPaymentOrder(models.Model):
related='payment_mode_id.bank_account_link', readonly=True)
journal_id = fields.Many2one(
'account.journal', string='Bank Journal',
required=True)
readonly=True, states={'draft': [('readonly', False)]})
# The journal_id field is only required at confirm step, to
# allow auto-creation of payment order from invoice
company_partner_bank_id = fields.Many2one(
related='journal_id.bank_account_id', string='Company Bank Account',
readonly=True)
@@ -174,6 +176,9 @@ class AccountPaymentOrder(models.Model):
bplo = self.env['bank.payment.line']
today = fields.Date.context_today(self)
for order in self:
if not order.journal_id:
raise UserError(_(
'Missing Bank Journal on payment order %s') % order.name)
# Delete existing bank payment lines
order.bank_line_ids.unlink()
# Create the bank payment lines from the payment lines
@@ -257,5 +262,144 @@ class AccountPaymentOrder(models.Model):
@api.multi
def generated2uploaded(self):
for order in self:
if order.payment_mode_id.transfer_move:
order.generate_transfer_move()
self.write({'state': 'uploaded'})
return True
# Generation of transfer move
@api.multi
def _prepare_transfer_move(self):
vals = {
'journal_id': self.mode.transfer_journal_id.id,
'ref': '%s %s' % (
self.payment_order_type[:3].upper(), self.reference)
}
return vals
@api.multi
def _prepare_move_line_transfer_account(
self, amount, move, bank_payment_lines, labels):
if len(bank_payment_lines) == 1:
partner_id = bank_payment_lines[0].partner_id.id
name = _('%s bank line %s') % (labels[self.payment_order_type],
bank_payment_lines[0].name)
else:
partner_id = False
name = '%s %s' % (
labels[self.payment_order_type], self.reference)
date_maturity = bank_payment_lines[0].date
vals = {
'name': name,
'move_id': move.id,
'partner_id': partner_id,
'account_id': self.mode.transfer_account_id.id,
'credit': (self.payment_order_type == 'payment' and
amount or 0.0),
'debit': (self.payment_order_type == 'debit' and
amount or 0.0),
'date_maturity': date_maturity,
}
return vals
@api.multi
def _prepare_move_line_partner_account(self, bank_line, move, labels):
# TODO : ALEXIS check don't group if move_line_id.account_id
# is not the same
if bank_line.payment_line_ids[0].move_line_id:
account_id =\
bank_line.payment_line_ids[0].move_line_id.account_id.id
else:
if self.payment_order_type == 'debit':
account_id =\
bank_line.partner_id.property_account_receivable.id
else:
account_id = bank_line.partner_id.property_account_payable.id
vals = {
'name': _('%s line %s') % (
labels[self.payment_order_type], bank_line.name),
'move_id': move.id,
'partner_id': bank_line.partner_id.id,
'account_id': account_id,
'credit': (self.payment_order_type == 'debit' and
bank_line.amount_currency or 0.0),
'debit': (self.payment_order_type == 'payment' and
bank_line.amount_currency or 0.0),
}
return vals
@api.multi
def action_sent_no_move_line_hook(self, pay_line):
"""This function is designed to be inherited"""
return
@api.multi
def _create_move_line_partner_account(self, bank_line, move, labels):
"""This method is designed to be inherited in a custom module"""
# TODO: take multicurrency into account
company_currency = self.env.user.company_id.currency_id
if bank_line.currency != company_currency:
raise UserError(_(
"Cannot generate the transfer move when "
"the currency of the payment (%s) is not the "
"same as the currency of the company (%s). This "
"is not supported for the moment.")
% (bank_line.currency.name, company_currency.name))
aml_obj = self.env['account.move.line']
# create the payment/debit counterpart move line
# on the partner account
partner_ml_vals = self._prepare_move_line_partner_account(
bank_line, move, labels)
partner_move_line = aml_obj.create(partner_ml_vals)
# register the payment/debit move line
# on the payment line and call reconciliation on it
bank_line.write({'transit_move_line_id': partner_move_line.id})
@api.multi
def _reconcile_payment_lines(self, bank_payment_lines):
for bline in bank_payment_lines:
if all([pline.move_line_id for pline in bline.payment_line_ids]):
bline.debit_reconcile()
else:
self.action_sent_no_move_line_hook(bline)
@api.multi
def generate_transfer_move(self):
"""
Create the moves that pay off the move lines from
the debit order.
"""
self.ensure_one()
am_obj = self.env['account.move']
aml_obj = self.env['account.move.line']
labels = {
'outbound': _('Payment'),
'inbound': _('Direct debit'),
}
# prepare a dict "trfmoves" that can be used when
# self.mode.transfer_move_option = date or line
# key = unique identifier (date or True or line.id)
# value = [pay_line1, pay_line2, ...]
trfmoves = {}
for bline in self.bank_line_ids:
hashcode = bline.move_line_transfer_account_hashcode()
if hashcode in trfmoves:
trfmoves[hashcode].append(bline)
else:
trfmoves[hashcode] = [bline]
for hashcode, blines in trfmoves.iteritems():
mvals = self._prepare_transfer_move()
move = am_obj.create(mvals)
total_amount = 0
for bline in blines:
total_amount += bline.amount_currency
self._create_move_line_partner_account(bline, move, labels)
# create the payment/debit move line on the transfer account
trf_ml_vals = self._prepare_move_line_transfer_account(
total_amount, move, blines, labels)
aml_obj.create(trf_ml_vals)
self._reconcile_payment_lines(blines)
move.post()

View File

@@ -29,6 +29,7 @@ class BankPaymentLine(models.Model):
readonly=True)
# Function Float fields are sometimes badly displayed in tree view,
# see bug report https://github.com/odoo/odoo/issues/8632
# But is it still true in v9 ?
amount_currency = fields.Monetary(
string='Amount', currency_field='currency_id',
compute='_compute_amount', store=True, readonly=True)
@@ -48,6 +49,28 @@ class BankPaymentLine(models.Model):
company_id = fields.Many2one(
related='order_id.payment_mode_id.company_id', store=True,
readonly=True)
# TODO : not shown in view ?
# why on bank payment line and not on payment line ?
transit_move_line_id = fields.Many2one(
'account.move.line', string='Transfer Move Line', readonly=True,
help="Move line through which the payment/debit order "
"pays the invoice")
transfer_move_line_id = fields.Many2one(
'account.move.line', compute='_get_transfer_move_line',
string='Transfer move line counterpart',
help="Counterpart move line on the transfer account")
@api.multi
def _get_transfer_move_line(self):
for bank_line in self:
if bank_line.transit_move_line_id:
payment_type = bank_line.payment_type
trf_lines = bank_line.transit_move_line_id.move_id.line_id
for move_line in trf_lines:
if payment_type == 'inbound' and move_line.debit > 0:
bank_line.transfer_move_line_id = move_line
elif payment_type == 'outbound' and move_line.credit > 0:
bank_line.transfer_move_line_id = move_line
@api.model
def same_fields_payment_line_and_bank_payment_line(self):
@@ -76,3 +99,17 @@ class BankPaymentLine(models.Model):
vals['name'] = self.env['ir.sequence'].next_by_code(
'bank.payment.line') or 'New'
return super(BankPaymentLine, self).create(vals)
@api.multi
def move_line_transfer_account_hashcode(self):
"""
This method is inherited in the module
account_banking_sepa_direct_debit
"""
self.ensure_one()
if self.order_id.payment_mode_id.transfer_move_option == 'date':
hashcode = self.date
else:
hashcode = unicode(self.id)
return hashcode