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