|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# This file is part of ofxstatement-austrian. |
| 3 | +# See README.rst for more information. |
| 4 | + |
| 5 | +import csv |
| 6 | +import re |
| 7 | +from datetime import datetime |
| 8 | +from ofxstatement.plugin import Plugin |
| 9 | +from ofxstatement.parser import CsvStatementParser |
| 10 | +from ofxstatement.statement import generate_transaction_id |
| 11 | +from ofxstatement import statement |
| 12 | +from ofxstatement.plugins.utils import \ |
| 13 | + clean_multiple_whitespaces, fix_amount_string |
| 14 | + |
| 15 | + |
| 16 | +# TODO |
| 17 | +# 1 Check account data, see https://github.com/kedder/ofxstatement/blob/master/src/ofxstatement/ofx.py#L117 |
| 18 | +# * account.bank_id |
| 19 | +# * account.acct_id |
| 20 | +# * account.acct_type |
| 21 | +# * ... |
| 22 | +# 2. Parse more data i.e. BANKOMAT? |
| 23 | + |
| 24 | +class BankAustriaCsvParser(CsvStatementParser): |
| 25 | + """The csv parser for Bank Austria.""" |
| 26 | + |
| 27 | + date_format = "%d.%m.%Y" |
| 28 | + |
| 29 | + # 0 Buchungsdatum |
| 30 | + # 1 Valutadatum |
| 31 | + # 2 Buchungstext |
| 32 | + # 3 Interne Notiz |
| 33 | + # 4 Waehrung |
| 34 | + # 5 Betrag |
| 35 | + # 6 Belegdaten |
| 36 | + # 7 Belegnummer |
| 37 | + # 8 Auftraggebername |
| 38 | + # 9 Auftraggeberkonto |
| 39 | + # 10 Auftraggeber BLZ |
| 40 | + # 11 Empfaengername |
| 41 | + # 12 Empfaengerkonto |
| 42 | + # 13 Empfaenger BLZ |
| 43 | + # 14 Zahlungsgrund |
| 44 | + |
| 45 | + mappings = { |
| 46 | + "date": 1, |
| 47 | + "date_user": 0, |
| 48 | + "memo": 14, |
| 49 | + "amount": 5, |
| 50 | + "check_no": 7, |
| 51 | + "payee": 11, |
| 52 | + } |
| 53 | + |
| 54 | + def parse(self): |
| 55 | + """Parse.""" |
| 56 | + stmt = super(BankAustriaCsvParser, self).parse() |
| 57 | + statement.recalculate_balance(stmt) |
| 58 | + return stmt |
| 59 | + |
| 60 | + def split_records(self): |
| 61 | + """Split records using a custom dialect.""" |
| 62 | + return csv.reader(self.fin, delimiter=';', quotechar='"') |
| 63 | + |
| 64 | + def parse_record(self, line): |
| 65 | + """Parse a single record.""" |
| 66 | + # Skip header line |
| 67 | + if self.cur_record == 1: |
| 68 | + return None |
| 69 | + |
| 70 | + # Fix German number format prior to parsing |
| 71 | + line[5] = format(fix_amount_string(line[5])) # German number format |
| 72 | + |
| 73 | + # Create statement |
| 74 | + # parse line elements using the mappings defined above (call parse_record() from parent class) |
| 75 | + stmtline = super(BankAustriaCsvParser, self).parse_record(line) |
| 76 | + |
| 77 | + stmtline.id = generate_transaction_id(stmtline) |
| 78 | + |
| 79 | + # TODO remove me when https://github.com/kedder/ofxstatement/commit/38af84d525f5c47c7fab67c02b36c32dcfc805b3 |
| 80 | + stmtline.date_user = datetime.strptime(line[1], self.date_format) # manual date_user conversion as date_user has wrong format |
| 81 | + |
| 82 | + stmtline.trntype = 'DEBIT' if stmtline.amount < 0 else 'CREDIT' |
| 83 | + |
| 84 | + # Account id |
| 85 | + #if not self.statement.account_id: |
| 86 | + # self.statement.account_id = line[9] |
| 87 | + |
| 88 | + # Currency |
| 89 | + if not self.statement.currency: |
| 90 | + self.statement.currency = line[4] |
| 91 | + |
| 92 | + # .payee is imported as "Description" in GnuCash |
| 93 | + # .memo is imported as "Notes" in GnuCash |
| 94 | + # When .payee is empty, GnuCash imports .memo to "Description" and keeps "Notes" empty |
| 95 | + # @see https://github.com/archont00/ofxstatement-unicreditcz/blob/master/src/ofxstatement/plugins/unicreditcz.py#L100 |
| 96 | + |
| 97 | + # Fixup Memo, Payee, and TRXTYPE |
| 98 | + if line[2].startswith('POS'): |
| 99 | + stmtline.trntype = 'POS' |
| 100 | + stmtline.memo = self.parsePosAtm(line[2]) |
| 101 | + |
| 102 | + elif line[2].startswith('ATM'): |
| 103 | + stmtline.trntype = 'ATM' |
| 104 | + stmtline.memo = self.parsePosAtm(line[2]) |
| 105 | + |
| 106 | + elif line[2].startswith('AUTOMAT') or line[2].startswith('BANKOMAT'): |
| 107 | + # > AUTOMAT 00011942 K1 14.01. 13:47 O |
| 108 | + # > BANKOMAT 00021241 K4 08.03. 09:43 O |
| 109 | + stmtline.trntype = 'ATM' |
| 110 | + # TODO stmtline.memo = self.parsePosAtm(line[2]) ? |
| 111 | + stmtline.memo = line[2] |
| 112 | + |
| 113 | + elif line[2].startswith('ABHEBUNG AUTOMAT'): |
| 114 | + # > ABHEBUNG AUTOMAT NR. 14547 AM 31.01. UM 15.53 UHR Fil.ABC BANKCARD 2 |
| 115 | + # TODO stmtline.memo = self.parsePosAtm(line[2]) ? |
| 116 | + stmtline.trntype = 'ATM' |
| 117 | + stmtline.memo = line[2] |
| 118 | + |
| 119 | + elif line[2].startswith('EINZAHLUNG'): |
| 120 | + # > EINZAHLUNG AUTOMAT NR. 55145 AM 31.01. / 15.55 UHR Fil.ABC BANKCARD 2 EIGENERLAG |
| 121 | + stmtline.memo = line[2] |
| 122 | + |
| 123 | + |
| 124 | + elif line[2].startswith('Lastschrift JustinCase'): |
| 125 | + # > Lastschrift JustinCase MRefAT123123123123123123JIC Entgelt für Bank Austria 0,69 enth‰lt 20% Ust., das sind EUR 0,12. |
| 126 | + stmtline.memo = line[2] |
| 127 | + |
| 128 | + elif line[6].startswith('SEPA-AUFTRAGSBESTÄTIGUNG'): |
| 129 | + if not stmtline.memo: |
| 130 | + stmtline.memo = self.parseDocument(line[6]) |
| 131 | + |
| 132 | + elif line[6].startswith('GUTSCHRIFT') or line[6].startswith('SEPA') or line[6].startswith('ÜBERWEISUNG'): |
| 133 | + # Auftraggebername holds the information we want |
| 134 | + stmtline.payee = line[8] |
| 135 | + if not stmtline.memo: |
| 136 | + stmtline.memo = self.parseDocument(line[6]) |
| 137 | + |
| 138 | + else: |
| 139 | + stmtline.memo = line[2] |
| 140 | + |
| 141 | + # Simple cleanup |
| 142 | + stmtline.payee = clean_multiple_whitespaces(stmtline.payee) |
| 143 | + stmtline.memo = clean_multiple_whitespaces(stmtline.memo) |
| 144 | + |
| 145 | + # Add Internal Note, if exists |
| 146 | + if line[3]: |
| 147 | + # Add trailing whitespace if memo exists |
| 148 | + if stmtline.memo: |
| 149 | + stmtline.memo = stmtline.memo + ' ' |
| 150 | + stmtline.memo = stmtline.memo + '(NOTE: )' + line[3] |
| 151 | + |
| 152 | + return stmtline |
| 153 | + |
| 154 | + def parseDocument(self, toparse): |
| 155 | + """Parse Belegdaten""" |
| 156 | + # 123456789x123456789x123456789x123456789x123456789x123456789x123456789x123456789x123456789x123456789x |
| 157 | + # SEPA-AUFTRAGSBESTÄTIGUNG |
| 158 | + # GUTSCHRIFT |
| 159 | + # ÜBERWEISUNG |
| 160 | + # SEPA LASTSCHRIFT |
| 161 | + p = re.compile('.*Belegnr.: ([0-9.]{18}).*(?:Zahlungsempf|Zahlungspfl).: (.{56}).*Zahlungsgrund: (.{105}).*Zahlungsref.: (.{110})') |
| 162 | + mm = p.findall(toparse) |
| 163 | + if mm: |
| 164 | + m = mm[0] |
| 165 | + no = m[0] |
| 166 | + myname = m[1].strip() |
| 167 | + reason = m[2].strip() |
| 168 | + ref = m[3].strip() |
| 169 | + |
| 170 | + if reason: |
| 171 | + text = reason |
| 172 | + else: |
| 173 | + text = ref |
| 174 | + |
| 175 | + memo = '%s' % (text) |
| 176 | + else: |
| 177 | + memo = 'ERR: ' + toparse |
| 178 | + |
| 179 | + #print(m) |
| 180 | + return memo |
| 181 | + |
| 182 | + def parsePosAtm(self, toparse): |
| 183 | + """Parse POS/ATM Lines""" |
| 184 | + |
| 185 | + # POS/ATM have a fixed layout in line[2]. Some data can also be found in other columns |
| 186 | + # i.e. |
| 187 | + # > 123456789x123456789x123456789x123456789x123456789x123456789x123456789x123456789x123456789x |
| 188 | + # > ATM 100,00 AT K1 15.01. 19:08 O ATM S6EE0275 KLOSTERNEUBUR 4300 |
| 189 | + # > POS 11,00 NL K1 16.01. 14:46 O NS SCHIPHOL 216 LUCHTHAVEN SC 1118 AX |
| 190 | + # |
| 191 | + # Matches: |
| 192 | + # > 0 1 2 3 4 5 - 6 7 8 |
| 193 | + # > TYPE AMT CC ## DATE TIME O SHOP LOCATION ZIP |
| 194 | + |
| 195 | + p = re.compile('(POS|ATM) +([0-9]+,[0-9]+) ([A-Z]+) +(K[0-9]) +(......) (..:..) O (.{22}) +(.{13}) +(.*)') |
| 196 | + mm = p.findall(toparse) |
| 197 | + if mm: |
| 198 | + # ex. result from above |
| 199 | + # ATM: ATM S6EE0275, 4300 KLOSTERNEUBUR, AT; 100,00 EUR on 15.01. 19:08h |
| 200 | + # POS: NS SCHIPHOL 216, 1118 AX LUCHTHAVEN SC, NL; 11,00 EUR on 16.01. 14:46h |
| 201 | + |
| 202 | + m = mm[0] |
| 203 | + memo = '%s: %s, %s %s, %s; %s %s on %s %sh' % (m[0], m[6].strip(), m[8], m[7].strip(), m[2], m[1], self.statement.currency, m[4], m[5]) |
| 204 | + else: |
| 205 | + memo = 'ERR: ' + toparse |
| 206 | + |
| 207 | + return memo |
| 208 | + |
| 209 | +class BankAustriaPlugin(Plugin): |
| 210 | + """Bank Austria (CSV)""" |
| 211 | + |
| 212 | + def get_parser(self, filename): |
| 213 | + """Get a parser instance.""" |
| 214 | + encoding = self.settings.get('charset', 'iso-8859-1') |
| 215 | + f = open(filename, 'r', encoding=encoding) |
| 216 | + parser = BankAustriaCsvParser(f) |
| 217 | + parser.statement.bank_id = self.settings.get('bank', 'Bank-Austria') |
| 218 | + return parser |
| 219 | + |
| 220 | +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 smartindent autoindent |
0 commit comments