Files
shopdb-flask/plugins/warranty/models/warranty.py
cproudlock 78a0ee8d83 Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session:

Settings IA
- Replace the flat 27-card settings hub with a persistent two-pane shell
  (SettingsLayout.vue): grouped, searchable left rail + content pane.
- Nest all settings/* routes under the shell via router post-processing;
  shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers,
  Equipment, Network) so per-type settings stop scattering.

Custom fields (core)
- customfields + customfieldvalues tables (migration 7d14), CRUD API at
  /api/customfields, per-asset value get/save.
- Settings management page + reusable CustomFieldsSection (detail) and
  CustomFieldsInputs (form) wired into all four asset types.

Warranty (new plugin)
- plugins/warranty: warranties + warrantyassets (migration 7d15), derived
  coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs).
- API CRUD + per-asset panel + report buckets; WarrantyPanel on all four
  detail pages; Warranties management page; Warranty report + Reports card.
- Seed warranty.* permissions.

Printer drivers
- printerdrivers table (migration 7d13) linked to printer models; drivers now
  surface on the matching printer's detail page.

Other
- PCDetail rebalanced (Network + Status + Warranty + custom fields on the right).
- Rename PCs list "Features" column to "Remote Access"; fix badge hover underline.
- Drop equipment islocationonly field.
- Centralize asset-type label/route maps into utils/assetTypes.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:37:21 -04:00

97 lines
3.2 KiB
Python

"""Warranty models.
A Warranty is provider-agnostic (manual entry, or looked up from Dell/Lenovo/HP
later). It links to one or more assets via warrantyassets. Coverage status is
DERIVED from enddate at read time, never stored, so it is always current.
"""
from datetime import date, timedelta
from shopdb.api import db
# Window before enddate where a warranty counts as "expiring soon".
EXPIRING_WINDOW_DAYS = 180
# Derived status -> display color (hex). Reused by the frontend status badge.
STATUS_COLORS = {
'active': '#4CAF50',
'expiring': '#FF9800',
'expired': '#F44336',
'unknown': '#9E9E9E',
}
def derive_status(enddate, today=None):
"""Coverage status from an end date. Never stored - always computed."""
if not enddate:
return 'unknown'
today = today or date.today()
if enddate < today:
return 'expired'
if enddate <= today + timedelta(days=EXPIRING_WINDOW_DAYS):
return 'expiring'
return 'active'
class Warranty(db.Model):
__tablename__ = 'warranties'
warrantyid = db.Column(db.Integer, primary_key=True)
vendor = db.Column(db.String(100), nullable=False)
# Service tag / serial the provider identifies the unit by.
servicetag = db.Column(db.String(100))
# Where the record came from: manual, dell, lenovo, hp.
provider = db.Column(db.String(20), nullable=False, server_default='manual')
servicelevel = db.Column(db.String(150))
startdate = db.Column(db.Date)
enddate = db.Column(db.Date)
# When a provider lookup last refreshed this record.
lastcheckeddate = db.Column(db.DateTime)
notes = db.Column(db.Text)
isactive = db.Column(db.Boolean, nullable=False, server_default='1')
links = db.relationship('WarrantyAsset', back_populates='warranty',
cascade='all, delete-orphan')
def status(self, today=None):
return derive_status(self.enddate, today)
def to_dict(self, today=None):
status = self.status(today)
return {
'warrantyid': self.warrantyid,
'vendor': self.vendor,
'servicetag': self.servicetag,
'provider': self.provider,
'servicelevel': self.servicelevel,
'startdate': self.startdate.isoformat() if self.startdate else None,
'enddate': self.enddate.isoformat() if self.enddate else None,
'lastcheckeddate': self.lastcheckeddate.isoformat() + 'Z' if self.lastcheckeddate else None,
'notes': self.notes,
'isactive': bool(self.isactive),
'status': status,
'statuscolor': STATUS_COLORS.get(status, STATUS_COLORS['unknown']),
}
class WarrantyAsset(db.Model):
__tablename__ = 'warrantyassets'
id = db.Column(db.Integer, primary_key=True)
warrantyid = db.Column(
db.Integer,
db.ForeignKey('warranties.warrantyid', ondelete='CASCADE'),
nullable=False
)
assetid = db.Column(
db.Integer,
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
nullable=False
)
warranty = db.relationship('Warranty', back_populates='links')
__table_args__ = (
db.UniqueConstraint('warrantyid', 'assetid', name='uq_warrantyasset_warranty_asset'),
)