Обновление приложения

This commit is contained in:
2026-04-28 22:37:19 +03:00
parent 7d00cf062e
commit 52594b5dc6
99 changed files with 5736 additions and 1052 deletions

View File

@@ -0,0 +1,19 @@
from app.models.user import Role, User
from app.models.currency import Currency, ExchangeRate
from app.models.location import Location
from app.models.account import AccountType, Account
from app.models.category import Category
from app.models.budget import Budget
from app.models.operation import OperationType, Operation, OperationRelation
from app.models.calendar import CalendarEvent
__all__ = [
"Role", "User",
"Currency", "ExchangeRate",
"Location",
"AccountType", "Account",
"Category",
"Budget",
"OperationType", "Operation", "OperationRelation",
"CalendarEvent",
]

View File

@@ -0,0 +1,42 @@
from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, Boolean, Text, Numeric, ForeignKey, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class AccountType(Base):
__tablename__ = "account_types"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
icon: Mapped[str | None] = mapped_column(String(50))
description: Mapped[str | None] = mapped_column(Text)
accounts: Mapped[list["Account"]] = relationship("Account", back_populates="account_type")
class Account(Base):
__tablename__ = "accounts"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
account_type_id: Mapped[int] = mapped_column(ForeignKey("account_types.id"), nullable=False)
currency_id: Mapped[int] = mapped_column(ForeignKey("currencies.id"), nullable=False)
balance: Mapped[Decimal] = mapped_column(Numeric(20, 2), default=0, nullable=False)
initial_balance: Mapped[Decimal] = mapped_column(Numeric(20, 2), default=0, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
credit_limit: Mapped[Decimal | None] = mapped_column(Numeric(20, 2))
description: Mapped[str | None] = mapped_column(Text)
icon: Mapped[str | None] = mapped_column(String(50))
color: Mapped[str | None] = mapped_column(String(7)) # hex #RRGGBB
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
user: Mapped["User"] = relationship("User", back_populates="accounts")
account_type: Mapped["AccountType"] = relationship("AccountType", back_populates="accounts")
currency: Mapped["Currency"] = relationship("Currency", back_populates="accounts")
operations: Mapped[list] = relationship("Operation", back_populates="account")

View File

@@ -0,0 +1,25 @@
from datetime import datetime, date
from decimal import Decimal
from sqlalchemy import String, Boolean, Numeric, ForeignKey, Date, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Budget(Base):
__tablename__ = "budgets"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"), nullable=False)
name: Mapped[str] = mapped_column(String(200), nullable=False)
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
currency_id: Mapped[int] = mapped_column(ForeignKey("currencies.id"), nullable=False)
period_type: Mapped[str] = mapped_column(String(20), nullable=False) # monthly | weekly | yearly | custom
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date | None] = mapped_column(Date)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User", back_populates="budgets")
category: Mapped["Category"] = relationship("Category", back_populates="budgets")
currency: Mapped["Currency"] = relationship("Currency", back_populates="budgets")

View File

@@ -0,0 +1,34 @@
from datetime import datetime, date, time
from decimal import Decimal
from sqlalchemy import String, Boolean, Text, Numeric, ForeignKey, Date, Time, DateTime, Integer, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class CalendarEvent(Base):
__tablename__ = "calendar_events"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
event_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
event_time: Mapped[time | None] = mapped_column(Time)
is_recurring: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
recurrence_pattern: Mapped[str | None] = mapped_column(String(50)) # daily | weekly | monthly | yearly
recurrence_end_date: Mapped[date | None] = mapped_column(Date)
related_operation_id: Mapped[int | None] = mapped_column(ForeignKey("operations.id"), nullable=True)
category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"), nullable=True)
amount: Mapped[Decimal | None] = mapped_column(Numeric(20, 2))
currency_id: Mapped[int | None] = mapped_column(ForeignKey("currencies.id"), nullable=True)
reminder_days: Mapped[int | None] = mapped_column(Integer)
is_completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
user: Mapped["User"] = relationship("User", back_populates="calendar_events")
related_operation: Mapped["Operation | None"] = relationship("Operation", back_populates="calendar_events")
category: Mapped["Category | None"] = relationship("Category", back_populates="calendar_events")
currency: Mapped["Currency | None"] = relationship("Currency", back_populates="calendar_events")

View File

@@ -0,0 +1,26 @@
from datetime import datetime
from sqlalchemy import String, ForeignKey, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Category(Base):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
type: Mapped[str] = mapped_column(String(20), nullable=False) # income | expense | transfer
parent_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"), nullable=True)
icon: Mapped[str | None] = mapped_column(String(50))
color: Mapped[str | None] = mapped_column(String(7))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User | None"] = relationship("User", back_populates="categories")
parent: Mapped["Category | None"] = relationship(
"Category", remote_side="Category.id", back_populates="children"
)
children: Mapped[list["Category"]] = relationship("Category", back_populates="parent")
operations: Mapped[list] = relationship("Operation", back_populates="category")
budgets: Mapped[list] = relationship("Budget", back_populates="category")
calendar_events: Mapped[list] = relationship("CalendarEvent", back_populates="category")

View File

@@ -0,0 +1,49 @@
from datetime import datetime, date
from decimal import Decimal
from sqlalchemy import String, Boolean, Numeric, ForeignKey, Date, DateTime, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Currency(Base):
__tablename__ = "currencies"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(String(3), unique=True, nullable=False) # ISO 4217
name: Mapped[str] = mapped_column(String(100), nullable=False)
symbol: Mapped[str | None] = mapped_column(String(10))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
exchange_rates_from: Mapped[list["ExchangeRate"]] = relationship(
"ExchangeRate", foreign_keys="ExchangeRate.currency_from_id", back_populates="currency_from"
)
exchange_rates_to: Mapped[list["ExchangeRate"]] = relationship(
"ExchangeRate", foreign_keys="ExchangeRate.currency_to_id", back_populates="currency_to"
)
accounts: Mapped[list] = relationship("Account", back_populates="currency")
operations: Mapped[list] = relationship("Operation", back_populates="currency")
budgets: Mapped[list] = relationship("Budget", back_populates="currency")
calendar_events: Mapped[list] = relationship("CalendarEvent", back_populates="currency")
class ExchangeRate(Base):
__tablename__ = "exchange_rates"
__table_args__ = (
UniqueConstraint("currency_from_id", "currency_to_id", "rate_date", name="uq_exchange_rate_day"),
)
id: Mapped[int] = mapped_column(primary_key=True)
currency_from_id: Mapped[int] = mapped_column(ForeignKey("currencies.id"), nullable=False)
currency_to_id: Mapped[int] = mapped_column(ForeignKey("currencies.id"), nullable=False)
rate: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False)
rate_date: Mapped[date] = mapped_column(Date, nullable=False)
source: Mapped[str | None] = mapped_column(String(100))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
currency_from: Mapped["Currency"] = relationship(
"Currency", foreign_keys=[currency_from_id], back_populates="exchange_rates_from"
)
currency_to: Mapped["Currency"] = relationship(
"Currency", foreign_keys=[currency_to_id], back_populates="exchange_rates_to"
)

View File

@@ -0,0 +1,26 @@
from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, Text, Numeric, ForeignKey, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Location(Base):
__tablename__ = "locations"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) # None = system location
name: Mapped[str] = mapped_column(String(200), nullable=False)
type: Mapped[str | None] = mapped_column(String(50)) # store, bank, atm, online, other
address: Mapped[str | None] = mapped_column(Text)
city: Mapped[str | None] = mapped_column(String(100))
country: Mapped[str | None] = mapped_column(String(100))
latitude: Mapped[Decimal | None] = mapped_column(Numeric(9, 6))
longitude: Mapped[Decimal | None] = mapped_column(Numeric(9, 6))
parent_id: Mapped[int | None] = mapped_column(ForeignKey("locations.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User | None"] = relationship("User", back_populates="locations")
parent: Mapped["Location | None"] = relationship("Location", remote_side="Location.id", back_populates="children")
children: Mapped[list["Location"]] = relationship("Location", back_populates="parent")
operations: Mapped[list] = relationship("Operation", back_populates="location")

View File

@@ -0,0 +1,70 @@
from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, Boolean, Text, Numeric, ForeignKey, DateTime, func, ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class OperationType(Base):
__tablename__ = "operation_types"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) # income | expense | transfer
description: Mapped[str | None] = mapped_column(Text)
operations: Mapped[list["Operation"]] = relationship("Operation", back_populates="operation_type")
class Operation(Base):
__tablename__ = "operations"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"), nullable=False, index=True)
category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"), nullable=True)
operation_type_id: Mapped[int] = mapped_column(ForeignKey("operation_types.id"), nullable=False)
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
currency_id: Mapped[int] = mapped_column(ForeignKey("currencies.id"), nullable=False)
amount_converted: Mapped[Decimal | None] = mapped_column(Numeric(20, 2)) # in account currency
exchange_rate: Mapped[Decimal | None] = mapped_column(Numeric(20, 6))
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
description: Mapped[str | None] = mapped_column(Text)
location_id: Mapped[int | None] = mapped_column(ForeignKey("locations.id"), nullable=True)
is_planned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
user: Mapped["User"] = relationship("User", back_populates="operations")
account: Mapped["Account"] = relationship("Account", back_populates="operations")
category: Mapped["Category | None"] = relationship("Category", back_populates="operations")
operation_type: Mapped["OperationType"] = relationship("OperationType", back_populates="operations")
currency: Mapped["Currency"] = relationship("Currency", back_populates="operations")
location: Mapped["Location | None"] = relationship("Location", back_populates="operations")
relations_from: Mapped[list["OperationRelation"]] = relationship(
"OperationRelation", foreign_keys="OperationRelation.operation_from_id", back_populates="operation_from"
)
relations_to: Mapped[list["OperationRelation"]] = relationship(
"OperationRelation", foreign_keys="OperationRelation.operation_to_id", back_populates="operation_to"
)
calendar_events: Mapped[list] = relationship("CalendarEvent", back_populates="related_operation")
class OperationRelation(Base):
"""Links two operations — e.g., the debit and credit legs of a transfer."""
__tablename__ = "operation_relations"
id: Mapped[int] = mapped_column(primary_key=True)
operation_from_id: Mapped[int] = mapped_column(ForeignKey("operations.id"), nullable=False)
operation_to_id: Mapped[int] = mapped_column(ForeignKey("operations.id"), nullable=False)
relation_type: Mapped[str] = mapped_column(String(50), nullable=False) # transfer | split | correction
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
operation_from: Mapped["Operation"] = relationship(
"Operation", foreign_keys=[operation_from_id], back_populates="relations_from"
)
operation_to: Mapped["Operation"] = relationship(
"Operation", foreign_keys=[operation_to_id], back_populates="relations_to"
)

View File

@@ -0,0 +1,38 @@
from datetime import datetime
from sqlalchemy import String, Boolean, ForeignKey, DateTime, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
description: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
users: Mapped[list["User"]] = relationship("User", back_populates="role")
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
role: Mapped["Role"] = relationship("Role", back_populates="users")
accounts: Mapped[list] = relationship("Account", back_populates="user")
operations: Mapped[list] = relationship("Operation", back_populates="user")
budgets: Mapped[list] = relationship("Budget", back_populates="user")
categories: Mapped[list] = relationship("Category", back_populates="user")
calendar_events: Mapped[list] = relationship("CalendarEvent", back_populates="user")
locations: Mapped[list] = relationship("Location", back_populates="user")