From b841607a05b363c7dd3795f3552f635909822c8c Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 20 Dec 2025 22:53:53 +0100 Subject: [PATCH] feat: add export job tracking to Letzshop jobs tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export operations now create LetzshopSyncLog entries with type 'product_export' - Added log_export method to LetzshopExportService (follows architecture rules) - Updated list_letzshop_jobs to include export jobs - Added export filter option back to jobs dropdown - Export jobs display with blue badge and cloud-upload icon 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/api/v1/admin/vendors.py | 27 ++++++++++ app/services/letzshop/order_service.py | 28 +++++++++++ app/services/letzshop_export_service.py | 49 +++++++++++++++++++ .../admin/partials/letzshop-jobs-table.html | 5 +- 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/app/api/v1/admin/vendors.py b/app/api/v1/admin/vendors.py index 57fa8f62..6f7a8c38 100644 --- a/app/api/v1/admin/vendors.py +++ b/app/api/v1/admin/vendors.py @@ -390,6 +390,7 @@ def export_vendor_products_letzshop_to_folder( JSON with export status and file paths """ import os + from datetime import UTC, datetime from pathlib import Path as FilePath from app.services.letzshop_export_service import letzshop_export_service @@ -398,12 +399,17 @@ def export_vendor_products_letzshop_to_folder( include_inactive = request.include_inactive if request else False + # Track export start time + started_at = datetime.now(UTC) + # 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"] + total_records = 0 + failed_count = 0 for lang in languages: try: @@ -420,6 +426,11 @@ def export_vendor_products_letzshop_to_folder( with open(filepath, "w", encoding="utf-8") as f: f.write(csv_content) + # Count lines (minus header) + line_count = csv_content.count("\n") + if line_count > 0: + total_records = max(total_records, line_count - 1) + exported_files.append({ "language": lang, "filename": filename, @@ -427,11 +438,27 @@ def export_vendor_products_letzshop_to_folder( "size_bytes": os.path.getsize(filepath), }) except Exception as e: + failed_count += 1 exported_files.append({ "language": lang, "error": str(e), }) + # Log the export operation via service + completed_at = datetime.now(UTC) + letzshop_export_service.log_export( + db=db, + vendor_id=vendor.id, + started_at=started_at, + completed_at=completed_at, + records_processed=total_records, + languages_succeeded=len(languages) - failed_count, + languages_failed=failed_count, + triggered_by=f"admin:{current_admin.id}", + error_details={"files": exported_files} if failed_count > 0 else None, + ) + db.commit() + return { "success": True, "message": f"Exported {len([f for f in exported_files if 'error' not in f])} language(s) to {export_dir}", diff --git a/app/services/letzshop/order_service.py b/app/services/letzshop/order_service.py index ae4db076..ee23de18 100644 --- a/app/services/letzshop/order_service.py +++ b/app/services/letzshop/order_service.py @@ -708,6 +708,34 @@ class LetzshopOrderService: } ) + # Product exports from letzshop_sync_logs + if job_type in (None, "export"): + export_query = self.db.query(LetzshopSyncLog).filter( + LetzshopSyncLog.vendor_id == vendor_id, + LetzshopSyncLog.operation_type == "product_export", + ) + if status: + export_query = export_query.filter(LetzshopSyncLog.status == status) + + export_logs = export_query.order_by( + LetzshopSyncLog.created_at.desc() + ).all() + + for log in export_logs: + jobs.append( + { + "id": log.id, + "type": "export", + "status": log.status, + "created_at": log.created_at, + "started_at": log.started_at, + "completed_at": log.completed_at, + "records_processed": log.records_processed or 0, + "records_succeeded": log.records_succeeded or 0, + "records_failed": log.records_failed or 0, + } + ) + # Sort all jobs by created_at descending jobs.sort(key=lambda x: x["created_at"], reverse=True) diff --git a/app/services/letzshop_export_service.py b/app/services/letzshop_export_service.py index 513f2dd9..600b7c71 100644 --- a/app/services/letzshop_export_service.py +++ b/app/services/letzshop_export_service.py @@ -8,9 +8,11 @@ Generates Google Shopping compatible CSV files for Letzshop marketplace. import csv import io import logging +from datetime import UTC, datetime from sqlalchemy.orm import Session, joinedload +from models.database.letzshop import LetzshopSyncLog from models.database.marketplace_product import MarketplaceProduct from models.database.product import Product @@ -279,6 +281,53 @@ class LetzshopExportService: "atalanda:delivery_method": "", } + def log_export( + self, + db: Session, + vendor_id: int, + started_at: datetime, + completed_at: datetime, + records_processed: int, + languages_succeeded: int, + languages_failed: int, + triggered_by: str, + error_details: dict | None = None, + ) -> LetzshopSyncLog: + """ + Log an export operation to the sync log. + + Args: + db: Database session + vendor_id: Vendor ID + started_at: When the export started + completed_at: When the export completed + records_processed: Number of products exported + languages_succeeded: Number of languages successfully exported + languages_failed: Number of languages that failed + triggered_by: Who triggered the export (e.g., "admin:123") + error_details: Optional error details if any failures + + Returns: + Created LetzshopSyncLog entry + """ + sync_log = LetzshopSyncLog( + vendor_id=vendor_id, + operation_type="product_export", + direction="outbound", + status="completed" if languages_failed == 0 else "partial", + records_processed=records_processed, + records_succeeded=languages_succeeded, + records_failed=languages_failed, + started_at=started_at, + completed_at=completed_at, + duration_seconds=int((completed_at - started_at).total_seconds()), + triggered_by=triggered_by, + error_details=error_details, + ) + db.add(sync_log) + db.flush() + return sync_log + # Singleton instance letzshop_export_service = LetzshopExportService() diff --git a/app/templates/admin/partials/letzshop-jobs-table.html b/app/templates/admin/partials/letzshop-jobs-table.html index 094f9da6..3b5440cd 100644 --- a/app/templates/admin/partials/letzshop-jobs-table.html +++ b/app/templates/admin/partials/letzshop-jobs-table.html @@ -28,6 +28,7 @@ > + @@ -87,14 +88,16 @@ class="px-2 py-1 text-xs font-medium rounded-full" :class="{ 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300': job.type === 'import', + 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': job.type === 'export', 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300': job.type === 'historical_import', 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300': job.type === 'order_sync' }" > + - +