Add the measuringtools plugin (ADR-005) and the plugin-system tutorial
Gage-lab instruments as Asset extensions: measuringtooltypes (color-coded lookup) + measuringtools (calibration interval/dates, provider, notes) with calibration status derived at read time (overdue / due soon / current / unknown), never stored. Full CRUD API with permission-gated writes, types management with in-use guard, calibration report, nav/reports/config-schema hooks, and a complete frontend (list/detail/form, types settings page, calibration report page, gated routes per ADR-009). First plugin whose migration chain really creates tables post-cutover (ADR-008), and the working example for docs/PLUGIN-GUIDE.md - a 12-section walkthrough of building a plugin on this framework, linked from PLUGIN-QUICKSTART and PLUGINS. Verified: full suite 323 passing, live E2E on all four pages, fresh scratch-MySQL migration dry-run green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
17
plugins/measuringtools/models/__init__.py
Normal file
17
plugins/measuringtools/models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Measuring-tools plugin models."""
|
||||
|
||||
from .measuringtool import (
|
||||
MeasuringTool,
|
||||
MeasuringToolType,
|
||||
derive_status,
|
||||
STATUS_COLORS,
|
||||
DUESOON_WINDOW_DAYS,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'MeasuringTool',
|
||||
'MeasuringToolType',
|
||||
'derive_status',
|
||||
'STATUS_COLORS',
|
||||
'DUESOON_WINDOW_DAYS',
|
||||
]
|
||||
133
plugins/measuringtools/models/measuringtool.py
Normal file
133
plugins/measuringtools/models/measuringtool.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Measuring-tools models.
|
||||
|
||||
Measuring tools are gage-lab instruments (calipers, micrometers, thread gages,
|
||||
bore gages, height gages, Genspect heads, ...) that measure parts, as opposed
|
||||
to equipment that makes parts (see ADR-005). Each tool is a core Asset plus a
|
||||
one-to-one measuringtools extension row.
|
||||
|
||||
The lifecycle a measuring tool cares about is CALIBRATION, not maintenance:
|
||||
an interval, a last date, and a next date. Calibration STATUS is DERIVED from
|
||||
nextcalibrationdate at read time (see derive_status), never stored, so it is
|
||||
always current no matter how long since the last write. This mirrors the
|
||||
warranty plugin's derive-at-read pattern.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from shopdb.api import db, BaseModel
|
||||
|
||||
# Window before nextcalibrationdate where a tool counts as "due soon".
|
||||
DUESOON_WINDOW_DAYS = 30
|
||||
|
||||
# Derived status -> display color (hex). Reused by the frontend status badge.
|
||||
STATUS_COLORS = {
|
||||
'overdue': '#F44336',
|
||||
'duesoon': '#FF9800',
|
||||
'current': '#4CAF50',
|
||||
'unknown': '#9E9E9E',
|
||||
}
|
||||
|
||||
|
||||
def derive_status(nextcalibrationdate, today=None):
|
||||
"""Calibration status from a next-calibration date. Never stored.
|
||||
|
||||
overdue - the next date is in the past
|
||||
duesoon - the next date is within DUESOON_WINDOW_DAYS from today
|
||||
current - the next date is further out than the window
|
||||
unknown - no next date recorded
|
||||
"""
|
||||
if not nextcalibrationdate:
|
||||
return 'unknown'
|
||||
today = today or date.today()
|
||||
if nextcalibrationdate < today:
|
||||
return 'overdue'
|
||||
if nextcalibrationdate <= today + timedelta(days=DUESOON_WINDOW_DAYS):
|
||||
return 'duesoon'
|
||||
return 'current'
|
||||
|
||||
|
||||
class MeasuringToolType(BaseModel):
|
||||
"""Measuring-tool classification (Caliper, Micrometer, Thread Gage, ...).
|
||||
|
||||
Site-managed lookup with a display color for badges and map markers,
|
||||
the same shape as the equipment/computer/printer type tables.
|
||||
"""
|
||||
__tablename__ = 'measuringtooltypes'
|
||||
|
||||
measuringtooltypeid = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
color = db.Column(db.String(20), comment='CSS color for UI/map markers')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MeasuringToolType {self.name}>"
|
||||
|
||||
|
||||
class MeasuringTool(BaseModel):
|
||||
"""Measuring-tool extension data, one-to-one with a core Asset.
|
||||
|
||||
Identity lives on the Asset (assetnumber = gage tag, serialnumber = vendor
|
||||
serial, gaugelabreference identifier). This row carries only the metrology
|
||||
domain fields: the tool type and the calibration lifecycle.
|
||||
"""
|
||||
__tablename__ = 'measuringtools'
|
||||
|
||||
measuringtoolid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Link to the core asset (one extension row per asset).
|
||||
assetid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
measuringtooltypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('measuringtooltypes.measuringtooltypeid'),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
# Calibration lifecycle. Status is DERIVED from nextcalibrationdate, so
|
||||
# none of these columns store a status.
|
||||
calibrationintervaldays = db.Column(db.Integer, nullable=True)
|
||||
lastcalibrationdate = db.Column(db.Date, nullable=True)
|
||||
nextcalibrationdate = db.Column(db.Date, nullable=True)
|
||||
calibrationprovider = db.Column(db.String(150), nullable=True)
|
||||
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
asset = db.relationship(
|
||||
'Asset',
|
||||
backref=db.backref('measuringtool', uselist=False, lazy='joined'),
|
||||
)
|
||||
measuringtooltype = db.relationship('MeasuringToolType', backref='tools')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_measuringtool_type', 'measuringtooltypeid'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MeasuringTool {self.assetid}>"
|
||||
|
||||
def calibrationstatus(self, today=None):
|
||||
return derive_status(self.nextcalibrationdate, today)
|
||||
|
||||
def to_dict(self, today=None):
|
||||
"""Extension dict with the type name and derived calibration status."""
|
||||
result = super().to_dict()
|
||||
# BaseModel.to_dict only isoformats datetime, not plain date columns.
|
||||
# Emit calibration dates as 'YYYY-MM-DD' so the frontend parses them
|
||||
# the same way it parses warranty dates.
|
||||
for field in ('lastcalibrationdate', 'nextcalibrationdate'):
|
||||
value = getattr(self, field)
|
||||
result[field] = value.isoformat() if value else None
|
||||
if self.measuringtooltype:
|
||||
result['measuringtooltypename'] = self.measuringtooltype.name
|
||||
result['measuringtooltypecolor'] = self.measuringtooltype.color
|
||||
status = self.calibrationstatus(today)
|
||||
result['calibrationstatus'] = status
|
||||
result['calibrationstatuscolor'] = STATUS_COLORS.get(
|
||||
status, STATUS_COLORS['unknown'])
|
||||
return result
|
||||
Reference in New Issue
Block a user