From 427b08f9f63c9b7c5a141a8bc20dff8f47ec2d92 Mon Sep 17 00:00:00 2001 From: Matteo Parenti Date: Fri, 20 Feb 2026 13:39:41 +0100 Subject: [PATCH] Add archive_id to SpoolUsageHistory model and update cost calculations in usage_tracker --- backend/app/api/routes/archives.py | 31 ++++++++++++++------- backend/app/models/spool_usage_history.py | 1 + backend/app/services/usage_tracker.py | 34 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 51475e0ee..46579b3c4 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -862,7 +862,7 @@ async def rescan_archive( if archive.filament_used_grams and archive.filament_type: usage_result = await db.execute( - select(func.sum(SpoolUsageHistory.cost)).where(SpoolUsageHistory.print_name == archive.print_name) + select(func.sum(SpoolUsageHistory.cost)).where(SpoolUsageHistory.archive_id == archive.id) ) usage_cost = usage_result.scalar() if usage_cost is not None and usage_cost > 0: @@ -912,24 +912,35 @@ async def recalculate_all_costs( default_cost_setting = await get_setting(db, "default_filament_cost") default_cost_per_kg = float(default_cost_setting) if default_cost_setting else 25.0 - # Pre-fetch all usage costs in one query + # Pre-fetch all usage costs by archive_id usage_costs_result = await db.execute( - select(SpoolUsageHistory.print_name, func.sum(SpoolUsageHistory.cost)).group_by(SpoolUsageHistory.print_name) + select(SpoolUsageHistory.archive_id, func.sum(SpoolUsageHistory.cost)).group_by(SpoolUsageHistory.archive_id) ) usage_costs = usage_costs_result.fetchall() - cost_map = {row[0]: row[1] for row in usage_costs if row[1] is not None and row[1] > 0} + cost_map = {row[0]: row[1] for row in usage_costs if row[0] is not None and row[1] is not None and row[1] > 0} updated = 0 for archive in archives: - usage_cost = cost_map.get(archive.print_name) + usage_cost = cost_map.get(archive.id) if usage_cost is not None: new_cost = round(usage_cost, 2) - elif archive.filament_used_grams and archive.filament_type: - primary_type = archive.filament_type.split(",")[0].strip() - cost_per_kg = filaments.get(primary_type, default_cost_per_kg) - new_cost = round((archive.filament_used_grams / 1000) * cost_per_kg, 2) else: - new_cost = None + # Fallback: sum costs for old records by print_name + usage_result = await db.execute( + select(func.sum(SpoolUsageHistory.cost)).where( + SpoolUsageHistory.print_name == archive.print_name, + SpoolUsageHistory.archive_id is None, + ) + ) + fallback_cost = usage_result.scalar() + if fallback_cost is not None and fallback_cost > 0: + new_cost = round(fallback_cost, 2) + elif archive.filament_used_grams and archive.filament_type: + primary_type = archive.filament_type.split(",")[0].strip() + cost_per_kg = filaments.get(primary_type, default_cost_per_kg) + new_cost = round((archive.filament_used_grams / 1000) * cost_per_kg, 2) + else: + new_cost = None if new_cost is not None and archive.cost != new_cost: archive.cost = new_cost updated += 1 diff --git a/backend/app/models/spool_usage_history.py b/backend/app/models/spool_usage_history.py index 912ba133a..4b66be3b5 100644 --- a/backend/app/models/spool_usage_history.py +++ b/backend/app/models/spool_usage_history.py @@ -15,6 +15,7 @@ class SpoolUsageHistory(Base): spool_id: Mapped[int] = mapped_column(ForeignKey("spool.id", ondelete="CASCADE")) printer_id: Mapped[int | None] = mapped_column(ForeignKey("printers.id", ondelete="SET NULL")) print_name: Mapped[str | None] = mapped_column(String(500)) + archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id"), nullable=True) weight_used: Mapped[float] = mapped_column(Float, default=0) percent_used: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(20), default="completed") # completed/failed/aborted diff --git a/backend/app/services/usage_tracker.py b/backend/app/services/usage_tracker.py index 58da028a3..8baab8e90 100644 --- a/backend/app/services/usage_tracker.py +++ b/backend/app/services/usage_tracker.py @@ -253,7 +253,10 @@ async def on_print_complete( Returns a list of dicts describing what was logged (for WebSocket broadcast). """ + from sqlalchemy import select + from backend.app.api.routes.settings import get_setting + from backend.app.models.spool_usage_history import SpoolUsageHistory session = _active_sessions.pop(printer_id, None) status = data.get("status", "completed") @@ -375,6 +378,7 @@ async def on_print_complete( percent_used=delta_pct, status=status, cost=cost, + archive_id=archive_id, ) db.add(history) @@ -405,6 +409,35 @@ async def on_print_complete( if results: await db.commit() + # --- Update PrintArchive.cost to sum all SpoolUsageHistory costs for this archive --- + + if archive_id: + from sqlalchemy import func, select + + from backend.app.models.archive import PrintArchive + + # First try: sum by archive_id + cost_result = await db.execute( + select(func.coalesce(func.sum(SpoolUsageHistory.cost), 0)).where(SpoolUsageHistory.archive_id == archive_id) + ) + total_cost = cost_result.scalar() or 0 + + # Fallback: if no cost found, sum by print_name and printer_id (legacy) + archive_result = await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id)) + archive = archive_result.scalar_one_or_none() + if archive and total_cost == 0 and archive.print_name and archive.printer_id: + legacy_cost_result = await db.execute( + select(func.coalesce(func.sum(SpoolUsageHistory.cost), 0)).where( + SpoolUsageHistory.archive_id is None, + SpoolUsageHistory.print_name == archive.print_name, + SpoolUsageHistory.printer_id == archive.printer_id, + ) + ) + total_cost = legacy_cost_result.scalar() or 0 + if archive: + archive.cost = total_cost + await db.commit() + return results @@ -678,6 +711,7 @@ async def _track_from_3mf( percent_used=percent, status=status, cost=cost, + archive_id=archive_id, ) db.add(history)