feature/api-doda-update

This commit is contained in:
2026-04-27 07:04:03 -06:00
parent 9d232998d6
commit 7f8599ac43
17 changed files with 4086 additions and 2793 deletions

View File

@@ -13,7 +13,7 @@ from typing import Any, List, Optional
from sqlalchemy import and_
from sqlalchemy.orm import Session
from .models import Doda
from .models import Doda, DodaPedimento
# Encabezados (orden legacy Clarion)
EXPORT_HEADERS: List[str] = [
@@ -231,3 +231,88 @@ def parse_export_params(
if mode not in ("raw", "formatted"):
raise ValueError("date_mode debe ser raw o formatted")
return d0, d1, fmt, mode
# --- Exportación de pedimentos (líneas) de un DODA específico (legacy / pantalla) ---
PEDIMENTO_EXPORT_HEADERS: List[str] = [
"PATENTE",
"DOCUMENTO",
"ACUSE_VA",
"REMESA",
"CANTIDAD",
"IMPORTE_USD",
"IMPORTE_DIF_USD",
"NIU",
"ARTICULO",
]
def _as_decimal_text(value: Any) -> str:
if value is None:
return ""
return _as_text(value)
def pedimento_row_values(row: DodaPedimento) -> List[str]:
"""
ACUSE_VA = COVE; CANTIDAD = UMC (captura típica en listado);
IMPORTE_USD / IMPORTE_DIF_USD = montos en USD; ARTICULO = art. 7 (0/1).
"""
return [
_as_text(row.authorization_patent),
_as_text(row.document),
_as_text(row.cove),
_as_text(row.shipment),
_as_text(row.umc),
_as_decimal_text(row.effective_amount_usd),
_as_decimal_text(row.difference_amount_usd),
_as_text(row.dta_niu),
_as_text(row.article_7),
]
def list_pedimentos_for_doda_export(
db: Session,
*,
tenant_id: int,
company_id: int,
doda_id: int,
) -> List[DodaPedimento]:
return (
db.query(DodaPedimento)
.join(Doda, DodaPedimento.doda_id == Doda.id)
.filter(
Doda.id == doda_id,
Doda.tenant_id == tenant_id,
Doda.company_id == company_id,
)
.order_by(DodaPedimento.pedimento_line.asc())
.all()
)
def build_pedimentos_export_text(
rows: List[DodaPedimento],
*,
export_format: DodaExportFormat,
) -> str:
delim = _delimiter_for_format(export_format)
out = io.StringIO()
w = csv.writer(
out,
delimiter=delim,
quoting=csv.QUOTE_MINIMAL,
lineterminator="\r\n",
)
w.writerow(PEDIMENTO_EXPORT_HEADERS)
for r in rows:
w.writerow(pedimento_row_values(r))
return out.getvalue()
def parse_pedimento_export_format(format_str: str) -> DodaExportFormat:
try:
return DodaExportFormat(format_str.lower().strip())
except ValueError as e:
raise ValueError("format debe ser csv, xls o txt") from e

View File

@@ -49,8 +49,11 @@ from .print_cache import touch_invalidate_doda_report
from .report_service import DodaReportPdfService
from .export_service import (
build_export_text,
build_pedimentos_export_text,
list_dodas_in_date_range,
list_pedimentos_for_doda_export,
parse_export_params,
parse_pedimento_export_format,
_content_type_and_filename,
)
from core.security import get_current_user, validate_access_to_resource
@@ -108,6 +111,49 @@ async def export_doda_list(
)
@router.get(
"/export/pedimentos/{doda_id}",
summary="Exportar líneas de pedimento de un DODA (CSV, TSV como XLS, TXT con |)",
)
async def export_doda_pedimentos(
doda_id: int,
company_id: int = Query(..., description="Company ID"),
file_format: str = Query("xls", alias="format", description="csv, xls o txt"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Reporte por DODA seleccionado: columnas alineadas al listado de pedimentos (PATENTE, DOCUMENTO, COVE, etc.).
Si no hay líneas, se devuelve el archivo solo con encabezados.
"""
tenant_id = int(validate_access_to_resource(db, company_id, current_user))
try:
fmt = parse_pedimento_export_format(file_format)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
) from e
doda = DodaService.get_by_id(db, doda_id, tenant_id, company_id)
if not doda:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="DODA no encontrado."
)
rows = list_pedimentos_for_doda_export(
db, tenant_id=tenant_id, company_id=company_id, doda_id=doda_id
)
text = build_pedimentos_export_text(rows, export_format=fmt)
content_type, _ = _content_type_and_filename(fmt)
fname = f"doda_pedimentos_{doda_id}.{fmt.value}"
data = ("\ufeff" + text).encode("utf-8")
return StreamingResponse(
io.BytesIO(data),
media_type=content_type,
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
)
# Incluir rutas CRUD (contiene GET /{id}, POST /, PUT /{id}, DELETE /{id}).
# Se registra DESPUÉS de los endpoints literales para que /export, /alta-logs,
# /alta-status no sean capturados por el parámetro /{id}.