Files
shopdb-flask/plugins/printedparts/models/printeditem.py
cproudlock 3a3dff285e
Some checks failed
CI / backend (push) Successful in 1m45s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s
printedparts stage 17: gage-lab asset tag + print-files redesign
The gage lab assigns real WJRP asset numbers, so identity splits: the
internal itemcode stays auto-minted and a new optional unique
gagelabtag (migration 0003) carries the lab's number - settable on
create/edit, searchable, and resolved by the kiosk for scans and bare
keypad digits against the numeric tail of either identifier
(unique-match only). The print-files table becomes stacked revision
cards - filename with rev/current badges, one meta line, delete pinned
right - ending the horizontal scroll in that column.
2026-07-17 14:01:09 -04:00

139 lines
5.6 KiB
Python

"""Printedparts models.
PrintedItem is a KIND of 3D-printed part with a quantity on hand - a
consumable, not an ADR-001 asset (which is one row per physical thing).
PrintedItemTransaction is the ledger: every take, restock, and adjust as a
signed quantity change attributed to a badge-resolved employee. The ledger is
the source of truth; quantityonhand is a cache moved in the same commit as
each ledger write, and the stock report reconciles the two.
"""
from datetime import datetime, timezone
from shopdb.api import db, BaseModel
def _utcnow():
return datetime.now(timezone.utc).replace(tzinfo=None)
TRANSACTION_TYPES = ('take', 'restock', 'adjust')
class PrintedItem(BaseModel):
"""A printable part the engineers stock in bins."""
__tablename__ = 'printeditems'
printeditemid = db.Column(db.Integer, primary_key=True)
itemcode = db.Column(db.String(20), unique=True, index=True,
comment='Generated bin-label code, e.g. 3DP-0042')
gagelabtag = db.Column(db.String(50), unique=True, index=True,
comment='Gage-lab assigned asset tag, e.g. WJRP0117')
itemname = db.Column(db.String(120), nullable=False)
itemdescription = db.Column(db.String(500))
imageurl = db.Column(db.String(255))
quantityonhand = db.Column(db.Integer, nullable=False, default=0)
lowstockthreshold = db.Column(db.Integer, nullable=False, default=5)
binlocation = db.Column(db.String(100))
printnotes = db.Column(db.Text, comment='Material, print time, slicer file')
transactions = db.relationship(
'PrintedItemTransaction', backref='printeditem',
cascade='all, delete-orphan', passive_deletes=True, lazy='dynamic')
@property
def islowstock(self):
return self.quantityonhand <= self.lowstockthreshold
def to_dict(self):
return {
'printeditemid': self.printeditemid,
'itemcode': self.itemcode,
'gagelabtag': self.gagelabtag,
'itemname': self.itemname,
'itemdescription': self.itemdescription,
'imageurl': self.imageurl,
'quantityonhand': self.quantityonhand,
'lowstockthreshold': self.lowstockthreshold,
'islowstock': self.islowstock,
'binlocation': self.binlocation,
'printnotes': self.printnotes,
'isactive': self.isactive,
'createddate': self.createddate.isoformat() + 'Z' if self.createddate else None,
'modifieddate': self.modifieddate.isoformat() + 'Z' if self.modifieddate else None,
}
class PrintedItemTransaction(BaseModel):
"""One signed stock movement, always attributed to an employee."""
__tablename__ = 'printeditemtransactions'
transactionid = db.Column(db.Integer, primary_key=True)
printeditemid = db.Column(
db.Integer,
db.ForeignKey('printeditems.printeditemid', ondelete='CASCADE'),
nullable=False, index=True)
transactiontype = db.Column(db.String(10), nullable=False,
comment='take, restock, or adjust')
quantitychange = db.Column(db.Integer, nullable=False,
comment='Negative for take, signed for adjust')
employeesso = db.Column(db.String(20), nullable=False, index=True)
employeename = db.Column(db.String(120))
reason = db.Column(db.String(255))
transactiondate = db.Column(db.DateTime, nullable=False, default=_utcnow,
index=True)
def to_dict(self):
return {
'transactionid': self.transactionid,
'printeditemid': self.printeditemid,
'transactiontype': self.transactiontype,
'quantitychange': self.quantitychange,
'employeesso': self.employeesso,
'employeename': self.employeename,
'reason': self.reason,
'transactiondate': self.transactiondate.isoformat() + 'Z' if self.transactiondate else None,
}
class PrintedItemFile(BaseModel):
"""One uploaded revision of an item's print file (STL/3MF/gcode/...).
Revisions are append-only per item: uploading assigns the next revision
number and never replaces earlier files, so the history of what was
actually printed stays reconstructible. The current file is simply the
highest revision.
"""
__tablename__ = 'printeditemfiles'
fileid = db.Column(db.Integer, primary_key=True)
printeditemid = db.Column(
db.Integer,
db.ForeignKey('printeditems.printeditemid', ondelete='CASCADE'),
nullable=False, index=True)
revision = db.Column(db.Integer, nullable=False)
filename = db.Column(db.String(255), nullable=False,
comment='Original upload name, used for download')
storedfilename = db.Column(db.String(191), nullable=False, unique=True,
comment='191: unique index fits the 767-byte MySQL prefix')
filesize = db.Column(db.Integer, nullable=False)
uploadnote = db.Column(db.String(255),
comment='What changed in this revision')
uploadedby = db.Column(db.String(80), nullable=False,
comment='Username of the uploader')
def to_dict(self):
return {
'fileid': self.fileid,
'printeditemid': self.printeditemid,
'revision': self.revision,
'filename': self.filename,
'filesize': self.filesize,
'uploadnote': self.uploadnote,
'uploadedby': self.uploadedby,
'uploadeddate': self.createddate.isoformat() + 'Z' if self.createddate else None,
}