From 0dfb7d2237a61754f13b0a45331d9ed9d6f31fb8 Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 5 Mar 2026 12:01:24 -0700 Subject: [PATCH] feature/csv-expo --- .../a76/layouts_csv/exportacion/__init__.py | 1 + .../a76/layouts_csv/exportacion/routes.py | 151 ++++++++++++++++++ .../a76/layouts_csv/exportacion/schemas.py | 23 +++ .../a76/layouts_csv/exportacion/tasks.py | 139 ++++++++++++++++ .../exportacion/template_config.py | 89 +++++++++++ backend/api/v1/modules/a76/router.py | 2 + backend/core/celery_app.py | 1 + frontend/src/lib/api.ts | 34 ++++ frontend/src/lib/config/csv-upload.ts | 6 +- .../routes/dashboard/csv-upload/+page.svelte | 36 ++++- 10 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 backend/api/v1/modules/a76/layouts_csv/exportacion/__init__.py create mode 100644 backend/api/v1/modules/a76/layouts_csv/exportacion/routes.py create mode 100644 backend/api/v1/modules/a76/layouts_csv/exportacion/schemas.py create mode 100644 backend/api/v1/modules/a76/layouts_csv/exportacion/tasks.py create mode 100644 backend/api/v1/modules/a76/layouts_csv/exportacion/template_config.py diff --git a/backend/api/v1/modules/a76/layouts_csv/exportacion/__init__.py b/backend/api/v1/modules/a76/layouts_csv/exportacion/__init__.py new file mode 100644 index 00000000..dadf0cb2 --- /dev/null +++ b/backend/api/v1/modules/a76/layouts_csv/exportacion/__init__.py @@ -0,0 +1 @@ +# layouts_csv.exportacion — carga CSV exportación (encabezado y partidas) diff --git a/backend/api/v1/modules/a76/layouts_csv/exportacion/routes.py b/backend/api/v1/modules/a76/layouts_csv/exportacion/routes.py new file mode 100644 index 00000000..a357f1f0 --- /dev/null +++ b/backend/api/v1/modules/a76/layouts_csv/exportacion/routes.py @@ -0,0 +1,151 @@ +""" +Rutas de importación CSV para Exportación (encabezado y partidas). +Flujo: upload → scan → status (polling) → commit. Sin validaciones ni inserción aún. +""" +import base64 +import json +import logging +import os +from uuid import uuid4 + +from fastapi import APIRouter, File, HTTPException, UploadFile, Depends, Form, Query +from sqlalchemy.orm import Session +from typing import Literal, Optional, Dict, Any + +from core.celery_app import celery_app +from core.database import get_core_db +from core.paths import layout_path +from core.security import get_current_user, validate_access_to_resource + +from .schemas import ImportJobResponse, CommitRequest +from .tasks import scan_file, insert_valid_rows, JOB_TYPE, EXP_IMPORT_REDIS_TTL +from ..common import storage as common_storage + +router = APIRouter() +logger = logging.getLogger(__name__) + + +def _get_redis(): + import redis + url = os.getenv("VALKEY_URL", os.getenv("REDIS_URL", "redis://valkey:6379/0")) + return redis.Redis.from_url(url, decode_responses=False) + + +@router.post("/upload/{model_target}", response_model=ImportJobResponse) +async def upload_import_file( + model_target: Literal["invoice_header", "invoice_details"], + file: UploadFile = File(...), + footer_config: Optional[str] = Form(None), + template_id: Optional[str] = Form(None), + company_id: int = Query(..., description="Company ID"), + operation_type: Optional[str] = Query("exp"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """Subir CSV, guardar en Redis, encolar scan. operation_type=exp para exportación.""" + try: + tenant_id = validate_access_to_resource(db, company_id, current_user) + except Exception as e: + logger.error("Exportación import: access validation failed: %s", e) + raise HTTPException(status_code=403, detail="Invalid company access") + + if not file.filename or not file.filename.lower().endswith(".csv"): + raise HTTPException(status_code=400, detail="Solo se permiten archivos .csv") + + job_id = str(uuid4()) + contents = await file.read() + + file_key, meta_key, _ = common_storage.storage_keys(JOB_TYPE, job_id) + meta_data = { + "tenant_id": tenant_id, + "company_id": company_id, + "user_id": current_user.get("id"), + "footer_config": footer_config, + "operation_type": operation_type or "exp", + "template_id": template_id or ("exp_def_header" if model_target == "invoice_header" else "exp_def_details"), + } + + try: + r = _get_redis() + r.set(file_key, base64.b64encode(contents), ex=EXP_IMPORT_REDIS_TTL) + r.set(meta_key, json.dumps(meta_data).encode("utf-8"), ex=EXP_IMPORT_REDIS_TTL) + except Exception as e: + logger.error("Exportación import: Redis store error: %s", e) + raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.") + + try: + upload_dir = layout_path("imports", "temp") + os.makedirs(upload_dir, exist_ok=True) + csv_path = common_storage.file_path_for_job(JOB_TYPE, job_id) + with open(csv_path, "wb") as f: + f.write(contents) + meta_path = csv_path.replace(".csv", ".meta.json") + with open(meta_path, "w") as f: + json.dump(meta_data, f) + except Exception as e: + logger.warning("Exportación import: local file save failed: %s", e) + + scan_file.apply_async(args=[job_id, model_target, footer_config], task_id=job_id) + + return ImportJobResponse( + job_id=job_id, + status="queued", + message="Archivo subido. Escaneo iniciado.", + ) + + +@router.get("/{job_id}/status") +async def get_import_status(job_id: str): + """Polling: estado del escaneo o del commit.""" + task_result = celery_app.AsyncResult(job_id) + + if task_result.state == "PENDING": + return {"status": "processing", "progress": 0} + if task_result.state == "PROGRESS": + info = task_result.info or {} + return { + "status": "processing", + "progress": info.get("current", 0), + "total": info.get("total", 0), + } + if task_result.state == "SUCCESS": + result = task_result.result + if isinstance(result, dict) and "status" in result: + return result + return {"status": "finished", "result": result} + + if isinstance(getattr(task_result, "result", None), dict) and task_result.result.get("status") in ("finished", "warning"): + return task_result.result + + logger.warning("Exportación import task %s failed: state=%s", job_id, task_result.state) + err_msg = None + tb = getattr(task_result, "traceback", None) + if tb and isinstance(tb, str): + lines = [l.strip() for l in tb.strip().split("\n") if l.strip()] + if lines: + err_msg = lines[-1] + if not err_msg: + try: + exc = task_result.get(propagate=False) + if exc is not None: + err_msg = str(exc) + except Exception: + pass + if not err_msg: + result = getattr(task_result, "result", None) + if result is not None and not isinstance(result, dict): + err_msg = str(result) + elif isinstance(result, dict) and (result.get("error") or result.get("message")): + err_msg = result.get("error") or result.get("message") + return {"status": "failed", "error": err_msg or "Task failed"} + + +@router.post("/{job_id}/commit") +async def commit_import_job(job_id: str, body: CommitRequest): + """Usuario confirma; se encola la tarea de commit (por ahora sin inserción real).""" + task = insert_valid_rows.delay(job_id, body.model_target) + return { + "status": "committing", + "message": "Proceso de commit iniciado.", + "commit_job_id": task.id, + } diff --git a/backend/api/v1/modules/a76/layouts_csv/exportacion/schemas.py b/backend/api/v1/modules/a76/layouts_csv/exportacion/schemas.py new file mode 100644 index 00000000..2a043f30 --- /dev/null +++ b/backend/api/v1/modules/a76/layouts_csv/exportacion/schemas.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel +from typing import Optional, Literal + + +class ImportJobResponse(BaseModel): + job_id: str + status: str + message: str + + +class CommitRequest(BaseModel): + model_target: Literal["invoice_header", "invoice_details"] + + +class ImportJobStatus(BaseModel): + status: str + job_id: str + total_rows: Optional[int] = 0 + error_count: Optional[int] = 0 + valid_rows: Optional[int] = 0 + error: Optional[str] = None + inserted: Optional[int] = 0 + error_file: Optional[str] = None diff --git a/backend/api/v1/modules/a76/layouts_csv/exportacion/tasks.py b/backend/api/v1/modules/a76/layouts_csv/exportacion/tasks.py new file mode 100644 index 00000000..0d43f5b1 --- /dev/null +++ b/backend/api/v1/modules/a76/layouts_csv/exportacion/tasks.py @@ -0,0 +1,139 @@ +""" +Tareas Celery para importación CSV de Exportación (encabezado y partidas). +Flujo: scan_file (sin validaciones) → insert_valid_rows (sin inserción en BD). +Usa layouts_csv.common (storage, normalize, meta, responses, csv_reader). +""" +import logging +import os +from typing import Dict, Any, Optional + +from core.celery_app import celery_app + +from ..common import storage as common_storage +from ..common import normalize as common_normalize +from ..common import meta as common_meta +from ..common import responses as common_responses +from ..common import csv_reader as common_csv_reader +from .template_config import row_from_template + +logger = logging.getLogger(__name__) + +JOB_TYPE = "exp" +EXP_IMPORT_REDIS_TTL = common_storage.IMPORT_REDIS_TTL + + +def _ensure_file(job_id: str) -> Optional[str]: + return common_storage.ensure_file_from_redis(JOB_TYPE, job_id, "Exportación import") + + +def _ensure_meta(job_id: str, file_path: str) -> bool: + return common_storage.ensure_meta_from_redis(JOB_TYPE, job_id, file_path, "Exportación import") + + +def _norm_row(row: Dict[str, Any], template_id: str) -> Dict[str, Any]: + return row_from_template(row, template_id, common_normalize.normalize_header) + + +@celery_app.task(bind=True, name="api.v1.modules.a76.layouts_csv.exportacion.tasks.scan_file") +def scan_file(self, job_id: str, model_target: str, config: str = None): + """ + Scan CSV sin validaciones: leer, normalizar con plantilla, devolver total_rows y 0 errores. + """ + logger.info("Exportación import: starting scan for job %s target %s", job_id, model_target) + + file_path = _ensure_file(job_id) + if not file_path: + return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."} + if os.path.getsize(file_path) == 0: + return {"status": "failed", "error": "El archivo está vacío."} + _ensure_meta(job_id, file_path) + + try: + common_meta.require_tenant_context(file_path) + except ValueError as e: + return {"status": "failed", "error": str(e)} + + meta = common_meta.load_meta(file_path) or {} + template_id = meta.get("template_id") or ( + "exp_def_header" if model_target == "invoice_header" else "exp_def_details" + ) + + total_rows = 0 + processed_rows = 0 + + try: + total_rows = common_csv_reader.count_csv_rows(file_path, has_header=True) + except Exception as e: + return {"status": "failed", "error": str(e)} + + def on_progress(current: int, total: int) -> None: + self.update_state(state="PROGRESS", meta={"current": current, "total": total}) + + try: + for i, row in common_csv_reader.iter_csv_rows(file_path, fieldnames=None): + if i % 500 == 0: + on_progress(i, total_rows) + _norm_row(row, template_id) + processed_rows += 1 + except Exception as e: + logger.error("Exportación import scan failed: %s", e) + return {"status": "failed", "error": str(e)} + + return common_responses.scan_result(job_id, processed_rows, 0, []) + + +@celery_app.task(bind=True, name="api.v1.modules.a76.layouts_csv.exportacion.tasks.insert_valid_rows") +def insert_valid_rows(self, job_id: str, model_target: str): + """ + Commit sin inserción en BD: leer CSV, omitir líneas de error (vacío por ahora), cleanup, devolver finished con inserted=0. + """ + logger.info("Exportación import: starting commit for job %s target %s", job_id, model_target) + + file_path = _ensure_file(job_id) + if not file_path: + alt_path = common_storage.file_path_for_job(JOB_TYPE, job_id) + if not os.path.exists(alt_path): + return {"status": "failed", "error": "Archivo no encontrado (expirado). Sube y confirma de nuevo."} + file_path = alt_path + else: + _ensure_meta(job_id, file_path) + + try: + common_meta.require_tenant_context(file_path) + except ValueError as e: + return {"status": "failed", "error": str(e)} + + meta = common_meta.load_meta(file_path) or {} + meta_path = common_meta.get_meta_path(file_path) + error_path = common_storage.error_path_for_job(JOB_TYPE, job_id) + error_lines = common_storage.get_error_lines(JOB_TYPE, job_id, error_path) + + template_id = meta.get("template_id") or ( + "exp_def_header" if model_target == "invoice_header" else "exp_def_details" + ) + + try: + for i, row in common_csv_reader.iter_csv_rows(file_path, fieldnames=None): + if i in error_lines: + continue + _norm_row(row, template_id) + except Exception as e: + logger.error("Exportación import commit read failed: %s", e) + return {"status": "failed", "error": str(e)} + + common_storage.cleanup_import_job( + JOB_TYPE, job_id, + file_path=file_path, + error_path=error_path, + meta_path=meta_path, + ) + + return { + "status": "finished", + "inserted": 0, + "skipped_invalid": 0, + "skipped_missing_fk": 0, + "skipped_duplicate": 0, + "skipped_details": [], + "message": "Proceso base listo; validaciones e inserción pendientes.", + } diff --git a/backend/api/v1/modules/a76/layouts_csv/exportacion/template_config.py b/backend/api/v1/modules/a76/layouts_csv/exportacion/template_config.py new file mode 100644 index 00000000..d6d598fc --- /dev/null +++ b/backend/api/v1/modules/a76/layouts_csv/exportacion/template_config.py @@ -0,0 +1,89 @@ +""" +Plantillas CSV para Exportación (encabezado y partidas). +Misma estructura que facturas exp_def_header / exp_def_details; módulo autocontenido. +""" + +from typing import Dict, List, Any, Optional + +# Columnas para encabezado y partidas de exportación (EstructuraEncFacExpoCamReg / EstructuraParExpoCamReg) +TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = { + "exp_def_header": [ + {"canonical": "NUMERO FACTURA", "aliases": ["NUM FACTURA", "FACTURA", "ID"]}, + {"canonical": "FECHA FACTURA", "aliases": ["FECHA"]}, + {"canonical": "FECHA EMISION"}, + {"canonical": "CLAVE PROVEEDOR"}, + {"canonical": "CLAVE VENDIDO A"}, + {"canonical": "CLAVE ENVIADO A"}, + {"canonical": "REGIMEN", "aliases": ["CLAVEDOCUMENTO"]}, + {"canonical": "ADUANA DE CRUCE"}, + {"canonical": "CLAVE MONEDA"}, + {"canonical": "CLAVE INCOTERM"}, + {"canonical": "TIPO MONEDA"}, + {"canonical": "TIPO DE CAMBIO"}, + {"canonical": "TIPO PESO"}, + {"canonical": "TIPO TRANSPORTE"}, + {"canonical": "REMESA"}, + {"canonical": "AGENTE ADUANAL"}, + {"canonical": "FLETES"}, + {"canonical": "VALOR SEGUROS"}, + {"canonical": "SEGUROS"}, + {"canonical": "EMBALAJES"}, + {"canonical": "OTROS INCREMENTABLES"}, + {"canonical": "NUM PROYECTO", "aliases": ["NUMPROYECTO"]}, + {"canonical": "ORDEN COMPRA", "aliases": ["ORDENCOMPRA"]}, + {"canonical": "FACTURA ALTERNA"}, + {"canonical": "FACTURA EXPO REF", "aliases": ["FACTURAEXPOREF"]}, + {"canonical": "OBSERVACIONES E"}, + {"canonical": "OBSERVACIONES I"}, + {"canonical": "E DOCUMENT"}, + {"canonical": "NUM OPERACION"}, + {"canonical": "CLAVE TRANSPORTISTA"}, + {"canonical": "NOMBRE CONDUCTOR"}, + {"canonical": "NUMERO TRANSPORTE"}, + {"canonical": "PRECINTO"}, + ], + "exp_def_details": [ + {"canonical": "NUMERO FACTURA", "aliases": ["NUM FACTURA", "FACTURA"]}, + {"canonical": "LINEA", "aliases": ["RENGLON", "PARTIDA"]}, + {"canonical": "NUMPARTE", "aliases": ["NUMERO PARTE"]}, + {"canonical": "PRECIO UNITARIO", "aliases": ["PRECIOUNITARIO"]}, + {"canonical": "VALOR COMERCIAL", "aliases": ["VALORCOMERCIAL"]}, + {"canonical": "CANTIDAD"}, + {"canonical": "CANTIDAD BULTOS", "aliases": ["CANTIDADBULTOS"]}, + {"canonical": "DESCRIPCION"}, + {"canonical": "PAIS ORIGEN", "aliases": ["PAISORIGEN"]}, + {"canonical": "FRACCION"}, + {"canonical": "ORDEN DE COMPRA", "aliases": ["ORDENCOMPRA"]}, + ], +} + + +def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]]: + return TEMPLATE_COLUMNS.get(template_id) + + +def build_normalized_lookup(template_id: str, normalize_header_fn) -> Dict[str, str]: + """normalized_header -> canonical_name.""" + cols = _resolve_template_columns(template_id) + if not cols: + return {} + lookup: Dict[str, str] = {} + for item in cols: + canonical = item["canonical"] + lookup[normalize_header_fn(canonical)] = canonical + for alias in item.get("aliases") or []: + lookup[normalize_header_fn(alias)] = canonical + return lookup + + +def row_from_template(row: Dict[str, Any], template_id: str, normalize_header_fn) -> Dict[str, Any]: + """Fila CSV -> dict con nombres canónicos de la plantilla.""" + lookup = build_normalized_lookup(template_id, normalize_header_fn) + if not lookup: + return {normalize_header_fn(k): v for k, v in row.items()} + out: Dict[str, Any] = {} + for csv_header, value in row.items(): + key_norm = normalize_header_fn(csv_header) + if key_norm in lookup: + out[lookup[key_norm]] = value + return out diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 33416d44..a8d36774 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -15,6 +15,7 @@ from .classes import router as classes_router from .clients_and_providers import router as client_and_provider_router from .layouts_csv.facturas.routes import router as imports_router +from .layouts_csv.exportacion.routes import router as exportacion_imports_router from .csv_templates.routes import router as csv_templates_router from .invoice_settings.routes import router as invoice_settings_router from .item_presets.routes import router as item_presets_router @@ -56,6 +57,7 @@ router.include_router(general_catalogs_router, prefix="/a76", tags=["a76 / gener router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"]) router.include_router(items_router, prefix="/a76", tags=["a76 / items"]) router.include_router(imports_router, prefix="/a76/imports", tags=["a76 / imports"]) +router.include_router(exportacion_imports_router, prefix="/a76/imports/exportacion", tags=["a76 / imports / exportacion"]) router.include_router(csv_templates_router, prefix="/a76/csv-templates", tags=["a76 / csv_templates"]) router.include_router(invoice_settings_router) router.include_router(item_presets_router, prefix="/a76/item-presets", tags=["a76 / item_presets"]) diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index 3ebcdde3..3ca84277 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -33,6 +33,7 @@ celery_app.conf.update( "api.v1.modules.a76.reports.movements.invoices.tasks", "api.v1.modules.a76.reports.exportacion.descargo.task", "api.v1.modules.a76.layouts_csv.facturas.tasks", + "api.v1.modules.a76.layouts_csv.exportacion.tasks", "api.v1.modules.a76.layouts_csv.customs_brokers.tasks", "api.v1.modules.a76.layouts_csv.clients_and_providers.tasks", "api.v1.modules.a76.layouts_csv.pedmientos.tasks", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 343ed257..75ba989b 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -360,6 +360,40 @@ export const api = { api.post(`/v1/a76/imports/${jobId}/commit`, { model_target: modelTarget }) }, + // CSV import for Operaciones de Exportación (encabezado y partidas). + // Backend: layouts_csv/exportacion — rutas /v1/a76/imports/exportacion/ + exportacionImports: { + upload: ( + file: File, + modelTarget: string, + footerConfig: any, + companyId: number, + templateId?: string + ) => { + const formData = new FormData(); + formData.append('file', file); + if (footerConfig) { + formData.append('footer_config', JSON.stringify(footerConfig)); + } + if (templateId) { + formData.append('template_id', templateId); + } + + const queryParams = new URLSearchParams({ + company_id: String(companyId), + operation_type: 'exp' + }).toString(); + + return fetchApi(`/v1/a76/imports/exportacion/upload/${modelTarget}?${queryParams}`, { + method: 'POST', + body: formData + }); + }, + status: (jobId: string) => api.get(`/v1/a76/imports/exportacion/${jobId}/status`), + commit: (jobId: string, modelTarget: string) => + api.post(`/v1/a76/imports/exportacion/${jobId}/commit`, { model_target: modelTarget }) + }, + // CSV import for Agentes Aduanales (flujo propio en customs_brokers/imports) customsBrokerImports: { upload: (file: File, companyId: number) => { diff --git a/frontend/src/lib/config/csv-upload.ts b/frontend/src/lib/config/csv-upload.ts index f665a1d4..23d2a96c 100644 --- a/frontend/src/lib/config/csv-upload.ts +++ b/frontend/src/lib/config/csv-upload.ts @@ -412,7 +412,7 @@ export const importacionConfig: CsvUploadItem[] = [ ]; // --- Operaciones de Exportación (facturas: encabezados y partidas) -// Backend: layouts_csv/facturas — mismas rutas /v1/a76/imports/ con operation_type=exp. +// Backend: layouts_csv/exportacion — rutas /v1/a76/imports/exportacion/ export const exportacionConfig: CsvUploadItem[] = [ // Expo Def / Cam. Reg. { @@ -422,7 +422,7 @@ export const exportacionConfig: CsvUploadItem[] = [ group: 'Expo. Def./Cam. Reg.', modelTarget: 'invoice_header', templateId: 'exp_def_header', - layoutModule: 'layouts_csv/facturas' + layoutModule: 'layouts_csv/exportacion' }, { id: 'exp_def_details', @@ -431,7 +431,7 @@ export const exportacionConfig: CsvUploadItem[] = [ group: 'Expo. Def./Cam. Reg.', modelTarget: 'invoice_details', templateId: 'exp_def_details', - layoutModule: 'layouts_csv/facturas' + layoutModule: 'layouts_csv/exportacion' }, { id: 'exp_def_series', diff --git a/frontend/src/routes/dashboard/csv-upload/+page.svelte b/frontend/src/routes/dashboard/csv-upload/+page.svelte index bc17f489..01642c52 100644 --- a/frontend/src/routes/dashboard/csv-upload/+page.svelte +++ b/frontend/src/routes/dashboard/csv-upload/+page.svelte @@ -81,6 +81,8 @@ let usePartNumbersImport = $state(false); // Cuando es true, usamos API de importación de BOMs (boms/imports) let useBomImport = $state(false); + // Cuando es true, usamos API de importación de Exportación (layouts_csv/exportacion) + let useExportacionImport = $state(false); // Initialize settings for all tabs upfront to avoid reactivity loops (sync init so child never receives undefined) const _initialSettings: Record = {}; @@ -135,6 +137,7 @@ useTransporterImport = config.id === 'transporters'; usePartNumbersImport = config.id === 'part_numbers'; useBomImport = config.id === 'boms'; + useExportacionImport = activeTab === 'exportacion'; const companyId = companyStore.activeCompany?.id || 1; @@ -388,6 +391,31 @@ if (activeTab === 'importacion') { footerConfig.invoice_type = config.id?.startsWith('imp_def_') ? 'DEF' : 'TEM'; } + + if (useExportacionImport) { + try { + const res = await api.exportacionImports.upload( + file, + config.modelTarget || '', + footerConfig, + companyId, + config.templateId || config.id + ); + if (res.data?.job_id) { + currentJobId = res.data.job_id; + pollStatus(); + } else { + toast.error(res.error || 'Error al subir el archivo'); + isUploading = false; + } + } catch (e) { + console.error('Upload exception', e); + toast.error('Error inesperado al subir el archivo'); + isUploading = false; + } + return; + } + const opType = activeTab === 'exportacion' ? 'exp' : 'imp'; try { @@ -441,7 +469,9 @@ ? await api.partNumberImports.status(currentJobId) : useBomImport ? await api.bomImports.status(currentJobId) - : await api.imports.status(currentJobId); + : useExportacionImport + ? await api.exportacionImports.status(currentJobId) + : await api.imports.status(currentJobId); console.log('Poll response', res); if (res.error && !res.data) { toast.error(res.error || 'Error al consultar el estado'); @@ -660,7 +690,9 @@ ? await api.partNumberImports.commit(currentJobId) : useBomImport ? await api.bomImports.commit(currentJobId) - : await api.imports.commit(currentJobId, activeModelTarget || ''); + : useExportacionImport + ? await api.exportacionImports.commit(currentJobId, activeModelTarget || '') + : await api.imports.commit(currentJobId, activeModelTarget || ''); if (res.data?.commit_job_id) { currentJobId = res.data.commit_job_id; pollStatus();