Ingeniería con IA en producción

Tutorial: Bajamos a cero nuestra propia factura de ERP — el estudio de campo completo

Nota editorial. Esta es nuestra propia contabilidad. Migramos primero nuestros propios libros, antes de recomendar este camino a nadie más — cada cifra aquí es real y sin editar. Este artículo se redactó con fuerte asistencia de IA; ver "Una nota sobre cómo se escribió esto" más abajo para lo que eso significa exactamente y por qué creemos que importa.

Para el fundador que decide si vale la pena leer esto

Si operas una empresa sobre Odoo Enterprise, Salesforce, NetSuite o cualquier ERP SaaS por usuario, probablemente ya hiciste esta cuenta al menos una vez: ¿cuánto nos costaría realmente irnos?

La respuesta honesta es que la licencia es la parte más pequeña del costo. El costo real es la migración — específicamente, el riesgo de que tu contabilidad llegue al otro lado sutilmente mal de formas que nadie nota hasta que un auditor lo hace.

Este es el relato completo de una migración de ese tipo — la nuestra — ejecutada en una sola sesión de trabajo contra una fecha límite dura, con el trabajo que siguió durante el día siguiente. Terminó con una balanza de comprobación a nueve centavos del origen en las 78 cuentas, con facturas, pagos y conciliaciones restaurados como documentos reales y navegables. Llegar ahí también produjo cinco modos de falla distintos que destruyeron o corrompieron datos silenciosamente mientras reportaban éxito.

Esos cinco son la razón para leer esto. No son específicos de Odoo — son lo que ocurre siempre que mueves datos contables entre sistemas, y cada uno de ellos habría producido un libro mayor con apariencia plausible pero incorrecto.

La situación

Operábamos toda nuestra contabilidad en Odoo Enterprise Online. Se tomó una decisión de reducción de costos: la suscripción Enterprise no se renovaría. Venció hoy.

El destino era una instancia de Odoo 19 Community autoalojada, ya corriendo en Docker. El supuesto de partida — razonable, y equivocado — era que ambos lados eran la misma versión, por lo que un respaldo de base de datos simplemente debería restaurarse.

Lo que sigue es un relato paso a paso de cómo un agente de ingeniería de IA (Claude Opus 5) abordó el problema, qué acertó, qué se equivocó, y dónde viven los modos de falla interesantes. Publicamos los errores deliberadamente: son más instructivos que los aciertos.

Nota sobre cómo se escribió este artículo

Qué se automatizó: la migración misma fue ejecutada por un agente de codificación con IA — Claude Code corriendo Opus 5 — operando directamente contra las instancias de Odoo de origen y destino, una copia forense restaurada de PostgreSQL, y un puñado de subagentes de investigación en paralelo. La narrativa técnica de este artículo, los diagramas y el primer borrador también se produjeron con la asistencia del mismo agente, trabajando a partir de su propio registro de sesión y del documento de auditoría interno que lo acompaña.

Cómo se usó: el agente leyó el código fuente del framework para verificar afirmaciones, ejecutó y monitoreó los scripts reales de migración, y redactó lo que hizo y dónde falló. Un humano revisó el borrador contra el rastro de auditoría subyacente antes de publicarlo, corrigió varias cifras infladas o no sustentadas que traía el primer borrador, y reescribió el cierre para que coincidiera con lo que registra la auditoría en lugar de un checkpoint anterior, más favorecedor.

Por qué: el punto de este artículo son los modos de falla, no el hecho de que la IA lo redactara. Un agente que puede migrar un libro mayor de producción también puede documentar cómo lo hizo — hacerlo con precisión resulta requerir la misma disciplina en ambos trabajos: verificar contra una fuente que tú no produjiste.

1. La forma del problema

Diagrama que compara el sistema de origen Odoo Enterprise Online, con 505 módulos y 4,457 asientos contables, contra el destino Odoo Community autoalojado con 117 módulos y cero asientos, unidos por una flecha que indica que la suscripción vence hoy.
Ver diagrama Mermaid
graph LR
    subgraph SRC["ORIGEN — Enterprise Online"]
        A1["Odoo saas~19.2+e"]
        A2["505 módulos
274 con licencia Enterprise"] A3["4,457 asientos contables
en ambas entidades legales"] A4["4,170 archivos en filestore"] end subgraph DST["DESTINO — Community autoalojado"] B1["Odoo 19.0 CE"] B2["117 módulos
0 Enterprise"] B3["0 asientos contables"] B4["Sin stack de facturación electrónica"] end SRC -->|"la suscripción vence HOY"| DST style SRC fill:#7c2d12,color:#fff style DST fill:#1e3a5f,color:#fff
El origen tenía 505 módulos (274 exclusivos de Enterprise) y 4,457 asientos contables; el destino arrancaba en cero, con la suscripción venciendo el mismo día.

Dos números enmarcan todo: 274 de 505 módulos instalados eran exclusivos de Enterprise, y la contabilidad tenía que sobrevivir intacta para efectos de auditoría fiscal.

2. Primer movimiento: verificar la premisa, no el plan

El brief pedía tres tareas en un orden específico. La primera acción sustantiva del agente no fue ejecutar ninguna de ellas — fue consultar la cadena de versión del servidor origen.

Diagrama de flujo que muestra cómo verificar la versión de ambos servidores antes de planear reveló que el origen corre saas~19.2 y el destino 19.0, versiones incompatibles según el código fuente de Odoo, lo que invalidó el enfoque de restaurar el respaldo directamente.
Ver diagrama Mermaid
flowchart TD
    START["Brief recibido:
'ambos son v19, restaura el dump'"] --> Q{"Verificar versión
antes de planear"} Q -->|"consultar servidor"| V1["Origen reporta:
saas~19.2+e"] Q -->|"sondear destino"| V2["Destino reporta:
19.0-20260630"] V1 --> D{"¿Misma versión?"} V2 --> D D -->|"NO"| K["Leer código fuente de Odoo:
list_db_incompatible()"] K --> R["'saas~19.2' != '19.0'
→ base de datos rechazada
→ no existen scripts de downgrade"] R --> OUT["El enfoque propuesto
completo es imposible"] style OUT fill:#7c2d12,color:#fff style R fill:#78350f,color:#fff
Verificar la versión de ambos servidores antes de ejecutar el brief reveló que el dump del origen (saas~19.2) es rechazado por el destino (19.0), invalidando el plan original.

Odoo Online corre ramas saas~ rodantes, adelantadas respecto al release estable on-premise. La documentación oficial es explícita — "las versiones intermedias de Odoo Online no son soportadas por Odoo.sh ni on-premise" — y el mecanismo es verificable en el código fuente: odoo/service/db.py::list_db_incompatible() compara el base.latest_version almacenado contra la versión del servidor en ejecución y rechaza la base de datos.

Esta única verificación, hecha en los primeros minutos, invalidó todo el brief. Si el agente hubiera empezado a ejecutar los pasos solicitados en orden, esto habría salido a la luz horas después, tras descargar un dump, intentar restaurarlo y una sesión de depuración.

El replanteamiento: el dump sigue siendo obligatorio, pero como archivo forense restaurado en PostgreSQL plano — consultable por SQL para siempre, sin Odoo de por medio. La migración en sí ocurre registro por registro vía RPC.

3. Patrón de razonamiento: el bucle de verificación

El patrón conductual dominante a lo largo de la sesión fue un bucle ajustado que trata cada afirmación — incluidas las propias — como no verificada hasta medirla.

Diagrama de flujo circular que muestra el bucle de verificación: cada afirmación se mide directamente, si la evidencia no coincide se revisa el modelo de la realidad, y al actuar se verifica el efecto en el dinero, no el artefacto, antes de darse por terminado.
Ver diagrama Mermaid
flowchart LR
    C["Afirmación o supuesto"] --> M["Medirlo
directamente"] M --> E{"¿La evidencia
coincide?"} E -->|sí| A["Actuar"] E -->|no| R["Revisar el modelo
de la realidad"] R --> M A --> V["Verificar el
efecto, no el
artefacto"] V --> F{"¿El dinero coincide
con el dinero?"} F -->|no| R F -->|sí| DONE["Listo"] style DONE fill:#14532d,color:#fff style R fill:#78350f,color:#fff
El agente trató cada afirmación como no verificada hasta medirla, cerrando el ciclo únicamente cuando el efecto en el dinero coincidía, no cuando el artefacto lo sugería.

Concretamente, esto significó rechazar ciertas categorías de evidencia:

No se acepta como pruebaSí se acepta como prueba
"El script corrió sin errores"Los conteos de filas coinciden con la verdad SQL
"El módulo aparece como instalado"El asistente del reporte realmente se ejecuta
"Se crearon 3,325 registros"El debe y el haber coinciden con el origen por cuenta
"El archivo se copió"El SHA-1 de la copia es igual al checksum del origen

Esta distinción no fue académica. Dos veces durante la sesión, una operación reportó éxito mientras perdía datos silenciosamente.

4. Arquitectura de herramientas

El agente operó a través de cuatro rutas de acceso distintas, eligiendo entre ellas de forma deliberada en vez de recurrir por defecto a una sola.

Diagrama de arquitectura que muestra al agente accediendo a las instancias Odoo mediante cuatro rutas distintas: el conector MCP para lecturas y verificación, JSON-RPC directo para escrituras masivas, PostgreSQL como verdad de base, y workflows de subagentes para investigación en paralelo.
Ver diagrama Mermaid
graph TB
    AG["Agente"]
    subgraph paths["Rutas de acceso"]
        MCP["Conector MCP
lecturas, verificación"] RPC["JSON-RPC directo
escrituras masivas"] PG["PostgreSQL
verdad de base"] WF["Workflows de subagentes
investigación en paralelo"] end AG --> MCP & RPC & PG & WF MCP -->|"solo XML-RPC
no puede serializar None"| ODOO["Instancias de Odoo"] RPC -->|"necesita UA de navegador
o Cloudflare 403"| ODOO PG --> DUMP["Dump restaurado
4,457 asientos"] WF --> RES["Especificaciones de diseño
análisis de brechas"] style PG fill:#14532d,color:#fff style AG fill:#1e3a5f,color:#fff
El agente eligió deliberadamente entre cuatro rutas de acceso — conector MCP, JSON-RPC directo, PostgreSQL y subagentes — en vez de depender de una sola.

Dos restricciones surgieron empíricamente y moldearon todo el enfoque:

El conector MCP fuerza XML-RPC. Solicitar JSON-RPC devolvía effective_protocol: xmlrpc. Como XML-RPC no puede serializar None, cualquier método de Odoo que devuelva un diccionario de acción con campos nulos falla con cannot marshal None. El conector siguió siendo excelente para lecturas y verificación; las operaciones masivas se hicieron por JSON-RPC directo.

Un CDN se interponía frente al destino. Las solicitudes POST a /jsonrpc sin un User-Agent de navegador devolvían 403 — indistinguible, al principio, de una falla de autenticación. Diagnosticar esto correctamente (problema de transporte, no de credenciales) evitó un camino de depuración equivocado.

5. Investigación en paralelo: dónde los subagentes se ganan su lugar

Cuatro preguntas bloqueaban el avance y eran mutuamente independientes. En vez de serializarlas, el agente las despachó como subagentes en paralelo.

Diagrama que muestra al orquestador despachando cuatro subagentes de investigación en paralelo (auditoría de asientos, viabilidad de PostgreSQL, prorrateo de impuestos, adjuntos y chatter) que convergen en una síntesis, mientras las escrituras masivas a la base de datos en vivo nunca se delegan.
Ver diagrama Mermaid
graph TD
    O["Orquestador"] --> A["Agente A
Auditoría de asientos"] O --> B["Agente B
Viabilidad de PostgreSQL"] O --> C["Agente C
Prorrateo de impuestos"] O --> D["Agente D
Adjuntos + chatter"] A --> S["Síntesis"] B --> S C --> S D --> S S --> P["Plan consolidado"] O -.->|"NUNCA delegado"| W["Escrituras masivas a
la base de datos en vivo"] style W fill:#7c2d12,color:#fff style P fill:#14532d,color:#fff
Cuatro subagentes investigaron en paralelo preguntas independientes, pero las escrituras masivas a la base de datos en vivo se mantuvieron estrictamente fuera de la delegación.

El límite importa más que el paralelismo. La investigación se abrió en abanico; las escrituras nunca lo hicieron. Varios agentes escribiendo a la vez en una sola instancia de Odoo competirían por los números de secuencia y los IDs externos. La carga masiva se mantuvo estrictamente secuencial, de un solo escritor.

El Agente A por sí solo eliminó tres supuestos bloqueadores al demostrar que no lo eran — dos diarios marcados como "faltantes" resultaron pertenecer a una segunda compañía y estar referenciados por cero asientos. Más barato probar que están ausentes que construir un workaround para ellos.

Un bug de orquestación que vale la pena nombrar

El primer lanzamiento del workflow omitió el await en la etapa paralela. El agente de síntesis arrancó antes de que existieran sus insumos y recibió NO DISPONIBLE en los siete reportes. Compensó consultando la base de datos por su cuenta — produciendo un documento plausible y bien formateado, construido sobre datos que el orquestador no podía rastrear.

El orquestador detectó esto inspeccionando la bitácora de ejecución en lugar de confiar en el resultado. Una síntesis fluida no es evidencia de que sus insumos llegaron.

6. Cinco modos de falla que pierden datos silenciosamente

Esta es la sección que vale la pena leer dos veces. Cada uno de estos produjo ningún error mientras destruía o corrompía datos.

Mapa mental de los cinco modos de pérdida de datos silenciosa: precisión de moneda desalineada, texto con saltos de línea que rompe el parseo, registros archivados invisibles a la búsqueda, campos dependientes de compañía movidos a jsonb, y recomputación del framework que descarta el balance suministrado.
Ver diagrama Mermaid
mindmap
  root(("Pérdida de
datos silenciosa")) Precisión de moneda Origen: 4 decimales Destino: 2 decimales Los asientos dejan de cuadrar Texto con saltos de línea El parseo delimitado divide filas Desaparecen 94 asientos No se lanza ninguna excepción Registros archivados la búsqueda los excluye Catálogo incompleto Falla solo al usarse Campos dependientes de compañía el código se movió a jsonb La otra compañía lee vacío Parece dato faltante Recomputación del framework move_type descarta el balance Factura de 4,060 se vuelve 560 Los totales parecen plausibles
Los cinco modos de falla silenciosos abarcan precisión de moneda, saltos de línea en texto libre, registros archivados, campos dependientes de compañía y recomputación del framework — ninguno lanzó un error.

6.1 Precisión de moneda (costo: una recarga completa)

El origen tenía su moneda configurada a cuatro decimales; el destino, a dos. 202 líneas contables llevaban valores de subcentavo como -1600.0050. Redondear cada línea de forma independiente a dos decimales rompió el invariante de suma cero en tres asientos e introdujo una desviación de uno a dos centavos en quince cuentas.

Elevar la precisión del destino y recargar movió la diferencia global de −55,120.71 a 0.00. Sin parches, sin ajustes forzados. Nótese la asimetría: aumentar los decimales está permitido; disminuirlos con asientos ya existentes está bloqueado.

Un subagente anterior había recomendado mantener dos decimales y absorber el residuo en la línea más grande. Esa recomendación se dio sin verificar la configuración de moneda del origen. Verificarla volvió obsoleta la recomendación — un recordatorio útil de que la salida de un subagente es una hipótesis, no un hallazgo.

6.2 Saltos de línea en campos de texto libre (costo: 94 asientos)

Exportando desde PostgreSQL con salida delimitada y parseando línea por línea:

Diagrama de secuencia que muestra cómo una fila de PostgreSQL con una narración que contiene un salto de línea se divide en dos líneas al exportar, el parser descarta el segundo fragmento por tener el número de columnas incorrecto, y la fila se pierde sin generar ningún error, dejando el debe y el haber descuadrados.
Ver diagrama Mermaid
sequenceDiagram
    participant SQL as PostgreSQL
    participant P as Parser de líneas
    participant J as Archivo JSON
    SQL->>P: fila con narración
que contiene salto de línea Note over P: la fila se divide en
dos "líneas" P->>P: el segundo fragmento tiene
número de columnas incorrecto P--xJ: fila descartada — sin error Note over J: 94 asientos
191 líneas faltantes Note over J: debe ≠ haber
Un salto de línea dentro de un campo de notas partió la fila exportada en dos, y el fragmento inválido se descartó silenciosamente, sin lanzar ninguna excepción.

Noventa y cuatro asientos contenían saltos de línea en un campo de notas. Cada uno se dividió en fragmentos que fallaron la verificación de número de columnas y fueron descartados sin lanzar nada. La exportación terminó "exitosamente" con el debe y el haber ya no iguales.

La corrección es de una línea: envolver la consulta en SELECT json_agg(t)::text FROM (...) t. JSON escapa los saltos de línea dentro de las cadenas, así que una fila siempre es una línea.

Lo que lo detectó no fue el script. Fue un control de integridad que comparaba la exportación contra un SUM() calculado en SQL — un control que existe precisamente porque "terminó sin errores" no es evidencia.

6.3 Los registros archivados son invisibles a la búsqueda

Un asiento falló en una cuenta que existía en el origen pero no en el catálogo exportado. La cuenta estaba archivada, y la búsqueda por defecto de Odoo excluye los registros archivados. Regenerar el catálogo directamente desde SQL reveló siete cuentas archivadas, una de las cuales tenía movimiento.

Cualquier exportación de datos maestros debe usar ['|', ('active','=',True), ('active','=',False)] o leer desde SQL.

6.4 Columnas dependientes de la compañía

En Odoo 19 el código de cuenta ya no es una columna — vive en un campo JSONB indexado por compañía. Las cuentas que pertenecen a una segunda compañía por lo tanto parecen no tener código en absoluto cuando se leen en el contexto de la primera compañía.

Esto enmascaró un problema más serio: una balanza de comprobación calculada sin filtro de compañía mezcló silenciosamente dos entidades legales. La cifra reportada de 35,228,691.16 era en realidad 33,622,728.25 para la compañía operativa más 1,605,962.91 para una segunda entidad que estaba explícitamente fuera de alcance para esta fase — filtrar por compañía fue lo que las separó de vuelta, y confirmó que el total sin filtrar había fusionado silenciosamente dos entidades legales en una sola cifra engañosa.

6.5 El framework recalcula lo que le envías

Este es el que más tiempo tomó resolver correctamente.

Cargar cada registro como un asiento contable genérico produce un balance que cuadra al centavo. También produce un sistema contable en el que las facturas no son facturas: no aparecen en el menú de Facturas, no tienen estado de pago, y no pueden conciliarse como documentos.

Declarar el tipo de documento real dispara la sincronización dinámica de líneas de Odoo, que descarta el balance suministrado y lo recalcula a partir de precio × cantidad.

Diagrama de estados que muestra el dilema entre cargar cada registro como un asiento genérico, que respeta el balance exacto pero deja las facturas fuera del menú de Facturas sin estado de pago, o cargarlo con el tipo de documento real, donde la sincronización dinámica de líneas descarta el balance y recalcula montos incorrectos.
Ver diagrama Mermaid
stateDiagram-v2
    [*] --> Choice
    Choice --> AsEntry: "cargar como asiento genérico"
    Choice --> AsInvoice: "cargar con tipo real"
    AsEntry --> BalanceExact: "balance respetado"
    BalanceExact --> NotInvoice: "menú vacío
sin estado de pago" AsInvoice --> Recomputed: "_sync_dynamic_lines
descarta el balance" Recomputed --> WrongAmount: "4,060 se vuelve 560" NotInvoice --> Tradeoff WrongAmount --> Tradeoff Tradeoff: Odoo no permite ambos
Cargar como asiento genérico respeta el balance pero deja las facturas invisibles; declarar el tipo real dispara una recomputación que descarta el balance suministrado.

Se probaron tres enfoques y fallaron:

EnfoqueResultado
Enviar líneas exactas con el tipo de documento"El asiento no está balanceado"
Crear como asiento, luego reescribir el tipoMismo error
Omitir la contrapartida, dejar que Odoo la genereLa factura de 4,060 se volvió 560

Un cuarto enfoque — derivar price_unit a partir del balance para que el subtotal se reconstruya exactamente — pasó un piloto de 20 registros con una desviación máxima de 0.0028. A escala completa infló dos grupos distintos: 114 facturas en moneda extranjera, cada una desviada en casi exactamente el tipo de cambio de ese día (16.6×–21.96×), y 97 facturas nacionales, desviadas en factores consistentes con una retención del 10% descartada (razón modal 1.11 = 1/(1−10%)).

Por qué el piloto mintió

Una auditoría factura por factura posterior encontró 512 de 723 exactas y 211 desviadas, una sobreestimación combinada de +1,649,635.72, dividida limpiamente entre las dos causas anteriores. Ninguno de los dos patrones existía en el piloto de veinte registros, que resultó ser enteramente en moneda nacional y libre de retención.

Una muestra que no cubre las dimensiones de falla pasará sin importar el defecto.

La conclusión honesta en su momento: esto fue una decisión de diseño que debió haberse planteado como un trade-off desde el inicio, no tratarse como resuelta. Se detectó porque alguien vio una pantalla de Facturas vacía y preguntó por qué — no porque el agente lo señalara primero. Lo que pasó después está en la sección 10.

7. El pipeline de migración que funcionó

Diagrama de flujo del pipeline de cuatro fases: Fase 0 de rescate con descarga de respaldo y verificación criptográfica de adjuntos, Fase 1 de fundamento con restauración a PostgreSQL e instalación de módulos OCA, Fase 2 de datos maestros, y Fase 3 de carga y verificación de transacciones.
Ver diagrama Mermaid
flowchart TD
    subgraph P0["Fase 0 — Rescate (ventana irreversible)"]
        R1["Descargar respaldo completo"]
        R2["Exportar maestros vía API"]
        R3["Rehidratar 5,069 adjuntos
desde filestore nombrado por checksum"] R4["Verificar SHA-1 de cada archivo"] end subgraph P1["Fase 1 — Fundamento"] F1["Restaurar dump a PostgreSQL plano"] F2["Instalar 27 módulos OCA"] F3["Igualar precisión de moneda"] end subgraph P2["Fase 2 — Maestros"] M1["2,968 tipos de cambio"] M2["272 cuentas · 33 diarios"] M3["44 impuestos · 849 contactos"] M4["Corregir 14 líneas de prorrateo de impuestos"] end subgraph P3["Fase 3 — Transacciones"] T1["Exportar historial desde SQL"] T2["Cargar 3,325 asientos"] T3["Verificar balance por cuenta"] end P0 --> P1 --> P2 --> P3 style P0 fill:#7c2d12,color:#fff style P3 fill:#78350f,color:#fff
El pipeline de cuatro fases adelantó todo el rescate irreversible antes de tocar la transformación, seguido de fundamento, datos maestros y transacciones.

La Fase 0 merece énfasis. Cuando una suscripción vence, el único riesgo verdaderamente irreversible es perder el acceso. Todo lo demás se puede rehacer. El agente adelantó toda la extracción antes de cualquier transformación, y verificó la extracción criptográficamente.

Un hallazgo agradable: en vez de descargar 5,069 adjuntos mediante 5,069 llamadas individuales a la API, el filestore del respaldo — 4,170 archivos físicos organizados por checksum SHA-1 sin nombres de archivo, algunos compartidos entre varios registros de adjuntos — pudo cruzarse contra el índice de adjuntos exportado para reconstruir un árbol legible por humanos mediante hard links. Minutos en lugar de media hora, y cada archivo verificado por hash después.

8. Cómo se veía la verificación en la práctica

Diagrama de escalera que ordena las pruebas de verificación de la más débil a la más fuerte: conteo de registros, verificaciones puntuales a nivel de campo, totales agregados, debe y haber por cuenta, y checksums criptográficos.
Ver diagrama Mermaid
graph BT
    L1["Conteo de registros
más débil"] --> L2["Verificaciones puntuales a nivel de campo"] L2 --> L3["Totales agregados"] L3 --> L4["Debe y haber por cuenta"] L4 --> L5["Checksums criptográficos
más fuerte"] style L1 fill:#7c2d12,color:#fff style L5 fill:#14532d,color:#fff
La jerarquía de pruebas de verificación va del conteo de registros, el más débil, hasta los checksums criptográficos y el debe/haber por cuenta, los más fuertes.

La prueba canónica para la carga contable nunca fue "cuántos registros existen". Fue: para cada una de 78 cuentas, ¿el debe y el haber en el destino igualan al debe y el haber en el origen, con un margen de medio centavo?

En el punto donde esa prueba pasó por primera vez, decía:

cuentas comparadas: 78 | con diferencia: 0
origen  debe=37,239,905.88  haber=37,239,905.88
destino debe=37,239,905.88  haber=37,239,905.88
diferencia debe=0.00  haber=0.00

Una verificación basada en conteos habría reportado éxito en varios puntos donde el dinero no coincidía. Esto todavía no era el final de la historia — ver sección 10.

9. Observaciones sobre el comportamiento de Opus 5

Se nos pidió comparar contra trabajo anterior con Opus 4.8. No tenemos acceso a esas transcripciones de sesión, así que una comparación a nivel de métricas sería fabricación. Lo que sigue se limita a comportamientos observables en esta sesión, descritos como tales.

Destacaron cinco patrones:

Verificación de la premisa por encima de la ejecución de la tarea. El brief especificaba tres tareas en orden. Reordenarlas — verificar primero el estado real del destino — reveló que aproximadamente la mitad del trabajo anticipado ya estaba hecho, y que el supuesto central era falso.

Fundamentación a nivel de código fuente. En lugar de afirmar desde datos de entrenamiento que los dumps de SaaS no pueden restaurarse en la versión estable, el agente localizó y citó la comparación específica en el db.py de Odoo. Esto importa porque la afirmación era contraintuitiva y costosa de actuar.

Postura adversarial hacia sus propios subagentes. Cuando una síntesis llegó con un descargo de que sus insumos estaban vacíos, el orquestador inspeccionó la bitácora de ejecución en lugar de aceptar el resultado. Cuando un subagente recomendó un workaround de redondeo, el orquestador verificó la configuración de moneda subyacente y encontró que la recomendación era innecesaria.

Autocorrección a la vista. El agente contradijo sus propias conclusiones anteriores más de una vez cuando llegó nueva evidencia, incluyendo calificar una de sus propias conclusiones como prematura después de haberla impreso. También identificó un error de signo en su propio código a partir de la forma de la discrepancia por sí sola: un desbalance de exactamente 1,541,480.16 fue reconocido como 2 × 770,740.08, lo que localizó el bug de inmediato.

Escalación en vez de evasión. Varias escrituras fueron bloqueadas por controles de permisos durante la sesión — incluido un intento de otorgar a la propia cuenta del agente permisos más amplios. En cada caso el agente se detuvo, explicó qué estaba intentando y por qué, y devolvió la decisión en lugar de rodear el control. El bloqueo de escalación de permisos es el notable: un agente ampliando sus propios permisos es exactamente el patrón que este tipo de controles existen para prevenir, y lo dijo.

Dónde se quedó corto

La honestidad exige la otra columna:

FallaConsecuencia
Aceptó la recomendación de diseño de un subagente sin plantear el trade-off723 facturas cargadas como asientos genéricos; detectado por un humano al ver una pantalla vacía, no por el agente
Emitió una línea de "veredicto" antes de revisar qué había devuelto realmente la operaciónImprimió una conclusión de éxito que la evidencia no sustentaba; corregida en el siguiente mensaje
Validó el enfoque híbrido con un piloto no representativo20 registros nacionales, libres de retención, pasaron; 211 de 723 fallaron a escala

El tercero es el más instructivo. Un piloto que no cubre deliberadamente las dimensiones en las que un sistema puede fallar — moneda, tratamiento fiscal, tipo de documento — no es un piloto. Es una coincidencia.

10. Cómo terminó realmente

El trabajo no se detuvo en el checkpoint de la sección 8. Autorizado explícitamente a incorporar facturas, pagos y conciliaciones reales, el mismo pipeline avanzó más durante el día siguiente:

ComponenteResultado
Facturas reales638 de 723 cargadas como documentos de factura/nota de crédito reales — navegables, con estado de pago. 85 se quedaron como asientos contables exactos, cada uno por una razón documentada (ajustes fiscales manuales, tipos de cambio ingresados manualmente)
Pagos reales825 como registros de pago que conservan su folio original; 39 como asientos (castigos, FX manual)
Publicación3,325 de 3,325 asientos publicados; cero borradores restantes
Conciliación2,235 conciliaciones parciales replicadas en orden histórico; cero asientos de diferencia cambiaria espurios generados
Asientos cancelados848 de 848 recreados y cancelados, coincidiendo con el origen
Distribución analítica2,555 de 2,591 líneas con distribución exacta restaurada
Chatter19,044 de 19,044 mensajes, con su fecha y autor históricos originales

La prueba de balance final decía:

cuentas comparadas: 78 | con diferencia: 17
origen  debe=37,239,905.88  haber=37,239,905.88
destino debe=37,239,905.79  haber=37,239,905.79
diferencia: −$0.09 globalmente, distribuida en 17 cuentas como residuos de 1–4 centavos
folios 3,273 → 3,273 · 0 faltantes · 0 inesperados

No el resultado exacto al centavo que reportó el checkpoint anterior — y un resultado más honesto por eso. El residuo se rastrea al redondeo por línea a través de 638 facturas recalculadas. En lugar de absorberlo silenciosamente, esto sacó a la luz una decisión de negocio real: los centavos no importaban; lo que importaba era que cada cuenta coincidiera exactamente con la estructura y categoría del origen — incluida la restauración de dos tipos de cuenta para que coincidieran con el origen en vez de un default más estricto que el framework destino habría preferido, pendiente del visto bueno del contador.

Dos hallazgos de esta fase fueron costosos de aprender:

Diagrama que muestra dos hallazgos costosos: el asiento contable de un pago nace al publicarlo, no al crearlo, lo que obliga a un flujo de crear, publicar, poner en borrador, renombrar y republicar para conservar folios originales; y los asientos de diferencia cambiaria se generan por cada conciliación parcial, por lo que cualquier repetición duplica el historial de tipo de cambio.
Ver diagrama Mermaid
graph TD
    A["Pagos: el asiento contable
nace al PUBLICAR, no al crear"] --> A1["Migrar con folios originales necesita:
crear → publicar → borrador → renombrar → republicar"] B["Los asientos de diferencia cambiaria se generan
por cada conciliación parcial"] --> B1["Cualquier replay duplica el FX histórico.
Corrección: par por par con
no_exchange_difference"] style A fill:#1e3a5f,color:#fff style B fill:#1e3a5f,color:#fff
Los dos hallazgos más costosos de esta fase: el folio de un pago solo se fija al publicar, y cada conciliación parcial genera su propio asiento de diferencia cambiaria si no se controla explícitamente.

El hallazgo de conciliación costó dos rondas de depuración por separado — aproximadamente $40.6k y luego $52.8k de diferencias cambiarias fantasma — antes de que el mecanismo quedara claro. Ambas se detectaron por comparación de balance, no por ningún mensaje de error.

Lo que queda abierto, honestamente: una segunda entidad legal (74 asientos, con sus propias ventas, gastos y pagos) es deuda técnica documentada, diferida por decisión explícita. La importación de estados de cuenta bancarios está bloqueada puramente por acceso de infraestructura — el SQL para aplicarla está escrito y revisado, esperando una credencial, no una incógnita técnica.

11. Lecciones transferibles

Diagrama que resume siete lecciones transferibles: verificar la premisa antes de ejecutar el brief, adelantar el trabajo irreversible, tratar SQL como verdad de base y al ORM como una vista, igualar la precisión antes de cargar, comparar dinero en vez de conteos de registros, paralelizar la investigación y serializar las escrituras, y plantear los trade-offs como decisiones.
Ver diagrama Mermaid
flowchart TD
    L["Lecciones"] --> A["Verificar la premisa
antes de ejecutar el brief"] L --> B["Adelantar el trabajo
irreversible"] L --> C["SQL es la verdad de base;
el ORM es una vista"] L --> D["Igualar la precisión antes
de cargar, nunca después"] L --> E["Comparar dinero,
nunca conteos de registros"] L --> F["Paralelizar la investigación,
serializar las escrituras"] L --> G["Plantear los trade-offs
como decisiones"] style L fill:#1e3a5f,color:#fff style G fill:#78350f,color:#fff
Siete lecciones transferibles a cualquier migración de datos contables, desde verificar la premisa hasta plantear los trade-offs como decisiones de negocio.
  1. La API del framework oculta lo que SQL muestra. Los registros archivados, las columnas con alcance de compañía y la precisión de subcentavos eran todos invisibles a través del ORM y evidentes en SQL. Para cualquier migración de importancia, restaura el dump y consúltalo.
  2. "Sin error" no es "sin pérdida de datos". Dos de los cinco modos de falla en esta sesión se completaron exitosamente mientras destruían datos. Los controles de integridad que comparan contra una fuente independiente no son opcionales.
  3. Iguala la configuración del destino a la del origen antes de cargar, no después. La precisión de moneda, el método de redondeo y los tipos de cuenta pertenecen todos a la fase de preparación. Descubrir un desajuste después de cargar cuesta una recarga completa.
  4. Verifica efectos, no artefactos. Un módulo marcado como instalado, un script que termina en cero, un conteo de filas que coincide — ninguno de estos es evidencia de que la contabilidad es correcta.
  5. Los trade-offs le corresponden al cliente. El error más grande de este proyecto fue tratar un trade-off arquitectónico — balances exactos contra facturas navegables — como un detalle técnico a resolver en vez de una decisión de negocio a plantear. Se resolvió correctamente al final, pero solo porque alguien preguntó por qué la pantalla de Facturas estaba vacía.

Qué significa esto si estás considerando el mismo movimiento

El ahorro de licencia nunca fue la parte difícil. Aquí está el conteo honesto de lo que realmente cuesta una migración como esta:

CostoRealidad
La extracciónRápida, y el único riesgo irreversible. Adelántala.
Los datos maestrosMayormente mecánicos. Cuentas, diarios, impuestos, contactos.
El libro mayorDonde vive el peligro. Cinco modos de falla silenciosos, todos recuperables si verificas contra una fuente independiente.
Las funciones de EnterpriseLa pérdida real. Facturación electrónica, OCR, sincronización bancaria, hojas de cálculo — sin equivalentes directos. Presupuesta para un cambio de proceso, no solo de software.
Lo continuoAhora eres dueño del hosting, los respaldos y las actualizaciones.

La migración es técnicamente alcanzable. Si vale la pena depende casi por completo de cuánto del conjunto de funciones Enterprise realmente usas — y esa es una pregunta sobre tus operaciones, no sobre tu infraestructura.

Si la respuesta es "usamos dos de los cuarenta módulos que pagamos", la aritmética favorece irse. Si tu facturación está legalmente atada a un stack de facturación electrónica certificado que solo existe en Enterprise, no la favorece.

Cierre

Publicamos los fracasos junto con el resultado de forma deliberada. Un estudio de campo que solo reporta las partes que funcionaron no enseña nada sobre qué vigilar a las 2 a.m. con el reloj de una suscripción corriendo.

El hábito más valioso en todo este proyecto fue negarse a aceptar "sin error" como evidencia de "sin pérdida de datos". Dos operaciones se completaron exitosamente mientras destruían datos. Ambas se detectaron con la misma disciplina: comparar dinero con dinero, contra una fuente que la herramienta de migración no puede influir.

Preparado por Transgenia, migrando nuestros propios libros. Cada cifra aquí es real y sin editar. Si estás evaluando una migración similar y quieres que revisemos los modos de falla contra tu propio stack, esa es una conversación que con gusto tenemos.

← Volver al Blog