feat: Add extra weight, price tracking, print history, usage analytics
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled

- Extra Weight Field (#14): Track DryPods, custom holders in spool weight
  calculations. New extra_weight field on spool with DB migration.

- Price/Cost Tracking: Compute remaining_value based on remaining weight
  and price. Added column to spool list, inventory value on dashboard.

- Print History: Show print job history on spool detail page with
  collapsible table showing filename, filament used, status, dates.

- Usage Analytics: New dashboard component with time-series chart
  showing daily consumption, period selector (7/30/90 days), and
  material breakdown. New API endpoints for analytics data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 22:38:14 -06:00
parent 0556be9e3b
commit 18cafc4361
15 changed files with 514 additions and 18 deletions

View File

@@ -100,6 +100,10 @@ class Spool(Base):
initial_weight: Mapped[Optional[float]] = mapped_column()
spool_weight: Mapped[Optional[float]] = mapped_column()
used_weight: Mapped[float] = mapped_column()
extra_weight: Mapped[Optional[float]] = mapped_column(
default=None,
comment="Extra weight to account for (DryPods, custom holders, etc.).",
)
location: Mapped[Optional[str]] = mapped_column(String(64), comment="Legacy flat location string.")
location_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("location.id", ondelete="SET NULL"),

View File

@@ -41,6 +41,7 @@ async def create(
remaining_weight: Optional[float] = None,
initial_weight: Optional[float] = None,
spool_weight: Optional[float] = None,
extra_weight: Optional[float] = None,
used_weight: Optional[float] = None,
first_used: Optional[datetime] = None,
last_used: Optional[datetime] = None,
@@ -84,6 +85,7 @@ async def create(
registered=datetime.utcnow().replace(microsecond=0),
initial_weight=initial_weight,
spool_weight=spool_weight,
extra_weight=extra_weight,
used_weight=used_weight,
price=price,
first_used=first_used,
@@ -440,7 +442,12 @@ async def measure(
"""
spool_result = await db.execute(
sqlalchemy.select(models.Spool.initial_weight, models.Spool.used_weight, models.Spool.spool_weight).where(
sqlalchemy.select(
models.Spool.initial_weight,
models.Spool.used_weight,
models.Spool.spool_weight,
models.Spool.extra_weight,
).where(
models.Spool.id == spool_id,
),
)
@@ -452,6 +459,7 @@ async def measure(
initial_weight = spool_info[0]
spool_weight = spool_info[2]
extra_weight = spool_info[3] or 0 # Default to 0 if not set
if initial_weight is None or initial_weight == 0 or spool_weight is None or spool_weight == 0:
# Get filament weight and spool_weight
result = await db.execute(
@@ -473,7 +481,7 @@ async def measure(
if initial_weight is None or initial_weight == 0:
raise SpoolMeasureError("Initial weight is not set.")
initial_gross_weight = initial_weight + spool_weight
initial_gross_weight = initial_weight + spool_weight + extra_weight
# if the measurement is greater than the initial weight, set the initial weight to the measurement
if weight > initial_gross_weight:
@@ -485,9 +493,10 @@ async def measure(
# Calculate the weight used since last measure
weight_to_use = current_use - weight
# If the measured weight is less than the empty weight, use the rest of the spool
if (initial_gross_weight - weight_to_use) < spool_weight:
weight_to_use = current_use - spool_weight
# If the measured weight is less than the empty weight (+ extra), use the rest of the spool
empty_gross_weight = spool_weight + extra_weight
if (initial_gross_weight - weight_to_use) < empty_gross_weight:
weight_to_use = current_use - empty_gross_weight
return await use_weight(db, spool_id, weight_to_use, comment=comment)