511 lines
16 KiB
Python
Executable File
511 lines
16 KiB
Python
Executable File
"""
|
|
Linux BenchTools - Peripheral Service
|
|
Handles business logic and cross-database operations
|
|
"""
|
|
|
|
from typing import Optional, List, Dict, Any, Tuple
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_, or_, func, desc
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from app.models.peripheral import (
|
|
Peripheral, PeripheralPhoto, PeripheralDocument,
|
|
PeripheralLink, PeripheralLoan
|
|
)
|
|
from app.models.location import Location
|
|
from app.models.peripheral_history import PeripheralLocationHistory
|
|
from app.schemas.peripheral import (
|
|
PeripheralCreate, PeripheralUpdate, PeripheralSummary,
|
|
PeripheralDetail, PeripheralListResponse,
|
|
LoanCreate, LoanReturn
|
|
)
|
|
|
|
|
|
class PeripheralService:
|
|
"""Service for peripheral operations"""
|
|
|
|
@staticmethod
|
|
def create_peripheral(
|
|
db: Session,
|
|
peripheral_data: PeripheralCreate,
|
|
user: Optional[str] = None
|
|
) -> Peripheral:
|
|
"""Create a new peripheral"""
|
|
peripheral = Peripheral(**peripheral_data.model_dump())
|
|
db.add(peripheral)
|
|
db.commit()
|
|
db.refresh(peripheral)
|
|
|
|
# Create history entry
|
|
if peripheral.location_id or peripheral.device_id:
|
|
PeripheralService._create_history(
|
|
db=db,
|
|
peripheral_id=peripheral.id,
|
|
action="created",
|
|
to_location_id=peripheral.location_id,
|
|
to_device_id=peripheral.device_id,
|
|
user=user
|
|
)
|
|
|
|
return peripheral
|
|
|
|
@staticmethod
|
|
def get_peripheral(db: Session, peripheral_id: int) -> Optional[Peripheral]:
|
|
"""Get a peripheral by ID"""
|
|
return db.query(Peripheral).filter(Peripheral.id == peripheral_id).first()
|
|
|
|
@staticmethod
|
|
def update_peripheral(
|
|
db: Session,
|
|
peripheral_id: int,
|
|
peripheral_data: PeripheralUpdate,
|
|
user: Optional[str] = None
|
|
) -> Optional[Peripheral]:
|
|
"""Update a peripheral"""
|
|
peripheral = PeripheralService.get_peripheral(db, peripheral_id)
|
|
if not peripheral:
|
|
return None
|
|
|
|
# Track location/device changes for history
|
|
old_location_id = peripheral.location_id
|
|
old_device_id = peripheral.device_id
|
|
|
|
# Update fields
|
|
update_data = peripheral_data.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(peripheral, key, value)
|
|
|
|
db.commit()
|
|
db.refresh(peripheral)
|
|
|
|
# Create history if location or device changed
|
|
new_location_id = peripheral.location_id
|
|
new_device_id = peripheral.device_id
|
|
|
|
if old_location_id != new_location_id or old_device_id != new_device_id:
|
|
action = "moved" if old_location_id != new_location_id else "assigned"
|
|
PeripheralService._create_history(
|
|
db=db,
|
|
peripheral_id=peripheral.id,
|
|
action=action,
|
|
from_location_id=old_location_id,
|
|
to_location_id=new_location_id,
|
|
from_device_id=old_device_id,
|
|
to_device_id=new_device_id,
|
|
user=user
|
|
)
|
|
|
|
return peripheral
|
|
|
|
@staticmethod
|
|
def delete_peripheral(db: Session, peripheral_id: int) -> bool:
|
|
"""Delete a peripheral and all related data"""
|
|
peripheral = PeripheralService.get_peripheral(db, peripheral_id)
|
|
if not peripheral:
|
|
return False
|
|
|
|
# Delete related records
|
|
db.query(PeripheralPhoto).filter(PeripheralPhoto.peripheral_id == peripheral_id).delete()
|
|
db.query(PeripheralDocument).filter(PeripheralDocument.peripheral_id == peripheral_id).delete()
|
|
db.query(PeripheralLink).filter(PeripheralLink.peripheral_id == peripheral_id).delete()
|
|
db.query(PeripheralLoan).filter(PeripheralLoan.peripheral_id == peripheral_id).delete()
|
|
db.query(PeripheralLocationHistory).filter(PeripheralLocationHistory.peripheral_id == peripheral_id).delete()
|
|
|
|
# Delete peripheral
|
|
db.delete(peripheral)
|
|
db.commit()
|
|
return True
|
|
|
|
@staticmethod
|
|
def list_peripherals(
|
|
db: Session,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
type_filter: Optional[str] = None,
|
|
search: Optional[str] = None,
|
|
location_id: Optional[int] = None,
|
|
device_id: Optional[int] = None,
|
|
en_pret: Optional[bool] = None,
|
|
is_complete_device: Optional[bool] = None,
|
|
sort_by: str = "date_creation",
|
|
sort_order: str = "desc"
|
|
) -> PeripheralListResponse:
|
|
"""List peripherals with pagination and filters"""
|
|
|
|
# Base query
|
|
query = db.query(Peripheral)
|
|
|
|
# Apply filters
|
|
if type_filter:
|
|
query = query.filter(Peripheral.type_principal == type_filter)
|
|
|
|
if search:
|
|
search_pattern = f"%{search}%"
|
|
query = query.filter(
|
|
or_(
|
|
Peripheral.nom.ilike(search_pattern),
|
|
Peripheral.marque.ilike(search_pattern),
|
|
Peripheral.modele.ilike(search_pattern),
|
|
Peripheral.numero_serie.ilike(search_pattern)
|
|
)
|
|
)
|
|
|
|
if location_id is not None:
|
|
query = query.filter(Peripheral.location_id == location_id)
|
|
|
|
if device_id is not None:
|
|
query = query.filter(Peripheral.device_id == device_id)
|
|
|
|
if en_pret is not None:
|
|
query = query.filter(Peripheral.en_pret == en_pret)
|
|
|
|
if is_complete_device is not None:
|
|
query = query.filter(Peripheral.is_complete_device == is_complete_device)
|
|
|
|
# Count total
|
|
total = query.count()
|
|
|
|
# Apply sorting
|
|
sort_column = getattr(Peripheral, sort_by, Peripheral.date_creation)
|
|
if sort_order == "desc":
|
|
query = query.order_by(desc(sort_column))
|
|
else:
|
|
query = query.order_by(sort_column)
|
|
|
|
# Apply pagination
|
|
offset = (page - 1) * page_size
|
|
peripherals = query.offset(offset).limit(page_size).all()
|
|
|
|
# Import PeripheralPhoto here to avoid circular import
|
|
from app.models.peripheral import PeripheralPhoto
|
|
|
|
# Convert to summary
|
|
items = []
|
|
for p in peripherals:
|
|
# Get primary photo thumbnail
|
|
thumbnail_url = None
|
|
primary_photo = db.query(PeripheralPhoto).filter(
|
|
PeripheralPhoto.peripheral_id == p.id,
|
|
PeripheralPhoto.is_primary == True
|
|
).first()
|
|
|
|
if primary_photo and primary_photo.thumbnail_path:
|
|
# Convert file path to URL
|
|
thumbnail_url = primary_photo.thumbnail_path.replace('/app/uploads/', '/uploads/')
|
|
|
|
items.append(PeripheralSummary(
|
|
id=p.id,
|
|
nom=p.nom,
|
|
type_principal=p.type_principal,
|
|
sous_type=p.sous_type,
|
|
marque=p.marque,
|
|
modele=p.modele,
|
|
etat=p.etat or "Inconnu",
|
|
rating=p.rating or 0.0,
|
|
prix=p.prix,
|
|
en_pret=p.en_pret or False,
|
|
is_complete_device=p.is_complete_device or False,
|
|
quantite_disponible=p.quantite_disponible or 0,
|
|
thumbnail_url=thumbnail_url
|
|
))
|
|
|
|
|
|
total_pages = (total + page_size - 1) // page_size
|
|
|
|
return PeripheralListResponse(
|
|
items=items,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=total_pages
|
|
)
|
|
|
|
@staticmethod
|
|
def get_peripherals_by_device(
|
|
db: Session,
|
|
device_id: int
|
|
) -> List[Peripheral]:
|
|
"""Get all peripherals assigned to a device (cross-database logical FK)"""
|
|
return db.query(Peripheral).filter(Peripheral.device_id == device_id).all()
|
|
|
|
@staticmethod
|
|
def get_peripherals_by_linked_device(
|
|
db: Session,
|
|
linked_device_id: int
|
|
) -> List[Peripheral]:
|
|
"""Get all peripherals that are part of a complete device"""
|
|
return db.query(Peripheral).filter(Peripheral.linked_device_id == linked_device_id).all()
|
|
|
|
@staticmethod
|
|
def assign_to_device(
|
|
db: Session,
|
|
peripheral_id: int,
|
|
device_id: int,
|
|
user: Optional[str] = None
|
|
) -> Optional[Peripheral]:
|
|
"""Assign a peripheral to a device"""
|
|
peripheral = PeripheralService.get_peripheral(db, peripheral_id)
|
|
if not peripheral:
|
|
return None
|
|
|
|
old_device_id = peripheral.device_id
|
|
peripheral.device_id = device_id
|
|
|
|
db.commit()
|
|
db.refresh(peripheral)
|
|
|
|
# Create history
|
|
PeripheralService._create_history(
|
|
db=db,
|
|
peripheral_id=peripheral.id,
|
|
action="assigned",
|
|
from_device_id=old_device_id,
|
|
to_device_id=device_id,
|
|
user=user
|
|
)
|
|
|
|
return peripheral
|
|
|
|
@staticmethod
|
|
def unassign_from_device(
|
|
db: Session,
|
|
peripheral_id: int,
|
|
user: Optional[str] = None
|
|
) -> Optional[Peripheral]:
|
|
"""Unassign a peripheral from a device"""
|
|
peripheral = PeripheralService.get_peripheral(db, peripheral_id)
|
|
if not peripheral:
|
|
return None
|
|
|
|
old_device_id = peripheral.device_id
|
|
peripheral.device_id = None
|
|
|
|
db.commit()
|
|
db.refresh(peripheral)
|
|
|
|
# Create history
|
|
PeripheralService._create_history(
|
|
db=db,
|
|
peripheral_id=peripheral.id,
|
|
action="unassigned",
|
|
from_device_id=old_device_id,
|
|
to_device_id=None,
|
|
user=user
|
|
)
|
|
|
|
return peripheral
|
|
|
|
@staticmethod
|
|
def create_loan(
|
|
db: Session,
|
|
loan_data: LoanCreate,
|
|
user: Optional[str] = None
|
|
) -> Optional[PeripheralLoan]:
|
|
"""Create a loan for a peripheral"""
|
|
peripheral = PeripheralService.get_peripheral(db, loan_data.peripheral_id)
|
|
if not peripheral or peripheral.en_pret:
|
|
return None
|
|
|
|
# Create loan
|
|
loan = PeripheralLoan(
|
|
**loan_data.model_dump(),
|
|
statut="en_cours",
|
|
created_by=user
|
|
)
|
|
db.add(loan)
|
|
|
|
# Update peripheral
|
|
peripheral.en_pret = True
|
|
peripheral.pret_actuel_id = None # Will be set after commit
|
|
peripheral.prete_a = loan_data.emprunte_par
|
|
|
|
db.commit()
|
|
db.refresh(loan)
|
|
|
|
# Update peripheral with loan ID
|
|
peripheral.pret_actuel_id = loan.id
|
|
db.commit()
|
|
db.refresh(peripheral)
|
|
|
|
return loan
|
|
|
|
@staticmethod
|
|
def return_loan(
|
|
db: Session,
|
|
loan_id: int,
|
|
return_data: LoanReturn
|
|
) -> Optional[PeripheralLoan]:
|
|
"""Return a loan"""
|
|
loan = db.query(PeripheralLoan).filter(PeripheralLoan.id == loan_id).first()
|
|
if not loan or loan.statut != "en_cours":
|
|
return None
|
|
|
|
# Update loan
|
|
loan.date_retour_effectif = return_data.date_retour_effectif
|
|
loan.etat_retour = return_data.etat_retour
|
|
loan.problemes_retour = return_data.problemes_retour
|
|
loan.caution_rendue = return_data.caution_rendue
|
|
loan.statut = "retourne"
|
|
|
|
if return_data.notes:
|
|
loan.notes = (loan.notes or "") + "\n" + return_data.notes
|
|
|
|
# Update peripheral
|
|
peripheral = PeripheralService.get_peripheral(db, loan.peripheral_id)
|
|
if peripheral:
|
|
peripheral.en_pret = False
|
|
peripheral.pret_actuel_id = None
|
|
peripheral.prete_a = None
|
|
|
|
db.commit()
|
|
db.refresh(loan)
|
|
|
|
return loan
|
|
|
|
@staticmethod
|
|
def get_overdue_loans(db: Session) -> List[PeripheralLoan]:
|
|
"""Get all overdue loans"""
|
|
today = date.today()
|
|
return db.query(PeripheralLoan).filter(
|
|
and_(
|
|
PeripheralLoan.statut == "en_cours",
|
|
PeripheralLoan.date_retour_prevue < today
|
|
)
|
|
).all()
|
|
|
|
@staticmethod
|
|
def get_upcoming_returns(db: Session, days: int = 7) -> List[PeripheralLoan]:
|
|
"""Get loans due within specified days"""
|
|
today = date.today()
|
|
future = today + timedelta(days=days)
|
|
return db.query(PeripheralLoan).filter(
|
|
and_(
|
|
PeripheralLoan.statut == "en_cours",
|
|
PeripheralLoan.date_retour_prevue.between(today, future)
|
|
)
|
|
).all()
|
|
|
|
@staticmethod
|
|
def get_statistics(db: Session) -> Dict[str, Any]:
|
|
"""Get peripheral statistics"""
|
|
total = db.query(Peripheral).count()
|
|
en_pret = db.query(Peripheral).filter(Peripheral.en_pret == True).count()
|
|
complete_devices = db.query(Peripheral).filter(Peripheral.is_complete_device == True).count()
|
|
|
|
# By type
|
|
by_type = db.query(
|
|
Peripheral.type_principal,
|
|
func.count(Peripheral.id).label('count')
|
|
).group_by(Peripheral.type_principal).all()
|
|
|
|
# By state
|
|
by_etat = db.query(
|
|
Peripheral.etat,
|
|
func.count(Peripheral.id).label('count')
|
|
).group_by(Peripheral.etat).all()
|
|
|
|
# Low stock
|
|
low_stock = db.query(Peripheral).filter(
|
|
Peripheral.quantite_disponible <= Peripheral.seuil_alerte
|
|
).count()
|
|
|
|
return {
|
|
"total_peripherals": total,
|
|
"en_pret": en_pret,
|
|
"disponible": total - en_pret,
|
|
"complete_devices": complete_devices,
|
|
"low_stock_count": low_stock,
|
|
"by_type": [{"type": t, "count": c} for t, c in by_type],
|
|
"by_etat": [{"etat": e or "Inconnu", "count": c} for e, c in by_etat]
|
|
}
|
|
|
|
@staticmethod
|
|
def _create_history(
|
|
db: Session,
|
|
peripheral_id: int,
|
|
action: str,
|
|
from_location_id: Optional[int] = None,
|
|
to_location_id: Optional[int] = None,
|
|
from_device_id: Optional[int] = None,
|
|
to_device_id: Optional[int] = None,
|
|
user: Optional[str] = None,
|
|
notes: Optional[str] = None
|
|
) -> PeripheralLocationHistory:
|
|
"""Create a history entry"""
|
|
history = PeripheralLocationHistory(
|
|
peripheral_id=peripheral_id,
|
|
action=action,
|
|
from_location_id=from_location_id,
|
|
to_location_id=to_location_id,
|
|
from_device_id=from_device_id,
|
|
to_device_id=to_device_id,
|
|
user=user,
|
|
notes=notes
|
|
)
|
|
db.add(history)
|
|
db.commit()
|
|
return history
|
|
|
|
|
|
class LocationService:
|
|
"""Service for location operations"""
|
|
|
|
@staticmethod
|
|
def get_location_tree(db: Session) -> List[Dict[str, Any]]:
|
|
"""Get hierarchical location tree"""
|
|
def build_tree(parent_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
locations = db.query(Location).filter(
|
|
Location.parent_id == parent_id
|
|
).order_by(Location.ordre_affichage, Location.nom).all()
|
|
|
|
return [
|
|
{
|
|
"id": loc.id,
|
|
"nom": loc.nom,
|
|
"type": loc.type,
|
|
"description": loc.description,
|
|
"image_path": loc.image_path,
|
|
"qr_code_path": loc.qr_code_path,
|
|
"children": build_tree(loc.id)
|
|
}
|
|
for loc in locations
|
|
]
|
|
|
|
return build_tree(None)
|
|
|
|
@staticmethod
|
|
def get_location_path(db: Session, location_id: int) -> List[Location]:
|
|
"""Get full path from root to location"""
|
|
path = []
|
|
current_id = location_id
|
|
|
|
while current_id:
|
|
location = db.query(Location).filter(Location.id == current_id).first()
|
|
if not location:
|
|
break
|
|
path.insert(0, location)
|
|
current_id = location.parent_id
|
|
|
|
return path
|
|
|
|
@staticmethod
|
|
def count_peripherals_in_location(
|
|
db: Session,
|
|
location_id: int,
|
|
recursive: bool = False
|
|
) -> int:
|
|
"""Count peripherals in a location (optionally recursive)"""
|
|
if not recursive:
|
|
return db.query(Peripheral).filter(Peripheral.location_id == location_id).count()
|
|
|
|
# Get all child locations
|
|
def get_children(parent_id: int) -> List[int]:
|
|
children = db.query(Location.id).filter(Location.parent_id == parent_id).all()
|
|
child_ids = [c[0] for c in children]
|
|
for child_id in child_ids[:]:
|
|
child_ids.extend(get_children(child_id))
|
|
return child_ids
|
|
|
|
location_ids = [location_id] + get_children(location_id)
|
|
return db.query(Peripheral).filter(Peripheral.location_id.in_(location_ids)).count()
|