Qui factura a Odoo a partir de comandes, tiquets d'helpdesk i registre d'hores en coneix el resultat: un client no rep a final de mes una factura, sinó cinc. Cadascuna prové d'una comanda o d'un tiquet diferent, cadascuna és correcta des del punt de vista tècnic i, tanmateix, el client pregunta si no caldria que tot cabés en un sol document.
Precisament per a això no hi ha cap solució neta a Odoo 19 Community. El conegut mòdul d'OCA account_invoice_merge encara no està habilitat per a Odoo 19 i resol el problema d'una manera que suposa un risc seriós per als proveïdors de serveis: les línies iguals s'agrupen i, així, es perd el vincle amb la comanda, la tasca i el temps registrat.
Per a la nostra pròpia instal·lació d'Odoo 19 Community hem desenvolupat per això el mòdul Merge Draft Invoices. Com que aquest problema no afecta només nosaltres, el posem gratuïtament a disposició a GitHub sota la llicència lliure AGPL-3.
Al projecte a GitHub:
Per què un mòdul propi en lloc de la solució d'OCA?
Fa molts anys que OCA manté account_invoice_merge al repositori OCA/account-invoicing. En el moment d'aquesta entrada, la migració a 19.0 hi és encara un pull request obert, no un mòdul publicat.
Més important, però, és que els dos mòduls fan coses diferents:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Resultat | es combina en una de les factures seleccionades | sempre una factura nova |
| Línies | les línies iguals s'agrupen, les quantitats se sumen | cada línia es tradueix un a un |
| Origen | invoice_origin / payment_reference enllaçats a la capçalera | camps d'origen per línia copiats mitjançant el hook _copy_data_extend_business_fields propi d'Odoo |
| Registre d'hores | no es té en compte | timesheet_invoice_id es reassigna a la factura nova |
| Llegible per al client | res | una capçalera sobre les línies de cada font amb la comanda i el tiquet |
| Factures d'origen | s'anul·len | anul·lar (per defecte), eliminar o conservar, amb conseqüències anomenades amb claredat |
Agrupar les línies iguals és la diferència decisiva. Una línia agrupada només pot tenir un origen. Tan bon punt dues línies de dues comandes diferents tenen el mateix aspecte, el vincle de la factura amb la comanda, la tasca i el registre d'hores desapareix. En una empresa de serveis en què la facturació segueix el circuit tiquet → registre d'hores → factura, això vol dir: les mateixes hores es facturen dues vegades o desapareixen.
Per això, el nostre mòdul, en principi, no agrupa res. La factura combinada té exactament tantes línies com totes les seves fonts juntes.
No s'ha pres cap codi del mòdul d'OCA. Tots dos mòduls estan sota AGPL-3.
Què fa el mòdul
A la vista de llista de factures, marqui dos o més esborranys i triï al menú d'accions Combinar factures en esborrany; un assistent mostra la selecció, les comprovacions i les opcions per a les factures d'origen. Després de confirmar, s'arriba directament a la factura nova.
Abans de combinar, el mòdul fa les comprovacions, i totes alhora, perquè no calgui veure l'error següent després de cada correcció:
- s'han seleccionat almenys dues factures
- totes en estat Esborrany
- cap no s'ha comptabilitzat mai (un document restablert a esborrany conserva el seu historial d'apunts i es rebutja)
- cap no s'ha combinat ja
- totes són factura, factura de proveïdor, abonament o document, no pas simples assentaments comptables
- mateix soci, tipus de document, empresa, moneda i diari
Les diferències en la condició de pagament, la posició fiscal, l'adreça d'enviament, el compte bancari o el venedor no bloquegen. L'assistent avisa i adopta el valor de la factura seleccionada més antiga.
Què es conserva
El nucli del mòdul és que cada vincle que una línia de factura té amb el seu origen també existeixi a la factura nova:
| Vincle | Tractament |
|---|---|
| Línies de comanda de venda (sale_line_ids) | copiat per línia mitjançant _copy_data_extend_business_fields d'Odoo |
| Línies de comanda de compra (purchase_line_id) | igualment |
| Distribució per centres de cost (analytic_distribution) | copiada per línia |
| altres camps d'origen per línia | automàticament, si el mòdul proveïdor implementa el hook documentat d'Odoo |
| Origen i referència a la capçalera de la factura | combinats, deduplicats, editables a l'assistent |
| Comanda de venda: invoice_ids, invoice_status, qty_invoiced | es calculen a partir de les línies; la comanda reconeix la factura nova per si mateixa |
| Registre d'hores (timesheet_invoice_id) | es reassigna a la factura nova abans que les fonts es tornin invàlides |
| Tiquet d'helpdesk | depèn del registre d'hores i, per tant, apunta automàticament a la factura nova |
| Chatter | missatge a la factura nova amb totes les fonts, missatge a cada font amb enllaç a la nova |
| merged_into_move_id / merged_from_move_ids | vincle desat en tots dos sentits |
Les línies d'impostos, de condicions de pagament, d'arrodoniment i de descompte per pagament immediat no es copien, sinó que Odoo les torna a calcular a partir del contingut. Així, la factura combinada té una línia d'impostos per tipus impositiu, no una per font. Es crea sense numerar, com a esborrany; combinar en si mateix no consumeix cap número de document.
Capçaleres: el client veu d'on prové cada línia
Aquesta és la funció que marca la diferència en el dia a dia. Sobre les línies de cada factura d'origen, el mòdul posa una línia de secció, el mateix element que Odoo utilitza per a la secció «Pagaments avançats»:
BE10181 <- Capçalera
22,500 x Hora de tècnic 3.577,50
Subtotal 3.577,50
BE10182 · Tiquet: HT00219 <- Capçalera
3,000 x Hora de tècnic 477,00
Subtotal 477,00
IMPORT NET 4.054,50
La capçalera s'imprimeix en el document que rep el client, i l'informe de factura estàndard d'Odoo afegeix per si mateix un subtotal per secció. Així el client també veu què ha costat cada part.
L'etiqueta es compon a partir del que la font té realment:
- les comandes, directament a partir dels vincles de les línies; els noms són, doncs, exactes; si falten els vincles de comanda, s'utilitza el text lliure d'invoice_origin
- els tiquets, determinats mitjançant el registre d'hores; es mostren com a Tiquet: HT00219 o Tiquets: …, limitats a cinc més «i N més»
- si no hi ha cap dels dos: el número de la factura d'origen i, després, la seva referència
- si no hi ha res: un neutre «Altres línies», perquè les línies d'una font mai no desapareguin en silenci al bloc anterior
El que falta s'omet, no s'inventa. El tiquet s'imprimeix amb el seu propi número (número d'helpdesk_mgmt, ticket_ref d'Enterprise o el nom visible del model), mai amb l'ID de la base de dades.
Les capçaleres s'escriuen en l'idioma del client, tal com Odoo ho fa en generar factures a partir de comandes. El chatter continua en l'idioma de l'usuari.
Una capçalera no porta cap producte, quantitat, preu, impost, compte ni distribució analítica. No pot influir en els totals, la base imposable ni la comptabilitat analítica. Qui desactiva les capçaleres a l'assistent obté exactament la factura que s'hauria generat sense aquesta funció; una prova compara tots dos resultats fins al cèntim.
Què passa amb les factures d'origen
L'assistent ofereix tres opcions:
- Anul·lar (per defecte, recomanat). Anul·lar un esborrany no té conseqüències: ni número de document ni apunt. Les factures romanen a la base de dades com a documents anul·lats, amb chatter i enllaç a la factura nova. qty_invoiced de la comanda de venda se salta els documents anul·lats; res no es compta dues vegades. És l'opció que encara es pot entendre d'aquí a sis mesos.
- Eliminar. Irrevocable. El chatter i la referència inversa moren amb el registre; només queda el missatge de la factura nova. Té sentit per a esborranys d'usar i llençar que ningú no vol tornar a veure.
- Deixar com a esborrany. Les fonts continuen obertes. Les seves comandes de venda compten llavors la font i la factura nova com a facturades; la feina sembla facturada dues vegades fins que algú netegi els esborranys a mà. Pensat per al cas poc freqüent en què es vol comparar abans de decidir. Una prova recull precisament aquest doble recompte perquè no sorprengui ningú.
Límits: què no fa deliberadament el mòdul
Si us plau, llegeixi-ho abans de l'ús en producció:
- Només esborranys. Les factures comptabilitzades no es combinen. Això és una tasca d'abonaments, no un procés de còpia.
- Els esborranys que ja es van comptabilitzar una vegada i es van restablir es rebutgen. Tenen un número i un historial d'apunts; amagar-ho en un altre document no és res que una acció conjunta hagi de fer en silenci.
- No es combina entre socis, monedes, empreses ni diaris. Per a això no hi ha cap valor únic raonable, de manera que el mòdul ho rebutja en lloc d'endevinar.
- payment_reference no s'adopta. Les referències de pagament estructurades identifiquen un document; dues d'enllaçades no encaixen amb res al banc. La factura nova obté la seva pròpia en comptabilitzar-se.
- Les línies d'avançament es copien com qualsevol altra línia. Si les fonts contenen línies d'avançament de sale, cal revisar el resultat abans de comptabilitzar.
- Només timesheet_invoice_id es reassigna automàticament. Qualsevol altre model amb un Many2one a account.move (execucions de facturació pròpies, vincles d'abonaments, transmissions EDI) continua apuntant a la font. Per a això hi ha el hook _merge_draft_relink.
- Cap agrupació de línies iguals, expressament. Qui vulgui una factura curta i ordenada s'equivoca de lloc i ho pagaria amb els vincles d'origen.
- Les capçaleres es creen una sola vegada en combinar. Després són línies de secció normals: editables, esborrables i no s'actualitzen si la comanda o el tiquet es reanomenen més endavant.
- Una capçalera esmenta els tiquets de tota la factura d'origen, no els de cada línia.
Instal·lació a Odoo 19 Community
La instal·lació es fa com amb qualsevol mòdul addicional d'Odoo habitual:
- Copiar el directori del mòdul en una ruta d'addons configurada.
- Actualitzar la llista d'aplicacions a Odoo.
- Instal·lar el mòdul account_invoice_merge_draft.
El mòdul només depèn d'account. Tot el que sap sobre sale, purchase, project, hr_timesheet, sale_timesheet o un mòdul d'helpdesk es determina en temps d'execució; s'instal·la i funciona també en una base de dades de facturació pura.
Després: Comptabilitat → Clients → Factures (o Proveïdors → Factures), marqui dos o més esborranys, menú de la roda dentada → Combinar factures en esborrany. L'acció està disponible per a tots els usuaris del grup Facturació.
Qui actualitzi des d'una versió anterior del mòdul hauria d'actualitzar una sola vegada amb -u account_invoice_merge_draft --i18n-overwrite perquè els noms de camp canviats arribin a la traducció a l'alemany. Una instal·lació nova no se'n veu afectada.
Per a desenvolupadors: API i hooks
La combinació també es pot cridar des de codi propi:
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, # capçaleres; per defecte True
)
merge_draft_moves llança un UserError amb tots els motius de rebuig quan la selecció no es pot combinar. _merge_draft_get_errors() retorna els mateixos motius com a llista, sense excepció.
Qui tingui referències inverses pròpies a account.move, sobreescriu _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}
)
Qui tingui una referència inversa a la línia de factura no necessita gens aquest mòdul: el hook propi d'Odoo account.move.line._copy_data_extend_business_fields n'hi ha prou, i el camp s'adopta igualment en combinar, en anul·lar i en duplicar. Per a altres mòduls d'helpdesk o una altra formulació de les capçaleres hi ha _merge_draft_get_source_tickets() i _merge_draft_section_name().
Provat abans que s'executi amb dades de producció
48 proves cobreixen estats barrejats, socis, monedes i tipus de document diferents, línies amb i sense comanda de venda o de compra, impostos, seccions i notes, distribucions per centres de cost, la reassignació del registre d'hores, les tres opcions per a les factures d'origen, l'assistent i les capçaleres: que estan actives per defecte, que desactivar-les reprodueix l'antic resultat fins al cèntim, que una capçalera no porta imports ni comptes, que les línies d'una font romanen juntes, que el tiquet apareix amb el seu número i mai amb l'ID de la base de dades.
Les dades de prova es basen en el pla comptable existent de l'empresa activa en lloc d'AccountTestInvoicingCommon; aquest helper sovint no arrenca en bases de dades de producció migrades, i el mòdul ha de funcionar precisament en aquestes.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Com amb qualsevol extensió de l'àmbit comptable, recomanem comprovar primer la instal·lació en un entorn de proves amb una còpia de la pròpia base de dades.
Gratuït i de codi obert per a l'Odoo Community
A Biricon IT Services GmbH desenvolupem solucions primer per a requisits concrets de la nostra pròpia empresa. Quan d'això en surt un mòdul d'ús general, no volem amagar-lo en una sola instal·lació.
Per això, Merge Draft Invoices està disponible gratuïtament com a projecte de codi obert. El codi font es pot consultar, revisar, adaptar i continuar desenvolupant en el marc de la llicència AGPL-3.
El nostre objectiu no era la factura més curta possible, sinó una factura en què, també d'aquí a un any, cada línia porti encara de tornada a la seva comanda, al seu tiquet i al seu apunt d'hores.
Projecte, codi font i col·laboració
El codi font complet, les indicacions d'instal·lació i la documentació tècnica estan disponibles a GitHub:
Obrir Odoo 19 Merge Draft Invoices a GitHub
Agraïm les experiències pràctiques, els informes d'errors, els suggeriments de millora i les contribucions de l'Odoo Community.
Gratuït. Traçable. Codi obert. Per a Odoo 19 Community.