generated from gilles/template-webapp
claude code
This commit is contained in:
15
backend/app/repositories/__init__.py
Normal file
15
backend/app/repositories/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Package des repositories pour l'accès aux données."""
|
||||
|
||||
from app.repositories.base import BaseRepository
|
||||
from app.repositories.category import CategoryRepository
|
||||
from app.repositories.document import DocumentRepository
|
||||
from app.repositories.item import ItemRepository
|
||||
from app.repositories.location import LocationRepository
|
||||
|
||||
__all__ = [
|
||||
"BaseRepository",
|
||||
"CategoryRepository",
|
||||
"LocationRepository",
|
||||
"ItemRepository",
|
||||
"DocumentRepository",
|
||||
]
|
||||
154
backend/app/repositories/base.py
Normal file
154
backend/app/repositories/base.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Repository de base générique.
|
||||
|
||||
Fournit les opérations CRUD de base pour tous les modèles.
|
||||
"""
|
||||
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
ModelType = TypeVar("ModelType", bound=Base)
|
||||
|
||||
|
||||
class BaseRepository(Generic[ModelType]):
|
||||
"""Repository générique avec opérations CRUD de base.
|
||||
|
||||
Attributes:
|
||||
model: Classe du modèle SQLAlchemy
|
||||
db: Session de base de données
|
||||
"""
|
||||
|
||||
def __init__(self, model: type[ModelType], db: AsyncSession) -> None:
|
||||
"""Initialise le repository.
|
||||
|
||||
Args:
|
||||
model: Classe du modèle SQLAlchemy
|
||||
db: Session de base de données async
|
||||
"""
|
||||
self.model = model
|
||||
self.db = db
|
||||
|
||||
async def get(self, id: int) -> ModelType | None:
|
||||
"""Récupère un élément par son ID.
|
||||
|
||||
Args:
|
||||
id: Identifiant de l'élément
|
||||
|
||||
Returns:
|
||||
L'élément trouvé ou None
|
||||
"""
|
||||
result = await self.db.execute(select(self.model).where(self.model.id == id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_all(
|
||||
self, skip: int = 0, limit: int = 100, **filters: Any
|
||||
) -> list[ModelType]:
|
||||
"""Récupère tous les éléments avec pagination et filtres optionnels.
|
||||
|
||||
Args:
|
||||
skip: Nombre d'éléments à sauter
|
||||
limit: Nombre max d'éléments à retourner
|
||||
**filters: Filtres additionnels (ex: status="active")
|
||||
|
||||
Returns:
|
||||
Liste des éléments
|
||||
"""
|
||||
query = select(self.model)
|
||||
|
||||
# Appliquer les filtres
|
||||
for field, value in filters.items():
|
||||
if value is not None and hasattr(self.model, field):
|
||||
query = query.where(getattr(self.model, field) == value)
|
||||
|
||||
query = query.offset(skip).limit(limit)
|
||||
result = await self.db.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def count(self, **filters: Any) -> int:
|
||||
"""Compte le nombre d'éléments avec filtres optionnels.
|
||||
|
||||
Args:
|
||||
**filters: Filtres additionnels
|
||||
|
||||
Returns:
|
||||
Nombre total d'éléments
|
||||
"""
|
||||
query = select(func.count(self.model.id))
|
||||
|
||||
for field, value in filters.items():
|
||||
if value is not None and hasattr(self.model, field):
|
||||
query = query.where(getattr(self.model, field) == value)
|
||||
|
||||
result = await self.db.execute(query)
|
||||
return result.scalar_one()
|
||||
|
||||
async def create(self, **data: Any) -> ModelType:
|
||||
"""Crée un nouvel élément.
|
||||
|
||||
Args:
|
||||
**data: Données de l'élément
|
||||
|
||||
Returns:
|
||||
L'élément créé
|
||||
"""
|
||||
instance = self.model(**data)
|
||||
self.db.add(instance)
|
||||
await self.db.flush()
|
||||
await self.db.refresh(instance)
|
||||
return instance
|
||||
|
||||
async def update(self, id: int, **data: Any) -> ModelType | None:
|
||||
"""Met à jour un élément existant.
|
||||
|
||||
Args:
|
||||
id: Identifiant de l'élément
|
||||
**data: Données à mettre à jour (seules les valeurs non-None)
|
||||
|
||||
Returns:
|
||||
L'élément mis à jour ou None si non trouvé
|
||||
"""
|
||||
instance = await self.get(id)
|
||||
if instance is None:
|
||||
return None
|
||||
|
||||
for field, value in data.items():
|
||||
if value is not None and hasattr(instance, field):
|
||||
setattr(instance, field, value)
|
||||
|
||||
await self.db.flush()
|
||||
await self.db.refresh(instance)
|
||||
return instance
|
||||
|
||||
async def delete(self, id: int) -> bool:
|
||||
"""Supprime un élément.
|
||||
|
||||
Args:
|
||||
id: Identifiant de l'élément
|
||||
|
||||
Returns:
|
||||
True si supprimé, False si non trouvé
|
||||
"""
|
||||
instance = await self.get(id)
|
||||
if instance is None:
|
||||
return False
|
||||
|
||||
await self.db.delete(instance)
|
||||
await self.db.flush()
|
||||
return True
|
||||
|
||||
async def exists(self, id: int) -> bool:
|
||||
"""Vérifie si un élément existe.
|
||||
|
||||
Args:
|
||||
id: Identifiant de l'élément
|
||||
|
||||
Returns:
|
||||
True si existe, False sinon
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(func.count(self.model.id)).where(self.model.id == id)
|
||||
)
|
||||
return result.scalar_one() > 0
|
||||
85
backend/app/repositories/category.py
Normal file
85
backend/app/repositories/category.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Repository pour les catégories."""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.category import Category
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class CategoryRepository(BaseRepository[Category]):
|
||||
"""Repository pour les opérations sur les catégories."""
|
||||
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
"""Initialise le repository."""
|
||||
super().__init__(Category, db)
|
||||
|
||||
async def get_by_name(self, name: str) -> Category | None:
|
||||
"""Récupère une catégorie par son nom.
|
||||
|
||||
Args:
|
||||
name: Nom de la catégorie
|
||||
|
||||
Returns:
|
||||
La catégorie trouvée ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Category).where(Category.name == name)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_with_item_count(self, id: int) -> tuple[Category, int] | None:
|
||||
"""Récupère une catégorie avec le nombre d'objets.
|
||||
|
||||
Args:
|
||||
id: ID de la catégorie
|
||||
|
||||
Returns:
|
||||
Tuple (catégorie, nombre d'objets) ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Category).options(selectinload(Category.items)).where(Category.id == id)
|
||||
)
|
||||
category = result.scalar_one_or_none()
|
||||
if category is None:
|
||||
return None
|
||||
return category, len(category.items)
|
||||
|
||||
async def get_all_with_item_count(
|
||||
self, skip: int = 0, limit: int = 100
|
||||
) -> list[tuple[Category, int]]:
|
||||
"""Récupère toutes les catégories avec le nombre d'objets.
|
||||
|
||||
Args:
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste de tuples (catégorie, nombre d'objets)
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Category)
|
||||
.options(selectinload(Category.items))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Category.name)
|
||||
)
|
||||
categories = result.scalars().all()
|
||||
return [(cat, len(cat.items)) for cat in categories]
|
||||
|
||||
async def name_exists(self, name: str, exclude_id: int | None = None) -> bool:
|
||||
"""Vérifie si un nom de catégorie existe déjà.
|
||||
|
||||
Args:
|
||||
name: Nom à vérifier
|
||||
exclude_id: ID à exclure (pour les mises à jour)
|
||||
|
||||
Returns:
|
||||
True si le nom existe déjà
|
||||
"""
|
||||
query = select(func.count(Category.id)).where(Category.name == name)
|
||||
if exclude_id is not None:
|
||||
query = query.where(Category.id != exclude_id)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalar_one() > 0
|
||||
113
backend/app/repositories/document.py
Normal file
113
backend/app/repositories/document.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Repository pour les documents attachés."""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.document import Document, DocumentType
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class DocumentRepository(BaseRepository[Document]):
|
||||
"""Repository pour les opérations sur les documents."""
|
||||
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
"""Initialise le repository."""
|
||||
super().__init__(Document, db)
|
||||
|
||||
async def get_by_item(self, item_id: int) -> list[Document]:
|
||||
"""Récupère tous les documents d'un objet.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
Liste des documents
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Document)
|
||||
.where(Document.item_id == item_id)
|
||||
.order_by(Document.type, Document.created_at)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_item_and_type(
|
||||
self, item_id: int, type: DocumentType
|
||||
) -> list[Document]:
|
||||
"""Récupère les documents d'un objet par type.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
type: Type de document
|
||||
|
||||
Returns:
|
||||
Liste des documents
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Document)
|
||||
.where(Document.item_id == item_id, Document.type == type)
|
||||
.order_by(Document.created_at)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_filename(self, filename: str) -> Document | None:
|
||||
"""Récupère un document par son nom de fichier.
|
||||
|
||||
Args:
|
||||
filename: Nom du fichier (UUID)
|
||||
|
||||
Returns:
|
||||
Le document trouvé ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Document).where(Document.filename == filename)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def count_by_item(self, item_id: int) -> int:
|
||||
"""Compte le nombre de documents d'un objet.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
Nombre de documents
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.count(Document.id)).where(Document.item_id == item_id)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
async def get_photos(self, item_id: int) -> list[Document]:
|
||||
"""Récupère les photos d'un objet.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
Liste des photos
|
||||
"""
|
||||
return await self.get_by_item_and_type(item_id, DocumentType.PHOTO)
|
||||
|
||||
async def get_invoices(self, item_id: int) -> list[Document]:
|
||||
"""Récupère les factures d'un objet.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
Liste des factures
|
||||
"""
|
||||
return await self.get_by_item_and_type(item_id, DocumentType.INVOICE)
|
||||
|
||||
async def get_manuals(self, item_id: int) -> list[Document]:
|
||||
"""Récupère les notices d'un objet.
|
||||
|
||||
Args:
|
||||
item_id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
Liste des notices
|
||||
"""
|
||||
return await self.get_by_item_and_type(item_id, DocumentType.MANUAL)
|
||||
247
backend/app/repositories/item.py
Normal file
247
backend/app/repositories/item.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""Repository pour les objets d'inventaire."""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.item import Item, ItemStatus
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class ItemRepository(BaseRepository[Item]):
|
||||
"""Repository pour les opérations sur les objets."""
|
||||
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
"""Initialise le repository."""
|
||||
super().__init__(Item, db)
|
||||
|
||||
async def get_with_relations(self, id: int) -> Item | None:
|
||||
"""Récupère un objet avec ses relations (catégorie, emplacement, documents).
|
||||
|
||||
Args:
|
||||
id: ID de l'objet
|
||||
|
||||
Returns:
|
||||
L'objet avec ses relations ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item)
|
||||
.options(
|
||||
selectinload(Item.category),
|
||||
selectinload(Item.location),
|
||||
selectinload(Item.documents),
|
||||
)
|
||||
.where(Item.id == id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_all_with_relations(
|
||||
self, skip: int = 0, limit: int = 100
|
||||
) -> list[Item]:
|
||||
"""Récupère tous les objets avec leurs relations.
|
||||
|
||||
Args:
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste des objets avec relations
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item)
|
||||
.options(
|
||||
selectinload(Item.category),
|
||||
selectinload(Item.location),
|
||||
)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Item.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
category_id: int | None = None,
|
||||
location_id: int | None = None,
|
||||
status: ItemStatus | None = None,
|
||||
min_price: Decimal | None = None,
|
||||
max_price: Decimal | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> list[Item]:
|
||||
"""Recherche des objets avec filtres.
|
||||
|
||||
Args:
|
||||
query: Texte de recherche (nom, description, marque, modèle)
|
||||
category_id: Filtre par catégorie
|
||||
location_id: Filtre par emplacement
|
||||
status: Filtre par statut
|
||||
min_price: Prix minimum
|
||||
max_price: Prix maximum
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste des objets correspondants
|
||||
"""
|
||||
stmt = select(Item).options(
|
||||
selectinload(Item.category),
|
||||
selectinload(Item.location),
|
||||
)
|
||||
|
||||
# Recherche textuelle
|
||||
if query:
|
||||
search_term = f"%{query}%"
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
Item.name.ilike(search_term),
|
||||
Item.description.ilike(search_term),
|
||||
Item.brand.ilike(search_term),
|
||||
Item.model.ilike(search_term),
|
||||
Item.notes.ilike(search_term),
|
||||
)
|
||||
)
|
||||
|
||||
# Filtres
|
||||
if category_id is not None:
|
||||
stmt = stmt.where(Item.category_id == category_id)
|
||||
if location_id is not None:
|
||||
stmt = stmt.where(Item.location_id == location_id)
|
||||
if status is not None:
|
||||
stmt = stmt.where(Item.status == status)
|
||||
if min_price is not None:
|
||||
stmt = stmt.where(Item.price >= min_price)
|
||||
if max_price is not None:
|
||||
stmt = stmt.where(Item.price <= max_price)
|
||||
|
||||
stmt = stmt.offset(skip).limit(limit).order_by(Item.name)
|
||||
result = await self.db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def count_filtered(
|
||||
self,
|
||||
query: str | None = None,
|
||||
category_id: int | None = None,
|
||||
location_id: int | None = None,
|
||||
status: ItemStatus | None = None,
|
||||
min_price: Decimal | None = None,
|
||||
max_price: Decimal | None = None,
|
||||
) -> int:
|
||||
"""Compte les objets avec filtres.
|
||||
|
||||
Returns:
|
||||
Nombre d'objets correspondants
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
stmt = select(func.count(Item.id))
|
||||
|
||||
if query:
|
||||
search_term = f"%{query}%"
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
Item.name.ilike(search_term),
|
||||
Item.description.ilike(search_term),
|
||||
Item.brand.ilike(search_term),
|
||||
Item.model.ilike(search_term),
|
||||
Item.notes.ilike(search_term),
|
||||
)
|
||||
)
|
||||
|
||||
if category_id is not None:
|
||||
stmt = stmt.where(Item.category_id == category_id)
|
||||
if location_id is not None:
|
||||
stmt = stmt.where(Item.location_id == location_id)
|
||||
if status is not None:
|
||||
stmt = stmt.where(Item.status == status)
|
||||
if min_price is not None:
|
||||
stmt = stmt.where(Item.price >= min_price)
|
||||
if max_price is not None:
|
||||
stmt = stmt.where(Item.price <= max_price)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalar_one()
|
||||
|
||||
async def get_by_category(
|
||||
self, category_id: int, skip: int = 0, limit: int = 100
|
||||
) -> list[Item]:
|
||||
"""Récupère les objets d'une catégorie.
|
||||
|
||||
Args:
|
||||
category_id: ID de la catégorie
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste des objets
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item)
|
||||
.where(Item.category_id == category_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Item.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_location(
|
||||
self, location_id: int, skip: int = 0, limit: int = 100
|
||||
) -> list[Item]:
|
||||
"""Récupère les objets d'un emplacement.
|
||||
|
||||
Args:
|
||||
location_id: ID de l'emplacement
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste des objets
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item)
|
||||
.where(Item.location_id == location_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Item.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_status(
|
||||
self, status: ItemStatus, skip: int = 0, limit: int = 100
|
||||
) -> list[Item]:
|
||||
"""Récupère les objets par statut.
|
||||
|
||||
Args:
|
||||
status: Statut recherché
|
||||
skip: Offset
|
||||
limit: Limite
|
||||
|
||||
Returns:
|
||||
Liste des objets
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item)
|
||||
.where(Item.status == status)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Item.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_serial_number(self, serial_number: str) -> Item | None:
|
||||
"""Récupère un objet par son numéro de série.
|
||||
|
||||
Args:
|
||||
serial_number: Numéro de série
|
||||
|
||||
Returns:
|
||||
L'objet trouvé ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Item).where(Item.serial_number == serial_number)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
171
backend/app/repositories/location.py
Normal file
171
backend/app/repositories/location.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Repository pour les emplacements."""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.location import Location, LocationType
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class LocationRepository(BaseRepository[Location]):
|
||||
"""Repository pour les opérations sur les emplacements."""
|
||||
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
"""Initialise le repository."""
|
||||
super().__init__(Location, db)
|
||||
|
||||
async def get_with_children(self, id: int) -> Location | None:
|
||||
"""Récupère un emplacement avec ses enfants.
|
||||
|
||||
Args:
|
||||
id: ID de l'emplacement
|
||||
|
||||
Returns:
|
||||
L'emplacement avec ses enfants ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.options(selectinload(Location.children))
|
||||
.where(Location.id == id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_root_locations(self) -> list[Location]:
|
||||
"""Récupère tous les emplacements racine (sans parent).
|
||||
|
||||
Returns:
|
||||
Liste des emplacements racine
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.where(Location.parent_id.is_(None))
|
||||
.order_by(Location.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_children(self, parent_id: int) -> list[Location]:
|
||||
"""Récupère les enfants directs d'un emplacement.
|
||||
|
||||
Args:
|
||||
parent_id: ID du parent
|
||||
|
||||
Returns:
|
||||
Liste des enfants
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.where(Location.parent_id == parent_id)
|
||||
.order_by(Location.name)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_type(self, type: LocationType) -> list[Location]:
|
||||
"""Récupère tous les emplacements d'un type donné.
|
||||
|
||||
Args:
|
||||
type: Type d'emplacement
|
||||
|
||||
Returns:
|
||||
Liste des emplacements
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.where(Location.type == type)
|
||||
.order_by(Location.path)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_full_tree(self) -> list[Location]:
|
||||
"""Récupère l'arborescence complète des emplacements.
|
||||
|
||||
Returns:
|
||||
Liste des emplacements racine avec enfants chargés récursivement
|
||||
"""
|
||||
# Charger tous les emplacements avec leurs enfants
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.options(selectinload(Location.children))
|
||||
.order_by(Location.path)
|
||||
)
|
||||
all_locations = list(result.scalars().all())
|
||||
|
||||
# Retourner seulement les racines (les enfants sont déjà chargés)
|
||||
return [loc for loc in all_locations if loc.parent_id is None]
|
||||
|
||||
async def get_with_item_count(self, id: int) -> tuple[Location, int] | None:
|
||||
"""Récupère un emplacement avec le nombre d'objets.
|
||||
|
||||
Args:
|
||||
id: ID de l'emplacement
|
||||
|
||||
Returns:
|
||||
Tuple (emplacement, nombre d'objets) ou None
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Location)
|
||||
.options(selectinload(Location.items))
|
||||
.where(Location.id == id)
|
||||
)
|
||||
location = result.scalar_one_or_none()
|
||||
if location is None:
|
||||
return None
|
||||
return location, len(location.items)
|
||||
|
||||
async def create_with_path(
|
||||
self,
|
||||
name: str,
|
||||
type: LocationType,
|
||||
parent_id: int | None = None,
|
||||
description: str | None = None,
|
||||
) -> Location:
|
||||
"""Crée un emplacement avec calcul automatique du chemin.
|
||||
|
||||
Args:
|
||||
name: Nom de l'emplacement
|
||||
type: Type d'emplacement
|
||||
parent_id: ID du parent (None si racine)
|
||||
description: Description optionnelle
|
||||
|
||||
Returns:
|
||||
L'emplacement créé
|
||||
"""
|
||||
# Calculer le chemin
|
||||
if parent_id is None:
|
||||
path = name
|
||||
else:
|
||||
parent = await self.get(parent_id)
|
||||
if parent is None:
|
||||
path = name
|
||||
else:
|
||||
path = f"{parent.path} > {name}"
|
||||
|
||||
return await self.create(
|
||||
name=name,
|
||||
type=type,
|
||||
parent_id=parent_id,
|
||||
path=path,
|
||||
description=description,
|
||||
)
|
||||
|
||||
async def update_paths_recursive(self, location: Location) -> None:
|
||||
"""Met à jour récursivement les chemins après modification.
|
||||
|
||||
Args:
|
||||
location: Emplacement modifié
|
||||
"""
|
||||
# Mettre à jour le chemin de cet emplacement
|
||||
if location.parent_id is None:
|
||||
location.path = location.name
|
||||
else:
|
||||
parent = await self.get(location.parent_id)
|
||||
if parent:
|
||||
location.path = f"{parent.path} > {location.name}"
|
||||
else:
|
||||
location.path = location.name
|
||||
|
||||
# Mettre à jour les enfants
|
||||
children = await self.get_children(location.id)
|
||||
for child in children:
|
||||
child.path = f"{location.path} > {child.name}"
|
||||
await self.update_paths_recursive(child)
|
||||
Reference in New Issue
Block a user