Add SQLite passive income dashboard with Excel import and deployment tools
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Shared validation and cent-exact money formatting."""
|
||||
import re
|
||||
import unicodedata
|
||||
from datetime import date
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
|
||||
CATEGORIES = {'dividend': 'Dividende', 'interest': 'Zinsen', 'distribution': 'Ausschüttung', 'other': 'Sonstiges'}
|
||||
ASSET_TYPES = {'stock': 'Aktie', 'etf': 'ETF', 'bond': 'Anleihe', 'crypto': 'Krypto', 'interest': 'Zinskonto', 'other': 'Sonstiges'}
|
||||
MONTHS = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
|
||||
|
||||
|
||||
def cents(value):
|
||||
text = str(value).strip()
|
||||
if not re.fullmatch(r'-?\d{1,10}(?:[.,]\d{1,2})?', text):
|
||||
raise ValueError('Betrag mit höchstens zwei Nachkommastellen eingeben, ohne Tausendertrennzeichen.')
|
||||
try:
|
||||
number = Decimal(text.replace(',', '.'))
|
||||
except InvalidOperation:
|
||||
raise ValueError('Ungültiger Betrag.') from None
|
||||
if abs(number) > Decimal('999999999.99'):
|
||||
raise ValueError('Betrag ist zu groß.')
|
||||
return int(number * 100)
|
||||
|
||||
|
||||
def money(value):
|
||||
return f'{Decimal(value) / 100:,.2f}'.replace(',', '_').replace('.', ',').replace('_', '.') + ' €'
|
||||
|
||||
|
||||
def percent(current, previous):
|
||||
if not previous:
|
||||
return None
|
||||
return (Decimal(current - previous) * 100 / abs(Decimal(previous))).quantize(Decimal('.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def percent_text(value):
|
||||
return '–' if value is None else f'{value:.2f}'.replace('.', ',') + ' %'
|
||||
|
||||
|
||||
def valid_date(value):
|
||||
if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', str(value)):
|
||||
raise ValueError('Bitte ein gültiges Datum eingeben.')
|
||||
try:
|
||||
return date.fromisoformat(value).isoformat()
|
||||
except ValueError:
|
||||
raise ValueError('Bitte ein gültiges Datum eingeben.') from None
|
||||
|
||||
|
||||
def name_key(name):
|
||||
return ''.join(c for c in unicodedata.normalize('NFKC', name).casefold() if c.isalnum())
|
||||
|
||||
|
||||
ALIASES = {
|
||||
'msc': 'Main Street Capital', 'mainstreet': 'Main Street Capital', 'mainstreetcapital': 'Main Street Capital',
|
||||
'agnc': 'AGNC', 'agncinvestment': 'AGNC', 'agncinvestmentcorp': 'AGNC',
|
||||
'csw': 'Capital Southwest', 'cswc': 'Capital Southwest', 'capitalsouthwest': 'Capital Southwest',
|
||||
'arcc': 'Ares Capital', 'arescapital': 'Ares Capital', 'realtyincome': 'Realty Income',
|
||||
'enbridge': 'Enbridge', 'bayer': 'Bayer', 'stoxx': 'STOXX Global Select Dividend 100',
|
||||
'stoxxglobalselectdividend100': 'STOXX Global Select Dividend 100',
|
||||
'airbaltic': 'airBaltic', 'pc': 'Prospect Capital', 'psec': 'Prospect Capital',
|
||||
'prospectcapital': 'Prospect Capital', 'komischen26dividende': 'N26 Dividende',
|
||||
}
|
||||
|
||||
|
||||
def canonical_name(value):
|
||||
name = ' '.join(str(value).split())
|
||||
name = re.sub(r'^(dividende[n]?|ausschüttung)\s+', '', name, flags=re.I)
|
||||
if not name or len(name) > 150:
|
||||
raise ValueError('Positionsname muss zwischen 1 und 150 Zeichen lang sein.')
|
||||
return ALIASES.get(name_key(name), name)
|
||||
Reference in New Issue
Block a user