Quienes facturan en Odoo a partir de pedidos, tickets del servicio de asistencia y registros de horas conocen el resultado: al final de mes, un cliente recibe cinco facturas en lugar de una. Cada una procede de un pedido o ticket distinto y todas son correctas, pero aun así el cliente pregunta si no se podrían reunir en un solo documento.
Odoo 19 Community no ofrece una solución adecuada para este caso. El conocido módulo account_invoice_merge de OCA aún no está disponible para Odoo 19 y resuelve el problema de una forma que supone un riesgo importante para los proveedores de servicios: agrupa las líneas idénticas y, con ello, se pierde el vínculo con el pedido, la tarea y las horas registradas.
Por eso hemos desarrollado el módulo Merge Draft Invoices para nuestra propia instalación de Odoo 19 Community. Como este problema también afecta a otros usuarios, lo ofrecemos gratuitamente en GitHub bajo la licencia libre AGPL-3.
Proyecto en GitHub:
¿Por qué desarrollar un módulo propio en lugar de utilizar la solución de OCA?
OCA lleva muchos años manteniendo account_invoice_merge en el repositorio OCA/account-invoicing. En el momento de publicar este artículo, la migración a 19.0 sigue siendo una solicitud de incorporación de cambios abierta, no un módulo publicado.
Pero lo más importante es que los dos módulos hacen cosas distintas:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Resultado | se fusionan en una de las facturas seleccionadas | siempre una factura nueva |
| Líneas | se agrupan las líneas idénticas y se suman las cantidades | cada línea se transfiere una por una, sin agrupar |
| Origen | invoice_origin / payment_reference concatenados en la cabecera | campos de origen de cada línea copiados mediante el hook _copy_data_extend_business_fields de Odoo |
| Registro de horas | no se tiene en cuenta | timesheet_invoice_id se reasigna a la nueva factura |
| Claridad para el cliente | ninguna | un encabezado sobre las líneas de cada origen que indica el pedido y el ticket |
| Facturas de origen | se cancelan | cancelar (opción predeterminada), eliminar o conservar, con las consecuencias claramente indicadas |
La agrupación de líneas idénticas es la diferencia clave. Una línea agrupada solo puede tener un origen. En cuanto se agrupan dos líneas aparentemente iguales procedentes de pedidos distintos, se pierde el vínculo de la factura con el pedido, la tarea y el registro de horas. En una empresa de servicios cuyo proceso de facturación sigue el recorrido ticket → registro de horas → factura, esto significa que las mismas horas se facturan dos veces o desaparecen.
Por eso, nuestro módulo nunca agrupa líneas. La factura fusionada tiene exactamente tantas líneas como todas las facturas de origen juntas.
No se ha incorporado código del módulo de OCA. Ambos módulos se distribuyen bajo la licencia AGPL-3.
Qué hace el módulo
En la vista de lista de facturas, selecciona dos o más borradores y elige Fusionar facturas en borrador en el menú de acciones. Un asistente muestra la selección, las comprobaciones y las opciones para las facturas de origen. Tras confirmar, se abre directamente la nueva factura.
Antes de fusionar, el módulo comprueba todos estos puntos a la vez, para evitar que aparezca un nuevo error después de cada corrección:
- se han seleccionado al menos dos facturas
- todas están en estado Borrador
- ninguna se ha contabilizado nunca (un documento restablecido a borrador conserva su historial contable y se rechaza)
- ninguna se ha fusionado previamente
- todas son facturas de cliente, facturas de proveedor, facturas rectificativas o recibos; no se admiten asientos contables independientes
- tienen el mismo contacto, tipo de documento, empresa, moneda y diario
Las diferencias en las condiciones de pago, la posición fiscal, la dirección de entrega, la cuenta bancaria o el comercial no bloquean la operación. El asistente muestra una advertencia y utiliza el valor de la factura más antigua de la selección.
Qué se conserva
La función esencial del módulo es conservar en la nueva factura todos los vínculos de cada línea de factura con su origen:
| Vínculo | Tratamiento |
|---|---|
| Líneas de pedidos de venta (sale_line_ids) | se copian en cada línea mediante _copy_data_extend_business_fields de Odoo |
| Líneas de pedidos de compra (purchase_line_id) | del mismo modo |
| Distribución analítica (analytic_distribution) | se copia en cada línea |
| otros campos de origen de cada línea | automáticamente, si el módulo que los proporciona implementa el hook documentado de Odoo |
| Origen y referencia en la cabecera de la factura | se combinan, se eliminan los duplicados y se pueden editar en el asistente |
| Pedido de venta: invoice_ids, invoice_status, qty_invoiced | se calculan a partir de las líneas: el pedido reconoce automáticamente la nueva factura |
| Registro de horas (timesheet_invoice_id) | se reasigna a la nueva factura antes de que las facturas de origen dejen de ser válidas |
| Ticket de Helpdesk | está vinculado al registro de horas y, por tanto, pasa a apuntar automáticamente a la nueva factura |
| Chatter | mensaje en la nueva factura con todas las facturas de origen y mensaje en cada factura de origen con un enlace a la nueva |
| merged_into_move_id / merged_from_move_ids | vínculo guardado en ambos sentidos |
Las líneas de impuestos, condiciones de pago, redondeo y descuentos por pronto pago no se copian: Odoo las recalcula a partir del contenido. Por tanto, la factura fusionada tiene una línea de impuestos por tipo impositivo, no una por cada factura de origen. Se crea como borrador sin numerar; la fusión en sí no consume ningún número de documento.
Encabezados: el cliente ve de dónde procede cada línea
Esta es la función que marca la diferencia en el día a día. El módulo inserta una línea de sección encima de las líneas de cada factura de origen: el mismo elemento que utiliza Odoo para la sección «Anticipos»:
BE10181 <- Encabezado
22,500 x Hora de técnico 3.577,50
Subtotal 3.577,50
BE10182 · Ticket: HT00219 <- Encabezado
3,000 x Hora de técnico 477,00
Subtotal 477,00
IMPORTE NETO 4.054,50
El encabezado se imprime en el documento que recibe el cliente, y el informe de factura estándar de Odoo añade automáticamente un subtotal por sección. Así, el cliente también ve cuánto ha costado cada parte.
El texto se compone a partir de los datos que realmente contiene la factura de origen:
- los pedidos, obtenidos directamente de los vínculos de las líneas, de modo que sus nombres son exactos; si no hay vínculos con pedidos, se utiliza el texto libre de invoice_origin
- los tickets, identificados a través del registro de horas; se muestran como Ticket: HT00219 o Tickets: …, con un máximo de cinco, seguido de «y N más»
- si no hay ninguno de los dos: el número de la factura de origen y, en su defecto, su referencia
- si no hay ningún dato: un texto neutro, «Otras líneas», para que las líneas de una factura de origen nunca queden integradas sin indicación alguna en el bloque anterior
Lo que falta se omite, no se inventa. El ticket se imprime con su propio número —el número de helpdesk_mgmt, el ticket_ref de Enterprise o el nombre mostrado del modelo—, nunca con el ID de la base de datos.
Los encabezados se escriben en el idioma del cliente, igual que hace Odoo al generar facturas a partir de pedidos. El Chatter se mantiene en el idioma del usuario.
Un encabezado no contiene producto, cantidad, precio, impuesto, cuenta ni distribución analítica. No puede afectar a los totales, la base imponible ni la contabilidad analítica. Si se desactivan los encabezados en el asistente, se obtiene exactamente la factura que se generaría sin esta función: una prueba compara ambos resultados hasta el último céntimo.
Qué ocurre con las facturas de origen
El asistente ofrece tres opciones:
- Cancelar (opción predeterminada y recomendada). Cancelar un borrador no tiene consecuencias contables: no genera número de documento ni asiento. Las facturas permanecen en la base de datos como documentos cancelados, con su Chatter y un enlace a la nueva factura. El campo qty_invoiced del pedido de venta omite los documentos cancelados, por lo que nada se cuenta dos veces. Esta opción permite seguir entendiendo lo ocurrido incluso seis meses después.
- Eliminar. Es irreversible. El Chatter y el enlace de retorno desaparecen con el registro; solo queda el mensaje en la nueva factura. Es útil para borradores de usar y tirar que ya no deba ver nadie.
- Mantener como borrador. Las facturas de origen permanecen abiertas. Sus pedidos de venta contabilizan entonces como facturadas tanto la factura de origen como la nueva, por lo que el trabajo parece haberse facturado dos veces hasta que alguien revise y corrija los borradores manualmente. Está pensado para el caso excepcional de querer comparar antes de decidir. Una prueba verifica expresamente este doble cómputo para que no sorprenda a nadie.
Límites: lo que el módulo no hace por diseño
Lee lo siguiente antes de utilizarlo en producción:
- Solo borradores. Las facturas contabilizadas no se fusionan. Para ello hay que recurrir a facturas rectificativas, no a una operación de copia.
- Se rechazan los borradores que se hayan contabilizado y restablecido posteriormente. Tienen un número y un historial contable; ocultarlos en otro documento no es algo que una acción masiva deba hacer sin más.
- No se fusionan documentos de distintos contactos, monedas, empresas o diarios. No hay un único valor que tenga sentido en esos casos, por lo que el módulo rechaza la operación en lugar de adivinar.
- payment_reference no se transfiere. Las referencias de pago estructuradas identifican un documento; concatenar dos no permite al banco identificar ninguno. La nueva factura recibe su propia referencia al contabilizarse.
- Las líneas de anticipo se copian como cualquier otra línea. Si las facturas de origen contienen líneas de anticipo de sale, hay que revisar el resultado antes de contabilizarlo.
- Solo timesheet_invoice_id se reasigna automáticamente. Cualquier otro modelo con un campo Many2one que apunte a account.move (procesos de facturación propios, vínculos de suscripciones o transmisiones EDI) sigue apuntando a la factura de origen. Para estos casos existe el hook _merge_draft_relink.
- Ninguna agrupación de líneas iguales, deliberadamente. Quien quiera una factura corta y ordenada se ha equivocado de módulo y lo pagaría con los vínculos de origen.
- Los encabezados se generan una sola vez, al fusionar. Después son líneas de sección normales: se pueden editar y eliminar, y no se actualizan si más adelante se cambia el nombre del pedido o del ticket.
- Cada encabezado indica los tickets de toda la factura de origen, no los de cada línea.
Instalación en Odoo 19 Community
La instalación se realiza como la de cualquier módulo adicional de Odoo:
- Copia el directorio del módulo en una ruta de complementos configurada.
- Actualiza la lista de aplicaciones en Odoo.
- Instala el módulo account_invoice_merge_draft.
El módulo solo depende de account. Toda la información que necesita de sale, purchase, project, hr_timesheet, sale_timesheet o de un módulo de Helpdesk se obtiene en tiempo de ejecución: también se instala y funciona en una base de datos dedicada exclusivamente a la facturación.
Después: Contabilidad → Clientes → Facturas (o Proveedores → Facturas), selecciona dos o más borradores y abre el menú del engranaje → Fusionar facturas en borrador. La acción está disponible para todos los usuarios del grupo Facturación / Facturación.
Si actualizas desde una versión anterior del módulo, ejecuta una vez la actualización con -u account_invoice_merge_draft --i18n-overwrite para que los cambios en las etiquetas de los campos se apliquen a la traducción alemana. Esto no afecta a las instalaciones nuevas.
Para desarrolladores: API y hooks
La fusión también se puede ejecutar desde código propio:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" o "keep"
add_source_sections=True, # Encabezados; True por defecto
)
merge_draft_moves lanza un UserError con todos los motivos de rechazo si la selección no se puede fusionar. _merge_draft_get_errors() devuelve esos mismos motivos en una lista, sin lanzar ninguna excepción.
Si tienes referencias propias a account.move, sobrescribe _merge_draft_relink:
python
class AccountMove(models.Model):
_inherit = "account.move"
def _merge_draft_relink(self, new_move):
super()._merge_draft_relink(new_move)
if "my_move_id" not in self.env["my.model"]._fields:
return
self.env["my.model"].search([("my_move_id", "in", self.ids)]).write(
{"my_move_id": new_move.id}
)
Si la referencia está en la línea de factura, ni siquiera necesitas este módulo: basta con el hook de Odoo account.move.line._copy_data_extend_business_fields, y el campo se transfiere tanto al fusionar como al cancelar y duplicar. Para otros módulos de Helpdesk o para cambiar el texto de los encabezados, existen _merge_draft_get_source_tickets() y _merge_draft_section_name().
Probado antes de utilizarlo con datos de producción
Las 48 pruebas cubren combinaciones de estados, distintos contactos, monedas y tipos de documento, líneas con y sin pedidos de venta o compra, impuestos, secciones y notas, distribuciones analíticas, la reasignación de los registros de horas, las tres opciones para las facturas de origen, el asistente y los encabezados: que estén activados por defecto, que al desactivarlos se reproduzca el resultado anterior hasta el último céntimo, que un encabezado no contenga importes ni cuentas, que las líneas de una misma factura de origen permanezcan juntas y que el ticket aparezca con su número, nunca con el ID de la base de datos.
Los datos de prueba se basan en el plan contable existente de la empresa activa, en lugar de utilizar AccountTestInvoicingCommon. Esta utilidad a menudo no se inicia en bases de datos de producción migradas, precisamente aquellas en las que debe funcionar el módulo.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Como con cualquier extensión de contabilidad, recomendamos probar primero la instalación en un entorno de pruebas con una copia de la base de datos propia.
Gratis y de código abierto para la comunidad de Odoo
En Biricon IT Services GmbH desarrollamos soluciones, en primer lugar, para necesidades concretas de nuestra propia actividad. Cuando de ellas surge un módulo de utilidad general, queremos que esté disponible más allá de una única instalación.
Por eso, Merge Draft Invoices está disponible gratuitamente como proyecto de código abierto. Su código fuente se puede consultar, revisar, adaptar y seguir desarrollando conforme a la licencia AGPL-3.
Nuestro objetivo no era conseguir la factura más corta posible, sino una factura en la que, incluso dentro de un año, cada línea siga permitiendo llegar hasta su pedido, su ticket y su registro de horas.
Proyecto, código fuente y colaboración
El código fuente completo, las instrucciones de instalación y la documentación técnica están disponibles en GitHub:
Abrir Odoo 19 Merge Draft Invoices en GitHub
Agradecemos las experiencias de uso, los informes de errores, las sugerencias de mejora y las contribuciones de la comunidad de Odoo.
Gratis. Trazable. De código abierto. Para Odoo 19 Community.