Files
bank-payment/account_banking_payment/model/banking_import_transaction.py
Stefan Rijnhart 5ae5d657f8 [ADD] Foreporting crude support for full payment workflow including
transferral move that pays the invoices
2013-05-27 21:36:20 +02:00

401 lines
17 KiB
Python

# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import netsvc
from openerp.tools.translate import _
from openerp.addons.decimal_precision import decimal_precision as dp
class banking_import_transaction(orm.Model):
_inherit = 'banking.import.transaction'
def _match_payment_order(
self, cr, uid, trans, log, order_type='payment', context=None):
def equals_order_amount(payment_order, transferred_amount):
if (not hasattr(payment_order, 'payment_order_type')
or payment_order.payment_order_type == 'payment'):
sign = 1
else:
sign = -1
total = payment_order.total + sign * transferred_amount
return self.pool.get('res.currency').is_zero(
cr, uid, trans.statement_id.currency, total)
payment_order_obj = self.pool.get('payment.order')
order_ids = payment_order_obj.search(
cr, uid, [('payment_order_type', '=', order_type),
('state', '=', 'sent'),
('date_sent', '<=', trans.execution_date),
],
limit=0, context=context)
orders = payment_order_obj.browse(cr, uid, order_ids, context)
candidates = [x for x in orders if
equals_order_amount(x.total - trans.transferred_amount)]
if len(candidates) > 0:
# retrieve the common account_id, if any
account_id = False
for line in candidates[0].line_ids[0].debit_move_line_id.move_id.line_id:
if line.account_id.type == 'other':
account_id = line.account_id.id
break
return dict(
move_line_ids = False,
match_type = 'payment_order',
payment_order_ids = [x.id for x in candidates],
account_id = account_id,
partner_id = False,
partner_bank_id = False,
reference = False,
type='general',
)
return False
def _match_storno(
self, cr, uid, trans, log, context=None):
payment_line_obj = self.pool.get('payment.line')
line_ids = payment_line_obj.search(
cr, uid, [
('order_id.payment_order_type', '=', 'debit'),
('order_id.state', 'in', ['sent', 'done']),
('communication', '=', trans.reference)
], context=context)
# stornos MUST have an exact match
if len(line_ids) == 1:
account_id = payment_line_obj.get_storno_account_id(
cr, uid, line_ids[0], trans.transferred_amount,
trans.statement_id.currency, context=None)
if account_id:
return dict(
account_id = account_id,
match_type = 'storno',
payment_line_id = line_ids[0],
move_line_ids=False,
partner_id=False,
partner_bank_id=False,
reference=False,
type='customer',
)
# TODO log the reason why there is no result for transfers marked
# as storno
return False
def _match_payment(self, cr, uid, trans, payment_lines,
partner_ids, bank_account_ids, log, linked_payments):
'''
Find the payment order belonging to this reference - if there is one
This is the easiest part: when sending payments, the returned bank info
should be identical to ours.
This also means that we do not allow for multiple candidates.
'''
# TODO: Not sure what side effects are created when payments are done
# for credited customer invoices, which will be matched later on too.
digits = dp.get_precision('Account')(cr)[1]
candidates = [
x for x in payment_lines
if x.communication == trans.reference
and round(x.amount, digits) == -round(
trans.transferred_amount, digits)
and trans.remote_account in (x.bank_id.acc_number,
x.bank_id.acc_number_domestic)
]
if len(candidates) == 1:
candidate = candidates[0]
# Check cache to prevent multiple matching of a single payment
if candidate.id not in linked_payments:
linked_payments[candidate.id] = True
move_info = self._get_move_info(
cr, uid, [candidate.move_line_id.id])
move_info.update({
'match_type': 'payment',
'payment_line_id': candidate.id,
})
return move_info
return False
def _confirm_storno(
self, cr, uid, transaction_id, context=None):
"""
Creation of the reconciliation has been delegated to
*a* direct debit module, to allow for various direct debit styles
"""
payment_line_pool = self.pool.get('payment.line')
statement_line_pool = self.pool.get('account.bank.statement.line')
transaction = self.browse(cr, uid, transaction_id, context=context)
if not transaction.payment_line_id:
raise orm.except_orm(
_("Cannot link with storno"),
_("No direct debit order item"))
reconcile_id = payment_line_pool.debit_storno(
cr, uid,
transaction.payment_line_id.id,
transaction.statement_line_id.amount,
transaction.statement_line_id.currency,
transaction.storno_retry,
context=context)
statement_line_pool.write(
cr, uid, transaction.statement_line_id.id,
{'reconcile_id': reconcile_id}, context=context)
transaction.refresh()
def _confirm_payment_order(
self, cr, uid, transaction_id, context=None):
"""
Creation of the reconciliation has been delegated to
*a* direct debit module, to allow for various direct debit styles
"""
payment_order_obj = self.pool.get('payment.order')
statement_line_pool = self.pool.get('account.bank.statement.line')
transaction = self.browse(cr, uid, transaction_id, context=context)
if not transaction.payment_order_id:
raise orm.except_orm(
_("Cannot reconcile"),
_("Cannot reconcile: no direct debit order"))
reconcile_id = payment_order_obj.debit_reconcile_transfer(
cr, uid,
transaction.payment_order_id.id,
transaction.statement_line_id.amount,
transaction.statement_line_id.currency,
context=context)
statement_line_pool.write(
cr, uid, transaction.statement_line_id.id,
{'reconcile_id': reconcile_id}, context=context)
def _confirm_payment(
self, cr, uid, transaction_id, context=None):
"""
Do some housekeeping on the payment line
then pass on to _reconcile_move
"""
transaction = self.browse(cr, uid, transaction_id, context=context)
payment_line_obj = self.pool.get('payment.line')
payment_line_obj.write(
cr, uid, transaction.payment_line_id.id, {
'export_state': 'done',
'date_done': transaction.statement_line_id.date,
}
)
self._confirm_move(cr, uid, transaction_id, context=context)
# Check if the payment order is 'done'
order_id = transaction.payment_line_id.order_id.id
other_lines = payment_line_obj.search(
cr, uid, [
('order_id', '=', order_id),
('date_done', '=', False),
], context=context)
if not other_lines:
wf_service = netsvc.LocalService('workflow')
wf_service.trg_validate(
uid, 'payment.order', order_id, 'done', cr)
def _cancel_payment(
self, cr, uid, transaction_id, context=None):
"""
Do not support cancelling individual lines yet, because the workflow
of the payment order does not support reopening.
"""
raise orm.except_orm(
_("Cannot unreconcile"),
_("Cannot unreconcile: this operation is not yet supported for "
"match type 'payment'"))
def _cancel_payment_order(
self, cr, uid, transaction_id, context=None):
"""
"""
payment_order_obj = self.pool.get('payment.order')
transaction = self.browse(cr, uid, transaction_id, context=context)
if not transaction.payment_order_id:
raise orm.except_orm(
_("Cannot unreconcile"),
_("Cannot unreconcile: no payment or direct debit order"))
return payment_order_obj.debit_unreconcile_transfer(
cr, uid, transaction.payment_order_id.id,
transaction.statement_line_id.reconcile_id.id,
transaction.statement_line_id.amount,
transaction.statement_line_id.currency)
def _cancel_storno(
self, cr, uid, transaction_id, context=None):
"""
TODO: delegate unreconciliation to the direct debit module,
to allow for various direct debit styles
"""
payment_line_obj = self.pool.get('payment.line')
reconcile_obj = self.pool.get('account.move.reconcile')
transaction = self.browse(cr, uid, transaction_id, context=context)
if not transaction.payment_line_id:
raise orm.except_orm(
_("Cannot cancel link with storno"),
_("No direct debit order item"))
if not transaction.payment_line_id.storno:
raise orm.except_orm(
_("Cannot cancel link with storno"),
_("The direct debit order item is not marked for storno"))
journal = transaction.statement_line_id.statement_id.journal_id
if transaction.statement_line_id.amount >= 0:
account_id = journal.default_credit_account_id.id
else:
account_id = journal.default_debit_account_id.id
cancel_line = False
move_lines = []
for move in transaction.statement_line_id.move_ids:
# There should usually be just one move, I think
move_lines += move.line_id
for line in move_lines:
if line.account_id.id != account_id:
cancel_line = line
break
if not cancel_line:
raise orm.except_orm(
_("Cannot cancel link with storno"),
_("Line id not found"))
reconcile = cancel_line.reconcile_id or cancel_line.reconcile_partial_id
lines_reconcile = reconcile.line_id or reconcile.line_partial_ids
if len(lines_reconcile) < 3:
# delete the full reconciliation
reconcile_obj.unlink(cr, uid, reconcile.id, context)
else:
# we are left with a partial reconciliation
reconcile_obj.write(
cr, uid, reconcile.id,
{'line_partial_ids':
[(6, 0, [x.id for x in lines_reconcile
if x.id != cancel_line.id])],
'line_id': [(6, 0, [])],
}, context)
# redo the original payment line reconciliation with the invoice
payment_line_obj.write(
cr, uid, transaction.payment_line_id.id,
{'storno': False}, context)
payment_line_obj.debit_reconcile(
cr, uid, transaction.payment_line_id.id, context)
_columns = {
'match_type': fields.selection(
# Add payment and storno types
[
('manual', 'Manual'),
('move','Move'),
('invoice', 'Invoice'),
('payment', 'Payment'),
('payment_order', 'Payment order'),
('storno', 'Storno'),
],
'Match type'),
'payment_order_ids': fields.many2many(
'payment.order', 'banking_transaction_payment_order_rel',
'order_id', 'transaction_id', 'Payment orders'),
'payment_order_id': fields.many2one(
'payment.order', 'Payment order to reconcile'),
'payment_line_id': fields.many2one('payment.line', 'Payment line'),
}
def _get_match_multi(self, cr, uid, ids, name, args, context=None):
if not ids:
return {}
res = super(banking_import_transaction, self)._get_match_multi(
cr, uid, ids, name, args, context=context)
for transaction in self.browse(cr, uid, ids, context):
if transaction.match_type == 'payment_order':
if (transaction.payment_order_ids and not
transaction.payment_order_id):
res[transaction.id] = True
return res
def clear_and_write(self, cr, uid, ids, vals=None, context=None):
super(banking_import_transaction, self).clear_and_write(
cr, uid, ids, vals=vals, context=context)
return self.write(
cr, uid, ids, {
'payment_line_id': False,
'payment_order_ids': [(6, 0, [])],
},
context=context)
def move_info2values(self, move_info):
vals = super(banking_import_transaction, self).move_info2values(
move_info)
vals['payment_line_id'] = move_info.get('payment_line_id', False)
vals['payment_order_ids'] = [
(6, 0, move_info.get('payment_order_ids') or [])]
vals['payment_order_id'] = (
move_info.get('payment_order_ids', False) and
len(move_info['payment_order_ids']) == 1 and
move_info['payment_order_ids'][0]
)
return vals
def hook_match_payment(cr, uid, transaction, log, context=None):
"""
Called from match() in the core module.
Match payment batches, direct debit orders and stornos
"""
move_info = False
if transaction.type == bt.PAYMENT_BATCH:
move_info = self._match_payment_order(
cr, uid, transaction, log,
order_type='payment', context=context)
elif transaction.type == bt.DIRECT_DEBIT:
move_info = self._match_payment_order(
cr, uid, transaction, log,
order_type='debit', context=context)
elif transaction.type == bt.STORNO:
move_info = self._match_storno(
cr, uid, transaction, log,
context=context)
return move_info
def __init__(self, pool, cr):
"""
Updating the function maps to handle the match types that this
module adds. While creating the map in the super object was
straightforward, the fact that these are now functions rather than
method requires the awkward way of updating it with the methods'
function objects.
As noted above, another implication is that any addon that inherits
one of these methods needs to overwrite the entry in the function
maps in the same way as is done below.
"""
super(banking_import_transaction, self).__init__(pool, cr)
self.confirm_map.update({
'storno': self._confirm_storno.__func__,
'payment_order': self._confirm_payment_order.__func__,
'payment': self._confirm_payment.__func__,
})
self.cancel_map.update({
'storno': self._cancel_storno.__func__,
'payment_order': self._cancel_payment_order.__func__,
'payment': self._cancel_payment.__func__,
})