diff options
author | Wolfgang Müller | 2025-02-18 13:21:20 +0100 |
---|---|---|
committer | Wolfgang Müller | 2025-02-18 13:21:20 +0100 |
commit | e6c6285479ca6ae22901e3e909898cda2fe8fbb5 (patch) | |
tree | 438827b4a331e5c19b3f54825de1a78a4f94f01e /beancount_oriole | |
download | beancount-oriole-e6c6285479ca6ae22901e3e909898cda2fe8fbb5.tar.gz |
Diffstat (limited to 'beancount_oriole')
-rw-r--r-- | beancount_oriole/__init__.py | 0 | ||||
-rw-r--r-- | beancount_oriole/prices/__init__.py | 0 | ||||
-rw-r--r-- | beancount_oriole/prices/ing_csv.py | 72 | ||||
-rw-r--r-- | beancount_oriole/tag_by_account.py | 37 |
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 |