printeditemfiles lands as the plugin's first incremental migration (0002 on the plugin chain - the ADR-008 payoff). Revisions are append-only per item: upload assigns the next number, records the uploader from the JWT, enforces an extension allowlist and a 100 MB cap; download serves the original filename; a permission-gated delete covers wrong-file mistakes. The detail page gains the revision table with a current badge. Unique storedfilename is sized 191 so the index fits MySQL's 767-byte prefix - the per-plugin chain does not apply the core env's ROW_FORMAT hook. Alert recipients gain roles: Role joins the 0.13.0 surface, a role picker on the settings page, and every active member of the selected roles is folded into the deduped recipient list.
136 lines
5.4 KiB
Python
136 lines
5.4 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')
|
|
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,
|
|
'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,
|
|
}
|