Separating Enums, Schemas and Models in account

This commit is contained in:
2025-02-04 22:59:59 +01:00
parent a4be703713
commit f26bd9846a
7 changed files with 144 additions and 118 deletions

View File

@@ -1,14 +1,26 @@
from typing import Optional
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from fastapi_filter import FilterDepends from fastapi_filter import FilterDepends
from fastapi_filter.contrib.sqlalchemy import Filter
from fastapi_pagination import Page from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate from fastapi_pagination.ext.sqlmodel import paginate
from account.models import Account, AccountCreate, AccountRead, AccountUpdate, AccountFilters from account.schemas import AccountCreate, AccountRead, AccountUpdate
from account.models import Account
from db import SessionDep from db import SessionDep
from user.manager import get_current_user from user.manager import get_current_user
class AccountFilters(Filter):
name__like: Optional[str] = None
order_by: Optional[list[str]] = None
class Constants(Filter.Constants):
model = Account
search_model_fields = ["name"]
router = APIRouter() router = APIRouter()
@router.post("") @router.post("")

View File

@@ -5,7 +5,9 @@ from fastapi_filter import FilterDepends
from fastapi_pagination import Page from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate from fastapi_pagination.ext.sqlmodel import paginate
from account.models import Account, AccountRead, CategoryCreate, CategoryUpdate, AccountFilters from account.account_routes import AccountFilters
from account.schemas import AccountRead, CategoryCreate, CategoryUpdate
from account.models import Account
from db import SessionDep from db import SessionDep
from user.manager import get_current_user from user.manager import get_current_user

43
api/app/account/enums.py Normal file
View File

@@ -0,0 +1,43 @@
from enum import Enum
class AccountType(Enum):
Asset = "Asset" # < Denotes a generic asset account.
Checkings = "Checkings" # < Standard checking account
Savings = "Savings" # < Typical savings account
Cash = "Cash" # < Denotes a shoe-box or pillowcase stuffed with cash
Liability = "Liability" # < Denotes a generic liability account.
CreditCard = "CreditCard" # < Credit card accounts
Loan = "Loan" # < Loan and mortgage accounts (liability)
CertificateDep = "CertificateDep" # < Certificates of Deposit
Investment = "Investment" # < Investment account
MoneyMarket = "MoneyMarket" # < Money Market Account
Currency = "Currency" # < Denotes a currency trading 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
Income = "Income" # < Denotes an income account
Expense = "Expense" # < Denotes an expense account
Equity = "Equity" # < Denotes an equity account e.g. opening/closing balance
class AccountFamily(Enum):
Asset = "Asset"
Liability = "Liability"
class CategoryFamily(Enum):
Income = "Income"
Expense = "Expense"
class Asset(Enum):
Asset = "Asset"
Checking = "Checking"
Savings = "Savings"
Cash = "Cash"
Investment = "Investment"
class Liability(Enum):
Liability = "Liability"
CreditCard = "CreditCard"
Loan = "Loan"

View File

@@ -1,23 +1,10 @@
from typing import Optional from typing import Optional
from uuid import UUID, uuid4
from enum import Enum
from fastapi_filter.contrib.sqlalchemy import Filter from sqlmodel import select, Relationship
from pydantic.json_schema import SkipJsonSchema
from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField
from sqlalchemy.sql import text from sqlalchemy.sql import text
from account.enums import CategoryFamily, Asset, Liability, AccountFamily
class AccountBase(SQLModel): from account.schemas import AccountBaseId
name: str = Field(index=True)
parent_account_id: Optional[UUID] = Field(default=None, foreign_key="account.id")
class AccountBaseId(AccountBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
family: str = Field(index=True)
type: str = Field(index=True)
path: str = Field(index=True)
class Account(AccountBaseId, table=True): class Account(AccountBaseId, table=True):
parent_account: Optional["Account"] = Relationship( parent_account: Optional["Account"] = Relationship(
@@ -38,7 +25,7 @@ class Account(AccountBaseId, table=True):
session.exec(text(request)) session.exec(text(request))
def is_category(self): def is_category(self):
return self.type in [v.value for v in CategoryType] return self.family in [v.value for v in CategoryFamily]
def get_parent(self, session): def get_parent(self, session):
if self.parent_account_id is None: if self.parent_account_id is None:
@@ -47,19 +34,23 @@ class Account(AccountBaseId, table=True):
self.parent_account = self.get(session, self.parent_account_id) self.parent_account = self.get(session, self.parent_account_id)
return self.parent_account return self.parent_account
def get_path(self, session): def compute_path(self, session):
if self.parent_account_id is None: if self.parent_account_id is None:
return self.get_root_path() self.path = self.get_root_path()
else:
self.parent_account = self.get(session, self.parent_account_id) self.parent_account = self.get(session, self.parent_account_id)
return self.parent_account.get_child_path() self.path = self.parent_account.get_child_path()
def get_family(self): return self.path
def compute_family(self):
if self.type in Asset: if self.type in Asset:
return "Asset" self.family = AccountFamily.Asset
if self.type in Liability: elif self.type in Liability:
return "Liability" self.family = AccountFamily.Liability
return self.type else:
self.family = self.type
return self.family
@classmethod @classmethod
def schema_to_model(cls, session, schema, model=None): def schema_to_model(cls, session, schema, model=None):
@@ -73,24 +64,23 @@ class Account(AccountBaseId, table=True):
except Exception as e: except Exception as e:
print(e) print(e)
model.family = model.get_family() model.compute_family()
cls.validate_parent(session, model) model.validate_parent(session)
model.path = model.get_path(session) model.path = model.get_path(session)
return model return model
@classmethod def validate_parent(self, session):
def validate_parent(cls, session, model): if self.parent_account_id is None:
if model.parent_account_id is None:
return True return True
parent = model.get_parent(session) parent = self.get_parent(session)
if not parent: if not parent:
raise ValueError("Parent account not found.") raise KeyError("Parent account not found.")
if parent.family != model.family: if parent.family != self.family:
raise ValueError("Account family mismatch with parent account..") raise ValueError("Account family mismatch with parent account..")
if parent.path.startswith(model.path): if parent.path.startswith(self.path):
raise ValueError("Parent Account is descendant") raise ValueError("Parent Account is descendant")
return True return True
@@ -99,6 +89,9 @@ class Account(AccountBaseId, table=True):
def create(cls, account, session): def create(cls, account, session):
account_db = cls.schema_to_model(session, account) account_db = cls.schema_to_model(session, account)
session.add(account_db) session.add(account_db)
session.flush()
session.refresh(account_db)
session.commit() session.commit()
session.refresh(account_db) session.refresh(account_db)
@@ -165,82 +158,3 @@ class Account(AccountBaseId, table=True):
session.delete(account) session.delete(account)
session.commit() session.commit()
class AccountRead(AccountBaseId):
pass
class AccountType(Enum):
Asset = "Asset" # < Denotes a generic asset account.
Checkings = "Checkings" # < Standard checking account
Savings = "Savings" # < Typical savings account
Cash = "Cash" # < Denotes a shoe-box or pillowcase stuffed with cash
Liability = "Liability" # < Denotes a generic liability account.
CreditCard = "CreditCard" # < Credit card accounts
Loan = "Loan" # < Loan and mortgage accounts (liability)
CertificateDep = "CertificateDep" # < Certificates of Deposit
Investment = "Investment" # < Investment account
MoneyMarket = "MoneyMarket" # < Money Market Account
Currency = "Currency" # < Denotes a currency trading 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
Income = "Income" # < Denotes an income account
Expense = "Expense" # < Denotes an expense account
Equity = "Equity" # < Denotes an equity account e.g. opening/closing balance
class Asset(Enum):
Asset = "Asset"
Checkings = "Checkings"
Savings = "Savings"
Cash = "Cash"
Investment = "Investment"
class Liability(Enum):
Liability = "Liability"
CreditCard = "CreditCard"
Loan = "Loan"
class BaseAccountWrite(AccountBase):
path: SkipJsonSchema[str] = Field(default="")
family: SkipJsonSchema[str] = Field(default="")
class AccountWrite(BaseAccountWrite):
type: Asset | Liability = Field()
parent_account_id: UUID | None = PydField(default=None, json_schema_extra={
"foreign_key": {
"reference": {
"resource": "accounts",
"schema": "AccountRead",
"label": "name"
}
}
})
class AccountCreate(AccountWrite):
pass
class AccountUpdate(AccountWrite):
pass
class CategoryType(Enum):
Income = "Income"
Expense = "Expense"
class CategoryWrite(BaseAccountWrite):
type: CategoryType = Field()
class CategoryCreate(CategoryWrite):
pass
class CategoryUpdate(CategoryWrite):
pass
class AccountFilters(Filter):
name__like: Optional[str] = None
order_by: Optional[list[str]] = None
class Constants(Filter.Constants):
model = Account
search_model_fields = ["name"]

View File

@@ -0,0 +1,53 @@
from typing import Optional
from uuid import UUID, uuid4
from pydantic.json_schema import SkipJsonSchema
from sqlmodel import Field, SQLModel
from pydantic import Field as PydField
from account.enums import Asset, Liability, CategoryFamily
class AccountBase(SQLModel):
name: str = Field(index=True)
parent_account_id: Optional[UUID] = Field(default=None, foreign_key="account.id")
class AccountBaseId(AccountBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
family: str = Field(index=True)
type: str = Field(index=True)
path: str = Field(index=True)
class AccountRead(AccountBaseId):
pass
class BaseAccountWrite(AccountBase):
path: SkipJsonSchema[str] = Field(default="")
family: SkipJsonSchema[str] = Field(default="")
class AccountWrite(BaseAccountWrite):
type: Asset | Liability = Field()
parent_account_id: UUID | None = PydField(default=None, json_schema_extra={
"foreign_key": {
"reference": {
"resource": "accounts",
"schema": "AccountRead",
"label": "name"
}
}
})
class AccountCreate(AccountWrite):
pass
class AccountUpdate(AccountWrite):
pass
class CategoryWrite(BaseAccountWrite):
type: CategoryFamily = Field()
class CategoryCreate(CategoryWrite):
pass
class CategoryUpdate(CategoryWrite):
pass

View File

@@ -5,7 +5,8 @@ from fastapi_filter.contrib.sqlalchemy import Filter
from sqlmodel import Field, SQLModel, select, Relationship from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField from pydantic import Field as PydField
from account.models import Account, AccountRead from account.models import Account
from account.schemas import AccountRead
class PayeeBase(SQLModel): class PayeeBase(SQLModel):

View File

@@ -4,7 +4,8 @@ from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel, select, Relationship from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField from pydantic import Field as PydField
from account.models import Account, AccountRead from account.models import Account
from account.schemas import AccountRead
from payee.models import Payee, PayeeRead from payee.models import Payee, PayeeRead