generated from gilles/template-webapp
claude code
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"""Package des modèles SQLAlchemy.
|
||||
|
||||
Importe tous les modèles pour qu'ils soient disponibles pour Alembic.
|
||||
"""
|
||||
|
||||
from app.models.category import Category
|
||||
from app.models.document import Document, DocumentType
|
||||
from app.models.item import Item, ItemStatus
|
||||
from app.models.location import Location, LocationType
|
||||
|
||||
__all__ = [
|
||||
"Category",
|
||||
"Location",
|
||||
"LocationType",
|
||||
"Item",
|
||||
"ItemStatus",
|
||||
"Document",
|
||||
"DocumentType",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Modèle SQLAlchemy pour les catégories d'objets.
|
||||
|
||||
Les catégories permettent de classer les objets par domaine d'utilisation
|
||||
(bricolage, informatique, électronique, cuisine, etc.).
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.item import Item
|
||||
|
||||
|
||||
class Category(Base):
|
||||
"""Catégorie d'objets.
|
||||
|
||||
Attributes:
|
||||
id: Identifiant unique auto-incrémenté
|
||||
name: Nom de la catégorie (ex: "Bricolage", "Informatique")
|
||||
description: Description optionnelle de la catégorie
|
||||
color: Couleur hexadécimale pour l'affichage (ex: "#3b82f6")
|
||||
icon: Nom d'icône optionnel (ex: "wrench", "computer")
|
||||
created_at: Date/heure de création (auto)
|
||||
updated_at: Date/heure de dernière modification (auto)
|
||||
items: Relation vers les objets de cette catégorie
|
||||
"""
|
||||
|
||||
__tablename__ = "categories"
|
||||
|
||||
# Colonnes
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
color: Mapped[str | None] = mapped_column(String(7), nullable=True) # Format: #RRGGBB
|
||||
icon: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relations
|
||||
items: Mapped[list["Item"]] = relationship(
|
||||
"Item", back_populates="category", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Représentation string de la catégorie."""
|
||||
return f"<Category(id={self.id}, name='{self.name}')>"
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Modèle SQLAlchemy pour les documents attachés aux objets.
|
||||
|
||||
Les documents peuvent être des photos, notices d'utilisation, factures, etc.
|
||||
Ils sont stockés sur le système de fichiers local.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.item import Item
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class DocumentType(str, enum.Enum):
|
||||
"""Type de document."""
|
||||
|
||||
PHOTO = "photo" # Photo de l'objet
|
||||
MANUAL = "manual" # Notice d'utilisation
|
||||
INVOICE = "invoice" # Facture d'achat
|
||||
WARRANTY = "warranty" # Garantie
|
||||
OTHER = "other" # Autre
|
||||
|
||||
|
||||
class Document(Base):
|
||||
"""Document attaché à un objet.
|
||||
|
||||
Attributes:
|
||||
id: Identifiant unique auto-incrémenté
|
||||
filename: Nom du fichier sur le disque (UUID + extension)
|
||||
original_name: Nom original du fichier uploadé
|
||||
type: Type de document (photo/manual/invoice/warranty/other)
|
||||
mime_type: Type MIME (ex: "image/jpeg", "application/pdf")
|
||||
size_bytes: Taille du fichier en octets
|
||||
file_path: Chemin relatif du fichier (ex: "uploads/photos/uuid.jpg")
|
||||
description: Description optionnelle
|
||||
item_id: ID de l'objet associé (FK)
|
||||
created_at: Date/heure de création (auto)
|
||||
updated_at: Date/heure de dernière modification (auto)
|
||||
item: Relation vers l'objet
|
||||
"""
|
||||
|
||||
__tablename__ = "documents"
|
||||
|
||||
# Colonnes
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
filename: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||
original_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
type: Mapped[DocumentType] = mapped_column(
|
||||
Enum(DocumentType, native_enum=False, length=20), nullable=False
|
||||
)
|
||||
mime_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
file_path: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# Foreign Key
|
||||
item_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("items.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relations
|
||||
item: Mapped["Item"] = relationship("Item", back_populates="documents")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Représentation string du document."""
|
||||
return f"<Document(id={self.id}, type={self.type.value}, filename='{self.filename}')>"
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Modèle SQLAlchemy pour les objets de l'inventaire.
|
||||
|
||||
Les objets (items) sont l'entité centrale de l'application.
|
||||
Chaque objet appartient à une catégorie et est situé à un emplacement.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, DateTime, Enum, ForeignKey, Integer, Numeric, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.category import Category
|
||||
from app.models.document import Document
|
||||
from app.models.location import Location
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class ItemStatus(str, enum.Enum):
|
||||
"""Statut d'un objet."""
|
||||
|
||||
IN_STOCK = "in_stock" # En stock (non utilisé)
|
||||
IN_USE = "in_use" # En cours d'utilisation
|
||||
BROKEN = "broken" # Cassé/HS
|
||||
SOLD = "sold" # Vendu
|
||||
LENT = "lent" # Prêté
|
||||
|
||||
|
||||
class Item(Base):
|
||||
"""Objet de l'inventaire domestique.
|
||||
|
||||
Attributes:
|
||||
id: Identifiant unique auto-incrémenté
|
||||
name: Nom de l'objet (ex: "Perceuse sans fil Bosch")
|
||||
description: Description détaillée optionnelle
|
||||
quantity: Quantité en stock (défaut: 1)
|
||||
price: Prix d'achat optionnel
|
||||
purchase_date: Date d'achat optionnelle
|
||||
status: Statut de l'objet (in_stock/in_use/broken/sold/lent)
|
||||
brand: Marque optionnelle (ex: "Bosch", "Samsung")
|
||||
model: Modèle optionnel (ex: "PSR 18 LI-2")
|
||||
serial_number: Numéro de série optionnel
|
||||
notes: Notes libres
|
||||
category_id: ID de la catégorie (FK)
|
||||
location_id: ID de l'emplacement (FK)
|
||||
created_at: Date/heure de création (auto)
|
||||
updated_at: Date/heure de dernière modification (auto)
|
||||
category: Relation vers la catégorie
|
||||
location: Relation vers l'emplacement
|
||||
documents: Relation vers les documents (photos, notices, factures)
|
||||
"""
|
||||
|
||||
__tablename__ = "items"
|
||||
|
||||
# Colonnes principales
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
quantity: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
status: Mapped[ItemStatus] = mapped_column(
|
||||
Enum(ItemStatus, native_enum=False, length=20),
|
||||
nullable=False,
|
||||
default=ItemStatus.IN_STOCK,
|
||||
)
|
||||
|
||||
# Informations produit
|
||||
brand: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
model: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
serial_number: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True)
|
||||
url: Mapped[str | None] = mapped_column(String(500), nullable=True) # Lien vers page produit
|
||||
|
||||
# Informations achat
|
||||
price: Mapped[Decimal | None] = mapped_column(Numeric(10, 2), nullable=True)
|
||||
purchase_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
|
||||
# Notes
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Foreign Keys
|
||||
category_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("categories.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
location_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("locations.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relations
|
||||
category: Mapped["Category"] = relationship("Category", back_populates="items")
|
||||
location: Mapped["Location"] = relationship("Location", back_populates="items")
|
||||
documents: Mapped[list["Document"]] = relationship(
|
||||
"Document", back_populates="item", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Représentation string de l'objet."""
|
||||
return f"<Item(id={self.id}, name='{self.name}', status={self.status.value})>"
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Modèle SQLAlchemy pour les emplacements de stockage.
|
||||
|
||||
Les emplacements sont organisés de manière hiérarchique :
|
||||
pièce → meuble → tiroir → boîte
|
||||
Exemple : Garage → Étagère A → Tiroir 2 → Boîte visserie
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.item import Item
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class LocationType(str, enum.Enum):
|
||||
"""Type d'emplacement dans la hiérarchie."""
|
||||
|
||||
ROOM = "room" # Pièce (ex: Garage, Cuisine)
|
||||
FURNITURE = "furniture" # Meuble (ex: Étagère, Armoire)
|
||||
DRAWER = "drawer" # Tiroir
|
||||
BOX = "box" # Boîte/Bac de rangement
|
||||
|
||||
|
||||
class Location(Base):
|
||||
"""Emplacement de stockage hiérarchique.
|
||||
|
||||
Attributes:
|
||||
id: Identifiant unique auto-incrémenté
|
||||
name: Nom de l'emplacement (ex: "Garage", "Étagère A")
|
||||
type: Type d'emplacement (room/furniture/drawer/box)
|
||||
parent_id: ID du parent (None si racine)
|
||||
path: Chemin complet calculé (ex: "Garage > Étagère A > Tiroir 2")
|
||||
description: Description optionnelle
|
||||
created_at: Date/heure de création (auto)
|
||||
updated_at: Date/heure de dernière modification (auto)
|
||||
parent: Relation vers le parent
|
||||
children: Relation vers les enfants
|
||||
items: Relation vers les objets à cet emplacement
|
||||
"""
|
||||
|
||||
__tablename__ = "locations"
|
||||
|
||||
# Colonnes
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
type: Mapped[LocationType] = mapped_column(
|
||||
Enum(LocationType, native_enum=False, length=20), nullable=False
|
||||
)
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("locations.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
path: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relations
|
||||
parent: Mapped["Location | None"] = relationship(
|
||||
"Location", remote_side=[id], back_populates="children"
|
||||
)
|
||||
children: Mapped[list["Location"]] = relationship(
|
||||
"Location", back_populates="parent", cascade="all, delete-orphan"
|
||||
)
|
||||
items: Mapped[list["Item"]] = relationship(
|
||||
"Item", back_populates="location", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Représentation string de l'emplacement."""
|
||||
return f"<Location(id={self.id}, name='{self.name}', type={self.type.value})>"
|
||||
|
||||
def calculate_path(self) -> str:
|
||||
"""Calcule le chemin complet de l'emplacement.
|
||||
|
||||
Returns:
|
||||
Chemin complet (ex: "Garage > Étagère A > Tiroir 2")
|
||||
"""
|
||||
if self.parent is None:
|
||||
return self.name
|
||||
return f"{self.parent.calculate_path()} > {self.name}"
|
||||
Reference in New Issue
Block a user