Ir al contenido

Odoo 19 Community: conjuntos de productos como secciones en los presupuestos con nuestro módulo de código abierto Sale Product Set Layout

2 de noviembre de 2026 | Odoo 19 Community | Desarrollo propio
2 de noviembre de 2026 por
Odoo 19 Community: conjuntos de productos como secciones en los presupuestos con nuestro módulo de código abierto Sale Product Set Layout

Módulo Sale Product Set Layout para Odoo 19: conjuntos de productos como sección del presupuesto, con modo de impresión, líneas ocultas y precio fijo

Quienes venden paquetes en Odoo, como un equipo de medición con accesorios e instalación, un puesto de trabajo con hardware y configuración o un paquete de mantenimiento que incluye varios servicios, necesitan dos cosas a la vez en el presupuesto: la composición del paquete debe figurar en el pedido para que el almacén, el equipo técnico y el departamento de facturación sepan qué se va a entregar. Sin embargo, en la versión impresa para el cliente no siempre conviene mostrar cada partida con su precio individual.

El módulo de OCA sale_product_set permite añadir conjuntos de productos (listas de materiales para ventas) a un presupuesto con un clic. No determina cómo se presentan al imprimirlos. Para eso hemos desarrollado el módulo Sale Product Set Layout (sale_product_set_layout). Está diseñado para Odoo 19 y está disponible gratuitamente en GitHub como software libre bajo la licencia AGPL-3:

github.com/Alex0176/odoo-sale-product-set-layout

El problema: conjuntos de productos sin control sobre la presentación impresa

En Odoo, un conjunto de productos es una lista predefinida de productos con sus cantidades. En lugar de buscar diez artículos uno por uno, basta con seleccionar Añadir conjunto en el presupuesto para incorporar todas las líneas de una vez. Así se ahorra tiempo y se evita olvidar artículos. El módulo sale_product_set del repositorio OCA/sale-workflow hace precisamente eso; los propios conjuntos proceden de product_set, del repositorio OCA/product-attribute.

En la práctica, quedan tres carencias:

  • No hay una agrupación clara. Las líneas del conjunto se añaden al presupuesto como líneas independientes. Una vez insertadas, ya no se distingue cuáles pertenecen al mismo conjunto, y el importe total del conjunto no aparece en ninguna parte.
  • No hay control sobre la impresión. A veces interesa que el cliente vea cada línea con su precio; otras, solo el precio del paquete o una única línea. Es una decisión que debe poder definirse por conjunto o por cliente, sin tener que configurarla manualmente en cada presupuesto.
  • Los ajustes de precio quedan poco elegantes. Si un paquete debe tener un precio fijo distinto de la suma de los precios de tarifa, hace falta una línea de ajuste. Esta debe intervenir en el cálculo, pero no necesariamente aparecer en el documento que recibe el cliente.

Odoo 19 incluye los elementos necesarios para resolverlo de forma sencilla: secciones y subsecciones en las líneas del presupuesto, con las opciones Ocultar precios y Ocultar composición (campos collapse_prices y collapse_composition). El módulo conecta el conjunto de productos con estas opciones estándar, en lugar de crear un formato de impresión propio.

Qué hace el módulo

  1. Insertar el conjunto como sección. El asistente Añadir conjunto crea una línea de sección con el nombre del conjunto, por ejemplo, 2 × Kit de medición estándar, y coloca debajo las líneas del conjunto. Las secciones que contiene el conjunto se convierten en subsecciones.
  2. Modo de impresión por conjunto. La forma de imprimirlo se configura una sola vez en el conjunto. Al insertarlo, se puede cambiar el modo en el asistente para ese caso concreto.
  3. Ocultar líneas. Se pueden marcar líneas individuales como No imprimir (en el conjunto: Oculta). Se incluyen en el total, pero no se imprimen ni se muestran en el portal del cliente. La marca se traslada del presupuesto a la factura.
  4. Precio fijo por línea del conjunto. Una línea del conjunto puede tener un precio unitario fijo que sustituya al de la tarifa, por ejemplo, para un ajuste de precio oculto.
Sin el módulo (solo sale_product_set)Con sale_product_set_layout
Líneas del conjunto como líneas independientesLínea de sección con el nombre del conjunto y la cantidad en el nombre, y las líneas debajo
Las secciones del conjunto siguen siendo secciones y rompen la estructuraLas secciones del conjunto se convierten en subsecciones (line_subsection)
Presentación impresa siempre igualModo de impresión por conjunto, modificable al insertarlo
Ajuste de precio visible en el presupuesto y en la facturaLa línea se puede ocultar y sigue contando en el total
Precio obtenido únicamente de la tarifaPrecio fijo opcional por línea del conjunto
El formato solo se aplica al presupuestoEl formato y la ocultación de líneas se aplican al presupuesto, al pedido, a la factura y al portal del cliente

Los tres modos de impresión en detalle

El modo de impresión es un campo de selección, print_mode, del conjunto de productos. Al insertarlo, se traduce en los dos controles estándar de la línea de sección. Para ello, el módulo utiliza una única correspondencia fácil de leer:

# Modo de impresión -> (collapse_prices, collapse_composition) de la línea de sección
PRINT_MODE_FLAGS = {
    "detailed":   (False, False),
    "total_only": (True,  False),
    "summary":    (False, True),
}

Tres formas de imprimir el mismo conjunto de productos: cada línea con su precio, solo el precio total o una única línea

Modo (print_mode)Etiqueta en el conjuntoOpción estándar de OdooPresentación impresa
detailedCada línea con su precioningunaSección y todas las líneas con su importe
total_onlyLíneas sin precios, solo el total del conjuntocollapse_prices (Ocultar precios)Sección con el total y líneas solo con descripción y cantidad
summaryUna única línea con el total del conjuntocollapse_composition (Ocultar composición)Solo la línea de sección con el total

Como el módulo utiliza las opciones estándar de Odoo 19, sin un formato de impresión propio, funciona en presupuestos, confirmaciones de pedido, facturas y el portal del cliente sin adaptar las plantillas de informes. Este comportamiento también se aplica a las plantillas personalizadas que tengan en cuenta las opciones de sección.

Líneas ocultas y precio fijo

Un caso habitual: un paquete tiene un precio inferior a la suma de los precios individuales. En lugar de alterar estos precios, se añade una línea de ajuste con precio fijo (por ejemplo, −50,00 € por conjunto) y se oculta. En el backend se puede comprobar el cálculo y en el documento impreso aparece el precio del paquete.

Ajuste de precio visible y marcado en el backend, oculto en el presupuesto impreso, el PDF, la factura y el portal, con el mismo total

Conviene tenerlo claro: la marca hide_in_report no modifica el cálculo. Odoo sigue calculando de la forma habitual las cantidades, los precios, los descuentos, los impuestos, los totales y los asientos contables. La línea solo se oculta en la impresión y en el portal.

  • En el conjunto de productos: línea del conjunto con la opción Oculta y, opcionalmente, Precio fijo junto con el Precio unitario.
  • En las líneas de presupuesto y de factura: columna No imprimir (hide_in_report), también disponible para líneas normales que no pertenecen a ningún conjunto.
  • Transferencia: al crear la factura a partir del pedido, la marca se transfiere a la línea de factura. También se conserva al copiar un presupuesto.

Implementación técnica

El módulo se ha mantenido pequeño de forma deliberada: amplía los modelos existentes y solo sobrescribe los puntos en los que Odoo decide qué líneas se imprimen y cómo se inserta un conjunto. No incorpora nuevas plantillas de informes ni nuevos modelos, ni realiza modificaciones mediante SQL.

Flujo de datos: conjunto de productos, asistente Añadir conjunto, líneas de presupuesto, líneas de factura, PDF y portal del cliente

Modelos y campos ampliados

ModeloAmpliación
product.setCampos insert_as_section (booleano, activado por defecto) y print_mode (selección, obligatorio, valor predeterminado: detailed)
product.set.lineCampos hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() los transfiere a la línea de presupuesto
sale.product.set.wizardCampos insert_as_section, section_name, print_mode; _prepare_order_lines() crea la línea de sección
sale.order / sale.order.lineCampo hide_in_report (con copy=True); _get_order_lines_to_report() omite las líneas ocultas; _prepare_invoice_line() transfiere la marca
account.move / account.move.lineCampo hide_in_report; _get_move_lines_to_report(), _get_detailed_lines() y _get_subsection_lines() excluyen las líneas ocultas; _get_aml_vals() incorpora la marca a los datos del informe

Ocultar líneas en el presupuesto

Odoo 19 determina las líneas del presupuesto que se deben imprimir en _get_order_lines_to_report(). El módulo añade un filtro en ese punto. No hace falta modificar la plantilla del informe: tanto la plantilla del presupuesto (sale/report/ir_actions_report_templates.xml) como la del portal (sale/views/sale_portal_templates.xml) llaman a 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
        )

Transferencia a la factura

Al generar la línea de factura a partir de la línea de presupuesto (_prepare_invoice_line()), se copia la marca. En Odoo 19, las líneas pueden llegar al informe de factura por varias vías: la lista principal, las líneas de una sección y las de una subsección. Por eso el módulo aplica filtros en varios puntos (_get_move_lines_to_report() en el documento, y _get_detailed_lines() y _get_subsection_lines() en la línea). De lo contrario, una línea oculta podría volver a aparecer por otra de esas vías:

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")]

El asistente: una sección en lugar de líneas sueltas

El asistente sale.product.set.wizard de sale_product_set genera las líneas del presupuesto mediante _prepare_order_lines(). El módulo sobrescribe este método. Si Insertar como sección está desactivado, simplemente llama al método original y mantiene el comportamiento del módulo de OCA. En caso contrario, crea primero la línea de sección con los dos controles estándar y después añade las líneas del conjunto. Las secciones del conjunto se convierten en subsecciones para no romper la estructura 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

Decisiones que no se ven

  • Métodos de cálculo independientes en el asistente. El nombre de la sección, el modo de impresión e Insertar como sección tienen cada uno su propio método compute (store=True, readonly=False). Si los tres compartieran el mismo método, un valor proporcionado explícitamente para un campo restablecería los demás. Esto se aprecia sobre todo al crear el asistente con valores desde código o desde pruebas.
  • Nombre de sección con cantidad. Cuando la cantidad es distinta de 1, se incluye en el nombre (2 × Kit de medición estándar, con coma como separador decimal), para que en el documento impreso quede claro que se trata de dos conjuntos y no de uno con el doble de unidades.
  • Precio fijo solo en líneas de producto. prepare_sale_order_line_values() solo establece hide_in_report y price_unit si la línea del conjunto no es una línea de sección ni de nota.
  • Funciones estándar de Odoo en lugar de soluciones propias. El modo de impresión y la ocultación utilizan los puntos de extensión y los campos existentes de Odoo 19. Esto facilita las actualizaciones: si Odoo cambia la plantilla del informe, el módulo seguirá siendo válido mientras se mantengan los métodos.

Instalación sin efectos secundarios no deseados

Requisitos: Odoo 19.0, sale_product_set (OCA/sale-workflow, rama 19.0) y product_set (OCA/product-attribute, rama 19.0). El propio módulo depende de sale_management, sale_product_set y account.

La trampa de auto_install en los módulos de OCA. Muchos repositorios de OCA contienen módulos con auto_install. En cuanto sus dependencias están disponibles, Odoo los instala también, sin preguntar, en la siguiente instalación o actualización. Por tanto, añadir el repositorio completo de OCA al addons_path de una instancia de producción puede acabar incorporando módulos que nadie quería. En OCA/product-attribute, esto afecta, por ejemplo, a purchase_product_template_tags. Nuestro procedimiento:

  1. Clonar los dos repositorios de OCA con un historial reducido (git clone --depth 1 --branch 19.0).
  2. Crear enlaces simbólicos únicamente a los dos módulos necesarios en un directorio propio y añadir ese directorio al addons_path, en lugar de los repositorios completos.
  3. Instalar en una base de datos de pruebas y comprobar qué módulos pasan al estado instalado.
  4. Solo entonces, instalar en producción con --workers=0, para que la ejecución no compita con el servicio en marcha por el puerto HTTP.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init

Tras la instalación, los nuevos campos y vistas ya están en la base de datos. Si el servicio funciona con varios procesos, los workers cargan el registro actualizado mediante la señal de la base de datos. Los cambios en los archivos Python del módulo solo surten efecto después de reiniciar el servicio.

Uso paso a paso

  1. Crear un conjunto: Ventas → Productos → Conjuntos de productos. Asignar un nombre al conjunto, configurar Insertar como sección y Modo de impresión, y añadir los productos con sus cantidades. Marcar las líneas de ajuste como Oculta y Precio fijo.
  2. En el presupuesto: hacer clic en Añadir conjunto y seleccionar el conjunto y la cantidad. El nombre de la sección y el modo de impresión se pueden cambiar antes de insertarlo.
  3. Ocultar líneas individuales: activar la columna No imprimir en el presupuesto o en la factura.
  4. Comprobar el resultado: abrir la vista previa o el PDF. Los clientes ven la misma presentación en el portal.

Pruebas

El módulo incluye seis pruebas automatizadas que ejecutamos en una base de datos de pruebas antes de llevar cualquier cambio a producción:

  • Inserción como sección, incluidos el modo de impresión y la subsección
  • La línea oculta no figura entre las líneas que se van a imprimir
  • Modo Una línea (summary)
  • Inserción sin sección, con el comportamiento original
  • La factura recibe la marca hide_in_report, la línea no aparece en _get_move_lines_to_report() y sigue incluida en el total de la sección
  • La factura en modo Solo precio total no muestra la línea oculta entre las líneas de la sección y mantiene íntegro el total de la sección
odoo-bin -d <testdatenbank> -i sale_product_set_layout \
         --test-enable --test-tags=/sale_product_set_layout --stop-after-init

La traducción al alemán se encuentra en i18n/de.po.

Limitaciones y aviso legal

  • Solo presentación. El módulo no modifica cómo calcula Odoo los precios, los descuentos o los impuestos. Solo cambia lo que se imprime y se muestra en el portal.
  • Las líneas ocultas siguen visibles en el backend. La marca afecta a la impresión y al portal, no a lo que ve el personal en pantalla.
  • Revisar los datos de la factura. Las líneas ocultas se incluyen en el total, pero no aparecen en el documento. Antes de utilizar esta función en facturas, consulte con su asesoría fiscal si esto es compatible con los requisitos de facturación de su país, por ejemplo, con la descripción obligatoria de los bienes entregados o los servicios prestados. Ocultar líneas en presupuestos plantea menos problemas. Este artículo no constituye asesoramiento jurídico.
  • Probado para Odoo 19. Hemos desarrollado y probado el módulo para Odoo 19.0 Community. Utiliza opciones de sección disponibles de esta forma en Odoo 19. No lo hemos probado en otras versiones.

Preguntas frecuentes

¿Para qué necesito el módulo si Odoo 19 ya admite secciones?

Las opciones de sección ya existen en Odoo. Lo que faltaba era conectarlas con el conjunto de productos: el conjunto debía insertarse automáticamente como una sección, con el modo de impresión definido en él. Sin el módulo, hay que crear y configurar manualmente la línea de sección después de cada inserción.

¿Qué diferencia hay entre «Ocultar precios» y «Ocultar composición»?

«Ocultar precios» (collapse_prices) mantiene las líneas, pero no muestra sus importes, solo el total de la sección. «Ocultar composición» (collapse_composition) oculta las líneas por completo: el cliente solo ve la sección con su total.

¿Se incluye una línea oculta en el total de la factura?

Sí. Las líneas ocultas siguen incluidas en el total, en el cálculo de impuestos y en los asientos contables. La marca solo afecta a la presentación en el documento impreso y en el portal del cliente.

¿Puedo cambiar el modo de impresión al insertar el conjunto?

Sí. El modo definido en el conjunto se utiliza como valor predeterminado y se puede cambiar en el asistente para cada inserción. El propio conjunto permanece sin cambios.

¿Qué ocurre si desactivo Insertar como sección?

El asistente se comporta como en el módulo de OCA sale_product_set: las líneas del conjunto se insertan sin sección.

¿El módulo instala otros módulos de forma inesperada?

El propio módulo no. Sin embargo, al añadir repositorios completos de OCA al addons_path, pueden instalarse módulos con auto_install. Por eso, cree enlaces únicamente a los dos módulos necesarios (véase el apartado de instalación).

¿Funciona el módulo con Odoo Enterprise?

El módulo solo depende de módulos de Community (sale_management, account) y de los dos módulos de OCA. Lo utilizamos en Odoo 19 Community. No lo hemos probado en Enterprise.

¿Qué licencia tiene el módulo?

Bajo la licencia AGPL-3. Puede utilizarlo, adaptarlo y distribuirlo siempre que las modificaciones se mantengan bajo la misma licencia. Los informes de errores y las sugerencias de mejora enviados como incidencias en el repositorio de GitHub serán bienvenidos.

Asistencia de Biricon IT Services

Gestionamos instalaciones de Odoo para pequeñas y medianas empresas: desde la implantación hasta el funcionamiento diario, pasando por adaptaciones y migraciones. Si desea adaptar los conjuntos de productos, las plantillas de presupuesto o los informes de Odoo 19 a sus procesos, o si quiere evaluar un módulo como este e incorporarlo de forma segura a su instancia de producción, estaremos encantados de ayudarle. Encontrará otros desarrollos propios en nuestro blog, por ejemplo, Combinar facturas en borrador y Asignar automáticamente los pagos bancarios a las facturas.

Póngase en contacto ahora

Actualizado el 2 de noviembre de 2026. Versión del módulo: 19.0.1.0.0. Los datos de ejemplo de las imágenes son ficticios y los importes no incluyen impuestos.