-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathkonekielivelho.py
201 lines (156 loc) · 6.04 KB
/
konekielivelho.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A small program for parsing (Nordea formatted) TITO output from business online bank.
TITO is the Finnish account statement format for storing bank account information in files.
This software is licensed under the MIT license and is provided as-is;
author assumes no responsibility for the use of this software by other parties.
Feel free to customize to your own needs; this is a very specific implementation.
Thanks to Danskebank for almost working manual (not 1:1 match to Nordea output):
http://www-2.danskebank.com/Link/Tito/$file/Tito.pdf
The full specification can be found at:
https://www.fkl.fi/materiaalipankki/ohjeet/Dokumentit/Konekielinen_tiliote_palvelukuvaus.pdf
Default behaviour is to read input.tito from current folder and parse output.csv from it:
./konekielivelho.py
Is equal to:
./konekielivelho.py --input input.tito --output output.csv
"""
import re
import csv
from datetime import datetime
from optparse import OptionParser
__author__ = "Aleksi Häkli"
__copyright__ = "Aleksi Häkli"
__license__ = "MIT"
__version__ = "0.1.0"
__date__ = "2015-04-02"
__status__ = "MVP"
class BankEventType:
HEADER = "T00"
TRANSACTION = "T10"
SPECIFICATION = "T11"
BALANCE = "T40"
CUMULATIVE = "T50"
DESCRIPTION_MATCHER = re.compile(r"^T11...00.*$", flags=re.MULTILINE)
NOT_INTERESTING_MATCHER = re.compile( # Sanitize some unnecessary fields aside payments
r"^.*(PALVELUMAKSU|S.*ST.*LIPAS|ITSEPALVELU).*$",
flags=re.IGNORECASE | re.MULTILINE,
)
@staticmethod
def is_transaction(string):
return string.startswith(BankEventType.TRANSACTION)
@staticmethod
def is_description(string):
return re.match(BankEventType.DESCRIPTION_MATCHER, string)
@staticmethod
def is_interesting(string):
return not re.match(BankEventType.NOT_INTERESTING_MATCHER, string)
class Payment:
"""
A simple class for manazing serialization and deserialization of payment objects.
Add any processing logic to parse_from_list you might require.
"""
DICT_FIELDS = ["datetime", "amount", "payer", "reference", "message"]
def __init__(self):
self.datetime = datetime(year=1970, month=1, day=1)
self.amount = int(0)
self.payer = str()
self.reference = str()
self.message = str()
def __repr__(self): # for readable print() with the object
return '%s,%5.2f,%s,%s,"%s"' % (
self.datetime,
self.amount,
self.payer,
self.reference or "",
self.message,
)
@staticmethod
def parse_from_list(rows):
payment_row = rows[0]
str_datetime = payment_row[42:48].strip()
str_amount = re.sub(r"[,.]", "", payment_row[87:106].strip())
str_payer = payment_row[108:143].strip()
str_reference = payment_row[160:179].strip().lstrip("0")
payment = Payment()
payment.datetime = datetime.strptime(str_datetime, "%y%m%d")
payment.amount = int(str_amount)
payment.payer = str_payer
payment.reference = str_reference
# Parse possible multiline message,
# assumes that input beyond first row
# of bank transaction is always just the message.
if len(rows) > 1:
def parse_message(msg_string):
if len(msg_string) <= 8:
return ""
return msg_string[8::].strip()
payment.message = "\n".join(map(parse_message, rows[1::]))
return payment
def convert_ascii_alphabets(string):
"""
Parse and convert good old ISO 646 ASCII encoded character string.
Characters in @[\]{|} range are reserved to language-specific special characters,
this function converts them to the Finnish / Scandic interpretation of the subset.
"""
substitutions = [
(r"\]", "Å"),
(r"\[", "Ä"),
(r"\\", "Ö"),
(r"\}", "å"),
(r"\{", "ä"),
(r"\|", "ö"),
]
for s in substitutions:
string = re.sub(s[0], s[1], string)
return string
def main():
parser = OptionParser(usage="%prog [options]\n" + __doc__)
parser.add_option(
"-i", "--input", dest="input", help="TITO file to parse", default="input.tito"
)
parser.add_option(
"-o", "--output", dest="output", help="CSV file to output", default="output.csv"
)
(options, args) = parser.parse_args()
with open(options.input, mode="r") as f:
filecontents = f.read()
contents = convert_ascii_alphabets(filecontents).splitlines()
transactions = []
for (i, v) in enumerate(contents):
if BankEventType.is_transaction(v) and BankEventType.is_interesting(v):
transaction = list()
transaction.append(v)
# Find following message lines; this is specific to only message strings
# At the moment no other types of specification string are parsed
# from the statement, so add any logic you need for that here.
j = i
while True:
j += 1
if BankEventType.is_description(contents[j]):
transaction.append(contents[j])
else:
break
transactions.append(transaction)
if transactions:
payments = map(Payment.parse_from_list, transactions)
with open(options.output, encoding="utf-8", mode="w") as f:
writer = csv.DictWriter(f, fieldnames=Payment.DICT_FIELDS)
writer.writeheader()
for p in payments:
writer.writerow(p.__dict__)
print(
"Wrote %d transactions parsed from %s to %s"
% (len(transactions), options.input, options.output)
)
else:
print(
"\n".join(
[
"Input file did not have any interesting transactions, ",
"cowardly refusing to produce an output file from such data.",
]
)
)
if __name__ == "__main__":
main()