[IMP] split account_banking_payment from account_banking

We drop the bank statement related part and we manage
the state of payment order with workflow transitions
triggered by the reconciliation of moves lines on the
transfer account.
This commit is contained in:
Stéphane Bidoul
2014-09-16 18:52:28 +02:00
parent 95d2c0a468
commit 424c4067cb
16 changed files with 125 additions and 762 deletions

View File

@@ -75,8 +75,6 @@ for electronic banking. It provides the following technical features:
To enable the use of payment order to collect money for customers,
it adds a payment_order_type (payment|debit) as a basis of direct debit support
(this field becomes visible when account_direct_debit is installed).
Refactoring note: this field should ideally go in account_direct_debit,
but account_banking_payment currently depends on it.
Bug fixes and enhancement that should land in official addons:

View File

@@ -53,7 +53,7 @@ class AccountMoveLine(orm.Model):
# this file. -- Alexis de Lattre
def _amount_to_pay(self, cr, uid, ids, name, arg=None, context=None):
""" Return the amount still to pay regarding all the payemnt orders
""" Return the amount still to pay regarding all the payment orders
(excepting cancelled orders)"""
if not ids:
return {}

View File

@@ -3,7 +3,7 @@
<data>
<!--
Add the payment mode type and transfer settings
Add the payment mode type settings
-->
<record id="view_payment_mode_form_inherit" model="ir.ui.view">
<field name="name">payment.mode.form.inherit</field>

View File

@@ -1 +1 @@
import model
from . import model

View File

@@ -3,6 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
# (C) 2014 ACSONE SA/NV (<http://acsone.eu>).
#
# All other contributions are (C) by their respective contributors
#
@@ -24,31 +25,28 @@
##############################################################################
{
'name': 'Account Banking - Payments',
'version': '0.1.164',
'name': 'Account Banking - Payments Transfer Account',
'version': '0.2',
'license': 'AGPL-3',
'author': 'Banking addons community',
'website': 'https://launchpad.net/banking-addons',
'website': 'https://github.com/OCA/banking',
'category': 'Banking addons',
'depends': [
'account_banking',
'account_banking_payment_export',
],
],
'data': [
'view/account_payment.xml',
'view/banking_transaction_wizard.xml',
# TODO: 'view/account_payment.xml',
'view/payment_mode.xml',
'workflow/account_payment.xml',
],
'description': '''
This addon adds payment reconciliation infrastructure to the Banking Addons.
'description': '''Payment order reconciliation infrastructure
* Extends payments for digital banking:
+ Adapted workflow in payments to reflect banking operations
+ Relies on account_payment mechanics to extend with export generators.
- ClieOp3 (NL) payment and direct debit orders files available as
account_banking_nl_clieop
This module reconciles invoices as soon as the payment order
is sent, by creating a move to a transfer account (aka suspense account).
When the moves on the suspense account are reconciled (typically through
the bank statement reconciliation, the payment order moves to the done
status).
''',
'auto_install': True,
'installable': False,
'auto_install': False,
'installable': True,
}

View File

@@ -1,7 +1,4 @@
import account_payment
import payment_line
import payment_mode
import payment_order_create
import banking_import_transaction
import banking_transaction_wizard
import banking_import_line
from . import account_payment
from . import payment_line
from . import payment_mode
from . import payment_order_create

View File

@@ -3,6 +3,7 @@
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
# (C) 2014 ACSONE SA (<http://acsone.eu>).
#
# All other contributions are (C) by their respective contributors
#
@@ -25,7 +26,6 @@
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp import netsvc
class payment_order(orm.Model):
@@ -124,103 +124,48 @@ class payment_order(orm.Model):
])
payment_line_obj.write(cr, uid, line_ids, kwargs)
def action_rejected(self, cr, uid, ids, *args):
'''
Set both self and payment lines to state 'rejected'.
'''
wf_service = netsvc.LocalService('workflow')
for id in ids:
wf_service.trg_validate(uid, 'payment.order', id, 'rejected', cr)
def action_rejected(self, cr, uid, ids, context=None):
return True
def set_done(self, cr, uid, ids, *args):
'''
Extend standard transition to update children as well.
'''
def action_done(self, cr, uid, ids, context=None):
self._write_payment_lines(
cr, uid, ids,
date_done=fields.date.context_today(self, cr, uid))
return super(payment_order, self).set_done(
cr, uid, ids, *args
)
def debit_reconcile_transfer(self, cr, uid, payment_order_id,
amount, currency, context=None):
"""
During import of bank statements, create the reconcile on the transfer
account containing all the open move lines on the transfer account.
"""
move_line_obj = self.pool.get('account.move.line')
order = self.browse(cr, uid, payment_order_id, context)
line_ids = []
reconcile_id = False
if not order.line_ids[0].transit_move_line_id:
wf_service = netsvc.LocalService('workflow')
wf_service.trg_validate(
uid, 'payment.order', payment_order_id, 'done', cr)
return False
for order_line in order.line_ids:
for line in order_line.transit_move_line_id.move_id.line_id:
if line.account_id.type == 'other' and not line.reconcile_id:
line_ids.append(line.id)
if self.pool.get('res.currency').is_zero(
cr, uid, currency,
move_line_obj.get_balance(cr, uid, line_ids) - amount):
reconcile_id = self.pool.get('account.move.reconcile').create(
cr, uid,
{'type': 'auto', 'line_id': [(6, 0, line_ids)]},
context)
# set direct debit order to finished state
wf_service = netsvc.LocalService('workflow')
wf_service.trg_validate(
uid, 'payment.order', payment_order_id, 'done', cr)
return reconcile_id
def debit_unreconcile_transfer(
self, cr, uid, payment_order_id, reconcile_id, amount, currency,
context=None):
"""
Due to a cancelled bank statements import, unreconcile the move on
the transfer account. Delegate the conditions to the workflow.
Raise on failure for rollback.
Workflow appears to return False even on success so we just check
the order's state that we know to be set to 'sent' in that case.
"""
self.pool.get('account.move.reconcile').unlink(
cr, uid, [reconcile_id], context=context)
netsvc.LocalService('workflow').trg_validate(
uid, 'payment.order', payment_order_id, 'undo_done', cr)
state = self.pool.get('payment.order').read(
cr, uid, payment_order_id, ['state'], context=context)['state']
if state != 'sent':
raise orm.except_orm(
_("Cannot unreconcile"),
_("Cannot unreconcile payment order: "
"Workflow will not allow it."))
date_done=fields.date.context_today(self, cr, uid,
context=context))
self.write(cr, uid, ids,
{'date_done': fields.date.
context_today(self, cr, uid, context=context)})
# state is written in workflow definition
return True
def test_undo_done(self, cr, uid, ids, context=None):
def _get_transfer_move_lines(self, cr, uid, ids, context=None):
"""
Called from the workflow. Used to unset done state on
payment orders that were reconciled with bank transfers
which are being cancelled.
Test if the payment order has not been reconciled. Depends
on the restriction that transit move lines should use an
account of type 'other', and on the restriction of payment
and debit orders that they only take moves on accounts
payable/receivable.
Get the transfer move lines (on the transfer account).
"""
res = []
for order in self.browse(cr, uid, ids, context=context):
for order_line in order.line_ids:
if order_line.transit_move_line_id.move_id:
for line in \
order_line.transit_move_line_id.move_id.line_id:
if (line.account_id.type == 'other' and
line.reconcile_id):
return False
return True
move_line = order_line.transfer_move_line_id
if move_line:
res.append(move_line)
return res
def get_transfer_move_line_ids(self, cr, uid, ids, context=None):
return [move_line.id for move_line in
self._get_transfer_move_lines(cr, uid, ids, context=context)]
def test_done(self, cr, uid, ids, context=None):
"""
Test if all moves on the transfer account are reconciled.
Called from the workflow to move to the done state when
all transfer move have been reconciled through bank statements.
"""
return all([move_line.reconcile_id for move_line in
self._get_transfer_move_lines(cr, uid, ids, context)])
def test_undo_done(self, cr, uid, ids, context=None):
return not self.test_done(cr, uid, ids, context=context)
def _prepare_transfer_move(
self, cr, uid, order, line, labels, context=None):
@@ -247,7 +192,7 @@ class payment_order(orm.Model):
or line.move_line_id.name)
or line.communication),
'move_id': move_id,
'partner_id': False,
'partner_id': line.partner_id.id,
'account_id': order.mode.transfer_account_id.id,
'credit': (order.payment_order_type == 'payment'
and line.amount or 0.0),
@@ -311,19 +256,20 @@ class payment_order(orm.Model):
# TODO: take multicurrency into account
# create the debit move line on the transfer account
# create the payment/debit move line on the transfer account
ml_vals = self._prepare_move_line_transfer_account(
cr, uid, order, line, move_id, labels, context=context)
account_move_line_obj.create(cr, uid, ml_vals, context=context)
# create the debit move line on the partner account
# create the payment/debit counterpart move line
# on the partner account
self._update_move_line_partner_account(
cr, uid, order, line, ml_vals, context=context)
reconcile_move_line_id = account_move_line_obj.create(
cr, uid, ml_vals, context=context)
# register the debit move line on the payment line
# and call reconciliation on it
# register the payment/debit move line
# on the payment line and call reconciliation on it
payment_line_obj.write(
cr, uid, line.id,
{'transit_move_line_id': reconcile_move_line_id},

View File

@@ -1,43 +0,0 @@
# -*- 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
class banking_import_line(orm.TransientModel):
_inherit = 'banking.import.line'
_columns = {
'payment_order_id': fields.many2one(
'payment.order', 'Payment order'),
'transaction_type': fields.selection([
# Add payment order related transaction types
('invoice', 'Invoice payment'),
('payment_order_line', 'Payment from a payment order'),
('payment_order', 'Aggregate payment order'),
('storno', 'Canceled debit order'),
('bank_costs', 'Bank costs'),
('unknown', 'Unknown'),
], 'Transaction type'),
}

View File

@@ -1,410 +0,0 @@
# -*- 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
from openerp.addons.account_banking.parsers.models import (
mem_bank_transaction as bt
)
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_line_id.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, trans.statement_line_id.amount)]
if len(candidates) > 0:
# retrieve the common account_id, if any
account_id = False
transit_move_lines = candidates[0].line_ids[0].transit_move_line_id
if transit_move_lines:
for line in transit_move_lines.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.statement_line_id.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.
def bank_match(account, partner_bank):
"""
Returns whether a given account number is equivalent to a
partner bank in the database. We simply call the search method,
which checks bank account number, disregarding spaces.
:param account: string representation of a bank account number
:param partner_bank: browse record of model res.partner.bank
"""
return partner_bank.id in self.pool['res.partner.bank'].search(
cr, uid, [('acc_number', '=', account)])
digits = dp.get_precision('Account')(cr)[1]
candidates = [
line for line in payment_lines
if (line.communication == trans.reference
and round(line.amount, digits) == -round(
trans.statement_line_id.amount, digits)
and bank_match(trans.remote_account, line.bank_id))
]
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, {
'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"))
if not transaction.statement_line_id.reconcile_id:
raise orm.except_orm(
_("Cannot unreconcile"),
_("Payment orders without transfer move lines cannot be "
"unreconciled this way"))
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 = {
'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):
write_vals = {
'payment_line_id': False,
'payment_order_id': False,
'payment_order_ids': [(6, 0, [])],
}
write_vals.update(vals or {})
return super(banking_import_transaction, self).clear_and_write(
cr, uid, ids, vals=vals, 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(self, 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.
"""
super(banking_import_transaction, self).__init__(pool, cr)
self.confirm_map.update({
'storno': banking_import_transaction._confirm_storno,
'payment_order': banking_import_transaction._confirm_payment_order,
'payment': banking_import_transaction._confirm_payment,
'payment_order_manual': (
banking_import_transaction._confirm_payment_order),
'payment_manual': banking_import_transaction._confirm_payment,
})
self.cancel_map.update({
'storno': banking_import_transaction._cancel_storno,
'payment_order': banking_import_transaction._cancel_payment_order,
'payment': banking_import_transaction._cancel_payment,
'payment_order_manual': (
banking_import_transaction._cancel_payment_order),
'payment_manual': banking_import_transaction._cancel_payment,
})

View File

@@ -1,122 +0,0 @@
# -*- 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.tools.translate import _
class banking_transaction_wizard(orm.TransientModel):
_inherit = 'banking.transaction.wizard'
def write(self, cr, uid, ids, vals, context=None):
"""
Check for manual payment orders or lines
"""
if not vals or not ids:
return True
manual_payment_order_id = vals.pop('manual_payment_order_id', False)
manual_payment_line_id = vals.pop('manual_payment_line_id', False)
res = super(banking_transaction_wizard, self).write(
cr, uid, ids, vals, context=context)
if manual_payment_order_id or manual_payment_line_id:
transaction_id = self.browse(
cr, uid, ids[0],
context=context).import_transaction_id
write_vals = {}
if manual_payment_order_id:
payment_order = self.pool.get('payment.order').browse(
cr, uid, manual_payment_order_id,
context=context)
if payment_order.payment_order_type == 'payment':
sign = 1
else:
sign = -1
total = (payment_order.total + sign *
transaction_id.statement_line_id.amount)
if not self.pool.get('res.currency').is_zero(
cr, uid,
transaction_id.statement_line_id.statement_id.currency,
total):
raise orm.except_orm(
_('Error'),
_('When matching a payment order, the amounts have to '
'match exactly'))
if (payment_order.mode
and payment_order.mode.transfer_account_id):
transaction_id.statement_line_id.write({
'account_id': (
payment_order.mode.transfer_account_id.id),
})
write_vals.update(
{'payment_order_id': manual_payment_order_id,
'match_type': 'payment_order_manual'})
else:
write_vals.update(
{'payment_line_id': manual_payment_line_id,
'match_type': 'payment_manual'})
self.pool.get('banking.import.transaction').clear_and_write(
cr, uid, transaction_id.id, write_vals, context=context)
return res
_columns = {
'payment_line_id': fields.related(
'import_transaction_id',
'payment_line_id',
string="Matching payment or storno",
type='many2one',
relation='payment.line',
readonly=True,
),
'payment_order_ids': fields.related(
'import_transaction_id',
'payment_order_ids',
string="Matching payment orders",
type='many2many',
relation='payment.order',
),
'payment_order_id': fields.related(
'import_transaction_id',
'payment_order_id',
string="Payment order to reconcile",
type='many2one',
relation='payment.order',
),
'manual_payment_order_id': fields.many2one(
'payment.order',
'Match this payment order',
domain=[
('state', '=', 'sent'),
],
),
'manual_payment_line_id': fields.many2one(
'payment.line',
'Match this payment line',
domain=[
('order_id.state', '=', 'sent'),
('date_done', '=', False),
],
),
}

View File

@@ -24,7 +24,7 @@
##############################################################################
from openerp.osv import orm, fields
from openerp import netsvc
from openerp import workflow
from openerp.tools.translate import _
@@ -36,16 +36,36 @@ class payment_line(orm.Model):
accounts.
'''
_inherit = 'payment.line'
def _get_transfer_move_line(self, cr, uid, ids, name, arg, context=None):
res = {}
for order_line in self.browse(cr, uid, ids, context=context):
if not order_line.transit_move_line_id:
continue
if len(order_line.transit_move_line_id.move_id.line_id) != 2:
continue
for move_line in order_line.transit_move_line_id.move_id.line_id:
if move_line.id != order_line.transit_move_line_id.id:
res[order_line.id] = move_line.id
return res
_columns = {
'msg': fields.char('Message', size=255, required=False, readonly=True),
'date_done': fields.date(
'Date Confirmed', select=True, readonly=True),
'transit_move_line_id': fields.many2one(
# this line is part of the credit side of move 2a
# from the documentation
'account.move.line', 'Debit move line',
'account.move.line', 'Transfer move line',
readonly=True,
help="Move line through which the debit order pays the invoice",
help="Move line through which the payment/debit order "
"pays the invoice",
),
'transfer_move_line_id': fields.function(
_get_transfer_move_line,
type='many2one',
relation='account.move.line',
string='Transfer move line counterpart',
read_only=True,
help="Counterpart move line on the transfer account",
),
}
@@ -98,7 +118,7 @@ class payment_line(orm.Model):
Reconcile a debit order's payment line with the the move line
that it is based on. Called from payment_order.action_sent().
As the amount is derived directly from the counterpart move line,
we do not expect a write off. Take partially reconcilions into
we do not expect a write off. Take partial reconciliations into
account though.
:param payment_line_id: the single id of the canceled payment line
@@ -160,12 +180,12 @@ class payment_line(orm.Model):
reconcile_obj.create(
cr, uid, vals, context=context)
for line_id in line_ids:
netsvc.LocalService("workflow").trg_trigger(
workflow.trg_trigger(
uid, 'account.move.line', line_id, cr)
# If a bank transaction of a storno was first confirmed
# and now canceled (the invoice is now in state 'debit_denied'
if torec_move_line.invoice:
netsvc.LocalService("workflow").trg_validate(
workflow.trg_validate(
uid, 'account.invoice', torec_move_line.invoice.id,
'undo_debit_denied', cr)

View File

@@ -43,6 +43,7 @@ class payment_mode(orm.Model):
help=('Journal to write payment entries when confirming '
'a debit order of this mode'),
),
# TODO: extract this to account_banking_payment_term
'payment_term_ids': fields.many2many(
'account.payment.term', 'account_payment_order_terms_rel',
'mode_id', 'term_id', 'Payment terms',

View File

@@ -27,6 +27,9 @@
from openerp.osv import orm
# TODO: extract this in anoter module such as account_banking_payment_term
class payment_order_create(orm.TransientModel):
_inherit = 'payment.order.create'

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="transaction_wizard">
<field name="name">transaction.wizard</field>
<field name="model">banking.transaction.wizard</field>
<field name="inherit_id"
ref="account_banking.transaction_wizard_first" />
<field name="arch" type="xml">
<field name="invoice_ids" position="before">
<field name="payment_order_ids" invisible="True"/>
</field>
<xpath expr="//group/separator[@string='Multiple matches']/.."
position="after">
<group>
<field name='payment_line_id'
attrs="{'invisible': [('match_type', 'not in',
('storno', 'payment', 'payment_manual'))]}" />
</group>
</xpath>
<field name="move_line_id" position="after">
<group>
<field name='payment_order_id'
attrs="{'readonly': [('match_multi', '=', False)],
'invisible': [('match_type', 'not in',
('payment_order', 'payment_order_manual'))]}"
domain="[('id', 'in', payment_order_ids[0][2])]" />
</group>
</field>
<page string="Manual match" position="after">
<page string="Match Payments">
<separator string="Match this payment line" colspan="4"/>
<field name="manual_payment_line_id"/>
<separator string="Match this payment order" colspan="4"/>
<field name="manual_payment_order_id"/>
</page>
</page>
</field>
</record>
</data>
</openerp>

View File

@@ -29,6 +29,7 @@
/>
</group>
<group colspan="2">
<!-- TODO: extract to account_banking_payment_term -->
<separator colspan="2"
string="Optional filter by payment term" />
<field name="payment_term_ids" nolabel="1" colspan="2"/>

View File

@@ -2,21 +2,21 @@
<openerp>
<data>
<!-- New activity for workflow payment order: sent -->
<record id="account_banking.act_sent" model="workflow.activity">
<record id="act_sent" model="workflow.activity">
<field name="name">sent</field>
<field name="wkf_id" ref="account_payment.wkf_payment_order"/>
<field name="action">action_sent()</field>
<field name="kind">function</field>
</record>
<!-- New activity for workflow payment order: sent -->
<record id="account_banking.act_sent_wait" model="workflow.activity">
<record id="act_sent_wait" model="workflow.activity">
<field name="name">sent_wait</field>
<field name="wkf_id" ref="account_payment.wkf_payment_order"/>
<field name="action">write({'state': 'sent'})</field>
<field name="kind">function</field>
</record>
<!-- New activity for workflow payment order: rejected -->
<record id="account_banking.act_rejected" model="workflow.activity">
<record id="act_rejected" model="workflow.activity">
<field name="name">rejected</field>
<field name="wkf_id" ref="account_payment.wkf_payment_order"/>
<field name="action">action_rejected()
@@ -26,30 +26,38 @@ write({'state':'rejected'})</field>
<!-- Rewrite existing open -> done transition to include 'sent' stage -->
<record id="account_payment.trans_open_done" model="workflow.transition">
<field name="act_from" ref="account_payment.act_open"/>
<field name="act_to" ref="account_banking.act_sent"/>
<field name="act_to" ref="act_sent"/>
<field name="signal">sent</field>
</record>
<!-- the done signal continues to work but goes to sent -->
<record id="account_banking.trans_open_done" model="workflow.transition">
<record id="trans_open_done" model="workflow.transition">
<field name="act_from" ref="account_payment.act_open"/>
<field name="act_to" ref="account_banking.act_sent"/>
<field name="act_to" ref="act_sent"/>
<field name="signal">done</field>
</record>
<!-- From sent straight to sent_wait -->
<record id="account_banking.trans_sent_sent_wait" model="workflow.transition">
<field name="act_from" ref="account_banking.act_sent"/>
<field name="act_to" ref="account_banking.act_sent_wait"/>
<record id="trans_sent_sent_wait" model="workflow.transition">
<field name="act_from" ref="act_sent"/>
<field name="act_to" ref="act_sent_wait"/>
</record>
<!-- Reconciliation from the banking statement leads to done state -->
<record id="account_banking.trans_sent_done" model="workflow.transition">
<field name="act_from" ref="account_banking.act_sent_wait"/>
<record id="trans_sent_done" model="workflow.transition">
<field name="act_from" ref="act_sent_wait"/>
<field name="act_to" ref="account_payment.act_done"/>
<field name="condition">test_done()</field>
<field name="signal">done</field>
</record>
<record id="trans_sent_done_auto" model="workflow.transition">
<field name="act_from" ref="act_sent_wait"/>
<field name="act_to" ref="account_payment.act_done"/>
<field name="condition">test_done()</field>
<field name="trigger_model">account.move.line</field>
<field name="trigger_expr_id">get_transfer_move_line_ids()</field>
</record>
<!-- Rejected by the bank -->
<record id="account_banking.trans_sent_rejected" model="workflow.transition">
<field name="act_from" ref="account_banking.act_sent"/>
<field name="act_to" ref="account_banking.act_rejected"/>
<record id="trans_sent_rejected" model="workflow.transition">
<field name="act_from" ref="act_sent"/>
<field name="act_to" ref="act_rejected"/>
<field name="signal">rejected</field>
</record>
<!--
@@ -60,16 +68,25 @@ write({'state':'rejected'})</field>
unfortunately.
-->
<record id="account_payment.act_done" model="workflow.activity">
<field name="action">action_done()
write({'state':'done'})</field>
<field name="flow_stop" eval="False"/>
</record>
<!-- Cancel the reconciled payment order -->
<record id="trans_done_sent" model="workflow.transition">
<field name="act_from" ref="account_payment.act_done"/>
<field name="act_to" ref="account_banking.act_sent_wait"/>
<field name="act_to" ref="act_sent_wait"/>
<field name="condition">test_undo_done()</field>
<field name="signal">undo_done</field>
</record>
<record id="trans_done_sent_auto" model="workflow.transition">
<field name="act_from" ref="account_payment.act_done"/>
<field name="act_to" ref="act_sent_wait"/>
<field name="condition">test_undo_done()</field>
<field name="trigger_model">account.move.line</field>
<field name="trigger_expr_id">get_transfer_move_line_ids()</field>
</record>
</data>
</openerp>