first sketch; incomplete; WiP
This commit is contained in:
+34
-2
@@ -18,7 +18,8 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
||||
|
||||
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]):
|
||||
@@ -75,6 +76,9 @@ class Category(ExportBaseModel):
|
||||
def __repr__(self):
|
||||
return f'Category.from_str("{self.__str__()}")'
|
||||
|
||||
def get_budget(self, date_valid: date | None) -> float:
|
||||
...
|
||||
|
||||
|
||||
class CategoryDb(BaseModelDb):
|
||||
__tablename__ = "category"
|
||||
@@ -187,7 +191,7 @@ def upsert_category(
|
||||
|
||||
class CategoryBudget(BaseModel):
|
||||
category: Category
|
||||
amount_eur: Decimal = Field(gt=0, decimal_places=2)
|
||||
amount_eur: AmountPos
|
||||
valid_from: date | None = Field(
|
||||
default=None, description="Start of validity. None means unbounded"
|
||||
)
|
||||
@@ -228,3 +232,31 @@ class CategoryBudgetDb(BaseModelDb):
|
||||
|
||||
# relationship
|
||||
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 decimal import Decimal
|
||||
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.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 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(",", "."))
|
||||
|
||||
|
||||
def decimal2str(value: Decimal) -> str:
|
||||
def decimal2str(value: Amount) -> str:
|
||||
value = Decimal(f"{value:.2f}")
|
||||
int_part, _, frac = str(value).partition(".")
|
||||
groups: list[str] = []
|
||||
@@ -60,6 +64,7 @@ type Csv2Dict = list[Csv2DictRow]
|
||||
type CsV2DictMulti = dict[str, Csv2Dict]
|
||||
|
||||
|
||||
|
||||
class ExportBaseModel(BaseModel):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -5,7 +5,7 @@ from pydantic import Field, computed_field
|
||||
from sqlalchemy import Date, ForeignKey, Index, Integer, String, UniqueConstraint
|
||||
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):
|
||||
@@ -22,12 +22,13 @@ class Transaction(ExportBaseModel):
|
||||
account_iban: str
|
||||
account_currency: str
|
||||
date_valid: date
|
||||
amount: Decimal = Field(description="Signed amount; negative = money out")
|
||||
amount: Amount
|
||||
amount_currency: str
|
||||
date_book: date | None = None
|
||||
text: str | None = None
|
||||
other_name: 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
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
|
||||
Reference in New Issue
Block a user