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>
97 lines
3.2 KiB
Python
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'),
|
|
)
|