Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
659
shopdb/core/api/assets.py
Normal file
659
shopdb/core/api/assets.py
Normal file
@@ -0,0 +1,659 @@
|
||||
"""Assets API endpoints - unified asset queries."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, AssetType, AssetStatus, AssetRelationship, RelationshipType
|
||||
from shopdb.utils.responses import (
|
||||
success_response,
|
||||
error_response,
|
||||
paginated_response,
|
||||
ErrorCodes
|
||||
)
|
||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
assets_bp = Blueprint('assets', __name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Types
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/types', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_asset_types():
|
||||
"""List all asset types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = AssetType.query
|
||||
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(AssetType.isactive == True)
|
||||
|
||||
query = query.order_by(AssetType.assettype)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [t.to_dict() for t in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@assets_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_asset_type(type_id: int):
|
||||
"""Get a single asset type."""
|
||||
t = AssetType.query.get(type_id)
|
||||
|
||||
if not t:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset type with ID {type_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
return success_response(t.to_dict())
|
||||
|
||||
|
||||
@assets_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_asset_type():
|
||||
"""Create a new asset type."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('assettype'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'assettype is required')
|
||||
|
||||
if AssetType.query.filter_by(assettype=data['assettype']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset type '{data['assettype']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
t = AssetType(
|
||||
assettype=data['assettype'],
|
||||
plugin_name=data.get('plugin_name'),
|
||||
table_name=data.get('table_name'),
|
||||
description=data.get('description'),
|
||||
icon=data.get('icon')
|
||||
)
|
||||
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(t.to_dict(), message='Asset type created', http_code=201)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Statuses
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/statuses', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_asset_statuses():
|
||||
"""List all asset statuses."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = AssetStatus.query
|
||||
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(AssetStatus.isactive == True)
|
||||
|
||||
query = query.order_by(AssetStatus.status)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [s.to_dict() for s in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_asset_status(status_id: int):
|
||||
"""Get a single asset status."""
|
||||
s = AssetStatus.query.get(status_id)
|
||||
|
||||
if not s:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset status with ID {status_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
return success_response(s.to_dict())
|
||||
|
||||
|
||||
@assets_bp.route('/statuses', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_asset_status():
|
||||
"""Create a new asset status."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('status'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'status is required')
|
||||
|
||||
if AssetStatus.query.filter_by(status=data['status']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset status '{data['status']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
s = AssetStatus(
|
||||
status=data['status'],
|
||||
description=data.get('description'),
|
||||
color=data.get('color')
|
||||
)
|
||||
|
||||
db.session.add(s)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(s.to_dict(), message='Asset status created', http_code=201)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Assets
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_assets():
|
||||
"""
|
||||
List all assets with filtering and pagination.
|
||||
|
||||
Query parameters:
|
||||
- page: Page number (default: 1)
|
||||
- per_page: Items per page (default: 20, max: 100)
|
||||
- active: Filter by active status (default: true)
|
||||
- search: Search by assetnumber or name
|
||||
- type: Filter by asset type name (e.g., 'equipment', 'computer')
|
||||
- type_id: Filter by asset type ID
|
||||
- status_id: Filter by status ID
|
||||
- location_id: Filter by location ID
|
||||
- businessunit_id: Filter by business unit ID
|
||||
- include_type_data: Include category-specific extension data (default: false)
|
||||
"""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = Asset.query
|
||||
|
||||
# Active filter
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(Asset.isactive == True)
|
||||
|
||||
# Search filter
|
||||
if search := request.args.get('search'):
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Asset.assetnumber.ilike(f'%{search}%'),
|
||||
Asset.name.ilike(f'%{search}%'),
|
||||
Asset.serialnumber.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
# Type filter by name
|
||||
if type_name := request.args.get('type'):
|
||||
query = query.join(AssetType).filter(AssetType.assettype == type_name)
|
||||
|
||||
# Type filter by ID
|
||||
if type_id := request.args.get('type_id'):
|
||||
query = query.filter(Asset.assettypeid == int(type_id))
|
||||
|
||||
# Status filter
|
||||
if status_id := request.args.get('status_id'):
|
||||
query = query.filter(Asset.statusid == int(status_id))
|
||||
|
||||
# Location filter
|
||||
if location_id := request.args.get('location_id'):
|
||||
query = query.filter(Asset.locationid == int(location_id))
|
||||
|
||||
# Business unit filter
|
||||
if bu_id := request.args.get('businessunit_id'):
|
||||
query = query.filter(Asset.businessunitid == int(bu_id))
|
||||
|
||||
# Sorting
|
||||
sort_by = request.args.get('sort', 'assetnumber')
|
||||
sort_dir = request.args.get('dir', 'asc')
|
||||
|
||||
sort_columns = {
|
||||
'assetnumber': Asset.assetnumber,
|
||||
'name': Asset.name,
|
||||
'createddate': Asset.createddate,
|
||||
'modifieddate': Asset.modifieddate,
|
||||
}
|
||||
|
||||
if sort_by in sort_columns:
|
||||
col = sort_columns[sort_by]
|
||||
query = query.order_by(col.desc() if sort_dir == 'desc' else col)
|
||||
else:
|
||||
query = query.order_by(Asset.assetnumber)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
|
||||
# Include type data if requested
|
||||
include_type_data = request.args.get('include_type_data', 'false').lower() == 'true'
|
||||
data = [a.to_dict(include_type_data=include_type_data) for a in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_asset(asset_id: int):
|
||||
"""
|
||||
Get a single asset with full details.
|
||||
|
||||
Query parameters:
|
||||
- include_type_data: Include category-specific extension data (default: true)
|
||||
"""
|
||||
asset = Asset.query.get(asset_id)
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with ID {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
include_type_data = request.args.get('include_type_data', 'true').lower() != 'false'
|
||||
return success_response(asset.to_dict(include_type_data=include_type_data))
|
||||
|
||||
|
||||
@assets_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_asset():
|
||||
"""Create a new asset."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('assetnumber'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'assetnumber is required')
|
||||
if not data.get('assettypeid'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'assettypeid is required')
|
||||
|
||||
# Check for duplicate assetnumber
|
||||
if Asset.query.filter_by(assetnumber=data['assetnumber']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset with number '{data['assetnumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Validate foreign keys exist
|
||||
if not AssetType.query.get(data['assettypeid']):
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
f"Asset type with ID {data['assettypeid']} not found"
|
||||
)
|
||||
|
||||
asset = Asset(
|
||||
assetnumber=data['assetnumber'],
|
||||
name=data.get('name'),
|
||||
serialnumber=data.get('serialnumber'),
|
||||
assettypeid=data['assettypeid'],
|
||||
statusid=data.get('statusid', 1),
|
||||
locationid=data.get('locationid'),
|
||||
businessunitid=data.get('businessunitid'),
|
||||
mapleft=data.get('mapleft'),
|
||||
maptop=data.get('maptop'),
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(asset)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(asset.to_dict(), message='Asset created', http_code=201)
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_asset(asset_id: int):
|
||||
"""Update an asset."""
|
||||
asset = Asset.query.get(asset_id)
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with ID {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Check for conflicting assetnumber
|
||||
if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber:
|
||||
if Asset.query.filter_by(assetnumber=data['assetnumber']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Asset with number '{data['assetnumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Update allowed fields
|
||||
allowed_fields = [
|
||||
'assetnumber', 'name', 'serialnumber', 'assettypeid', 'statusid',
|
||||
'locationid', 'businessunitid', 'mapleft', 'maptop', 'notes', 'isactive'
|
||||
]
|
||||
|
||||
for key in allowed_fields:
|
||||
if key in data:
|
||||
setattr(asset, key, data[key])
|
||||
|
||||
db.session.commit()
|
||||
return success_response(asset.to_dict(), message='Asset updated')
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_asset(asset_id: int):
|
||||
"""Delete (soft delete) an asset."""
|
||||
asset = Asset.query.get(asset_id)
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with ID {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
asset.isactive = False
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Asset deleted')
|
||||
|
||||
|
||||
@assets_bp.route('/lookup/<assetnumber>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def lookup_asset_by_number(assetnumber: str):
|
||||
"""
|
||||
Look up an asset by its asset number.
|
||||
|
||||
Useful for finding the asset ID when you only have the machine/asset number.
|
||||
"""
|
||||
asset = Asset.query.filter_by(assetnumber=assetnumber, isactive=True).first()
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with number {assetnumber} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
return success_response(asset.to_dict(include_type_data=True))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Relationships
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/<int:asset_id>/relationships', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_asset_relationships(asset_id: int):
|
||||
"""
|
||||
Get all relationships for an asset.
|
||||
|
||||
Returns both outgoing (source) and incoming (target) relationships.
|
||||
"""
|
||||
asset = Asset.query.get(asset_id)
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with ID {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
# Get outgoing relationships (this asset is source)
|
||||
outgoing = AssetRelationship.query.filter_by(
|
||||
source_assetid=asset_id
|
||||
).filter(AssetRelationship.isactive == True).all()
|
||||
|
||||
# Get incoming relationships (this asset is target)
|
||||
incoming = AssetRelationship.query.filter_by(
|
||||
target_assetid=asset_id
|
||||
).filter(AssetRelationship.isactive == True).all()
|
||||
|
||||
outgoing_data = []
|
||||
for rel in outgoing:
|
||||
r = rel.to_dict()
|
||||
r['target_asset'] = rel.target_asset.to_dict() if rel.target_asset else None
|
||||
r['relationship_type_name'] = rel.relationship_type.relationshiptype if rel.relationship_type else None
|
||||
outgoing_data.append(r)
|
||||
|
||||
incoming_data = []
|
||||
for rel in incoming:
|
||||
r = rel.to_dict()
|
||||
r['source_asset'] = rel.source_asset.to_dict() if rel.source_asset else None
|
||||
r['relationship_type_name'] = rel.relationship_type.relationshiptype if rel.relationship_type else None
|
||||
incoming_data.append(r)
|
||||
|
||||
return success_response({
|
||||
'outgoing': outgoing_data,
|
||||
'incoming': incoming_data
|
||||
})
|
||||
|
||||
|
||||
@assets_bp.route('/relationships', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_asset_relationship():
|
||||
"""Create a relationship between two assets."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Validate required fields
|
||||
required = ['source_assetid', 'target_assetid', 'relationshiptypeid']
|
||||
for field in required:
|
||||
if not data.get(field):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, f'{field} is required')
|
||||
|
||||
source_id = data['source_assetid']
|
||||
target_id = data['target_assetid']
|
||||
type_id = data['relationshiptypeid']
|
||||
|
||||
# Validate assets exist
|
||||
if not Asset.query.get(source_id):
|
||||
return error_response(ErrorCodes.NOT_FOUND, f'Source asset {source_id} not found', http_code=404)
|
||||
if not Asset.query.get(target_id):
|
||||
return error_response(ErrorCodes.NOT_FOUND, f'Target asset {target_id} not found', http_code=404)
|
||||
if not RelationshipType.query.get(type_id):
|
||||
return error_response(ErrorCodes.NOT_FOUND, f'Relationship type {type_id} not found', http_code=404)
|
||||
|
||||
# Check for duplicate relationship
|
||||
existing = AssetRelationship.query.filter_by(
|
||||
source_assetid=source_id,
|
||||
target_assetid=target_id,
|
||||
relationshiptypeid=type_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
'This relationship already exists',
|
||||
http_code=409
|
||||
)
|
||||
|
||||
rel = AssetRelationship(
|
||||
source_assetid=source_id,
|
||||
target_assetid=target_id,
|
||||
relationshiptypeid=type_id,
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(rel.to_dict(), message='Relationship created', http_code=201)
|
||||
|
||||
|
||||
@assets_bp.route('/relationships/<int:rel_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_asset_relationship(rel_id: int):
|
||||
"""Delete an asset relationship."""
|
||||
rel = AssetRelationship.query.get(rel_id)
|
||||
|
||||
if not rel:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Relationship with ID {rel_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
rel.isactive = False
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Relationship deleted')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Communications
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Unified Asset Map
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/map', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_assets_map():
|
||||
"""
|
||||
Get all assets with map positions for unified floor map display.
|
||||
|
||||
Returns assets with mapleft/maptop coordinates, joined with type-specific data.
|
||||
|
||||
Query parameters:
|
||||
- assettype: Filter by asset type name (equipment, computer, network, printer)
|
||||
- businessunitid: Filter by business unit ID
|
||||
- statusid: Filter by status ID
|
||||
- locationid: Filter by location ID
|
||||
- search: Search by assetnumber, name, or serialnumber
|
||||
"""
|
||||
from shopdb.core.models import Location, BusinessUnit
|
||||
|
||||
query = Asset.query.filter(
|
||||
Asset.isactive == True,
|
||||
Asset.mapleft.isnot(None),
|
||||
Asset.maptop.isnot(None)
|
||||
)
|
||||
|
||||
# Filter by asset type name
|
||||
if assettype := request.args.get('assettype'):
|
||||
types = assettype.split(',')
|
||||
query = query.join(AssetType).filter(AssetType.assettype.in_(types))
|
||||
|
||||
# Filter by business unit
|
||||
if bu_id := request.args.get('businessunitid'):
|
||||
query = query.filter(Asset.businessunitid == int(bu_id))
|
||||
|
||||
# Filter by status
|
||||
if status_id := request.args.get('statusid'):
|
||||
query = query.filter(Asset.statusid == int(status_id))
|
||||
|
||||
# Filter by location
|
||||
if location_id := request.args.get('locationid'):
|
||||
query = query.filter(Asset.locationid == int(location_id))
|
||||
|
||||
# Search filter
|
||||
if search := request.args.get('search'):
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Asset.assetnumber.ilike(f'%{search}%'),
|
||||
Asset.name.ilike(f'%{search}%'),
|
||||
Asset.serialnumber.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
assets = query.all()
|
||||
|
||||
# Build response with type-specific data
|
||||
data = []
|
||||
for asset in assets:
|
||||
item = {
|
||||
'assetid': asset.assetid,
|
||||
'assetnumber': asset.assetnumber,
|
||||
'name': asset.name,
|
||||
'displayname': asset.display_name,
|
||||
'serialnumber': asset.serialnumber,
|
||||
'mapleft': asset.mapleft,
|
||||
'maptop': asset.maptop,
|
||||
'assettype': asset.assettype.assettype if asset.assettype else None,
|
||||
'assettypeid': asset.assettypeid,
|
||||
'status': asset.status.status if asset.status else None,
|
||||
'statusid': asset.statusid,
|
||||
'statuscolor': asset.status.color if asset.status else None,
|
||||
'location': asset.location.locationname if asset.location else None,
|
||||
'locationid': asset.locationid,
|
||||
'businessunit': asset.businessunit.businessunit if asset.businessunit else None,
|
||||
'businessunitid': asset.businessunitid,
|
||||
'primaryip': asset.primary_ip,
|
||||
}
|
||||
|
||||
# Add type-specific data
|
||||
type_data = asset._get_extension_data()
|
||||
if type_data:
|
||||
item['typedata'] = type_data
|
||||
|
||||
data.append(item)
|
||||
|
||||
# Get available asset types for filters
|
||||
asset_types = AssetType.query.filter(AssetType.isactive == True).all()
|
||||
types_data = [{'assettypeid': t.assettypeid, 'assettype': t.assettype, 'icon': t.icon} for t in asset_types]
|
||||
|
||||
# Get status options
|
||||
statuses = AssetStatus.query.filter(AssetStatus.isactive == True).all()
|
||||
status_data = [{'statusid': s.statusid, 'status': s.status, 'color': s.color} for s in statuses]
|
||||
|
||||
# Get business units
|
||||
business_units = BusinessUnit.query.filter(BusinessUnit.isactive == True).all()
|
||||
bu_data = [{'businessunitid': bu.businessunitid, 'businessunit': bu.businessunit} for bu in business_units]
|
||||
|
||||
# Get locations
|
||||
locations = Location.query.filter(Location.isactive == True).all()
|
||||
loc_data = [{'locationid': loc.locationid, 'locationname': loc.locationname} for loc in locations]
|
||||
|
||||
return success_response({
|
||||
'assets': data,
|
||||
'total': len(data),
|
||||
'filters': {
|
||||
'assettypes': types_data,
|
||||
'statuses': status_data,
|
||||
'businessunits': bu_data,
|
||||
'locations': loc_data
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>/communications', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_asset_communications(asset_id: int):
|
||||
"""Get all communications for an asset."""
|
||||
from shopdb.core.models import Communication
|
||||
|
||||
asset = Asset.query.get(asset_id)
|
||||
|
||||
if not asset:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Asset with ID {asset_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
comms = Communication.query.filter_by(
|
||||
assetid=asset_id,
|
||||
isactive=True
|
||||
).all()
|
||||
|
||||
data = []
|
||||
for comm in comms:
|
||||
c = comm.to_dict()
|
||||
c['comtype_name'] = comm.comtype.comtype if comm.comtype else None
|
||||
data.append(c)
|
||||
|
||||
return success_response(data)
|
||||
Reference in New Issue
Block a user