Un caso real de e-commerce, anonimizado. Una comercializadora de refacciones automotrices que opera en MercadoLibre Full pidió automatizar su reabastecimiento. Lo que empezó como un botón en Odoo terminó siendo el descubrimiento de una relación many-to-many que rompe el sentido común de casi cualquiera que vende en marketplaces con logística consolidada. El addon quedó con el cliente; el método se queda con Transgenia.
El comercio en marketplaces con logística consolidada, MercadoLibre Full en Latinoamérica, Amazon FBA en Norteamérica, tiene una superficie engañosamente simple. Al equipo comercial el problema le parece operativo: cargar cantidades a mano en la plataforma es lento y propenso a error. Al equipo técnico el problema le parece de integración: si existe una API, hay que llamarla desde Odoo y ya. Ambos diagnósticos son consecuencia del mismo cuello de botella real, pero ninguno lo describe.
El cuello vive en un supuesto que casi todo el mundo hereda sin cuestionarlo: un SKU = una publicación. Cuando ese supuesto se rompe, y en MercadoLibre Full se rompe siempre, el diseño del reabastecimiento se rompe con él, silenciosamente, en la forma de faltantes intermitentes y compras que "por alguna razón" siempre quedan cortas.
Este texto reconstruye cómo llegamos a esa conclusión durante un refinamiento ágil con un cliente al que ya no atendemos, cómo diseñamos un addon Odoo que sustituye el reabastecimiento manual por un motor determinista sin tocar la magia nativa de Odoo, y por qué el patrón resultante es replicable a cualquier marketplace con fulfillment consolidado.
1. Cuando el sentido común te miente: el bug oculto de MercadoLibre Full
La primera reunión con el cliente empezó con una petición operativa nítida: querían un botón dentro de Odoo que empujara stock a MercadoLibre. El equipo interno de la comercializadora ya había construido dos hojas de cálculo con el algoritmo de reabastecimiento; querían "trasladarlo a Odoo" y que Odoo hiciera el trabajo. Todo lo demás (modelo de datos, publicación en el marketplace, secuencia de aprobación) se daba por evidente.
No lo era. En un momento del refinamiento, ante una pantalla compartida con el archivo Excel del cliente, se produjo el intercambio que redefinió el proyecto:
"Yo estaba pensando que el Order ID de MercadoLibre era el número de publicación, y que podíamos anidar todos esos productos dentro de ese Order ID para que, al momento de generar el esqueleto, MercadoLibre actualizara el stock."
"Aquí hay que ser bien claros con los nombres, porque nos podemos confundir horrible. El Order ID en MercadoLibre es una venta, no una publicación. Lo que tú estás pensando es un Item ID: eso sí es una publicación. Y una publicación tiene un solo producto, pero un mismo producto puede tener cien publicaciones. Es una relación uno a muchos."
Este diálogo (anonimizado; en la reunión hubo nombres) condensa el hallazgo de fondo. El Order ID identifica una venta consumada; el Item ID identifica un anuncio publicado. Son entidades distintas con ciclos de vida distintos, y MercadoLibre permite que un mismo SKU aparezca en publicaciones diferentes con precios, envíos y stock independientes. Antes de esa conversación, el proyecto iba camino a modelar un Many2one entre publicación y producto y a diseñar la interfaz alrededor de esa suposición. Habría funcionado en desarrollo; habría fallado en producción a la primera compra al proveedor que llegara corta.
La lección no es "leer mejor la documentación de MercadoLibre", esa distinción está ahí, en el glosario del vendedor, hace años. La lección es más incómoda: los sesgos de información no se corrigen con documentación, se corrigen conversando. El refinamiento ágil no es un ritual para gente que hace Scrum; es donde vive la mayor parte del riesgo de un proyecto Odoo.
2. Por qué no es "un producto = una publicación"
En MercadoLibre, un producto es lo que existe en el mundo físico: una pieza, con un SKU, un costo, una ubicación de almacenamiento. Una publicación es lo que existe en la plataforma: un anuncio con título, fotos, descripción, precio, condición (nuevo, usado), tipo de envío (Full, Flex, Colecta) y stock asignado. En Odoo, el primero es product.product; el segundo es una entidad ajena que la plataforma numera con su propio consecutivo (el Item ID).
La cardinalidad real es uno a muchos desde el lado del producto: un mismo SKU puede vivir en varias publicaciones simultáneas. En un negocio de refacciones automotrices, donde el catálogo se cuenta en miles de piezas y la competencia por posición en la búsqueda es feroz, tener varias publicaciones del mismo producto no es un accidente: es estrategia comercial deliberada. Una publicación puede llevar el precio ancla; otra, la variante con envío bonificado; otra, un bundle con producto complementario; otra, la del canal MELI líder que solo acepta ciertos parámetros.
flowchart LR P((SKU unico
ej. filtro de aceite XYZ)) L1[Publicacion 1
Envio Full
Precio ancla] L2[Publicacion 2
Envio Full
MELI lider] L3[Publicacion 3
Envio Full
Bundle con complementario] L4[Publicacion 4
Colecta
Otro canal] S1[Stock Full asignado 1] S2[Stock Full asignado 2] S3[Stock Full asignado 3] S4[Stock matriz] P --> L1 P --> L2 P --> L3 P --> L4 L1 --> S1 L2 --> S2 L3 --> S3 L4 --> S4
Cada una de esas publicaciones se comporta como una sucursal virtual con su propia demanda: acumula visitas, preguntas, ventas y velocidad de rotación por separado. MercadoLibre Full agrava la asimetría: para cada publicación con envío Full, la plataforma exige un stock asignado a esa publicación específica, no al SKU general. La consecuencia práctica es que, cuando llega el momento de calcular cuánto pedir al proveedor, típicamente un fabricante en China con lead times largos, hay que agregar la demanda por SKU; pero cuando llega el momento de decidir cuánto enviar a Full, hay que desagregar por publicación.
Cualquier motor de reabastecimiento que trate estos dos cálculos como el mismo problema empieza a acumular error. Un error pequeño, difícil de rastrear, que se manifiesta como el faltante intermitente que la operación diaria "resuelve" pidiendo un urgente al proveedor cada tanto, el síntoma clásico de un supuesto de modelo mal calibrado.
3. Refinamiento ágil: la conversación que descubrió el bug
En la comunidad de desarrollo de software, el refinamiento es la práctica de desagregar requerimientos en unidades ejecutables a través de conversaciones cortas y sucesivas, cada una centrada en un artefacto concreto que ambas partes pueden ver al mismo tiempo. En proyectos Odoo se usa poco. La suposición implícita es que Odoo, al ser un producto configurable, elimina la necesidad de refinamiento: basta con "recibir el requerimiento del cliente" y traducirlo a Studio, server actions o un módulo custom. Es un supuesto caro.
La reunión que descubrió el many-to-many entre SKU y publicación no fue una junta de discovery de dos horas. Fue un sprint corto, una hora, agenda mínima, un archivo Excel proyectado en pantalla, en el que se preguntó qué hacía cada columna, cada fórmula, cada pestaña. En una de esas preguntas, la persona operativa del cliente que sí conocía el archivo lo dijo con toda naturalidad:
"Es bueno tener esas conversaciones contigo o con quien él señale, porque esos detalles él no se sienta a explicarlos. Él nada más te avienta las cosas y hazlo."
El sponsor había entregado el Excel dando por hecho que el algoritmo estaba autocontenido y que un desarrollador podría trasladarlo. La persona operativa sabía que no, pero nadie le había preguntado antes. El refinamiento sirvió aquí menos para "recolectar requisitos" y más para descubrir que el modelo mental de una parte no coincidía con el de la otra, y que ambos modelos coexistían en el proyecto sin haber sido explicitados.
El hallazgo del many-to-many llegó en el minuto veinte de esa reunión. No estaba escrito en ningún brief anterior. En un proceso lineal habría llegado tarde y caro, probablemente en la ronda de pruebas de aceptación, cuando ya hubiera código, migraciones, capacitación y expectativas alineadas al modelo equivocado. En cambio llegó como una conversación de veinte minutos con dos personas mirando la misma hoja de cálculo.
La transferibilidad de esta lección al resto del proyecto se ve mejor a través de una regla operativa: en implementaciones Odoo, "seguir el algoritmo del archivo Excel" no basta. Hay que traducir el modelo mental antes de traducirlo a Python. El refinamiento no es un accesorio metodológico; es donde vive el riesgo.
4. Anatomía del reabastecimiento: clasificación ABCD por venta acumulada
Una vez resuelto el problema del modelo (SKU y publicación son entidades distintas con cardinalidad uno-a-muchos), viene el problema del cálculo. Con miles de piezas en catálogo, no todas merecen el mismo nivel de atención: unas venden todo el día, otras casi nunca, y unas terceras están ahí como parte del catálogo pero llevan meses sin salir. Tratar a todas por igual es el error que produce dos síntomas simultáneos, capital atrapado en producto muerto y faltantes en el producto vivo.
La clasificación ABC es una herramienta clásica de logística; su aportación es simple pero contraintuitiva para quien nunca la ha usado: se clasifica por valor de venta acumulado, no por unidades vendidas. Un producto que vende cinco piezas al mes a cinco mil pesos aporta más al negocio que uno que vende cien piezas al mes a cincuenta. La primera regla del reabastecimiento serio en marketplaces es medir en pesos, no en piezas.
En este proyecto añadimos la letra D. La familia A concentra aproximadamente el 70 % del valor de ventas acumulado, la B el 20 % siguiente y la C el 10 % restante. La D es la excepción operativa: productos que no vendieron en el periodo evaluado. La distinción importa porque un reporte de ventas puede tener ruido, piezas sin salida que aparecen por errores de captura o por movimientos internos, y esos productos deben quedarse fuera del cálculo, no acabar en la familia C con un mínimo positivo que fuerce una compra injustificada.
pie showData title Distribucion del VALOR de ventas por familia "Familia A (70% del valor)" : 70 "Familia B (20% del valor)" : 20 "Familia C (10% del valor)" : 10
El recálculo se corre una vez al mes con un cron dedicado. Los productos migran naturalmente entre familias con el tiempo, y esa migración es señal de negocio: una pieza que pasa de B a A merece revisión del gerente porque probablemente está aguantando el trimestre; una pieza que pasa de A a D es una alerta temprana de descontinuación. Técnicamente el campo family_id vive en product.template, no en product.product, decisión importante que discutimos en la sección de errores de diseño: la venta se agrupa por producto principal, no por variante.
El propio cron mensual sigue un pipeline reducido y auditable: leer el reporte de ventas del periodo, agrupar por producto principal, calcular el valor total por SKU, ordenar de mayor a menor, acumular hasta los cortes de 70 %, 90 % y 100 %, asignar familia y persistir en product.template.family_id. Los productos que aparecen en el catálogo pero no en el reporte de ventas quedan en la familia D. Esto último no es un detalle menor: es lo que evita que basura del catálogo termine consumiendo capital de compra.
flowchart LR A([Cron mensual]) --> B[Leer sale.report
ultimos N meses] B --> C[Agrupar por
product.template] C --> D[Sumar valor de venta
qty x precio_unit] D --> E[Ordenar
de mayor a menor] E --> F{Corte
acumulado} F -->|hasta 70%| A2[Familia A] F -->|71% - 90%| B2[Familia B] F -->|91% - 100%| C2[Familia C] F -->|sin venta| D2[Familia D] A2 --> Z[(product.template.family_id)] B2 --> Z C2 --> Z D2 --> Z
Con la clasificación al día, el sistema tiene lo que necesita para poner las palancas donde ganan: apretar mínimos en la C, generar holgura en la A, mantener a la D marcada como fuera del cálculo de reabastecimiento hasta que vuelva a mover. Los productos migran naturalmente entre familias con el tiempo, y esa migración es señal de negocio: una pieza que pasa de B a A merece revisión del gerente porque probablemente está aguantando el trimestre; una pieza que pasa de A a D es una alerta temprana de descontinuación. Técnicamente el campo family_id vive en product.template, no en product.product, decisión importante que discutimos en la sección de errores de diseño: la venta se agrupa por producto principal, no por variante.
5. Demanda Diaria Ponderada: por qué diciembre y enero pesan más
Clasificar los productos en familias no basta; hay que estimar cuánto se venderá de cada uno en el periodo próximo, y hacerlo con una precisión razonable dado el material disponible: doce a veinticuatro meses de ventas históricas por publicación y por SKU. La Demanda Diaria Ponderada (DDP) es el motor que traduce ese histórico a un número operativo. La idea es sencilla: tomar el promedio diario de ventas de cada mes histórico, multiplicarlo por un peso configurable, sumar y normalizar. La fórmula conceptual queda así:
DDP = Σ (ventas_del_mes × peso_del_mes) / Σ (peso_del_mes × días_del_mes)
La palabra clave es peso configurable. En un negocio automotriz mexicano, diciembre y enero no son meses cualesquiera: diciembre concentra compra impulsada por aguinaldo y regalos, enero recibe el rebote del "arreglo pendiente" que la gente pospuso todo el año. Un promedio simple sin ponderar sepulta esa estacionalidad y produce un mínimo demasiado bajo para la temporada alta y un máximo demasiado alto para la temporada baja. Los pesos son parámetros del sistema, no constantes hardcodeadas; el gerente los ajusta cuando el negocio cambia, una campaña anticipada, un problema en la aduana, una migración de canal.
La DDP se combina con dos parámetros más de la familia del producto para producir el par mínimo/máximo: días de stock mínimo y días de stock máximo. Multiplicando la DDP por esos días se obtienen las cantidades en unidades. Un producto de familia A típicamente lleva un mínimo mayor, se acepta capital inmovilizado a cambio de no romper stock en el bestseller; un producto de familia C lleva mínimos apretados para no cargar el inventario con piezas que giran lento. La configuración de días por familia se define una vez y rige para todo el catálogo; la personalización adicional por producto individual está intencionalmente ausente porque abriría la puerta a criterios inconsistentes.
flowchart LR H[(Historico de ventas
por mes)] --> V1[Ventas ene] H --> V2[Ventas feb] H --> V3[...] H --> V12[Ventas dic] P[(Pesos configurables
por mes)] --> M[Ponderacion
ventas_mes x peso_mes] V1 --> M V2 --> M V3 --> M V12 --> M M --> S[Suma ponderada] P --> N[Normalizacion
sum peso x dias] S --> DDP[[Demanda Diaria
Ponderada]] N --> DDP F[(Familia ABCD
dias min y max)] --> R[Min = DDP x dias_min
Max = DDP x dias_max] DDP --> R R --> O[(stock.warehouse.orderpoint
product_min_qty product_max_qty)]
6. Los dos algoritmos gemelos y por qué están vinculados
De la relación many-to-many entre SKU y publicación se desprende una consecuencia arquitectónica inevitable: hacen falta dos cálculos, no uno. El primero opera por publicación y sirve para decidir cuánto stock enviar a MercadoLibre Full. El segundo opera por SKU y sirve para decidir cuánto pedir al proveedor. Un motor honesto los ejecuta en ese orden, no al revés.
El algoritmo por publicación toma la demanda histórica de cada publicación activa, la pasa por la DDP y multiplica por los días de stock mínimo y máximo de la familia. Devuelve un par de números por publicación. Como una publicación no admite fracciones, no se envían 3.8 piezas a Full, se envían 4, el algoritmo redondea hacia arriba en cada línea. Ese redondeo es correcto operativamente: mejor tener una pieza extra en cada publicación que perder una venta por decimales. Pero introduce una desviación silenciosa cuando se agrega hacia arriba.
El algoritmo por SKU toma la suma de los mínimos por publicación como cota inferior del inventario disponible que hay que respaldar, más la demanda estimada de canales no-Full (venta en Colecta, en tienda física, en otros marketplaces), y calcula la cantidad a pedir al proveedor. Si esa segunda parte del cálculo ignora los redondeos hacia arriba de la primera, el pedido al proveedor queda sistemáticamente corto en la cantidad exacta que la operación necesita para respaldar los envíos a Full. Es un error que en la operación diaria se manifiesta como el faltante estacional que nadie sabe explicar y que "se resuelve" con un urgente al proveedor cada dos meses. La cita del refinamiento lo dijo con claridad:
"Esos que se van sumando extra por el redondeo, al final de cuentas pues los tienes que comprar. Tienen que salir de tu primer resurtido. De China."
La consecuencia arquitectónica es que los dos algoritmos no son componentes independientes: son un pipeline. El estado intermedio del primero, los redondeos acumulados, alimenta al segundo. En el addon se implementaron como dos métodos de un mismo motor con orden garantizado; el intento inicial de separarlos como microservicios independientes se abandonó en el refinamiento por razones no de infraestructura sino de correctitud.
flowchart TB
subgraph A1[Algoritmo A - por publicacion Full]
P1[Demanda por publicacion] --> D1[DDP publicacion]
D1 --> F1[Familia dias min/max]
F1 --> Q1[Cantidades por publicacion]
Q1 --> R1[Redondeo hacia arriba
por linea]
R1 --> E1[[Envio a MercadoLibre Full
por publicacion]]
end
subgraph A2[Algoritmo B - por SKU al proveedor]
S1[Suma minimos por publicacion] --> C1[Cota inferior por SKU]
N1[Demanda canales no-Full] --> C2[Demanda agregada por SKU]
C1 --> C2
C2 --> R2[Cantidad a pedir al proveedor]
end
R1 -. redondeos acumulados
que hay que reponer .-> S1
R2 --> PO[[Purchase Order borrador
via Odoo nativo]]
7. La decisión arquitectónica clave: potenciar el planificador nativo de Odoo
Con los dos algoritmos definidos, quedaba la pregunta de fondo: ¿qué hace el sistema con los números que produce? La respuesta ingenua, y la que casi cualquier equipo elige por defecto, es construir un motor de compras nuevo dentro del addon: crear directamente las órdenes de compra, generar las transferencias internas, gestionar los estados. Esa decisión duplica funcionalidad que Odoo ya tiene resuelta, crea dos fuentes de verdad para el inventario, y convierte cada futura migración de versión en un problema de arqueología.
La decisión que tomamos fue la opuesta y es el corazón de la solución: el addon no compra, no transfiere, no ordena; escribe niveles de reabastecimiento en los puntos de pedido nativos de Odoo (stock.warehouse.orderpoint) y deja que el planificador de aprovisionamiento haga su trabajo. El planificador nativo se ejecuta como cron independiente, lee los orderpoints, revisa el stock virtual, lo que hay en almacén más lo que va llegando menos lo comprometido, y cuando detecta que el stock virtual cae por debajo del mínimo genera automáticamente el borrador de la orden de compra o la transferencia interna, según lo indiquen las rutas y reglas de abastecimiento configuradas para ese producto y ese almacén.
Este enfoque tiene tres ganancias tangibles. La primera es correctitud: la lógica de Odoo para compras y transferencias es la que la comunidad y Odoo S.A. han pulido durante años; no la vamos a superar con código nuevo escrito en semanas. La segunda es longevidad: cuando llegue Odoo v20 o v21, el addon seguirá funcionando siempre y cuando stock.warehouse.orderpoint siga existiendo, y ese modelo es una piedra angular del ERP, no un artefacto marginal. La tercera es integración con la operación existente: los orderpoints aparecen en la UI estándar de Odoo, el gerente los revisa desde el módulo de Inventario que ya usa, y las órdenes de compra generadas siguen el flujo de aprobación de compras que la empresa ya conoce.
Vale la pena decirlo en negativo: el addon no es un motor paralelo de MRP, no es una integración con MercadoLibre, esa fase, si el negocio la necesita, es separable, y no es un botón para "empujar stock a MercadoLibre". Es un calculador de niveles que alimenta un socket estándar. Todo lo demás lo hace Odoo. Como lo dijo la persona operativa del cliente en el refinamiento, después de que fuimos por el camino largo primero,:
"Yo te recomiendo que no te compliques en generar algo para mandar. No necesitamos que lo tenga MercadoLibre. Lo que necesitas es mostrarle al usuario qué tiene que mandar. Lo más importante es el cálculo, no la conexión."
8. stock.warehouse.orderpoint como protocolo estándar
Vale la pena detenerse a mirar por qué el punto de pedido nativo de Odoo funciona como un contrato tan estable. El modelo stock.warehouse.orderpoint define, para cada combinación de producto y ubicación, cuatro campos operativos: cantidad mínima, cantidad máxima, múltiplo de compra y días de espera. Nada más. No decide cuándo comprar; no decide a qué proveedor; no genera órdenes. Solo declara un estado deseado. Toda la maquinaria de aprovisionamiento, el _procurement_cron, las rutas, las reglas de compra, la fabricación bajo pedido, gira alrededor de ese estado declarado y actúa en consecuencia.
La consecuencia práctica es que stock.warehouse.orderpoint se comporta como un socket estándar: cualquier fuente puede escribir en él y el planificador lo tratará idénticamente. Escribir un orderpoint desde un addon custom, desde la UI manual del gerente, desde un wizard de importación masiva o desde un módulo OCA de reabastecimiento comunitario produce exactamente el mismo comportamiento aguas abajo. El sistema no sabe, ni le importa, quién puso el mínimo en cincuenta; solo sabe que ahora hay que reponer cuando el stock virtual baje de esa marca.
flowchart TB A[Addon custom de reabastecimiento] B[Modulo OCA de reabastecimiento] C[UI manual del gerente] D[Wizard de importacion masiva] E[(stock.warehouse.orderpoint)] F([Planificador nativo Odoo]) G[Purchase Order borrador] H[Transferencia interna borrador] A --> E B --> E C --> E D --> E E --> F F --> G F --> H
Diseñar contra este socket cambia la naturaleza del problema. En lugar de preguntarse "¿cómo construyo un motor de compras que respete las reglas fiscales, los términos con proveedores, las rutas multialmacén y los canales de aprobación?", uno se pregunta "¿cómo produzco números correctos para escribir en product_min_qty y product_max_qty?". La primera pregunta abre un proyecto de años; la segunda es tratable en semanas. El resto del sistema, correcto, auditado, integrado, ya vino en Odoo. Reconocer esa asimetría entre lo que hay que construir y lo que hay que reutilizar es, en la práctica, la habilidad más económica de un buen equipo de implementación.
9. Flujo end-to-end
Con las piezas conceptuales en su sitio, cardinalidad correcta, clasificación ABCD, Demanda Diaria Ponderada, dos algoritmos gemelos vinculados, orderpoint como socket estándar, el sistema completo se lee como una coreografía en cinco actos, tres de ellos automatizados y dos con intervención humana.
Acto uno, mensual. Un cron mensual recalcula las familias del catálogo. Lee el reporte de ventas del periodo, agrupa por producto principal, calcula el valor acumulado, ordena de mayor a menor y asigna la letra A a los productos que representan el 70 % del valor, B al siguiente 20 %, C al 10 % y D a los que no vendieron. El campo family_id en product.template queda actualizado.
Acto dos, diario. Un cron diario ejecuta los dos algoritmos gemelos. Primero calcula los mínimos y máximos por publicación con la DDP y los días de stock de la familia, redondeando hacia arriba. Luego agrega esos resultados por SKU y agrega la demanda de canales no-Full para producir los mínimos y máximos por SKU en el almacén matriz. Cada línea de cálculo se materializa como un registro en replenishment.suggestion en estado draft.
Acto tres, humano. El gerente abre la vista de sugerencias. Filtra por familia, por almacén o por SKU. Compara los valores actuales contra los sugeridos, la vista los muestra lado a lado, y decide: aprobar en bloque, aprobar selectivamente o descartar. El sistema no obliga a aceptar todo; el criterio del gerente es explícito y auditable.
Acto cuatro, automático. Al aprobar, el registro replenishment.suggestion pasa a done y escribe los valores en el stock.warehouse.orderpoint correspondiente. Si el orderpoint no existía, lo crea. Aquí termina el trabajo del addon.
Acto cinco, nativo. El _procurement_cron de Odoo se ejecuta como siempre, encuentra los orderpoints actualizados, calcula el stock virtual y genera los borradores de órdenes de compra o transferencias internas según las rutas configuradas para cada producto. El equipo de compras confirma esas RFQ desde el módulo que ya usa. El equipo de almacén valida las transferencias desde el módulo de Inventario. Nada nuevo que aprender.
flowchart LR A([Cron mensual]) --> B[Recalcular familias ABCD sobre sale.report] B --> C[(product.template.family_id)] D([Cron diario]) --> E[Algoritmo por publicacion con DDP] E --> F[Algoritmo por SKU + canales no-Full] F --> G[(replenishment.suggestion state=draft)] G --> H[Gerente revisa y aprueba] H --> I[(stock.warehouse.orderpoint)] I --> J([_procurement_cron nativo Odoo]) J --> K[Purchase Order borrador] J --> L[Transferencia interna borrador]
10. Historial auditable: replenishment.suggestion y el flujo de aprobación
Que cada ejecución del algoritmo genere registros nuevos en lugar de sobrescribir los anteriores es una decisión con consecuencias que valen la pena explicitar. El modelo replenishment.suggestion es transaccional: cada línea captura un momento, qué producto, qué ubicación, qué mínimo y máximo actuales, qué mínimo y máximo sugeridos, qué demanda calculada, qué familia, qué usuario aprobó y cuándo. Nada se pierde. Un cambio incómodo el próximo trimestre puede rastrearse al día exacto en que se aprobó, con la firma electrónica del gerente que lo autorizó.
Los estados son cuatro y su semántica es rígida. Al ejecutarse el cron diario, cada sugerencia nace en draft. El gerente la mueve a approved cuando decide aplicarla; el sistema la mueve automáticamente a done cuando termina de escribir el orderpoint correspondiente. Alternativamente, el gerente puede llevarla directo a rejected, con opción a dejar una nota que explique por qué. Ese registro rechazado también se conserva, una sugerencia descartada suele ser tan informativa como una aceptada, porque señala dónde el algoritmo pierde contra el criterio humano y qué señales le faltan.
stateDiagram-v2 [*] --> draft : cron diario propone draft --> approved : gerente aprueba draft --> rejected : gerente descarta con nota approved --> done : escribe stock.warehouse.orderpoint done --> [*] rejected --> [*]
Este historial cumple dos funciones que suelen pedirse tarde en la vida de un ERP. La primera es de cumplimiento: en negocios con auditoría anual, fiscal, de calidad ISO, de proveedores exigentes, el rastro de quién decidió qué y cuándo es requisito, no adorno. La segunda es de mejora continua: acumular meses de sugerencias contra aprobaciones permite calcular, con datos, en qué categorías el algoritmo acierta y en cuáles el gerente lo corrige sistemáticamente. Esa señal es la materia prima para ajustar los pesos de la DDP o los días de stock por familia con evidencia, no con corazonadas.
11. Qué cambió al migrar a Odoo v19
El addon se construyó originalmente sobre Odoo v18 y, meses después, acompañó al cliente en su migración a Odoo v19. Es un buen laboratorio para revisar qué tanto sobrevive un diseño cuando cambia la versión del ERP debajo. La respuesta corta: la parte del addon que respeta el socket estándar migra sin dolor; la parte que se acopla a modelos internos siempre se ve obligada a revisarse.
Lo que sobrevivió intacto fue toda la lógica de cálculo. Los dos algoritmos gemelos, la Demanda Diaria Ponderada, la clasificación ABCD, el modelo replenishment.suggestion y los cron que orquestan el pipeline pasaron a v19 sin ajustes de fondo. La razón es simple: leen sale.report (modelo estable desde v14), escriben stock.warehouse.orderpoint (contrato central del ERP) y no dependen de detalles internos del planificador de aprovisionamiento. La superficie de contacto con Odoo estaba, por diseño, en las orillas.
Lo que sí requirió revisión fueron dos puntos previsibles. Uno, las vistas XML de los wizards y del formulario de sugerencias: v19 evolucionó atributos en las vistas y algunos widgets se comportan distinto; hubo que ajustar declaraciones, no reescribir. Dos, un método de bajo nivel en la extensión de product.template que llamaba a un helper interno de Odoo cuya firma cambió; se sustituyó por la API pública equivalente y el resto siguió igual. Nada estructural.
La ganancia colateral fue Odoo Spreadsheet, que en v19 madura lo suficiente para servir como capa de análisis sobre replenishment.suggestion sin necesidad de exportar a Excel. El gerente puede armar tablas dinámicas contra el histórico de sugerencias, cruzarlas con familias y visualizarlas, todo dentro del ERP.
Vale una nota de contexto: al cierre de esta redacción, el engagement con el cliente ya está terminado. El addon quedó instalado en su instancia y ellos tomaron propiedad del código y de su operación; Transgenia ya no lo mantiene ni lo soporta. Este texto documenta el diseño y el método, no un servicio activo. La lección que sí queda es que un addon diseñado contra los contratos estables de Odoo, y no contra sus interiores, se traslada limpiamente entre versiones aun sin quienes lo escribieron originalmente.
12. Qué de esto es replicable a Amazon FBA, Shopify, Prestashop
Aunque el caso vivió en MercadoLibre Full, ninguna pieza esencial del diseño depende de MercadoLibre. La relación uno a muchos entre SKU y publicación existe en cualquier marketplace que permita listar un mismo producto en varios anuncios; el redondeo hacia arriba en logística consolidada existe en cualquier fulfillment tipo FBA. La arquitectura, entonces, es transferible con ajustes menores.
Amazon FBA es el caso hermano más obvio. La equivalencia es casi punto a punto: el ASIN cumple la función de la publicación (contenedor de venta con demanda propia), el SKU sigue siendo el producto físico, el inventario asignado a FBA es análogo al stock Full, la relación entre ASINs y SKUs también es de uno a muchos. Los dos algoritmos gemelos funcionan idénticos; solo cambian los orígenes de la demanda histórica (Amazon Sales Reports en lugar de MercadoLibre) y el vocabulario. Un módulo custom de Odoo diseñado sobre este patrón puede servir a ambos marketplaces conmutando el conector de origen sin tocar la lógica.
Shopify y Prestashop son casos ligeramente distintos porque su modelo base no es consolidado: la tienda es tu propio almacén salvo que uses un servicio 3PL. Ahí el algoritmo por publicación se vuelve trivial (una tienda = un canal, no hay múltiples publicaciones del mismo SKU), pero la parte del algoritmo por SKU sigue vigente y gana valor si operas simultáneamente en Shopify más varios marketplaces (Amazon, MercadoLibre, eBay). El patrón deja de ser "resolver el problema del many-to-many" y pasa a ser "orquestar reabastecimiento multicanal contra un solo inventario matriz". La estructura del addon no cambia; cambia qué tan poblada queda la tabla de publicaciones.
Lo que no es transferible sin trabajo real es la parametrización de la Demanda Diaria Ponderada. Los pesos mensuales son un artefacto cultural del negocio: aguinaldos, buen fin, temporadas de reparación automotriz. Trasladar el mismo peso vector a un e-commerce de ropa deportiva o a distribución B2B produciría estimaciones malas. La regla operativa es: la fórmula viaja, los pesos no. El primer entregable de una réplica siempre debería ser calibrar los pesos con al menos doce meses de historial en el nuevo contexto.
13. Errores de diseño que corregimos en el camino
El addon salió al mundo después de tres rondas explícitas de refinamiento sobre el código, no sobre requerimientos. Cada ronda encontró errores de diseño de esos que no producen bugs visibles pero que envejecen mal. Dejarlos documentados sirve como caja de herramientas para quien enfrente un proyecto parecido.
Primer error: family_id en product.product en vez de product.template. La primera versión asignaba la familia ABCD a la variante, no al producto principal. En un catálogo con variantes por talla o color, esto significaba que la misma pieza podía terminar clasificada como A en color rojo y como C en color azul, con niveles de reabastecimiento inconsistentes. La corrección fue mover el campo al product.template y, dentro del método de recálculo, escalar desde la variante al padre antes de escribir. La regla transferible: al integrar con sale.report (que agrupa por product_id), no confundas variante con producto principal.
Segundo error: el cron con dueño semántico equivocado. El cron mensual de recálculo de familias colgaba originalmente del modelo product.family porque ahí vivía el método. Funcionaba, pero era confuso: el cron modificaba product.template, no product.family. Cuando alguien nuevo entraba al código, pasaba minutos entendiendo por qué el cron estaba donde estaba. La corrección fue mover el cron para que colgara de product.template (el modelo modificado) y que este llamara internamente al método de product.family. Cero cambio funcional; ganancia grande en legibilidad para el siguiente desarrollador.
Tercer error: order_id como nombre de campo en marketplace.publication. Al modelar la publicación de MercadoLibre, la primera propuesta llamaba order_id al campo que guarda el identificador de la publicación. El problema es que order_id en Odoo colisiona semánticamente con sale.order.id, lo cual invita a confusiones y a joins accidentales durante consultas SQL. La corrección fue renombrar el campo a publication_key: técnico, unívoco, sin posibilidad de choque. Regla: cuando integres con una plataforma externa, respeta el vocabulario de Odoo en tus modelos y prefija con <canal>_ los identificadores foráneos.
Cuarto error: location_id opcional en marketplace.publication. El campo que enlaza la publicación a una ubicación de stock estaba modelado como opcional en la primera versión. En la práctica siempre debía estar poblado porque el algoritmo por publicación necesita saber dónde mide el stock disponible. La corrección fue marcarlo obligatorio a nivel de modelo (required=True) y añadir una validación previa que rechaza publicaciones sin ubicación antes de que lleguen al cron diario. Regla: si un campo es obligatorio en el algoritmo, hazlo obligatorio en el modelo; no dejes la validación como responsabilidad del código de aplicación.
Ninguno de estos cuatro errores producía fallos visibles en las pruebas manuales iniciales. Los cuatro habrían aparecido como problemas caros seis o doce meses después, cuando el catálogo hubiera crecido, otro desarrollador hubiera entrado al código y la operación empezara a apoyarse en el sistema para decisiones grandes. Encontrarlos temprano, en el refinamiento y no en producción, es la única razón por la que hoy este texto describe un diseño limpio en vez de contar la crónica de una migración forzosa.
14. Cierre: refinamiento ágil como método operativo para implementaciones Odoo
Si algo hay que llevarse de este caso, es que las conversaciones cortas y bien enmarcadas producen valor asimétrico. La reunión que descubrió el many-to-many duró menos que la reunión promedio de estatus. La ronda de refinamiento que evitó cuatro errores estructurales cabía en una tarde. El costo hipotético de no haberlas hecho se paga en meses y en presupuesto que ya no se puede recuperar. En una implementación Odoo, el momento más rentable del proyecto no es el momento en que se escribe el código: es el momento en que dos personas miran la misma hoja de cálculo y se preguntan qué hace cada columna.
Refinamiento ágil no es una etiqueta metodológica ni un ritual heredado de otras industrias. Es la mecánica más económica que se conoce para reducir la varianza de un proyecto de software antes de que la varianza se materialice en código. En proyectos Odoo se usa poco porque la promesa implícita del ERP configurable ("solo hay que capturar el requerimiento") genera la ilusión de que el trabajo cognitivo pesado ya está resuelto. No lo está. El diseño del reabastecimiento que este texto describe existe en la forma en que existe porque hubo alguien preguntando "por qué" cuando todos los demás daban las respuestas por hechas.
Si te encuentras diseñando el reabastecimiento para un negocio con presencia en marketplaces, tres decisiones te ahorran la mayoría del dolor: (1) modela SKU y publicación como entidades distintas con cardinalidad uno a muchos desde el día cero; (2) escribe niveles en stock.warehouse.orderpoint, no cantidades a comprar; (3) ejecuta el algoritmo por publicación primero y agrega hacia SKU después, respetando los redondeos acumulados. Con esas tres reglas y un método de refinamiento honesto, la mayor parte del proyecto se convierte en trabajo tranquilo.
Si quieres ver cómo Transgenia opera este tipo de proyectos en la práctica, entra a la demo gated (correo corporativo, acceso de quince días, cero compromiso). Si operas una comercializadora B2B y quieres explorar cómo aplicaría un enfoque análogo a tu inventario, la pillar de Sectores › Comercializadoras resume la propuesta.
Cierro con una nota que este texto ha repetido a propósito y que sigue siendo verdad: el addon quedó con el cliente. El método se queda con Transgenia, y con quien haya llegado hasta acá.
Preguntas frecuentes
¿Qué hace que un mismo SKU tenga varias publicaciones en MercadoLibre Full?
MercadoLibre no impone una relación uno a uno entre producto físico y anuncio publicado. Un mismo SKU puede vivir en publicaciones distintas por razones comerciales: una con precio ancla, otra con envío bonificado, otra en el canal MELI líder, otra como bundle con un producto complementario. Cada publicación acumula visitas, preguntas y ventas por separado, y en Full requiere stock asignado propio.
La consecuencia operativa es que el cálculo de reabastecimiento no puede tratar al SKU como una unidad monolítica. Hay dos cálculos distintos que hacer: uno por publicación (cuánto enviar a Full a cada una) y uno por SKU (cuánto pedir al proveedor considerando todas las publicaciones y los canales no-Full).
¿Por qué ABCD y no solo el ABC clásico?
El ABC clásico clasifica los productos vivos del catálogo en tres grupos por valor de venta acumulado (70 %, 20 %, 10 %). El problema es que en un reporte de ventas real siempre aparece ruido: productos con salida nula que llegan por errores de captura o por movimientos internos que no son ventas de mercado.
Añadir la letra D permite marcar explícitamente los productos sin venta durante el periodo evaluado y sacarlos del cálculo de reabastecimiento. Si no lo haces, esos productos acaban en la familia C con un mínimo positivo y fuerzan compras injustificadas. La D es una etiqueta de higiene, no una categoría de gestión.
¿La Demanda Diaria Ponderada es un método estadístico probado?
La DDP no pretende ser una técnica académica novedosa. Es un promedio móvil ponderado con pesos configurables aplicado al histórico de ventas mensual. Su valor no está en la sofisticación matemática, sino en que expone los pesos como parámetros del sistema y no como constantes ocultas dentro del código.
Esa apertura convierte al gerente en operador del modelo: cuando cambia la estacionalidad, cuando hay un evento comercial anticipado o cuando se detecta un problema en la aduana, ajusta los pesos y el sistema responde. Métodos más elaborados (suavizamiento exponencial, ARIMA, redes neuronales) son válidos, pero mueven la conversación del negocio a la ingeniería y suelen tardar más en pagar su costo de implementación.
¿Por qué un addon custom en vez de solo configurar Odoo nativo?
Odoo tiene módulos comunitarios OCA para reabastecimiento con reglas de MIN/MAX estáticos y algunos con reglas ligeramente dinámicas. Los revisamos antes de escribir código propio. No encajaban por tres razones. Primera: ninguno modela la relación many-to-many entre SKU y publicación de marketplace; suponen que un punto de pedido es siempre por producto y por almacén. Segunda: los pesos mensuales configurables por el gerente son una necesidad del negocio automotriz que no aparece en la mayoría de los OCA. Tercera: la cadena de aprobación con replenishment.suggestion como paso intermedio no existe en los módulos revisados.
El addon custom no reinventa la rueda del planificador ni de las órdenes de compra. Aporta exactamente lo que faltaba: clasificación ABCD, dos algoritmos gemelos vinculados y sugerencias auditables antes de escribir orderpoints. Todo lo demás lo hace Odoo estándar.
¿Funciona el mismo enfoque para Amazon FBA o Shopify?
Sí, con ajustes de conector y de vocabulario, no de arquitectura. El patrón (many-to-many entre SKU y contenedor de venta, dos algoritmos gemelos vinculados por redondeos, escritura en stock.warehouse.orderpoint) es el mismo. Amazon FBA es el caso más directo por su parecido operativo con MercadoLibre Full: ASIN cumple el papel de publicación, el inventario en FBA es análogo al stock Full.
En Shopify o Prestashop, salvo que uses un 3PL, el algoritmo por publicación se simplifica porque la tienda es tu propio almacén. Pero si operas simultáneamente en Shopify y en un marketplace, el patrón multicanal recupera todo su valor. Lo único que no se traslada sin trabajo son los pesos mensuales de la DDP: los patrones estacionales son culturales del negocio y hay que calibrarlos con al menos doce meses de historial en el nuevo contexto.