Nota de edición: artículo y código generados con IA para el equipo de Numio. El ejemplo se ha ejecutado localmente con datos ficticios. El recurso enlazado al final pertenece al blog de Numio.

Un informe puede estar bien diseñado y sumar documentos duplicados. También puede mostrar cero donde en realidad falta un dato. Antes de construir un gráfico, conviene establecer qué archivo se admite, qué controles se aplican y qué pasa si alguno falla.

Este ejemplo en Python valida una exportación pequeña antes de calcular un resumen mensual por empresa. No se conecta a un ERP, no modifica la contabilidad y no envía información a servicios externos.

El contrato del archivo

Trabajaremos con cinco columnas, separadas por punto y coma, en este orden: empresa;documento;fecha;moneda;importe. El archivo es UTF-8. Cada fila representa un documento ya agregado, no una línea de asiento ni una partida de factura. La clave (empresa, documento) debe ser única dentro de esta exportación.

Las fechas tienen formato YYYY-MM-DD. Los importes usan punto decimal, exactamente dos decimales y ningún separador de miles. Una exportación con coma decimal necesita una conversión explícita y revisada antes de este paso; el script la rechaza en vez de adivinar su significado.

empresa;documento;fecha;moneda;importe cliente_demo_A;F001;2026-10-01;EUR;0.10 cliente_demo_A;F002;2026-10-02;EUR;0.20 cliente_demo_A;F003;2026-10-03;EUR;0.00 cliente_demo_B;F001;2026-10-01;EUR;5.00 

F001 puede existir en dos empresas diferentes. Un cero explícito es válido. Un importe vacío bloquea la entrega: no significa cero. Los identificadores son ficticios y sirven únicamente para explicar el formato.

Validar antes de devolver un total

"""Ejemplo educativo: validar un CSV de documentos antes de resumirlo.""" import csv import re from collections import defaultdict from datetime import date from decimal import Decimal, localcontext FIELDS = ["empresa", "documento", "fecha", "moneda", "importe"] def resumir(stream): reader = csv.DictReader(stream, delimiter=";", strict=True) try: fieldnames = reader.fieldnames except csv.Error as exc: raise ValueError("Cabecera CSV no valida") from exc if fieldnames != FIELDS: raise ValueError("Cabecera distinta del contrato esperado") seen = set() totals = defaultdict(lambda: Decimal("0.00")) errors = [] rows = 0 # Limites del ejemplo: 10 000 documentos, 18 digitos enteros por importe. with localcontext() as ctx: ctx.prec = 28 try: for row in reader: rows += 1 line = reader.line_num if rows > 10_000: raise ValueError("El ejemplo admite hasta 10 000 documentos") try: if None in row or any(v is None for v in row.values()): raise ValueError("Numero de columnas incorrecto") row = {k: v.strip() for k, v in row.items()} if not all(row.values()): raise ValueError("Dato pendiente o identificador vacio") if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", row["fecha"]): raise ValueError("Fecha fuera del formato YYYY-MM-DD") day = date.fromisoformat(row["fecha"]) if row["moneda"] != "EUR": raise ValueError("Este ejemplo solo agrega EUR") if not re.fullmatch(r"-?[0-9]{1,18}\.[0-9]{2}", row["importe"]): raise ValueError("Importe: punto decimal y dos decimales") key = (row["empresa"], row["documento"]) if key in seen: raise ValueError("Documento repetido en la misma empresa") seen.add(key) totals[(row["empresa"], day.strftime("%Y-%m"))] += Decimal(row["importe"]) except ValueError as exc: errors.append(f"Linea {line}: {exc}") except csv.Error as exc: raise ValueError(f"CSV no valido cerca de linea {reader.line_num}") from exc if errors: # Nunca devolver un total parcial como si el archivo estuviera validado. raise ValueError("\n".join(errors)) if rows == 0: raise ValueError("El archivo no contiene documentos") return dict(totals) 

Usamos csv.DictReader para leer las columnas por nombre y Decimal a partir del texto original. Evitamos convertir primero a float: convertir después a Decimal no recuperaría necesariamente el valor decimal que se pretendía representar.

El ejemplo solo agrega EUR, admite hasta 10.000 documentos y limita cada importe a 18 dígitos enteros. Con esos límites, la precisión local de 28 posiciones permite estas sumas sin redondeo por falta de precisión. No pretende ser un importador universal ni interpretar todos los dialectos CSV posibles.

Las incidencias incluyen el número de línea física del lector, que puede diferir del número de documento si hay campos entrecomillados con saltos de línea. No imprimimos el contenido completo de las filas en los mensajes de error.

Si aparece cualquier incidencia, la función lanza ValueError y no devuelve los totales parciales. En una aplicación real conviene registrar el estado del archivo y mostrar las incidencias solo a las personas autorizadas para esa empresa.

Ejecutar el ejemplo

Guarda la función en validar_csv.py y el archivo ficticio en documentos.csv. Después puedes invocarla así:

from validar_csv import resumir with open("documentos.csv", encoding="utf-8-sig", newline="") as source: totals = resumir(source) for (company, month), amount in sorted(totals.items()): print(company, month, f"{amount:.2f}") 

El resultado esperado es cliente_demo_A 2026-10 0.30 y cliente_demo_B 2026-10 5.00. Si repites F001 para cliente_demo_A, dejas un importe vacío o cambias EUR por USD, la función debe detener el resumen con un error.

Lo que esta validación no demuestra

Un archivo que supera estos controles todavía puede estar incompleto respecto al sistema de origen. Conserva la fecha de exportación, el periodo, el identificador de la fuente y el número esperado de documentos. Contrasta el resumen con un control independiente antes de entregarlo.

El total del campo importe tampoco es automáticamente beneficio, caja o ingreso. Su significado depende de cómo se haya definido la exportación. Los documentos rectificativos, series distintas, monedas y políticas de signos requieren un contrato específico.

Para conectar esta capa técnica con la entrega al cliente, nuestra guía de reporting financiero colaborativo para asesorías desarrolla criterios de selección e implantación. El código anterior funciona por separado y no requiere utilizar Numio.

Documentación de referencia: lector CSV de Python y Decimal.