tariochbctools 0.38.1__py2.py3-none-any.whl → 1.0.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tariochbctools/importers/bcge/importer.py +4 -3
- tariochbctools/importers/bitst/importer.py +28 -19
- tariochbctools/importers/blockchain/importer.py +10 -9
- tariochbctools/importers/cembrastatement/importer.py +44 -28
- tariochbctools/importers/general/mailAdapterImporter.py +12 -13
- tariochbctools/importers/general/mt940importer.py +18 -19
- tariochbctools/importers/general/priceLookup.py +6 -6
- tariochbctools/importers/ibkr/importer.py +22 -19
- tariochbctools/importers/neon/importer.py +17 -15
- tariochbctools/importers/netbenefits/importer.py +24 -16
- tariochbctools/importers/nordigen/importer.py +7 -7
- tariochbctools/importers/nordigen/nordigen_config.py +12 -11
- tariochbctools/importers/postfinance/importer.py +20 -17
- tariochbctools/importers/quickfile/importer.py +11 -11
- tariochbctools/importers/raiffeisench/importer.py +3 -7
- tariochbctools/importers/revolut/importer.py +23 -18
- tariochbctools/importers/schedule/importer.py +11 -8
- tariochbctools/importers/swisscard/importer.py +17 -15
- tariochbctools/importers/transferwise/importer.py +15 -14
- tariochbctools/importers/truelayer/importer.py +25 -16
- tariochbctools/importers/viseca/importer.py +24 -16
- tariochbctools/importers/zak/importer.py +51 -44
- tariochbctools/importers/zkb/importer.py +3 -2
- tariochbctools/plugins/prices/ibkr.py +6 -3
- {tariochbctools-0.38.1.dist-info → tariochbctools-1.0.1.dist-info}/METADATA +6 -3
- tariochbctools-1.0.1.dist-info/RECORD +55 -0
- {tariochbctools-0.38.1.dist-info → tariochbctools-1.0.1.dist-info}/WHEEL +1 -1
- tariochbctools-0.38.1.dist-info/RECORD +0 -55
- {tariochbctools-0.38.1.dist-info → tariochbctools-1.0.1.dist-info}/LICENSE.txt +0 -0
- {tariochbctools-0.38.1.dist-info → tariochbctools-1.0.1.dist-info}/entry_points.txt +0 -0
- {tariochbctools-0.38.1.dist-info → tariochbctools-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,15 @@
|
|
1
1
|
import re
|
2
|
+
from typing import Any
|
2
3
|
|
3
4
|
from tariochbctools.importers.general import mt940importer
|
4
5
|
|
5
6
|
|
6
|
-
def strip_newline(string):
|
7
|
+
def strip_newline(string: str) -> str:
|
7
8
|
return string.replace("\n", "").replace("\r", "")
|
8
9
|
|
9
10
|
|
10
11
|
class BCGEImporter(mt940importer.Importer):
|
11
|
-
def prepare_payee(self, trxdata):
|
12
|
+
def prepare_payee(self, trxdata: dict[str, Any]) -> str:
|
12
13
|
transaction_details = strip_newline(trxdata["transaction_details"])
|
13
14
|
payee = re.search(r"ORDP/([^/]+)", transaction_details)
|
14
15
|
if payee is None:
|
@@ -16,7 +17,7 @@ class BCGEImporter(mt940importer.Importer):
|
|
16
17
|
else:
|
17
18
|
return payee.group(1)
|
18
19
|
|
19
|
-
def prepare_narration(self, trxdata):
|
20
|
+
def prepare_narration(self, trxdata: dict[str, Any]) -> str:
|
20
21
|
transaction_details = strip_newline(trxdata["transaction_details"])
|
21
22
|
extra_details = strip_newline(trxdata["extra_details"])
|
22
23
|
beneficiary = re.search(r"/BENM/([^/]+)", transaction_details)
|
@@ -1,36 +1,38 @@
|
|
1
1
|
from datetime import date
|
2
2
|
from os import path
|
3
|
+
from typing import Any
|
3
4
|
|
5
|
+
import beangulp
|
4
6
|
import bitstamp.client
|
5
7
|
import yaml
|
6
8
|
from beancount.core import amount, data
|
7
9
|
from beancount.core.number import MISSING, D
|
8
|
-
from beancount.ingest import importer
|
9
10
|
from dateutil.parser import parse
|
10
11
|
from dateutil.relativedelta import relativedelta
|
11
12
|
|
12
13
|
from tariochbctools.importers.general.priceLookup import PriceLookup
|
13
14
|
|
14
15
|
|
15
|
-
class Importer(
|
16
|
+
class Importer(beangulp.Importer):
|
16
17
|
"""An importer for Bitstamp."""
|
17
18
|
|
18
|
-
def identify(self,
|
19
|
-
return path.basename(
|
19
|
+
def identify(self, filepath: str) -> bool:
|
20
|
+
return path.basename(filepath).endswith("bitstamp.yaml")
|
20
21
|
|
21
|
-
def
|
22
|
+
def account(self, filepath: str) -> data.Account:
|
22
23
|
return ""
|
23
24
|
|
24
|
-
def extract(self,
|
25
|
-
self.priceLookup = PriceLookup(
|
25
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
26
|
+
self.priceLookup = PriceLookup(existing, "CHF")
|
26
27
|
|
27
|
-
|
28
|
+
with open(filepath) as file:
|
29
|
+
config = yaml.safe_load(file)
|
28
30
|
self.config = config
|
29
31
|
self.client = bitstamp.client.Trading(
|
30
32
|
username=config["username"], key=config["key"], secret=config["secret"]
|
31
33
|
)
|
32
34
|
self.currencies = config["currencies"]
|
33
|
-
self.
|
35
|
+
self._account = config["account"]
|
34
36
|
self.otherExpensesAccount = config["otherExpensesAccount"]
|
35
37
|
self.capGainAccount = config["capGainAccount"]
|
36
38
|
|
@@ -46,7 +48,7 @@ class Importer(importer.ImporterProtocol):
|
|
46
48
|
|
47
49
|
return result
|
48
50
|
|
49
|
-
def fetchSingle(self, trx):
|
51
|
+
def fetchSingle(self, trx: dict[str, Any]) -> data.Transaction:
|
50
52
|
id = int(trx["id"])
|
51
53
|
type = int(trx["type"])
|
52
54
|
date = parse(trx["datetime"]).date()
|
@@ -67,12 +69,18 @@ class Importer(importer.ImporterProtocol):
|
|
67
69
|
|
68
70
|
if type == 0:
|
69
71
|
narration = "Deposit"
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
if posCcy:
|
73
|
+
cost = data.CostSpec(
|
74
|
+
self.priceLookup.fetchPriceAmount(posCcy, date),
|
75
|
+
None,
|
76
|
+
"CHF",
|
77
|
+
None,
|
78
|
+
None,
|
79
|
+
False,
|
80
|
+
)
|
73
81
|
postings = [
|
74
82
|
data.Posting(
|
75
|
-
self.
|
83
|
+
self._account + ":" + posCcy,
|
76
84
|
amount.Amount(posAmt, posCcy),
|
77
85
|
cost,
|
78
86
|
None,
|
@@ -84,7 +92,7 @@ class Importer(importer.ImporterProtocol):
|
|
84
92
|
narration = "Withdrawal"
|
85
93
|
postings = [
|
86
94
|
data.Posting(
|
87
|
-
self.
|
95
|
+
self._account + ":" + negCcy,
|
88
96
|
amount.Amount(negAmt, negCcy),
|
89
97
|
None,
|
90
98
|
None,
|
@@ -94,14 +102,15 @@ class Importer(importer.ImporterProtocol):
|
|
94
102
|
]
|
95
103
|
elif type == 2:
|
96
104
|
fee = D(trx["fee"])
|
97
|
-
if posCcy.lower() + "_" + negCcy.lower() in trx:
|
105
|
+
if posCcy and negCcy and posCcy.lower() + "_" + negCcy.lower() in trx:
|
98
106
|
feeCcy = negCcy
|
99
107
|
negAmt -= fee
|
100
108
|
else:
|
101
109
|
feeCcy = posCcy
|
102
110
|
posAmt -= fee
|
103
111
|
|
104
|
-
|
112
|
+
if feeCcy:
|
113
|
+
rateFiatCcy = self.priceLookup.fetchPriceAmount(feeCcy, date)
|
105
114
|
if feeCcy == posCcy:
|
106
115
|
posCcyCost = None
|
107
116
|
posCcyPrice = amount.Amount(rateFiatCcy, "CHF")
|
@@ -119,7 +128,7 @@ class Importer(importer.ImporterProtocol):
|
|
119
128
|
|
120
129
|
postings = [
|
121
130
|
data.Posting(
|
122
|
-
self.
|
131
|
+
self._account + ":" + posCcy,
|
123
132
|
amount.Amount(posAmt, posCcy),
|
124
133
|
posCcyCost,
|
125
134
|
posCcyPrice,
|
@@ -127,7 +136,7 @@ class Importer(importer.ImporterProtocol):
|
|
127
136
|
None,
|
128
137
|
),
|
129
138
|
data.Posting(
|
130
|
-
self.
|
139
|
+
self._account + ":" + negCcy,
|
131
140
|
amount.Amount(negAmt, negCcy),
|
132
141
|
negCcyCost,
|
133
142
|
negCcyPrice,
|
@@ -1,28 +1,29 @@
|
|
1
1
|
from os import path
|
2
2
|
|
3
|
+
import beangulp
|
3
4
|
import blockcypher
|
4
5
|
import yaml
|
5
6
|
from beancount.core import amount, data
|
6
7
|
from beancount.core.number import D
|
7
|
-
from beancount.ingest import importer
|
8
8
|
|
9
9
|
from tariochbctools.importers.general.priceLookup import PriceLookup
|
10
10
|
|
11
11
|
|
12
|
-
class Importer(
|
12
|
+
class Importer(beangulp.Importer):
|
13
13
|
"""An importer for Blockchain data."""
|
14
14
|
|
15
|
-
def identify(self,
|
16
|
-
return path.basename(
|
15
|
+
def identify(self, filepath: str) -> bool:
|
16
|
+
return path.basename(filepath).endswith("blockchain.yaml")
|
17
17
|
|
18
|
-
def
|
18
|
+
def account(self, filepath: str) -> data.Entries:
|
19
19
|
return ""
|
20
20
|
|
21
|
-
def extract(self,
|
22
|
-
|
21
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
22
|
+
with open(filepath) as file:
|
23
|
+
config = yaml.safe_load(file)
|
23
24
|
self.config = config
|
24
25
|
baseCcy = config["base_ccy"]
|
25
|
-
priceLookup = PriceLookup(
|
26
|
+
priceLookup = PriceLookup(existing, baseCcy)
|
26
27
|
|
27
28
|
entries = []
|
28
29
|
for address in self.config["addresses"]:
|
@@ -38,7 +39,7 @@ class Importer(importer.ImporterProtocol):
|
|
38
39
|
|
39
40
|
date = trx["confirmed"].date()
|
40
41
|
price = priceLookup.fetchPriceAmount(currency, date)
|
41
|
-
cost = data.
|
42
|
+
cost = data.CostSpec(price, None, baseCcy, None, None, None, False)
|
42
43
|
|
43
44
|
outputType = "ether" if currency.lower() == "eth" else "btc"
|
44
45
|
amt = blockcypher.from_base_unit(trx["value"], outputType)
|
@@ -1,26 +1,31 @@
|
|
1
|
+
import datetime
|
1
2
|
import re
|
2
|
-
from datetime import
|
3
|
+
from datetime import timedelta
|
3
4
|
|
5
|
+
import beangulp
|
4
6
|
import camelot
|
5
7
|
from beancount.core import amount, data
|
6
8
|
from beancount.core.number import D
|
7
|
-
from beancount.ingest import importer
|
8
|
-
from beancount.ingest.importers.mixins import identifier
|
9
9
|
|
10
10
|
|
11
|
-
class Importer(
|
11
|
+
class Importer(beangulp.Importer):
|
12
12
|
"""An importer for Cembra Card Statement PDF files."""
|
13
13
|
|
14
|
-
def __init__(self,
|
15
|
-
|
16
|
-
self.
|
14
|
+
def __init__(self, filepattern: str, account: data.Account):
|
15
|
+
self._filepattern = filepattern
|
16
|
+
self._account = account
|
17
17
|
self.currency = "CHF"
|
18
18
|
|
19
|
-
def
|
20
|
-
return self.
|
19
|
+
def identify(self, filepath: str) -> bool:
|
20
|
+
return re.search(self._filepattern, filepath) is not None
|
21
21
|
|
22
|
-
def
|
23
|
-
|
22
|
+
def account(self, filepath: str) -> data.Account:
|
23
|
+
return self._account
|
24
|
+
|
25
|
+
def createEntry(
|
26
|
+
self, filepath: str, date: datetime.date, amt: data.Decimal, text: str
|
27
|
+
) -> data.Transaction:
|
28
|
+
meta = data.new_metadata(filepath, 0)
|
24
29
|
return data.Transaction(
|
25
30
|
meta,
|
26
31
|
date,
|
@@ -30,19 +35,21 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
30
35
|
data.EMPTY_SET,
|
31
36
|
data.EMPTY_SET,
|
32
37
|
[
|
33
|
-
data.Posting(self.
|
38
|
+
data.Posting(self._account, amt, None, None, None, None),
|
34
39
|
],
|
35
40
|
)
|
36
41
|
|
37
|
-
def createBalanceEntry(
|
38
|
-
|
39
|
-
|
42
|
+
def createBalanceEntry(
|
43
|
+
self, filepath: str, date: datetime.date, amt: data.Decimal
|
44
|
+
) -> data.Balance:
|
45
|
+
meta = data.new_metadata(filepath, 0)
|
46
|
+
return data.Balance(meta, date, self._account, amt, None, None)
|
40
47
|
|
41
|
-
def extract(self,
|
48
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
42
49
|
entries = []
|
43
50
|
|
44
51
|
tables = camelot.read_pdf(
|
45
|
-
|
52
|
+
filepath, pages="2-end", flavor="stream", table_areas=["50,700,560,50"]
|
46
53
|
)
|
47
54
|
for table in tables:
|
48
55
|
df = table.df
|
@@ -63,7 +70,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
63
70
|
|
64
71
|
# Transaction entry
|
65
72
|
try:
|
66
|
-
book_date = datetime.strptime(book_date, "%d.%m.%Y").date()
|
73
|
+
book_date = datetime.datetime.strptime(book_date, "%d.%m.%Y").date()
|
67
74
|
except Exception:
|
68
75
|
book_date = None
|
69
76
|
|
@@ -71,17 +78,24 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
71
78
|
amount = self.getAmount(debit, credit)
|
72
79
|
|
73
80
|
if amount:
|
74
|
-
entries.append(
|
81
|
+
entries.append(
|
82
|
+
self.createEntry(filepath, book_date, amount, text)
|
83
|
+
)
|
75
84
|
continue
|
76
85
|
|
77
86
|
# Balance entry
|
78
87
|
try:
|
79
|
-
|
80
|
-
r"Saldo per (\d\d\.\d\d\.\d\d\d\d) zu unseren Gunsten CHF",
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
88
|
+
m = re.search(
|
89
|
+
r"Saldo per (\d\d\.\d\d\.\d\d\d\d) zu unseren Gunsten CHF",
|
90
|
+
text,
|
91
|
+
)
|
92
|
+
if m:
|
93
|
+
book_date = m.group(1)
|
94
|
+
book_date = datetime.datetime.strptime(
|
95
|
+
book_date, "%d.%m.%Y"
|
96
|
+
).date()
|
97
|
+
# add 1 day: cembra provides balance at EOD, but beancount checks it at SOD
|
98
|
+
book_date = book_date + timedelta(days=1)
|
85
99
|
except Exception:
|
86
100
|
book_date = None
|
87
101
|
|
@@ -89,14 +103,16 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
89
103
|
amount = self.getAmount(debit, credit)
|
90
104
|
|
91
105
|
if amount:
|
92
|
-
entries.append(
|
106
|
+
entries.append(
|
107
|
+
self.createBalanceEntry(filepath, book_date, amount)
|
108
|
+
)
|
93
109
|
|
94
110
|
return entries
|
95
111
|
|
96
|
-
def cleanDecimal(self, formattedNumber):
|
112
|
+
def cleanDecimal(self, formattedNumber: str) -> data.Decimal:
|
97
113
|
return D(formattedNumber.replace("'", ""))
|
98
114
|
|
99
|
-
def getAmount(self, debit, credit):
|
115
|
+
def getAmount(self, debit: str, credit: str) -> data.Amount:
|
100
116
|
amt = -self.cleanDecimal(debit) if debit else self.cleanDecimal(credit)
|
101
117
|
if amt:
|
102
118
|
return amount.Amount(amt, self.currency)
|
@@ -2,24 +2,26 @@ import tempfile
|
|
2
2
|
from os import path
|
3
3
|
|
4
4
|
import yaml
|
5
|
-
from beancount.
|
5
|
+
from beancount.core import data
|
6
|
+
from beangulp import Importer
|
6
7
|
from imap_tools import MailBox
|
7
8
|
|
8
9
|
|
9
|
-
class MailAdapterImporter(
|
10
|
+
class MailAdapterImporter(Importer):
|
10
11
|
"""An importer adapter that fetches file from mails and then calls another importer."""
|
11
12
|
|
12
|
-
def __init__(self, importers):
|
13
|
+
def __init__(self, importers: list[Importer]):
|
13
14
|
self.importers = importers
|
14
15
|
|
15
|
-
def identify(self,
|
16
|
-
return "mail.yaml" == path.basename(
|
16
|
+
def identify(self, filepath: str) -> bool:
|
17
|
+
return "mail.yaml" == path.basename(filepath)
|
17
18
|
|
18
|
-
def
|
19
|
+
def account(self, filepath: str) -> data.Account:
|
19
20
|
return ""
|
20
21
|
|
21
|
-
def extract(self,
|
22
|
-
|
22
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
23
|
+
with open(filepath) as file:
|
24
|
+
config = yaml.safe_load(file)
|
23
25
|
|
24
26
|
with MailBox(config["host"]).login(
|
25
27
|
config["user"], config["password"], initial_folder=config["folder"]
|
@@ -33,13 +35,10 @@ class MailAdapterImporter(importer.ImporterProtocol):
|
|
33
35
|
with open(attFileName, "wb") as attFile:
|
34
36
|
attFile.write(att.payload)
|
35
37
|
attFile.flush()
|
36
|
-
fileMemo = cache.get_file(attFileName)
|
37
38
|
|
38
39
|
for delegate in self.importers:
|
39
|
-
if delegate.identify(
|
40
|
-
newEntries = delegate.extract(
|
41
|
-
fileMemo, existing_entries
|
42
|
-
)
|
40
|
+
if delegate.identify(attFileName):
|
41
|
+
newEntries = delegate.extract(attFileName, existing)
|
43
42
|
result.extend(newEntries)
|
44
43
|
processed = True
|
45
44
|
|
@@ -1,29 +1,28 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
import beangulp
|
1
5
|
import mt940
|
2
6
|
from beancount.core import amount, data
|
3
7
|
from beancount.core.number import D
|
4
|
-
from beancount.ingest import importer
|
5
|
-
from beancount.ingest.importers.mixins import identifier
|
6
8
|
|
7
9
|
|
8
|
-
class Importer(
|
10
|
+
class Importer(beangulp.Importer):
|
9
11
|
"""An importer for MT940 files."""
|
10
12
|
|
11
|
-
def __init__(self,
|
12
|
-
|
13
|
-
self.
|
14
|
-
|
15
|
-
def identify(self, file):
|
16
|
-
if file.mimetype() != "text/plain":
|
17
|
-
return False
|
13
|
+
def __init__(self, filepattern: str, account: data.Account):
|
14
|
+
self._filepattern = filepattern
|
15
|
+
self._account = account
|
18
16
|
|
19
|
-
|
17
|
+
def identify(self, filepath: str) -> bool:
|
18
|
+
return re.search(self._filepattern, filepath) is not None
|
20
19
|
|
21
|
-
def
|
22
|
-
return self.
|
20
|
+
def account(self, filepath: str) -> data.Account:
|
21
|
+
return self._account
|
23
22
|
|
24
|
-
def extract(self,
|
23
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
25
24
|
entries = []
|
26
|
-
transactions = mt940.parse(
|
25
|
+
transactions = mt940.parse(filepath)
|
27
26
|
for trx in transactions:
|
28
27
|
trxdata = trx.data
|
29
28
|
ref = trxdata["bank_reference"]
|
@@ -31,7 +30,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
31
30
|
metakv = {"ref": ref}
|
32
31
|
else:
|
33
32
|
metakv = None
|
34
|
-
meta = data.new_metadata(
|
33
|
+
meta = data.new_metadata(filepath, 0, metakv)
|
35
34
|
if "entry_date" in trxdata:
|
36
35
|
date = trxdata["entry_date"]
|
37
36
|
else:
|
@@ -46,7 +45,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
46
45
|
data.EMPTY_SET,
|
47
46
|
[
|
48
47
|
data.Posting(
|
49
|
-
self.
|
48
|
+
self._account,
|
50
49
|
amount.Amount(
|
51
50
|
D(trxdata["amount"].amount), trxdata["amount"].currency
|
52
51
|
),
|
@@ -61,8 +60,8 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
61
60
|
|
62
61
|
return entries
|
63
62
|
|
64
|
-
def prepare_payee(self, trxdata):
|
63
|
+
def prepare_payee(self, trxdata: dict[str, Any]) -> str:
|
65
64
|
return ""
|
66
65
|
|
67
|
-
def prepare_narration(self, trxdata):
|
66
|
+
def prepare_narration(self, trxdata: dict[str, Any]) -> str:
|
68
67
|
return trxdata["transaction_details"] + " " + trxdata["extra_details"]
|
@@ -1,18 +1,18 @@
|
|
1
1
|
from datetime import date
|
2
2
|
|
3
|
-
from beancount.core import amount, prices
|
3
|
+
from beancount.core import amount, data, prices
|
4
4
|
from beancount.core.number import D
|
5
5
|
|
6
6
|
|
7
7
|
class PriceLookup:
|
8
|
-
def __init__(self,
|
9
|
-
if
|
10
|
-
self.priceMap = prices.build_price_map(
|
8
|
+
def __init__(self, existing: data.Entries, baseCcy: str):
|
9
|
+
if existing:
|
10
|
+
self.priceMap = prices.build_price_map(existing)
|
11
11
|
else:
|
12
12
|
self.priceMap = None
|
13
13
|
self.baseCcy = baseCcy
|
14
14
|
|
15
|
-
def fetchPriceAmount(self, instrument: str, date: date):
|
15
|
+
def fetchPriceAmount(self, instrument: str, date: date) -> data.Amount:
|
16
16
|
if self.priceMap:
|
17
17
|
price = prices.get_price(
|
18
18
|
self.priceMap, tuple([instrument, self.baseCcy]), date
|
@@ -21,7 +21,7 @@ class PriceLookup:
|
|
21
21
|
else:
|
22
22
|
return D(1)
|
23
23
|
|
24
|
-
def fetchPrice(self, instrument: str, date: date):
|
24
|
+
def fetchPrice(self, instrument: str, date: date) -> data.Amount:
|
25
25
|
if instrument == self.baseCcy:
|
26
26
|
return None
|
27
27
|
|
@@ -2,27 +2,30 @@ import re
|
|
2
2
|
from datetime import date
|
3
3
|
from decimal import Decimal
|
4
4
|
from os import path
|
5
|
+
from typing import Any
|
5
6
|
|
7
|
+
import beangulp
|
6
8
|
import yaml
|
7
9
|
from beancount.core import amount, data
|
8
10
|
from beancount.core.number import D
|
9
|
-
from beancount.ingest import importer
|
10
11
|
from ibflex import Types, client, parser
|
11
12
|
from ibflex.enums import CashAction
|
12
13
|
|
13
14
|
from tariochbctools.importers.general.priceLookup import PriceLookup
|
14
15
|
|
15
16
|
|
16
|
-
class Importer(
|
17
|
+
class Importer(beangulp.Importer):
|
17
18
|
"""An importer for Interactive Broker using the flex query service."""
|
18
19
|
|
19
|
-
def identify(self,
|
20
|
-
return path.basename(
|
20
|
+
def identify(self, filepath: str) -> bool:
|
21
|
+
return path.basename(filepath).endswith("ibkr.yaml")
|
21
22
|
|
22
|
-
def
|
23
|
+
def account(self, filepath: str) -> data.Account:
|
23
24
|
return ""
|
24
25
|
|
25
|
-
def matches(
|
26
|
+
def matches(
|
27
|
+
self, trx: Types.CashTransaction, t: Any, account: data.Account
|
28
|
+
) -> bool:
|
26
29
|
p = re.compile(r".* (?P<perShare>\d+\.?\d+) PER SHARE")
|
27
30
|
|
28
31
|
trxPerShareGroups = p.search(trx.description)
|
@@ -38,13 +41,13 @@ class Importer(importer.ImporterProtocol):
|
|
38
41
|
and t["account"] == account
|
39
42
|
)
|
40
43
|
|
41
|
-
def extract(self,
|
42
|
-
with open(
|
44
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
45
|
+
with open(filepath, "r") as f:
|
43
46
|
config = yaml.safe_load(f)
|
44
47
|
token = config["token"]
|
45
48
|
queryId = config["queryId"]
|
46
49
|
|
47
|
-
priceLookup = PriceLookup(
|
50
|
+
priceLookup = PriceLookup(existing, config["baseCcy"])
|
48
51
|
|
49
52
|
response = client.download(token, queryId)
|
50
53
|
statement = parser.parse(response)
|
@@ -52,7 +55,7 @@ class Importer(importer.ImporterProtocol):
|
|
52
55
|
|
53
56
|
result = []
|
54
57
|
for stmt in statement.FlexStatements:
|
55
|
-
transactions = []
|
58
|
+
transactions: list = []
|
56
59
|
account = stmt.accountId
|
57
60
|
for trx in stmt.Trades:
|
58
61
|
result.append(
|
@@ -147,7 +150,7 @@ class Importer(importer.ImporterProtocol):
|
|
147
150
|
priceLookup: PriceLookup,
|
148
151
|
description: str,
|
149
152
|
account: str,
|
150
|
-
):
|
153
|
+
) -> data.Transaction:
|
151
154
|
narration = "Dividend: " + description
|
152
155
|
liquidityAccount = self.getLiquidityAccount(account, currency)
|
153
156
|
incomeAccount = self.getIncomeAccount(account)
|
@@ -190,7 +193,7 @@ class Importer(importer.ImporterProtocol):
|
|
190
193
|
def createBuy(
|
191
194
|
self,
|
192
195
|
date: date,
|
193
|
-
account:
|
196
|
+
account: data.Account,
|
194
197
|
asset: str,
|
195
198
|
quantity: Decimal,
|
196
199
|
currency: str,
|
@@ -199,7 +202,7 @@ class Importer(importer.ImporterProtocol):
|
|
199
202
|
netCash: amount.Amount,
|
200
203
|
baseCcy: str,
|
201
204
|
fxRateToBase: Decimal,
|
202
|
-
):
|
205
|
+
) -> data.Transaction:
|
203
206
|
narration = "Buy"
|
204
207
|
feeAccount = self.getFeeAccount(account)
|
205
208
|
liquidityAccount = self.getLiquidityAccount(account, currency)
|
@@ -217,7 +220,7 @@ class Importer(importer.ImporterProtocol):
|
|
217
220
|
data.Posting(
|
218
221
|
assetAccount,
|
219
222
|
amount.Amount(quantity, asset),
|
220
|
-
data.
|
223
|
+
data.CostSpec(price, None, baseCcy, None, None, False),
|
221
224
|
None,
|
222
225
|
None,
|
223
226
|
None,
|
@@ -238,17 +241,17 @@ class Importer(importer.ImporterProtocol):
|
|
238
241
|
meta, date, "*", "", narration, data.EMPTY_SET, data.EMPTY_SET, postings
|
239
242
|
)
|
240
243
|
|
241
|
-
def getAssetAccount(self, account: str, asset: str):
|
244
|
+
def getAssetAccount(self, account: str, asset: str) -> data.Account:
|
242
245
|
return f"Assets:{account}:Investment:IB:{asset}"
|
243
246
|
|
244
|
-
def getLiquidityAccount(self, account: str, currency: str):
|
247
|
+
def getLiquidityAccount(self, account: str, currency: str) -> data.Account:
|
245
248
|
return f"Assets:{account}:Liquidity:IB:{currency}"
|
246
249
|
|
247
|
-
def getReceivableAccount(self, account: str):
|
250
|
+
def getReceivableAccount(self, account: str) -> data.Account:
|
248
251
|
return f"Assets:{account}:Receivable:Verrechnungssteuer"
|
249
252
|
|
250
|
-
def getIncomeAccount(self, account: str):
|
253
|
+
def getIncomeAccount(self, account: str) -> data.Account:
|
251
254
|
return f"Income:{account}:Interest"
|
252
255
|
|
253
|
-
def getFeeAccount(self, account: str):
|
256
|
+
def getFeeAccount(self, account: str) -> data.Account:
|
254
257
|
return f"Expenses:{account}:Fees"
|
@@ -1,30 +1,32 @@
|
|
1
1
|
import csv
|
2
|
-
|
2
|
+
import re
|
3
3
|
|
4
|
+
import beangulp
|
4
5
|
from beancount.core import amount, data
|
5
6
|
from beancount.core.number import D
|
6
|
-
from beancount.ingest import importer
|
7
|
-
from beancount.ingest.importers.mixins import identifier
|
8
7
|
from dateutil.parser import parse
|
9
8
|
|
10
9
|
|
11
|
-
class Importer(
|
10
|
+
class Importer(beangulp.Importer):
|
12
11
|
"""An importer for Neon CSV files."""
|
13
12
|
|
14
|
-
def __init__(self,
|
15
|
-
|
16
|
-
self.
|
13
|
+
def __init__(self, filepattern: str, account: data.Account):
|
14
|
+
self._filepattern = filepattern
|
15
|
+
self._account = account
|
17
16
|
|
18
|
-
def
|
19
|
-
return
|
17
|
+
def identify(self, filepath: str) -> bool:
|
18
|
+
return re.search(self._filepattern, filepath) is not None
|
20
19
|
|
21
|
-
def
|
22
|
-
return self.
|
20
|
+
def name(self) -> str:
|
21
|
+
return super().name() + self._account
|
23
22
|
|
24
|
-
def
|
23
|
+
def account(self, filepath: str) -> data.Account:
|
24
|
+
return self._account
|
25
|
+
|
26
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
25
27
|
entries = []
|
26
28
|
|
27
|
-
with
|
29
|
+
with open(filepath) as csvfile:
|
28
30
|
reader = csv.DictReader(
|
29
31
|
csvfile,
|
30
32
|
[
|
@@ -55,7 +57,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
55
57
|
metakv["original_amount"] = row["Original amount"]
|
56
58
|
metakv["exchange_rate"] = row["Exchange rate"]
|
57
59
|
|
58
|
-
meta = data.new_metadata(
|
60
|
+
meta = data.new_metadata(filepath, 0, metakv)
|
59
61
|
description = row["Description"].strip()
|
60
62
|
if row["Subject"].strip() != "":
|
61
63
|
description = description + ": " + row["Subject"].strip()
|
@@ -69,7 +71,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
|
|
69
71
|
data.EMPTY_SET,
|
70
72
|
data.EMPTY_SET,
|
71
73
|
[
|
72
|
-
data.Posting(self.
|
74
|
+
data.Posting(self._account, amt, None, None, None, None),
|
73
75
|
],
|
74
76
|
)
|
75
77
|
entries.append(entry)
|