aboutsummaryrefslogtreecommitdiffstats
path: root/beancount_oriole
diff options
context:
space:
mode:
authorWolfgang Müller2025-02-18 13:21:20 +0100
committerWolfgang Müller2025-02-18 13:21:20 +0100
commite6c6285479ca6ae22901e3e909898cda2fe8fbb5 (patch)
tree438827b4a331e5c19b3f54825de1a78a4f94f01e /beancount_oriole
downloadbeancount-oriole-e6c6285479ca6ae22901e3e909898cda2fe8fbb5.tar.gz
Initial commitHEADtrunk
Diffstat (limited to 'beancount_oriole')
-rw-r--r--beancount_oriole/__init__.py0
-rw-r--r--beancount_oriole/prices/__init__.py0
-rw-r--r--beancount_oriole/prices/ing_csv.py72
-rw-r--r--beancount_oriole/tag_by_account.py37
4 files changed, 109 insertions, 0 deletions
diff --git a/beancount_oriole/__init__.py b/beancount_oriole/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/beancount_oriole/__init__.py
diff --git a/beancount_oriole/prices/__init__.py b/beancount_oriole/prices/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/beancount_oriole/prices/__init__.py
diff --git a/beancount_oriole/prices/ing_csv.py b/beancount_oriole/prices/ing_csv.py
new file mode 100644
index 0000000..a6d394d
--- /dev/null
+++ b/beancount_oriole/prices/ing_csv.py
@@ -0,0 +1,72 @@
+import csv
+import os
+import re
+from dataclasses import dataclass
+from datetime import datetime
+
+import pytz
+from beancount.core.number import D
+from beanprices import source
+
+FOLDER = "~/net/downloads"
+PATTERN = re.compile(r"Depot_\d{2}\.\d{2}\.\d{4}\.csv")
+TZ = pytz.timezone("Europe/Berlin")
+
+
+@dataclass
+class WatchlistEntry:
+ date: datetime
+ price_map: dict
+
+
+def parse_header(line):
+ datestr = line.strip().removeprefix("Depotbewertung vom ")
+
+ return datetime.strptime(datestr, "%d.%m.%Y %H:%M:%S")
+
+
+def parse_file(handle):
+ date = parse_header(handle.readline()).replace(tzinfo=TZ)
+ handle.readline()
+ handle.readline()
+
+ handle.readline()
+ handle.readline()
+ handle.readline()
+
+ reader = csv.DictReader(handle, delimiter=";")
+
+ prices = dict()
+
+ for row in reader:
+ if isin := row["ISIN"]:
+ price = D(row["Aktueller Preis"].replace(",", "."))
+ prices[isin] = source.SourcePrice(price, date, row["Währung"])
+
+ return WatchlistEntry(date, prices)
+
+
+def get_entries():
+ for entry in os.scandir(os.path.expanduser(FOLDER)):
+ if entry.is_file():
+ if not PATTERN.match(entry.name):
+ continue
+
+ with open(entry, encoding="cp1252") as handle:
+ yield parse_file(handle)
+
+
+class Source(source.Source):
+ def get_latest_price(self, isin):
+ entries = sorted(get_entries(), key=lambda e: e.date, reverse=True)
+ if not entries:
+ return None
+
+ return entries[0].price_map.get(isin, None)
+
+ def get_historical_price(self, isin, time):
+ for entry in get_entries():
+ if entry.date.date() == time.date():
+ return entry.price_map.get(isin, None)
+
+ return None
diff --git a/beancount_oriole/tag_by_account.py b/beancount_oriole/tag_by_account.py
new file mode 100644
index 0000000..4ccd203
--- /dev/null
+++ b/beancount_oriole/tag_by_account.py
@@ -0,0 +1,37 @@
+import ast
+import collections
+
+from beancount.core import data
+
+__plugins__ = ["tag_by_account"]
+
+ConfigError = collections.namedtuple("ConfigError", ["source", "message", "entry"])
+
+
+def tag_by_account(entries, option_map, config):
+ errors = []
+
+ if config.strip():
+ try:
+ tag_map = ast.literal_eval(config)
+ except (SyntaxError, ValueError):
+ filename = data.new_metadata(option_map["filename"], 0)
+ errors.append(
+ ConfigError(filename, f"Syntax error in config: {config}", None)
+ )
+ return entries, errors
+ else:
+ return entries, errors
+
+ for index, entry in enumerate(entries):
+ if not isinstance(entry, data.Transaction):
+ continue
+
+ for posting in entry.postings:
+ if posting.account in tag_map:
+ new_tags = tag_map[posting.account]
+ old_tags = entry.tags or []
+ entries[index] = entry._replace(tags=set(old_tags).union(new_tags))
+ break
+
+ return entries, errors