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:
@@ -390,6 +390,7 @@ def export_vendor_products_letzshop_to_folder(
|
|||||||
JSON with export status and file paths
|
JSON with export status and file paths
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
|
|
||||||
from app.services.letzshop_export_service import letzshop_export_service
|
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
|
include_inactive = request.include_inactive if request else False
|
||||||
|
|
||||||
|
# Track export start time
|
||||||
|
started_at = datetime.now(UTC)
|
||||||
|
|
||||||
# Create export directory
|
# Create export directory
|
||||||
export_dir = FilePath(f"exports/letzshop/{vendor.vendor_code.lower()}")
|
export_dir = FilePath(f"exports/letzshop/{vendor.vendor_code.lower()}")
|
||||||
export_dir.mkdir(parents=True, exist_ok=True)
|
export_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
exported_files = []
|
exported_files = []
|
||||||
languages = ["fr", "de", "en"]
|
languages = ["fr", "de", "en"]
|
||||||
|
total_records = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
try:
|
try:
|
||||||
@@ -420,6 +426,11 @@ def export_vendor_products_letzshop_to_folder(
|
|||||||
with open(filepath, "w", encoding="utf-8") as f:
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
f.write(csv_content)
|
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({
|
exported_files.append({
|
||||||
"language": lang,
|
"language": lang,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
@@ -427,11 +438,27 @@ def export_vendor_products_letzshop_to_folder(
|
|||||||
"size_bytes": os.path.getsize(filepath),
|
"size_bytes": os.path.getsize(filepath),
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
exported_files.append({
|
exported_files.append({
|
||||||
"language": lang,
|
"language": lang,
|
||||||
"error": str(e),
|
"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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Exported {len([f for f in exported_files if 'error' not in f])} language(s) to {export_dir}",
|
"message": f"Exported {len([f for f in exported_files if 'error' not in f])} language(s) to {export_dir}",
|
||||||
|
|||||||
@@ -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
|
# Sort all jobs by created_at descending
|
||||||
jobs.sort(key=lambda x: x["created_at"], reverse=True)
|
jobs.sort(key=lambda x: x["created_at"], reverse=True)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ Generates Google Shopping compatible CSV files for Letzshop marketplace.
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
|
||||||
|
from models.database.letzshop import LetzshopSyncLog
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
from models.database.product import Product
|
from models.database.product import Product
|
||||||
|
|
||||||
@@ -279,6 +281,53 @@ class LetzshopExportService:
|
|||||||
"atalanda:delivery_method": "",
|
"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
|
# Singleton instance
|
||||||
letzshop_export_service = LetzshopExportService()
|
letzshop_export_service = LetzshopExportService()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
>
|
>
|
||||||
<option value="">All Types</option>
|
<option value="">All Types</option>
|
||||||
<option value="import">Product Import</option>
|
<option value="import">Product Import</option>
|
||||||
|
<option value="export">Product Export</option>
|
||||||
<option value="historical_import">Historical Order Import</option>
|
<option value="historical_import">Historical Order Import</option>
|
||||||
<option value="order_sync">Order Sync</option>
|
<option value="order_sync">Order Sync</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -87,14 +88,16 @@
|
|||||||
class="px-2 py-1 text-xs font-medium rounded-full"
|
class="px-2 py-1 text-xs font-medium rounded-full"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300': job.type === 'import',
|
'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-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'
|
'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 === '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 === '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-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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user