# -*- coding: utf-8 -*- ############################################################################## # # Copyright (C) 2011 Smile (). # Copyright (C) 2011 Therp BV (). # # Based on the multibank module by EduSense BV # Copyright (C) 2009 EduSense BV (), # # 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 . # ############################################################################## from datetime import datetime from openerp.addons.account_banking.parsers import models from openerp.addons.account_banking.parsers.convert import str2date from openerp.addons.account_banking.sepa import postalcode from openerp.tools.translate import _ import re import csv __all__ = ['parser'] bt = models.mem_bank_transaction """ First line states the legend "Datum","Naam / Omschrijving","Rekening","Tegenrekening","Code","Af Bij","Bedrag (EUR)","MutatieSoort","Mededelingen """ class transaction_message(object): ''' A auxiliary class to validate and coerce read values ''' attrnames = [ 'date', 'remote_owner', 'local_account', 'remote_account', 'transfer_type', 'debcred', 'transferred_amount', 'transfer_type_verbose', 'message' ] def __init__(self, values, subno): ''' Initialize own dict with attributes and coerce values to right type ''' if len(self.attrnames) != len(values): raise ValueError( _('Invalid transaction line: expected %d columns, found %d') % (len(self.attrnames), len(values))) self.__dict__.update(dict(zip(self.attrnames, values))) # for lack of a standardized locale function to parse amounts self.transferred_amount = float( re.sub(',', '.', self.transferred_amount)) if self.debcred == 'Af': self.transferred_amount = -self.transferred_amount try: self.execution_date = self.effective_date = str2date(self.date, '%Y%m%d') except ValueError: self.execution_date = self.effective_date = str2date(self.date, '%d-%m-%Y') self.statement_id = '' #self.effective_date.strftime('%Yw%W') self.id = str(subno).zfill(4) self.reference = '' # Normalize basic account numbers self.remote_account = self.remote_account.replace('.', '').zfill(10) self.local_account = self.local_account.replace('.', '').zfill(10) class transaction(models.mem_bank_transaction): ''' Implementation of transaction communication class for account_banking. ''' attrnames = ['local_account', 'remote_account', 'remote_owner', 'transferred_amount', 'execution_date', 'effective_date', 'transfer_type', 'id', 'reference', 'statement_id', 'message', ] """ Presumably the same transaction types occur in the MT940 format of ING. From www.ing.nl/Images/MT940_Technische_handleiding_tcm7-69020.pdf """ type_map = { 'AC': bt.ORDER, # Acceptgiro 'BA': bt.PAYMENT_TERMINAL, # Betaalautomaattransactie 'CH': bt.ORDER, # Cheque 'DV': bt.ORDER, # Diversen 'FL': bt.BANK_TERMINAL, # Filiaalboeking, concernboeking 'GF': bt.ORDER, # Telefonisch bankieren 'GM': bt.BANK_TERMINAL, # Geldautomaat 'GT': bt.ORDER, # Internetbankieren 'IC': bt.DIRECT_DEBIT, # Incasso 'OV': bt.ORDER, # Overschrijving 'PK': bt.BANK_TERMINAL, # Opname kantoor 'PO': bt.ORDER, # Periodieke overschrijving 'ST': bt.BANK_TERMINAL, # Storting (eigen rekening of derde) 'VZ': bt.ORDER, # Verzamelbetaling 'NO': bt.STORNO, # Storno } # global expression for matching storno references ref_expr = re.compile('REF[\*:]([0-9A-Z-z_-]+)') # match references for Acceptgiro's through Internet banking kn_expr = re.compile('KN: ([^ ]+)') def __init__(self, line, *args, **kwargs): ''' Initialize own dict with read values. ''' super(transaction, self).__init__(*args, **kwargs) # Copy attributes from auxiliary class to self. for attr in self.attrnames: 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' and self.transferred_amount < 0: res = self.ref_expr.search(self.remote_owner) if res: self.transfer_type = 'NO' self.reference = res.group(1) self.remote_owner = False else: res = self.ref_expr.search(self.message) if res: self.transfer_type = 'NO' self.reference = res.group(1) if self.transfer_type == 'IC': 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 if self.transfer_type == 'GT': res = self.kn_expr.search(self.message) if res: self.reference = res.group(1) if self.transfer_type == 'AC': self.parse_acceptgiro() if self.message and not self.reference: self.reference = self.message def parse_acceptgiro(self): """ Entries of type 'Acceptgiro' can contain the reference in the 'name' column, as well as full address information in the 'message' column' """ reference = '' street = False zipcode = False street = False before = False if self.remote_owner.startswith('KN: '): self.reference = self.remote_owner[4:] self.remote_owner = '' if 'KN: ' in self.message: index = self.message.index('KN: ') before = self.message[:index] self.message = self.message[index:] expression = ( "^\s*(KN:\s*(?P[^\s]+))?(\s*)" "(?PNAVR:\s*[^\s]+)?(\s*)(?P.*?)$") msg_match = re.match(expression, self.message) after = msg_match.group('after') kn = msg_match.group('kn') navr = msg_match.group('navr') if kn: self.reference = kn[4:] self.message = 'Acceptgiro %s' % (navr or '') if after: parts = [after[i:i+33] for i in range(0, len(after), 33)] if parts and not self.remote_owner: self.remote_owner = parts.pop(0).strip() if parts: self.remote_owner_address = [parts.pop(0).strip()] if parts: zip_city = parts.pop(0).strip() zip_match = re.match( "^(?P[^ ]{6})\s+(?P.*?)$", zip_city) if zip_match: self.remote_owner_postalcode = zip_match.group('zipcode') self.remote_owner_city = zip_match.group('city') if before and not self.remote_owner_city: self.remote_owner_city = before.strip() def is_valid(self): if not self.error_message: if not self.transferred_amount: self.error_message = "No transferred amount" elif not self.execution_date: self.error_message = "No execution date" elif not self.remote_account and self.transfer_type not in [ 'BA', 'FL', 'GM', 'IC', 'PK', 'ST' ]: self.error_message = ( "No remote account for transaction type %s" % self.transfer_type) return not self.error_message def parse_message(self): ''' Parse structured message parts into appropriate attributes. No processing done here for Triodos, maybe later. ''' class statement(models.mem_bank_statement): ''' Implementation of bank_statement communication class of account_banking ''' def __init__(self, msg, *args, **kwargs): ''' Set decent start values based on first transaction read ''' super(statement, self).__init__(*args, **kwargs) self.id = msg.statement_id self.local_account = msg.local_account try: self.date = str2date(msg.date, '%Y%m%d') except ValueError: self.date = str2date(msg.date, '%d-%m-%Y') self.start_balance = self.end_balance = 0 # msg.start_balance self.import_transaction(msg) def import_transaction(self, msg): ''' Import a transaction and keep some house holding in the mean time. ''' trans = transaction(msg) self.end_balance += trans.transferred_amount self.transactions.append(trans) class parser(models.parser): code = 'ING' country_code = 'NL' name = _('ING Bank') doc = _('''\ The Dutch ING format is basicly a MS Excel CSV format. It is specifically distinct from the Dutch multibank format. Transactions are not tied to Bank Statements. ''') def parse(self, cr, data): result = [] stmnt = None dialect = csv.excel() dialect.quotechar = '"' dialect.delimiter = ',' lines = data.split('\n') # Transaction lines are not numbered, so keep a tracer subno = 0 statement_id = False for line in csv.reader(lines, dialect=dialect): # Skip empty (last) lines and header line if not line or line[0] == 'Datum': continue subno += 1 msg = transaction_message(line, subno) if not statement_id: statement_id = self.get_unique_statement_id( cr, msg.effective_date.strftime('%Yw%W')) msg.statement_id = statement_id if stmnt: stmnt.import_transaction(msg) else: stmnt = statement(msg) result.append(stmnt) return result # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: