diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 26701c125..6a43696f5 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -18,6 +18,7 @@ from backend.app.core.database import get_db from backend.app.core.permissions import Permission from backend.app.models.archive import PrintArchive from backend.app.models.filament import Filament +from backend.app.models.spool_usage_history import SpoolUsageHistory from backend.app.models.user import User from backend.app.schemas.archive import ArchiveResponse, ArchiveStats, ArchiveUpdate, ReprintRequest from backend.app.services.archive import ArchiveService @@ -857,9 +858,8 @@ async def rescan_archive( archive.designer = metadata["designer"] # Calculate cost: prefer spool-based cost if available, else catalog-based - if archive.filament_used_grams and archive.filament_type: - from backend.app.models.spool_usage_history import SpoolUsageHistory + 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) ) @@ -889,6 +889,7 @@ async def recalculate_all_costs( _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), ): """Recalculate costs for all archives based on filament usage and prices.""" + from backend.app.api.routes.settings import get_setting result = await db.execute(select(PrintArchive)) @@ -902,17 +903,17 @@ 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 - # Import SpoolUsageHistory for cost lookup - from backend.app.models.spool_usage_history import SpoolUsageHistory + # Pre-fetch all usage costs in one query + usage_costs_result = await db.execute( + select(SpoolUsageHistory.print_name, func.sum(SpoolUsageHistory.cost)).group_by(SpoolUsageHistory.print_name) + ) + 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} updated = 0 for archive in archives: - # Prefer sum of spool_usage_history.cost for this archive's print_name - usage_result = await db.execute( - select(func.sum(SpoolUsageHistory.cost)).where(SpoolUsageHistory.print_name == archive.print_name) - ) - usage_cost = usage_result.scalar() - if usage_cost is not None and usage_cost > 0: + usage_cost = cost_map.get(archive.print_name) + 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() diff --git a/backend/tests/integration/test_cost_statistics.py b/backend/tests/integration/test_cost_statistics.py index f80a176d7..b1dc828b5 100644 --- a/backend/tests/integration/test_cost_statistics.py +++ b/backend/tests/integration/test_cost_statistics.py @@ -239,10 +239,6 @@ class TestSpoolCostPersistence: assert result["cost_per_kg"] is None -class TestSpoolUsageHistoryCost: - """Tests for cost field in SpoolUsageHistory.""" - - class TestCostCalculationScenarios: """End-to-end tests for various cost calculation scenarios.""" diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index 9928dcff3..2eff3474d 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -257,7 +257,7 @@ const columnCells: Record ReactNode> = { ), cost_per_kg: ({ spool, currencySymbol }) => ( - {spool.cost_per_kg != null ? `${currencySymbol} ${spool.cost_per_kg.toFixed(2)}` : '-'} + {spool.cost_per_kg != null ? `${currencySymbol}${spool.cost_per_kg.toFixed(2)}` : '-'} ), };