Files
finance-dashboard/app/models.py
T

70 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)