Add Print Job tracking for slicer integration
Backend for tracking print jobs from Elegoo/Orca Slicer: - PrintJob model: tracks pending/completed/cancelled jobs - API endpoints: create, list, complete, cancel jobs - needs_weighing flag on Spool: for cancelled print recalibration - Database migration for new table and column Workflow: 1. Slicer post-processing script creates pending job 2. User completes job → auto-deducts filament 3. User cancels job → flags spool for manual weigh-in Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,10 @@ class Spool(Base):
|
||||
lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
||||
archived: Mapped[Optional[bool]] = mapped_column()
|
||||
needs_weighing: Mapped[Optional[bool]] = mapped_column(
|
||||
default=False,
|
||||
comment="Flag indicating spool needs manual weigh-in (e.g., after cancelled print).",
|
||||
)
|
||||
extra: Mapped[list["SpoolField"]] = relationship(
|
||||
back_populates="spool",
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
@@ -106,6 +110,26 @@ class SpoolAdjustment(Base):
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(256), comment="Optional user comment.")
|
||||
|
||||
|
||||
class PrintJob(Base):
|
||||
"""Track pending/completed print jobs from slicer."""
|
||||
|
||||
__tablename__ = "print_job"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
spool_id: Mapped[int] = mapped_column(ForeignKey("spool.id", ondelete="CASCADE"), index=True)
|
||||
spool: Mapped["Spool"] = relationship()
|
||||
created: Mapped[datetime] = mapped_column(comment="When the job was created (slice time).")
|
||||
finished: Mapped[Optional[datetime]] = mapped_column(comment="When the job was completed/cancelled.")
|
||||
filename: Mapped[str] = mapped_column(String(256), comment="G-code filename.")
|
||||
filament_used_g: Mapped[float] = mapped_column(comment="Estimated filament usage in grams.")
|
||||
filament_used_mm: Mapped[Optional[float]] = mapped_column(comment="Estimated filament usage in mm.")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(16),
|
||||
default="pending",
|
||||
comment="Status: 'pending', 'completed', 'cancelled'.",
|
||||
)
|
||||
|
||||
|
||||
class Setting(Base):
|
||||
__tablename__ = "setting"
|
||||
|
||||
|
||||
157
spoolman/database/print_job.py
Normal file
157
spoolman/database/print_job.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Database operations for print jobs."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from spoolman.database.models import PrintJob, Spool, SpoolAdjustment
|
||||
from spoolman.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
async def create(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
spool_id: int,
|
||||
filename: str,
|
||||
filament_used_g: float,
|
||||
filament_used_mm: Optional[float] = None,
|
||||
) -> PrintJob:
|
||||
"""Create a new pending print job."""
|
||||
# Verify spool exists
|
||||
spool = await db.get(Spool, spool_id)
|
||||
if spool is None:
|
||||
raise ItemNotFoundError(f"Spool with ID {spool_id} not found.")
|
||||
|
||||
job = PrintJob(
|
||||
spool_id=spool_id,
|
||||
created=datetime.utcnow(),
|
||||
filename=filename,
|
||||
filament_used_g=filament_used_g,
|
||||
filament_used_mm=filament_used_mm,
|
||||
status="pending",
|
||||
)
|
||||
db.add(job)
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
# Load spool relationship
|
||||
result = await db.execute(
|
||||
select(PrintJob).where(PrintJob.id == job.id).options(joinedload(PrintJob.spool))
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def get_by_id(db: AsyncSession, job_id: int) -> PrintJob:
|
||||
"""Get a print job by ID."""
|
||||
result = await db.execute(
|
||||
select(PrintJob).where(PrintJob.id == job_id).options(joinedload(PrintJob.spool))
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
if job is None:
|
||||
raise ItemNotFoundError(f"Print job with ID {job_id} not found.")
|
||||
return job
|
||||
|
||||
|
||||
async def find(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
status: Optional[str] = None,
|
||||
spool_id: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[PrintJob], int]:
|
||||
"""Find print jobs with optional filters."""
|
||||
query = select(PrintJob).options(joinedload(PrintJob.spool))
|
||||
|
||||
if status:
|
||||
query = query.where(PrintJob.status == status)
|
||||
if spool_id:
|
||||
query = query.where(PrintJob.spool_id == spool_id)
|
||||
|
||||
# Get total count
|
||||
count_query = select(PrintJob)
|
||||
if status:
|
||||
count_query = count_query.where(PrintJob.status == status)
|
||||
if spool_id:
|
||||
count_query = count_query.where(PrintJob.spool_id == spool_id)
|
||||
count_result = await db.execute(count_query)
|
||||
total = len(count_result.all())
|
||||
|
||||
# Apply ordering and pagination
|
||||
query = query.order_by(PrintJob.created.desc())
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def complete(db: AsyncSession, job_id: int, comment: Optional[str] = None) -> PrintJob:
|
||||
"""Complete a print job - deduct filament from spool."""
|
||||
job = await get_by_id(db, job_id)
|
||||
if job.status != "pending":
|
||||
raise ValueError(f"Job {job_id} is not pending (status: {job.status}).")
|
||||
|
||||
# Deduct filament from spool
|
||||
spool = await db.get(Spool, job.spool_id)
|
||||
if spool is None:
|
||||
raise ItemNotFoundError(f"Spool with ID {job.spool_id} not found.")
|
||||
|
||||
spool.used_weight += job.filament_used_g
|
||||
if spool.first_used is None:
|
||||
spool.first_used = datetime.utcnow()
|
||||
spool.last_used = datetime.utcnow()
|
||||
|
||||
# Record adjustment
|
||||
adjustment = SpoolAdjustment(
|
||||
spool_id=spool.id,
|
||||
timestamp=datetime.utcnow(),
|
||||
adjustment_type="weight",
|
||||
value=-job.filament_used_g,
|
||||
comment=comment or f"Print job: {job.filename}",
|
||||
)
|
||||
db.add(adjustment)
|
||||
|
||||
# Update job status
|
||||
job.status = "completed"
|
||||
job.finished = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Reload with relationships
|
||||
return await get_by_id(db, job_id)
|
||||
|
||||
|
||||
async def cancel(db: AsyncSession, job_id: int) -> PrintJob:
|
||||
"""Cancel a print job - flag spool for weighing."""
|
||||
job = await get_by_id(db, job_id)
|
||||
if job.status != "pending":
|
||||
raise ValueError(f"Job {job_id} is not pending (status: {job.status}).")
|
||||
|
||||
# Flag spool for weighing
|
||||
spool = await db.get(Spool, job.spool_id)
|
||||
if spool is None:
|
||||
raise ItemNotFoundError(f"Spool with ID {job.spool_id} not found.")
|
||||
|
||||
spool.needs_weighing = True
|
||||
|
||||
# Update job status
|
||||
job.status = "cancelled"
|
||||
job.finished = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Reload with relationships
|
||||
return await get_by_id(db, job_id)
|
||||
|
||||
|
||||
async def delete(db: AsyncSession, job_id: int) -> None:
|
||||
"""Delete a print job."""
|
||||
job = await get_by_id(db, job_id)
|
||||
await db.delete(job)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user