diff --git a/app/api/v1/admin/vendor_products.py b/app/api/v1/admin/vendor_products.py index f7c2512f..c8a4abc6 100644 --- a/app/api/v1/admin/vendor_products.py +++ b/app/api/v1/admin/vendor_products.py @@ -206,11 +206,12 @@ def get_vendor_products( @router.get("/stats", response_model=VendorProductStats) def get_vendor_product_stats( + vendor_id: int | None = Query(None, description="Filter stats by vendor ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Get vendor product statistics for admin dashboard.""" - stats = vendor_product_service.get_product_stats(db) + stats = vendor_product_service.get_product_stats(db, vendor_id=vendor_id) return VendorProductStats(**stats) diff --git a/app/api/v1/admin/vendors.py b/app/api/v1/admin/vendors.py index 62dc92a0..1016f412 100644 --- a/app/api/v1/admin/vendors.py +++ b/app/api/v1/admin/vendors.py @@ -363,3 +363,83 @@ def export_vendor_products_letzshop( "Content-Disposition": f'attachment; filename="{filename}"', }, ) + + +class LetzshopExportRequest(BaseModel): + """Request body for Letzshop export to pickup folder.""" + + include_inactive: bool = False + + +@router.post("/{vendor_identifier}/export/letzshop") +def export_vendor_products_letzshop_to_folder( + vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), + request: LetzshopExportRequest = None, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_api), +): + """ + Export vendor products to Letzshop pickup folder (Admin only). + + Generates CSV files for all languages (FR, DE, EN) and places them in a folder + that Letzshop scheduler can fetch from. This is the preferred method for + automated product sync. + + **Behavior:** + - Creates CSV files for each language (fr, de, en) + - Places files in: exports/letzshop/{vendor_code}/ + - Filename format: {vendor_code}_products_{language}.csv + + Returns: + JSON with export status and file paths + """ + import os + from pathlib import Path as FilePath + + from app.services.letzshop_export_service import letzshop_export_service + + vendor = vendor_service.get_vendor_by_identifier(db, vendor_identifier) + + include_inactive = request.include_inactive if request else False + + # Create export directory + export_dir = FilePath(f"exports/letzshop/{vendor.vendor_code.lower()}") + export_dir.mkdir(parents=True, exist_ok=True) + + exported_files = [] + languages = ["fr", "de", "en"] + + for lang in languages: + try: + csv_content = letzshop_export_service.export_vendor_products( + db=db, + vendor_id=vendor.id, + language=lang, + include_inactive=include_inactive, + ) + + filename = f"{vendor.vendor_code.lower()}_products_{lang}.csv" + filepath = export_dir / filename + + with open(filepath, "w", encoding="utf-8") as f: + f.write(csv_content) + + exported_files.append({ + "language": lang, + "filename": filename, + "path": str(filepath), + "size_bytes": os.path.getsize(filepath), + }) + except Exception as e: + exported_files.append({ + "language": lang, + "error": str(e), + }) + + return { + "success": True, + "message": f"Exported {len([f for f in exported_files if 'error' not in f])} language(s) to {export_dir}", + "vendor_code": vendor.vendor_code, + "export_directory": str(export_dir), + "files": exported_files, + } diff --git a/app/services/vendor_product_service.py b/app/services/vendor_product_service.py index f8a52f65..cfd0be06 100644 --- a/app/services/vendor_product_service.py +++ b/app/services/vendor_product_service.py @@ -75,12 +75,24 @@ class VendorProductService: return result, total - def get_product_stats(self, db: Session) -> dict: - """Get vendor product statistics for admin dashboard.""" - total = db.query(func.count(Product.id)).scalar() or 0 + def get_product_stats(self, db: Session, vendor_id: int | None = None) -> dict: + """Get vendor product statistics for admin dashboard. + + Args: + db: Database session + vendor_id: Optional vendor ID to filter stats + + Returns: + Dict with product counts (total, active, inactive, etc.) + """ + # Base query filter + base_filter = Product.vendor_id == vendor_id if vendor_id else True + + total = db.query(func.count(Product.id)).filter(base_filter).scalar() or 0 active = ( db.query(func.count(Product.id)) + .filter(base_filter) .filter(Product.is_active == True) # noqa: E712 .scalar() or 0 @@ -89,6 +101,7 @@ class VendorProductService: featured = ( db.query(func.count(Product.id)) + .filter(base_filter) .filter(Product.is_featured == True) # noqa: E712 .scalar() or 0 @@ -97,6 +110,7 @@ class VendorProductService: # Digital/physical counts digital = ( db.query(func.count(Product.id)) + .filter(base_filter) .join(Product.marketplace_product) .filter(Product.marketplace_product.has(is_digital=True)) .scalar() @@ -104,17 +118,19 @@ class VendorProductService: ) physical = total - digital - # Count by vendor - vendor_counts = ( - db.query( - Vendor.name, - func.count(Product.id), + # Count by vendor (only when not filtered by vendor_id) + by_vendor = {} + if not vendor_id: + vendor_counts = ( + db.query( + Vendor.name, + func.count(Product.id), + ) + .join(Vendor, Product.vendor_id == Vendor.id) + .group_by(Vendor.name) + .all() ) - .join(Vendor, Product.vendor_id == Vendor.id) - .group_by(Vendor.name) - .all() - ) - by_vendor = {name or "unknown": count for name, count in vendor_counts} + by_vendor = {name or "unknown": count for name, count in vendor_counts} return { "total": total, diff --git a/app/templates/admin/partials/letzshop-products-tab.html b/app/templates/admin/partials/letzshop-products-tab.html index d0f6fcf0..a108e61b 100644 --- a/app/templates/admin/partials/letzshop-products-tab.html +++ b/app/templates/admin/partials/letzshop-products-tab.html @@ -1,236 +1,382 @@ {# app/templates/admin/partials/letzshop-products-tab.html #} -{# Products tab for admin Letzshop management - Import & Export #} -{% from 'shared/macros/inputs.html' import number_stepper %} +{# Products tab for admin Letzshop management - Product listing with Import/Export #} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/tables.html' import table_wrapper %} -
- Import products from a Letzshop CSV feed into the marketplace catalog. + +
Vendor products synced with Letzshop marketplace
+Total Products
+ +Active
+ +Inactive
+ +Last Sync
+ +No products found
+ +GTIN:
+ + +SKU:
+ + +No identifiers
+ +-
+ ++ Import products from Letzshop CSV feeds. All languages will be imported. +
+ + ++ Imports products from all configured CSV URLs (FR, EN, DE)
++ Configure settings for product import and export operations. +
+ + ++ Products processed per batch (100-5000). Higher = faster but more memory. +
++ Export products that are currently marked as inactive +
+