fix/carga-csv-headers

This commit is contained in:
2026-04-27 09:38:15 -06:00
parent df6ffe422a
commit 2929890994
2 changed files with 168 additions and 2 deletions

View File

@@ -49,6 +49,168 @@ from api.v1.modules.a76.layouts_csv.transportistas.template_config import (
)
def _normalize_header_for_match(header: str) -> str:
"""Normaliza cabeceras para comparación interna (sin alterar salida)."""
if not header:
return ""
cleaned = header.strip()
if cleaned.startswith("* "):
cleaned = cleaned[2:]
return cleaned.strip().upper()
ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE: Dict[str, set[str]] = {
# Facturas encabezados
"imp_temp_header": {
"NUMERO FACTURA",
"FECHA FACTURA",
"REGIMEN",
"CLAVE PROVEEDOR",
"CLAVE VENDIDO A",
"CLAVE ENVIADO A",
"AGENTE ADUANAL",
"ADUANA DE CRUCE",
},
"imp_def_header": {
"NUMERO FACTURA",
"FECHA FACTURA",
"REGIMEN",
"CLAVE PROVEEDOR",
"CLAVE VENDIDO A",
"CLAVE ENVIADO A",
"AGENTE ADUANAL",
},
"exp_def_header": {
"NUMERO FACTURA",
"FECHA FACTURA",
"REGIMEN",
"CLAVE PROVEEDOR",
"CLAVE VENDIDO A",
"CLAVE ENVIADO A",
"AGENTE ADUANAL",
"ADUANA DE CRUCE",
},
"cmex_header": {
"NUMERO FACTURA",
"FECHA FACTURA",
"CLAVE PROVEEDOR",
"CLAVE VENDIDO A",
"CLAVE ENVIADO A",
},
# Facturas partidas (plantillas del registry)
"imp_temp_details": {
"NUMERO FACTURA",
"CLASE",
"CANTIDAD IMPORTADA",
"COSTO UNITARIO",
"PESO NETO",
"PAIS ORIGEN",
"PREFERENCIA ARANCELARIA",
},
"imp_def_details": {
"NUMERO FACTURA",
"CLASE",
"CANTIDAD IMPORTADA",
"COSTO UNITARIO",
"PESO NETO",
"PAIS ORIGEN",
"PREFERENCIA ARANCELARIA",
"NUM. PARTE",
},
"exp_def_details": {
"NUMERO FACTURA",
"CLASE",
"CANTIDAD IMPORTADA",
"COSTO UNITARIO",
"PESO NETO",
"PAIS ORIGEN",
"PREFERENCIA ARANCELARIA",
},
"cmex_details": {
"NUMERO FACTURA",
"CLASE",
"CANTIDAD IMPORTADA",
"COSTO UNITARIO",
"PESO NETO",
"PAIS ORIGEN",
"PREFERENCIA ARANCELARIA",
"NUM. PARTE",
},
# Facturas series (plantillas del registry)
"imp_temp_series": {"NUMERO FACTURA", "LINEA FACTURA"},
"imp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"},
"cmex_series": {"NUMERO FACTURA", "LINEA FACTURA"},
# Catálogos y transportes
"customs_brokers": {"TIPO", "CLAVE", "NOMBRE"},
"clients_providers": {"PROCEDENCIA", "SHORT_NAME", "NOMBRE", "RFC"},
"exchange_rates": {"FECHA", "VALOR"},
"american_fractions": {"FRACCION_ARANCELARIA", "DESCRIPCION"},
"material_classes": {
"CLAVE CLASE",
"CLASE",
"DESCRIPCION ESPAÑOL",
"DESCRIPCIONE",
"TIPO DE MATERIAL",
"CLAVEMAT",
"U.M. COMERCIAL",
"UNIMED",
"FRACCION ARANCELARIA",
"FRACCION",
},
"part_numbers": {
"NUMERO DE PARTE",
"NUMPARTE",
"DESCRIPCION EN ESPAÑOL",
"DESCRIPCIONE",
"UNIDAD DE MEDIDA COMERCIAL",
"UNIMED",
},
"items": {
"NUMERO DE PARTE",
"NUMPARTE",
"DESCRIPCION EN ESPAÑOL",
"DESCRIPCIONE",
"UNIDAD DE MEDIDA COMERCIAL",
"UNIMED",
},
"boms": {"NUMPARTE_PADRE", "NUMPARTE_COMPONENTE", "CANTIDAD"},
"pedimentos": {
"AÑO", "PATENTE", "NUMERO", "PEDIMENTO",
"TIPO_OPERACION",
"CLAVE_PEDIMENTO",
"REGIMEN",
"FECHA_INICIO",
"FECHA_FINAL",
"FECHA_PAGO",
"ADUANA_SECCION_CRUCE",
},
"transports": {"CLAVE", "CODIGO DE ENTIDAD"},
"drivers": {"TRANSPORTISTA", "LINEA", "CLAVE CONDUCTOR"},
"trailers": {"NUMERO TRAILER"},
"transporters": {"CLAVE TRANSPORTISTA", "NOMBRE"},
}
def _apply_required_prefix(template_id: str, headers: List[str]) -> List[str]:
"""Prefija '* ' a cabeceras siempre obligatorias para la plantilla."""
required_headers = ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE.get(template_id)
if not required_headers:
return headers
required_norm = {_normalize_header_for_match(h) for h in required_headers}
out: List[str] = []
for header in headers:
norm_header = _normalize_header_for_match(header)
if norm_header and norm_header in required_norm:
if header.startswith("* "):
out.append(header)
else:
out.append(f"* {header}")
else:
out.append(header)
return out
def _canonicals_from_columns(cols: Optional[List[Dict]]) -> List[str]:
"""Extrae la lista de nombres canónicos en orden a partir de una lista de columnas."""
if not cols:
@@ -163,7 +325,10 @@ TEMPLATE_FILENAMES: Dict[str, str] = {
def get_template_headers(template_id: str) -> Optional[List[str]]:
"""Devuelve la lista de cabeceras canónicas para el template_id, o None si no existe."""
return _TEMPLATE_HEADERS.get(template_id)
headers = _TEMPLATE_HEADERS.get(template_id)
if headers is None:
return None
return _apply_required_prefix(template_id, headers)
def get_template_filename(template_id: str) -> str:

View File

@@ -248,7 +248,8 @@ export const catalogosConfig: CsvUploadItem[] = [
icon: Briefcase,
modelTarget: 'Bom',
templateId: 'boms',
layoutModule: 'layouts_csv/boms'
layoutModule: 'layouts_csv/boms',
disabled: true
},
{
id: 'items',