Browse Source

Add first working version

Alois Mahdal 5 years ago
parent
commit
21ee621fec
3 changed files with 328 additions and 2 deletions
  1. 2
    2
      README.md
  2. 58
    0
      gcconv
  3. 268
    0
      gcconv.py

+ 2
- 2
README.md View File

@@ -1,5 +1,5 @@
1
-GCIMPORT
2
-========
1
+GCCONV
2
+======
3 3
 
4 4
 GnuCash import helper.
5 5
 

+ 58
- 0
gcconv View File

@@ -0,0 +1,58 @@
1
+#!/usr/bin/python
2
+# coding=utf-8
3
+"""Convert bank statement CSV to GnuCash-importable CSV
4
+
5
+usage: gcconv [options] <format> <file>
6
+       gcconv --list
7
+
8
+Options:
9
+    -e, --encoding ENCODING     input file encoding as understood by Python [default: utf-8]
10
+    -l, --list                  list known formats
11
+    --help                      show this screen
12
+    --version                   show version
13
+"""
14
+
15
+__VERSION__ = "gcconv 0.0.1"
16
+
17
+import sys
18
+
19
+import docopt
20
+
21
+import gcconv
22
+
23
+class App(object):
24
+    """
25
+    Main application
26
+    """
27
+
28
+    class UsageError(ValueError):
29
+        """Usage error"""
30
+        pass
31
+
32
+    def __init__(self, args):
33
+        self.args = args
34
+
35
+    @classmethod
36
+    def main(cls, args):
37
+        """
38
+        Main entry point
39
+        """
40
+        app = cls(args)
41
+        app.run()
42
+
43
+    def run(self):
44
+        """
45
+        Run the app (main entry point)
46
+        """
47
+        if self.args['--list']:
48
+            print "\n".join(gcconv.valid_formats())
49
+            sys.exit(0)
50
+        print gcconv.convert_file(
51
+            self.args['<file>'],
52
+            self.args['<format>'],
53
+            encoding=self.args['--encoding']
54
+        )
55
+
56
+
57
+if __name__ == "__main__":
58
+    App.main(docopt.docopt(__doc__, version=__VERSION__))

+ 268
- 0
gcconv.py View File

@@ -0,0 +1,268 @@
1
+# coding=utf-8
2
+"""Convert Bank statement CSV to Gnucash-acceptable format"""
3
+
4
+
5
+def valid_formats():
6
+    return _parsers.keys()
7
+
8
+
9
+def convert_file(fpath, fmt, encoding='utf-8'):
10
+    try:
11
+        parser_class = _parsers[fmt]
12
+    except KeyError:
13
+        raise ValueError('unsupported format: %s' % fmt)
14
+    else:
15
+        parser = parser_class()
16
+        parser.parse_file(fpath, encoding)
17
+        return parser.format_csv()
18
+
19
+
20
+def _unquote(value):
21
+    if len(value) < 2:
22
+        return value
23
+    if value[0] == value[-1] == "'":
24
+        return value[1:-1]
25
+    if value[0] == value[-1] == '"':
26
+        return value[1:-1]
27
+    return value
28
+
29
+
30
+class BaseTransaction(object):
31
+    """A CSV-imported transaction"""
32
+    pass
33
+
34
+
35
+class GCTransaction(object):
36
+    """A Gnucash-acceptable transaction"""
37
+
38
+    def __init__(self, date, deposit, withdrawal, description):
39
+        self.date = date
40
+        self.deposit = deposit
41
+        self.withdrawal = withdrawal
42
+        self.description = description
43
+
44
+    def __str__(self):
45
+        """Naive CSV line printer"""
46
+        values = [self.date, self.deposit, self.withdrawal, self.description]
47
+        return ";".join(['"%s"' % v for v in values])
48
+
49
+
50
+class BaseTransactionParser(object):
51
+    """A statement parser"""
52
+    name = "__NONE__"
53
+
54
+    def __init__(self):
55
+        self.transactions = []
56
+        self.state = False
57
+        self.next_state = False
58
+
59
+    def is_open(self):
60
+        return self.state
61
+
62
+    def is_closed(self):
63
+        return not self.state
64
+
65
+    def parse_file(self, fpath, encoding='utf-8'):
66
+        lines_read = 0
67
+        with open(fpath) as f:
68
+            for line in f.readlines():
69
+                line = line.decode(encoding).strip()
70
+                lines_read += 1
71
+                self.state = self.pick_state(line, lineno=lines_read)
72
+                if self.is_open():
73
+                    self.transactions.append(self.parse_line(line).to_gc())
74
+
75
+    def format_csv(self):
76
+        return "\n".join([
77
+            unicode(t).encode('utf-8')
78
+            for t in self.transactions
79
+        ])
80
+
81
+
82
+class MbankTransaction(BaseTransaction):
83
+    """Item in an Mbank CSV"""
84
+
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
96
+
97
+    def __init__(self, line):
98
+        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())
104
+        if amount >= 0:
105
+            self.amountd = amount
106
+            self.amountw = 0
107
+        else:
108
+            self.amountd = 0
109
+            self.amountw = -amount
110
+        self._scrap = fields
111
+
112
+    def _cleanup_field(self, field):
113
+        x = ' '.join(_unquote(field).strip().split())
114
+        return '' if x == '-' else x
115
+
116
+    def _convert_date(self, text):
117
+        day, month, year = text.split('-')
118
+        return '-'.join([year, month, day])
119
+
120
+    def _description(self):
121
+        type_, message, party, number, ks, vs, ss = self._scrap
122
+        message = message.replace(u' DATUM PROVEDENÍ TRANSAKCE: ', '; DATUM: ')
123
+        out = []
124
+        if type_:
125
+            out.append(type_)
126
+        if message:
127
+            out.append(message)
128
+        if party:
129
+            out.append(party)
130
+        return " / ".join(out)
131
+
132
+    def _parse_currency(self, text):
133
+        """Read Mbank currency format"""
134
+        num = text.replace(' ', '')
135
+        num = num.replace(',', '.')
136
+        return float(num)
137
+
138
+    def to_gc(self):
139
+        """Convert to GCTransaction"""
140
+        return GCTransaction(
141
+            date=self.date_r,
142
+            deposit=self.amountd,
143
+            withdrawal=self.amountw,
144
+            description=self._description()
145
+        )
146
+
147
+
148
+class MbankTransactionParser(BaseTransactionParser):
149
+
150
+    def parse_line(self, line):
151
+        return MbankTransaction(line)
152
+
153
+    def pick_state(self, line, lineno=None):
154
+        """Choose parser state according to current line and line no."""
155
+        if self.next_state:
156
+            self.next_state = False
157
+            return True
158
+        if self.is_closed() and line.startswith(u'#Datum uskute'):
159
+            self.next_state = True
160
+            return False
161
+        if self.is_open() and not line:
162
+            return False
163
+        return self.state
164
+
165
+
166
+class FioTransaction(BaseTransaction):
167
+
168
+    # ID operace
169
+    # Datum
170
+    # Objem
171
+    # Měna
172
+    # Protiúčet
173
+    # Název protiúčtu
174
+    # Kód banky
175
+    # Název banky
176
+    # KS
177
+    # VS
178
+    # SS
179
+    # Poznámka
180
+    # Zpráva pro příjemce
181
+    # Typ
182
+    # Provedl
183
+    # Upřesnění
184
+    # Poznámka
185
+    # BIC
186
+    # ID pokynu
187
+
188
+    def __init__(self, line):
189
+        fields = [self._cleanup_field(f) for f in line.split(";")]
190
+        __ = fields.pop(0)
191
+        self.date = self._convert_date(fields.pop(0))
192
+        amount = self._parse_currency(fields.pop(0))
193
+        if amount >= 0:
194
+            self.amountd = amount
195
+            self.amountw = 0
196
+        else:
197
+            self.amountd = 0
198
+            self.amountw = -amount
199
+        self.currency = fields.pop(0)
200
+        assert self.currency == 'CZK'
201
+        self._scrap = [self._cleanup_field(f) for f in fields]
202
+
203
+    def _cleanup_field(self, field):
204
+        x = ' '.join(_unquote(field).strip().split())
205
+        return '' if x == '-' else x
206
+
207
+    def _description(self):
208
+        (
209
+            party_acc,
210
+            party_accnname,
211
+            party_bankcode,
212
+            party_bankname,
213
+            ks,
214
+            vs,
215
+            ss,
216
+            note1,
217
+            message,
218
+            type_,
219
+            who,
220
+            explanation,
221
+            note2,
222
+            bic,
223
+            comm_id
224
+            ) = self._scrap
225
+        out = []
226
+        if message:
227
+            out.append(message)
228
+        return " / ".join(out)
229
+
230
+    def _convert_date(self, text):
231
+        day, month, year = text.split('.')
232
+        return '-'.join([year, month, day])
233
+
234
+    def _parse_currency(self, text):
235
+        return float(text)
236
+
237
+    def to_gc(self):
238
+        """Convert to GCTransaction"""
239
+        return GCTransaction(
240
+            date=self.date,
241
+            deposit=self.amountd,
242
+            withdrawal=self.amountw,
243
+            description=self._description()
244
+        )
245
+
246
+
247
+class FioTransactionParser(BaseTransactionParser):
248
+
249
+    def parse_line(self, line):
250
+        return FioTransaction(line)
251
+
252
+    def pick_state(self, line, lineno=None):
253
+        """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
260
+        if self.is_open() and line.startswith(u'"bankId";"2010"'):
261
+            return False
262
+        return self.state
263
+
264
+
265
+_parsers = {
266
+    'CzMbank': MbankTransactionParser,
267
+    'CzFio': FioTransactionParser,
268
+}