feat: Phase 1 – Konten, Kategorien, Transaktionen, EZB-Währungsumrechnung
Backend: - Modelle + Migration 0002: accounts, account_identifiers, category_groups, categories, transactions, exchange_rates (alle owner-gescoped). - EZB-Kursdienst (90-Tage/Historie) + Backfill-Skript; Umrechnung EUR<->CHF zum Buchungsdatum mit lokalem Cache und manueller Übersteuerung; EUR-Pivot (amount_eur_cents) eingefroren. - Kontostände + Netto-Vermögen (Umrechnung in Hauptwährung). - Router: accounts (inkl. IBAN-Identifikatoren), categories (Gruppen+Kategorien), transactions (Ausgabe/Einnahme/Transfer, cross-currency), rates. - Gruppierte Standard-Kategorien je Nutzer beim Anlegen. - pytest-Suite: Währung, Salden, Cross-Currency-Transfer, Nutzer-Isolation. Frontend: - Seiten Konten, Kategorien, Buchungen; Icon-Chip, Geldbetrag mit Umrechnung, Icon-/Farbwähler, Modal; erweitertes SVG-Icon-Set (Konto-/Kategorie-Icons). - Währungs-/Datumsformatierung (EUR de-DE, CHF de-CH), i18n DE/EN erweitert. - Live-Umrechnungs-Vorschau im Transfer-Formular. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Konten, Identifikatoren, Kategorien(-gruppen), Transaktionen, Wechselkurse.
|
||||
|
||||
Revision ID: 0002
|
||||
Revises: 0001
|
||||
Create Date: 2026-08-19
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0002"
|
||||
down_revision = "0001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"accounts",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("name", sa.String(length=120), nullable=False),
|
||||
sa.Column("type", sa.String(length=32), nullable=False),
|
||||
sa.Column("currency", sa.String(length=3), nullable=False),
|
||||
sa.Column("icon", sa.String(length=40), nullable=False, server_default="wallet"),
|
||||
sa.Column("color", sa.String(length=9), nullable=False, server_default="#3f51b5"),
|
||||
sa.Column("opening_balance_cents", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("opening_date", sa.Date(), nullable=True),
|
||||
sa.Column("is_archived", sa.Boolean(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("position", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
op.create_index("ix_accounts_owner_id", "accounts", ["owner_id"])
|
||||
|
||||
op.create_table(
|
||||
"account_identifiers",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("account_id", sa.Integer(), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("kind", sa.String(length=32), nullable=False),
|
||||
sa.Column("value", sa.String(length=64), nullable=False),
|
||||
sa.Column("value_norm", sa.String(length=64), nullable=False),
|
||||
sa.UniqueConstraint("account_id", "kind", "value_norm", name="uq_ident_account_kind_value"),
|
||||
)
|
||||
op.create_index("ix_account_identifiers_account_id", "account_identifiers", ["account_id"])
|
||||
op.create_index("ix_account_identifiers_value_norm", "account_identifiers", ["value_norm"])
|
||||
|
||||
op.create_table(
|
||||
"category_groups",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("name", sa.String(length=120), nullable=False),
|
||||
sa.Column("kind", sa.String(length=32), nullable=False),
|
||||
sa.Column("icon", sa.String(length=40), nullable=False, server_default="list"),
|
||||
sa.Column("color", sa.String(length=9), nullable=False, server_default="#6b7480"),
|
||||
sa.Column("position", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("is_builtin", sa.Boolean(), nullable=False, server_default=sa.text("0")),
|
||||
)
|
||||
op.create_index("ix_category_groups_owner_id", "category_groups", ["owner_id"])
|
||||
|
||||
op.create_table(
|
||||
"categories",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("group_id", sa.Integer(), sa.ForeignKey("category_groups.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("name", sa.String(length=120), nullable=False),
|
||||
sa.Column("type", sa.String(length=32), nullable=False),
|
||||
sa.Column("icon", sa.String(length=40), nullable=False, server_default="more-horizontal"),
|
||||
sa.Column("color", sa.String(length=9), nullable=False, server_default="#6b7480"),
|
||||
sa.Column("position", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("is_builtin", sa.Boolean(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("is_archived", sa.Boolean(), nullable=False, server_default=sa.text("0")),
|
||||
)
|
||||
op.create_index("ix_categories_owner_id", "categories", ["owner_id"])
|
||||
op.create_index("ix_categories_group_id", "categories", ["group_id"])
|
||||
|
||||
op.create_table(
|
||||
"transactions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("type", sa.String(length=32), nullable=False),
|
||||
sa.Column("date", sa.Date(), nullable=False),
|
||||
sa.Column("time", sa.Time(), nullable=True),
|
||||
sa.Column("account_id", sa.Integer(), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("to_account_id", sa.Integer(), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=True),
|
||||
sa.Column("amount_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("to_amount_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("fx_rate", sa.Numeric(18, 8), nullable=True),
|
||||
sa.Column("amount_eur_cents", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("category_id", sa.Integer(), sa.ForeignKey("categories.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("budget_treatment", sa.String(length=32), nullable=True),
|
||||
sa.Column("budget_category_id", sa.Integer(), sa.ForeignKey("categories.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("payee", sa.String(length=255), nullable=True),
|
||||
sa.Column("description", sa.String(length=255), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
op.create_index("ix_transactions_owner_id", "transactions", ["owner_id"])
|
||||
op.create_index("ix_transactions_date", "transactions", ["date"])
|
||||
op.create_index("ix_transactions_account_id", "transactions", ["account_id"])
|
||||
op.create_index("ix_transactions_to_account_id", "transactions", ["to_account_id"])
|
||||
op.create_index("ix_transactions_category_id", "transactions", ["category_id"])
|
||||
|
||||
op.create_table(
|
||||
"exchange_rates",
|
||||
sa.Column("date", sa.Date(), primary_key=True),
|
||||
sa.Column("currency", sa.String(length=3), primary_key=True),
|
||||
sa.Column("rate_per_eur", sa.Numeric(18, 8), nullable=False),
|
||||
sa.Column("source", sa.String(length=16), nullable=False, server_default="ecb"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("exchange_rates")
|
||||
for ix in (
|
||||
"ix_transactions_category_id",
|
||||
"ix_transactions_to_account_id",
|
||||
"ix_transactions_account_id",
|
||||
"ix_transactions_date",
|
||||
"ix_transactions_owner_id",
|
||||
):
|
||||
op.drop_index(ix, table_name="transactions")
|
||||
op.drop_table("transactions")
|
||||
op.drop_index("ix_categories_group_id", table_name="categories")
|
||||
op.drop_index("ix_categories_owner_id", table_name="categories")
|
||||
op.drop_table("categories")
|
||||
op.drop_index("ix_category_groups_owner_id", table_name="category_groups")
|
||||
op.drop_table("category_groups")
|
||||
op.drop_index("ix_account_identifiers_value_norm", table_name="account_identifiers")
|
||||
op.drop_index("ix_account_identifiers_account_id", table_name="account_identifiers")
|
||||
op.drop_table("account_identifiers")
|
||||
op.drop_index("ix_accounts_owner_id", table_name="accounts")
|
||||
op.drop_table("accounts")
|
||||
Reference in New Issue
Block a user