Files
finanz-cockpit/src/ingest/b4w.py
T

186 lines
5.4 KiB
Python

from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from sqlalchemy.orm import Session
from src.ingest._common import _reader_raw, _upsert
from src.ingest.transactions import consolidate_transactions, map_b4w_to_transaction
from src.models.b4w import B4wDb, B4wExport, Category, CategoryDb
def reader_b4w(path: Path) -> list[B4wExport]:
raw = _reader_raw(path)
return list(map(B4wExport.model_validate, raw))
def upsert_category(
session: Session, category: Category | None, do_commit=True
) -> CategoryDb | None:
if category is None:
return None
parent = upsert_category(session, category.parent, do_commit=do_commit)
parent_id = parent.id if parent else None
matching_rows = session.query(CategoryDb).filter_by(
name=category.name, parent_id=parent_id
)
active = matching_rows.filter(CategoryDb.valid_to.is_(None)).one_or_none()
if active is not None:
return active
parameters = {"name": category.name, "parent": parent}
parameters_existing = {**parameters, "valid_to": None}
parameters_new: dict[str, Any] = {
**parameters,
"valid_from": None,
"valid_to": None,
"last_action": "created",
}
historical = matching_rows.filter(CategoryDb.valid_to.is_not(None)).first()
if historical is not None:
parameters_new["valid_from"] = _utc_datetime(None)
parameters_new["last_action"] = "changed"
return _upsert(
session,
CategoryDb,
parameters_existing,
parameters_new,
do_update=False,
do_commit=do_commit,
)
def _utc_datetime(value: datetime | None) -> datetime:
if value is None:
return datetime.now(UTC)
if value.tzinfo is None or value.utcoffset() is None:
return value.replace(tzinfo=UTC)
return value.astimezone(UTC)
def _ensure_after_valid_from(category: CategoryDb, effective_at: datetime) -> None:
if category.valid_from is None:
return
valid_from = _utc_datetime(category.valid_from)
if effective_at <= valid_from:
raise ValueError("The action time must be later than valid_from")
def change_category(
session: Session,
category_id: int,
replacement: Category,
effective_at: datetime | None = None,
do_commit: bool = True,
) -> CategoryDb:
"""Close an active category version and start its replacement version."""
current = (
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
)
if current is None:
raise ValueError(f"No active category found with id {category_id}")
action_time = _utc_datetime(effective_at)
_ensure_after_valid_from(current, action_time)
parent = upsert_category(session, replacement.parent, do_commit=False)
parent_id = parent.id if parent else None
if current.name == replacement.name and current.parent_id == parent_id:
return current
replacement_active = (
session.query(CategoryDb)
.filter_by(name=replacement.name, parent_id=parent_id, valid_to=None)
.one_or_none()
)
current.valid_to = action_time
current.last_action = "changed"
session.flush()
if replacement_active is None:
replacement_active = CategoryDb(
name=replacement.name,
parent=parent,
valid_from=action_time,
valid_to=None,
last_action="changed",
)
session.add(replacement_active)
session.flush()
if do_commit:
session.commit()
return replacement_active
def retire_category(
session: Session,
category_id: int,
effective_at: datetime | None = None,
do_commit: bool = True,
) -> CategoryDb:
"""Close an active category version without creating a replacement."""
category = (
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
)
if category is None:
raise ValueError(f"No active category found with id {category_id}")
action_time = _utc_datetime(effective_at)
_ensure_after_valid_from(category, action_time)
category.valid_to = action_time
category.last_action = "retired"
session.flush()
if do_commit:
session.commit()
return category
def upsert_b4w(
session: Session, data: B4wExport, do_update=False, do_commit=True
) -> B4wDb | None:
category_db = upsert_category(session, data.category)
parameters_existing = {"hash_id": data.hash_id}
parameters_new = {
"id_b4w": data.id,
"hash_id": data.hash_id,
"account_iban": data.account_iban,
"account_currency": data.account_currency.value,
"date_valid": data.date_valid,
"date_book": data.date_book,
"amount": data.amount_signed,
"amount_currency": data.amount_currency.value,
"text": data.reference,
"other_name": data.other_name,
"other_iban": data.other_iban,
"category": category_db,
"notes": data.notes,
}
return _upsert(
session,
B4wDb,
parameters_existing,
parameters_new,
do_update=do_update,
do_commit=do_commit,
)
def ingest_b4w(path: Path, session: Session):
data = reader_b4w(path)
for row in data:
upsert_b4w(session, row, do_commit=False)
consolidate_transactions(
session,
source="b4w",
StagingDb=B4wDb,
mapper=map_b4w_to_transaction,
do_commit=False,
)
session.commit()