PrintedItem (catalog: code, name, image, cached quantityonhand, per-item threshold, bin) and PrintedItemTransaction (the ledger: signed quantity change attributed to a badge-resolved employee). Both registered in PLUGIN_TABLE_OWNERS; 0001 is a post-cutover real baseline. The migration-guard test learns the new expected head. Routes are a placeholder ping until the next stage - the scaffold's list route imported the deleted scaffold model, which surfaces as an empty 'Migration error' because the alembic env imports the models package.
96 lines
3.7 KiB
Python
96 lines
3.7 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,
|
|
}
|