Chi in Odoo fattura a partire da ordini, ticket helpdesk e rilevazione dei tempi conosce il risultato: a fine mese un cliente non riceve una fattura, ma cinque. Ognuna proviene da un altro ordine o ticket, ognuna è corretta dal punto di vista tecnico – e tuttavia il cliente si chiede se non potrebbero stare tutte in un unico documento.
Proprio per questo in Odoo 19 Community non esiste una soluzione pulita. Il noto modulo OCA account_invoice_merge non è ancora rilasciato per Odoo 19 e risolve il problema in un modo che per i fornitori di servizi rappresenta un serio rischio: le righe uguali vengono accorpate e con ciò si perde il collegamento con l'ordine, l'attività e il tempo registrato.
Per la nostra installazione di Odoo 19 Community abbiamo quindi sviluppato il modulo Merge Draft Invoices. Poiché questo problema non riguarda solo noi, lo mettiamo a disposizione gratuitamente su GitHub con la licenza libera AGPL-3.
Al progetto su GitHub:
Perché un modulo dedicato invece della soluzione OCA?
Da molti anni l'OCA mantiene account_invoice_merge nel repository OCA/account-invoicing. Al momento della stesura di questo articolo la migrazione alla 19.0 è lì ancora una pull request aperta, non un modulo pubblicato.
Più importante è però che i due moduli fanno cose diverse:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Risultato | viene unita in una delle fatture selezionate | sempre una nuova fattura |
| Righe | le righe uguali vengono accorpate, le quantità sommate | ogni riga viene riportata uno a uno |
| Origine | invoice_origin / payment_reference concatenati nell'intestazione | campi di origine copiati per ogni riga tramite l'hook _copy_data_extend_business_fields di Odoo |
| Rilevazione dei tempi | non considerata | timesheet_invoice_id viene spostato sulla nuova fattura |
| Leggibile per il cliente | nulla | un'intestazione sopra le righe di ogni origine con ordine e ticket |
| Fatture di origine | vengono annullate | annullare (predefinito), eliminare o mantenere – con conseguenze chiaramente indicate |
L'accorpamento delle righe uguali è la differenza decisiva. Una riga accorpata può avere una sola origine. Non appena due righe di due ordini diversi sembrano uguali, il collegamento dalla fattura all'ordine, all'attività e alla registrazione del tempo va perso. In un'azienda di servizi in cui la fatturazione segue il percorso ticket → rilevazione dei tempi → fattura, questo significa: le stesse ore vengono fatturate due volte oppure spariscono.
Il nostro modulo, quindi, non accorpa mai nulla. La fattura unita ha esattamente tante righe quante ne hanno le sue origini messe insieme.
Non è stato ripreso alcun codice dal modulo OCA. Entrambi i moduli sono sotto AGPL-3.
Cosa fa il modulo
Nella vista elenco delle fatture contrassegnare due o più bozze, scegliere nel menu Azioni Unire le fatture in bozza – una procedura guidata mostra la selezione, i controlli e le opzioni per le fatture di origine. Dopo la conferma si arriva direttamente alla nuova fattura.
Prima dell'unione il modulo esegue dei controlli, e precisamente tutti i punti in una volta sola, così da non dover vedere l'errore successivo dopo ogni correzione:
- selezionate almeno due fatture
- tutte nello stato Bozza
- nessuna è mai stata registrata (un documento riportato in bozza conserva la propria cronologia di registrazione e viene rifiutato)
- nessuna è già stata unita
- tutte sono fattura, fattura fornitore, nota di credito o ricevuta – non semplici scritture contabili
- stesso partner, tipo di documento, azienda, valuta e registro
Le differenze nelle condizioni di pagamento, nella posizione fiscale, nell'indirizzo di consegna, nel conto bancario o nel venditore non bloccano l'operazione. La procedura guidata avvisa e adotta il valore della fattura selezionata più vecchia.
Cosa viene conservato
Il nucleo del modulo è che ogni collegamento che una riga di fattura ha con la propria origine esista anche nella nuova fattura:
| Collegamento | Trattamento |
|---|---|
| Righe dell'ordine di vendita (sale_line_ids) | copiate per ogni riga tramite _copy_data_extend_business_fields di Odoo |
| Righe dell'ordine di acquisto (purchase_line_id) | allo stesso modo |
| Distribuzione dei centri di costo (analytic_distribution) | copiata per ogni riga |
| altri campi di origine per riga | automaticamente, se il modulo fornitore implementa l'hook documentato di Odoo |
| Origine e riferimento nell'intestazione della fattura | unificati, deduplicati, modificabili nella procedura guidata |
| Ordine di vendita: invoice_ids, invoice_status, qty_invoiced | vengono calcolati dalle righe – l'ordine riconosce da solo la nuova fattura |
| Rilevazione dei tempi (timesheet_invoice_id) | viene spostato sulla nuova fattura prima che le origini diventino non valide |
| Ticket helpdesk | è collegato alla rilevazione dei tempi e quindi punta automaticamente alla nuova fattura |
| Chatter | messaggio sulla nuova fattura con tutte le origini, messaggio su ogni origine con link alla nuova |
| merged_into_move_id / merged_from_move_ids | collegamento memorizzato in entrambe le direzioni |
Le righe delle imposte, delle condizioni di pagamento, degli arrotondamenti e dello sconto cassa non vengono copiate, ma ricalcolate da Odoo in base al contenuto. La fattura unita ha quindi una riga di imposta per aliquota, non una per origine. Viene creata come bozza senza numero; l'unione in sé non consuma alcun numero di documento.
Intestazioni: il cliente vede da dove proviene ogni riga
È la funzione che nel lavoro quotidiano fa la differenza. Sopra le righe di ogni fattura di origine il modulo inserisce una riga di sezione – lo stesso elemento che Odoo stesso utilizza per la sezione «Acconti»:
BE10181 <- Intestazione
22,500 x Ora tecnico 3.577,50
Subtotale 3.577,50
BE10182 · Ticket: HT00219 <- Intestazione
3,000 x Ora tecnico 477,00
Subtotale 477,00
IMPORTO NETTO 4.054,50
L'intestazione viene stampata sul documento che il cliente riceve, e il report fattura standard di Odoo aggiunge di suo un subtotale per sezione. Il cliente vede così anche quanto è costata ciascuna parte.
L'etichetta viene composta con ciò che l'origine effettivamente contiene:
- gli ordini, direttamente dai collegamenti delle righe – i nomi sono quindi esatti; se mancano i collegamenti agli ordini, viene usato il testo libero di invoice_origin
- i ticket, ricavati tramite la rilevazione dei tempi – riportati come Ticket: HT00219 oppure Tickets: …, limitati a cinque più «e altri N»
- se non esiste nessuno dei due: il numero della fattura di origine, poi il suo riferimento
- se non esiste proprio nulla: un neutro «Altre righe», in modo che le righe di un'origine non spariscano mai silenziosamente nel blocco sopra
Ciò che manca viene omesso, non inventato. Il ticket viene stampato con il proprio numero – numero helpdesk_mgmt, ticket_ref Enterprise o nome visualizzato del modello – mai con l'ID del database.
Le intestazioni vengono scritte nella lingua del cliente, come fa Odoo quando genera fatture a partire dagli ordini. Il chatter resta nella lingua dell'utente.
Un'intestazione non reca alcun prodotto, quantità, prezzo, imposta, conto né distribuzione dei centri di costo. Non può influire su totali, base imponibile e contabilità analitica. Chi disattiva le intestazioni nella procedura guidata ottiene esattamente la fattura che si avrebbe senza questa funzione – un test confronta i due risultati fino al centesimo.
Cosa succede alle fatture di origine
La procedura guidata offre tre opzioni:
- Annullare (predefinito, consigliato). L'annullamento di una bozza non ha conseguenze: nessun numero di documento, nessuna registrazione. Le fatture restano nel database come documenti annullati con chatter e link alla nuova fattura. qty_invoiced sull'ordine di vendita salta i documenti annullati, nulla viene conteggiato due volte. È l'opzione che resta comprensibile anche dopo sei mesi.
- Eliminare. Irreversibile. Chatter e riferimento inverso vengono eliminati con il record; resta solo il messaggio sulla nuova fattura. Ha senso per bozze usa e getta che nessuno deve più vedere.
- Lasciare come bozza. Le origini restano aperte. I loro ordini di vendita contano allora l'origine e la nuova fattura come fatturate – il lavoro sembra fatturato due volte finché qualcuno non ripulisce le bozze a mano. Pensata per il raro caso in cui si voglia confrontare prima di decidere. Un test registra proprio questo doppio conteggio, affinché non sorprenda nessuno.
Limiti – ciò che il modulo volutamente non fa
Si prega di leggere prima dell'uso in produzione:
- Solo bozze. Le fatture registrate non vengono unite. È un compito da note di credito, non un'operazione di copia.
- Le bozze registrate una volta e poi riportate in bozza vengono rifiutate. Possiedono un numero e una cronologia di registrazione; nasconderli in un altro documento non è qualcosa che un'azione collettiva debba fare in silenzio.
- Nessuna unione tra partner, valute, aziende o registri diversi. Per questi casi non esiste un singolo valore sensato, quindi il modulo rifiuta invece di tirare a indovinare.
- payment_reference non viene ripreso. I riferimenti di pagamento strutturati identificano un documento; due concatenati non corrispondono a nulla presso la banca. La nuova fattura riceve il proprio al momento della registrazione.
- Le righe di acconto vengono copiate come qualsiasi altra riga. Se le origini contengono righe di acconto provenienti da sale, il risultato va verificato prima della registrazione.
- Solo timesheet_invoice_id viene spostato automaticamente. Ogni altro modello con un Many2one su account.move (cicli di fatturazione propri, collegamenti ad abbonamenti, trasmissioni EDI) continua a puntare all'origine. Per questo esiste l'hook _merge_draft_relink.
- Nessun accorpamento delle righe uguali – volutamente. Chi vuole una fattura breve e ordinata qui è nel posto sbagliato e pagherebbe con i collegamenti di origine.
- Le intestazioni vengono create una sola volta al momento dell'unione. Dopodiché sono normali righe di sezione: modificabili, eliminabili e non aggiornate se in seguito l'ordine o il ticket vengono rinominati.
- Un'intestazione riporta i ticket dell'intera fattura di origine, non della singola riga.
Installazione in Odoo 19 Community
L'installazione avviene come per un normale modulo aggiuntivo di Odoo:
- Copiare la cartella del modulo in un percorso addons configurato.
- Aggiornare l'elenco delle app in Odoo.
- Installare il modulo account_invoice_merge_draft.
Il modulo dipende solo da account. Tutto ciò che sa su sale, purchase, project, hr_timesheet, sale_timesheet o un modulo helpdesk viene rilevato a runtime – si installa e funziona anche su un database di sola fatturazione.
Poi: Contabilità → Clienti → Fatture (oppure Fornitori → Fatture), contrassegnare due o più bozze, menu a ingranaggio → Unire le fatture in bozza. L'azione è a disposizione di tutti gli utenti con il gruppo Fatturazione.
Chi aggiorna da una versione precedente del modulo dovrebbe eseguire una sola volta l'aggiornamento con -u account_invoice_merge_draft --i18n-overwrite, affinché le etichette di campo modificate arrivino nella traduzione tedesca. Una nuova installazione non è interessata.
Per gli sviluppatori: API e hook
L'unione può essere richiamata anche dal proprio codice:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" oppure "keep"
add_source_sections=True, # intestazioni; predefinito True
)
merge_draft_moves genera un UserError con tutti i motivi di rifiuto se la selezione non può essere unita. _merge_draft_get_errors() restituisce gli stessi motivi come elenco senza eccezione.
Chi ha propri riferimenti inversi verso account.move sovrascrive _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}
)
Chi ha un riferimento inverso sulla riga di fattura non ha affatto bisogno di questo modulo: l'hook di Odoo account.move.line._copy_data_extend_business_fields è sufficiente e il campo viene ripreso allo stesso modo in caso di unione, annullamento e duplicazione. Per altri moduli helpdesk o per una diversa formulazione delle intestazioni esistono _merge_draft_get_source_tickets() e _merge_draft_section_name().
Testato prima di entrare in funzione su dati di produzione
48 test coprono stati misti, partner, valute e tipi di documento diversi, righe con e senza ordine di vendita o di acquisto, imposte, sezioni e note, distribuzioni dei centri di costo, lo spostamento della rilevazione dei tempi, tutte e tre le opzioni per le fatture di origine, la procedura guidata e le intestazioni – che siano attive per impostazione predefinita, che la loro disattivazione riproduca il vecchio risultato al centesimo, che un'intestazione non rechi importi o conti, che le righe di un'origine restino insieme, che il ticket compaia con il proprio numero e mai con l'ID del database.
I dati di test si basano sul piano dei conti esistente dell'azienda attiva anziché su AccountTestInvoicingCommon – questo helper spesso non si avvia su database di produzione migrati, ed è proprio su questi che il modulo deve funzionare.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Come per ogni estensione in ambito contabile, consigliamo di verificare l'installazione prima in un ambiente di test con una copia del proprio database.
Gratuito e open source per la community di Odoo
In Biricon IT Services GmbH sviluppiamo soluzioni innanzitutto per esigenze concrete della nostra attività. Se ne nasce un modulo utilizzabile in generale, non vogliamo nasconderlo in una singola installazione.
Merge Draft Invoices è quindi disponibile gratuitamente come progetto open source. Il codice sorgente può essere consultato, verificato, adattato e sviluppato ulteriormente nell'ambito della licenza AGPL-3.
Il nostro obiettivo non era la fattura più breve possibile, ma una fattura in cui anche tra un anno ogni riga riconduca ancora al proprio ordine, al proprio ticket e alla propria registrazione del tempo.
Progetto, codice sorgente e collaborazione
Il codice sorgente completo, le istruzioni di installazione e la documentazione tecnica sono disponibili su GitHub:
Apri Odoo 19 Merge Draft Invoices su GitHub
Ci fa piacere ricevere esperienze pratiche, segnalazioni di errori, suggerimenti di miglioramento e contributi dalla community di Odoo.
Gratuito. Tracciabile. Open source. Per Odoo 19 Community.