Label switches from CODE128 to a QR encoding 'TAG|rev' (gage lab tag + latest print-file revision), so a physical part carries which revision it was printed from - short payload stays low-version + reliable at 0.5in (margin quiet zone, EC M, no logo). Item exposes latestrevision; kiosk strips the |rev to resolve and records the scanned revision on the take (migration 0004 adds printeditemtransactions.revision) for traceability of which rev was consumed. Manual entry records a null revision.
153 lines
6.3 KiB
Python
153 lines
6.3 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
|
|
|
|
@property
|
|
def latestrevision(self):
|
|
"""Highest print-file revision, or None when the item has no files.
|
|
Stamped on the label QR so the physical part carries its source rev."""
|
|
from sqlalchemy import func
|
|
return db.session.query(func.max(PrintedItemFile.revision)).filter(
|
|
PrintedItemFile.printeditemid == self.printeditemid).scalar()
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'printeditemid': self.printeditemid,
|
|
'itemcode': self.itemcode,
|
|
'gagelabtag': self.gagelabtag,
|
|
'latestrevision': self.latestrevision,
|
|
'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))
|
|
# Print-file revision the physical part carried, captured from the label QR
|
|
# at take time (null for manual entry / pre-QR labels). Traceability of
|
|
# which revision was actually consumed.
|
|
revision = db.Column(db.Integer)
|
|
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,
|
|
'revision': self.revision,
|
|
'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,
|
|
}
|