mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
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.
101 lines
4.5 KiB
Python
101 lines
4.5 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class PrintBatch(Base):
|
|
"""Batch grouping for multiple queue items created from the same file.
|
|
|
|
A batch carries the *intent* — how many of each plate are wanted — in its
|
|
:class:`PrintBatchPlate` rows, while the queue items it spawned carry what
|
|
was actually dispatched. Keeping the two apart is what lets a failed print
|
|
still count as owed work: the plate row's ``quantity_target`` stays put
|
|
while the failed item lands in the "failed" bucket, so ``remaining`` goes
|
|
back up instead of the order silently under-delivering (#342).
|
|
|
|
Batches created before plate rows existed simply have none; every consumer
|
|
falls back to deriving progress from the queue items alone.
|
|
"""
|
|
|
|
__tablename__ = "print_batches"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255))
|
|
|
|
# Source file (one of these)
|
|
archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id", ondelete="SET NULL"), nullable=True)
|
|
library_file_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("library_files.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
|
|
# Total requested quantity (for display — actual items may differ if cancelled)
|
|
quantity: Mapped[int] = mapped_column(Integer, default=1)
|
|
|
|
# Status: active, completed, cancelled
|
|
status: Mapped[str] = mapped_column(String(20), default="active")
|
|
|
|
# Optional link to a Project, which owns the heavier planning metadata
|
|
# (BOM, attachments, tags). The batch keeps only the two fields that are
|
|
# useless without it — a date and free text — so an order doesn't force
|
|
# the user to create a Project first.
|
|
project_id: Mapped[int | None] = mapped_column(ForeignKey("projects.id", ondelete="SET NULL"), nullable=True)
|
|
due_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
# Timestamps
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# User tracking
|
|
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
|
|
# Relationships
|
|
archive: Mapped["PrintArchive | None"] = relationship()
|
|
library_file: Mapped["LibraryFile | None"] = relationship()
|
|
created_by: Mapped["User | None"] = relationship()
|
|
queue_items: Mapped[list["PrintQueueItem"]] = relationship(back_populates="batch")
|
|
plates: Mapped[list["PrintBatchPlate"]] = relationship(
|
|
back_populates="batch",
|
|
cascade="all, delete-orphan",
|
|
order_by="PrintBatchPlate.sort_order",
|
|
)
|
|
|
|
|
|
class PrintBatchPlate(Base):
|
|
"""How many runs of one plate a batch still owes.
|
|
|
|
``plate_id`` is the plate index within the source 3MF, or NULL for a
|
|
single-plate file / whole-file print — the same convention
|
|
``PrintQueueItem.plate_id`` uses, so progress can be derived by grouping
|
|
the batch's items on that column.
|
|
"""
|
|
|
|
__tablename__ = "print_batch_plates"
|
|
__table_args__ = (UniqueConstraint("batch_id", "plate_id", name="uq_batch_plate"),)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
batch_id: Mapped[int] = mapped_column(
|
|
ForeignKey("print_batches.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
|
|
plate_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
plate_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
|
|
# How many runs of this plate the order wants. Zero is legal — a plate the
|
|
# user explicitly marked "not required" keeps its row so it can be raised
|
|
# later without re-creating the order.
|
|
quantity_target: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
|
|
# Display order; mirrors the plate order in the source file.
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
|
|
batch: Mapped["PrintBatch"] = relationship(back_populates="plates")
|
|
|
|
|
|
from backend.app.models.archive import PrintArchive # noqa: E402
|
|
from backend.app.models.library import LibraryFile # noqa: E402
|
|
from backend.app.models.print_queue import PrintQueueItem # noqa: E402
|
|
from backend.app.models.user import User # noqa: E402
|