8 Commits

Author SHA1 Message Date
  Alois Mahdal 8572ed626e Add account identification to _desctription (AKA memo) 5 years ago
  Alois Mahdal 1b0de8315c Avoid confusion with RaifTransaction field names 5 years ago
  Alois Mahdal 4d0fdd1eb3 Emphasize typo in the source file 5 years ago
  Alois Mahdal 5559b51e7a Remove boring parts of MbankTransaction memos 5 years ago
  Alois Mahdal 823f186728 Add support for Raiffeisen Bank 5 years ago
  Alois Mahdal 53ab4ffc74 Make parser state logic a bit more bearable 5 years ago
  Alois Mahdal 5f4ada05e7 Just remove "DATUM PROVEDENÍ TRANSAKCE" altogether 5 years ago
  Alois Mahdal 6be3f3d8c1 Add comments to help identify fields 5 years ago
1 changed files with 168 additions and 39 deletions
  1. 168
    39
      gcconv.py

+ 168
- 39
gcconv.py View File

@@ -1,6 +1,10 @@
1 1
 # coding=utf-8
2 2
 """Convert Bank statement CSV to Gnucash-acceptable format"""
3 3
 
4
+from collections import namedtuple
5
+
6
+ParserState = namedtuple('ParserState', 'ths nxt')
7
+
4 8
 
5 9
 def valid_formats():
6 10
     return _parsers.keys()
@@ -53,14 +57,13 @@ class BaseTransactionParser(object):
53 57
 
54 58
     def __init__(self):
55 59
         self.transactions = []
56
-        self.state = False
57
-        self.next_state = False
60
+        self.state = ParserState(ths=False, nxt=False)
58 61
 
59 62
     def is_open(self):
60
-        return self.state
63
+        return self.state.ths
61 64
 
62 65
     def is_closed(self):
63
-        return not self.state
66
+        return not self.state.ths
64 67
 
65 68
     def parse_file(self, fpath, encoding='utf-8'):
66 69
         lines_read = 0
@@ -68,7 +71,7 @@ class BaseTransactionParser(object):
68 71
             for line in f.readlines():
69 72
                 line = line.decode(encoding).strip()
70 73
                 lines_read += 1
71
-                self.state = self.pick_state(line, lineno=lines_read)
74
+                self.state = self.advance(line, lineno=lines_read)
72 75
                 if self.is_open():
73 76
                     self.transactions.append(self.parse_line(line).to_gc())
74 77
 
@@ -82,32 +85,33 @@ class BaseTransactionParser(object):
82 85
 class MbankTransaction(BaseTransaction):
83 86
     """Item in an Mbank CSV"""
84 87
 
85
-    # Datum uskutečnění transakce
86
-    # Datum zaúčtování transakce
87
-    # Popis transakce
88
-    # Zpráva pro příjemce
89
-    # Plátce/Příjemce
90
-    # Číslo účtu plátce/příjemce
91
-    # KS
92
-    # VS
93
-    # SS
94
-    # Částka transakce
95
-    # Účetní zůstatek po transakci
88
+    # (a) Datum uskutečnění transakce
89
+    # (b) Datum zaúčtování transakce
90
+    # (c) Popis transakce
91
+    # (d) Zpráva pro příjemce
92
+    # (e) Plátce/Příjemce
93
+    # (f) Číslo účtu plátce/příjemce
94
+    # (g) KS
95
+    # (h) VS
96
+    # (i) SS
97
+    # (j) Částka transakce
98
+    # (k) Účetní zůstatek po transakci
99
+    # (l) (empty column at the end)
96 100
 
97 101
     def __init__(self, line):
98 102
         fields = [self._cleanup_field(f) for f in line.split(";")]
99
-        self.date_r = self._convert_date(fields.pop(0))
100
-        self.date_b = self._convert_date(fields.pop(0))
101
-        __ = fields.pop()
102
-        self.new_balance = self._parse_currency(fields.pop())
103
-        amount = self._parse_currency(fields.pop())
103
+        self.date_r = self._convert_date(fields.pop(0))         # (a)
104
+        self.date_b = self._convert_date(fields.pop(0))         # (b)
105
+        __ = fields.pop()                                       # (l)
106
+        self.new_balance = self._parse_currency(fields.pop())   # (k)
107
+        amount = self._parse_currency(fields.pop())             # (j)
104 108
         if amount >= 0:
105 109
             self.amountd = amount
106 110
             self.amountw = 0
107 111
         else:
108 112
             self.amountd = 0
109 113
             self.amountw = -amount
110
-        self._scrap = fields
114
+        self._scrap = fields                # (c-i)
111 115
 
112 116
     def _cleanup_field(self, field):
113 117
         x = ' '.join(_unquote(field).strip().split())
@@ -118,12 +122,36 @@ class MbankTransaction(BaseTransaction):
118 122
         return '-'.join([year, month, day])
119 123
 
120 124
     def _description(self):
125
+        def type_interesting(T):
126
+            boring_types = [
127
+                u'PŘÍCHOZÍ PLATBA Z MBANK',
128
+                u'ODCHOZÍ PLATBA DO MBANK',
129
+                u'PŘÍCHOZÍ PLATBA Z JINÉ BANKY',
130
+                u'ODCHOZÍ PLATBA DO JINÉ BANKY',
131
+                u'PLATBA KARTOU',
132
+            ]
133
+            if not T:
134
+                return False
135
+            if T in boring_types:
136
+                return False
137
+            return True
138
+        def message_interesting(M):
139
+            boring_messages = [
140
+                u'PŘEVOD PROSTŘEDKŮ',
141
+            ]
142
+            if not M:
143
+                return False
144
+            if M in boring_messages:
145
+                return False
146
+            return True
121 147
         type_, message, party, number, ks, vs, ss = self._scrap
122
-        message = message.replace(u' DATUM PROVEDENÍ TRANSAKCE: ', '; DATUM: ')
148
+        dpt = u' DATUM PROVEDENÍ TRANSAKCE: '
149
+        if dpt in message:
150
+            message, __ = message.split(dpt)
123 151
         out = []
124
-        if type_:
152
+        if type_interesting(type_):
125 153
             out.append(type_)
126
-        if message:
154
+        if message_interesting(message):
127 155
             out.append(message)
128 156
         if party:
129 157
             out.append(party)
@@ -150,16 +178,14 @@ class MbankTransactionParser(BaseTransactionParser):
150 178
     def parse_line(self, line):
151 179
         return MbankTransaction(line)
152 180
 
153
-    def pick_state(self, line, lineno=None):
181
+    def advance(self, line, lineno=None):
154 182
         """Choose parser state according to current line and line no."""
155
-        if self.next_state:
156
-            self.next_state = False
157
-            return True
183
+        if self.state.nxt:
184
+            return ParserState(ths=True, nxt=False)
158 185
         if self.is_closed() and line.startswith(u'#Datum uskute'):
159
-            self.next_state = True
160
-            return False
186
+            return ParserState(ths=False, nxt=True)
161 187
         if self.is_open() and not line:
162
-            return False
188
+            return ParserState(ths=False, nxt=False)
163 189
         return self.state
164 190
 
165 191
 
@@ -249,18 +275,121 @@ class FioTransactionParser(BaseTransactionParser):
249 275
     def parse_line(self, line):
250 276
         return FioTransaction(line)
251 277
 
252
-    def pick_state(self, line, lineno=None):
278
+    def advance(self, line, lineno=None):
253 279
         """Choose parser state according to current line and line no."""
254
-        if self.next_state:
255
-            self.next_state = False
256
-            return True
257
-        if self.is_closed() and line.startswith(u'"ID operace";"Datum";'):
258
-            self.next_state = True
259
-            return False
280
+        if self.state.nxt:
281
+            return ParserState(ths=True, nxt=False)
282
+        if self.is_open() and line.startswith(u'"ID operace";"Datum";'):
283
+            return ParserState(ths=False, nxt=True)
284
+        return self.state
285
+
286
+
287
+class RaifTransaction(BaseTransaction):
288
+
289
+    # (a) Transaction Date
290
+    # (b) Booking Date
291
+    # (c) Account number
292
+    # (d) Account Name
293
+    # (e) Transaction Category
294
+    # (f) Accocunt Number [SIC]
295
+    # (g) Name of Account
296
+    # (h) Transaction type
297
+    # (i) Message
298
+    # (j) Note
299
+    # (k) VS
300
+    # (l) KS
301
+    # (m) SS
302
+    # (n) Booked amount
303
+    # (o) Account Currency
304
+    # (p) Original Amount and Currency
305
+    # (q) Original Amount and Currency
306
+    # (r) Fee
307
+    # (s) Transaction ID
308
+
309
+    def __init__(self, line):
310
+        self._fields = [self._cleanup_field(f) for f in line.split(";")]
311
+        self.date = self._convert_date(self._fields[0])
312
+        amount = self._parse_currency(self._fields[13])
313
+        if amount >= 0:
314
+            self.amountd = amount
315
+            self.amountw = 0
316
+        else:
317
+            self.amountd = 0
318
+            self.amountw = -amount
319
+        self.currency = self._fields[14]
320
+        assert self.currency == 'CZK'
321
+
322
+    def _cleanup_field(self, field):
323
+        x = ' '.join(_unquote(field).strip().split())
324
+        return '' if x == '-' else x
325
+
326
+    def _description(self):
327
+        (
328
+            transaction_date,
329
+            booking_date,
330
+            src_account_number,
331
+            src_account_name,
332
+            transaction_category,
333
+            dst_account_number,
334
+            dst_account_name,
335
+            dst_ransaction_type,
336
+            message,
337
+            note,
338
+            vs,
339
+            ks,
340
+            ss,
341
+            booked_amount,
342
+            account_currency,
343
+            original_amount_and_currency,
344
+            original_amount_and_currency,
345
+            fee,
346
+            transaction_id,
347
+            ) = self._fields
348
+        out = []
349
+        if message:
350
+            out.append(message)
351
+        if note and note != message:
352
+            out.append(note)
353
+        if dst_account_name:
354
+            out.append(dst_account_name)
355
+        else:
356
+            out.append(dst_account_number)
357
+        out.append("VS:%s" % vs)
358
+        return " / ".join(out)
359
+
360
+    def _convert_date(self, text):
361
+        day, month, year = text.split('.')
362
+        return '-'.join([year, month, day])
363
+
364
+    def _parse_currency(self, text):
365
+        return float(text.replace(',', '.').replace(' ', ''))
366
+
367
+    def to_gc(self):
368
+        """Convert to GCTransaction"""
369
+        return GCTransaction(
370
+            date=self.date,
371
+            deposit=self.amountd,
372
+            withdrawal=self.amountw,
373
+            description=self._description()
374
+        )
375
+
376
+
377
+class RaifTransactionParser(BaseTransactionParser):
378
+
379
+    def parse_line(self, line):
380
+        return RaifTransaction(line)
381
+
382
+    def advance(self, line, lineno=None):
383
+        """Choose parser state according to current line and line no."""
384
+        if self.state.nxt:
385
+            return ParserState(ths=True, nxt=False)
386
+        if self.is_closed() and line.startswith(u'Transaction Date;Booking'):
387
+            return ParserState(ths=False, nxt=True)
260 388
         return self.state
261 389
 
262 390
 
263 391
 _parsers = {
264 392
     'CzMbank': MbankTransactionParser,
265 393
     'CzFio': FioTransactionParser,
394
+    'CzRaif': RaifTransactionParser,
266 395
 }