Changeset View
Changeset View
Standalone View
Standalone View
plugins/import/skrooge_import_backend/skrooge-sabb.py
Show All 19 Lines | |||||
20 | Skrooge AqBanking Bridge (SABB) | 20 | Skrooge AqBanking Bridge (SABB) | ||
21 | ------------------------------- | 21 | ------------------------------- | ||
22 | 22 | | |||
23 | Authors: | 23 | Authors: | ||
24 | * Bernhard Scheirle <bernhard@scheirle.de> | 24 | * Bernhard Scheirle <bernhard@scheirle.de> | ||
25 | 25 | | |||
26 | Changelog: | 26 | Changelog: | ||
27 | 27 | | |||
28 | 1.2.0 - 2019.04.28 | ||||
29 | * Allow processing of accounts without an IBAN | ||||
30 | (e.g credit card accounts). | ||||
31 | In this case a fake IBAN is used: | ||||
32 | XX00<bank_number><account_number> | ||||
33 | | ||||
28 | 1.1.0 - 2018.05.21 | 34 | 1.1.0 - 2018.05.21 | ||
29 | * Added command line parameter --terminal-emulator | 35 | * Added command line parameter --terminal-emulator | ||
30 | 36 | | |||
31 | 1.0.0 - 2017.07.29 | 37 | 1.0.0 - 2017.07.29 | ||
32 | * Initial release | 38 | * Initial release | ||
33 | 39 | | |||
34 | """ | 40 | """ | ||
35 | 41 | | |||
36 | import argparse | 42 | import argparse | ||
37 | import contextlib | 43 | import contextlib | ||
38 | import csv | 44 | import csv | ||
39 | import datetime | 45 | import datetime | ||
40 | import io | 46 | import io | ||
41 | import os | 47 | import os | ||
42 | import re | 48 | import re | ||
43 | import shutil | 49 | import shutil | ||
44 | import subprocess | 50 | import subprocess | ||
45 | import sys | 51 | import sys | ||
46 | import tempfile | 52 | import tempfile | ||
47 | from distutils.version import LooseVersion | 53 | from distutils.version import LooseVersion | ||
48 | 54 | | |||
49 | __VERSION__ = "1.1.0" | 55 | __VERSION__ = "1.2.0" | ||
56 | | ||||
50 | 57 | | |||
51 | class Account(object): | 58 | class Account(object): | ||
52 | def __init__(self): | 59 | def __init__(self): | ||
53 | self.bank_number = "" | 60 | self.bank_number = "" | ||
54 | self.account_number = "" | 61 | self.account_number = "" | ||
55 | self.iban = "" | 62 | self.iban = "" | ||
63 | self.fake_iban = False | ||||
56 | 64 | | |||
57 | def toString(self): | 65 | def toString(self): | ||
58 | return 'Account(' + self.bank_number + ', ' + self.account_number + ', ' + self.iban + ')' | 66 | return 'Account({}, {}, {})'.format(self.bank_number, | ||
67 | self.account_number, | ||||
68 | self.iban) | ||||
59 | 69 | | |||
60 | def isValid(self): | 70 | def isValid(self): | ||
61 | return self.bank_number != "" and self.account_number != "" and self.iban != "" | 71 | return self.bank_number != "" \ | ||
72 | and self.account_number != "" \ | ||||
73 | and self.iban != "" | ||||
74 | | ||||
75 | @staticmethod | ||||
76 | def _parse_single(file_content, regex): | ||||
77 | match = regex.search(file_content) | ||||
78 | if match: | ||||
79 | return match.group(1) | ||||
80 | return "" | ||||
81 | | ||||
82 | def parse(self, file_content): | ||||
83 | regex_account_number = re.compile('accountNumber=\"(.*)\"') | ||||
84 | regex_bank_number = re.compile('bankCode=\"(.*)\"') | ||||
85 | regex_iban = re.compile('iban=\"(.*)\"') | ||||
86 | | ||||
87 | self.account_number = self._parse_single(file_content, regex_account_number) | ||||
88 | self.bank_number = self._parse_single(file_content, regex_bank_number) | ||||
89 | self.iban = self._parse_single(file_content, regex_iban) | ||||
90 | | ||||
91 | if self.iban == "": | ||||
92 | self.fake_iban = True | ||||
93 | self.iban = "XX00{}{}".format(self.bank_number, self.account_number) | ||||
94 | | ||||
95 | def matches(self, bank_number, account_number): | ||||
96 | return self.bank_number == bank_number \ | ||||
97 | and self.account_number == account_number | ||||
62 | 98 | | |||
63 | 99 | | |||
64 | class Accounts(object): | 100 | class Accounts(object): | ||
65 | def __init__(self): | 101 | def __init__(self): | ||
66 | self._account_map = {} | 102 | self._account_map = {} | ||
67 | 103 | | |||
68 | def account_map_key(self, bank_number, account_number): | 104 | def _account_map_key(self, bank_number, account_number): | ||
69 | return str(bank_number) + '.' + str(account_number) | 105 | return str(bank_number) + '.' + str(account_number) | ||
70 | 106 | | |||
107 | def _update_account_map(self, account): | ||||
108 | if not account.isValid(): | ||||
109 | return | ||||
110 | | ||||
111 | overwrite = True | ||||
112 | key = self._account_map_key(account.bank_number, account.account_number) | ||||
113 | if key in self._account_map: | ||||
114 | # Only overwrite the IBAN if the currently stored one is fake: | ||||
115 | overwrite = self._account_map[key].fake_iban | ||||
116 | | ||||
117 | if overwrite: | ||||
118 | self._account_map[key] = account | ||||
119 | | ||||
71 | def _build_iban_map(self): | 120 | def _build_iban_map(self): | ||
72 | self._account_map = {} | 121 | self._account_map = {} | ||
73 | 122 | | |||
74 | regex_account_number = re.compile('accountNumber=\"(.*)\"') | | |||
75 | regex_bank_number = re.compile('bankCode=\"(.*)\"') | | |||
76 | regex_iban = re.compile('iban=\"(.*)\"') | | |||
77 | #regex_bic = re.compile('bic=\"(.*)\"') | | |||
78 | | ||||
79 | accounts_folder = os.path.expanduser('~/.aqbanking/settings/accounts/') | 123 | accounts_folder = os.path.expanduser('~/.aqbanking/settings/accounts/') | ||
80 | for file_name in os.listdir(accounts_folder): | 124 | for file_name in os.listdir(accounts_folder): | ||
81 | if not file_name.endswith('.conf'): | 125 | if not file_name.endswith('.conf'): | ||
82 | continue | 126 | continue | ||
83 | file_path = os.path.join(accounts_folder, file_name) | 127 | file_path = os.path.join(accounts_folder, file_name) | ||
84 | with open(file_path, 'r') as account_file: | 128 | with open(file_path, 'r') as account_file: | ||
85 | content = account_file.read() | 129 | content = account_file.read() | ||
86 | account = Account() | 130 | account = Account() | ||
87 | match = regex_account_number.search(content) | 131 | account.parse(content) | ||
88 | if match: | 132 | self._update_account_map(account) | ||
89 | account.account_number = match.group(1) | | |||
90 | match = regex_bank_number.search(content) | | |||
91 | if match: | | |||
92 | account.bank_number = match.group(1) | | |||
93 | match = regex_iban.search(content) | | |||
94 | if match: | | |||
95 | account.iban = match.group(1) | | |||
96 | # match = regex_bic.search(content) | | |||
97 | # if match: | | |||
98 | # account.bic = match.group(1) | | |||
99 | # print(account.toString()) | | |||
100 | if account.isValid(): | | |||
101 | key = self.account_map_key(account.bank_number, account.account_number) | | |||
102 | self._account_map[key] = account | | |||
103 | 133 | | |||
104 | def get_account(self, bank_number, account_number): | 134 | def get_account(self, bank_number, account_number): | ||
105 | if not self._account_map: | 135 | if not self._account_map: | ||
106 | self._build_iban_map() | 136 | self._build_iban_map() | ||
107 | 137 | | |||
108 | key = self.account_map_key(bank_number, account_number) | 138 | key = self._account_map_key(bank_number, account_number) | ||
109 | if key in self._account_map: | 139 | if key in self._account_map: | ||
110 | return self._account_map[key] | 140 | return self._account_map[key] | ||
111 | else: | 141 | else: | ||
112 | return Account() | 142 | return Account() | ||
113 | 143 | | |||
114 | def get_accounts(self): | 144 | def get_accounts(self): | ||
115 | if not self._account_map: | 145 | if not self._account_map: | ||
116 | self._build_iban_map() | 146 | self._build_iban_map() | ||
▲ Show 20 Lines • Show All 151 Lines • ▼ Show 20 Line(s) | 269 | def write_balance_file(self, output_folder_path, context_file_path): | |||
268 | output_file_path = os.path.join(output_folder_path, "balance.csv") | 298 | output_file_path = os.path.join(output_folder_path, "balance.csv") | ||
269 | with open(output_file_path, 'w') as f: | 299 | with open(output_file_path, 'w') as f: | ||
270 | f.write(output.getvalue()) | 300 | f.write(output.getvalue()) | ||
271 | 301 | | |||
272 | def convert_transactions(self, aqbanking_output, generateHeader=True): | 302 | def convert_transactions(self, aqbanking_output, generateHeader=True): | ||
273 | reader = csv.DictReader(aqbanking_output.splitlines(), dialect=AqDialectTransfairs) | 303 | reader = csv.DictReader(aqbanking_output.splitlines(), dialect=AqDialectTransfairs) | ||
274 | fieldnames_output = ['date', 'mode', 'comment', 'payee', 'amount', 'unit'] | 304 | fieldnames_output = ['date', 'mode', 'comment', 'payee', 'amount', 'unit'] | ||
275 | output, writer = self.get_csv_writer(fieldnames_output, generateHeader) | 305 | output, writer = self.get_csv_writer(fieldnames_output, generateHeader) | ||
276 | for record in reader: | 306 | for record in sorted(reader, key=lambda row: row['date']): | ||
277 | row = {} | 307 | row = {} | ||
278 | row['date'] = record['date'] | 308 | row['date'] = record['date'] | ||
279 | row['mode'] = record['purpose'] | 309 | row['mode'] = record['purpose'] | ||
280 | comment = '' | 310 | comment = '' | ||
281 | for i in range(1, 12): | 311 | for i in range(1, 12): | ||
282 | comment = comment + record['purpose' + str(i)] + " " | 312 | comment = comment + record['purpose' + str(i)] + " " | ||
283 | row['comment'] = comment.strip() | 313 | row['comment'] = comment.strip() | ||
284 | row['payee'] = (record['remoteName'] + " " + record['remoteName1']).strip() | 314 | row['payee'] = (record['remoteName'] + " " + record['remoteName1']).strip() | ||
▲ Show 20 Lines • Show All 91 Lines • Show Last 20 Lines |