Files
home_hub/backend/app/models/shopping.py
T
gilles dee7037d70 feat(shopping): stats achat produit + édition quantité article
Backend :
- Migration 004 : last_purchased_at (DATE) + avg_interval_days (NUMERIC)
  sur shopping.products
- update_item : met à jour les stats au premier cochage d'un article
  lié à un produit (moyenne mobile exp. 70/30)
- ProductResponse expose les deux nouveaux champs

Frontend :
- ItemRow : long press 500ms → onEdit() (mobile) ; crayon + croix (laptop)
- ShoppingPage : modal édition quantité/unité, état editingItem
- api/shopping.ts : Product inclut last_purchased_at + avg_interval_days

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 07:08:14 +02:00

91 lines
4.9 KiB
Python

import uuid
from datetime import datetime, date
from decimal import Decimal
from sqlalchemy import String, Text, Integer, TIMESTAMP, Date, Numeric, Boolean, ForeignKey, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Product(Base):
__tablename__ = "products"
__table_args__ = {"schema": "shopping"}
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(150), nullable=False)
brand: Mapped[str | None] = mapped_column(String(100))
category: Mapped[str | None] = mapped_column(String(50))
description: Mapped[str | None] = mapped_column(Text)
image_path: Mapped[str | None] = mapped_column(String(255))
thumbnail_path: Mapped[str | None] = mapped_column(String(255))
default_unit: Mapped[str | None] = mapped_column(String(20))
barcode: Mapped[str | None] = mapped_column(String(50))
price: Mapped[Decimal | None] = mapped_column(Numeric(8, 2))
quantity_per_unit: Mapped[Decimal | None] = mapped_column(Numeric(8, 3))
default_store_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.stores.id", ondelete="SET NULL"))
frequency_score: Mapped[int] = mapped_column(Integer, server_default=text("0"))
last_purchased_at: Mapped[date | None] = mapped_column(Date)
avg_interval_days: Mapped[Decimal | None] = mapped_column(Numeric(8, 1))
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
created_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=text("now()"))
class Store(Base):
__tablename__ = "stores"
__table_args__ = {"schema": "shopping"}
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(100), nullable=False)
location: Mapped[str | None] = mapped_column(Text)
url: Mapped[str | None] = mapped_column(Text)
store_type: Mapped[str | None] = mapped_column(String(50))
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
class PriceHistory(Base):
__tablename__ = "price_history"
__table_args__ = {"schema": "shopping"}
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
product_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.products.id", ondelete="CASCADE"), nullable=False)
store_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.stores.id", ondelete="SET NULL"))
price: Mapped[Decimal] = mapped_column(Numeric(8, 2), nullable=False)
unit: Mapped[str | None] = mapped_column(String(20))
quantity: Mapped[Decimal | None] = mapped_column(Numeric(8, 3))
source: Mapped[str] = mapped_column(String(20), server_default="manual")
recorded_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=text("now()"))
class ShoppingList(Base):
__tablename__ = "lists"
__table_args__ = {"schema": "shopping"}
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str | None] = mapped_column(String(100))
store_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.stores.id", ondelete="SET NULL"))
week_date: Mapped[date | None] = mapped_column(Date)
status: Mapped[str] = mapped_column(String(20), server_default="draft")
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
created_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=text("now()"))
items: Mapped[list["ListItem"]] = relationship("ListItem", back_populates="shopping_list", cascade="all, delete-orphan")
class ListItem(Base):
__tablename__ = "list_items"
__table_args__ = {"schema": "shopping"}
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
list_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.lists.id", ondelete="CASCADE"), nullable=False)
product_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.products.id", ondelete="SET NULL"))
custom_name: Mapped[str | None] = mapped_column(String(150))
quantity: Mapped[Decimal | None] = mapped_column(Numeric(8, 3))
unit: Mapped[str | None] = mapped_column(String(20))
is_checked: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
price_recorded: Mapped[Decimal | None] = mapped_column(Numeric(8, 2))
carried_over: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
sort_order: Mapped[int | None] = mapped_column(Integer)
shopping_list: Mapped["ShoppingList"] = relationship("ShoppingList", back_populates="items")
product: Mapped["Product | None"] = relationship("Product", lazy="select")