diff --git a/backend/api/v1/modules/a76/csv_templates/registry.py b/backend/api/v1/modules/a76/csv_templates/registry.py index b14ae3a6..b17906b3 100644 --- a/backend/api/v1/modules/a76/csv_templates/registry.py +++ b/backend/api/v1/modules/a76/csv_templates/registry.py @@ -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: diff --git a/frontend/src/lib/config/csv-upload.ts b/frontend/src/lib/config/csv-upload.ts index 6d1915b0..426880c2 100644 --- a/frontend/src/lib/config/csv-upload.ts +++ b/frontend/src/lib/config/csv-upload.ts @@ -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',