Ir al contenido

⚡ El Imperio de las Facturas Impagas: Cómo Convertir tu Módulo de Cobros en la Flota Estelar más Efectiva de la Galaxia

📌 Contexto
2 de julio de 2026 por
BloVox

⚡ El Imperio de las Facturas Impagas: Cómo Convertir tu Módulo de Cobros en la Flota Estelar más Efectiva de la Galaxia

"Ayudame, Obi-Wan Kenobi. Sos mi única esperanza." — Princesa Leia, grabando un mensaje de cobro vencido

🌌 El Problema Galáctico

Imaginá esta escena: estás en la Estrella de la Muerte de tu empresa. Tenés un cliente importante —digamos, el Planeta Alderaan Corp.— con veinte contactos, quince departamentos y una factura vencida que no paga nadie porque... el correo de seguimiento se lo mandaste a la persona equivocada.

No a cualquiera. A la persona que renunció hace tres meses y se fue a la Alianza Rebelde. Su buzón rebota. La factura se pierde en el hiperespacio. Y vos, desde tu puente de mando, ves cómo el indicador de "Días de Atraso" parpadea en rojo, como las alertas de la Estrella de la Muerte antes de que llegue un X-Wing a estropear tu día.

¿Te suena? Bienvenido al club. Dejame contarte cómo salir de este agujero de gusano sin tener que escribir ni una línea de código Python (bueno, casi).

👋 ¿Para Quién es Esto?

Para consultores funcionales, administradores de Odoo y líderes de implementación que:

  • Están hartos de que las facturas vencidas no lleguen a quien corresponde
  • No quieren meter un desarrollo custom para algo tan "simple"
  • Tienen un cliente con una estructura organizacional más compleja que el árbol genealógico de los Skywalker
  • Quieren quedar como héroes Jedi frente al equipo de cuentas por cobrar

Si marcaste al menos dos de estos, agarrate. Que la Fuerza del cobro oportuno te acompañe.


🎯 El Desafío: Una Historia de Usuario (con R2-D2 de testigo)

Historia de usuario real:

Como administrador de cuentas por cobrar de la Alianza Rebelde, necesito que las facturas vencidas de cada planeta miembro le lleguen automáticamente a varios de sus contactos (no solo al Senador de turno), para que el aviso no quede perdido en un buzón que nadie revisa y el pago llegue antes de que tengamos que llamar a Darth Vader para ejercer cobranza extrajudicial.

El Flujo Actual (Antes de la Solución)

graph TD
    A[Cron de Seguimiento] --> B{¿Factura Vencida?}
    B -->|| C[Buscar destinatario]
    C --> D[¡Oh no! Solo la Dirección de Facturación]
    D --> E[El contacto está de vacaciones en Tatooine]
    E --> F[Factura sin pagar]
    F --> G[Llamada incómoda: "Nunca recibí nada"]
    G --> H[Activar protocolo de emergencia]
    H --> I[Invocar al departamento legal]
    I --> J[Los intereses corren. Todos pierden.]

Versión coral de lo que pasa después:
1. El módulo de seguimiento revisa las facturas (vía cron, religiosamente, como un droide protocolo que nunca falla)
2. Llama a _get_all_followup_contacts() que, en Odoo 19, dice: "Mirá, acá solo tengo una dirección. La de facturación."
3. El correo llega a UNA sola persona
4. Esa persona se fue de vacaciones a Tatooine / cambió de área / desertó a la Alianza Rebelde
5. La factura no se paga
6. Llamada incómoda del cliente: "Nunca recibí el aviso, ¿es una trampa?"
7. Tercer nivel de seguimiento. Ya van intereses. Sad wookie noises.

Lo Que NO Funciona (O Funciona Tan Mal Como un Protocolo Droide Bilingüe)

  • Cambiar la dirección de facturación a otro contacto: Es como poner a Chewbacca de piloto del Halcón Milenario sin enseñarle los controles. Funciona... un rato. Pero perdés la dirección original y terminas mandando facturas al almacén.

  • Poner varios emails en el campo "Email" del cliente: La tentación del desarrollador pobre. "Total, si separo con comas..." decís. Hasta que el sistema interpreta "juan@corp.com,pedro@corp.com,maria@corp.com" como un solo email y falla espectacularmente, como la Estrella de la Muerte ante un agujero de ventilación mal diseñado.

  • Desarrollo custom pesado: Sobreescribir _get_all_followup_contacts() con una lista fija de contactos es la opción técnicamente correcta. Pero requiere módulo custom, despliegue en producción, CI/CD, tests, mantenimiento... y probablemente un Sith Lord que firme el cambio de alcance. Para algo que debería ser configuración.

  • Agregar contactos en el wizard "Send & Print" cada vez: El wizard tiene un campo email_recipient_ids donde podés agregar contactos extra. Pero hay que hacerlo manualmente, cada vez. Es como tener que recargar los cañones láser uno por uno entre disparo y disparo. Funciona para misiones manuales ocasionales, no para el piloto automático del cron que ejecuta todo mientras dormís. ¿Quién va a estar despierto a las 3 AM para agregar al contador?


🔍 El Análisis Técnico (La Verdad del Lado Oscuro)

Cómo Viene en Odoo Estándar (Spoiler: No Viene)

El módulo account_followup (Enterprise) define en res.partner el método _get_all_followup_contacts(). Vamos a verlo con mis propios ojos:

def _get_all_followup_contacts(self):
    """ Followup contacts are defined as billing address and defaults to
    contact as defined in 'address_get' """
    self.ensure_one()
    return self.env['res.partner'].browse(self.address_get(['invoice'])['invoice'])

Traducción al básico: "Tomá la dirección de facturación. Si no hay, agarrá cualquier contacto. Fin. De. La. Historia."

No hay un campo "Seguimiento" en ningún lado. No hay una lista alternativa. Es un muro de contención tan sólido como el casco de un Destructor Estelar clase Imperial. Y tan inflexible también.

El Flujo Interno (El Verdadero Misterio del Hiperespacio)

Cuando el cron ejecuta _send_email(), el sistema construye la lista de destinatarios con este bailecore hiperspacial:

def _get_email_recipients(self, options):
    if options.get('email_recipient_ids'):
        return options.get('email_recipient_ids')  # Solo para el wizard manual

    partner = self.env['res.partner'].browse(options.get('partner_id'))
    recipients = partner._get_all_followup_contacts() or partner
    return recipients

Dos puntos de extensión:
1. El wizard manual (account_followup.manual_reminder) — permite agregar destinatarios extra en email_recipient_ids cada vez que enviás manualmente
2. El mail template del nivel de seguimiento — tiene un campo partner_to que SE RENDERIZA COMO QWEB y puede incluir IDs de contactos adicionales

El segundo es nuestro Hyperdrive oculto. Una puerta trasera que nadie había configurado porque está disfrazada de herramienta de administración aburrida.

Cómo Funciona partner_to (o: El Campo Más Poderoso de la Galaxia... de los Templates)

El campo partner_to de un mail template (mail.template) se renderiza usando QWeb sobre el modelo del template. En nuestro caso, el modelo es res.partner (el cliente).

El resultado se parsea así:

@classmethod
def _parse_partner_to(cls, partner_to):
    try:
        partner_to = literal_eval(partner_to or '[]')
    except (ValueError, SyntaxError):
        partner_to = partner_to.split(',')
    # ...

Esto significa que partner_to acepta:
- Una lista Python como [43, 44] — que literal_eval parsea como si fuera el C3PO de los parsers
- Un string separado por comas como "43,44" — que el split entiende como el idioma wookie

Ambos formatos funcionan. Pero no mezclar tipos. Hacer [42] + "43,44" es como querer que Jabba el Hutt corra una maratón: va a fallar, y va a ser espectacular.


💡 La Solución (Que Reluce como un Sable de Luz bien Calibrado)

El Concepto

"Hazlo o no lo hagas, pero no lo intentes." — Yoda, sobre mail templates

La idea es tan simple que parece magia:
1. Creás una etiqueta (categoría) llamada "Seguimiento de Cobros"
2. Marcás con esa etiqueta los contactos del cliente que deben recibir los avisos
3. En el mail template del nivel de seguimiento, ponés una expresión QWeb que recoja esos contactos

Zero desarrollo. Zero módulos custom. Solo configuración. Es como descubrir que el Halcón Milenario podía hacer el Kessel Run en menos de 12 pársecs porque ya tenía el hyperdrive, solo nadie lo había configurado.

Paso a Paso (Que Hasta un Stormtrooper Podría Seguir)

Paso 1: Crear la Etiqueta (El Primer Ladrillo de la Estrella de la Muerte)

  1. Ir a Contactos → Configuración → Etiquetas
  2. Crear una nueva etiqueta: "Seguimiento de Cobros"
  3. Anotar el ID numérico. Mirá la URL cuando estás editando: res.partner.category/42 → el 42 es tu tesoro. Guardalo como si fuera la ubicación del cuartel general rebelde.

Paso 2: Marcar los Contactos (Alistando a los Soldados)

Para cada cliente que tenga contactos que deban recibir seguimiento:
1. Ir a la ficha del contacto (sub-contacto del cliente)
2. En la pestaña de contacto, agregar la etiqueta "Seguimiento de Cobros"
3. Verificar que el contacto tenga email válido (sí, parece obvio, pero te sorprendería)

Repetir para cada contacto. Como el General Organa reclutando aliados por toda la galaxia.

⚠️ Regla de oro: Solo los contactos hijos del cliente (child_ids) van a ser filtrados. Si el cliente es una empresa y sus contactos están vinculados correctamente (como contactos hijos con parent_id seteado), esto funciona out of the box. Si alguien puso todos los contactos como empresas separadas sin jerarquía... bueno, ahí vas a tener un problema mayor, y este artículo no es una sesión de terapia organizacional.

Paso 3: Configurar el Mail Template (El Engine del Halcón Milenario)

  1. Ir a Contabilidad → Configuración → Seguimiento de cobros → Niveles de seguimiento
  2. Editar el nivel de seguimiento (o crear uno nuevo, como si estuvieras construyendo una segunda Estrella de la Muerte, pero mejor)
  3. Localizar el campo "Content Template" (es un mail.template vinculado)
  4. Si no existe, crearlo desde ahí mismo:
    - Modelo: res.partner
    - Tema: algo épico (ej: "Aviso de cobro - Nivel 1: el que avisa no traiciona")
  5. En el campo "Destinatarios (Empresas)" (field name mágico: partner_to), pegar esta expresión que cambiará tu vida profesional:
{{ object.child_ids.filtered(lambda c: TU_ID_ETIQUETA in c.category_id.ids).ids }}

Reemplazando TU_ID_ETIQUETA por el ID del Paso 1 (el que anotaste como si fuera la ubicación del tesoro).

¿Qué hace esta expresión que parece un encantamiento de la Fuerza?

  • object → es el cliente (la empresa a la que le estás haciendo el seguimiento)
  • object.child_ids → son TODOS los contactos hijos de esa empresa
  • .filtered(lambda c: TU_ID in c.category_id.ids) → es como un escáner de la Alianza: solo pasa los que tienen la etiqueta correcta
  • .ids → devuelve una lista de IDs que parece [43, 44, 45]literal_eval lo parsea feliz → el sistema los agrega como destinatarios

Y funciona. En serio.

Paso 4: Probar (El Vuelo de Prueba)

  1. Buscar una factura vencida de un cliente con contactos marcados (como encontrar un planeta rebelde, pero más fácil)
  2. Ir a la ficha del cliente → sección "Seguimiento de cobros" → botón "Enviar"
  3. Verificar que el wizard muestre los contactos extra en email_recipient_ids
  4. Enviar (apretá el botón como si fuera el hyperdrive del Halcón Milenario)
  5. Confirmar que los correos llegaron (si no llegaron, revisá que los emails sean reales y no los de un protocolo droide)

Verificación Funcional (Checklist del Comandante)

  • [ ] La etiqueta "Seguimiento de Cobros" existe en la configuración
  • [ ] Los contactos que deben recibir seguimiento tienen la etiqueta asignada
  • [ ] El mail template del nivel de seguimiento tiene la expresión en partner_to
  • [ ] Al abrir el wizard manual, los contactos aparecen como destinatarios
  • [ ] El envío automático (cron, el droide que trabaja mientras dormís) envía a los contactos adicionales
  • [ ] La dirección de facturación también recibe el correo (no la estamos sacando, solo sumando)

¿Y Esto Por Qué Funciona?

Porque el método _get_email_recipients hace esto en el código fuente:

recipients = partner._get_all_followup_contacts() or partner  # La invoice address
# ...
if mail_template:
    rendered_values = mail_template._generate_template_recipients(...)
    recipients |= partner.browse(rendered_values.get('partner_ids'))  # + los de partner_to
return recipients

Los destinatarios finales son:
1. ✅ La dirección de facturación (siempre, como el Imperio, que nunca se rinde)
2. ✅ Los contactos con la etiqueta "Seguimiento de Cobros" (los nuevos aliados rebeldes)

No hay duplicados. El operador |= sobre un recordset Odoo opera como una unión de conjuntos. Si el contacto de facturación ya está en la lista, no se agrega dos veces. La Fuerza obra de maneras misteriosas y eficientes.


✅ Resultados: El Antes y Después

Métricas de Batalla

Métrica Antes (Primera Estrella de la Muerte) Después (Flota Estelar Renovada)
Destinatarios por seguimiento 1 (invoice address) Los que necesites
Correos perdidos por contacto incorrecto "Es una trampa" "Todo está bien"
Tiempo de configuración por cliente 0 (no había opción) 30 segundos
Dependencia de desarrollo Sí, como Luke necesitando a Obi-Wan Ninguna. Es como usar la Fuerza.
Noches sin dormir por facturas impagas 3 por semana Lo justo y necesario

Beneficios por Rol (Porque en la Galaxia Hay de Todo)

Rol Qué Gana
Administración / Cuentas por Cobrar Los avisos llegan a quien corresponde; menos llamadas de "nunca recibí nada". Podés dormir tranquilo aunque el Imperio te persiga.
Consultor funcional Una solución limpia y configurable sin desarrollo que explicar al cliente. Salís como un Jedi delante del equipo.
Cliente final Sus contactos correctos reciben los avisos. Sin configuración extra de su lado. Y sin tener que aprender a usar la Fuerza.
Desarrollador No tiene que escribir ni mantener código para este caso. Días de trabajo que vuelven a ser para feature reales.

Lecciones Aprendidas en el Campo de Batalla

  • El campo partner_to de los mail templates en Odoo es mucho más poderoso de lo que parece. Es como descubrir que un droide de batalla podía pilotar un caza estelar y nadie lo había intentado. Renderiza QWeb sobre el modelo y puede hacer filtros complejos sin tocar Python.
  • La desaparición de la dirección de seguimiento en Odoo 17+ fue un retroceso funcional digno de la burocracia imperial, pero la plataforma ofrece alternativas más flexables y menos conocidas.
  • Usar el ID numérico de la etiqueta en vez del nombre es más robusto: si alguien traduce o cambia el nombre de la categoría, la expresión sigue funcionando. Como los sables de luz: no importa el color, lo que importa es que corte.

Extensiones Futuras (La Próxima Generación de la Flota Estelar)

  • Múltiples etiquetas: podrías tener "Seguimiento Nivel 1", "Seguimiento Final" y asignar diferentes contactos según la instancia del nivel de seguimiento. Como tener diferentes flotas para diferentes misiones.
  • Incluir o excluir según el rol: combiná con otras categorías para filtros más específicos. "A este contacto solo mandale el nivel 3". "A esta otra, todas".
  • Email CC automático: en vez de partner_to, podrías usar email_cc en el template para mandar copia oculta a ciertos contactos. Ideal para el supervisor que "solo quiere estar al tanto".

⚠️ El Lado Oscuro (Cosas a Tener en Cuenta)

Porque ninguna solución es perfecta, ni siquiera la Estrella de la Muerte:

  1. El ID de la etiqueta puede cambiar entre bases de datos. Si exportás / importás configuraciones, el ID puede ser diferente. Es como la constante cosmológica de la galaxia: no confíes en que el mismo número funcione en otro sistema. Reemplazalo.

  2. La expresión QWeb se renderiza en el contexto del partner. Si el cliente no tiene child_ids (no es una empresa con contactos hijos), la expresión devuelve una lista vacía y solo se envía a la dirección de facturación. Eso está bien como fallback. Como tener un droide de protocolo esperando por si las moscas.

  3. Esto funciona para el template del nivel de seguimiento. El partner_to se evalúa cuando el sistema renderiza el mail template. Si tenés múltiples niveles de seguimiento con diferentes templates, cada uno necesita su propia configuración. Nada es automático en la galaxia (excepto el cron de seguimiento, irónicamente).

  4. No probado en Odoo Community. El módulo account_followup es Enterprise. Si estás en Community, buscá el módulo de OCA account_followup. La Alianza Rebelde siempre tiene alternativas open source.

  5. Si el contacto marcado no tiene email, el sistema simplemente lo skipea. No hay error, no hay crash, no hay "se rompió todo". El resto de los destinatarios reciben el correo normalmente. Es como un droide que falla en silencio: no molesta, pero tampoco cumple su misión.


📚 Para Profundizar (Los Pergaminos Jedi)

  • Documentación oficial de Odoo - Seguimiento de cobros: account_followup en Enterprise. Ahí están los niveles, los templates, la magia.
  • Mail templates: El campo partner_to acepta expresiones QWeb que se renderizan en el contexto del modelo. Buscá _generate_template_recipients en addons/mail/models/mail_template.py. Código fuente, el verdadero manual de la Fuerza.
  • El método clave: res.partner._get_all_followup_contacts() en addons/account_followup/models/res_partner.py — el punto de extensión natural. Aunque no lo estemos sobreescribiendo, es bueno saber dónde está (por si las moscas, si el Imperio cambia las reglas).

💬 ¿Tu Experiencia es Diferente?

¿Resolviste este problema de otra forma? ¿Usaste algún módulo OCA? ¿Sobreescribiste _get_all_followup_contacts con una lógica más compleja? ¿Descubriste que el Halcón Milenario podía hacerlo mejor?

Me encantaría saber cómo lo solucionó la comunidad porque —seamos honestos— hay mil formas de hacer esto, y la mía es solo una más. Como dijo Obi-Wan: "Muchas de las verdades en las que creemos dependen en gran medida de nuestro propio punto de vista."

O en criollo: si tenés una solución mejor, compatila. El conocimiento es como la Fuerza: cuanto más se comparte, más crece.


Que el flujo de caja te acompañe. 🚀

📬 ¿No querés perderte ningún artículo nuevo?

Dejanos tu correo y te avisamos cuando publiquemos algo.

¡Gracias por suscribirte!