feat: add export job tracking to Letzshop jobs tab

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 22:53:53 +01:00
parent ee690e95c9
commit b841607a05
4 changed files with 108 additions and 1 deletions

View File

@@ -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}",

View File

@@ -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)

View File

@@ -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()

View File

@@ -28,6 +28,7 @@
>
<option value="">All Types</option>
<option value="import">Product Import</option>
<option value="export">Product Export</option>
<option value="historical_import">Historical Order Import</option>
<option value="order_sync">Order Sync</option>
</select>
@@ -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'
}"
>
<span x-show="job.type === 'import'" x-html="$icon('cloud-download', 'inline w-3 h-3 mr-1')"></span>
<span x-show="job.type === 'export'" x-html="$icon('cloud-upload', 'inline w-3 h-3 mr-1')"></span>
<span x-show="job.type === 'historical_import'" x-html="$icon('clock', 'inline w-3 h-3 mr-1')"></span>
<span x-show="job.type === 'order_sync'" x-html="$icon('refresh', 'inline w-3 h-3 mr-1')"></span>
<span x-text="job.type === 'import' ? 'Product Import' : job.type === 'historical_import' ? 'Historical Import' : 'Order Sync'"></span>
<span x-text="job.type === 'import' ? 'Product Import' : job.type === 'export' ? 'Product Export' : job.type === 'historical_import' ? 'Historical Import' : 'Order Sync'"></span>
</span>
</td>
<td class="px-4 py-3">