Ir al contenido

Odoo 19 Community: fusionar facturas en borrador – nuestro módulo de código abierto Merge Draft Invoices

18 de septiembre de 2026 por
Odoo 19 Community: fusionar facturas en borrador – nuestro módulo de código abierto Merge Draft Invoices
Alexander Biringer | Biricon IT Services GmbH

Quien factura en Odoo a partir de pedidos, tickets de helpdesk y registro de horas conoce el resultado: a final de mes un cliente no recibe una factura, sino cinco. Cada una procede de un pedido o ticket distinto, cada una es correcta en cuanto al contenido, y aun así el cliente pregunta si no cabría todo en un solo documento.

Precisamente para eso no existe en Odoo 19 Community una solución limpia. El conocido módulo OCA account_invoice_merge todavía no está publicado para Odoo 19 y resuelve el problema de una manera que supone un riesgo serio para los prestadores de servicios: las líneas iguales se agrupan y, con ello, se pierde el vínculo con el pedido, la tarea y el tiempo registrado.

Para nuestra propia instalación de Odoo 19 Community hemos desarrollado por eso el módulo Merge Draft Invoices. Como este problema no nos afecta solo a nosotros, lo ponemos gratuitamente a disposición en GitHub bajo la licencia libre AGPL-3.

Ir al proyecto en GitHub:

Odoo 19 Merge Draft Invoices

¿Por qué un módulo propio en lugar de la solución de la OCA?

La OCA mantiene desde hace muchos años account_invoice_merge en el repositorio OCA/account-invoicing. En el momento de esta entrada, la migración a 19.0 sigue siendo allí un pull request abierto, no un módulo publicado.

Más importante es, sin embargo, que los dos módulos hacen cosas distintas:


OCA account_invoice_mergeMerge Draft Invoices
Resultadose fusiona en una de las facturas seleccionadassiempre una factura nueva
Líneaslas líneas iguales se agrupan, las cantidades se sumancada línea se traslada una a una
Origeninvoice_origin / payment_reference concatenados en la cabeceracampos de origen por línea copiados mediante el propio hook _copy_data_extend_business_fields de Odoo
Registro de horasno se tiene en cuentatimesheet_invoice_id se reasigna a la nueva factura
Legible para el clientenadaun encabezado sobre las líneas de cada origen con pedido y ticket
Facturas de origense cancelancancelar (por defecto), eliminar o conservar, con consecuencias claramente indicadas

La agrupación de líneas iguales es la diferencia decisiva. Una línea agrupada solo puede tener un origen. En cuanto dos líneas de dos pedidos distintos tienen el mismo aspecto, se pierde el vínculo desde la factura hacia el pedido, la tarea y el registro de tiempo. En una empresa de servicios, en la que la facturación discurre a través de ticket → registro de horas → factura, esto significa que las mismas horas se cobran dos veces o desaparecen.

Por eso nuestro módulo, por principio, no agrupa nada. La factura fusionada tiene exactamente tantas líneas como sus orígenes juntos.

No se ha tomado ningún código del módulo de la OCA. Ambos módulos están bajo AGPL-3.

Qué hace el módulo

En la vista de lista de facturas, marque dos o más borradores y elija en el menú de acciones Fusionar facturas en borrador; un asistente muestra la selección, las comprobaciones y las opciones para las facturas de origen. Tras confirmar, se llega directamente a la nueva factura.

Antes de fusionar, el módulo comprueba, y lo hace todos los puntos a la vez, para que no aparezca un nuevo error tras cada corrección:

  • al menos dos facturas seleccionadas
  • todas en estado Borrador
  • ninguna se ha contabilizado nunca (un documento devuelto a borrador conserva su historial contable y se rechaza)
  • ninguna se ha fusionado ya
  • todas son factura, factura de proveedor, nota de crédito o recibo, no asientos puros
  • mismo contacto, tipo de documento, empresa, moneda y diario

Las diferencias en condición de pago, posición fiscal, dirección de entrega, cuenta bancaria o vendedor no bloquean. El asistente avisa y adopta el valor de la factura seleccionada más antigua.

Qué se conserva

El núcleo del módulo es que todo vínculo que una línea de factura tiene con su origen también exista en la nueva factura:

VínculoTratamiento
Líneas de pedido de venta (sale_line_ids)copiadas por línea mediante _copy_data_extend_business_fields de Odoo
Líneas de pedido de compra (purchase_line_id)igual
Distribución analítica (analytic_distribution)copiada por línea
otros campos de origen por líneaautomáticamente, si el módulo proveedor implementa el hook documentado de Odoo
Origen y referencia en la cabecera de la facturafusionados, sin duplicados, editables en el asistente
Pedido de venta: invoice_ids, invoice_status, qty_invoicedse calculan a partir de las líneas; el pedido reconoce la nueva factura por sí mismo
Registro de horas (timesheet_invoice_id)se reasigna a la nueva factura antes de que los orígenes dejen de ser válidos
Ticket de helpdeskdepende del registro de horas y, por tanto, apunta automáticamente a la nueva factura
Chattermensaje en la nueva factura con todos los orígenes, mensaje en cada origen con enlace a la nueva
merged_into_move_id / merged_from_move_idsvínculo guardado en ambas direcciones

Las líneas de impuestos, las líneas de condiciones de pago, las líneas de redondeo y de descuento por pronto pago no se copian, sino que Odoo las recalcula a partir del contenido. La factura fusionada tiene, por tanto, una línea de impuestos por tipo impositivo, no una por origen. Se crea sin numerar como borrador; la propia fusión 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. Sobre las líneas de cada factura de origen, el módulo coloca una línea de sección, el mismo elemento que Odoo utiliza para el apartado “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 por sí solo un subtotal por sección. Así el cliente ve también lo que ha costado cada parte.

La etiqueta se compone a partir de lo que el origen realmente tiene:

  1. los pedidos, directamente a partir de los vínculos de las líneas, de modo que los nombres son exactos; si faltan vínculos con pedidos, se utiliza el texto libre de invoice_origin
  2. los tickets, determinados a través del registro de horas, mostrados como Ticket: HT00219 o Tickets: …, limitados a cinco más “y N más”
  3. si no hay ninguno de los dos: el número de la factura de origen y después su referencia
  4. si no hay nada en absoluto: un neutro “Otras líneas”, para que las líneas de un origen nunca desaparezcan en silencio en el bloque anterior

Lo que falta se omite, no se inventa. El ticket se imprime con su propio número (número de helpdesk_mgmt, ticket_ref de Enterprise o el nombre para mostrar del modelo), nunca con el ID de la base de datos.

Los encabezados se escriben en el idioma del cliente, tal como lo hace Odoo al generar facturas a partir de pedidos. El chatter permanece en el idioma del usuario.

Un encabezado no lleva producto, ni cantidad, ni precio, ni impuesto, ni cuenta, ni distribución analítica. No puede influir en los totales, la base imponible ni la contabilidad analítica. Quien desactive los encabezados en el asistente obtiene exactamente la factura que resultaría sin esta función; una prueba compara ambos resultados hasta el céntimo.

Qué sucede con las facturas de origen

El asistente ofrece tres opciones:

  • Cancelar (por defecto, recomendado). Cancelar un borrador no tiene consecuencias: ni número de documento ni asiento. Las facturas permanecen en la base de datos como documentos cancelados, con chatter y enlace a la nueva factura. qty_invoiced en el pedido de venta omite los documentos cancelados, nada se cuenta dos veces. Esta es la opción que sigue siendo trazable incluso dentro de seis meses.
  • Eliminar. Irreversible. El chatter y la referencia inversa desaparecen con el registro; solo queda el mensaje en la nueva factura. Tiene sentido para borradores desechables que nadie debe volver a ver.
  • Dejar como borrador. Los orígenes permanecen abiertos. Sus pedidos de venta cuentan entonces el origen y la nueva factura como facturados; el trabajo parece facturado dos veces hasta que alguien depure los borradores a mano. Pensado para el caso poco frecuente en que se quiere comparar antes de decidir. Una prueba refleja precisamente este doble recuento para que no sorprenda a nadie.

Límites: lo que el módulo deliberadamente no hace

Léalo antes del uso en producción:

  • Solo borradores. Las facturas contabilizadas no se fusionan. Eso es una tarea de notas de crédito, no una operación de copia.
  • Se rechazan los borradores que se contabilizaron una vez y se revirtieron. Tienen un número y un historial contable; esconder eso en otro documento no es algo que una acción en bloque deba hacer en silencio.
  • No se fusiona entre distintos contactos, monedas, empresas ni diarios. No existe para ello un valor único con sentido, así que el módulo rechaza la operación en lugar de adivinar.
  • payment_reference no se traslada. Las referencias de pago estructuradas identifican un documento; dos concatenadas no coinciden con nada en el banco. La nueva factura recibe la suya propia al contabilizarse.
  • Las líneas de anticipo se copian como cualquier otra línea. Si los orígenes contienen líneas de anticipo de sale, debe revisarse el resultado antes de contabilizar.
  • Solo timesheet_invoice_id se reasigna automáticamente. Cualquier otro modelo con un Many2one hacia account.move (procesos de facturación propios, vínculos de suscripción, envíos EDI) sigue apuntando al origen. Para ello 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 crean una sola vez, al fusionar. Después son líneas de sección normales: editables, eliminables y no se actualizan si el pedido o el ticket se renombran más tarde.
  • Un encabezado nombra 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:

  1. Copiar la carpeta del módulo en una ruta de addons configurada.
  2. Actualizar la lista de aplicaciones en Odoo.
  3. Instalar el módulo account_invoice_merge_draft.

El módulo depende solo de account. Todo lo que sabe sobre sale, purchase, project, hr_timesheet, sale_timesheet o un módulo de helpdesk se determina en tiempo de ejecución; se instala y funciona también en una base de datos puramente de facturación.

Después: Contabilidad → Clientes → Facturas (o Proveedores → Facturas), marque dos o más borradores, menú de la rueda dentada → Fusionar facturas en borrador. La acción está disponible para todos los usuarios con el grupo Facturación.

Quien actualice desde una versión anterior del módulo debería ejecutar una sola vez la actualización con -u account_invoice_merge_draft --i18n-overwrite, para que los nombres de campo modificados lleguen a la traducción alemana. Una instalación nueva no se ve afectada.

Para desarrolladores: API y hooks

La fusión también puede invocarse 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; por defecto True
)

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 los mismos motivos como lista, sin excepción.

Quien tenga referencias inversas 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}
        )

Quien tenga una referencia inversa en la línea de factura no necesita este módulo en absoluto: basta el propio hook de Odoo account.move.line._copy_data_extend_business_fields, y el campo se traslada por igual al fusionar, al cancelar y al duplicar. Para otros módulos de helpdesk o para otra redacción de los encabezados existen _merge_draft_get_source_tickets() y _merge_draft_section_name().

Probado antes de ejecutarse con datos de producción

48 pruebas cubren estados mezclados, contactos, monedas y tipos de documento distintos, líneas con y sin pedido de venta o de compra, impuestos, secciones y notas, distribuciones analíticas, la reasignación del registro de horas, las tres opciones para las facturas de origen, el asistente y los encabezados: que están activos por defecto, que desactivarlos reproduce el resultado antiguo hasta el céntimo, que un encabezado no lleva importes ni cuentas, que las líneas de un origen permanecen juntas, que el ticket aparece con su número y nunca con el ID de la base de datos.

Los datos de prueba se construyen sobre el plan contable existente de la empresa activa en lugar de sobre AccountTestInvoicingCommon: este helper a menudo no arranca en bases de datos de producción migradas, y justamente en ellas 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 en el ámbito contable, recomendamos probar primero la instalación en un entorno de pruebas con una copia de la propia base de datos.

Gratuito 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 empresa. Si de ello surge un módulo de uso general, no queremos esconderlo en una única instalación.

Por eso Merge Draft Invoices está disponible gratuitamente como proyecto de código abierto. El código fuente puede consultarse, revisarse, adaptarse y seguir desarrollándose en el marco de la licencia AGPL-3.

Nuestro objetivo no era la factura más corta posible, sino una factura en la que, incluso dentro de un año, cada línea siga llevando de vuelta a su pedido, a su ticket y a su registro de tiempo.

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 prácticas, los informes de errores, las sugerencias de mejora y las contribuciones de la comunidad de Odoo.

Gratuito. Trazable. Código abierto. Para Odoo 19 Community.