Files
home_hub/backend/app/schemas/shopping.py
T
gilles 7bf6caa3dd feat(shopping): listes projet + déduplication nommage hebdo
Backend :
- Migration 007 : list_type VARCHAR(20) sur shopping.lists (weekly/project),
  url/description/image_url sur shopping.list_items
- Modèle ShoppingList : champ list_type
- Modèle ListItem : champs url, description, image_url
- Schémas : list_type sur Create/Response, nouveaux champs sur ItemCreate/Update/Response
- _unique_week_label() : évite les doublons S22 2026 → S22 2026 (2)
- finish_shopping : carry-over uniquement pour list_type='weekly'

Frontend :
- api/shopping.ts : list_type, champs enrichis item, createProjectList()
- ProjectItemCard.tsx : carte avec image, description, URL, boutique, cochage
- ShoppingPage :
  · Séparation weekly / project dans la sélection de liste active
  · Section "Listes projet" sur l'écran vide avec navigation
  · Badge PROJET dans l'en-tête
  · Bouton "Clôturer la semaine" et badge "semaine dépassée" masqués sur projet
  · Bouton "+ Ajouter" (mobile + laptop) sur les listes projet
  · Vue grille ProjectItemCard pour les listes projet
  · Modale création liste projet (nom + boutique)
  · Modale ajout/édition item projet (nom, description, URL, image URL)

v0.5.14

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 09:59:53 +02:00

168 lines
4.4 KiB
Python

import uuid
from datetime import datetime, date
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, model_validator
class StoreCreate(BaseModel):
name: str
location: str | None = None
url: str | None = None
store_type: str | None = None
class StoreUpdate(BaseModel):
name: str | None = None
location: str | None = None
url: str | None = None
store_type: str | None = None
class StoreResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
location: str | None
url: str | None
store_type: str | None
class ProductCreate(BaseModel):
name: str
brand: str | None = None
category: str | None = None
description: str | None = None
default_unit: str | None = None
barcode: str | None = None
price: Decimal | None = None
quantity_per_unit: Decimal | None = None
default_store_id: uuid.UUID | None = None
image_path: str | None = None
thumbnail_path: str | None = None
tags: list[str] | None = None
class ProductUpdate(BaseModel):
name: str | None = None
brand: str | None = None
category: str | None = None
description: str | None = None
default_unit: str | None = None
barcode: str | None = None
price: Decimal | None = None
quantity_per_unit: Decimal | None = None
default_store_id: uuid.UUID | None = None
image_path: str | None = None
thumbnail_path: str | None = None
tags: list[str] | None = None
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
brand: str | None
category: str | None
description: str | None
default_unit: str | None
barcode: str | None
price: Decimal | None
quantity_per_unit: Decimal | None
default_store_id: uuid.UUID | None
frequency_score: int
last_purchased_at: date | None
avg_interval_days: Decimal | None
image_path: str | None
thumbnail_path: str | None
tags: list[str] | None
class ListItemCreate(BaseModel):
product_id: uuid.UUID | None = None
custom_name: str | None = None
quantity: Decimal | None = None
unit: str | None = None
url: str | None = None
description: str | None = None
image_url: str | None = None
@model_validator(mode='after')
def must_have_name(self) -> 'ListItemCreate':
if not self.product_id and not self.custom_name:
raise ValueError('product_id ou custom_name requis')
return self
class ListItemUpdate(BaseModel):
is_checked: bool | None = None
quantity: Decimal | None = None
unit: str | None = None
price_recorded: Decimal | None = None
url: str | None = None
description: str | None = None
image_url: str | None = None
class ListItemResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
product_id: uuid.UUID | None
custom_name: str | None
display_name: str
quantity: Decimal | None
unit: str | None
is_checked: bool
price_recorded: Decimal | None
carried_over: bool
sort_order: int | None
url: str | None
description: str | None
image_url: str | None
class ShoppingListCreate(BaseModel):
name: str | None = None
list_type: Literal['weekly', 'project'] = 'weekly'
store_id: uuid.UUID | None = None
week_date: date | None = None
@model_validator(mode='after')
def project_requires_name(self) -> 'ShoppingListCreate':
if self.list_type == 'project' and not self.name:
raise ValueError('Une liste projet doit avoir un nom')
return self
class ShoppingListUpdate(BaseModel):
name: str | None = None
store_id: uuid.UUID | None = None
status: Literal['draft', 'active', 'done'] | None = None
class ShoppingListResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str | None
list_type: str
store_id: uuid.UUID | None
week_date: date | None
status: str
created_at: datetime
item_count: int
checked_count: int
class ShoppingListDetailResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str | None
list_type: str
store_id: uuid.UUID | None
week_date: date | None
status: str
created_at: datetime
item_count: int
checked_count: int
items: list[ListItemResponse]