Changing Account/category structure and Adding transactions and splits

This commit is contained in:
2025-01-25 01:56:41 +01:00
parent 38c5a69130
commit cda36315c3
20 changed files with 424 additions and 160 deletions

View File

@@ -1,11 +1,7 @@
from uuid import UUID, uuid4
from enum import Enum
from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField
from category.models import CategoryRead, Category
from sqlmodel import Field, SQLModel, select
class AccountType(Enum):
Asset = "Asset" # < Denotes a generic asset account.
@@ -21,24 +17,21 @@ class AccountType(Enum):
MoneyMarket = "MoneyMarket" # < Money Market Account
Currency = "Currency" # < Denotes a currency trading account.
Income = "Income" # < Denotes an income account
Expense = "Expense" # < Denotes an expense account
AssetLoan = "AssetLoan" # < Denotes a loan (asset of the owner of this object)
Stock = "Stock" # < Denotes an security account as sub-account for an investment
Equity = "Equity" # < Denotes an equity account e.g. opening/closing balance
Payee = "Payee"
Income = "Income" # < Denotes an income account
Expense = "Expense" # < Denotes an expense account
class AccountBase(SQLModel):
name: str = Field(index=True)
type: AccountType = Field(index=True)
default_category_id: UUID | None = Field(default=None, foreign_key="category.id")
class AccountBaseId(AccountBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
class Account(AccountBaseId, table=True):
default_category: Category | None = Relationship()
@classmethod
def create(cls, account, session):
@@ -51,7 +44,7 @@ class Account(AccountBaseId, table=True):
@classmethod
def list(cls):
return select(Account).join(Category)
return select(Account)
@classmethod
def get(cls, session, account_id):
@@ -71,18 +64,10 @@ class Account(AccountBaseId, table=True):
session.commit()
class AccountRead(AccountBaseId):
default_category: CategoryRead
pass
class AccountWrite(AccountBase):
default_category_id: UUID = PydField(default=None, json_schema_extra={
"foreign_key": {
"reference": {
"resource": "categories",
"schema": "CategoryRead",
"label": "name"
}
}
})
pass
class AccountCreate(AccountWrite):
pass

View File

@@ -1,60 +0,0 @@
from uuid import UUID, uuid4
from enum import Enum
from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter
from sqlmodel import Field, SQLModel, select
class CategoryBase(SQLModel):
name: str = Field(index=True)
class CategoryRead(CategoryBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
class Category(CategoryRead, table=True):
@classmethod
def create(cls, category, session):
category_db = cls.model_validate(category)
session.add(category_db)
session.commit()
session.refresh(category_db)
return category_db
@classmethod
def list(cls, filters):
return filters.filter(select(cls))
@classmethod
def get(cls, session, category_id):
return session.get(Category, category_id)
@classmethod
def update(cls, session, category_db, category_data):
category_db.sqlmodel_update(category_data)
session.add(category_db)
session.commit()
session.refresh(category_db)
return category_db
@classmethod
def delete(cls, session, category):
session.delete(category)
session.commit()
class CategoryWrite(CategoryBase):
pass
class CategoryCreate(CategoryWrite):
pass
class CategoryUpdate(CategoryWrite):
pass
class CategoryFilters(Filter):
name__like: Optional[str] = None
class Constants(Filter.Constants):
model = Category
search_model_fields = ["name"]

View File

@@ -1,49 +0,0 @@
from uuid import UUID
from fastapi import APIRouter, HTTPException, Depends
from fastapi_filter import FilterDepends
from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate
from category.models import Category, CategoryCreate, CategoryRead, CategoryUpdate, CategoryFilters
from db import SessionDep
from user.manager import get_current_user
router = APIRouter()
@router.post("")
def create_category(category: CategoryCreate, session: SessionDep, current_user=Depends(get_current_user)) -> CategoryRead:
Category.create(category, session)
return category
@router.get("")
def read_categories(session: SessionDep,
filters: CategoryFilters = FilterDepends(CategoryFilters),
current_user=Depends(get_current_user)) -> Page[CategoryRead]:
return paginate(session, Category.list(filters))
@router.get("/{category_id}")
def read_category(category_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> CategoryRead:
category = Category.get(session, category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
return category
@router.put("/{category_id}")
def update_category(category_id: UUID, category: CategoryUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> CategoryRead:
db_category = Category.get(session, category_id)
if not db_category:
raise HTTPException(status_code=404, detail="Category not found")
category_data = category.model_dump(exclude_unset=True)
category = Category.update(session, db_category, category_data)
return category
@router.delete("/{category_id}")
def delete_category(category_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
category = Category.get(session, category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
Category.delete(session, category)
return {"ok": True}

View File

@@ -9,7 +9,8 @@ from fastapi_pagination import add_pagination
from db import create_db_and_tables
from user import user_router, auth_router, create_admin_account
from account.routes import router as account_router
from category.routes import router as category_router
from payee.routes import router as payee_router
from transaction.routes import router as transaction_router
@asynccontextmanager
@@ -38,7 +39,8 @@ app.add_middleware(
app.include_router(auth_router, prefix="/auth", tags=["auth"], )
app.include_router(user_router, prefix="/users", tags=["users"])
app.include_router(account_router, prefix="/accounts", tags=["accounts"])
app.include_router(category_router, prefix="/categories", tags=["categories"])
app.include_router(payee_router, prefix="/payees", tags=["payees"])
app.include_router(transaction_router, prefix="/transactions", tags=["transactions"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

76
api/app/payee/models.py Normal file
View File

@@ -0,0 +1,76 @@
from uuid import UUID, uuid4
from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter
from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField
from account.models import Account, AccountRead
class PayeeBase(SQLModel):
name: str = Field(index=True)
default_account_id: UUID | None = Field(default=None, foreign_key="account.id")
class PayeeBaseId(PayeeBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
class Payee(PayeeBaseId, table=True):
default_account: Account | None = Relationship()
@classmethod
def create(cls, payee, session):
payee_db = cls.model_validate(payee)
session.add(payee_db)
session.commit()
session.refresh(payee_db)
return payee_db
@classmethod
def list(cls, filters):
return filters.filter(select(cls)).join(Account)
@classmethod
def get(cls, session, payee_id):
return session.get(Payee, payee_id)
@classmethod
def update(cls, session, payee_db, payee_data):
payee_db.sqlmodel_update(payee_data)
session.add(payee_db)
session.commit()
session.refresh(payee_db)
return payee_db
@classmethod
def delete(cls, session, payee):
session.delete(payee)
session.commit()
class PayeeRead(PayeeBaseId):
default_account: AccountRead
class PayeeWrite(PayeeBase):
default_account_id: UUID = PydField(default=None, json_schema_extra={
"foreign_key": {
"reference": {
"resource": "accounts",
"schema": "AccountRead",
"label": "name"
}
}
})
class PayeeCreate(PayeeWrite):
pass
class PayeeUpdate(PayeeWrite):
pass
class PayeeFilters(Filter):
name__like: Optional[str] = None
class Constants(Filter.Constants):
model = Payee
search_model_fields = ["name"]

49
api/app/payee/routes.py Normal file
View File

@@ -0,0 +1,49 @@
from uuid import UUID
from fastapi import APIRouter, HTTPException, Depends
from fastapi_filter import FilterDepends
from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate
from payee.models import Payee, PayeeCreate, PayeeRead, PayeeUpdate, PayeeFilters
from db import SessionDep
from user.manager import get_current_user
router = APIRouter()
@router.post("")
def create_payee(payee: PayeeCreate, session: SessionDep, current_user=Depends(get_current_user)) -> PayeeRead:
result = Payee.create(payee, session)
return Payee.get(session, result.id)
@router.get("")
def read_categories(session: SessionDep,
filters: PayeeFilters = FilterDepends(PayeeFilters),
current_user=Depends(get_current_user)) -> Page[PayeeRead]:
return paginate(session, Payee.list(filters))
@router.get("/{payee_id}")
def read_payee(payee_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> PayeeRead:
payee = Payee.get(session, payee_id)
if not payee:
raise HTTPException(status_code=404, detail="Payee not found")
return payee
@router.put("/{payee_id}")
def update_payee(payee_id: UUID, payee: PayeeUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> PayeeRead:
db_payee = Payee.get(session, payee_id)
if not db_payee:
raise HTTPException(status_code=404, detail="Payee not found")
payee_data = payee.model_dump(exclude_unset=True)
payee = Payee.update(session, db_payee, payee_data)
return payee
@router.delete("/{payee_id}")
def delete_payee(payee_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
payee = Payee.get(session, payee_id)
if not payee:
raise HTTPException(status_code=404, detail="Payee not found")
Payee.delete(session, payee)
return {"ok": True}

View File

View File

@@ -0,0 +1,127 @@
from decimal import Decimal
from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField
from account.models import Account, AccountRead
from payee.models import Payee, PayeeRead
class TransactionBase(SQLModel):
pass
class TransactionBaseId(TransactionBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
class Transaction(TransactionBaseId, table=True):
splits: list["Split"] = Relationship(back_populates="transaction")
@classmethod
def create(cls, transaction, session):
transaction_db = cls.model_validate(transaction)
session.add(transaction_db)
session.commit()
session.refresh(transaction_db)
return transaction_db
@classmethod
def list(cls):
return select(Transaction).join(Split).join(Account)
@classmethod
def get(cls, session, transaction_id):
return session.get(Transaction, transaction_id)
@classmethod
def update(cls, session, transaction_db, transaction_data):
transaction_db.sqlmodel_update(transaction_data)
session.add(transaction_db)
session.commit()
session.refresh(transaction_db)
return transaction_db
@classmethod
def delete(cls, session, transaction):
session.delete(transaction)
session.commit()
class SplitBase(SQLModel):
account_id: UUID = Field(foreign_key="account.id")
payee_id: UUID = Field(foreign_key="payee.id")
amount: Decimal = Field(decimal_places=2)
class SplitBaseId(SplitBase):
transaction_id: UUID = Field(primary_key=True, foreign_key="transaction.id")
id: int = Field(primary_key=True)
class SplitRead(SplitBaseId):
account: AccountRead
payee: PayeeRead
class TransactionRead(TransactionBaseId):
splits: list[SplitRead]
class SplitWrite(SplitBase):
account_id: UUID = PydField(json_schema_extra={
"foreign_key": {
"reference": {
"resource": "accounts",
"schema": "AccountRead",
"label": "name"
}
}
})
payee_id: UUID = PydField(json_schema_extra={
"foreign_key": {
"reference": {
"resource": "payees",
"schema": "PayeeRead",
"label": "name"
}
}
})
class TransactionWrite(TransactionBase):
splits: list[SplitWrite] = Field()
class TransactionCreate(TransactionWrite):
pass
class TransactionUpdate(TransactionWrite):
pass
class Split(SplitBaseId, table=True):
transaction: Transaction = Relationship(back_populates="splits")
account: Account | None = Relationship()
payee: Payee | None = Relationship()
@classmethod
def create(cls, transaction_split, session):
transaction_split_db = cls.model_validate(transaction_split)
session.add(transaction_split_db)
session.commit()
session.refresh(transaction_split_db)
return transaction_split_db
@classmethod
def update(cls, session, transaction_db, transaction_data):
transaction_db.sqlmodel_update(transaction_data)
session.add(transaction_db)
session.commit()
session.refresh(transaction_db)
return transaction_db
@classmethod
def delete(cls, session, transaction):
session.delete(transaction)
session.commit()
class SplitCreate(SplitWrite):
pass
class SplitUpdate(SplitWrite):
pass

View File

@@ -0,0 +1,46 @@
from uuid import UUID
from fastapi import APIRouter, HTTPException, Depends
from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate
from transaction.models import Transaction, TransactionCreate, TransactionRead, TransactionUpdate
from db import SessionDep
from user.manager import get_current_user
router = APIRouter()
@router.post("")
def create_transaction(transaction: TransactionCreate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
result = Transaction.create(transaction, session)
return result
@router.get("")
def read_transactions(session: SessionDep, current_user=Depends(get_current_user)) -> Page[TransactionRead]:
return paginate(session, Transaction.list())
@router.get("/{transaction_id}")
def read_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
transaction = Transaction.get(session, transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
return transaction
@router.put("/{transaction_id}")
def update_transaction(transaction_id: UUID, transaction: TransactionUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
db_transaction = Transaction.get(session, transaction_id)
if not db_transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
transaction_data = transaction.model_dump(exclude_unset=True)
transaction = Transaction.update(session, db_transaction, transaction_data)
return transaction
@router.delete("/{transaction_id}")
def delete_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
transaction = Transaction.get(session, transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
Transaction.delete(session, transaction)
return {"ok": True}