Quem vende pacotes no Odoo, por exemplo um aparelho de medição com acessórios e montagem, um posto de trabalho com hardware e configuração, ou um pacote de manutenção composto por vários serviços, precisa de duas coisas em simultâneo no orçamento: a composição tem de constar da encomenda, para que o armazém, a equipa técnica e a faturação saibam o que é fornecido. Na impressão para o cliente, porém, nem sempre deve aparecer cada posição individual com cada preço individual.
O módulo OCA sale_product_set permite inserir conjuntos de produtos (listas de materiais para a venda) num orçamento com um só clique. Não decide, porém, sobre o aspeto da impressão. Para isso desenvolvemos o módulo Sale Product Set Layout (sale_product_set_layout). Está escrito para o Odoo 19 e está disponível gratuitamente no GitHub como software livre sob a licença AGPL-3:
github.com/Alex0176/odoo-sale-product-set-layout
Conteúdo deste artigo
- O problema: conjuntos de produtos sem influência no aspeto da impressão
- O que o módulo faz
- Os três modos de impressão em pormenor
- Linhas ocultas e preço fixo
- Implementação técnica
- Instalação sem efeitos secundários indesejados
- Utilização passo a passo
- Testes
- Limitações e aviso legal
- Perguntas frequentes
- Apoio da Biricon IT Services
O problema: conjuntos de produtos sem influência no aspeto da impressão
No Odoo, um conjunto de produtos é uma lista preparada de produtos com quantidades. Em vez de procurar dez artigos um a um, escolhe-se no orçamento Adicionar conjunto e obtêm-se todas as linhas de uma só vez. Isto poupa tempo e evita posições esquecidas. O módulo sale_product_set do repositório OCA/sale-workflow faz precisamente isso; os próprios conjuntos provêm de product_set, no repositório OCA/product-attribute.
Na prática, subsistem três lacunas:
- Nenhum agrupamento visível. As linhas do conjunto ficam no orçamento como posições soltas. Depois de inseridas, já não se vê quais as linhas que pertencem umas às outras, e o total do conjunto não consta de lado nenhum.
- Nenhuma influência na impressão. Umas vezes o cliente deve ver cada posição com preço, outras vezes apenas o preço do pacote, outras ainda uma única linha. Esta é uma decisão por conjunto ou por cliente, não algo a fazer de novo à mão em cada orçamento.
- As correções de preço são desagradáveis. Se um pacote deve ter um preço fixo que difere da soma dos preços de tabela, é necessária uma linha de correção. Esta pertence ao cálculo, mas não necessariamente ao papel do cliente.
O Odoo 19 traz os componentes para resolver isto de forma limpa: secções e subsecções nas linhas de orçamento com as opções Ocultar preços e Ocultar composição (campos collapse_prices e collapse_composition). O módulo liga o conjunto de produtos a estas opções padrão, em vez de inventar um formato de impressão próprio.
O que o módulo faz
- Inserir o conjunto como secção. O assistente Adicionar conjunto cria uma linha de secção com o nome do conjunto, por exemplo 2 × Kit de medição Standard, e coloca as linhas do conjunto por baixo. As secções dentro do conjunto passam a subsecções.
- Modo de impressão por conjunto. No conjunto define-se uma só vez como é impresso. Ao inserir, o modo pode ser alterado no assistente para esse caso concreto.
- Ocultar linhas. Linhas individuais podem ser marcadas como Não imprimir (no conjunto: Oculto). Contam para o total, mas não são impressas nem apresentadas no portal do cliente. A marcação passa do orçamento para a fatura.
- Preço fixo por linha do conjunto. Uma linha do conjunto pode ter um preço unitário fixo que substitui o preço da lista de preços, por exemplo para uma correção de preço oculta.
Sem o módulo (apenas sale_product_set) | Com sale_product_set_layout |
|---|---|
| Linhas do conjunto como posições soltas | Linha de secção com o nome do conjunto, quantidade no nome, linhas por baixo |
| As secções no conjunto continuam a ser secções e quebram a estrutura | As secções no conjunto passam a subsecções (line_subsection) |
| Aspeto de impressão sempre igual | Modo de impressão por conjunto, alterável ao inserir |
| Correção de preço visível no orçamento e na fatura | Linha ocultável, mas conta para o total |
| Preço apenas a partir da lista de preços | Preço fixo opcional por linha do conjunto |
| O layout termina no orçamento | O layout e a ocultação aplicam-se ao orçamento, à encomenda, à fatura e ao portal do cliente |
Os três modos de impressão em pormenor
O modo de impressão é um campo de seleção print_mode no conjunto de produtos. Ao inserir, é convertido nos dois interruptores padrão da linha de secção. Para isso, o módulo contém um único mapeamento, fácil de ler:
# Modo de impressão -> (collapse_prices, collapse_composition) da linha de secção
PRINT_MODE_FLAGS = {
"detailed": (False, False),
"total_only": (True, False),
"summary": (False, True),
}
Modo (print_mode) | Apresentação no conjunto | Opção padrão no Odoo | Aspeto de impressão |
|---|---|---|---|
detailed | Cada linha com preço | nenhuma | Secção, todas as linhas com montante |
total_only | Linhas sem preços, apenas o total do conjunto | collapse_prices (Ocultar preços) | Secção com total, linhas apenas com designação e quantidade |
summary | Apenas uma linha com o total do conjunto | collapse_composition (Ocultar composição) | Apenas a linha de secção com o total |
Como o módulo utiliza as opções padrão do Odoo 19 e não um formato de impressão próprio, o orçamento, a confirmação de encomenda, a fatura e o portal do cliente funcionam sem modelos de relatório personalizados. Quem tiver modelos de relatório próprios que tenham em conta as opções de secção obtém igualmente este comportamento.
Linhas ocultas e preço fixo
Um caso frequente: um pacote tem um preço de pacote inferior à soma dos preços individuais. Em vez de distorcer os preços individuais, utiliza-se uma linha de correção com preço fixo (por exemplo −50,00 € por conjunto) e oculta-se. No backend, a fatura é rastreável; na impressão consta o preço do pacote.
Importante para a compreensão: a marcação hide_in_report não altera nada no cálculo. Quantidade, preço, desconto, imposto, totais e lançamentos contabilísticos resultam inalterados do Odoo padrão. A linha apenas falta na impressão e no portal.
- No conjunto de produtos: linha do conjunto com Oculto e, opcionalmente, Preço fixo com Preço unitário.
- Nas linhas de orçamento e de fatura: coluna Não imprimir (
hide_in_report), também para linhas normais sem conjunto. - Transmissão: ao criar a fatura a partir da encomenda, a marcação é transferida para a linha da fatura. Ao copiar um orçamento, é mantida.
Implementação técnica
O módulo é deliberadamente pequeno: estende modelos existentes e substitui apenas os pontos em que o Odoo decide que linhas são impressas e como um conjunto é inserido. Não há novos modelos de relatório, nem novos modelos de dados, nem intervenções em SQL.
Modelos e campos estendidos
| Modelo | Complemento |
|---|---|
product.set | Campos insert_as_section (booleano, ativo por predefinição) e print_mode (seleção, obrigatório, predefinição detailed) |
product.set.line | Campos hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() transmite-os à linha de orçamento |
sale.product.set.wizard | Campos insert_as_section, section_name, print_mode; _prepare_order_lines() constrói a linha de secção |
sale.order / sale.order.line | Campo hide_in_report (com copy=True); _get_order_lines_to_report() ignora as linhas ocultas; _prepare_invoice_line() transmite a marcação |
account.move / account.move.line | Campo hide_in_report; _get_move_lines_to_report(), _get_detailed_lines() e _get_subsection_lines() filtram as linhas ocultas; _get_aml_vals() inclui a marcação nos dados do relatório |
Ocultar no orçamento
O Odoo 19 determina as linhas de orçamento a imprimir em _get_order_lines_to_report(). O módulo acrescenta aí um filtro. Não é necessária qualquer intervenção no modelo de relatório: tanto o modelo de relatório do orçamento (sale/report/ir_actions_report_templates.xml) como o modelo do portal (sale/views/sale_portal_templates.xml) invocam este método:
class SaleOrder(models.Model):
_inherit = "sale.order"
def _get_order_lines_to_report(self):
return super()._get_order_lines_to_report().filtered(
lambda line: not line.hide_in_report
)
Transmissão para a fatura
Ao gerar a linha de fatura a partir da linha de orçamento (_prepare_invoice_line()), a marcação é copiada. Do lado da fatura, o Odoo 19 tem vários caminhos pelos quais as linhas chegam ao relatório: a lista principal, as linhas de uma secção e as linhas de uma subsecção. Por isso, o módulo filtra em vários pontos (_get_move_lines_to_report() no documento, _get_detailed_lines() e _get_subsection_lines() na linha); caso contrário, uma linha oculta poderia reaparecer por um dos outros caminhos:
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
hide_in_report = fields.Boolean(string="Hidden in report")
def _get_aml_vals(self, hide_taxes):
res = super()._get_aml_vals(hide_taxes)
res["hide_in_report"] = self.hide_in_report
return res
def _get_detailed_lines(self, children_lines, direct_children_lines, subsection_lines):
res = super()._get_detailed_lines(children_lines, direct_children_lines, subsection_lines)
return [vals for vals in res if not vals.get("hide_in_report")]
def _get_subsection_lines(self, subsection_line, children_lines):
res = super()._get_subsection_lines(subsection_line, children_lines)
return [vals for vals in res if not vals.get("hide_in_report")]
O assistente: secção em vez de linhas soltas
O assistente sale.product.set.wizard de sale_product_set gera as linhas de orçamento em _prepare_order_lines(). O módulo substitui este método. Se Inserir como secção estiver desativado, limita-se a invocar o método original, e o comportamento mantém-se como no módulo OCA. Caso contrário, é criada primeiro a linha de secção com os dois interruptores padrão e, a seguir, as linhas do conjunto. As secções dentro do conjunto passam a subsecções, para não quebrarem a estrutura principal:
def _prepare_order_lines(self):
if not self.insert_as_section:
return super()._prepare_order_lines()
sequence = self._get_max_sequence() + 1
collapse_prices, collapse_composition = PRINT_MODE_FLAGS[self.print_mode or "detailed"]
order_lines = [Command.create({
"display_type": "line_section",
"name": self.section_name or self.product_set_id.name,
"sequence": sequence,
"collapse_prices": collapse_prices,
"collapse_composition": collapse_composition,
})]
for set_line in self._get_lines():
sequence += 1
values = set_line.prepare_sale_order_line_values(self.order_id, self.quantity, sequence)
if values.get("display_type") == "line_section":
values["display_type"] = "line_subsection"
order_lines.append(Command.create(values))
return order_lines
Decisões que não se veem
- Métodos de cálculo separados no assistente. O nome da secção, o modo de impressão e Inserir como secção têm, cada um, o seu próprio método
compute(store=True,readonly=False). Se os três partilhassem o mesmo método, um valor passado explicitamente para um campo repunha os outros. Isto nota-se sobretudo quando o assistente é criado com valores a partir de código ou de testes. - Nome da secção com quantidade. Quando a quantidade é diferente de 1, consta do nome (2 × Kit de medição Standard, com vírgula como separador decimal), para que na impressão se perceba que são dois conjuntos e não um conjunto com o dobro da quantidade.
- Preço fixo apenas em linhas de produto.
prepare_sale_order_line_values()definehide_in_reporteprice_unitapenas quando a linha do conjunto não é uma linha de secção ou de nota. - Odoo padrão em vez de solução própria. O modo de impressão e a ocultação utilizam ganchos e campos existentes do Odoo 19. Isto torna as atualizações controláveis: se o Odoo alterar o modelo de relatório, o módulo continua válido enquanto os métodos existirem.
Instalação sem efeitos secundários indesejados
Requisitos: Odoo 19.0, sale_product_set (OCA/sale-workflow, ramo 19.0) e product_set (OCA/product-attribute, ramo 19.0). O próprio módulo depende de sale_management, sale_product_set e account.
A armadilha do auto_install nos módulos OCA. Muitos repositórios OCA contêm módulos com auto_install. Assim que as respetivas dependências estão presentes, o Odoo instala-os sem perguntar na instalação ou atualização seguinte. Quem, por isso, colocar o repositório OCA completo no addons_path de uma instância de produção arrisca-se a ficar com módulos que ninguém queria. No caso de OCA/product-attribute, isto afeta, por exemplo, purchase_product_template_tags. O nosso procedimento:
- Clonar superficialmente os dois repositórios OCA (
git clone --depth 1 --branch 19.0). - Num diretório próprio, ligar por symlink apenas os dois módulos necessários e incluir esse diretório no
addons_path, não os repositórios inteiros. - Instalar numa base de dados de teste e verificar que módulos passam a ter o estado instalado.
- Só então instalar em produção, com
--workers=0, para que a chamada não dispute a porta HTTP com o serviço em execução.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init
Os novos campos e vistas ficam na base de dados após a instalação. Se o serviço funcionar em modo multiprocesso, os workers recarregam o registo alterado através do sinal da base de dados. As alterações aos ficheiros Python do módulo só entram em vigor após o reinício do serviço.
Utilização passo a passo
- Criar um conjunto: Vendas → Produtos → Conjuntos de produtos. Dar um nome ao conjunto, escolher Inserir como secção e Modo de impressão, introduzir os produtos com quantidades. Atribuir Oculto e Preço fixo às linhas de correção.
- No orçamento: clicar em Adicionar conjunto, escolher o conjunto e a quantidade. O nome da secção e o modo de impressão podem ser alterados antes da inserção.
- Ocultar linhas individuais: no orçamento ou na fatura, ativar a coluna Não imprimir.
- Verificar: abrir a pré-visualização ou o PDF. Os clientes veem a mesma apresentação no portal.
Testes
O módulo inclui seis testes automatizados, que executamos contra uma base de dados de teste antes de passar o que quer que seja para produção:
- Inserção como secção, incluindo modo de impressão e subsecção
- A linha oculta não consta das linhas a imprimir
- Modo Uma linha (
summary) - Inserção sem secção, o comportamento original
- A fatura assume a marcação
hide_in_report, a linha não consta de_get_move_lines_to_report(), o total da secção continua a incluí-la - A fatura no modo Apenas preço total não mostra a linha oculta nas linhas da secção, o total da secção permanece completo
odoo-bin -d <testdatenbank> -i sale_product_set_layout \
--test-enable --test-tags=/sale_product_set_layout --stop-after-init
A tradução alemã encontra-se em i18n/de.po.
Limitações e aviso legal
- Apenas layout. O módulo não altera a forma como o Odoo calcula preços, descontos ou impostos. Altera apenas o que é impresso e apresentado no portal.
- As linhas ocultas são visíveis no backend. A marcação diz respeito à impressão e ao portal, não ao ecrã dos colaboradores.
- Verificar os dados da fatura. As linhas ocultas contam para o total, mas não constam do documento. Se isto é compatível com os requisitos aplicáveis às faturas no seu país, por exemplo com a descrição obrigatória dos bens fornecidos ou dos serviços prestados, deve esclarecê-lo com o seu consultor fiscal antes de utilizar em faturas. Nos orçamentos, a ocultação é menos problemática. O artigo não constitui aconselhamento jurídico.
- Testado para o Odoo 19. Desenvolvemos e testámos o módulo para o Odoo 19.0 Community. Utiliza opções de secção que existem desta forma no Odoo 19. Não o verificámos para outras versões.
Perguntas frequentes
Para que preciso do módulo se o Odoo 19 já suporta secções?
As opções de secção existem no próprio Odoo. Faltava a ligação ao conjunto de produtos: um conjunto devia ser inserido automaticamente como secção, com o modo de impressão definido no conjunto. Sem o módulo, é preciso criar e configurar manualmente a linha de secção após cada inserção.
Qual é a diferença entre «Ocultar preços» e «Ocultar composição»?
«Ocultar preços» (collapse_prices) mantém as linhas, mas não mostra montantes, apenas o total na secção. «Ocultar composição» (collapse_composition) oculta as linhas por completo, e o cliente vê apenas a secção com o total.
Uma linha oculta conta para o total da fatura?
Sim. As linhas ocultas permanecem no total, no imposto e no lançamento contabilístico. A marcação diz respeito apenas à apresentação na impressão e no portal do cliente.
Posso ainda alterar o modo de impressão ao inserir?
Sim. O modo vem como predefinição do conjunto e pode ser alterado no assistente para a inserção em causa. O próprio conjunto permanece inalterado.
O que acontece se desativar Inserir como secção?
Nesse caso, o assistente comporta-se como no módulo OCA sale_product_set: as linhas do conjunto são inseridas sem secção.
O módulo instala inesperadamente outros módulos?
O próprio módulo não. Nos repositórios OCA, porém, pode haver módulos auto_install se se colocarem repositórios inteiros no addons_path. Por isso, ligue apenas os dois módulos necessários (ver a secção Instalação).
O módulo funciona com o Odoo Enterprise?
O módulo depende apenas de módulos Community (sale_management, account) e dos dois módulos OCA. Utilizamo-lo no Odoo 19 Community. No Enterprise, não o testámos.
Sob que licença está o módulo?
Sob a AGPL-3. Pode utilizá-lo, adaptá-lo e distribuí-lo, desde que as alterações permaneçam sob a mesma licença. Erros e sugestões de melhoria são bem-vindos como issue no repositório GitHub .
Apoio da Biricon IT Services
Damos assistência a instalações Odoo para pequenas e médias empresas: da implementação, passando por adaptações e migrações, até à operação corrente. Se pretende adaptar conjuntos de produtos, modelos de orçamento ou relatórios no Odoo 19 aos seus processos, ou se quer verificar um módulo como este e colocá-lo em segurança na sua instância de produção, teremos todo o gosto em apoiá-lo. Pode encontrar outros desenvolvimentos próprios no nosso blogue, por exemplo Unir faturas rascunho e Atribuir automaticamente pagamentos bancários a faturas.
Situação em: 2 de novembro de 2026. Versão do módulo 19.0.1.0.0. Os dados de exemplo nas figuras são de livre escolha, montantes líquidos.