- Add admin media API endpoints for vendor media management - Create reusable media_picker_modal macro in modals.html - Create mediaPickerMixin Alpine.js helper for media selection - Update product create/edit forms with media picker UI - Support main image + additional images selection - Add upload functionality within the picker modal - Update vendor_product_service to handle additional_images - Add additional_images field to Pydantic schemas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
248 lines
6.9 KiB
Python
248 lines
6.9 KiB
Python
# models/schema/vendor_product.py
|
|
"""
|
|
Pydantic schemas for vendor product catalog operations.
|
|
|
|
Used by admin vendor product endpoints for:
|
|
- Product listing and filtering
|
|
- Product statistics
|
|
- Product detail views
|
|
- Catalog vendor listings
|
|
"""
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
class VendorProductListItem(BaseModel):
|
|
"""Product item for vendor catalog list view."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
vendor_name: str | None = None
|
|
vendor_code: str | None = None
|
|
marketplace_product_id: int | None = None
|
|
vendor_sku: str | None = None
|
|
title: str | None = None
|
|
brand: str | None = None
|
|
price: float | None = None
|
|
currency: str | None = None
|
|
effective_price: float | None = None
|
|
effective_currency: str | None = None
|
|
is_active: bool | None = None
|
|
is_featured: bool | None = None
|
|
is_digital: bool | None = None
|
|
image_url: str | None = None
|
|
source_marketplace: str | None = None
|
|
source_vendor: str | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class VendorProductListResponse(BaseModel):
|
|
"""Paginated vendor product list response."""
|
|
|
|
products: list[VendorProductListItem]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
class VendorProductStats(BaseModel):
|
|
"""Vendor product statistics."""
|
|
|
|
total: int
|
|
active: int
|
|
inactive: int
|
|
featured: int
|
|
digital: int
|
|
physical: int
|
|
by_vendor: dict[str, int]
|
|
|
|
|
|
class CatalogVendor(BaseModel):
|
|
"""Vendor with products in catalog."""
|
|
|
|
id: int
|
|
name: str
|
|
vendor_code: str
|
|
|
|
|
|
class CatalogVendorsResponse(BaseModel):
|
|
"""Response for catalog vendors list."""
|
|
|
|
vendors: list[CatalogVendor]
|
|
|
|
|
|
class VendorProductDetail(BaseModel):
|
|
"""Detailed vendor product information.
|
|
|
|
Products are independent entities - all fields are populated at creation.
|
|
Source values are kept for "view original source" comparison only.
|
|
"""
|
|
|
|
id: int
|
|
vendor_id: int
|
|
vendor_name: str | None = None
|
|
vendor_code: str | None = None
|
|
marketplace_product_id: int | None = None # Optional for direct product creation
|
|
vendor_sku: str | None = None
|
|
# Product identifiers
|
|
gtin: str | None = None
|
|
gtin_type: str | None = None # ean13, ean8, upc, isbn, etc.
|
|
# Product fields with source comparison
|
|
price: float | None = None
|
|
price_cents: int | None = None
|
|
price_source: float | None = None # For "view original source" feature
|
|
sale_price: float | None = None
|
|
sale_price_cents: int | None = None
|
|
sale_price_source: float | None = None
|
|
currency: str | None = None
|
|
currency_source: str | None = None
|
|
brand: str | None = None
|
|
brand_source: str | None = None
|
|
condition: str | None = None
|
|
condition_source: str | None = None
|
|
availability: str | None = None
|
|
availability_source: str | None = None
|
|
primary_image_url: str | None = None
|
|
primary_image_url_source: str | None = None
|
|
additional_images: list[str] | None = None
|
|
is_digital: bool | None = None
|
|
product_type: str | None = None
|
|
# Vendor-specific fields
|
|
is_featured: bool | None = None
|
|
is_active: bool | None = None
|
|
display_order: int | None = None
|
|
min_quantity: int | None = None
|
|
max_quantity: int | None = None
|
|
# Supplier tracking
|
|
supplier: str | None = None
|
|
supplier_product_id: str | None = None
|
|
cost: float | None = None # What vendor pays to acquire product
|
|
margin_percent: float | None = None
|
|
# Tax/profit info
|
|
tax_rate_percent: int | None = None
|
|
net_price: float | None = None
|
|
vat_amount: float | None = None
|
|
profit: float | None = None
|
|
profit_margin_percent: float | None = None
|
|
# Digital fulfillment
|
|
download_url: str | None = None
|
|
license_type: str | None = None
|
|
fulfillment_email_template: str | None = None
|
|
# Source info
|
|
source_marketplace: str | None = None
|
|
source_vendor: str | None = None
|
|
source_gtin: str | None = None
|
|
source_sku: str | None = None
|
|
# Translations
|
|
marketplace_translations: dict | None = None
|
|
vendor_translations: dict | None = None
|
|
# Convenience fields for UI display
|
|
title: str | None = None
|
|
description: str | None = None
|
|
image_url: str | None = None # Alias for primary_image_url
|
|
# Timestamps
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
|
|
class RemoveProductResponse(BaseModel):
|
|
"""Response from product removal."""
|
|
|
|
message: str
|
|
|
|
|
|
class TranslationUpdate(BaseModel):
|
|
"""Translation data for a single language."""
|
|
|
|
title: str | None = None
|
|
description: str | None = None
|
|
|
|
|
|
class VendorProductCreate(BaseModel):
|
|
"""Schema for creating a vendor product (admin use - includes vendor_id)."""
|
|
|
|
vendor_id: int
|
|
|
|
# Translations by language code (en, fr, de, lu)
|
|
translations: dict[str, TranslationUpdate] | None = None
|
|
|
|
# Product identifiers
|
|
brand: str | None = None
|
|
vendor_sku: str | None = None
|
|
gtin: str | None = None
|
|
gtin_type: str | None = None # ean13, ean8, upc, isbn
|
|
|
|
# Pricing
|
|
price: float | None = None
|
|
sale_price: float | None = None
|
|
currency: str = "EUR"
|
|
tax_rate_percent: int | None = 17 # Default Luxembourg VAT
|
|
availability: str | None = None
|
|
|
|
# Images
|
|
primary_image_url: str | None = None
|
|
additional_images: list[str] | None = None
|
|
|
|
# Status
|
|
is_active: bool = True
|
|
is_featured: bool = False
|
|
is_digital: bool = False
|
|
|
|
|
|
class VendorDirectProductCreate(BaseModel):
|
|
"""Schema for vendor direct product creation (vendor_id from JWT token)."""
|
|
|
|
title: str
|
|
brand: str | None = None
|
|
vendor_sku: str | None = None
|
|
gtin: str | None = None
|
|
price: float | None = None
|
|
currency: str = "EUR"
|
|
availability: str | None = None
|
|
is_active: bool = True
|
|
is_featured: bool = False
|
|
description: str | None = None
|
|
|
|
|
|
class VendorProductUpdate(BaseModel):
|
|
"""Schema for updating a vendor product."""
|
|
|
|
# Translations by language code (en, fr, de, lu)
|
|
translations: dict[str, TranslationUpdate] | None = None
|
|
|
|
# Product identifiers
|
|
brand: str | None = None
|
|
vendor_sku: str | None = None
|
|
gtin: str | None = None
|
|
gtin_type: str | None = None # ean13, ean8, upc, isbn, etc.
|
|
|
|
# Pricing
|
|
price: float | None = None # Price incl. VAT in euros
|
|
sale_price: float | None = None # Optional sale price
|
|
currency: str | None = None
|
|
tax_rate_percent: int | None = None # 3, 8, 14, 17
|
|
availability: str | None = None # in_stock, out_of_stock, preorder, backorder
|
|
|
|
# Status
|
|
is_digital: bool | None = None
|
|
is_active: bool | None = None
|
|
is_featured: bool | None = None
|
|
|
|
# Images
|
|
primary_image_url: str | None = None
|
|
additional_images: list[str] | None = None
|
|
|
|
# Optional supplier info
|
|
supplier: str | None = None
|
|
cost: float | None = None # Cost in euros
|
|
|
|
|
|
class VendorProductCreateResponse(BaseModel):
|
|
"""Response from product creation."""
|
|
|
|
id: int
|
|
message: str
|