From e70e7a70eaedeed16b680cf52d3e86efc2b71417 Mon Sep 17 00:00:00 2001 From: "Pieter J. Kersten" Date: Wed, 9 Feb 2011 20:54:03 +0100 Subject: [PATCH] [FIX] account_banking_nl_clieop: description records are not being used [FIX] account_banking_nl_clieop: wrong position of beneficiary in payments [FIX] account_banking_nl_clieop: wrong alignment of truncates messages [IMP] account_banking_nl_clieop: Allow long messages to wrap over multiple records [IMP] account_banking: Improved matching of partial payments [IMP] account_banking: Dutch ING accounts no longer require online IBAN conversion --- account_banking/record.py | 2 +- account_banking/sepa/online.py | 9 ++-- account_banking/wizard/bank_import.py | 54 +++++++++++++++++----- account_banking_nl_clieop/wizard/clieop.py | 40 ++++++++++++---- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/account_banking/record.py b/account_banking/record.py index 945a156e2..694672b98 100644 --- a/account_banking/record.py +++ b/account_banking/record.py @@ -51,7 +51,7 @@ class Field(object): def format(self, value): value = str(value) if len(value) > self.length: - return value[len(value) - self.length:] + return value[:self.length] return value.ljust(self.length, self.fillchar) def take(self, buffer): diff --git a/account_banking/sepa/online.py b/account_banking/sepa/online.py index c5adc5c3b..c5a7ecef8 100644 --- a/account_banking/sepa/online.py +++ b/account_banking/sepa/online.py @@ -49,11 +49,12 @@ def get_iban_bic_NL(bank_acc): # calculates accounts, so no need to consult it - calculate our own number = bank_acc.lstrip('0') if len(number) <= 7: + iban = IBAN.create(BBAN='INGB' + number.rjust(10, '0'), + countrycode='NL' + ) return struct( - iban = IBAN(BBAN='INGB' + number.rjust(10, '0'), - countrycode='NL' - ).replace(' ',''), - account = iban, + iban = iban.replace(' ',''), + account = iban.BBAN[4:], bic = 'INGBNL2A', code = 'INGBNL', bank = 'ING Bank N.V.', diff --git a/account_banking/wizard/bank_import.py b/account_banking/wizard/bank_import.py index 8033716ab..fd43bf837 100644 --- a/account_banking/wizard/bank_import.py +++ b/account_banking/wizard/bank_import.py @@ -33,6 +33,7 @@ import time import wizard import netsvc import base64 +import datetime from tools import config from tools.translate import _ from account_banking.parsers import models @@ -43,6 +44,10 @@ from banktools import * bt = models.mem_bank_transaction +# This variable is used to match supplier invoices with an invoice date after +# the real payment date. This can occur with online transactions (web shops). +payment_window = datetime.timedelta(days=10) + def parser_types(*args, **kwargs): '''Delay evaluation of parser types until start of wizard, to allow depending modules to initialize and add their parsers to the list @@ -106,6 +111,7 @@ class banking_import(wizard.interface): self.__state = '' self.__linked_invoices = {} self.__linked_payments = {} + self.__multiple_matches = [] def _fill_results(self, *args, **kwargs): return {'log': self._log} @@ -135,7 +141,9 @@ class banking_import(wizard.interface): ) else: if move_line.reconcile_partial_id: - partial_ids = [x.id for x in move_line.reconcile_partial_id] + partial_ids = [x.id for x in + move_line.reconcile_partial_id.line_partial_ids + ] else: partial_ids = [] move_line.reconcile_id = reconcile_obj.create( @@ -284,7 +292,7 @@ class banking_import(wizard.interface): if partner_ids: candidates = [x for x in move_lines if x.partner_id.id in partner_ids and - str2date(x.date, '%Y-%m-%d') <= trans.execution_date + str2date(x.date, '%Y-%m-%d') <= (trans.execution_date + payment_window) and (not _cached(x) or _remaining(x)) ] else: @@ -303,8 +311,9 @@ class banking_import(wizard.interface): # reporting this. candidates = [x for x in candidates or move_lines if x.invoice and has_id_match(x.invoice, ref, msg) - and str2date(x.invoice.date_invoice, '%Y-%m-%d') <= trans.execution_date - and (not _cached(x) or _remaining(x)) + and str2date(x.invoice.date_invoice, '%Y-%m-%d') + <= (trans.execution_date + payment_window) + and (not _cached(x) or _remaining(x)) ] # Match on amount expected. Limit this kind of search to known @@ -312,9 +321,10 @@ class banking_import(wizard.interface): if not candidates and partner_ids: candidates = [x for x in move_lines if round(abs(x.credit or x.debit), digits) == - round(abs(trans.transferred_amount), digits) and - str2date(x.date, '%Y-%m-%d') <= trans.execution_date and - (not _cached(x) or _remaining(x)) + round(abs(trans.transferred_amount), digits) + and str2date(x.date, '%Y-%m-%d') <= + (trans.execution_date + payment_window) + and (not _cached(x) or _remaining(x)) ] move_line = False @@ -325,8 +335,9 @@ class banking_import(wizard.interface): # TODO: currency coercing best = [x for x in candidates if round(abs(x.credit or x.debit), digits) == - round(abs(trans.transferred_amount), digits) and - str2date(x.date, '%Y-%m-%d') <= trans.execution_date + round(abs(trans.transferred_amount), digits) + and str2date(x.date, '%Y-%m-%d') <= + (trans.execution_date + payment_window) ] if len(best) == 1: # Exact match @@ -343,8 +354,9 @@ class banking_import(wizard.interface): # transfers first paid = [x for x in move_lines if x.invoice and has_id_match(x.invoice, ref, msg) - and str2date(x.invoice.date_invoice, '%Y-%m-%d') <= trans.execution_date - and (_cached(x) and not _remaining(x)) + and str2date(x.invoice.date_invoice, '%Y-%m-%d') + <= trans.execution_date + and (_cached(x) and not _remaining(x)) ] if paid: log.append( @@ -364,6 +376,14 @@ class banking_import(wizard.interface): 'ref': trans.reference, 'no_candidates': len(best) or len(candidates) }) + log.append(' ' + + _('Candidates: %(candidates)s') % { + 'candidates': ', '.join([x.invoice.number + for x in best or candidates + ]) + }) + self.__multiple_matches.append((trans, best or + candidates)) move_line = False partial = False @@ -403,6 +423,15 @@ class banking_import(wizard.interface): x.id for x in bank_account_ids if x.partner_id.id == move_line.partner_id.id ] + + # Re-check the cases with multiple candidates again: + # later matches may have removed possible candidates. + for trans, candidates in self.__multiple_matches: + best = [x for x in candidates if not self._cached(x)] + if len(best) == 1: + # Now an exact match can be made + pass + return ( self._get_move_info(cursor, uid, move_line, account_ids and account_ids[0] or False, @@ -411,6 +440,7 @@ class banking_import(wizard.interface): trans2 ) + return (False, False) def _link_canceled_debit(self, cursor, uid, trans, payment_lines, @@ -877,7 +907,7 @@ class banking_import(wizard.interface): i += 1 results.stat_loaded_cnt += 1 - + if payment_lines: # As payments lines are treated as individual transactions, the # batch as a whole is only marked as 'done' when all payment lines diff --git a/account_banking_nl_clieop/wizard/clieop.py b/account_banking_nl_clieop/wizard/clieop.py index 617fb7874..50769d6a2 100644 --- a/account_banking_nl_clieop/wizard/clieop.py +++ b/account_banking_nl_clieop/wizard/clieop.py @@ -37,6 +37,14 @@ def eleven_test(s): r += (l-i) * int(c) return (r % 11) == 0 +def chunk(str, length): + ''' + Split a string in equal sized substrings of length + ''' + while str: + yield str[:length] + str = str[length:] + class HeaderRecord(record.Record): #{{{ '''ClieOp3 header record''' _fields = [ @@ -145,7 +153,7 @@ class DescriptionRecord(record.Record): '''Description''' _fields = [ record.Filler('recordcode', 4, '0160'), - record.Filler('variantcode', 1, 'B'), + record.Filler('variantcode', 1, 'A'), record.Field('description', 32), record.Filler('filler', 13), ] @@ -209,6 +217,17 @@ class Optional(object): setattr(newitem, attr, value) self._guts.append(newitem) + def __len__(self): + '''Return actual contents''' + return len(self._guts) + + def length(self, attr): + '''Return length of optional record''' + res = [x for x in self._klass._fields if x.name == attr] + if res: + return res[0].length + raise AttributeError(attr) + def __getattr__(self, attr): '''Only return if used''' if attr[0] == '_': @@ -246,10 +265,15 @@ class Transaction(object): self.transaction.amount = int(amount * 100) if reference: self.paymentreference.paymentreference = reference - for msg in messages: + # Allow long message lines to redistribute over multiple message + # records + for msg in chunk(''.join(messages), + self.description.length('description') + )[:4]: self.description.description = msg self.name.name = name + class DirectDebit(Transaction): '''Direct Debit Payment transaction''' def __init__(self, *args, **kwargs): @@ -266,8 +290,8 @@ class DirectDebit(Transaction): items = [str(self.transaction)] if self.name: items.append(str(self.name)) - for kenmerk in self.paymentreference: - items.append(str(kenmerk)) + for reference in self.paymentreference: + items.append(str(reference)) for description in self.description: items.append(str(description)) return '\r\n'.join(items) @@ -289,12 +313,12 @@ class Payment(Transaction): Return self as writeable file content object ''' items = [str(self.transaction)] - for kenmerk in self.paymentreference: - items.append(str(kenmerk)) - if self.name: - items.append(str(self.name)) + for reference in self.paymentreference: + items.append(str(reference)) for description in self.description: items.append(str(description)) + if self.name: + items.append(str(self.name)) return '\r\n'.join(items) class SalaryPayment(Payment):