feature/layouts-csv-refact

This commit is contained in:
hreyes
2026-03-03 16:45:12 -07:00
parent 973298ccd9
commit 4c781b460a
81 changed files with 151 additions and 97 deletions

1
.gitignore vendored
View File

@@ -67,6 +67,7 @@ htmlcov/
*.dockerignore
postgres-data/
backend/uploads/
backend/layouts/imports/
docker-compose.yml
.mypy_cache/

View File

@@ -42,14 +42,24 @@ anexo76/
│ ├── .env.example # Variables de entorno de ejemplo
│ ├── core/ # Módulos core
│ │ ├── config.py # Configuración centralizada
│ │ ├── paths.py # Rutas base y layout_path() para importación CSV
│ │ ├── database.py # Configuración de BD multi-tenant
│ │ ├── security.py # Autenticación y autorización
│ │ └── middleware.py # Middlewares personalizados
│ ├── layouts/ # Directorio de datos: temp y errors de importación CSV (creado al arranque)
│ └── api/
│ └── v1/
│ ├── router.py # Router principal API v1
│ └── modules/ # Módulos de negocio
── auth/ # Autenticación
── a76/
│ ├── layouts_csv/ # Lógica centralizada de cargas por CSV (routes, tasks, validaciones por proceso)
│ │ ├── facturas/
│ │ ├── customs_brokers/
│ │ ├── clients_and_providers/
│ │ └── ... # Una carpeta por proceso de importación
│ ├── csv_templates/
│ └── ...
│ ├── auth/
│ ├── tenants/ # Gestión de tenants
│ ├── licenses/ # Control de licencias
│ └── ...
@@ -136,6 +146,11 @@ python -c "from core.database import init_db; init_db()"
```
docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend
```
**Importación CSV y workers Celery**: La lógica de cargas por CSV está en `api/v1/modules/a76/layouts_csv/` (una carpeta por proceso). Los workers Celery deben cargar los módulos `api.v1.modules.a76.layouts_csv.<proceso>.tasks`. El directorio de trabajo del worker debe ser la raíz del backend para que `layout_path("imports", "temp")` y `layout_path("imports", "errors")` apunten a los mismos directorios que la API. Si API y worker comparten volumen, los archivos temporales y de errores se escriben en `backend/layouts/imports/temp` y `backend/layouts/imports/errors`.
```
docker build \
--build-arg VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/ \
--build-arg VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/ \
@@ -165,6 +180,8 @@ pip install -r requirements.txt
uvicorn main:app --reload
```
Los directorios `backend/layouts/imports/temp` y `backend/layouts/imports/errors` se crean automáticamente al arrancar el backend para la importación CSV.
### Frontend
```bash

View File

@@ -5,7 +5,7 @@ Mismo patrón que parts y classes: upload → scan → status → commit.
from fastapi import APIRouter
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.boms.routes import router as imports_router
router = APIRouter()

View File

@@ -12,7 +12,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_t
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO, ClassWithFADataResponse
from .service import ClassService
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.classes.routes import router as imports_router
# Create a new router for custom endpoints
router = APIRouter()

View File

@@ -20,7 +20,7 @@ from .dto import (
)
from .service import ClientProviderService
from .models import ClientProvider
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.clients_and_providers.routes import router as imports_router
# Create main router to add custom endpoints
router = APIRouter(prefix="/clients-providers")

View File

@@ -7,35 +7,35 @@ import io
from typing import Dict, List, Optional
# Importar configs de cada módulo
from api.v1.modules.a76.imports.template_config import (
from api.v1.modules.a76.layouts_csv.facturas.template_config import (
TEMPLATE_COLUMNS as IMPORTS_TEMPLATE_COLUMNS,
_resolve_template_columns as resolve_imports_template,
)
from api.v1.modules.a76.parts.imports.template_config import TEMPLATE_COLUMNS as PARTS_TEMPLATE_COLUMNS
from api.v1.modules.a76.boms.imports.template_config import TEMPLATE_COLUMNS as BOMS_TEMPLATE_COLUMNS
from api.v1.modules.a76.classes.imports.template_config import TEMPLATE_COLUMNS as CLASSES_TEMPLATE_COLUMNS
from api.v1.modules.a76.customs_brokers.imports.template_config import (
from api.v1.modules.a76.layouts_csv.parts.template_config import TEMPLATE_COLUMNS as PARTS_TEMPLATE_COLUMNS
from api.v1.modules.a76.layouts_csv.boms.template_config import TEMPLATE_COLUMNS as BOMS_TEMPLATE_COLUMNS
from api.v1.modules.a76.layouts_csv.classes.template_config import TEMPLATE_COLUMNS as CLASSES_TEMPLATE_COLUMNS
from api.v1.modules.a76.layouts_csv.customs_brokers.template_config import (
TEMPLATE_COLUMNS as CUSTOMS_BROKERS_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.clients_and_providers.imports.template_config import (
from api.v1.modules.a76.layouts_csv.clients_and_providers.template_config import (
TEMPLATE_COLUMNS as CLIENTS_PROVIDERS_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.general_catalogs.exchange_rate.imports.template_config import (
from api.v1.modules.a76.layouts_csv.exchange_rate.template_config import (
TEMPLATE_COLUMNS as EXCHANGE_RATE_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.imports.template_config import (
from api.v1.modules.a76.layouts_csv.us_tariff_fractions.template_config import (
TEMPLATE_COLUMNS as US_TARIFF_FRACTIONS_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.pedmientos.imports.template_config import (
from api.v1.modules.a76.layouts_csv.pedmientos.template_config import (
TEMPLATE_COLUMNS as PEDIMENTOS_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.transportation.vehicles.imports.template_config import (
from api.v1.modules.a76.layouts_csv.vehicles.template_config import (
TEMPLATE_COLUMNS as VEHICLES_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.transportation.drivers.imports.template_config import (
from api.v1.modules.a76.layouts_csv.drivers.template_config import (
TEMPLATE_COLUMNS as DRIVERS_TEMPLATE_COLUMNS,
)
from api.v1.modules.a76.transportation.trailers.imports.template_config import (
from api.v1.modules.a76.layouts_csv.trailers.template_config import (
TEMPLATE_COLUMNS as TRAILERS_TEMPLATE_COLUMNS,
)

View File

@@ -1,2 +0,0 @@
# CSV import flow for Agentes Aduanales (Customs Brokers).
# Replicates the same two-phase flow as a76.imports: upload → scan → waiting_confirmation → commit.

View File

@@ -7,7 +7,7 @@ from core.security import get_current_user, validate_access_to_resource
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from . import dto, services
from .imports.routes import router as imports_router
from ..layouts_csv.customs_brokers.routes import router as imports_router
router = APIRouter()

View File

@@ -35,7 +35,7 @@ from fastapi import APIRouter
custom_router = APIRouter(prefix="/exchange-rate", tags=[])
# CSV import: add to custom_router BEFORE including it in master, so /exchange-rate/imports/* is registered
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.exchange_rate.routes import router as imports_router
custom_router.include_router(imports_router, prefix="/imports", tags=["exchange_rate / csv_import"])
@custom_router.get("/test-ping")

View File

@@ -31,7 +31,7 @@ crud_router = TenantCRUDRoutes(
)
# Master router with prefix so all routes live under /us-tariff-fractions
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.us_tariff_fractions.routes import router as imports_router
main_router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
main_router.include_router(imports_router, prefix="/imports", tags=["us_tariff_fractions / csv_import"])
main_router.include_router(crud_router.router)

View File

@@ -0,0 +1 @@
# Lógica centralizada de cargas por CSV (routes, tasks, validaciones, template_config por proceso)

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -78,7 +79,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"bom_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List, Set
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -33,7 +34,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -162,7 +163,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
@@ -306,8 +307,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"bom_{job_id}.jsonl")
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -78,7 +79,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"cls_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List, Set
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -31,7 +32,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -156,7 +157,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
@@ -310,8 +311,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"cls_{job_id}.jsonl")
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"cp_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
from api.v1.modules.a76.clients_and_providers.models import (
@@ -37,7 +38,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -165,7 +166,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
@@ -290,8 +291,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"cp_{job_id}.jsonl")
error_lines = set()

View File

@@ -0,0 +1 @@
# layouts_csv.customs_brokers

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"cb_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -32,7 +33,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -120,7 +121,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
@@ -245,8 +246,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"cb_{job_id}.jsonl")
error_lines = set()

View File

@@ -15,6 +15,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"drv_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -13,6 +13,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -32,7 +33,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -281,7 +282,7 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
@@ -429,8 +430,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"drv_{job_id}.jsonl")
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"er_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -36,7 +37,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -150,7 +151,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
@@ -274,8 +275,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"er_{job_id}.jsonl")
error_lines = set()

View File

@@ -11,6 +11,7 @@ from typing import Optional, Literal, Dict, Any
from core.celery_app import celery_app
from core.config import settings
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 .tasks import (
@@ -88,7 +89,7 @@ async def upload_import_file(
# Optional: also write to local disk (e.g. for same-machine worker or debugging)
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(upload_dir, f"{job_id}.csv")
meta_path = os.path.join(upload_dir, f"{job_id}.meta.json")

View File

@@ -11,6 +11,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
# Models are imported inside tasks to avoid circular dependencies and mapper initialization issues in the API process
@@ -37,7 +38,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
"""Directory on the worker for temp CSV and meta (same structure as API, but local to worker)."""
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -189,8 +190,9 @@ def scan_file(self, job_id: str, model_target: str, config: str = None):
_ensure_worker_has_meta_from_redis(job_id, file_path)
# 2. Setup Error Log
error_path = file_path.replace("temp", "errors").replace(".csv", ".jsonl")
os.makedirs(os.path.dirname(error_path), exist_ok=True)
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"{job_id}.jsonl")
total_rows = 0
error_count = 0
@@ -805,7 +807,8 @@ def insert_valid_rows(self, job_id: str, model_target: str):
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
from api.v1.modules.a76.parts.models import Part
error_path = file_path.replace("temp", "errors").replace(".csv", ".jsonl")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"{job_id}.jsonl")
# 1. Load Error Line Numbers (from Redis if scan ran on another worker, else from file)
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -78,7 +79,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"part_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List, Set
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -32,7 +33,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -183,7 +184,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
@@ -368,8 +369,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"part_{job_id}.jsonl")
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"ped_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List, Set
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -34,7 +35,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -243,7 +244,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
@@ -381,8 +382,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"ped_{job_id}.jsonl")
error_lines = set()

View File

@@ -15,6 +15,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"trl_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -33,7 +34,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -180,7 +181,7 @@ def _do_scan(job_id: str, progress_callback: Optional[Any] = None) -> Dict[str,
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
@@ -328,8 +329,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"trl_{job_id}.jsonl")
error_lines = set()

View File

@@ -14,6 +14,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"fa_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -34,7 +35,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -157,7 +158,7 @@ def scan_file(self, job_id: str, config: str = None):
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
@@ -290,8 +291,7 @@ def insert_valid_rows(self, job_id: str):
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"fa_{job_id}.jsonl")
error_lines = set()

View File

@@ -15,6 +15,7 @@ from typing import 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
@@ -81,7 +82,7 @@ async def upload_import_file(
raise HTTPException(status_code=500, detail="No se pudo encolar el archivo.")
try:
upload_dir = os.path.join(os.getcwd(), "uploads", "temp")
upload_dir = layout_path("imports", "temp")
os.makedirs(upload_dir, exist_ok=True)
with open(os.path.join(upload_dir, f"veh_{job_id}.csv"), "wb") as f:
f.write(contents)

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, List
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.paths import layout_path
from .template_config import row_from_template
@@ -34,7 +35,7 @@ def _get_redis():
def _worker_upload_dir() -> str:
return os.path.join(os.getcwd(), "uploads", "temp")
return layout_path("imports", "temp")
def _ensure_worker_has_file_from_redis(job_id: str) -> Optional[str]:
@@ -213,7 +214,7 @@ def _do_scan(
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
_ensure_worker_has_meta_from_redis(job_id, file_path)
error_dir = os.path.join(os.path.dirname(file_path).replace("temp", "errors"), "")
error_dir = layout_path("imports", "errors")
os.makedirs(error_dir, exist_ok=True)
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
@@ -404,8 +405,7 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
else:
_ensure_worker_has_meta_from_redis(job_id, file_path)
base_dir = os.path.dirname(file_path)
error_dir = base_dir.replace("temp", "errors")
error_dir = layout_path("imports", "errors")
error_path = os.path.join(error_dir, f"veh_{job_id}.jsonl")
error_lines = set()

View File

@@ -8,7 +8,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import PartCreateDTO, PartResponseDTO, PartUpdateDTO
from .service import PartService
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.parts.routes import router as imports_router
router = APIRouter()

View File

@@ -31,7 +31,7 @@ from .routes.pedimento_rectification_origin import (
from .routes.pedimento_transport_means import router as pedimento_transport_means_router
from .routes.pedimento_validation import router as pedimento_validation_router
from .routes.pedimentos import router as pedimentos_router
from .imports.routes import router as pedimentos_imports_router
from api.v1.modules.a76.layouts_csv.pedmientos.routes import router as pedimentos_imports_router
router = APIRouter()

View File

@@ -14,7 +14,7 @@ from .items.routes import router as items_router
from .classes import router as classes_router
from .clients_and_providers import router as client_and_provider_router
from .imports.routes import router as imports_router
from .layouts_csv.facturas.routes import router as 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

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from .dto import DriverCreateDTO, DriverResponseDTO
from .models import Driver
from .services import DriverService
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.drivers.routes import router as imports_router
router = APIRouter(prefix="/drivers")

View File

@@ -4,7 +4,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import TrailerCreateDTO, TrailerResponseDTO, TrailerUpdateDTO
from .services import TrailerService
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.trailers.routes import router as imports_router
# Main router: trailers CRUD + CSV imports
router = APIRouter()

View File

@@ -4,7 +4,7 @@ from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import VehicleCreateDTO, VehicleResponseDTO, VehicleUpdateDTO
from .services import VehicleService
from .imports.routes import router as imports_router
from api.v1.modules.a76.layouts_csv.vehicles.routes import router as imports_router
# Main router: vehicles CRUD + CSV imports
router = APIRouter()

Binary file not shown.

View File

@@ -32,16 +32,18 @@ celery_app.conf.update(
"api.v1.modules.a76.reports.exportacion.aviso_consolidado.task",
"api.v1.modules.a76.reports.movements.invoices.tasks",
"api.v1.modules.a76.reports.exportacion.descargo.task",
"api.v1.modules.a76.imports.tasks",
"api.v1.modules.a76.customs_brokers.imports.tasks",
"api.v1.modules.a76.clients_and_providers.imports.tasks",
"api.v1.modules.a76.pedmientos.imports.tasks",
"api.v1.modules.a76.general_catalogs.exchange_rate.imports.tasks",
"api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.imports.tasks",
"api.v1.modules.a76.classes.imports.tasks",
"api.v1.modules.a76.parts.imports.tasks",
"api.v1.modules.a76.boms.imports.tasks",
"api.v1.modules.a76.transportation.vehicles.imports.tasks",
"api.v1.modules.a76.layouts_csv.facturas.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",
"api.v1.modules.a76.layouts_csv.exchange_rate.tasks",
"api.v1.modules.a76.layouts_csv.us_tariff_fractions.tasks",
"api.v1.modules.a76.layouts_csv.classes.tasks",
"api.v1.modules.a76.layouts_csv.parts.tasks",
"api.v1.modules.a76.layouts_csv.boms.tasks",
"api.v1.modules.a76.layouts_csv.vehicles.tasks",
"api.v1.modules.a76.layouts_csv.drivers.tasks",
"api.v1.modules.a76.layouts_csv.trailers.tasks",
"api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task",
"api.v1.modules.a76.reports.importacion.transmission.temporal.MAINX30.task",
"api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task",

15
backend/core/paths.py Normal file
View File

@@ -0,0 +1,15 @@
"""
Rutas base y resolución de paths para layouts (importación CSV, temp, errors).
"""
from pathlib import Path
# Raíz del backend (directorio que contiene api/, core/, etc.)
BASE_DIR = Path(__file__).resolve().parent.parent
def layout_path(*parts: str) -> str:
"""Construye una ruta absoluta bajo backend/layouts/."""
p = BASE_DIR / "layouts"
for part in parts:
p = p / part
return str(p)

View File

@@ -89,6 +89,7 @@ import core.celery_app # Initialize Celery App
from api.v1.router import router as api_v1_router
from core.config import settings
from core.database import init_db
from core.paths import layout_path
from core.error_handlers import register_exception_handlers
from core.middleware import (
LicenseValidationMiddleware,
@@ -359,6 +360,9 @@ uploads_dir = Path("uploads").resolve()
uploads_dir.mkdir(parents=True, exist_ok=True)
app.mount("/api/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
# Directorios para importación CSV (layouts: temp y errors)
Path(layout_path("imports", "temp")).mkdir(parents=True, exist_ok=True)
Path(layout_path("imports", "errors")).mkdir(parents=True, exist_ok=True)
# Registrar routers
app.include_router(api_v1_router, prefix="/api/v1")