Quem fatura no Odoo a partir de encomendas, tickets de helpdesk e registo de tempos conhece o resultado: no fim do mês, um cliente não recebe uma fatura, mas cinco. Cada uma provém de uma encomenda ou ticket diferente, cada uma está tecnicamente correta – e, mesmo assim, o cliente pergunta se isso não caberia num único documento.
É precisamente para isto que não existe uma solução limpa no Odoo 19 Community. O conhecido módulo OCA account_invoice_merge ainda não está disponível para o Odoo 19 e resolve o problema de uma forma que representa um risco sério para prestadores de serviços: as linhas iguais são agrupadas e, com isso, perde-se a ligação à encomenda, à tarefa e ao tempo registado.
Para a nossa própria instalação Odoo 19 Community, desenvolvemos por isso o módulo Merge Draft Invoices. Como este problema não afeta apenas a nós, disponibilizamo-lo gratuitamente no GitHub, sob a licença livre AGPL-3.
Ir para o projeto no GitHub:
Porquê um módulo próprio em vez da solução OCA?
A OCA mantém há muitos anos o account_invoice_merge no repositório OCA/account-invoicing. À data desta publicação, a migração para a 19.0 é aí ainda um pull request em aberto, não um módulo publicado.
Mais importante, porém, é que os dois módulos fazem coisas diferentes:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Resultado | é unida a uma das faturas selecionadas | sempre uma fatura nova |
| Linhas | as linhas iguais são agrupadas, as quantidades somadas | cada linha é transferida um para um |
| Origem | invoice_origin / payment_reference concatenados no cabeçalho | campos de origem copiados por linha através do próprio hook _copy_data_extend_business_fields do Odoo |
| Registo de tempos | não considerado | timesheet_invoice_id é transferido para a nova fatura |
| Legível para o cliente | nada | um título acima das linhas de cada origem, com encomenda e ticket |
| Faturas de origem | são canceladas | cancelar (predefinição), eliminar ou manter – com consequências claramente descritas |
O agrupamento de linhas iguais é a diferença decisiva. Uma linha agrupada só pode ter uma origem. Assim que duas linhas de duas encomendas diferentes têm o mesmo aspeto, perde-se a ligação da fatura de volta à encomenda, à tarefa e ao lançamento de tempo. Numa empresa de serviços em que a faturação decorre através de ticket → registo de tempos → fatura, isto significa: as mesmas horas são faturadas em duplicado ou desaparecem.
O nosso módulo não agrupa, por isso, nada, por princípio. A fatura unida tem exatamente tantas linhas quantas as suas origens somadas.
Não foi aproveitado código do módulo OCA. Ambos os módulos estão sob AGPL-3.
O que o módulo faz
Na vista de lista de faturas, marcar dois ou mais rascunhos e escolher, no menu de ações, Unir faturas em rascunho – um assistente mostra a seleção, as verificações e as opções para as faturas de origem. Após a confirmação, vai-se diretamente para a nova fatura.
Antes de unir, o módulo verifica, e fá-lo para todos os pontos de uma só vez, para que não se veja o erro seguinte após cada correção:
- pelo menos duas faturas selecionadas
- todas no estado Rascunho
- nenhuma foi alguma vez lançada (um documento reposto em rascunho mantém o seu histórico de lançamentos e é rejeitado)
- nenhuma foi já unida
- todas são fatura, fatura de fornecedor, nota de crédito ou recibo – não meros lançamentos contabilísticos
- mesmo parceiro, tipo de documento, empresa, moeda e diário
Diferenças em condição de pagamento, posição fiscal, endereço de entrega, conta bancária ou vendedor não bloqueiam. O assistente avisa e adota o valor da fatura selecionada mais antiga.
O que se mantém
O cerne do módulo é que cada ligação que uma linha de fatura tem à sua origem também existe na nova fatura:
| Ligação | Tratamento |
|---|---|
| Linhas de encomenda de venda (sale_line_ids) | copiadas por linha através de _copy_data_extend_business_fields do Odoo |
| Linhas de encomenda de compra (purchase_line_id) | o mesmo |
| Distribuição analítica (analytic_distribution) | copiada por linha |
| outros campos de origem por linha | automaticamente, quando o módulo fornecedor implementa o hook documentado do Odoo |
| Origem e referência no cabeçalho da fatura | unidas, sem duplicados, editáveis no assistente |
| Encomenda de venda: invoice_ids, invoice_status, qty_invoiced | são calculados a partir das linhas – a encomenda reconhece a nova fatura por si própria |
| Registo de tempos (timesheet_invoice_id) | é transferido para a nova fatura antes de as origens se tornarem inválidas |
| Ticket de helpdesk | está associado ao registo de tempos e, assim, aponta automaticamente para a nova fatura |
| Chatter | mensagem na nova fatura com todas as origens, mensagem em cada origem com ligação para a nova |
| merged_into_move_id / merged_from_move_ids | ligação guardada nos dois sentidos |
As linhas de imposto, as linhas de condição de pagamento, as linhas de arredondamento e de desconto por pronto pagamento não são copiadas, mas recalculadas pelo Odoo a partir do conteúdo. A fatura unida tem, portanto, uma linha de imposto por taxa de imposto, e não uma por origem. É criada como rascunho sem numeração; a própria união não consome qualquer número de documento.
Títulos: o cliente vê de onde provém cada linha
Esta é a função que faz a diferença no dia a dia. Acima das linhas de cada fatura de origem, o módulo insere uma linha de secção – o mesmo elemento que o próprio Odoo utiliza para a secção “Adiantamentos”:
BE10181 <- Título
22,500 x Hora de técnico 3.577,50
Subtotal 3.577,50
BE10182 · Ticket: HT00219 <- Título
3,000 x Hora de técnico 477,00
Subtotal 477,00
MONTANTE LÍQUIDO 4.054,50
O título é impresso no documento que o cliente recebe e o relatório de fatura padrão do Odoo acrescenta, por si só, um subtotal por secção. O cliente vê assim também quanto custou cada parte.
A legenda é composta a partir do que a origem realmente tem:
- as encomendas, diretamente a partir das ligações das linhas – os nomes são, assim, exatos; se faltarem ligações a encomendas, é utilizado o texto livre de invoice_origin
- os tickets, determinados através do registo de tempos – apresentados como Ticket: HT00219 ou Tickets: …, limitados a cinco mais “e N outros”
- se não existir nenhum dos dois: o número da fatura de origem e, depois, a sua referência
- se não existir absolutamente nada: um neutro “Outras linhas”, para que as linhas de uma origem nunca desapareçam silenciosamente no bloco acima
O que falta é omitido, não inventado. O ticket é impresso com o seu próprio número – número do helpdesk_mgmt, ticket_ref do Enterprise ou o nome de apresentação do modelo – nunca com o ID da base de dados.
Os títulos são escritos no idioma do cliente, tal como o Odoo faz ao gerar faturas a partir de encomendas. O chatter mantém-se no idioma do utilizador.
Um título não tem produto, quantidade, preço, imposto, conta nem distribuição analítica. Não pode influenciar totais, base tributável nem contabilidade analítica. Quem desativar os títulos no assistente obtém exatamente a fatura que resultaria sem esta função – um teste compara os dois resultados até ao cêntimo.
O que acontece às faturas de origem
O assistente oferece três opções:
- Cancelar (predefinição, recomendado). Cancelar um rascunho não tem consequências: sem número de documento, sem lançamento. As faturas permanecem na base de dados como documentos cancelados, com chatter e ligação para a nova fatura. O qty_invoiced na encomenda de venda ignora os documentos cancelados, nada é contado em duplicado. É a opção que continua a ser rastreável mesmo daqui a seis meses.
- Eliminar. Irreversível. O chatter e a referência de retorno desaparecem com o registo; só a mensagem na nova fatura permanece. Útil para rascunhos descartáveis que ninguém deve voltar a ver.
- Manter como rascunho. As origens permanecem em aberto. As respetivas encomendas de venda contam então a origem e a nova fatura como faturadas – o trabalho parece faturado em duplicado até alguém limpar manualmente os rascunhos. Pensado para o caso raro em que se pretende comparar antes de decidir. Um teste regista precisamente esta dupla contagem, para que não surpreenda ninguém.
Limites – o que o módulo deliberadamente não faz
Por favor, ler antes da utilização em produção:
- Apenas rascunhos. As faturas lançadas não são unidas. Isso é uma tarefa de notas de crédito, não um processo de cópia.
- Rascunhos já lançados uma vez e repostos são rejeitados. Possuem um número e um histórico de lançamentos; escondê-lo noutro documento não é algo que uma ação em lote deva fazer silenciosamente.
- Sem união entre parceiros, moedas, empresas ou diários diferentes. Não existe para isso nenhum valor único com sentido, pelo que o módulo recusa em vez de adivinhar.
- payment_reference não é transferida. As referências de pagamento estruturadas identificam um documento; duas concatenadas não correspondem a nada no banco. A nova fatura recebe a sua própria ao ser lançada.
- As linhas de adiantamento são copiadas como qualquer outra linha. Se as origens contiverem linhas de adiantamento do sale, o resultado deve ser verificado antes do lançamento.
- Apenas timesheet_invoice_id é transferido automaticamente. Qualquer outro modelo com um Many2one para account.move (execuções de faturação próprias, ligações de subscrições, envios EDI) continua a apontar para a origem. Para isso existe o hook _merge_draft_relink.
- Sem agrupamento de linhas iguais – de propósito. Quem quer uma fatura curta e arrumada está no sítio errado e pagaria por isso com as ligações de origem.
- Os títulos são criados uma única vez, ao unir. Depois, são linhas de secção normais: podem ser editados e eliminados e não são atualizados se a encomenda ou o ticket forem renomeados mais tarde.
- Um título indica os tickets de toda a fatura de origem, não os da linha individual.
Instalação no Odoo 19 Community
A instalação é feita como num módulo adicional Odoo habitual:
- Copiar o diretório do módulo para um caminho de addons configurado.
- Atualizar a lista de aplicações no Odoo.
- Instalar o módulo account_invoice_merge_draft.
O módulo depende apenas de account. Tudo o que sabe sobre sale, purchase, project, hr_timesheet, sale_timesheet ou um módulo de helpdesk é determinado em tempo de execução – instala-se e funciona também numa base de dados puramente de faturação.
Depois: Contabilidade → Clientes → Faturas (ou Fornecedores → Faturas), marcar dois ou mais rascunhos, menu da roda dentada → Unir faturas em rascunho. A ação está disponível para todos os utilizadores com o grupo Faturação / Faturação.
Quem atualiza a partir de uma versão anterior do módulo deve fazer, uma única vez, a atualização com -u account_invoice_merge_draft --i18n-overwrite, para que as designações de campo alteradas cheguem à tradução alemã. Uma nova instalação não é afetada.
Para programadores: API e hooks
A união também pode ser invocada a partir de código próprio:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" ou "keep"
add_source_sections=True, # títulos; predefinição True
)
merge_draft_moves lança um UserError com todos os motivos de rejeição, se a seleção não puder ser unida. _merge_draft_get_errors() devolve os mesmos motivos como lista, sem exceção.
Quem tiver referências de retorno próprias para account.move substitui _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}
)
Quem tiver uma referência de retorno na linha da fatura não precisa sequer deste módulo: o hook do próprio Odoo account.move.line._copy_data_extend_business_fields é suficiente, e o campo é transferido da mesma forma ao unir, ao cancelar e ao duplicar. Para outros módulos de helpdesk ou outra formulação dos títulos existem _merge_draft_get_source_tickets() e _merge_draft_section_name().
Testado antes de funcionar com dados de produção
48 testes cobrem estados mistos, parceiros, moedas e tipos de documento diferentes, linhas com e sem encomenda de venda ou de compra, impostos, secções e notas, distribuições analíticas, a transferência do registo de tempos, as três opções para as faturas de origem, o assistente e os títulos – que estão ativos por predefinição, que desativá-los reproduz o resultado antigo até ao cêntimo, que um título não tem montantes nem contas, que as linhas de uma origem permanecem juntas, que o ticket aparece com o seu número e nunca com o ID da base de dados.
Os dados de teste assentam no plano de contas existente da empresa ativa, em vez de AccountTestInvoicingCommon – este helper muitas vezes não arranca em bases de dados de produção migradas, e é precisamente nelas que o módulo deve funcionar.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Como em qualquer extensão na área da contabilidade, recomendamos verificar primeiro a instalação num ambiente de teste, com uma cópia da própria base de dados.
Gratuito e open source para a Odoo Community
Na Biricon IT Services GmbH, desenvolvemos soluções, em primeiro lugar, para requisitos concretos da nossa própria operação. Quando daí resulta um módulo de utilização geral, não queremos escondê-lo numa única instalação.
O Merge Draft Invoices está, por isso, disponível gratuitamente como projeto open source. O código-fonte pode ser consultado, verificado, adaptado e desenvolvido no âmbito da licença AGPL-3.
O nosso objetivo não era a fatura mais curta possível, mas uma fatura em que, mesmo daqui a um ano, cada linha conduza de volta à sua encomenda, ao seu ticket e ao seu lançamento de tempo.
Projeto, código-fonte e colaboração
O código-fonte completo, as instruções de instalação e a documentação técnica estão disponíveis no GitHub:
Abrir o Odoo 19 Merge Draft Invoices no GitHub
Agradecemos experiências práticas, relatórios de erros, sugestões de melhoria e contributos da Odoo Community.
Gratuito. Rastreável. Open source. Para o Odoo 19 Community.