Wer in Odoo Pakete verkauft, etwa ein Messgerät mit Zubehör und Montage, einen Arbeitsplatz mit Hardware und Einrichtung oder ein Wartungspaket aus mehreren Leistungen, braucht im Angebot zwei Dinge gleichzeitig: Die Zusammensetzung muss im Auftrag stehen, damit Lager, Technik und Rechnungsstellung wissen, was geliefert wird. Auf dem Ausdruck für den Kunden soll aber nicht in jedem Fall jede Einzelposition mit jedem Einzelpreis erscheinen.
Das OCA-Modul sale_product_set erlaubt es, Produkt-Sets (Stücklisten für den Verkauf) mit einem Klick in ein Angebot einzufügen. Über das Druckbild entscheidet es nicht. Dafür haben wir das Modul Sale Product Set Layout (sale_product_set_layout) entwickelt. Es ist für Odoo 19 geschrieben und steht als freie Software unter der AGPL-3-Lizenz kostenlos auf GitHub zur Verfügung:
github.com/Alex0176/odoo-sale-product-set-layout
Inhalt dieses Beitrags
- Das Problem: Produkt-Sets ohne Einfluss aufs Druckbild
- Was das Modul macht
- Die drei Druckmodi im Detail
- Ausgeblendete Zeilen und Festpreis
- Technische Umsetzung
- Installation ohne unerwünschte Nebenwirkungen
- Bedienung Schritt für Schritt
- Tests
- Grenzen und rechtlicher Hinweis
- Häufige Fragen
- Unterstützung durch Biricon IT Services
Das Problem: Produkt-Sets ohne Einfluss aufs Druckbild
Ein Produkt-Set ist in Odoo eine vorbereitete Liste von Produkten mit Mengen. Statt zehn Artikel einzeln zu suchen, wählt man im Angebot Set hinzufügen und bekommt alle Zeilen auf einmal. Das spart Zeit und verhindert vergessene Positionen. Das Modul sale_product_set aus dem Repository OCA/sale-workflow macht genau das; die Sets selbst stammen aus product_set im Repository OCA/product-attribute.
In der Praxis bleiben drei Lücken:
- Keine erkennbare Gruppierung. Die Set-Zeilen landen als lose Positionen im Angebot. Nach dem Einfügen sieht man nicht mehr, welche Zeilen zusammengehören, und die Gesamtsumme des Sets steht nirgends.
- Kein Einfluss auf den Ausdruck. Mal soll der Kunde jede Position mit Preis sehen, mal nur den Paketpreis, mal nur eine einzige Zeile. Das ist eine Entscheidung je Set oder je Kunde, nicht je Angebot neu per Hand.
- Preiskorrekturen sind unschön. Soll ein Paket einen festen Preis haben, der von der Summe der Listenpreise abweicht, braucht es eine Korrekturzeile. Die gehört in die Berechnung, aber nicht unbedingt auf das Papier des Kunden.
Odoo 19 bringt die Bausteine mit, um das sauber zu lösen: Abschnitte und Unterabschnitte in Angebotszeilen mit den Optionen Preise ausblenden und Zusammensetzung ausblenden (Felder collapse_prices und collapse_composition). Das Modul verbindet das Produkt-Set mit diesen Standard-Optionen, statt ein eigenes Druckformat zu erfinden.
Was das Modul macht
- Set als Abschnitt einfügen. Der Assistent Set hinzufügen legt eine Abschnittszeile mit dem Namen des Sets an, zum Beispiel 2 × Messkit Standard, und setzt die Set-Zeilen darunter. Abschnitte innerhalb des Sets werden zu Unterabschnitten.
- Druckmodus je Set. Im Set wird einmal festgelegt, wie es gedruckt wird. Beim Einfügen lässt sich der Modus im Assistenten für diesen einen Fall ändern.
- Zeilen ausblenden. Einzelne Zeilen können als Nicht drucken (am Set: Versteckt) markiert werden. Sie zählen in der Summe mit, werden aber weder gedruckt noch im Kundenportal angezeigt. Das Kennzeichen wandert vom Angebot auf die Rechnung.
- Festpreis je Set-Zeile. Eine Set-Zeile kann einen festen Einzelpreis tragen, der den Preis aus der Preisliste ersetzt, zum Beispiel für eine ausgeblendete Preiskorrektur.
Ohne das Modul (nur sale_product_set) | Mit sale_product_set_layout |
|---|---|
| Set-Zeilen als lose Positionen | Abschnittszeile mit Set-Name, Menge im Namen, Zeilen darunter |
| Abschnitte im Set bleiben Abschnitte und zerreißen die Struktur | Abschnitte im Set werden Unterabschnitte (line_subsection) |
| Druckbild immer gleich | Druckmodus je Set, beim Einfügen änderbar |
| Preiskorrektur sichtbar auf Angebot und Rechnung | Zeile ausblendbar, zählt aber in der Summe mit |
| Preis nur aus der Preisliste | Optional Festpreis je Set-Zeile |
| Layout endet am Angebot | Layout und Ausblenden gelten für Angebot, Auftrag, Rechnung und Kundenportal |
Die drei Druckmodi im Detail
Der Druckmodus ist ein Auswahlfeld print_mode am Produkt-Set. Er wird beim Einfügen in die beiden Standard-Schalter der Abschnittszeile übersetzt. Dafür steht im Modul eine einzige, leicht lesbare Zuordnung:
# Druckmodus -> (collapse_prices, collapse_composition) der Abschnittszeile
PRINT_MODE_FLAGS = {
"detailed": (False, False),
"total_only": (True, False),
"summary": (False, True),
}
Modus (print_mode) | Anzeige im Set | Standard-Option in Odoo | Druckbild |
|---|---|---|---|
detailed | Jede Zeile mit Preis | keine | Abschnitt, alle Zeilen mit Betrag |
total_only | Zeilen ohne Preise, nur Set-Summe | collapse_prices (Preise ausblenden) | Abschnitt mit Summe, Zeilen nur mit Bezeichnung und Menge |
summary | Nur eine Zeile mit Set-Summe | collapse_composition (Zusammensetzung ausblenden) | Nur die Abschnittszeile mit der Summe |
Weil das Modul die Standard-Optionen von Odoo 19 verwendet und kein eigenes Druckformat, funktionieren Angebot, Auftragsbestätigung, Rechnung und Kundenportal ohne angepasste Berichtsvorlagen. Wer eigene Berichtsvorlagen hat, die die Abschnitts-Optionen berücksichtigen, bekommt das Verhalten ebenfalls.
Ausgeblendete Zeilen und Festpreis
Ein häufiger Fall: Ein Paket hat einen Paketpreis, der unter der Summe der Einzelpreise liegt. Statt die Einzelpreise zu verbiegen, nimmt man eine Korrekturzeile mit Festpreis (zum Beispiel −50,00 € je Set) und blendet sie aus. Im Backend ist die Rechnung nachvollziehbar, auf dem Ausdruck steht der Paketpreis.
Wichtig für das Verständnis: Das Kennzeichen hide_in_report ändert nichts an der Berechnung. Menge, Preis, Rabatt, Steuer, Summen und Buchungssätze entstehen unverändert durch Standard-Odoo. Die Zeile fehlt nur im Ausdruck und im Portal.
- Am Produkt-Set: Set-Zeile mit Versteckt und optional Festpreis samt Einzelpreis.
- An Angebots- und Rechnungszeilen: Spalte Nicht drucken (
hide_in_report), auch für normale Zeilen ohne Set. - Weitergabe: Beim Erstellen der Rechnung aus dem Auftrag wird das Kennzeichen auf die Rechnungszeile übertragen. Beim Kopieren eines Angebots bleibt es erhalten.
Technische Umsetzung
Das Modul ist bewusst klein: Es erweitert vorhandene Modelle und überschreibt nur die Stellen, an denen Odoo entscheidet, welche Zeilen gedruckt werden und wie ein Set eingefügt wird. Es gibt keine neuen Berichtsvorlagen, keine neuen Modelle und keine SQL-Eingriffe.
Erweiterte Modelle und Felder
| Modell | Ergänzung |
|---|---|
product.set | Felder insert_as_section (Boolean, Standard an) und print_mode (Auswahl, Pflichtfeld, Standard detailed) |
product.set.line | Felder hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() reicht sie an die Angebotszeile weiter |
sale.product.set.wizard | Felder insert_as_section, section_name, print_mode; _prepare_order_lines() baut die Abschnittszeile |
sale.order / sale.order.line | Feld hide_in_report (mit copy=True); _get_order_lines_to_report() überspringt ausgeblendete Zeilen; _prepare_invoice_line() gibt das Kennzeichen weiter |
account.move / account.move.line | Feld hide_in_report; _get_move_lines_to_report(), _get_detailed_lines() und _get_subsection_lines() filtern ausgeblendete Zeilen; _get_aml_vals() trägt das Kennzeichen in die Berichtsdaten |
Ausblenden im Angebot
Odoo 19 ermittelt die zu druckenden Angebotszeilen in _get_order_lines_to_report(). Das Modul hängt dort einen Filter an. Ein Eingriff in die Berichtsvorlage ist nicht nötig: Sowohl die Berichtsvorlage des Angebots (sale/report/ir_actions_report_templates.xml) als auch die Portalvorlage (sale/views/sale_portal_templates.xml) rufen diese Methode auf:
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
)
Weitergabe auf die Rechnung
Beim Erzeugen der Rechnungszeile aus der Angebotszeile (_prepare_invoice_line()) wird das Kennzeichen kopiert. Auf der Rechnungsseite gibt es in Odoo 19 mehrere Wege, über die Zeilen in den Bericht gelangen: die Hauptliste, die Zeilen eines Abschnitts und die Zeilen eines Unterabschnitts. Deshalb filtert das Modul an mehreren Stellen (_get_move_lines_to_report() am Beleg, _get_detailed_lines() und _get_subsection_lines() an der Zeile), sonst könnte eine ausgeblendete Zeile über einen der anderen Wege wieder auftauchen:
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")]
Der Assistent: Abschnitt statt loser Zeilen
Der Assistent sale.product.set.wizard aus sale_product_set erzeugt die Angebotszeilen in _prepare_order_lines(). Das Modul überschreibt diese Methode. Ist Als Abschnitt einfügen aus, ruft es einfach die ursprüngliche Methode auf, das Verhalten bleibt dann wie im OCA-Modul. Andernfalls entsteht zuerst die Abschnittszeile mit den beiden Standard-Schaltern, danach folgen die Set-Zeilen. Abschnitte innerhalb des Sets werden zu Unterabschnitten, damit sie die Hauptstruktur nicht zerreißen:
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
Entscheidungen, die man nicht sieht
- Getrennte Berechnungsmethoden im Assistenten. Abschnittsname, Druckmodus und Als Abschnitt einfügen haben je eine eigene
compute-Methode (store=True,readonly=False). Hätten alle drei dieselbe Methode, würde ein explizit übergebener Wert für ein Feld die anderen zurücksetzen. Das fällt vor allem auf, wenn der Assistent aus Code oder Tests mit Werten angelegt wird. - Abschnittsname mit Menge. Bei einer Menge ungleich 1 steht sie im Namen (2 × Messkit Standard, mit Komma als Dezimaltrenner), damit man im Ausdruck erkennt, dass es zwei Sets sind und nicht ein Set mit doppelter Menge.
- Festpreis nur an Produktzeilen.
prepare_sale_order_line_values()setzthide_in_reportundprice_unitnur, wenn die Set-Zeile keine Abschnitts- oder Notizzeile ist. - Standard-Odoo statt Eigenbau. Druckmodus und Ausblenden benutzen vorhandene Hooks und Felder von Odoo 19. Das macht Updates beherrschbar: Ändert Odoo die Berichtsvorlage, bleibt das Modul gültig, solange die Methoden bestehen.
Installation ohne unerwünschte Nebenwirkungen
Voraussetzungen: Odoo 19.0, sale_product_set (OCA/sale-workflow, Branch 19.0) und product_set (OCA/product-attribute, Branch 19.0). Das Modul selbst hängt von sale_management, sale_product_set und account ab.
Die auto_install-Falle bei OCA-Modulen. Viele OCA-Repositories enthalten Module mit auto_install. Sobald deren Abhängigkeiten vorhanden sind, installiert Odoo sie beim nächsten Installieren oder Aktualisieren ungefragt mit. Wer deshalb das komplette OCA-Repository in den addons_path einer Produktivinstanz legt, bekommt womöglich Module, die niemand haben wollte. Bei OCA/product-attribute betrifft das zum Beispiel purchase_product_template_tags. Unser Vorgehen:
- Die beiden OCA-Repositories flach klonen (
git clone --depth 1 --branch 19.0). - In einem eigenen Verzeichnis nur die zwei benötigten Module per Symlink verlinken und dieses Verzeichnis in den
addons_pathaufnehmen, nicht die ganzen Repositories. - Auf einer Testdatenbank installieren und prüfen, welche Module dabei den Status installiert bekommen.
- Erst dann produktiv installieren, mit
--workers=0, damit der Aufruf nicht mit dem laufenden Dienst um den HTTP-Port konkurriert.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init
Neue Felder und Ansichten sind nach dem Installieren in der Datenbank. Läuft der Dienst im Mehrprozessbetrieb, laden die Worker die geänderte Registry über das Datenbank-Signal nach. Änderungen an Python-Dateien des Moduls werden erst nach einem Neustart des Dienstes wirksam.
Bedienung Schritt für Schritt
- Set anlegen: Verkauf → Produkte → Produkt-Sets. Set benennen, Als Abschnitt einfügen und Druckmodus wählen, die Produkte mit Mengen eintragen. Korrekturzeilen mit Versteckt und Festpreis versehen.
- Im Angebot: Auf Set hinzufügen klicken, Set und Menge wählen. Abschnittsname und Druckmodus lassen sich vor dem Einfügen ändern.
- Einzelne Zeilen ausblenden: Im Angebot oder in der Rechnung die Spalte Nicht drucken aktivieren.
- Prüfen: Vorschau oder PDF öffnen. Dieselbe Darstellung sehen Kunden im Portal.
Tests
Das Modul bringt sechs automatisierte Tests mit, die wir gegen eine Testdatenbank ausführen, bevor etwas produktiv geht:
- Einfügen als Abschnitt inklusive Druckmodus und Unterabschnitt
- Ausgeblendete Zeile fehlt in den zu druckenden Zeilen
- Modus Eine Zeile (
summary) - Einfügen ohne Abschnitt, das ursprüngliche Verhalten
- Rechnung übernimmt das Kennzeichen
hide_in_report, die Zeile fehlt in_get_move_lines_to_report(), die Abschnittssumme enthält sie weiterhin - Rechnung im Modus Nur Gesamtpreis zeigt die versteckte Zeile nicht in den Abschnittszeilen, die Abschnittssumme bleibt vollständig
odoo-bin -d <testdatenbank> -i sale_product_set_layout \
--test-enable --test-tags=/sale_product_set_layout --stop-after-init
Die deutsche Übersetzung liegt in i18n/de.po.
Grenzen und rechtlicher Hinweis
- Nur Layout. Das Modul ändert nicht, wie Odoo Preise, Rabatte oder Steuern berechnet. Es ändert nur, was gedruckt und im Portal angezeigt wird.
- Ausgeblendete Zeilen sind im Backend sichtbar. Das Kennzeichen betrifft Ausdruck und Portal, nicht die Bildschirmansicht der Mitarbeiter.
- Rechnungsangaben prüfen. Ausgeblendete Zeilen zählen in der Summe mit, stehen aber nicht auf dem Beleg. Ob das mit den Anforderungen an Rechnungen in Ihrem Land vereinbar ist, etwa mit der vorgeschriebenen Beschreibung der gelieferten Gegenstände oder erbrachten Leistungen, sollten Sie vor dem Einsatz auf Rechnungen mit Ihrer Steuerberatung klären. Auf Angeboten ist das Ausblenden unproblematischer. Der Beitrag ist keine Rechtsberatung.
- Getestet für Odoo 19. Entwickelt und getestet haben wir das Modul für Odoo 19.0 Community. Es nutzt Abschnitts-Optionen, die in dieser Form in Odoo 19 vorhanden sind. Für andere Versionen haben wir es nicht geprüft.
Häufige Fragen
Wozu brauche ich das Modul, wenn Odoo 19 schon Abschnitte kann?
Die Abschnitts-Optionen gibt es in Odoo selbst. Es fehlte die Verbindung zum Produkt-Set: Ein Set sollte automatisch als Abschnitt eingefügt werden, mit dem im Set hinterlegten Druckmodus. Ohne das Modul muss man nach jedem Einfügen die Abschnittszeile von Hand anlegen und konfigurieren.
Wie unterscheiden sich „Preise ausblenden“ und „Zusammensetzung ausblenden“?
„Preise ausblenden“ (collapse_prices) lässt die Zeilen stehen, zeigt aber keine Beträge, nur die Summe am Abschnitt. „Zusammensetzung ausblenden“ (collapse_composition) blendet die Zeilen ganz aus, der Kunde sieht nur den Abschnitt mit der Summe.
Zählt eine ausgeblendete Zeile in der Rechnungssumme mit?
Ja. Ausgeblendete Zeilen bleiben in Summe, Steuer und Buchung. Das Kennzeichen betrifft nur die Darstellung auf dem Ausdruck und im Kundenportal.
Kann ich den Druckmodus beim Einfügen noch ändern?
Ja. Der Modus kommt als Vorgabe aus dem Set, im Assistenten kann er für das jeweilige Einfügen geändert werden. Das Set selbst bleibt dabei unverändert.
Was passiert, wenn ich Als Abschnitt einfügen ausschalte?
Dann verhält sich der Assistent wie im OCA-Modul sale_product_set: Die Set-Zeilen werden ohne Abschnitt eingefügt.
Installiert das Modul unerwartet weitere Module?
Das Modul selbst nicht. Bei OCA-Repositories kann es aber zu auto_install-Modulen kommen, wenn man ganze Repositories in den addons_path legt. Verlinken Sie deshalb nur die beiden benötigten Module (siehe Abschnitt Installation).
Läuft das Modul mit Odoo Enterprise?
Das Modul hängt nur von Community-Modulen ab (sale_management, account) und den beiden OCA-Modulen. Wir setzen es auf Odoo 19 Community ein. Auf Enterprise haben wir es nicht getestet.
Unter welcher Lizenz steht das Modul?
Unter der AGPL-3. Sie dürfen es nutzen, anpassen und weitergeben, solange Änderungen unter derselben Lizenz bleiben. Fehler und Verbesserungsvorschläge sind als Issue im GitHub-Repository willkommen.
Unterstützung durch Biricon IT Services
Wir betreuen Odoo-Installationen für kleine und mittlere Unternehmen: von der Einführung über Anpassungen und Migrationen bis zum laufenden Betrieb. Wenn Sie Produkt-Sets, Angebotsvorlagen oder Berichte in Odoo 19 an Ihre Abläufe anpassen möchten, oder wenn Sie ein Modul wie dieses prüfen und sicher in Ihre Produktivinstanz bringen wollen, unterstützen wir Sie gerne. Weitere Eigenentwicklungen finden Sie in unserem Blog, zum Beispiel Entwurfsrechnungen zusammenführen und Bankzahlungen automatisch Rechnungen zuordnen.
Stand: 2. November 2026. Modulversion 19.0.1.0.0. Die Beispieldaten in den Abbildungen sind frei gewählt, Beträge netto.