"""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, }