diff --git a/account_banking/account_banking.py b/account_banking/account_banking.py
index 351ea78b8..c452796c1 100644
--- a/account_banking/account_banking.py
+++ b/account_banking/account_banking.py
@@ -751,6 +751,26 @@ class payment_line(osv.osv):
return res
+ def debit_storno(self, cr, uid, payment_line_id, amount,
+ currency_id, storno_retry=True, context=None):
+ """
+ Hook for handling a canceled item of a direct debit order.
+ Presumably called from a bank statement import routine.
+
+ Decide on the direction that the invoice's workflow needs to take.
+ You may optionally return an incomplete reconcile for the caller
+ to reconcile the now void payment.
+
+ :param payment_line_id: the single payment line id
+ :param amount: the (negative) amount debited from the bank account
+ :param currency_id: the bank account's currency id
+ :param boolean storno_retry: whether the storno is considered fatal \
+ or not.
+ :return: an incomplete reconcile for the caller to fill
+ :rtype: database id of an account.move.reconcile resource.
+ """
+
+ return False
payment_line()
diff --git a/account_banking/parsers/models.py b/account_banking/parsers/models.py
index 2af0b30cd..d0451f228 100644
--- a/account_banking/parsers/models.py
+++ b/account_banking/parsers/models.py
@@ -174,6 +174,10 @@ class mem_bank_transaction(object):
# An error message for interaction with the user
# Only used when mem_transaction.valid returns False.
'error_message',
+
+ # Storno attribute. When True, make the cancelled debit eligible for
+ # a next direct debit run
+ 'storno_retry',
]
@@ -206,6 +210,10 @@ class mem_bank_transaction(object):
# PERIODIC_ORDER An automated payment by the bank on your behalf.
# Always outgoing.
# Will be selected for matching.
+ # STORNO A failed or reversed attempt at direct debit.
+ # Either due to an action on the payer's side
+ # or a failure observed by the bank (lack of
+ # credit for instance)
#
# Perhaps more will follow.
#
diff --git a/account_banking/wizard/bank_import.py b/account_banking/wizard/bank_import.py
index 7d096fe4f..999348a37 100644
--- a/account_banking/wizard/bank_import.py
+++ b/account_banking/wizard/bank_import.py
@@ -77,10 +77,10 @@ class banking_import(osv.osv_memory):
retval.type = 'general'
if partial:
- move_line.reconcile_partial_id = reconcile_obj.create(
+ reconcile_obj.create(
cursor, uid, {
'type': 'auto',
- 'line_partial_ids': [(4, 0, [move_line.id])]
+ 'line_partial_ids': [(4, 0, [move_line.id])],
}
)
else:
@@ -90,19 +90,56 @@ class banking_import(osv.osv_memory):
]
else:
partial_ids = []
- move_line.reconcile_id = reconcile_obj.create(
+ reconcile_obj.create(
cursor, uid, {
'type': 'auto',
- 'line_id': [
- (4, x, False) for x in [move_line.id] + partial_ids
- ],
- 'line_partial_ids': [
- (3, x, False) for x in partial_ids
- ]
+ 'line_id': [(6, 0, [move_line.id] + partial_ids)],
+ 'line_partial_ids': [(6, 0, [])],
}
)
return retval
+ def _link_storno(
+ self, cr, uid, trans, account_info, log, context=None):
+ payment_line_obj = self.pool.get('payment.line')
+ move_line_obj = self.pool.get('account.move.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)
+ if len(line_ids) == 1:
+ reconcile_id = payment_line_obj.debit_storno(
+ cr, uid, line_ids[0], trans.transferred_amount,
+ account_info.currency_id, trans.storno_retry, context=None)
+ if reconcile_id:
+ # we need to retrieve the move line as per consistency
+ # but it is only used to retrieve the account_id to book
+ # the transfer to. By necessity, we can use any of the
+ # move lines from the reconcile as all of them should have
+ # the same account.
+ move_line_ids = move_line_obj.search(
+ cr, uid,
+ [
+ '|', ('reconcile_id', '=', reconcile_id),
+ ('reconcile_partial_id', '=', reconcile_id),
+ ]
+ , context=context)
+ if move_line_ids:
+ move_line=move_line_obj.browse(
+ cr, uid, move_line_ids[0], context=context)
+ return struct(
+ move_line=move_line,
+ partner_id=False,
+ partner_bank_id=False,
+ reference=False,
+ type='general',
+ )
+ # TODO log the reason why there is no result for transfers marked
+ # as storno
+ return False
+
def _link_debit_order(
self, cr, uid, trans, account_info, log, context=None):
@@ -309,6 +346,7 @@ class banking_import(osv.osv_memory):
]
move_line = False
+
if candidates and len(candidates) > 0:
# Now a possible selection of invoices has been found, check the
# amounts expected and received.
@@ -753,6 +791,9 @@ class banking_import(osv.osv_memory):
if transaction.type == bt.DIRECT_DEBIT:
move_info = self._link_debit_order(
cursor, uid, transaction, account_info, results.log, context)
+ if transaction.type == bt.STORNO:
+ move_info = self._link_storno(
+ cursor, uid, transaction, account_info, results.log, context)
# Allow inclusion of generated bank invoices
if transaction.type == bt.BANK_COSTS:
lines = self._link_costs(
@@ -864,7 +905,12 @@ class banking_import(osv.osv_memory):
)
if move_info:
values.type = move_info.type
- values.reconcile_id = move_info.move_line.reconcile_id.id
+ values.reconcile_id = (
+ move_info.move_line.reconcile_id and
+ move_info.move_line.reconcile_id.id or
+ move_info.move_line.reconcile_partial_id and
+ move_info.move_line.reconcile_partial_id.id
+ )
values.partner_id = move_info.partner_id
values.partner_bank_id = move_info.partner_bank_id
else:
diff --git a/account_banking_nl_ing/ing.py b/account_banking_nl_ing/ing.py
index 498935471..1bddd8824 100644
--- a/account_banking_nl_ing/ing.py
+++ b/account_banking_nl_ing/ing.py
@@ -54,8 +54,6 @@ class transaction_message(object):
# 'remote_owner', 'remote_account', 'transfer_type', 'reference',
]
- ref_expr = re.compile('REF[\*:]([0-9A-Z-z_-]+)')
-
def __init__(self, values, subno):
'''
Initialize own dict with attributes and coerce values to right type
@@ -85,7 +83,7 @@ class transaction(models.mem_bank_transaction):
attrnames = ['local_account', 'remote_account',
'remote_owner', 'transferred_amount',
'execution_date', 'effective_date', 'transfer_type',
- 'reference', 'id',
+ 'id', #'reference',
]
"""
@@ -112,6 +110,9 @@ class transaction(models.mem_bank_transaction):
'NO': bt.STORNO, # Storno
}
+ # global expression for matching storno references
+ ref_expr = re.compile('REF[\*:]([0-9A-Z-z_-]+)')
+
def __init__(self, line, *args, **kwargs):
'''
Initialize own dict with read values.
@@ -119,15 +120,15 @@ class transaction(models.mem_bank_transaction):
super(transaction, self).__init__(*args, **kwargs)
# Copy attributes from auxiliary class to self.
for attr in self.attrnames:
- if attr == 'reference':
- setattr(self, 'reference', False)
- else:
- setattr(self, attr, getattr(line, attr))
+ #if attr == 'reference':
+ # setattr(self, 'reference', False)
+ #else:
+ setattr(self, attr, getattr(line, attr))
# self.message = ''
# Decompose structured messages
self.parse_message()
# Adaptations to direct debit orders ands stornos
- if self.transfer_type == 'DV':
+ if self.transfer_type == 'DV' and self.transferred_amount < 0:
res = self.ref_expr.search(self.remote_owner)
if res:
self.transfer_type = 'NO'
@@ -139,7 +140,15 @@ class transaction(models.mem_bank_transaction):
self.transfer_type = 'NO'
self.reference = res.group(1)
if self.transfer_type == 'IC':
- self.reference = self.remote_owner
+ if self.transferred_amount > 0:
+ self.reference = self.remote_owner
+ else:
+ self.transfer_type = 'NO'
+ self.message = self.remote_owner + self.message
+ res = self.ref_expr.search(self.message)
+ if res:
+ self.reference = res.group(1)
+ self.storno_retry = True
self.remote_owner = False
def is_valid(self):
diff --git a/account_direct_debit/__openerp__.py b/account_direct_debit/__openerp__.py
index df50a2161..8ae7e8973 100644
--- a/account_direct_debit/__openerp__.py
+++ b/account_direct_debit/__openerp__.py
@@ -36,6 +36,7 @@
'view/account_payment.xml',
'view/account_invoice.xml',
'workflow/account_invoice.xml',
+ 'data/account_payment_term.xml',
],
'demo_xml': [],
'description': '''
diff --git a/account_direct_debit/data/account_payment_term.xml b/account_direct_debit/data/account_payment_term.xml
new file mode 100644
index 000000000..1efc35545
--- /dev/null
+++ b/account_direct_debit/data/account_payment_term.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Direct debit
+ Direct debit in 14 days
+
+
+ Direct debit in 14 days
+ balance
+
+
+
+
+
+
diff --git a/account_direct_debit/model/account_payment.py b/account_direct_debit/model/account_payment.py
index 93dad0a01..3601948ea 100644
--- a/account_direct_debit/model/account_payment.py
+++ b/account_direct_debit/model/account_payment.py
@@ -19,6 +19,21 @@ class payment_mode(osv.osv):
help=('Journal to write payment entries when confirming ' +
'a debit order of this mode'),
),
+ 'reference_filter': fields.char(
+ 'Reference filter', size=16,
+ help=(
+ 'Optional substring filter on move line references. ' +
+ 'You can use this in combination with a specific journal ' +
+ 'for items that you want to handle with this mode. Use ' +
+ 'a separate sequence for the journal with a distinguished ' +
+ 'prefix or suffix and enter that character string here.'),
+ ),
+# 'payment_term_ids': fields.many2many(
+# 'account.payment.term', 'account_payment_order_terms_rel',
+# 'mode_id', 'term_id', 'Payment terms',
+# help=('Limit selected invoices to invoices with these payment ' +
+# 'terms')
+# ),
}
payment_mode()
@@ -77,14 +92,17 @@ class payment_order(osv.osv):
for line in order_line.debit_move_line_id.move_id.line_id:
if line.account_id.type == 'other' and not line.reconcile_id:
line_ids.append(line.id)
- import pdb
- pdb.set_trace()
if is_zero(order.line_ids[0].debit_move_line_id,
get_balance(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 action_sent(self, cr, uid, ids, context=None):
@@ -165,86 +183,99 @@ payment_order()
class payment_line(osv.osv):
_inherit = 'payment.line'
- def debit_storno(self, cr, uid, payment_line_id, storno_move_line_id, context=None):
+ def debit_storno(self, cr, uid, payment_line_id, amount,
+ currency_id, storno_retry=True, context=None):
"""
- Process a payment line from a direct debit order which has
- been canceled by the bank or by the user:
- - Undo the reconciliation of the payment line with the move
- line that it originated from, and re-reconciliated with
- the credit payment in the bank journal of the same amount and
- on the same account.
- - Mark the payment line for being reversed.
-
- :param payment_line_id: the single id of the canceled payment line
- :param storno_move_line_id: the credit payment in the bank journal
+ The processing of a storno is triggered by a debit
+ transfer on one of the company's bank accounts.
+ This method offers to re-reconcile the original debit
+ payment. For this purpose, we have registered that
+ payment move on the payment line.
+
+ Return the (now incomplete) reconcile id. The caller MUST
+ re-reconcile this reconcile with the bank transfer and
+ re-open the associated invoice.
+
+ :param payment_line_id: the single payment line id
+ :param amount: the (signed) amount debited from the bank account
+ :param currency_id: the bank account's currency id
+ :param boolean storno_retry: when True, attempt to reopen the invoice, \
+ set the invoice to 'Debit denied' otherwise.
+ :return: an incomplete reconcile for the caller to fill
+ :rtype: database id of an account.move.reconcile resource.
"""
- if isinstance(payment_line_id, (list, tuple)):
- payment_line_id = payment_line_id[0]
- reconcile_obj = self.pool.get('account.move.reconcile')
move_line_obj = self.pool.get('account.move.line')
- payment_line = self.browse(cr, uid, payment_line_id, context=context)
-
- debit_move_line = payment_line.debit_move_line_id
- if (not debit_move_line):
- raise osv.except_osv(
- _('Can not process storno'),
- _('No move line for line %s') % payment_line.name)
- if payment_line.storno:
- raise osv.except_osv(
- _('Can not process storno'),
- _('Cancelation of payment line \'%s\' has already been ' +
- 'processed') % payment_line.name)
-
- def is_zero(total):
- return self.pool.get('res.currency').is_zero(
- cr, uid, debit_move_line.company_id.currency_id, total)
-
- # check validity of the proposed move line
- torec_move_line = move_line_obj.browse(
- cr, uid, storno_move_line_id, context=context)
- if not (is_zero(torec_move_line.debit - debit_move_line.debit) and
- is_zero(torec_move_line.credit - debit_move_line.credit) and
- torec_move_line.account_id.id == debit_move_line.account_id.id):
- raise osv.except_osv(
- _('Can not process storno'),
- _('%s is not a drop-in replacement for %s') % (
- torec_move_line.name, debit_move_line.name))
- if payment_line.storno:
- raise osv.except_osv(
- _('Can not process storno'),
- _('Debit order line %s has already been cancelled') % (
- payment_line.name))
-
- # replace move line in reconciliation
+ reconcile_obj = self.pool.get('account.move.reconcile')
+ line = self.browse(cr, uid, payment_line_id)
reconcile_id = False
- if (payment_line.move_line_id.reconcile_partial_id and
- debit_move_line_id.id in
- payment_line.move_line_id.reconcile_partial_id.line_partial_ids):
- reconcile_id = payment_line.move_line_id.reconcile_partial_id
- vals = {
- 'line_partial_ids':
- [(3, debit_move_line_id.id), (4, torec_move_line.id)],
- }
- elif (payment_line.move_line_id.reconcile_id and
- debit_move_line_id.id in
- payment_line.move_line_id.reconcile_id.line_id):
- reconcile_id = payment_line.move_line_id.reconcile_id
- vals = {
- 'line_id':
- [(3, debit_move_line_id.id), (4, torec_move_line.id)]
- }
- if not reconcile_id:
- raise osv.except_osv(
- _('Can not perform storno'),
- _('Debit order line %s does not occur in the list of '
- 'reconciliation move lines of its origin') %
- debit_move_line_id.name)
- reconcile_obj.write(cr, uid, reconcile_id, vals, context=context)
- self.write(cr, uid, payment_line_id, {'storno': True}, context=context)
- #for line_id in line_ids:
- # netsvc.LocalService("workflow").trg_trigger(
- # uid, 'account.move.line', line_id, cr)
+ if (line.debit_move_line_id and not line.storno and
+ self.pool.get('res.currency').is_zero(
+ cr, uid, currency_id, (
+ (line.debit_move_line_id.credit or 0.0) -
+ (line.debit_move_line_id.debit or 0.0) + amount))):
+ # Two different cases, full and partial
+ # Both cases differ subtly in the procedure to follow
+ # Needs refractoring, but why is this not in the OpenERP API?
+ if line.debit_move_line_id.reconcile_partial_id:
+ reconcile_id = line.debit_move_line_id.reconcile_partial_id.id
+ attribute = 'reconcile_partial_id'
+ if len(line.debit_move_line_id.reconcile_id.line_partial_ids) == 2:
+ # reuse the simple reconcile for the storno transfer
+ reconcile_obj.write(
+ cr, uid, reconcile_id, {
+ 'line_id': [(6, 0, line.debit_move_line_id.id)],
+ 'line_partial_ids': [(6, 0, [])],
+ }, context=context)
+ else:
+ # split up the original reconcile in a partial one
+ # and a new one for reconciling the storno transfer
+ reconcile_obj.write(
+ cr, uid, reconcile_id, {
+ 'line_partial_ids': [(3, line.debit_move_line_id.id)],
+ }, context=context)
+ reconcile_id = reconcile_obj.create(
+ cr, uid, {
+ 'type': 'auto',
+ 'line_id': [(6, 0, line.debit_move_line_id.id)],
+ }, context=context)
+ elif line.debit_move_line_id.reconcile_id:
+ reconcile_id = line.debit_move_line_id.reconcile_id.id
+ if len(line.debit_move_line_id.reconcile_id.line_id) == 2:
+ # reuse the simple reconcile for the storno transfer
+ reconcile_obj.write(
+ cr, uid, reconcile_id, {
+ 'line_id': [(6, 0, [line.debit_move_line_id.id])]
+ }, context=context)
+ else:
+ # split up the original reconcile in a partial one
+ # and a new one for reconciling the storno transfer
+ partial_ids = [
+ x.id for x in line.debit_move_line_id.reconcile_id.line_id
+ if x.id != line.debit_move_line_id.id
+ ]
+ reconcile_obj.write(
+ cr, uid, reconcile_id, {
+ 'line_partial_ids': [(6, 0, partial_ids)],
+ 'line_id': [(6, 0, [])],
+ }, context=context)
+ reconcile_id = reconcile_obj.create(
+ cr, uid, {
+ 'type': 'auto',
+ 'line_id': [(6, 0, line.debit_move_line_id.id)],
+ }, context=context)
+ # mark the payment line for storno processed
+ if reconcile_id:
+ self.write(cr, uid, [payment_line_id],
+ {'storno': True}, context=context)
+ # put forth the invoice workflow
+ if line.move_line_id.invoice:
+ activity = (storno_retry and 'open_test'
+ or 'invoice_debit_denied')
+ netsvc.LocalService("workflow").trg_validate(
+ uid, 'account.invoice', line.move_line_id.invoice.id,
+ activity, cr)
+ return reconcile_id
def debit_reconcile(self, cr, uid, payment_line_id, context=None):
"""
@@ -356,9 +387,29 @@ class payment_order_create(osv.osv_memory):
payment = self.pool.get('payment.order').browse(cr, uid, context['active_id'], context=context)
# Search for move line to pay:
if payment.payment_order_type == 'debit':
- domain = [('reconcile_id', '=', False), ('account_id.type', '=', 'receivable'), ('amount_to_receive', '>', 0)]
+ domain = [
+ ('reconcile_id', '=', False),
+ ('account_id.type', '=', 'receivable'),
+ ('amount_to_receive', '>', 0),
+ ]
+ # cannot filter on properties of (searchable)
+ # function fields. Needs work in expression.expression.parse()
+ # Currently gives an SQL error.
+# # apply payment term filter
+# if payment.mode.payment_term_ids:
+# term_ids = [term.id for term in payment.mode.payment_term_ids]
+# domain = domain + [
+# '|', ('invoice', '=', False),
+# ('invoice.payment_term', 'in', term_ids),
+# ]
else:
- domain = [('reconcile_id', '=', False), ('account_id.type', '=', 'payable'), ('amount_to_pay', '>', 0)]
+ domain = [
+ ('reconcile_id', '=', False),
+ ('account_id.type', '=', 'payable'),
+ ('amount_to_pay', '>', 0)
+ ]
+ if payment.mode.reference_filter:
+ domain.append(('ref', 'ilike', payment.mode.reference_filter))
# domain = [('reconcile_id', '=', False), ('account_id.type', '=', 'payable'), ('amount_to_pay', '>', 0)]
### end account_direct_debit ###
diff --git a/account_direct_debit/view/account_payment.xml b/account_direct_debit/view/account_payment.xml
index 64accde3b..cd70c3ca7 100644
--- a/account_direct_debit/view/account_payment.xml
+++ b/account_direct_debit/view/account_payment.xml
@@ -45,6 +45,10 @@
icon="gtk-find"
/>
+
+
+
+
@@ -59,8 +63,22 @@
+
+
+
+ Payment Lines
+ payment.line
+ tree
+
+
+
+
+
+
+
+
diff --git a/account_direct_debit/workflow/account_invoice.xml b/account_direct_debit/workflow/account_invoice.xml
index c6fe8396d..080d1187f 100644
--- a/account_direct_debit/workflow/account_invoice.xml
+++ b/account_direct_debit/workflow/account_invoice.xml
@@ -16,6 +16,7 @@
+ open_test