From e0c32319e8f23932da456a935f4b1db67c3f9b6d Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 28 Oct 2026 07:12:31 +0100 Subject: [PATCH] draft: category tree building --- src/models/category.py | 51 +++++++++++++++++++++++++++++++------- src/models/transactions.py | 4 ++- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/models/category.py b/src/models/category.py index c296834..d626417 100644 --- a/src/models/category.py +++ b/src/models/category.py @@ -84,6 +84,7 @@ class Category(ExportBaseModel): # TODO (maybe) ... + class CategoryDb(BaseModelDb): __tablename__ = "category" __table_args__ = ( @@ -240,15 +241,15 @@ class CategoryBudgetDb(BaseModelDb): class CategoryTree(BaseModel): category: Category - total_amount: float - total_budget: float - sub_categories: list[ CategoryTree ] | None - transactions: list[Transaction] | None + total_amount: float | None = None + total_budget: float | None = None + sub_categories: list[CategoryTree] | None = None + transactions: list[Transaction] | None = None def get_total_amount(self) -> float: result = 0.0 if self.transactions: - sum(transaction.amount for transaction in 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 @@ -257,14 +258,46 @@ class CategoryTree(BaseModel): 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) + result += sum( + category.category.get_budget(date_valid) + for category in self.sub_categories + ) return result - def __add__(self, other): - ... + def __add__(self, other: CategoryTree): + if self.category != other.category: + return + sub_categories = [] + for category1 in self.sub_categories or []: + for category2 in other.sub_categories or []: + if category1.category == category2.category: + sub_categories.append(category1 + category2) + else: + sub_categories.append(category1) + sub_categories.append(category2) + + return CategoryTree( + category=self.category, + total_amount=(self.total_amount or 0.0) + (other.total_amount or 0.0), + total_budget=(self.total_budget or 0.0) + (other.total_budget or 0.0), + sub_categories=sub_categories, + transactions=self.transactions + other.transactions, + ) + def build_category_tree(transactions: list[Transaction]) -> list[CategoryTree]: # TODO: loop through flat list a build a category tree result = [] for transaction in transactions: - pass + bla = _transaction_to_tree(transaction) + + +def _transaction_to_tree(transaction: Transaction) -> CategoryTree | None: + category = transaction.category + if not category: + return + result = CategoryTree(category=category, transactions=[transaction]) + while category.parent is not None: + category = category.parent + result = CategoryTree(category=category, sub_categories=[result]) + return result diff --git a/src/models/transactions.py b/src/models/transactions.py index 8ed9268..dfd7fee 100644 --- a/src/models/transactions.py +++ b/src/models/transactions.py @@ -5,6 +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.category import Category from src.models.common import Amount, ExportBaseModel, IngestdbBase, date2str, hashify @@ -29,7 +30,8 @@ class Transaction(ExportBaseModel): 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 + category: Category | None + category_id: int | None = None # remove! @computed_field # type: ignore[prop-decorator] @property