Merge pull request #65 from acsone/8.0-refactor-account_banking_payment-sbi

8.0 migration of account_banking_payment
This commit is contained in:
Stéphane Bidoul (ACSONE)
2015-01-20 23:42:56 +01:00
39 changed files with 729 additions and 1167 deletions

View File

@@ -621,22 +621,6 @@ class account_bank_statement_line(orm.Model):
class invoice(orm.Model):
'''
Create other reference types as well.
Descendant classes can extend this function to add more reference
types, ie.
def _get_reference_type(self, cr, uid, context=None):
return super(my_class, self)._get_reference_type(cr, uid,
context=context) + [('my_ref', _('My reference')]
Don't forget to redefine the column "reference_type" as below or
your method will never be triggered.
TODO: move 'structured' part to account_banking_payment module
where it belongs
'''
_inherit = 'account.invoice'
def test_undo_paid(self, cr, uid, ids, context=None):
@@ -649,35 +633,3 @@ class invoice(orm.Model):
return False
return True
def _get_reference_type(self, cr, uid, context=None):
'''
Return the list of reference types
'''
return [('none', _('Free Reference')),
('structured', _('Structured Reference')),
]
_columns = {
'reference_type': fields.selection(_get_reference_type,
'Reference Type', required=True
)
}
class account_move_line(orm.Model):
_inherit = "account.move.line"
def get_balance(self, cr, uid, ids, context=None):
"""
Return the balance of any set of move lines.
Not to be confused with the 'balance' field on this model, which
returns the account balance that the move line applies to.
"""
total = 0.0
if not ids:
return total
for line in self.read(
cr, uid, ids, ['debit', 'credit'], context=context):
total += (line['debit'] or 0.0) - (line['credit'] or 0.0)
return total

View File

@@ -1 +0,0 @@
import model

View File

@@ -1,7 +0,0 @@
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

View File

@@ -1,346 +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 _
from openerp import netsvc
class payment_order(orm.Model):
'''
Enable extra states for payment exports
'''
_inherit = 'payment.order'
_columns = {
'date_scheduled': fields.date(
'Scheduled date if fixed',
states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
help='Select a date if you have chosen Preferred Date to be fixed.'
),
'reference': fields.char(
'Reference', size=128, required=True,
states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
),
'mode': fields.many2one(
'payment.mode', 'Payment mode', select=True, required=True,
states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
help='Select the Payment Mode to be applied.',
),
'state': fields.selection([
('draft', 'Draft'),
('open', 'Confirmed'),
('cancel', 'Cancelled'),
('sent', 'Sent'),
('rejected', 'Rejected'),
('done', 'Done'),
], 'State', select=True
),
'line_ids': fields.one2many(
'payment.line', 'order_id', 'Payment lines',
states={
'open': [('readonly', True)],
'cancel': [('readonly', True)],
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
),
'user_id': fields.many2one(
'res.users', 'User', required=True,
states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
),
'date_prefered': fields.selection([
('now', 'Directly'),
('due', 'Due date'),
('fixed', 'Fixed date')
], "Preferred date", change_default=True, required=True,
states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
},
help=("Choose an option for the Payment Order:'Fixed' stands for "
"a date specified by you.'Directly' stands for the direct "
"execution.'Due date' stands for the scheduled date of "
"execution."
)
),
'date_sent': fields.date('Send date', readonly=True),
}
def _write_payment_lines(self, cr, uid, ids, **kwargs):
'''
ORM method for setting attributes of corresponding payment.line
objects.
Note that while this is ORM compliant, it is also very ineffecient due
to the absence of filters on writes and hence the requirement to
filter on the client(=OpenERP server) side.
'''
if not hasattr(ids, '__iter__'):
ids = [ids]
payment_line_obj = self.pool.get('payment.line')
line_ids = payment_line_obj.search(
cr, uid, [
('order_id', 'in', ids)
])
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)
return True
def set_done(self, cr, uid, ids, *args):
'''
Extend standard transition to update children as well.
'''
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."))
return True
def test_undo_done(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.
"""
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
def _prepare_transfer_move(
self, cr, uid, order, line, labels, context=None):
vals = {
'journal_id': order.mode.transfer_journal_id.id,
'name': '%s %s' % (labels[order.payment_order_type],
line.move_line_id
and line.move_line_id.move_id.name
or line.communication),
'ref': '%s %s' % (order.payment_order_type[:3].upper(),
line.move_line_id
and line.move_line_id.move_id.name
or line.communication),
}
return vals
def _prepare_move_line_transfer_account(
self, cr, uid, order, line, move_id, labels, context=None):
vals = {
'name': _('%s for %s') % (
labels[order.payment_order_type],
line.move_line_id and (line.move_line_id.invoice
and line.move_line_id.invoice.number
or line.move_line_id.name)
or line.communication),
'move_id': move_id,
'partner_id': False,
'account_id': order.mode.transfer_account_id.id,
'credit': (order.payment_order_type == 'payment'
and line.amount or 0.0),
'debit': (order.payment_order_type == 'debit'
and line.amount or 0.0),
'date': fields.date.context_today(
self, cr, uid, context=context),
}
return vals
def _update_move_line_partner_account(
self, cr, uid, order, line, vals, context=None):
vals.update({
'partner_id': line.partner_id.id,
'account_id': (line.move_line_id
and line.move_line_id.account_id.id
or False),
# if not line.move_line_id, the field 'account_id' must be set by
# another module that inherit this function, like for example in
# the module purchase_payment_order
'credit': (order.payment_order_type == 'debit'
and line.amount or 0.0),
'debit': (order.payment_order_type == 'payment'
and line.amount or 0.0),
})
return vals
def action_sent_no_move_line_hook(self, cr, uid, pay_line, context=None):
"""This function is designed to be inherited"""
return
def action_sent(self, cr, uid, ids, context=None):
"""
Create the moves that pay off the move lines from
the debit order. This happens when the debit order file is
generated.
"""
account_move_obj = self.pool.get('account.move')
account_move_line_obj = self.pool.get('account.move.line')
payment_line_obj = self.pool.get('payment.line')
labels = {
'payment': _('Payment order'),
'debit': _('Direct debit order'),
}
for order in self.browse(cr, uid, ids, context=context):
if not order.mode.transfer_journal_id \
or not order.mode.transfer_account_id:
continue
for line in order.line_ids:
# basic checks
if line.move_line_id and line.move_line_id.reconcile_id:
raise orm.except_orm(
_('Error'),
_('Move line %s has already been paid/reconciled')
% line.move_line_id.name)
move_id = account_move_obj.create(
cr, uid, self._prepare_transfer_move(
cr, uid, order, line, labels, context=context),
context=context)
# TODO: take multicurrency into account
# create the 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
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
payment_line_obj.write(
cr, uid, line.id,
{'transit_move_line_id': reconcile_move_line_id},
context=context)
if line.move_line_id:
payment_line_obj.debit_reconcile(
cr, uid, line.id, context=context)
else:
self.action_sent_no_move_line_hook(
cr, uid, line, context=context)
account_move_obj.post(cr, uid, [move_id], context=context)
# State field is written by act_sent_wait
self.write(cr, uid, ids, {
'date_sent': fields.date.context_today(
self, cr, uid, context=context),
}, context=context)
return True

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

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Make buttons on payment order sensitive for extra states,
restore wizard functionality when making payments
-->
<record id="view_banking_payment_order_form_1" model="ir.ui.view">
<field name="name">account.payment.order.form.banking-1</field>
<field name="inherit_id" ref="account_payment.view_payment_order_form" />
<field name="model">payment.order</field>
<field name="arch" type="xml">
<data>
<xpath expr="//button[@string='Select Invoices to Pay']"
position="attributes">
<attribute name="attrs">{
'invisible':[('state','!=','draft')]
}</attribute>
</xpath>
</data>
</field>
</record>
</data>
</openerp>

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

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!--
Add the payment mode type and transfer settings
-->
<record id="view_payment_mode_form_inherit" model="ir.ui.view">
<field name="name">payment.mode.form.inherit</field>
<field name="model">payment.mode</field>
<field name="inherit_id" ref="account_banking_payment_export.view_payment_mode_form_inherit"/>
<field name="arch" type="xml">
<field name="type" position="after">
<group colspan="4" col="4">
<group colspan="2">
<separator colspan="2"
string="Transfer move settings" />
<field name="transfer_account_id"
domain="[('type', '=', 'other'),
('reconcile', '=', True),
('company_id', '=', company_id)]"
context="{
'default_type': 'other',
'default_reconcile': True,
'default_company_id': company_id}"
/>
<field name="transfer_journal_id"
domain="[('company_id', '=', company_id)]"
/>
</group>
<group colspan="2">
<separator colspan="2"
string="Optional filter by payment term" />
<field name="payment_term_ids" nolabel="1" colspan="2"/>
</group>
</group>
</field>
</field>
</record>
</data>
</openerp>

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

@@ -30,6 +30,14 @@
<field name="country" ref="base.fr"/>
</record>
<record id="bank_fortis" model="res.bank">
<field name="name">BNP Paribas Fortis Charleroi</field>
<field name="bic">GEBABEBB03A</field>
<field name="city">Charleroi</field>
<field name="country" ref="base.be"/>
</record>
<record id="main_company_iban" model="res.partner.bank">
<field name="acc_number">FR76 4242 4242 4242 4242 4242 424</field>
<field name="state">iban</field>
@@ -57,6 +65,15 @@
<field name="bank_bic">FTNOFRP1XXX</field>
</record>
<record id="res_partner_2_iban" model="res.partner.bank">
<field name="acc_number">BE96 9988 7766 5544</field>
<field name="state">iban</field>
<field name="bank" ref="bank_fortis"/>
<field name="partner_id" ref="base.res_partner_2" />
<field name="bank_name">BNP Paribas Fortis Charleroi</field>
<field name="bank_bic">GEBABEBB03A</field>
</record>
<record id="account_payment.payment_mode_1" model="payment.mode">
<field name="type" ref="account_banking_payment_export.manual_bank_tranfer"/>
</record>

View File

@@ -3,3 +3,4 @@ from . import account_payment
from . import payment_mode
from . import payment_mode_type
from . import account_move_line
from . import account_invoice

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2014 ACSONE SA (<http://acsone.eu>).
#
# 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 import api, models, _
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
@api.model
def _get_reference_type(self):
rt = super(AccountInvoice, self)._get_reference_type()
rt.append(('structured', _('Structured Reference')))
return rt

View File

@@ -23,20 +23,37 @@ from openerp.osv import orm, fields
from operator import itemgetter
# All the code below aims at fixing one small issue in _to_pay_search()
# But _to_pay_search() is the search function of the field 'amount_to_pay'
# which is a field.function and these functions are not inheritable in OpenERP.
# So we have to inherit the field 'amount_to_pay' and duplicate the related
# functions
# If the patch that I proposed in this bug report
# https://bugs.launchpad.net/openobject-addons/+bug/1275478
# is integrated in addons/account_payment, then we will be able to remove this
# file. -- Alexis de Lattre
class AccountMoveLine(orm.Model):
_inherit = 'account.move.line'
def get_balance(self, cr, uid, ids, context=None):
"""
Return the balance of any set of move lines.
Not to be confused with the 'balance' field on this model, which
returns the account balance that the move line applies to.
"""
total = 0.0
if not ids:
return total
for line in self.read(
cr, uid, ids, ['debit', 'credit'], context=context):
total += (line['debit'] or 0.0) - (line['credit'] or 0.0)
return total
# All the code below aims at fixing one small issue in _to_pay_search()
# But _to_pay_search() is the search function of the field 'amount_to_pay'
# which is a field.function and these functions are not inheritable in
# OpenERP.
# So we have to inherit the field 'amount_to_pay' and duplicate the related
# functions
# If the patch that I proposed in this bug report
# https://bugs.launchpad.net/openobject-addons/+bug/1275478
# is integrated in addons/account_payment, then we will be able to remove
# 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

@@ -16,6 +16,10 @@
<field name="mode" position="after">
<field name="mode_type" invisible="1"/>
</field>
<xpath expr="//button[@string='Invoices']" position="attributes">
<attribute name="attrs">{
'invisible': [('state', '!=', 'draft')]}</attribute>
</xpath>
</field>
</record>

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

@@ -0,0 +1 @@
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,27 @@
##############################################################################
{
'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',
'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

@@ -0,0 +1,4 @@
from . import account_payment
from . import payment_line
from . import payment_mode
from . import account_move_reconcile

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2014 ACSONE SA (<http://acsone.eu>).
# Copyright (C) 2014 Akretion (www.akretion.com)
#
# 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 import models, workflow, api
class AccountMoveReconcile(models.Model):
_inherit = 'account.move.reconcile'
@api.multi
def unlink(self):
"""
Workflow triggers upon unreconcile. This should go into the core.
"""
line_ids = []
for reconcile in self:
for move_line in reconcile.line_id:
line_ids.append(move_line.id)
res = super(AccountMoveReconcile, self).unlink()
for line_id in line_ids:
workflow.trg_trigger(
self._uid, 'account.move.line', line_id, self._cr)
return res

View File

@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
# (C) 2014 ACSONE SA (<http://acsone.eu>).
# (C) 2014 Akretion (www.akretion.com)
#
# 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 import models, fields, api, _
class PaymentOrder(models.Model):
'''
Enable extra states for payment exports
'''
_inherit = 'payment.order'
date_scheduled = fields.Date(states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)],
})
reference = fields.Char(states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)],
})
mode = fields.Many2one(states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)],
})
state = fields.Selection([
('draft', 'Draft'),
('open', 'Confirmed'),
('cancel', 'Cancelled'),
('sent', 'Sent'),
('rejected', 'Rejected'),
('done', 'Done'),
], string='State')
line_ids = fields.One2many(states={
'open': [('readonly', True)],
'cancel': [('readonly', True)],
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
})
user_id = fields.Many2one(states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
})
date_prefered = fields.Selection(states={
'sent': [('readonly', True)],
'rejected': [('readonly', True)],
'done': [('readonly', True)]
})
date_sent = fields.Date(string='Send date', readonly=True)
def action_rejected(self, cr, uid, ids, context=None):
return True
@api.multi
def action_done(self):
for line in self.line_ids:
line.date_done = fields.Date.context_today(self)
self.date_done = fields.Date.context_today(self)
# state is written in workflow definition
return True
@api.multi
def _get_transfer_move_lines(self):
"""
Get the transfer move lines (on the transfer account).
"""
res = []
for order in self:
for order_line in order.line_ids:
move_line = order_line.transfer_move_line_id
if move_line:
res.append(move_line)
return res
@api.multi
def get_transfer_move_line_ids(self, *args):
'''Used in the workflow for trigger_expr_id'''
return [move_line.id for move_line in self._get_transfer_move_lines()]
@api.multi
def test_done(self):
"""
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()])
@api.multi
def test_undo_done(self):
return not self.test_done()
@api.model
def _prepare_transfer_move(self):
# TODO question : can I use self.mode.xxx in an @api.model ??
# It works, but I'm not sure we are supposed to do that !
# I didn't want to use api.one to avoid having to
# do self._prepare_transfer_move()[0] in action_sent
# I prefer to just have to do self._prepare_transfer_move()
vals = {
'journal_id': self.mode.transfer_journal_id.id,
'ref': '%s %s' % (
self.payment_order_type[:3].upper(), self.reference)
}
return vals
@api.model
def _prepare_move_line_transfer_account(
self, amount, move, payment_lines, labels):
if len(payment_lines) == 1:
partner_id = payment_lines[0].partner_id.id
name = _('%s line %s') % (
labels[self.payment_order_type], payment_lines[0].name)
else:
partner_id = False
name = '%s %s' % (
labels[self.payment_order_type], self.reference)
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),
}
return vals
@api.model
def _prepare_move_line_partner_account(self, line, move, labels):
if line.move_line_id:
account_id = line.move_line_id.account_id.id
else:
if self.payment_order_type == 'debit':
account_id = line.partner_id.property_account_receivable.id
else:
account_id = line.partner_id.property_account_payable.id
vals = {
'name': _('%s line %s') % (
labels[self.payment_order_type], line.name),
'move_id': move.id,
'partner_id': line.partner_id.id,
'account_id': account_id,
'credit': (self.payment_order_type == 'debit'
and line.amount or 0.0),
'debit': (self.payment_order_type == 'payment'
and line.amount or 0.0),
}
return vals
@api.model
def action_sent_no_move_line_hook(self, pay_line):
"""This function is designed to be inherited"""
return
@api.one
def action_sent(self):
"""
Create the moves that pay off the move lines from
the debit order. This happens when the debit order file is
generated.
"""
am_obj = self.env['account.move']
aml_obj = self.env['account.move.line']
pl_obj = self.env['payment.line']
labels = {
'payment': _('Payment order'),
'debit': _('Direct debit order'),
}
if self.mode.transfer_journal_id and self.mode.transfer_account_id:
# 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 = {}
if self.mode.transfer_move_option == 'line':
for line in self.line_ids:
trfmoves[line.id] = [line]
else:
if self.date_prefered in ('now', 'fixed'):
trfmoves[True] = []
for line in self.line_ids:
trfmoves[True].append(line)
else: # date_prefered == due
for line in self.line_ids:
if line.date in trfmoves:
trfmoves[line.date].append(line)
else:
trfmoves[line.date] = [line]
for identifier, lines in trfmoves.iteritems():
mvals = self._prepare_transfer_move()
move = am_obj.create(mvals)
total_amount = 0
for line in lines:
# TODO: take multicurrency into account
# create the payment/debit counterpart move line
# on the partner account
partner_ml_vals = self._prepare_move_line_partner_account(
line, move, labels)
partner_move_line = aml_obj.create(partner_ml_vals)
total_amount += line.amount
# register the payment/debit move line
# on the payment line and call reconciliation on it
line.write({'transit_move_line_id': partner_move_line.id})
if line.move_line_id:
pl_obj.debit_reconcile(line.id)
else:
self.action_sent_no_move_line_hook(line)
# create the payment/debit move line on the transfer account
trf_ml_vals = self._prepare_move_line_transfer_account(
total_amount, move, lines, labels)
aml_obj.create(trf_ml_vals)
# post account move
move.post()
# State field is written by act_sent_wait
self.write({'date_sent': fields.Date.context_today(self)})
return True

View File

@@ -24,11 +24,11 @@
##############################################################################
from openerp.osv import orm, fields
from openerp import netsvc
from openerp import workflow
from openerp.tools.translate import _
class payment_line(orm.Model):
class PaymentLine(orm.Model):
'''
Add some fields; make destination bank account
mandatory, as it makes no sense to send payments into thin air.
@@ -36,16 +36,39 @@ 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 order_line.transit_move_line_id:
order_type = order_line.order_id.payment_order_type
trf_lines = order_line.transit_move_line_id.move_id.line_id
for move_line in trf_lines:
if order_type == 'debit' and move_line.debit > 0:
res[order_line.id] = move_line.id
elif order_type == 'payment' and move_line.credit > 0:
res[order_line.id] = move_line.id
else:
res[order_line.id] = False
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',
readonly=True,
help="Counterpart move line on the transfer account",
),
}
@@ -98,7 +121,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 +183,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

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
# (C) 2014 Akretion (www.akretion.com)
#
# 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 import models, fields
class PaymentMode(models.Model):
_inherit = "payment.mode"
transfer_account_id = fields.Many2one(
'account.account', string='Transfer account',
domain=[('type', '=', 'other'), ('reconcile', '=', True)],
help='Pay off lines in sent 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 '
'a debit order 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')

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!--
Add the payment mode transfer account settings
-->
<record id="view_payment_mode_form_inherit" model="ir.ui.view">
<field name="name">payment.mode.form.inherit</field>
<field name="model">payment.mode</field>
<field name="inherit_id" ref="account_banking_payment_export.view_payment_mode_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="/form/group[@col='4']" position="inside">
<group name="trf-move-config" string="Transfer move settings" colspan="2">
<field name="transfer_account_id"
domain="[('type', '=', 'other'),
('reconcile', '=', True),
('company_id', '=', company_id)]"
context="{
'default_type': 'other',
'default_reconcile': True,
'default_company_id': company_id}"
/>
<field name="transfer_journal_id"
domain="[('company_id', '=', company_id)]"
/>
<field name="transfer_move_option"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

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>

View File

@@ -23,5 +23,13 @@
<field name="state">valid</field>
</record>
<record id="res_partner_2_mandate" model="account.banking.mandate">
<field name="partner_bank_id" ref="account_banking_payment_export.res_partner_2_iban"/>
<field name="type">recurrent</field>
<field name="recurrent_sequence_type">first</field>
<field name="signature_date" eval="time.strftime('%Y-%m-01')" />
<field name="state">valid</field>
</record>
</data>
</openerp>

View File

@@ -27,7 +27,7 @@
'category': 'Banking addons',
'depends': [
'account_accountant',
'account_banking_payment',
'account_banking_payment_transfer',
'account_banking_sepa_credit_transfer',
],
'description': '''
@@ -37,5 +37,5 @@ dependencies installed, so that you can run the tests. If you only
run the tests manually, you don't even have to install this module,
only its dependencies.
''',
'installable': False,
'installable': True,
}

View File

@@ -1,4 +1,4 @@
import test_payment_roundtrip
from . import test_payment_roundtrip
fast_suite = [
test_payment_roundtrip,

View File

@@ -18,11 +18,11 @@
#
##############################################################################
from datetime import datetime
from openerp.tests.common import SingleTransactionCase
from openerp import netsvc
from openerp.tests.common import TransactionCase
from openerp import workflow
class TestPaymentRoundtrip(SingleTransactionCase):
class TestPaymentRoundtrip(TransactionCase):
def assert_payment_order_state(self, expected):
"""
@@ -121,7 +121,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
can be validated properly.
"""
partner_model = reg('res.partner')
supplier1 = partner_model.create(
self.supplier1 = partner_model.create(
cr, uid, {
'name': 'Supplier 1',
'supplier': True,
@@ -135,7 +135,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
})
],
}, context=context)
supplier2 = partner_model.create(
self.supplier2 = partner_model.create(
cr, uid, {
'name': 'Supplier 2',
'supplier': True,
@@ -160,7 +160,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
invoice_model = reg('account.invoice')
values = {
'type': 'in_invoice',
'partner_id': supplier1,
'partner_id': self.supplier1,
'account_id': self.payable_id,
'invoice_line': [
(0, False, {
@@ -179,7 +179,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'type': 'in_invoice',
})]
values.update({
'partner_id': supplier2,
'partner_id': self.supplier2,
'name': 'Purchase 2',
'reference_type': 'structured',
'supplier_invoice_number': 'INV2',
@@ -189,13 +189,13 @@ class TestPaymentRoundtrip(SingleTransactionCase):
invoice_model.create(
cr, uid, values, context={
'type': 'in_invoice'}))
wf_service = netsvc.LocalService('workflow')
for invoice_id in self.invoice_ids:
wf_service.trg_validate(
workflow.trg_validate(
uid, 'account.invoice', invoice_id, 'invoice_open', cr)
self.assert_invoices_state('open')
def setup_payment_config(self, reg, cr, uid):
def setup_payment_config(self, reg, cr, uid,
transfer_move_option='line'):
"""
Configure an additional account and journal for payments
in transit and configure a payment mode with them.
@@ -235,6 +235,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'company_id': self.company_id,
'transfer_account_id': transfer_account_id,
'transfer_journal_id': transfer_journal_id,
'transfer_move_option': transfer_move_option,
'type': payment_mode_type_id,
})
@@ -242,11 +243,15 @@ class TestPaymentRoundtrip(SingleTransactionCase):
"""
Create a payment order with the invoices' payable move lines.
Check that the payment order can be confirmed.
date_preferred is set to 'now', to ensure one transfer move
when transfer_move_option = 'date'.
"""
self.payment_order_id = reg('payment.order').create(
cr, uid, {
'reference': 'PAY001',
'mode': self.payment_mode_id,
'date_prefered': 'now',
})
context = {'active_id': self.payment_order_id}
entries = reg('account.move.line').search(
@@ -281,8 +286,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'No payment line created from invoice 2 or with the wrong '
'communication')
wf_service = netsvc.LocalService('workflow')
wf_service.trg_validate(
workflow.trg_validate(
uid, 'payment.order', self.payment_order_id, 'open', cr)
self.assert_payment_order_state('open')
@@ -294,9 +298,7 @@ class TestPaymentRoundtrip(SingleTransactionCase):
"""
export_model = reg('banking.export.sepa.wizard')
export_id = export_model.create(
cr, uid, {
'msg_identification': 'EXP001'},
context={'active_ids': [self.payment_order_id]})
cr, uid, {}, context={'active_ids': [self.payment_order_id]})
export_model.create_sepa(
cr, uid, [export_id])
export_model.save_sepa(
@@ -306,13 +308,13 @@ class TestPaymentRoundtrip(SingleTransactionCase):
def setup_bank_statement(self, reg, cr, uid):
"""
Create a bank statement with a single line. Call the reconciliation
Create a bank statement with a one line for each
payment order line. Call the reconciliation
wizard to match the line with the open payment order. Confirm the
bank statement. Check if the payment order is done.
"""
statement_model = reg('account.bank.statement')
line_model = reg('account.bank.statement.line')
wizard_model = reg('banking.transaction.wizard')
statement_id = statement_model.create(
cr, uid, {
'name': 'Statement',
@@ -320,19 +322,67 @@ class TestPaymentRoundtrip(SingleTransactionCase):
'balance_end_real': -200.0,
'period_id': reg('account.period').find(cr, uid)[0]
})
line_id = line_model.create(
line1_id = line_model.create(
cr, uid, {
'name': 'Statement line',
'statement_id': statement_id,
'amount': -100.0,
'account_id': self.payable_id,
'partner_id': self.supplier1,
})
line1 = line_model.browse(cr, uid, line1_id)
rec_line1 = line_model.\
get_reconciliation_proposition(cr, uid, line1)[0]
line_model.process_reconciliation(cr, uid, line1_id, [
{'counterpart_move_line_id': rec_line1['id'],
'debit': rec_line1['credit'],
'credit': rec_line1['debit']}])
line2_id = line_model.create(
cr, uid, {
'name': 'Statement line',
'statement_id': statement_id,
'amount': -100.0,
'account_id': self.payable_id,
'partner_id': self.supplier2,
})
line2 = line_model.browse(cr, uid, line2_id)
rec_line2 = line_model.\
get_reconciliation_proposition(cr, uid, line2)[0]
line_model.process_reconciliation(cr, uid, line2_id, [
{'counterpart_move_line_id': rec_line2['id'],
'debit': rec_line2['credit'],
'credit': rec_line2['debit']}])
self.assert_payment_order_state('done')
def setup_bank_statement_one_move(self, reg, cr, uid):
"""
Create a bank statement with a single line. Call the reconciliation
wizard to match the line with the open payment order. Confirm the
bank statement. Check if the payment order is done.
"""
statement_model = reg('account.bank.statement')
line_model = reg('account.bank.statement.line')
statement_id = statement_model.create(
cr, uid, {
'name': 'Statement',
'journal_id': self.bank_journal_id,
'balance_end_real': -200.0,
'period_id': reg('account.period').find(cr, uid)[0]
})
line1_id = line_model.create(
cr, uid, {
'name': 'Statement line',
'statement_id': statement_id,
'amount': -200.0,
'account_id': self.payable_id,
})
wizard_id = wizard_model.create(
cr, uid, {'statement_line_id': line_id})
wizard_model.write(
cr, uid, [wizard_id], {
'manual_payment_order_id': self.payment_order_id})
statement_model.button_confirm_bank(cr, uid, [statement_id])
line1 = line_model.browse(cr, uid, line1_id)
rec_line1 = line_model.\
get_reconciliation_proposition(cr, uid, line1)[0]
line_model.process_reconciliation(cr, uid, line1_id, [
{'counterpart_move_line_id': rec_line1['id'],
'debit': rec_line1['credit'],
'credit': rec_line1['debit']}])
self.assert_payment_order_state('done')
def check_reconciliations(self, reg, cr, uid):
@@ -340,6 +390,10 @@ class TestPaymentRoundtrip(SingleTransactionCase):
Check if the payment order has any lines and that
the transit move lines of those payment lines are
reconciled by now.
The transit move line is the line that pays the invoice
so it is reconciled as soon as the payment order
is sent.
"""
payment_order = reg('payment.order').browse(
cr, uid, self.payment_order_id)
@@ -348,9 +402,27 @@ class TestPaymentRoundtrip(SingleTransactionCase):
assert line.transit_move_line_id, \
'Payment order has no transfer move line'
assert line.transit_move_line_id.reconcile_id, \
'Transit move line on payment line is not reconciled'
def check_reconciliations_after_bank_statement(self, reg, cr, uid):
"""
Check if the payment order has any lines and that
the transfer move lines of those payment lines are
reconciled by now.
"""
payment_order = reg('payment.order').browse(
cr, uid, self.payment_order_id)
assert payment_order.line_ids, 'Payment order has no payment lines'
for line in payment_order.line_ids:
assert line.transfer_move_line_id, \
'Payment order has no transfer move line'
assert line.transfer_move_line_id.reconcile_id, \
'Transfer move line on payment line is not reconciled'
def test_payment_roundtrip(self):
""" Payment round trip using transfer account,
with one move per payment order line on the transfer account
"""
reg, cr, uid, = self.registry, self.cr, self.uid
self.setup_company(reg, cr, uid)
self.setup_chart(reg, cr, uid)
@@ -358,5 +430,21 @@ class TestPaymentRoundtrip(SingleTransactionCase):
self.setup_payment_config(reg, cr, uid)
self.setup_payment(reg, cr, uid)
self.export_payment(reg, cr, uid)
self.setup_bank_statement(reg, cr, uid)
self.check_reconciliations(reg, cr, uid)
self.setup_bank_statement(reg, cr, uid)
self.check_reconciliations_after_bank_statement(reg, cr, uid)
def test_payment_roundtrip_one_move(self):
""" Payment round trip using transfer account,
with one move per payment order on the transfer account
"""
reg, cr, uid, = self.registry, self.cr, self.uid
self.setup_company(reg, cr, uid)
self.setup_chart(reg, cr, uid)
self.setup_payables(reg, cr, uid)
self.setup_payment_config(reg, cr, uid, transfer_move_option='date')
self.setup_payment(reg, cr, uid)
self.export_payment(reg, cr, uid)
self.check_reconciliations(reg, cr, uid)
self.setup_bank_statement_one_move(reg, cr, uid)
self.check_reconciliations_after_bank_statement(reg, cr, uid)

View File

@@ -6,20 +6,18 @@
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<data>
<!--
Add new state 'debit_denied' to applicable buttons.
Maybe apply trick in fields_view_get instead, for
better compatibility with other modules?
-->
<!-- button name="invoice_open" position="attributes">
<attribute name="states">draft,proforma2,debit_denied</attribute>
</button -->
<!--
Add new state 'debit_denied' to applicable buttons.
Maybe apply trick in fields_view_get instead, for
better compatibility with other modules?
-->
<!-- button name="invoice_open" position="attributes">
<attribute name="states">draft,proforma2,debit_denied</attribute>
</button -->
<button name="invoice_open" position="after">
<button name="invoice_debit_denied" states="paid"
string="Debit Denied" icon="gtk-cancel"/>
string="Debit Denied"/>
</button>
</data>
</field>
</record>
<record id="view_account_invoice_filter" model="ir.ui.view">
@@ -29,7 +27,7 @@
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<filter name="invoices" position="after">
<filter name="debit_denied" icon="terp-dolar_ok!"
<filter name="debit_denied"
string="Debit denied"
domain="[('state','=','debit_denied')]"
help="Show only invoices with state Debit denied"

View File

@@ -0,0 +1 @@
from . import models

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
#
@@ -23,21 +24,24 @@
#
##############################################################################
from openerp.osv import orm, fields
{
'name': 'Account Banking - Payments Term Filter',
'version': '0.1.1',
'license': 'AGPL-3',
'author': 'Banking addons community',
'website': 'https://github.com/OCA/banking',
'category': 'Banking addons',
'depends': [
'account_banking_payment_export',
],
'data': [
'views/payment_mode.xml',
],
'description': '''Payment term filter on payment mode.
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'),
}
When set, only open invoices corresponding to the mode's
payment term are proposed when populating payment orders.
''',
'auto_install': False,
'installable': True,
}

View File

@@ -0,0 +1,2 @@
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/NV (<http://acsone.eu>).
#
# All other contributions are (C) by their respective contributors
#
@@ -30,19 +31,6 @@ class payment_mode(orm.Model):
_inherit = "payment.mode"
_columns = {
'transfer_account_id': fields.many2one(
'account.account', 'Transfer account',
domain=[('type', '=', 'other'),
('reconcile', '=', True)],
help=('Pay off lines in sent 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', 'Transfer journal',
help=('Journal to write payment entries when confirming '
'a debit order of this mode'),
),
'payment_term_ids': fields.many2many(
'account.payment.term', 'account_payment_order_terms_rel',
'mode_id', 'term_id', 'Payment terms',

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!--
Add the payment mode term filter settings
-->
<record id="view_payment_mode_form_inherit" model="ir.ui.view">
<field name="name">payment.mode.form.inherit</field>
<field name="model">payment.mode</field>
<field name="inherit_id" ref="account_banking_payment_export.view_payment_mode_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="/form/group[@col='4']" position="inside">
<group string="Optional filter by payment term"
name="filter-payment-term" colspan="2">
<field name="payment_term_ids" nolabel="1" colspan="2"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>