diff --git a/api/app/account/models.py b/api/app/account/models.py index 3b04933..b05d67a 100644 --- a/api/app/account/models.py +++ b/api/app/account/models.py @@ -1,21 +1,55 @@ +from typing import Optional from uuid import UUID, uuid4 from enum import Enum -from sqlmodel import Field, SQLModel, select +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 class AccountBase(SQLModel): name: str = Field(index=True) - type: 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) + type: str = Field(index=True) + path: str = Field(index=True) class Account(AccountBaseId, table=True): + parent_account: Optional["Account"] = Relationship( + back_populates="children_accounts", + sa_relationship_kwargs=dict(remote_side='Account.id') + ) + children_accounts: list["Account"] = Relationship(back_populates='parent_account') + + def get_child_path(self): + return f"{self.path}/{self.name}" + + def get_root_path(self): + root = "/Categories" if self.is_category() else "/Accounts" + return f"{root}/{self.type}" + + def is_category(self): + return self.type in [v.value for v in CategoryType] + + def get_path(self, session): + if self.parent_account_id is None: + return self.get_root_path() + + parent = self.get(session, self.parent_account_id) + return parent.get_child_path() + + @classmethod + def schema_to_model(cls, session, schema): + model = cls.model_validate(schema) + model.path = model.get_path(session) + return model @classmethod def create(cls, account, session): - account_db = cls.model_validate(account) + account_db = cls.schema_to_model(session, account) session.add(account_db) session.commit() session.refresh(account_db) @@ -42,10 +76,19 @@ class Account(AccountBaseId, table=True): def get(cls, session, account_id): return session.get(Account, account_id) + def update_children_path(self, session): + for child in self.children_accounts: + child.path = self.get_child_path() + session.add(child) + child.update_children_path(session) + @classmethod def update(cls, session, account_db, account_data): - account_db.sqlmodel_update(cls.model_validate(account_data)) + previous_path = account_db.path + account_db.sqlmodel_update(cls.schema_to_model(session, account_data)) session.add(account_db) + if previous_path != account_db.path: + account_db.update_children_path(session, account_db) session.commit() session.refresh(account_db) return account_db @@ -84,15 +127,25 @@ class Asset(Enum): Checkings = "Checkings" Savings = "Savings" Cash = "Cash" + Investment = "Investment" class Liability(Enum): Liability = "Liability" CreditCard = "CreditCard" Loan = "Loan" - Investment = "Investment" class AccountWrite(AccountBase): type: Asset | Liability = Field() + path: SkipJsonSchema[str] = Field(default="") + parent_account_id: UUID = PydField(default=None, json_schema_extra={ + "foreign_key": { + "reference": { + "resource": "accounts", + "schema": "AccountRead", + "label": "name" + } + } + }) class AccountCreate(AccountWrite): pass @@ -111,4 +164,11 @@ class CategoryCreate(CategoryWrite): pass class CategoryUpdate(CategoryWrite): - pass \ No newline at end of file + pass + +class AccountFilter(Filter): + name__like: Optional[str] = None + + class Constants(Filter.Constants): + model = Account + search_model_fields = ["name"] diff --git a/api/app/payee/models.py b/api/app/payee/models.py index ef24960..ac784e8 100644 --- a/api/app/payee/models.py +++ b/api/app/payee/models.py @@ -16,7 +16,7 @@ class PayeeBaseId(PayeeBase): id: UUID | None = Field(default_factory=uuid4, primary_key=True) class Payee(PayeeBaseId, table=True): - default_account: Account | None = Relationship() + default_account: Optional[Account] = Relationship() @classmethod def create(cls, payee, session): diff --git a/gui/app/src/common/crud/widgets/foreign-key.tsx b/gui/app/src/common/crud/widgets/foreign-key.tsx index 3c62c70..3fe2650 100644 --- a/gui/app/src/common/crud/widgets/foreign-key.tsx +++ b/gui/app/src/common/crud/widgets/foreign-key.tsx @@ -10,7 +10,7 @@ export const ForeignKeyWidget = (props: WidgetProps) => { const valueResult = useOne({ resource: resource, - id: props.value + id: props.value != "" ? props.value : undefined }); const [inputValue, setInputValue] = useState(""); @@ -42,6 +42,7 @@ export const ForeignKeyWidget = (props: WidgetProps) => { onChange={(event, newValue) => { setSelectedValue(newValue) props.onChange(newValue ? newValue.id : "") + return true }} //inputValue={inputValue} onInputChange={(event, newInputValue) => setInputValue(newInputValue)} diff --git a/gui/app/src/pages/accounts/list.tsx b/gui/app/src/pages/accounts/list.tsx index f6992a9..ad74c07 100644 --- a/gui/app/src/pages/accounts/list.tsx +++ b/gui/app/src/pages/accounts/list.tsx @@ -15,6 +15,7 @@ export const AccountList: React.FC = () => { () => [ { field: "id", headerName: "ID" }, { field: "name", headerName: "Name", flex: 1 }, + { field: "path", headerName: "path", flex: 1 }, { field: "type", headerName: "Type", flex: 0.3 }, { field: "actions",