first sketch; incomplete; WiP
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"month": "2026-10",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"category": "super-category-1",
|
||||||
|
"total_amount": 100,
|
||||||
|
"total_budget": 120,
|
||||||
|
"sub_categories": [
|
||||||
|
{
|
||||||
|
"category": "category-1",
|
||||||
|
"total_amount": 60,
|
||||||
|
"total_budget": 50,
|
||||||
|
"sub_categories": [],
|
||||||
|
"transactions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "category-2",
|
||||||
|
"total_amount": 40,
|
||||||
|
"total_budget": 60,
|
||||||
|
"sub_categories": [],
|
||||||
|
"transactions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transactions": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+34
-2
@@ -18,7 +18,8 @@ from sqlalchemy import (
|
|||||||
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
||||||
|
|
||||||
from src.db_utils import upsert
|
from src.db_utils import upsert
|
||||||
from src.models.common import BaseModelDb, ExportBaseModel, hashify
|
from src.models.common import Amount, AmountPos, BaseModelDb, ExportBaseModel, hashify
|
||||||
|
from src.models.transactions import Transaction
|
||||||
|
|
||||||
|
|
||||||
class UTCDateTime(TypeDecorator[datetime]):
|
class UTCDateTime(TypeDecorator[datetime]):
|
||||||
@@ -75,6 +76,9 @@ class Category(ExportBaseModel):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'Category.from_str("{self.__str__()}")'
|
return f'Category.from_str("{self.__str__()}")'
|
||||||
|
|
||||||
|
def get_budget(self, date_valid: date | None) -> float:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class CategoryDb(BaseModelDb):
|
class CategoryDb(BaseModelDb):
|
||||||
__tablename__ = "category"
|
__tablename__ = "category"
|
||||||
@@ -187,7 +191,7 @@ def upsert_category(
|
|||||||
|
|
||||||
class CategoryBudget(BaseModel):
|
class CategoryBudget(BaseModel):
|
||||||
category: Category
|
category: Category
|
||||||
amount_eur: Decimal = Field(gt=0, decimal_places=2)
|
amount_eur: AmountPos
|
||||||
valid_from: date | None = Field(
|
valid_from: date | None = Field(
|
||||||
default=None, description="Start of validity. None means unbounded"
|
default=None, description="Start of validity. None means unbounded"
|
||||||
)
|
)
|
||||||
@@ -228,3 +232,31 @@ class CategoryBudgetDb(BaseModelDb):
|
|||||||
|
|
||||||
# relationship
|
# relationship
|
||||||
category = relationship("CategoryDb", back_populates="budgets")
|
category = relationship("CategoryDb", back_populates="budgets")
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryTree(BaseModel):
|
||||||
|
category: Category
|
||||||
|
total_amount: float
|
||||||
|
total_budget: float
|
||||||
|
sub_categories: list[ CategoryTree ] | None
|
||||||
|
transactions: list[Transaction] | None
|
||||||
|
|
||||||
|
def get_total_amount(self) -> float:
|
||||||
|
result = 0.0
|
||||||
|
if self.transactions:
|
||||||
|
sum(transaction.amount for transaction in self.transactions )
|
||||||
|
if self.sub_categories:
|
||||||
|
sum(sub_category.get_total_amount() for sub_category in self.sub_categories)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_total_budget(self, date_valid: date) -> float:
|
||||||
|
result = 0.0
|
||||||
|
result += self.category.get_budget(date_valid)
|
||||||
|
if self.sub_categories:
|
||||||
|
result += sum(category.category.get_budget(date_valid) for category in self.sub_categories)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def build_category_tree(transactions: list[Transaction]) -> list[CategoryTree]:
|
||||||
|
result = []
|
||||||
|
for transaction in transactions:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import hashlib
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import Date, Numeric, String
|
from sqlalchemy import Date, Numeric, String
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
@@ -39,12 +40,15 @@ def date2str(value: date | None, default="--", date_format="%Y-%m-%d") -> str:
|
|||||||
return value.strftime(date_format)
|
return value.strftime(date_format)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
type AmountPos = Annotated[Decimal, Field(decimal_places=2, gt=0)]
|
||||||
|
type Amount = Annotated[Decimal, Field(decimal_places=2)]
|
||||||
|
|
||||||
def _parse_decimal_german(decimal_str: str) -> Decimal:
|
|
||||||
|
def _parse_decimal_german(decimal_str: str) -> Amount:
|
||||||
return Decimal(decimal_str.replace(".", "").replace(",", "."))
|
return Decimal(decimal_str.replace(".", "").replace(",", "."))
|
||||||
|
|
||||||
|
|
||||||
def decimal2str(value: Decimal) -> str:
|
def decimal2str(value: Amount) -> str:
|
||||||
value = Decimal(f"{value:.2f}")
|
value = Decimal(f"{value:.2f}")
|
||||||
int_part, _, frac = str(value).partition(".")
|
int_part, _, frac = str(value).partition(".")
|
||||||
groups: list[str] = []
|
groups: list[str] = []
|
||||||
@@ -60,6 +64,7 @@ type Csv2Dict = list[Csv2DictRow]
|
|||||||
type CsV2DictMulti = dict[str, Csv2Dict]
|
type CsV2DictMulti = dict[str, Csv2Dict]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ExportBaseModel(BaseModel):
|
class ExportBaseModel(BaseModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pydantic import Field, computed_field
|
|||||||
from sqlalchemy import Date, ForeignKey, Index, Integer, String, UniqueConstraint
|
from sqlalchemy import Date, ForeignKey, Index, Integer, String, UniqueConstraint
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.models.common import ExportBaseModel, IngestdbBase, date2str, hashify
|
from src.models.common import Amount, ExportBaseModel, IngestdbBase, date2str, hashify
|
||||||
|
|
||||||
|
|
||||||
class Transaction(ExportBaseModel):
|
class Transaction(ExportBaseModel):
|
||||||
@@ -22,12 +22,13 @@ class Transaction(ExportBaseModel):
|
|||||||
account_iban: str
|
account_iban: str
|
||||||
account_currency: str
|
account_currency: str
|
||||||
date_valid: date
|
date_valid: date
|
||||||
amount: Decimal = Field(description="Signed amount; negative = money out")
|
amount: Amount
|
||||||
amount_currency: str
|
amount_currency: str
|
||||||
date_book: date | None = None
|
date_book: date | None = None
|
||||||
text: str | None = None
|
text: str | None = None
|
||||||
other_name: str | None = None
|
other_name: str | None = None
|
||||||
other_iban: str | None = None
|
other_iban: str | None = None
|
||||||
|
# TODO: The Pydantic model should hold a category object, NOT the category id which is owned by the DB
|
||||||
category_id: int | None = None
|
category_id: int | None = None
|
||||||
|
|
||||||
@computed_field # type: ignore[prop-decorator]
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
|||||||
Reference in New Issue
Block a user