Files
bambuddy/backend/app/models/print_log.py
T
maziggy 71a06f3638 Add batch orders with a quantity per plate (#342)
Printing a multi-plate file in different quantities per plate meant
queueing each plate separately and tracking the counts by hand: one
shared Quantity field cannot say "plate 1 once, plate 2 twice, plate 3
three times". Each selected plate now carries its own quantity, and the
submission becomes an order on a new Batches tab.

The point is the distinction the old flat batch could not express.
print_batch_plates stores how many runs of each plate were wanted,
separately from what was queued, so a run that fails, is cancelled or is
skipped does not satisfy a target -- the order goes on saying it owes a
print instead of quietly under-delivering. Queue remaining re-queues
exactly what is missing, for the whole order or one plate, by cloning
the most recent item for that plate: that inherits the printer or model
target, AMS mapping, filament overrides and print options along with the
validation they already passed, rather than re-serialising twenty fields
through a template that would drift from the model the first time
someone adds a column. Clones append to the end of the relevant
printer's queue and take the same advisory lock the add-to-queue route
does; positions are per-printer sequences, not global.

Cost is measured, not estimated. print_log_entries gains queue_item_id,
set where the queue item is already in scope, so each run's material and
energy are attributed through the item that produced them -- an
unrelated reprint of the same archive never lands in an order's total,
and a multi-plate order gets each plate's own cost rather than the whole
file's via the plate-scoped estimate from #2614. Before any run has
completed there is no honest figure, so cost reads as unknown instead of
a fabricated 0.00.

The Batches tab wires up GET /queue/batches, which has been unreferenced
since the batch MVP shipped, along with six locale keys that were
translated and never used. It is a separate tab because an order
outlives the queue that produced it: once its runs finish they leave the
active queue, so Queue and History each hold half the picture.

completed was not a reachable status before now, so every batch created
since April is still marked active however long ago its last print
finished -- 73 of them on the development install. A startup pass closes
out the finished ones: those whose runs all completed become completed,
and groupings whose items were all cancelled become cancelled, which is
what they are. Not applied to orders, which state their intent
independently of their runs and still owe the work. Only batches with
nothing queued or printing are considered, and repeating the pass also
catches an order whose last run landed while the process was down.
Batches with neither items nor targets are no longer listed at all --
empty shells left when a grouping's items went with their source
archive.

Dispatch applies the same source-file gates as POST /queue/. It creates
queue items, so without them it would be a weaker door to the same
outcome; the archive and library-file checks move into shared helpers
so a third route cannot drift from them.
2026-08-04 11:11:36 +02:00

52 lines
2.6 KiB
Python

from datetime import datetime
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.core.database import Base
class PrintLogEntry(Base):
"""Independent print log entry. Written when print events occur.
This is a separate table from archives/queue — clearing the log
never touches archives or queue items.
archive_id is a nullable FK so log entries survive archive deletion (ON
DELETE SET NULL). Aggregating runs per archive — for the per-archive
"Print Log" view and for statistics that should not double-count
overwritten archives (#1378) — is done via WHERE archive_id = X.
"""
__tablename__ = "print_log_entries"
id: Mapped[int] = mapped_column(primary_key=True)
archive_id: Mapped[int | None] = mapped_column(
ForeignKey("print_archives.id", ondelete="SET NULL"), nullable=True, index=True
)
# Which queue item produced this run, when one did. Printer-initiated
# prints have none. Batch cost/energy roll-up joins on this (#342): the
# archive alone can't attribute a run to an order because several orders
# — and plain reprints — share one archive.
queue_item_id: Mapped[int | None] = mapped_column(
ForeignKey("print_queue.id", ondelete="SET NULL"), nullable=True, index=True
)
print_name: Mapped[str | None] = mapped_column(String(255))
printer_name: Mapped[str | None] = mapped_column(String(255))
printer_id: Mapped[int | None] = mapped_column(Integer)
status: Mapped[str] = mapped_column(String(20)) # completed, failed, stopped, cancelled, skipped
started_at: Mapped[datetime | None] = mapped_column(DateTime)
completed_at: Mapped[datetime | None] = mapped_column(DateTime)
duration_seconds: Mapped[int | None] = mapped_column(Integer)
filament_type: Mapped[str | None] = mapped_column(String(50))
filament_color: Mapped[str | None] = mapped_column(String(50))
filament_used_grams: Mapped[float | None] = mapped_column(Float)
cost: Mapped[float | None] = mapped_column(Float)
energy_kwh: Mapped[float | None] = mapped_column(Float)
energy_cost: Mapped[float | None] = mapped_column(Float)
failure_reason: Mapped[str | None] = mapped_column(String(100))
thumbnail_path: Mapped[str | None] = mapped_column(String(500))
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_by_username: Mapped[str | None] = mapped_column(String(100))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())