feat: Abos & Vertraege – was regelmaessig abgeht, an einer Stelle
Netflix, Handyvertrag, Krankenkasse, Halbtax. Bisher sah man sie nur als einzelne Buchungen im Auszug; die Frage "welche Abos habe ich eigentlich, und was kosten die zusammen im Monat" liess sich nicht beantworten. Genau die steht jetzt oben auf der Seite. Der Bereich heisst "Abos & Vertraege", nicht "Abos": Ein Handyvertrag ist kein Abo und die Versicherung erst recht nicht – niemand soll ueberlegen muessen, wo sie hingehoert. Der Monatswert wird gerechnet, nicht gespeichert (Betrag durch Intervall). Gespeichert veraltete er beim naechsten Preisaufschlag stillschweigend. Bei laengerem Intervall klappt "Ich lege monatlich zurueck auf" auf, mit dem gerechneten Monatsbetrag daneben. Bei monatlicher Abrechnung gibt es nichts anzusparen, also steht es da auch nicht. DAZU: Waehrungen sind offen. Die Rechen-Maschinerie konnte immer schon beliebige Waehrungen – sie rechnet ueber den Euro als Drehpunkt. Beschraenkt haben es zwei Konstanten: eine Erlaubnisliste mit EUR und CHF, und ein Filter, der aus dem EZB-Feed alles ausser CHF wegwarf, obwohl er dreissig Waehrungen liefert. Massgeblich ist jetzt die Liste der EZB. Frei erfundene Waehrungen bleiben draussen, und das ist kein Geiz: Ohne Kurs liesse sich der Betrag nie umrechnen – er stuende in keiner Summe und in keinem Budget, und das faellt erst spaet auf. Ein Vertrag in einer Waehrung ohne Kurs wird deshalb genannt, statt die Summe stillschweigend zu klein anzuzeigen. Erkannt und gebucht wird noch nichts; der Erkennungstext wird nur erfasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff621984dc
commit
f99be47587
@@ -913,6 +913,83 @@ class LinkIgnored(Base):
|
||||
)
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
"""Ein Abo oder Vertrag: wiederkehrende Zahlung mit bekanntem Betrag.
|
||||
|
||||
Netflix, Handyvertrag, Krankenkasse, Halbtax – dieselbe Form. Deshalb
|
||||
heisst der Bereich "Abos & Vertraege": Ein Handyvertrag ist kein Abo, und
|
||||
niemand soll ueberlegen muessen, wo die Versicherung hingehoert.
|
||||
|
||||
Der Monatswert wird NICHT gespeichert, sondern gerechnet
|
||||
(``amount_cents / interval_months``). Gespeichert veraltete er beim
|
||||
naechsten Preisaufschlag stillschweigend.
|
||||
"""
|
||||
|
||||
__tablename__ = "subscriptions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
# Wie bei Konten und Kategorien: Am Symbol findet man die Zeile schneller
|
||||
# wieder als am Namen, wenn dreissig davon untereinander stehen.
|
||||
icon: Mapped[str] = mapped_column(String(40), nullable=False, default="refresh-ccw")
|
||||
color: Mapped[str] = mapped_column(String(9), nullable=False, default="#6b7480")
|
||||
|
||||
amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
# Eigene Waehrung, nicht die des Kontos: Ein deutscher Handyvertrag laeuft
|
||||
# in Euro, auch wenn er von einem Franken-Konto bezahlt wird.
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
# 1 = monatlich, 3 = vierteljaehrlich, 6 = halbjaehrlich, 12 = jaehrlich.
|
||||
# Als Zahl und nicht als Aufzaehlung: Der Monatswert ist dann eine Division
|
||||
# statt einer Tabelle, und ein zweimonatlicher Vertrag braucht keine
|
||||
# Migration.
|
||||
interval_months: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
# Womit bezahlt wird. Manche Abos laufen ueber die Kreditkarte, andere per
|
||||
# eBill direkt vom Konto – ohne diese Angabe findet man den Abbucher nicht.
|
||||
account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
category_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("categories.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# Naechste Verlaengerung/Abbuchung.
|
||||
next_due: Mapped[date_t | None] = mapped_column(Date, nullable=True)
|
||||
# Woran die Abbuchung im Kontoauszug zu erkennen ist. Aus dem Namen
|
||||
# vorbelegt, aenderbar – auf dem Auszug steht selten "Netflix".
|
||||
payee_pattern: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
|
||||
# Nur bei laengerem Intervall: das Konto, auf das monatlich zurueckgelegt
|
||||
# wird. Die Jahresrechnung wird dann daraus bezahlt, statt einen Monat zu
|
||||
# sprengen.
|
||||
savings_account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
# Gekuendigtes wird nicht geloescht: Der Verlauf ist die Antwort auf
|
||||
# "was habe ich dafuer insgesamt bezahlt".
|
||||
cancelled_at: Mapped[date_t | None] = mapped_column(Date, nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now, onupdate=_now)
|
||||
|
||||
@property
|
||||
def monatlich_cents(self) -> int:
|
||||
"""Was der Vertrag im Monat kostet – in SEINER Waehrung.
|
||||
|
||||
Kaufmaennisch gerundet. Die Summe ueber viele Vertraege weicht dadurch
|
||||
um ein paar Rappen von der exakten ab; das ist der Preis dafuer, dass
|
||||
jede einzelne Zeile eine Zahl zeigt, die man nachrechnen kann.
|
||||
"""
|
||||
n = self.interval_months or 1
|
||||
return round(self.amount_cents / n)
|
||||
|
||||
|
||||
class BankApp(Base):
|
||||
"""Die bei Enable Banking registrierte Anwendung dieser Installation.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user