Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail

This commit is contained in:
2026-04-24 07:14:27 +03:00
commit e63b486ec2
58 changed files with 6468 additions and 0 deletions

114
app/models/asset.py Normal file
View File

@@ -0,0 +1,114 @@
from datetime import datetime
from app.extensions import db
ASSET_TYPES = [
'Laptop', 'Desktop', 'Monitor', 'Keyboard', 'Mouse',
'Headset', 'Docking Station', 'Printer', 'Scanner',
'Tablet', 'Phone', 'Server', 'Network Equipment', 'Other',
]
ASSET_STATUSES = [
('available', 'Available'),
('assigned', 'Assigned'),
('maintenance', 'In Maintenance'),
('retired', 'Retired'),
('lost', 'Lost / Stolen'),
]
class Asset(db.Model):
"""Hardware asset tracked by serial number and/or service tag."""
__tablename__ = 'assets'
id = db.Column(db.Integer, primary_key=True)
# Primary identifiers
serial_number = db.Column(db.String(200), unique=True, nullable=False, index=True)
service_tag = db.Column(db.String(200), unique=True, nullable=True, index=True)
asset_tag = db.Column(db.String(100), nullable=True) # internal barcode / tag
# Classification
asset_type = db.Column(db.String(50), nullable=False)
brand = db.Column(db.String(100), nullable=True)
model = db.Column(db.String(150), nullable=True)
# Technical specs (optional)
processor = db.Column(db.String(200), nullable=True)
ram_gb = db.Column(db.Integer, nullable=True)
storage_gb = db.Column(db.Integer, nullable=True)
operating_system = db.Column(db.String(100), nullable=True)
mac_address = db.Column(db.String(50), nullable=True)
# Procurement
purchase_date = db.Column(db.Date, nullable=True)
warranty_expiry = db.Column(db.Date, nullable=True)
purchase_price = db.Column(db.Numeric(10, 2), nullable=True)
supplier = db.Column(db.String(200), nullable=True)
po_number = db.Column(db.String(100), nullable=True)
# Current state
status = db.Column(db.String(30), default='available', nullable=False)
location = db.Column(db.String(200), nullable=True)
notes = db.Column(db.Text, nullable=True)
# Compliance / IT checks — Desktop & Laptop only
inventory_number = db.Column(db.String(100), nullable=True)
ad_device_name = db.Column(db.String(150), nullable=True)
location_note = db.Column(db.Text, nullable=True) # free-text location note
# Current boolean state
encryption_checked = db.Column(db.Boolean, default=False, nullable=False)
backup_checked = db.Column(db.Boolean, default=False, nullable=False)
hr_notified = db.Column(db.Boolean, default=False, nullable=False)
# Who last changed each check and when
encryption_checked_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
encryption_checked_at = db.Column(db.DateTime, nullable=True)
backup_checked_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
backup_checked_at = db.Column(db.DateTime, nullable=True)
hr_notified_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
hr_notified_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
# Relationships
assignments = db.relationship(
'Assignment', backref='asset', lazy='dynamic', cascade='all, delete-orphan'
)
paperwork_docs = db.relationship(
'Paperwork', backref='asset', lazy='dynamic'
)
compliance_checks = db.relationship(
'ComplianceCheck', back_populates='asset', lazy='dynamic',
cascade='all, delete-orphan',
order_by='ComplianceCheck.performed_at.desc()',
)
created_by = db.relationship('AdminUser', foreign_keys=[created_by_id])
encryption_checked_by = db.relationship('AdminUser', foreign_keys=[encryption_checked_by_id])
backup_checked_by = db.relationship('AdminUser', foreign_keys=[backup_checked_by_id])
hr_notified_by = db.relationship('AdminUser', foreign_keys=[hr_notified_by_id])
@property
def current_assignment(self):
return self.assignments.filter_by(is_active=True).first()
@property
def current_user(self):
a = self.current_assignment
return a.user if a else None
@property
def status_badge(self):
colours = {
'available': 'success',
'assigned': 'primary',
'maintenance': 'warning',
'retired': 'secondary',
'lost': 'danger',
}
return colours.get(self.status, 'secondary')
def __repr__(self):
return f'<Asset sn={self.serial_number} type={self.asset_type}>'