first sketch; incomplete; WiP
Tests / test (push) Successful in 11s
Tests / lint (push) Failing after 8s

This commit is contained in:
2026-10-27 20:35:49 +01:00
parent 6babc560c7
commit 6b2821ba96
4 changed files with 72 additions and 7 deletions
+34 -2
View File
@@ -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
+8 -3
View File
@@ -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
+3 -2
View File
@@ -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]