feat: Add spool adjustment history tracking (#6)
Track history of all spool weight/length adjustments with timestamps and comments:
Backend:
- Add SpoolAdjustment database model with spool_id, timestamp, type, value, comment
- Add Alembic migration for spool_adjustment table
- Add SpoolAdjustment Pydantic model for API responses
- Update use_weight, use_length, and measure functions to record adjustments
- Add GET /api/v1/spool/{id}/adjustments endpoint for history retrieval
- Add optional comment parameter to use/measure endpoints
Frontend:
- Add ISpoolAdjustment interface to spool model
- Add collapsible adjustment history table to spool show page
- Display timestamp, type (weight/length), amount, and comment
- Add translation keys for history table
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -275,16 +275,24 @@ async def use_weight_safe(db: AsyncSession, spool_id: int, weight: float) -> Non
|
||||
)
|
||||
|
||||
|
||||
async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.Spool:
|
||||
async def use_weight(
|
||||
db: AsyncSession,
|
||||
spool_id: int,
|
||||
weight: float,
|
||||
*,
|
||||
comment: Optional[str] = None,
|
||||
) -> models.Spool:
|
||||
"""Consume filament from a spool by weight.
|
||||
|
||||
Increases the used_weight attribute of the spool.
|
||||
Updates the first_used and last_used attributes where appropriate.
|
||||
Records the adjustment in the history.
|
||||
|
||||
Args:
|
||||
db (AsyncSession): Database session
|
||||
spool_id (int): Spool ID
|
||||
weight (float): Filament weight to consume, in grams
|
||||
comment (Optional[str]): Optional comment for this adjustment
|
||||
|
||||
Returns:
|
||||
models.Spool: Updated spool object
|
||||
@@ -298,21 +306,39 @@ async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.S
|
||||
spool.first_used = datetime.utcnow().replace(microsecond=0)
|
||||
spool.last_used = datetime.utcnow().replace(microsecond=0)
|
||||
|
||||
# Record the adjustment in history (negative value = filament used)
|
||||
adjustment = models.SpoolAdjustment(
|
||||
spool_id=spool_id,
|
||||
timestamp=datetime.utcnow().replace(microsecond=0),
|
||||
adjustment_type="weight",
|
||||
value=-weight, # Negative because we're using filament
|
||||
comment=comment,
|
||||
)
|
||||
db.add(adjustment)
|
||||
|
||||
await db.commit()
|
||||
await spool_changed(spool, EventType.UPDATED)
|
||||
return spool
|
||||
|
||||
|
||||
async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.Spool:
|
||||
async def use_length(
|
||||
db: AsyncSession,
|
||||
spool_id: int,
|
||||
length: float,
|
||||
*,
|
||||
comment: Optional[str] = None,
|
||||
) -> models.Spool:
|
||||
"""Consume filament from a spool by length.
|
||||
|
||||
Increases the used_weight attribute of the spool.
|
||||
Updates the first_used and last_used attributes where appropriate.
|
||||
Records the adjustment in the history.
|
||||
|
||||
Args:
|
||||
db (AsyncSession): Database session
|
||||
spool_id (int): Spool ID
|
||||
length (float): Length of filament to consume, in mm
|
||||
comment (Optional[str]): Optional comment for this adjustment
|
||||
|
||||
Returns:
|
||||
models.Spool: Updated spool object
|
||||
@@ -344,21 +370,39 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
|
||||
spool.first_used = datetime.utcnow().replace(microsecond=0)
|
||||
spool.last_used = datetime.utcnow().replace(microsecond=0)
|
||||
|
||||
# Record the adjustment in history (negative value = filament used)
|
||||
adjustment = models.SpoolAdjustment(
|
||||
spool_id=spool_id,
|
||||
timestamp=datetime.utcnow().replace(microsecond=0),
|
||||
adjustment_type="length",
|
||||
value=-length, # Negative because we're using filament
|
||||
comment=comment,
|
||||
)
|
||||
db.add(adjustment)
|
||||
|
||||
await db.commit()
|
||||
await spool_changed(spool, EventType.UPDATED)
|
||||
return spool
|
||||
|
||||
|
||||
async def measure(db: AsyncSession, spool_id: int, weight: float) -> models.Spool:
|
||||
async def measure(
|
||||
db: AsyncSession,
|
||||
spool_id: int,
|
||||
weight: float,
|
||||
*,
|
||||
comment: Optional[str] = None,
|
||||
) -> models.Spool:
|
||||
"""Record usage based on current gross weight of spool.
|
||||
|
||||
Increases the used_weight attribute of the spool.
|
||||
Updates the first_used and last_used attributes where appropriate.
|
||||
Records the adjustment in the history.
|
||||
|
||||
Args:
|
||||
db (AsyncSession): Database session
|
||||
spool_id (int): Spool ID
|
||||
weight (float): Length of filament to consume, in mm
|
||||
weight (float): Current gross weight of the spool, in grams
|
||||
comment (Optional[str]): Optional comment for this adjustment
|
||||
|
||||
Returns:
|
||||
models.Spool: Updated spool object
|
||||
@@ -414,7 +458,7 @@ async def measure(db: AsyncSession, spool_id: int, weight: float) -> models.Spoo
|
||||
if (initial_gross_weight - weight_to_use) < spool_weight:
|
||||
weight_to_use = current_use - spool_weight
|
||||
|
||||
return await use_weight(db, spool_id, weight_to_use)
|
||||
return await use_weight(db, spool_id, weight_to_use, comment=comment)
|
||||
|
||||
|
||||
async def find_locations(
|
||||
@@ -475,3 +519,46 @@ async def rename_location(
|
||||
await db.execute(
|
||||
sqlalchemy.update(models.Spool).where(models.Spool.location == current_name).values(location=new_name),
|
||||
)
|
||||
|
||||
|
||||
async def get_adjustments(
|
||||
db: AsyncSession,
|
||||
spool_id: int,
|
||||
*,
|
||||
limit: Optional[int] = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[models.SpoolAdjustment], int]:
|
||||
"""Get the adjustment history for a spool.
|
||||
|
||||
Args:
|
||||
db (AsyncSession): Database session
|
||||
spool_id (int): Spool ID
|
||||
limit (Optional[int]): Maximum number of items to return
|
||||
offset (int): Offset for pagination
|
||||
|
||||
Returns:
|
||||
tuple: List of adjustment records and total count
|
||||
|
||||
"""
|
||||
# First verify the spool exists
|
||||
await get_by_id(db, spool_id)
|
||||
|
||||
# Build the query
|
||||
stmt = (
|
||||
sqlalchemy.select(models.SpoolAdjustment)
|
||||
.where(models.SpoolAdjustment.spool_id == spool_id)
|
||||
.order_by(models.SpoolAdjustment.timestamp.desc())
|
||||
)
|
||||
|
||||
# Get total count
|
||||
count_stmt = sqlalchemy.select(func.count()).select_from(models.SpoolAdjustment).where(
|
||||
models.SpoolAdjustment.spool_id == spool_id,
|
||||
)
|
||||
total_count = (await db.execute(count_stmt)).scalar() or 0
|
||||
|
||||
# Apply pagination
|
||||
if limit is not None:
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
|
||||
rows = await db.execute(stmt)
|
||||
return list(rows.scalars().all()), total_count
|
||||
|
||||
Reference in New Issue
Block a user