printedparts stage 17: gage-lab asset tag + print-files redesign
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

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.
This commit is contained in:
cproudlock
2026-07-17 14:01:09 -04:00
parent 6160a5142a
commit 3a3dff285e
8 changed files with 185 additions and 52 deletions

View File

@@ -41,6 +41,7 @@ def list_items():
like = f'%{search}%'
query = query.filter(or_(
PrintedItem.itemcode.ilike(like),
PrintedItem.gagelabtag.ilike(like),
PrintedItem.itemname.ilike(like),
PrintedItem.itemdescription.ilike(like),
PrintedItem.binlocation.ilike(like),
@@ -103,18 +104,28 @@ def _mint_itemcode(item):
@jwt_required()
@require_permission('printedparts.create')
def create_item():
"""Create a printed item; the itemcode is minted from the row id."""
"""Create a printed item.
The internal itemcode is always auto-minted; the OPTIONAL gagelabtag
carries the gage lab's assigned WJRP asset number (unique-checked)."""
data = request.get_json() or {}
itemname = (data.get('itemname') or '').strip()
if not itemname:
return error_response(ErrorCodes.VALIDATION_ERROR, 'itemname is required')
gagelabtag = (data.get('gagelabtag') or '').strip().upper()
if gagelabtag and PrintedItem.query.filter_by(gagelabtag=gagelabtag).first():
return error_response(ErrorCodes.CONFLICT,
f'Gage lab tag {gagelabtag} is already in use',
http_code=409)
threshold = data.get('lowstockthreshold')
if threshold is None:
threshold = int(Setting.get('printedparts_default_threshold') or 5)
item = PrintedItem(
itemname=itemname,
gagelabtag=gagelabtag or None,
itemdescription=data.get('itemdescription'),
lowstockthreshold=threshold,
binlocation=data.get('binlocation'),
@@ -143,6 +154,18 @@ def update_item(item_id: int):
return error_response(
ErrorCodes.VALIDATION_ERROR,
'quantityonhand is ledger-managed; use restock or adjust')
if 'gagelabtag' in data:
gagelabtag = (data.get('gagelabtag') or '').strip().upper()
if gagelabtag:
clash = PrintedItem.query.filter(
PrintedItem.gagelabtag == gagelabtag,
PrintedItem.printeditemid != item.printeditemid).first()
if clash:
return error_response(
ErrorCodes.CONFLICT,
f'Gage lab tag {gagelabtag} is already in use',
http_code=409)
item.gagelabtag = gagelabtag or None
for field in EDITABLE_FIELDS:
if field in data:
setattr(item, field, data[field])
@@ -391,16 +414,25 @@ def _kiosk_find_item(itemcode):
Accepts the full code (WJRP0042) or bare digits from the touch keypad
(42 -> prefix + zero-pad), so manual entry never needs letters."""
itemcode = (itemcode or '').strip()
scanned = (itemcode or '').strip().upper()
item = PrintedItem.query.filter(
PrintedItem.itemcode == itemcode,
or_(PrintedItem.itemcode == scanned,
PrintedItem.gagelabtag == scanned),
PrintedItem.isactive == True).first()
if not item and itemcode.isdigit():
# The digits in a minted code ARE the row id, so id lookup keeps
# working even for labels printed under an older prefix.
candidate = db.session.get(PrintedItem, int(itemcode))
if candidate and candidate.isactive:
item = candidate
if not item and scanned.isdigit():
# Bare digits from the touch keypad match the NUMBER inside either
# identifier (internal code or gage-lab tag). Small catalog: scan
# actives and compare numeric tails; only a UNIQUE match counts.
wanted = int(scanned)
matches = []
for candidate in PrintedItem.query.filter_by(isactive=True).all():
for value in (candidate.itemcode, candidate.gagelabtag):
tail = ''.join(ch for ch in (value or '') if ch.isdigit())
if tail and int(tail) == wanted:
matches.append(candidate)
break
if len(matches) == 1:
item = matches[0]
return item

View File

@@ -0,0 +1,27 @@
"""Add printeditems.gagelabtag: the gage-lab assigned asset tag.
The gage lab issues WJRP asset numbers for printed parts; the internal
itemcode stays auto-generated, and this optional unique tag carries the
lab's number. The kiosk resolves scans/typed digits against both.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'printedparts0003gagetag'
down_revision = 'printedparts0002files'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('printeditems',
sa.Column('gagelabtag', sa.String(length=50), nullable=True))
op.create_index('ix_printeditems_gagelabtag', 'printeditems',
['gagelabtag'], unique=True)
def downgrade():
op.drop_index('ix_printeditems_gagelabtag', table_name='printeditems')
op.drop_column('printeditems', 'gagelabtag')

View File

@@ -28,6 +28,8 @@ class PrintedItem(BaseModel):
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))
@@ -48,6 +50,7 @@ class PrintedItem(BaseModel):
return {
'printeditemid': self.printeditemid,
'itemcode': self.itemcode,
'gagelabtag': self.gagelabtag,
'itemname': self.itemname,
'itemdescription': self.itemdescription,
'imageurl': self.imageurl,