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
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
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.
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
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.
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
Concretamente, esto significó rechazar ciertas categorías de evidencia:
| No se acepta como prueba | Sí 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.
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
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.
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
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.
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
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:
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
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.
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
Se probaron tres enfoques y fallaron:
| Enfoque | Resultado |
|---|---|
| Enviar líneas exactas con el tipo de documento | "El asiento no está balanceado" |
| Crear como asiento, luego reescribir el tipo | Mismo error |
| Omitir la contrapartida, dejar que Odoo la genere | La 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ó
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
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
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 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:
| Falla | Consecuencia |
|---|---|
| Aceptó la recomendación de diseño de un subagente sin plantear el trade-off | 723 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ón | Imprimió una conclusión de éxito que la evidencia no sustentaba; corregida en el siguiente mensaje |
| Validó el enfoque híbrido con un piloto no representativo | 20 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:
| Componente | Resultado |
|---|---|
| Facturas reales | 638 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 reales | 825 como registros de pago que conservan su folio original; 39 como asientos (castigos, FX manual) |
| Publicación | 3,325 de 3,325 asientos publicados; cero borradores restantes |
| Conciliación | 2,235 conciliaciones parciales replicadas en orden histórico; cero asientos de diferencia cambiaria espurios generados |
| Asientos cancelados | 848 de 848 recreados y cancelados, coincidiendo con el origen |
| Distribución analítica | 2,555 de 2,591 líneas con distribución exacta restaurada |
| Chatter | 19,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:
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
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
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
- 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.
- "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.
- 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.
- 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.
- 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:
| Costo | Realidad |
|---|---|
| La extracción | Rápida, y el único riesgo irreversible. Adelántala. |
| Los datos maestros | Mayormente mecánicos. Cuentas, diarios, impuestos, contactos. |
| El libro mayor | Donde vive el peligro. Cinco modos de falla silenciosos, todos recuperables si verificas contra una fuente independiente. |
| Las funciones de Enterprise | La 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 continuo | Ahora 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.