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:
@@ -0,0 +1,53 @@
|
|||||||
|
"""add_print_jobs.
|
||||||
|
|
||||||
|
Revision ID: c3d4e5f6g7h8
|
||||||
|
Revises: b2c3d4e5f6g7
|
||||||
|
Create Date: 2025-01-15 04:00:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "c3d4e5f6g7h8"
|
||||||
|
down_revision = "b2c3d4e5f6g7"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Create print_job table and add needs_weighing to spool."""
|
||||||
|
# Create print_job table
|
||||||
|
op.create_table(
|
||||||
|
"print_job",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("spool_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("created", sa.DateTime(), nullable=False),
|
||||||
|
sa.Column("finished", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("filename", sa.String(length=256), nullable=False),
|
||||||
|
sa.Column("filament_used_g", sa.Float(), nullable=False),
|
||||||
|
sa.Column("filament_used_mm", sa.Float(), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=16), nullable=False, server_default="pending"),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["spool_id"],
|
||||||
|
["spool.id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(op.f("ix_print_job_id"), "print_job", ["id"], unique=False)
|
||||||
|
op.create_index(op.f("ix_print_job_spool_id"), "print_job", ["spool_id"], unique=False)
|
||||||
|
|
||||||
|
# Add needs_weighing column to spool table
|
||||||
|
op.add_column(
|
||||||
|
"spool",
|
||||||
|
sa.Column("needs_weighing", sa.Boolean(), nullable=True, server_default="0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Drop print_job table and remove needs_weighing from spool."""
|
||||||
|
op.drop_column("spool", "needs_weighing")
|
||||||
|
op.drop_index(op.f("ix_print_job_spool_id"), table_name="print_job")
|
||||||
|
op.drop_index(op.f("ix_print_job_id"), table_name="print_job")
|
||||||
|
op.drop_table("print_job")
|
||||||
@@ -331,6 +331,10 @@ class Spool(BaseModel):
|
|||||||
examples=[""],
|
examples=[""],
|
||||||
)
|
)
|
||||||
archived: bool = Field(description="Whether this spool is archived and should not be used anymore.")
|
archived: bool = Field(description="Whether this spool is archived and should not be used anymore.")
|
||||||
|
needs_weighing: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Whether this spool needs manual weigh-in (e.g., after a cancelled print).",
|
||||||
|
)
|
||||||
extra: dict[str, str] = Field(
|
extra: dict[str, str] = Field(
|
||||||
description=(
|
description=(
|
||||||
"Extra fields for this spool. All values are JSON-encoded data. "
|
"Extra fields for this spool. All values are JSON-encoded data. "
|
||||||
@@ -384,6 +388,7 @@ class Spool(BaseModel):
|
|||||||
lot_nr=item.lot_nr,
|
lot_nr=item.lot_nr,
|
||||||
comment=item.comment,
|
comment=item.comment,
|
||||||
archived=item.archived if item.archived is not None else False,
|
archived=item.archived if item.archived is not None else False,
|
||||||
|
needs_weighing=item.needs_weighing if item.needs_weighing is not None else False,
|
||||||
extra={field.key: field.value for field in item.extra},
|
extra={field.key: field.value for field in item.extra},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
199
spoolman/api/v1/print_job.py
Normal file
199
spoolman/api/v1/print_job.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
"""Print job API endpoints."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from spoolman.api.v1.models import Message, Spool
|
||||||
|
from spoolman.database import print_job
|
||||||
|
from spoolman.database.database import get_db_session
|
||||||
|
from spoolman.database.models import PrintJob as DBPrintJob
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/print-job",
|
||||||
|
tags=["print-job"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrintJobResponse(BaseModel):
|
||||||
|
"""Print job response model."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
spool_id: int
|
||||||
|
spool: Optional[Spool] = None
|
||||||
|
created: datetime
|
||||||
|
finished: Optional[datetime] = None
|
||||||
|
filename: str
|
||||||
|
filament_used_g: float
|
||||||
|
filament_used_mm: Optional[float] = None
|
||||||
|
status: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db(cls, db_item: DBPrintJob) -> "PrintJobResponse":
|
||||||
|
"""Create from database model."""
|
||||||
|
return cls(
|
||||||
|
id=db_item.id,
|
||||||
|
spool_id=db_item.spool_id,
|
||||||
|
spool=Spool.from_db(db_item.spool) if db_item.spool else None,
|
||||||
|
created=db_item.created,
|
||||||
|
finished=db_item.finished,
|
||||||
|
filename=db_item.filename,
|
||||||
|
filament_used_g=db_item.filament_used_g,
|
||||||
|
filament_used_mm=db_item.filament_used_mm,
|
||||||
|
status=db_item.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePrintJobRequest(BaseModel):
|
||||||
|
"""Request to create a print job."""
|
||||||
|
|
||||||
|
spool_id: int = Field(..., description="ID of the spool to use.")
|
||||||
|
filename: str = Field(..., max_length=256, description="G-code filename.")
|
||||||
|
filament_used_g: float = Field(..., ge=0, description="Estimated filament usage in grams.")
|
||||||
|
filament_used_mm: Optional[float] = Field(None, ge=0, description="Estimated filament usage in mm.")
|
||||||
|
|
||||||
|
|
||||||
|
class CompletePrintJobRequest(BaseModel):
|
||||||
|
"""Request to complete a print job."""
|
||||||
|
|
||||||
|
comment: Optional[str] = Field(None, max_length=256, description="Optional comment for adjustment history.")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"",
|
||||||
|
name="List print jobs",
|
||||||
|
description="Get a list of print jobs.",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={200: {"model": list[PrintJobResponse]}},
|
||||||
|
)
|
||||||
|
async def find(
|
||||||
|
*,
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
status: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
Query(description="Filter by status: 'pending', 'completed', 'cancelled'."),
|
||||||
|
] = None,
|
||||||
|
spool_id: Annotated[
|
||||||
|
Optional[int],
|
||||||
|
Query(description="Filter by spool ID."),
|
||||||
|
] = None,
|
||||||
|
limit: Annotated[
|
||||||
|
Optional[int],
|
||||||
|
Query(description="Maximum number of items to return."),
|
||||||
|
] = None,
|
||||||
|
offset: Annotated[
|
||||||
|
int,
|
||||||
|
Query(description="Offset for pagination."),
|
||||||
|
] = 0,
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Find print jobs."""
|
||||||
|
db_items, total_count = await print_job.find(
|
||||||
|
db=db,
|
||||||
|
status=status,
|
||||||
|
spool_id=spool_id,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content=jsonable_encoder(
|
||||||
|
[PrintJobResponse.from_db(item) for item in db_items],
|
||||||
|
exclude_none=True,
|
||||||
|
),
|
||||||
|
headers={"x-total-count": str(total_count)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{job_id}",
|
||||||
|
name="Get print job",
|
||||||
|
description="Get a specific print job.",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={404: {"model": Message}},
|
||||||
|
)
|
||||||
|
async def get(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
job_id: int,
|
||||||
|
) -> PrintJobResponse:
|
||||||
|
"""Get a print job by ID."""
|
||||||
|
db_item = await print_job.get_by_id(db, job_id)
|
||||||
|
return PrintJobResponse.from_db(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"",
|
||||||
|
name="Create print job",
|
||||||
|
description="Create a new pending print job (called by slicer post-processing script).",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={400: {"model": Message}, 404: {"model": Message}},
|
||||||
|
)
|
||||||
|
async def create(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
body: CreatePrintJobRequest,
|
||||||
|
) -> PrintJobResponse:
|
||||||
|
"""Create a new print job."""
|
||||||
|
db_item = await print_job.create(
|
||||||
|
db=db,
|
||||||
|
spool_id=body.spool_id,
|
||||||
|
filename=body.filename,
|
||||||
|
filament_used_g=body.filament_used_g,
|
||||||
|
filament_used_mm=body.filament_used_mm,
|
||||||
|
)
|
||||||
|
return PrintJobResponse.from_db(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{job_id}/complete",
|
||||||
|
name="Complete print job",
|
||||||
|
description="Mark a print job as completed - deducts filament from spool.",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={400: {"model": Message}, 404: {"model": Message}},
|
||||||
|
)
|
||||||
|
async def complete(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
job_id: int,
|
||||||
|
body: Optional[CompletePrintJobRequest] = None,
|
||||||
|
) -> PrintJobResponse:
|
||||||
|
"""Complete a print job."""
|
||||||
|
db_item = await print_job.complete(
|
||||||
|
db=db,
|
||||||
|
job_id=job_id,
|
||||||
|
comment=body.comment if body else None,
|
||||||
|
)
|
||||||
|
return PrintJobResponse.from_db(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{job_id}/cancel",
|
||||||
|
name="Cancel print job",
|
||||||
|
description="Mark a print job as cancelled - flags spool for manual weigh-in.",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={400: {"model": Message}, 404: {"model": Message}},
|
||||||
|
)
|
||||||
|
async def cancel(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
job_id: int,
|
||||||
|
) -> PrintJobResponse:
|
||||||
|
"""Cancel a print job."""
|
||||||
|
db_item = await print_job.cancel(db=db, job_id=job_id)
|
||||||
|
return PrintJobResponse.from_db(db_item)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{job_id}",
|
||||||
|
name="Delete print job",
|
||||||
|
description="Delete a print job.",
|
||||||
|
response_model=Message,
|
||||||
|
responses={404: {"model": Message}},
|
||||||
|
)
|
||||||
|
async def delete(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
job_id: int,
|
||||||
|
) -> Message:
|
||||||
|
"""Delete a print job."""
|
||||||
|
await print_job.delete(db=db, job_id=job_id)
|
||||||
|
return Message(message="Success!")
|
||||||
@@ -15,7 +15,7 @@ from spoolman.database.database import backup_global_db
|
|||||||
from spoolman.exceptions import ItemNotFoundError
|
from spoolman.exceptions import ItemNotFoundError
|
||||||
from spoolman.ws import websocket_manager
|
from spoolman.ws import websocket_manager
|
||||||
|
|
||||||
from . import backup as backup_module, export, externaldb, field, filament, models, other, setting, spool, vendor
|
from . import backup as backup_module, export, externaldb, field, filament, models, other, print_job, setting, spool, vendor
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -113,3 +113,4 @@ app.include_router(other.router)
|
|||||||
app.include_router(externaldb.router)
|
app.include_router(externaldb.router)
|
||||||
app.include_router(export.router)
|
app.include_router(export.router)
|
||||||
app.include_router(backup_module.router)
|
app.include_router(backup_module.router)
|
||||||
|
app.include_router(print_job.router)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class SpoolParameters(BaseModel):
|
|||||||
examples=[""],
|
examples=[""],
|
||||||
)
|
)
|
||||||
archived: bool = Field(default=False, description="Whether this spool is archived and should not be used anymore.")
|
archived: bool = Field(default=False, description="Whether this spool is archived and should not be used anymore.")
|
||||||
|
needs_weighing: bool = Field(default=False, description="Whether this spool needs manual weigh-in.")
|
||||||
extra: Optional[dict[str, str]] = Field(
|
extra: Optional[dict[str, str]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Extra fields for this spool.",
|
description="Extra fields for this spool.",
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ class Spool(Base):
|
|||||||
lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
|
lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
|
||||||
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
||||||
archived: Mapped[Optional[bool]] = mapped_column()
|
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(
|
extra: Mapped[list["SpoolField"]] = relationship(
|
||||||
back_populates="spool",
|
back_populates="spool",
|
||||||
cascade="save-update, merge, delete, delete-orphan",
|
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.")
|
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):
|
class Setting(Base):
|
||||||
__tablename__ = "setting"
|
__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