fix(tests): Testsuite unabhängig von einer lokalen .env machen
Ohne backend/.env galt COOKIE_SECURE=true. Der Testclient spricht über http, httpx verwarf die Secure-Cookies, und 106 Tests scheiterten mit 401. Lokal fiel das nicht auf, weil dort eine nicht eingecheckte .env den Wert überschrieb – die CI hat es aufgedeckt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
co-authored by
Claude Opus 5
parent
55975338f9
commit
bdf4097bd7
@@ -0,0 +1,57 @@
|
||||
"""CLI: Demodaten anlegen – `python -m app.scripts.seed_demo [--reset]`.
|
||||
|
||||
Legt einen vollständigen deutschen Beispielhaushalt an: Konten, Firmen samt
|
||||
Logos, wiederkehrende Posten, bestätigte Historie, Buchungen, Budgets und
|
||||
Sparziele. Alle Termine liegen relativ zum heutigen Monat.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.services.demo import has_data, seed_demo
|
||||
|
||||
|
||||
async def main(reset: bool, resolve_logos: bool) -> int:
|
||||
async with SessionLocal() as session:
|
||||
if not reset and await has_data(session):
|
||||
print(
|
||||
"Es sind bereits Daten vorhanden. Mit --reset werden Konten, Firmen, "
|
||||
"Posten, Buchungen, Budgets und Sparziele zuvor gelöscht.\n"
|
||||
"Kategorien und Benutzer bleiben in jedem Fall erhalten.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
await engine.dispose()
|
||||
return 1
|
||||
|
||||
ergebnis = await seed_demo(session, reset=reset, resolve_logos=resolve_logos)
|
||||
|
||||
await engine.dispose()
|
||||
print(
|
||||
"Demodaten angelegt:\n"
|
||||
f" {ergebnis.accounts} Konten\n"
|
||||
f" {ergebnis.merchants} Firmen ({ergebnis.logos} mit Logo)\n"
|
||||
f" {ergebnis.recurrences} wiederkehrende Posten\n"
|
||||
f" {ergebnis.occurrences} bestätigte Fälligkeiten\n"
|
||||
f" {ergebnis.transactions} Buchungen\n"
|
||||
f" {ergebnis.budgets} Budgets und Vorlagen\n"
|
||||
f" {ergebnis.goals} Sparziele"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="Vorhandene Konten, Firmen, Posten, Buchungen, Budgets und Sparziele löschen.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-logos",
|
||||
action="store_true",
|
||||
help="Logosuche überspringen – schneller und ohne jeden Netzzugriff.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
raise SystemExit(asyncio.run(main(args.reset, not args.no_logos)))
|
||||
@@ -0,0 +1,545 @@
|
||||
"""Demodaten: ein realistischer deutscher Haushalt.
|
||||
|
||||
Alle Termine liegen relativ zum heutigen Monat, damit Dashboard, Kalender und
|
||||
Auswertungen unmittelbar etwas zeigen – unabhängig davon, wann der Seed läuft.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.clock import add_months, month_start, today
|
||||
from app.models import (
|
||||
Account,
|
||||
AmountVersion,
|
||||
Budget,
|
||||
BudgetTemplate,
|
||||
Category,
|
||||
Merchant,
|
||||
Occurrence,
|
||||
Recurrence,
|
||||
ReserveLedger,
|
||||
SavingsGoal,
|
||||
Transaction,
|
||||
)
|
||||
from app.models.enums import (
|
||||
AccountType,
|
||||
BusinessDayShift,
|
||||
EntryKind,
|
||||
LogoStatus,
|
||||
)
|
||||
from app.services.logos import resolve_merchant_logo
|
||||
from app.services.merchants import normalize_name
|
||||
from app.services.occurrences import confirm
|
||||
from app.services.recurrence import expand
|
||||
from app.services.seed import seed_all
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DemoResult:
|
||||
"""Was der Seed angelegt hat."""
|
||||
|
||||
accounts: int = 0
|
||||
merchants: int = 0
|
||||
recurrences: int = 0
|
||||
transactions: int = 0
|
||||
occurrences: int = 0
|
||||
budgets: int = 0
|
||||
goals: int = 0
|
||||
logos: int = 0
|
||||
|
||||
|
||||
# Firmen mit ihrer Domain – die Domain verbessert die Logosuche.
|
||||
DEMO_MERCHANTS: tuple[tuple[str, str | None], ...] = (
|
||||
("Netflix", "netflix.com"),
|
||||
("Spotify", "spotify.com"),
|
||||
("Vodafone", "vodafone.de"),
|
||||
("Deutsche Telekom", "telekom.de"),
|
||||
("REWE", "rewe.de"),
|
||||
("dm", "dm.de"),
|
||||
("Deutsche Bahn", "bahn.de"),
|
||||
("Sparkasse", "sparkasse.de"),
|
||||
# Bewusst ohne Eintrag im Markenkatalog: zeigt den erzeugten Avatar.
|
||||
("Stadtwerke Musterstadt", None),
|
||||
("HUK-COBURG", "huk.de"),
|
||||
)
|
||||
|
||||
|
||||
async def _category_map(session: AsyncSession) -> dict[str, int]:
|
||||
"""Kategoriename -> ID. Bei doppelten Namen gewinnt die Unterkategorie."""
|
||||
rows = (await session.execute(select(Category))).scalars().all()
|
||||
namen: dict[str, int] = {}
|
||||
for kategorie in rows:
|
||||
if kategorie.name not in namen or kategorie.parent_id is not None:
|
||||
namen[kategorie.name] = kategorie.id
|
||||
return namen
|
||||
|
||||
|
||||
async def has_data(session: AsyncSession) -> bool:
|
||||
"""True, wenn schon Bewegungsdaten vorhanden sind."""
|
||||
for modell in (Recurrence, Transaction, Account):
|
||||
anzahl = (await session.execute(select(func.count()).select_from(modell))).scalar_one()
|
||||
if anzahl:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def reset_data(session: AsyncSession) -> None:
|
||||
"""Entfernt alle Bewegungs- und Stammdaten außer Kategorien und Benutzern.
|
||||
|
||||
Die Reihenfolge folgt den Fremdschlüsseln.
|
||||
"""
|
||||
for modell in (
|
||||
Occurrence,
|
||||
AmountVersion,
|
||||
ReserveLedger,
|
||||
Recurrence,
|
||||
Transaction,
|
||||
Budget,
|
||||
BudgetTemplate,
|
||||
SavingsGoal,
|
||||
Merchant,
|
||||
Account,
|
||||
):
|
||||
await session.execute(delete(modell))
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def seed_demo(
|
||||
session: AsyncSession, *, reset: bool = False, resolve_logos: bool = True
|
||||
) -> DemoResult:
|
||||
"""Legt einen vollständigen Beispielhaushalt an."""
|
||||
await seed_all(session)
|
||||
|
||||
if reset:
|
||||
await reset_data(session)
|
||||
|
||||
ergebnis = DemoResult()
|
||||
heute = today()
|
||||
monat = month_start(heute)
|
||||
|
||||
# --- Konten ---------------------------------------------------------------
|
||||
konten = {
|
||||
"giro": Account(
|
||||
name="Girokonto",
|
||||
type=AccountType.CHECKING,
|
||||
iban_last4="4711",
|
||||
opening_balance=Decimal("2450.00"),
|
||||
# Sechs Monate Historie, damit Vergleiche etwas zeigen.
|
||||
opening_balance_date=add_months(monat, -6),
|
||||
color="#3b82f6",
|
||||
icon="wallet",
|
||||
sort_order=0,
|
||||
),
|
||||
"karte": Account(
|
||||
name="Kreditkarte",
|
||||
type=AccountType.CREDIT_CARD,
|
||||
iban_last4="8820",
|
||||
opening_balance=Decimal("0.00"),
|
||||
opening_balance_date=add_months(monat, -6),
|
||||
color="#a855f7",
|
||||
icon="credit-card",
|
||||
sort_order=1,
|
||||
),
|
||||
"tagesgeld": Account(
|
||||
name="Tagesgeld",
|
||||
type=AccountType.SAVINGS,
|
||||
opening_balance=Decimal("8200.00"),
|
||||
opening_balance_date=add_months(monat, -6),
|
||||
color="#10b981",
|
||||
icon="piggy-bank",
|
||||
sort_order=2,
|
||||
),
|
||||
}
|
||||
session.add_all(konten.values())
|
||||
await session.flush()
|
||||
ergebnis.accounts = len(konten)
|
||||
|
||||
# --- Firmen ---------------------------------------------------------------
|
||||
firmen: dict[str, Merchant] = {}
|
||||
for name, domain in DEMO_MERCHANTS:
|
||||
firma = Merchant(
|
||||
name=name,
|
||||
normalized_name=normalize_name(name),
|
||||
domain=domain,
|
||||
logo_status=LogoStatus.PENDING,
|
||||
)
|
||||
session.add(firma)
|
||||
firmen[name] = firma
|
||||
await session.flush()
|
||||
ergebnis.merchants = len(firmen)
|
||||
|
||||
if resolve_logos:
|
||||
for firma in firmen.values():
|
||||
try:
|
||||
if await resolve_merchant_logo(session, firma) is not None:
|
||||
ergebnis.logos += 1
|
||||
except Exception: # eine fehlende Grafik darf den Seed nicht stoppen
|
||||
logger.exception("Logo für %s konnte nicht ermittelt werden.", firma.name)
|
||||
|
||||
# --- Wiederkehrende Posten ------------------------------------------------
|
||||
kategorie = await _category_map(session)
|
||||
giro = konten["giro"].id
|
||||
karte = konten["karte"].id
|
||||
|
||||
posten: list[Recurrence] = []
|
||||
|
||||
def anlegen(**felder) -> Recurrence:
|
||||
eintrag = Recurrence(**felder)
|
||||
session.add(eintrag)
|
||||
posten.append(eintrag)
|
||||
return eintrag
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.INCOME,
|
||||
title="Gehalt",
|
||||
category_id=kategorie["Gehalt"],
|
||||
account_id=giro,
|
||||
amount=Decimal("3450.00"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=28",
|
||||
dtstart=add_months(monat, -6).replace(day=28),
|
||||
# Das Gehalt kommt vor dem Wochenende, nicht danach.
|
||||
business_day_shift=BusinessDayShift.PREVIOUS,
|
||||
notes="Monatliches Nettogehalt.",
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Miete",
|
||||
category_id=kategorie["Miete"],
|
||||
account_id=giro,
|
||||
amount=Decimal("1180.00"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=1",
|
||||
dtstart=add_months(monat, -6),
|
||||
merchant_id=None,
|
||||
notes="Kaltmiete zuzüglich Nebenkosten.",
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Strom",
|
||||
category_id=kategorie["Strom"],
|
||||
account_id=giro,
|
||||
merchant_id=firmen["Stadtwerke Musterstadt"].id,
|
||||
amount=Decimal("89.00"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=15",
|
||||
dtstart=add_months(monat, -6).replace(day=15),
|
||||
# Abschlag; die Jahresabrechnung weicht regelmäßig ab.
|
||||
is_variable=True,
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Internet & Telefon",
|
||||
category_id=kategorie["Internet"],
|
||||
account_id=giro,
|
||||
merchant_id=firmen["Vodafone"].id,
|
||||
amount=Decimal("39.99"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=5",
|
||||
dtstart=add_months(monat, -6).replace(day=5),
|
||||
)
|
||||
|
||||
netflix = anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Netflix Standard",
|
||||
category_id=kategorie["Streaming"],
|
||||
account_id=karte,
|
||||
merchant_id=firmen["Netflix"].id,
|
||||
amount=Decimal("13.99"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=17",
|
||||
dtstart=add_months(monat, -6).replace(day=17),
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Spotify Premium",
|
||||
category_id=kategorie["Streaming"],
|
||||
account_id=karte,
|
||||
merchant_id=firmen["Spotify"].id,
|
||||
amount=Decimal("11.99"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=3",
|
||||
dtstart=add_months(monat, -6).replace(day=3),
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Rundfunkbeitrag",
|
||||
category_id=kategorie["Rundfunkbeitrag"],
|
||||
account_id=giro,
|
||||
amount=Decimal("55.08"),
|
||||
# Vierteljährlich, jeweils Mitte des Quartalsmonats.
|
||||
rrule="FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=15",
|
||||
dtstart=add_months(monat, -6).replace(day=15),
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Kfz-Versicherung",
|
||||
category_id=kategorie["Kfz"],
|
||||
account_id=giro,
|
||||
merchant_id=firmen["HUK-COBURG"].id,
|
||||
amount=Decimal("612.00"),
|
||||
rrule="FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=15",
|
||||
# Vorjahr, damit eine Zahlung in der Historie liegt und die nächste im Januar.
|
||||
dtstart=date(heute.year - 1, 1, 15),
|
||||
# Jahresbeitrag auf zwölf Monate verteilt zurücklegen.
|
||||
reserve_enabled=True,
|
||||
notes="Jahresbeitrag, Zahlung im Januar.",
|
||||
)
|
||||
|
||||
handyvertrag_start = add_months(monat, -8)
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Handyvertrag",
|
||||
category_id=kategorie["Internet"],
|
||||
account_id=giro,
|
||||
merchant_id=firmen["Deutsche Telekom"].id,
|
||||
amount=Decimal("29.99"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=8",
|
||||
dtstart=handyvertrag_start.replace(day=8),
|
||||
contract_start=handyvertrag_start,
|
||||
contract_min_term_months=24,
|
||||
contract_notice_period_days=90,
|
||||
contract_auto_renew_months=12,
|
||||
notes="Mindestlaufzeit 24 Monate, danach jährliche Verlängerung.",
|
||||
)
|
||||
|
||||
kredit_start = add_months(monat, -9)
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Autokredit",
|
||||
category_id=kategorie["Kredite"],
|
||||
account_id=giro,
|
||||
merchant_id=firmen["Sparkasse"].id,
|
||||
amount=Decimal("289.00"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=1",
|
||||
dtstart=kredit_start,
|
||||
installments_total=36,
|
||||
principal_amount=Decimal("10404.00"),
|
||||
notes="36 Raten, Restschuld siehe Detailansicht.",
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Sparplan Tagesgeld",
|
||||
category_id=kategorie["Sparen"],
|
||||
account_id=giro,
|
||||
amount=Decimal("250.00"),
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=28",
|
||||
dtstart=add_months(monat, -6).replace(day=28),
|
||||
)
|
||||
|
||||
anlegen(
|
||||
kind=EntryKind.INCOME,
|
||||
title="Nebeneinkünfte",
|
||||
category_id=kategorie["Nebeneinkünfte"],
|
||||
account_id=giro,
|
||||
amount=Decimal("320.00"),
|
||||
# Alle zwei Monate.
|
||||
rrule="FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=20",
|
||||
dtstart=add_months(monat, -6).replace(day=20),
|
||||
is_variable=True,
|
||||
)
|
||||
|
||||
await session.flush()
|
||||
ergebnis.recurrences = len(posten)
|
||||
|
||||
# Preishistorie: für jeden Posten der Anfangsbetrag ab Serienstart.
|
||||
for eintrag in posten:
|
||||
session.add(
|
||||
AmountVersion(
|
||||
recurrence_id=eintrag.id,
|
||||
amount=eintrag.amount,
|
||||
valid_from=eintrag.dtstart,
|
||||
note="Anfangsbetrag",
|
||||
)
|
||||
)
|
||||
|
||||
# Die Versionen müssen in der Datenbank stehen, bevor sie abgefragt werden.
|
||||
await session.flush()
|
||||
|
||||
# Netflix ist vor drei Monaten teurer geworden – vergangene Monate bleiben
|
||||
# dadurch betragstreu.
|
||||
preiswechsel = add_months(monat, -3)
|
||||
session.add(
|
||||
AmountVersion(
|
||||
recurrence_id=netflix.id,
|
||||
amount=Decimal("13.99"),
|
||||
valid_from=preiswechsel,
|
||||
note="Preiserhöhung laut Schreiben",
|
||||
)
|
||||
)
|
||||
# Der Anfangsbetrag lag darunter.
|
||||
anfang = next(
|
||||
version
|
||||
for version in await _versions_of(session, netflix.id)
|
||||
if version.valid_from == netflix.dtstart
|
||||
)
|
||||
anfang.amount = Decimal("11.99")
|
||||
await session.flush()
|
||||
|
||||
# --- Vergangene Fälligkeiten bestätigen -----------------------------------
|
||||
# Erst dadurch zeigen die Berichte einen Plan-Ist-Vergleich.
|
||||
ergebnis.occurrences = await _confirm_past(session, posten, heute)
|
||||
|
||||
# --- Einmalige Buchungen --------------------------------------------------
|
||||
ergebnis.transactions = await _seed_transactions(session, kategorie, konten, firmen, monat)
|
||||
|
||||
# --- Budgets --------------------------------------------------------------
|
||||
budgets = [
|
||||
Budget(
|
||||
category_id=kategorie["Lebensmittel"],
|
||||
period_month=monat,
|
||||
limit_amount=Decimal("450.00"),
|
||||
rollover=True,
|
||||
),
|
||||
Budget(
|
||||
category_id=kategorie["Restaurant"],
|
||||
period_month=monat,
|
||||
limit_amount=Decimal("120.00"),
|
||||
),
|
||||
Budget(
|
||||
category_id=kategorie["Drogerie"],
|
||||
period_month=monat,
|
||||
limit_amount=Decimal("60.00"),
|
||||
),
|
||||
]
|
||||
session.add_all(budgets)
|
||||
session.add(
|
||||
BudgetTemplate(
|
||||
category_id=kategorie["Sprit"],
|
||||
valid_from=add_months(monat, -6),
|
||||
limit_amount=Decimal("180.00"),
|
||||
)
|
||||
)
|
||||
ergebnis.budgets = len(budgets) + 1
|
||||
|
||||
# --- Sparziele ------------------------------------------------------------
|
||||
ziele = [
|
||||
SavingsGoal(
|
||||
name="Neues Fahrrad",
|
||||
target_amount=Decimal("1800.00"),
|
||||
current_amount=Decimal("620.00"),
|
||||
target_date=add_months(monat, 10),
|
||||
monthly_contribution=Decimal("100.00"),
|
||||
account_id=konten["tagesgeld"].id,
|
||||
color="#22c55e",
|
||||
icon="bike",
|
||||
),
|
||||
SavingsGoal(
|
||||
name="Notgroschen",
|
||||
target_amount=Decimal("6000.00"),
|
||||
current_amount=Decimal("4200.00"),
|
||||
monthly_contribution=Decimal("250.00"),
|
||||
account_id=konten["tagesgeld"].id,
|
||||
color="#3b82f6",
|
||||
icon="shield",
|
||||
),
|
||||
]
|
||||
session.add_all(ziele)
|
||||
ergebnis.goals = len(ziele)
|
||||
|
||||
await session.commit()
|
||||
logger.info("Demodaten angelegt: %s", ergebnis)
|
||||
return ergebnis
|
||||
|
||||
|
||||
async def _versions_of(session: AsyncSession, recurrence_id: int) -> list[AmountVersion]:
|
||||
stmt = select(AmountVersion).where(AmountVersion.recurrence_id == recurrence_id)
|
||||
return list((await session.execute(stmt)).scalars().all())
|
||||
|
||||
|
||||
async def _confirm_past(session: AsyncSession, posten: list[Recurrence], heute: date) -> int:
|
||||
"""Bestätigt alle Fälligkeiten, die vor heute lagen.
|
||||
|
||||
Beim Strom weicht der Ist-Betrag ab – so wie eine echte Abrechnung.
|
||||
"""
|
||||
anzahl = 0
|
||||
for eintrag in posten:
|
||||
versionen = await _versions_of(session, eintrag.id)
|
||||
faellig = expand(
|
||||
eintrag,
|
||||
eintrag.dtstart,
|
||||
heute - timedelta(days=1),
|
||||
amount_versions=versionen,
|
||||
)
|
||||
|
||||
for index, geplant in enumerate(faellig):
|
||||
ist = geplant.amount
|
||||
if eintrag.is_variable:
|
||||
# Abwechselnd etwas darüber und darunter.
|
||||
abweichung = Decimal("7.40") if index % 2 == 0 else Decimal("-4.20")
|
||||
ist = max(geplant.amount + abweichung, Decimal("0.01"))
|
||||
|
||||
await confirm(
|
||||
session,
|
||||
eintrag.id,
|
||||
geplant.nominal_date,
|
||||
actual_amount=ist,
|
||||
)
|
||||
anzahl += 1
|
||||
|
||||
await session.flush()
|
||||
return anzahl
|
||||
|
||||
|
||||
async def _seed_transactions(
|
||||
session: AsyncSession,
|
||||
kategorie: dict[str, int],
|
||||
konten: dict[str, Account],
|
||||
firmen: dict[str, Merchant],
|
||||
monat: date,
|
||||
) -> int:
|
||||
"""Einmalige Buchungen der letzten drei Monate."""
|
||||
giro = konten["giro"].id
|
||||
karte = konten["karte"].id
|
||||
|
||||
muster: tuple[tuple[int, int, str, str, str, str | None, int], ...] = (
|
||||
# (Monatsversatz, Tag, Titel, Betrag, Kategorie, Firma, Konto)
|
||||
(0, 3, "Wocheneinkauf", "84.30", "Lebensmittel", "REWE", giro),
|
||||
(0, 7, "Drogerie", "23.40", "Drogerie", "dm", giro),
|
||||
(0, 9, "Tanken", "72.10", "Sprit", None, karte),
|
||||
(0, 11, "Wocheneinkauf", "96.85", "Lebensmittel", "REWE", giro),
|
||||
(0, 12, "Abendessen", "48.60", "Restaurant", None, karte),
|
||||
(0, 18, "Wocheneinkauf", "78.20", "Lebensmittel", "REWE", giro),
|
||||
(0, 21, "Bahnticket", "49.00", "ÖPNV", "Deutsche Bahn", giro),
|
||||
(-1, 4, "Wocheneinkauf", "91.15", "Lebensmittel", "REWE", giro),
|
||||
(-1, 8, "Werkstatt Inspektion", "312.00", "Werkstatt", None, giro),
|
||||
(-1, 14, "Drogerie", "31.75", "Drogerie", "dm", giro),
|
||||
(-1, 16, "Tanken", "68.40", "Sprit", None, karte),
|
||||
(-1, 19, "Wocheneinkauf", "88.90", "Lebensmittel", "REWE", giro),
|
||||
(-1, 24, "Restaurant", "62.30", "Restaurant", None, karte),
|
||||
(-2, 6, "Wocheneinkauf", "79.55", "Lebensmittel", "REWE", giro),
|
||||
(-2, 13, "Tanken", "70.20", "Sprit", None, karte),
|
||||
(-2, 22, "Steuererstattung", "418.00", "Erstattungen", None, giro),
|
||||
)
|
||||
|
||||
anzahl = 0
|
||||
for versatz, tag, titel, betrag, kategoriename, firmenname, konto in muster:
|
||||
buchungsmonat = add_months(monat, versatz)
|
||||
ist_einkunft = kategoriename == "Erstattungen"
|
||||
|
||||
session.add(
|
||||
Transaction(
|
||||
kind=EntryKind.INCOME if ist_einkunft else EntryKind.EXPENSE,
|
||||
title=titel,
|
||||
category_id=kategorie[kategoriename],
|
||||
account_id=konto,
|
||||
merchant_id=firmen[firmenname].id if firmenname else None,
|
||||
amount=Decimal(betrag),
|
||||
booking_date=buchungsmonat.replace(day=tag),
|
||||
)
|
||||
)
|
||||
anzahl += 1
|
||||
|
||||
await session.flush()
|
||||
return anzahl
|
||||
|
||||
|
||||
__all__ = ["DemoResult", "has_data", "reset_data", "seed_demo"]
|
||||
@@ -13,6 +13,10 @@ os.environ.setdefault(
|
||||
)
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-mindestens-32-zeichen-lang")
|
||||
os.environ.setdefault("ENVIRONMENT", "test")
|
||||
# Der Testclient spricht über http; `Secure`-Cookies würde httpx verwerfen und
|
||||
# jede geschützte Route liefe in einen 401. Ohne diese Zeile hinge die Suite an
|
||||
# einer lokalen .env – die CI hat genau das aufgedeckt.
|
||||
os.environ.setdefault("COOKIE_SECURE", "false")
|
||||
# Die Hintergrundsuche liefe außerhalb der Testtransaktion – sie wird gezielt aufgerufen.
|
||||
os.environ.setdefault("LOGO_AUTO_RESOLVE", "false")
|
||||
os.environ.setdefault("LOGO_STORAGE_DIR", tempfile.mkdtemp(prefix="moneyfy-logos-"))
|
||||
|
||||
Reference in New Issue
Block a user