From f26bd9846a2d9b2615cf82f54293eb35bed2d658 Mon Sep 17 00:00:00 2001 From: ewandor Date: Tue, 4 Feb 2025 22:59:59 +0100 Subject: [PATCH] Separating Enums, Schemas and Models in account --- api/app/account/account_routes.py | 14 ++- api/app/account/category_routes.py | 4 +- api/app/account/enums.py | 43 +++++++++ api/app/account/models.py | 142 ++++++----------------------- api/app/account/schemas.py | 53 +++++++++++ api/app/payee/models.py | 3 +- api/app/transaction/models.py | 3 +- 7 files changed, 144 insertions(+), 118 deletions(-) create mode 100644 api/app/account/enums.py create mode 100644 api/app/account/schemas.py diff --git a/api/app/account/account_routes.py b/api/app/account/account_routes.py index 6a8623d..0d5f36a 100644 --- a/api/app/account/account_routes.py +++ b/api/app/account/account_routes.py @@ -1,14 +1,26 @@ +from typing import Optional from uuid import UUID from fastapi import APIRouter, HTTPException, Depends from fastapi_filter import FilterDepends +from fastapi_filter.contrib.sqlalchemy import Filter from fastapi_pagination import Page 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 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.post("") diff --git a/api/app/account/category_routes.py b/api/app/account/category_routes.py index 2c568e6..706a5d7 100644 --- a/api/app/account/category_routes.py +++ b/api/app/account/category_routes.py @@ -5,7 +5,9 @@ from fastapi_filter import FilterDepends from fastapi_pagination import Page 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 user.manager import get_current_user diff --git a/api/app/account/enums.py b/api/app/account/enums.py new file mode 100644 index 0000000..18e4ae6 --- /dev/null +++ b/api/app/account/enums.py @@ -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" \ No newline at end of file diff --git a/api/app/account/models.py b/api/app/account/models.py index ce8a2d0..75be0e8 100644 --- a/api/app/account/models.py +++ b/api/app/account/models.py @@ -1,23 +1,10 @@ from typing import Optional -from uuid import UUID, uuid4 -from enum import Enum -from fastapi_filter.contrib.sqlalchemy import Filter -from pydantic.json_schema import SkipJsonSchema -from sqlmodel import Field, SQLModel, select, Relationship -from pydantic import Field as PydField +from sqlmodel import select, Relationship from sqlalchemy.sql import text - -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) +from account.enums import CategoryFamily, Asset, Liability, AccountFamily +from account.schemas import AccountBaseId class Account(AccountBaseId, table=True): parent_account: Optional["Account"] = Relationship( @@ -38,7 +25,7 @@ class Account(AccountBaseId, table=True): session.exec(text(request)) 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): 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) return self.parent_account - def get_path(self, session): + def compute_path(self, session): 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.path = self.parent_account.get_child_path() - self.parent_account = self.get(session, self.parent_account_id) - return self.parent_account.get_child_path() + return self.path - def get_family(self): + def compute_family(self): if self.type in Asset: - return "Asset" - if self.type in Liability: - return "Liability" - return self.type + self.family = AccountFamily.Asset + elif self.type in Liability: + self.family = AccountFamily.Liability + else: + self.family = self.type + return self.family @classmethod def schema_to_model(cls, session, schema, model=None): @@ -73,24 +64,23 @@ class Account(AccountBaseId, table=True): except Exception as e: print(e) - model.family = model.get_family() - cls.validate_parent(session, model) + model.compute_family() + model.validate_parent(session) model.path = model.get_path(session) return model - @classmethod - def validate_parent(cls, session, model): - if model.parent_account_id is None: + def validate_parent(self, session): + if self.parent_account_id is None: return True - parent = model.get_parent(session) + parent = self.get_parent(session) 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..") - if parent.path.startswith(model.path): + if parent.path.startswith(self.path): raise ValueError("Parent Account is descendant") return True @@ -99,6 +89,9 @@ class Account(AccountBaseId, table=True): def create(cls, account, session): account_db = cls.schema_to_model(session, account) session.add(account_db) + session.flush() + session.refresh(account_db) + session.commit() session.refresh(account_db) @@ -165,82 +158,3 @@ class Account(AccountBaseId, table=True): session.delete(account) 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"] diff --git a/api/app/account/schemas.py b/api/app/account/schemas.py new file mode 100644 index 0000000..d134fed --- /dev/null +++ b/api/app/account/schemas.py @@ -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 diff --git a/api/app/payee/models.py b/api/app/payee/models.py index ac784e8..56b188c 100644 --- a/api/app/payee/models.py +++ b/api/app/payee/models.py @@ -5,7 +5,8 @@ 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 +from account.models import Account +from account.schemas import AccountRead class PayeeBase(SQLModel): diff --git a/api/app/transaction/models.py b/api/app/transaction/models.py index fe19ad4..81bb163 100644 --- a/api/app/transaction/models.py +++ b/api/app/transaction/models.py @@ -4,7 +4,8 @@ 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 account.models import Account +from account.schemas import AccountRead from payee.models import Payee, PayeeRead