"""Polymorphic Asset models - core of the new asset architecture.""" from shopdb.extensions import db from .base import BaseModel, SoftDeleteMixin, AuditMixin class AssetType(BaseModel): """ Registry of asset categories. Each type maps to a plugin-owned extension table. Examples: equipment, computer, network_device, printer """ __tablename__ = 'assettypes' assettypeid = db.Column(db.Integer, primary_key=True) assettype = db.Column( db.String(50), unique=True, nullable=False, comment='Category name: equipment, computer, network_device, printer' ) plugin_name = db.Column( db.String(100), nullable=True, comment='Plugin that owns this type' ) table_name = db.Column( db.String(100), nullable=True, comment='Extension table name for this type' ) description = db.Column(db.Text) icon = db.Column(db.String(50), comment='Icon name for UI') def __repr__(self): return f"" class AssetStatus(BaseModel): """Asset status options.""" __tablename__ = 'assetstatuses' statusid = db.Column(db.Integer, primary_key=True) status = db.Column(db.String(50), unique=True, nullable=False) description = db.Column(db.Text) color = db.Column(db.String(20), comment='CSS color for UI') def __repr__(self): return f"" class Asset(BaseModel, SoftDeleteMixin, AuditMixin): """ Core asset model - minimal shared fields. Category-specific data lives in plugin extension tables (equipment, computers, network_devices, printers). The assetid matches original machineid for migration compatibility. """ __tablename__ = 'assets' assetid = db.Column(db.Integer, primary_key=True) # Identification assetnumber = db.Column( db.String(50), unique=True, nullable=False, index=True, comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)' ) name = db.Column( db.String(100), comment='Display name/alias' ) serialnumber = db.Column( db.String(100), index=True, comment='Hardware serial number' ) # Classification assettypeid = db.Column( db.Integer, db.ForeignKey('assettypes.assettypeid'), nullable=False ) statusid = db.Column( db.Integer, db.ForeignKey('assetstatuses.statusid'), default=1, comment='In Use, Spare, Retired, etc.' ) # Location and organization locationid = db.Column( db.Integer, db.ForeignKey('locations.locationid'), nullable=True ) businessunitid = db.Column( db.Integer, db.ForeignKey('businessunits.businessunitid'), nullable=True ) # Floor map position mapleft = db.Column(db.Integer, comment='X coordinate on floor map') maptop = db.Column(db.Integer, comment='Y coordinate on floor map') # Notes notes = db.Column(db.Text, nullable=True) # Relationships assettype = db.relationship('AssetType', backref='assets') status = db.relationship('AssetStatus', backref='assets') location = db.relationship('Location', backref='assets') businessunit = db.relationship('BusinessUnit', backref='assets') # Communications (one-to-many) - will be migrated to use assetid communications = db.relationship( 'Communication', foreign_keys='Communication.assetid', backref='asset', cascade='all, delete-orphan', lazy='dynamic' ) # Indexes __table_args__ = ( db.Index('idx_asset_type_bu', 'assettypeid', 'businessunitid'), db.Index('idx_asset_location', 'locationid'), db.Index('idx_asset_active', 'isactive'), db.Index('idx_asset_status', 'statusid'), ) def __repr__(self): return f"" @property def display_name(self): """Get display name (name if set, otherwise assetnumber).""" return self.name or self.assetnumber @property def primary_ip(self): """Get primary IP address from communications.""" comm = self.communications.filter_by( isprimary=True, comtypeid=1 # IP type ).first() if comm: return comm.ipaddress # Fall back to any IP comm = self.communications.filter_by(comtypeid=1).first() return comm.ipaddress if comm else None def get_inherited_location(self): """ Get location data from a related asset if this asset has none. Returns dict with locationid, location_name, mapleft, maptop, and inherited_from (assetnumber of source asset) if location was inherited. Returns None if no location data available. """ # If we have our own location data, don't inherit if self.locationid is not None or (self.mapleft is not None and self.maptop is not None): return None # Check related assets for location data # Look in both incoming and outgoing relationships related_assets = [] if hasattr(self, 'incoming_relationships'): for rel in self.incoming_relationships: if rel.source_asset and rel.isactive: related_assets.append(rel.source_asset) if hasattr(self, 'outgoing_relationships'): for rel in self.outgoing_relationships: if rel.target_asset and rel.isactive: related_assets.append(rel.target_asset) # Find first related asset with location data for related in related_assets: if related.locationid is not None or (related.mapleft is not None and related.maptop is not None): return { 'locationid': related.locationid, 'location_name': related.location.locationname if related.location else None, 'mapleft': related.mapleft, 'maptop': related.maptop, 'inherited_from': related.assetnumber } return None def to_dict(self, include_type_data=False, include_inherited_location=True): """ Convert model to dictionary. Args: include_type_data: If True, include category-specific data from extension table include_inherited_location: If True, include location from related assets when missing """ result = super().to_dict() # Add related object names for convenience if self.assettype: result['assettype_name'] = self.assettype.assettype if self.status: result['status_name'] = self.status.status if self.location: result['location_name'] = self.location.locationname if self.businessunit: result['businessunit_name'] = self.businessunit.businessunit # Add plugin-specific ID for navigation purposes if hasattr(self, 'equipment') and self.equipment: result['plugin_id'] = self.equipment.equipmentid elif hasattr(self, 'computer') and self.computer: result['plugin_id'] = self.computer.computerid elif hasattr(self, 'network_device') and self.network_device: result['plugin_id'] = self.network_device.networkdeviceid elif hasattr(self, 'printer') and self.printer: result['plugin_id'] = self.printer.printerid # Include inherited location if this asset has no location data if include_inherited_location: inherited = self.get_inherited_location() if inherited: result['inherited_location'] = inherited # Also set the location fields if they're missing if result.get('locationid') is None: result['locationid'] = inherited['locationid'] result['location_name'] = inherited['location_name'] if result.get('mapleft') is None: result['mapleft'] = inherited['mapleft'] if result.get('maptop') is None: result['maptop'] = inherited['maptop'] # Include extension data if requested if include_type_data: ext_data = self._get_extension_data() if ext_data: result['type_data'] = ext_data return result def _get_extension_data(self): """Get category-specific data from extension table.""" # Check for equipment extension if hasattr(self, 'equipment') and self.equipment: return self.equipment.to_dict() # Check for computer extension if hasattr(self, 'computer') and self.computer: return self.computer.to_dict() # Check for network_device extension if hasattr(self, 'network_device') and self.network_device: return self.network_device.to_dict() # Check for printer extension if hasattr(self, 'printer') and self.printer: return self.printer.to_dict() return None