import datetime import decimal import typing from sqlalchemy import Column, ForeignKey, Numeric, Table from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship TABLE_PREFIX = "aps_" class Base(DeclarativeBase): pass class Account(Base): __tablename__ = TABLE_PREFIX + "account" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(nullable=False, unique=True) users: Mapped[list["User"]] = relationship( "User", secondary=TABLE_PREFIX + "user_account_association", back_populates="accounts", ) transactions: Mapped[list["Transaction"]] = relationship("Transaction") @property def balance(self): return sum(t.total_amount for t in self.transactions) user_account_association = Table( TABLE_PREFIX + "user_account_association", Base.metadata, Column("user_id", ForeignKey(TABLE_PREFIX + "user.id"), primary_key=True), Column("account_id", ForeignKey(TABLE_PREFIX + "account.id"), primary_key=True), ) class User(Base): __tablename__ = TABLE_PREFIX + "user" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(nullable=False, unique=True) display_name: Mapped[str] = mapped_column(nullable=False) accounts: Mapped[list["Account"]] = relationship( "Account", secondary=user_account_association, back_populates="users" ) class Area(Base): __tablename__ = TABLE_PREFIX + "area" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(nullable=False, unique=True) description: Mapped[str] = mapped_column(nullable=True) image_path: Mapped[str] = mapped_column(nullable=True) products: Mapped[list["Product"]] = relationship("Product") UnitsOfMeasure = typing.Literal[ "g", "kg", "piece", ] class Product(Base): __tablename__ = TABLE_PREFIX + "product" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(nullable=False, unique=True) price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2)) unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False) # TODO: limit this to actually used vat rates? vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2)) area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id")) area: Mapped["Area"] = relationship("Area", back_populates="products") image_path: Mapped[str] = mapped_column(nullable=True) TransactionTypes = typing.Literal[ "product", "deposit", "withdrawal", "expense", ] class Transaction(Base): __tablename__ = TABLE_PREFIX + "transaction" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) type: Mapped[TransactionTypes] = mapped_column(nullable=False) quantity: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2), nullable=True) timestamp: Mapped[datetime.datetime] = mapped_column( nullable=False, default=datetime.datetime.now() ) total_amount: Mapped[decimal.Decimal] = mapped_column( Numeric(10, 2), nullable=False ) product_id: Mapped[int] = mapped_column( ForeignKey(TABLE_PREFIX + "product.id"), nullable=True ) product: Mapped["Product"] = relationship("Product") account_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "account.id")) account: Mapped["Account"] = relationship("Account", back_populates="transactions")