Fix monthly comparison calculation ignoring quantity multiplier (Issue #229)

The filament statistics were under-reporting totals because the quantity
field was not being multiplied with filament_used_grams. When users
printed multiple items (quantity > 1), only the base filament amount
was counted instead of the total.

Closes #229
This commit is contained in:
maziggy
2026-02-03 07:29:38 +01:00
parent ea93535bfc
commit 11456c0d2a
6 changed files with 33 additions and 17 deletions
+7
View File
@@ -26,6 +26,13 @@ All notable changes to Bambuddy will be documented in this file.
- Added locale parity test to ensure English and German stay in sync
### Fixed
- **Monthly Comparison Calculation Off** (Issue #229):
- Fixed filament statistics not accounting for quantity multiplier
- Monthly comparison chart now correctly multiplies `filament_used_grams` by `quantity`
- Daily and weekly charts also now account for quantity
- Filament type breakdown includes quantity in calculations
- Backend stats endpoint (`/archives/stats`) and Prometheus metrics also fixed
- Prints count now shows total items (sum of quantities) instead of archive count
- **Authentication Required for Downloads** (Issue #231):
- Fixed support bundle download returning 401 Unauthorized when auth is enabled
- Fixed archive export (CSV/XLSX) failing with authentication enabled
+4 -1
View File
@@ -451,7 +451,10 @@ async def get_archive_stats(db: AsyncSession = Depends(get_db)):
total_seconds += print_time_seconds
total_time = total_seconds / 3600 # Convert to hours
filament_result = await db.execute(select(func.sum(PrintArchive.filament_used_grams)))
# Multiply filament by quantity to account for multiple items printed
filament_result = await db.execute(
select(func.sum(PrintArchive.filament_used_grams * func.coalesce(PrintArchive.quantity, 1)))
)
total_filament = filament_result.scalar() or 0
cost_result = await db.execute(select(func.sum(PrintArchive.cost)))
+4 -2
View File
@@ -362,11 +362,13 @@ async def get_metrics(
)
lines.append(f"bambuddy_printer_prints_total{labels} {count}")
# Total filament used
# Total filament used (multiply by quantity to account for multiple items printed)
lines.append("")
lines.append("# HELP bambuddy_filament_used_grams Total filament used in grams")
lines.append("# TYPE bambuddy_filament_used_grams counter")
result = await db.execute(select(func.coalesce(func.sum(PrintArchive.filament_used_grams), 0)))
result = await db.execute(
select(func.coalesce(func.sum(PrintArchive.filament_used_grams * func.coalesce(PrintArchive.quantity, 1)), 0))
)
total_filament = result.scalar() or 0
lines.append(f"bambuddy_filament_used_grams {total_filament:.1f}")
+16 -12
View File
@@ -61,9 +61,10 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
const existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, prints: 0 };
existing.filament += archive.filament_used_grams || 0;
const qty = archive.quantity || 1;
existing.filament += (archive.filament_used_grams || 0) * qty;
existing.cost += archive.cost || 0;
existing.prints += 1;
existing.prints += qty;
dataMap.set(key, existing);
});
@@ -89,9 +90,10 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
const key = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`;
const existing = dataMap.get(key) || { week: key, filament: 0, cost: 0, prints: 0 };
existing.filament += archive.filament_used_grams || 0;
const qty = archive.quantity || 1;
existing.filament += (archive.filament_used_grams || 0) * qty;
existing.cost += archive.cost || 0;
existing.prints += 1;
existing.prints += qty;
dataMap.set(key, existing);
});
@@ -110,10 +112,11 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
filteredArchives.forEach(archive => {
const type = archive.filament_type || 'Unknown';
const qty = archive.quantity || 1;
// Handle multiple types (e.g., "PLA, PETG")
const types = type.split(', ');
types.forEach(t => {
const grams = (archive.filament_used_grams || 0) / types.length;
const grams = ((archive.filament_used_grams || 0) * qty) / types.length;
dataMap.set(t, (dataMap.get(t) || 0) + grams);
});
});
@@ -140,9 +143,9 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
months.push({
month: monthStr,
filament: Math.round(monthArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0)),
filament: Math.round(monthArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0) * (a.quantity || 1), 0)),
cost: monthArchives.reduce((sum, a) => sum + (a.cost || 0), 0),
prints: monthArchives.length,
prints: monthArchives.reduce((sum, a) => sum + (a.quantity || 1), 0),
});
}
@@ -150,8 +153,9 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
}, [archives]);
const chartData = timeRange === '7d' || timeRange === '30d' ? dailyData : weeklyData;
const totalFilament = filteredArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0);
const totalFilament = filteredArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0) * (a.quantity || 1), 0);
const totalCost = filteredArchives.reduce((sum, a) => sum + (a.cost || 0), 0);
const totalPrints = filteredArchives.reduce((sum, a) => sum + (a.quantity || 1), 0);
return (
<div className="space-y-6">
@@ -185,17 +189,17 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps
<div className="bg-bambu-dark rounded-lg p-4">
<p className="text-sm text-bambu-gray">Period Cost</p>
<p className="text-2xl font-bold text-white">{currency}{totalCost.toFixed(2)}</p>
<p className="text-xs text-bambu-gray">{filteredArchives.length} prints</p>
<p className="text-xs text-bambu-gray">{totalPrints} prints</p>
</div>
<div className="bg-bambu-dark rounded-lg p-4">
<p className="text-sm text-bambu-gray">Avg per Print</p>
<p className="text-2xl font-bold text-white">
{filteredArchives.length > 0
? (totalFilament / filteredArchives.length).toFixed(0)
{totalPrints > 0
? (totalFilament / totalPrints).toFixed(0)
: 0}g
</p>
<p className="text-xs text-bambu-gray">
{currency}{filteredArchives.length > 0 ? (totalCost / filteredArchives.length).toFixed(2) : '0.00'} avg
{currency}{totalPrints > 0 ? (totalCost / totalPrints).toFixed(2) : '0.00'} avg
</p>
</div>
</div>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BXCvE4aN.js"></script>
<script type="module" crossorigin src="/assets/index-POsspFOS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CPqcJWwC.css">
</head>
<body>