Gå til indhold

Odoo 19 Community: produktsæt som afsnit i tilbuddet – vores open source-modul Sale Product Set Layout

2. november 2026 | Odoo 19 Community | Egenudvikling
2. november 2026 af
Odoo 19 Community: produktsæt som afsnit i tilbuddet – vores open source-modul Sale Product Set Layout

Odoo 19-modulet Sale Product Set Layout: Produktsæt som afsnit i tilbud med udskriftstilstand, skjulte linjer og fast pris

Hvis du sælger pakker i Odoo, for eksempel et måleinstrument med tilbehør og montering, en arbejdsplads med hardware og opsætning eller en vedligeholdelsespakke med flere ydelser, skal tilbuddet opfylde to behov på én gang: Sammensætningen skal fremgå af ordren, så lageret, teknikerne og faktureringsafdelingen ved, hvad der skal leveres. Men på udskriften til kunden skal alle enkeltposter og deres priser ikke nødvendigvis vises.

OCA-modulet sale_product_set gør det muligt at indsætte produktsæt (styklister til salg) i et tilbud med ét klik. Det styrer ikke udskriftens udseende. Derfor har vi udviklet modulet Sale Product Set Layout (sale_product_set_layout). Det er skrevet til Odoo 19 og er gratis tilgængeligt på GitHub som fri software under AGPL-3-licensen:

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

Problemet: Produktsæt uden indflydelse på udskriftens udseende

Et produktsæt i Odoo er en forberedt liste over produkter med antal. I stedet for at finde ti varer enkeltvis vælger du Tilføj sæt i tilbuddet og får alle linjerne indsat på én gang. Det sparer tid og sikrer, at ingen poster bliver glemt. Modulet sale_product_set fra kodearkivet OCA/sale-workflow gør netop dette; selve sættene kommer fra product_set i kodearkivet OCA/product-attribute.

I praksis mangler der tre ting:

  • Ingen tydelig gruppering. Sættets linjer indsættes som enkeltstående poster i tilbuddet. Efter indsættelsen kan man ikke længere se, hvilke linjer der hører sammen, og sættets samlede pris vises ingen steder.
  • Ingen kontrol over udskriften. Nogle gange skal kunden se hver post med pris, andre gange kun pakkeprisen eller blot en enkelt linje. Det skal kunne vælges for hvert sæt eller hver kunde, uden at tilbuddet skal tilpasses manuelt hver gang.
  • Priskorrektioner ser ikke pæne ud. Hvis en pakke skal have en fast pris, der afviger fra summen af listepriserne, kræver det en korrektionslinje. Den skal indgå i beregningen, men behøver ikke stå på kundens udskrift.

Odoo 19 leverer byggestenene til at løse det rent: sektioner og undersektioner i tilbudslinjer med indstillingerne Skjul priser og Skjul sammensætning (felterne collapse_prices og collapse_composition). Modulet forbinder produktsættet med disse standardindstillinger i stedet for at opfinde et eget udskriftsformat.

Det gør modulet

  1. Indsæt sættet som et afsnit. Guiden Tilføj sæt opretter en afsnitslinje med sættets navn, for eksempel 2 × Messkit Standard, og placerer sættets linjer nedenunder. Afsnit i sættet bliver til underafsnit.
  2. Udskriftstilstand for hvert sæt. Det angives én gang i sættet, hvordan det skal udskrives. Ved indsættelsen kan tilstanden ændres i guiden for det enkelte tilfælde.
  3. Skjul linjer. Enkelte linjer kan markeres med Udskriv ikke (på sættet: Skjult). De indgår i summen, men bliver hverken udskrevet eller vist i kundeportalen. Markeringen overføres fra tilbuddet til fakturaen.
  4. Fast pris for hver sætlinje. En sætlinje kan have en fast enhedspris, der erstatter prisen fra prislisten, for eksempel til en skjult priskorrektion.
Uden modulet (kun sale_product_set)Med sale_product_set_layout
Sætlinjer som enkeltstående posterAfsnitslinje med sættets navn, antal i navnet og linjerne nedenunder
Afsnit i sættet forbliver afsnit og bryder strukturenAfsnit i sættet bliver til underafsnit (line_subsection)
Udskriften ser altid ens udUdskriftstilstand for hvert sæt, som kan ændres ved indsættelsen
Priskorrektion synlig på tilbud og fakturaLinjen kan skjules, men indgår stadig i summen
Prisen hentes kun fra prislistenMulighed for fast pris på hver sætlinje
Layoutet gælder kun tilbuddetLayout og skjulte linjer gælder for tilbud, ordre, faktura og kundeportal

De tre udskriftstilstande i detaljer

Udskriftstilstanden er et valgfelt print_mode på produktsættet. Ved indsættelsen oversættes den til sektionslinjens to standardafbrydere. Til det formål indeholder modulet en enkelt, letlæselig tilordning:

# Udskriftstilstand -> (collapse_prices, collapse_composition) på afsnitslinjen
PRINT_MODE_FLAGS = {
    "detailed":   (False, False),
    "total_only": (True,  False),
    "summary":    (False, True),
}

Tre udskrifter af samme produktsæt: hver linje med pris, kun samlet pris, kun én enkelt linje

Tilstand (print_mode)Visning i sættetStandardindstilling i OdooUdskriftens udseende
detailedHver linje med prisingenAfsnit, alle linjer med beløb
total_onlyLinjer uden priser, kun sættets samlede priscollapse_prices (Skjul priser)Afsnit med samlet pris, linjer kun med betegnelse og antal
summaryKun én linje med sættets samlede priscollapse_composition (Skjul sammensætning)Kun afsnitslinjen med den samlede pris

Fordi modulet bruger standardindstillingerne i Odoo 19 og ikke sit eget udskriftsformat, fungerer tilbud, ordrebekræftelse, faktura og kundeportal uden tilpassede rapportskabeloner. Har du egne rapportskabeloner, der tager højde for afsnitsindstillingerne, fungerer det også med dem.

Skjulte linjer og fast pris

Et typisk tilfælde: En pakke har en pakkepris, der er lavere end summen af enkeltpriserne. I stedet for at justere enkeltpriserne bruger du en korrektionslinje med fast pris (for eksempel −50,00 € pr. sæt) og skjuler den. I backend kan beregningen følges, mens udskriften viser pakkeprisen.

Priskorrektionen er synlig og markeret i backend, men skjult i tilbudsudskriften, PDF'en, fakturaen og portalen. Summen er den samme

Det er vigtigt at forstå, at markeringen hide_in_report ikke ændrer beregningen. Antal, pris, rabat, afgifter, summer og bogføringsposter beregnes fortsat af standardfunktionerne i Odoo. Linjen er blot udeladt fra udskriften og portalen.

  • På produktsættet: Sætlinje med Skjult og eventuelt Fast pris samt Enhedspris.
  • På tilbuds- og fakturalinjer: Kolonnen Udskriv ikke (hide_in_report), også til almindelige linjer uden sæt.
  • Overførsel: Når fakturaen oprettes ud fra ordren, overføres markeringen til fakturalinjen. Den bevares også, når et tilbud kopieres.

Teknisk implementering

Modulet er bevidst holdt lille: Det udvider eksisterende modeller og tilsidesætter kun de steder, hvor Odoo afgør, hvilke linjer der skal udskrives, og hvordan et sæt indsættes. Der er ingen nye rapportskabeloner, ingen nye modeller og ingen SQL-indgreb.

Dataflow: Produktsæt, guiden Tilføj sæt, tilbudslinjer, fakturalinjer, PDF og kundeportal

Udvidede modeller og felter

ModelTilføjelse
product.setFelterne insert_as_section (boolesk, aktiveret som standard) og print_mode (valgfelt, obligatorisk, standardværdi detailed)
product.set.lineFelterne hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() overfører dem til tilbudslinjen
sale.product.set.wizardFelterne insert_as_section, section_name, print_mode; _prepare_order_lines() opretter afsnitslinjen
sale.order / sale.order.lineFeltet hide_in_report (med copy=True); _get_order_lines_to_report() springer skjulte linjer over; _prepare_invoice_line() overfører markeringen
account.move / account.move.lineFeltet hide_in_report; _get_move_lines_to_report(), _get_detailed_lines() og _get_subsection_lines() filtrerer skjulte linjer fra; _get_aml_vals() overfører markeringen til rapportdataene

Skjul linjer i tilbuddet

Odoo 19 finder de tilbudslinjer, der skal udskrives, i _get_order_lines_to_report(). Modulet tilføjer et filter her. Det er ikke nødvendigt at ændre rapportskabelonen: Både tilbuddets rapportskabelon (sale/report/ir_actions_report_templates.xml) og portalskabelonen (sale/views/sale_portal_templates.xml) kalder denne metode:

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
        )

Overførsel til fakturaen

Når fakturalinjen oprettes ud fra tilbudslinjen (_prepare_invoice_line()), kopieres markeringen. På fakturasiden er der i Odoo 19 flere veje, som linjerne kan komme ind i rapporten ad: hovedlisten, linjerne i et afsnit og linjerne i et underafsnit. Derfor filtrerer modulet flere steder (_get_move_lines_to_report() på bilaget, _get_detailed_lines() og _get_subsection_lines() på linjen). Ellers kunne en skjult linje dukke op igen ad en af de andre veje:

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

Guiden: et afsnit i stedet for løse linjer

Guiden sale.product.set.wizard fra sale_product_set opretter tilbudslinjerne i _prepare_order_lines(). Modulet tilsidesætter denne metode. Hvis Indsæt som afsnit er slået fra, kalder det blot den oprindelige metode, så adfærden er den samme som i OCA-modulet. Ellers oprettes først afsnitslinjen med de to standardindstillinger, efterfulgt af sættets linjer. Afsnit i sættet bliver til underafsnit, så de ikke bryder hovedstrukturen:

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

Valg bag kulisserne

  • Separate beregningsmetoder i guiden. Afsnitsnavn, udskriftstilstand og Indsæt som afsnit har hver sin compute-metode (store=True, readonly=False). Hvis alle tre brugte samme metode, ville en værdi, der blev angivet eksplicit for ét felt, nulstille de andre. Det mærkes især, når guiden oprettes med værdier fra kode eller test.
  • Afsnitsnavn med antal. Hvis antallet er forskelligt fra 1, indgår det i navnet (2 × Messkit Standard, med komma som decimaltegn), så det fremgår af udskriften, at der er tale om to sæt og ikke ét sæt med dobbelt antal.
  • Fast pris kun på produktlinjer. prepare_sale_order_line_values() sætter kun hide_in_report og price_unit, hvis linjen i sættet hverken er en afsnitslinje eller en notelinje.
  • Standardfunktioner i Odoo frem for egne løsninger. Udskriftstilstand og skjulning bruger eksisterende hooks og felter i Odoo 19. Det gør opdateringer nemmere at håndtere: Hvis Odoo ændrer rapportskabelonen, fungerer modulet fortsat, så længe metoderne findes.

Installation uden uønskede bivirkninger

Forudsætninger: Odoo 19.0, sale_product_set (OCA/sale-workflow, gren 19.0) og product_set (OCA/product-attribute, gren 19.0). Selve modulet afhænger af sale_management, sale_product_set og account.

Faldgruben med auto_install i OCA-moduler. Mange OCA-repositories indeholder moduler med auto_install. Så snart deres afhængigheder er til stede, installerer Odoo dem automatisk ved næste installation eller opdatering. Hvis man derfor lægger hele OCA-repositoriet i addons_path på en produktionsinstans, kan man ende med moduler, som ingen ønskede. I OCA/product-attribute gælder det for eksempel purchase_product_template_tags. Sådan gør vi:

  1. Klon de to OCA-repositories med begrænset historik (git clone --depth 1 --branch 19.0).
  2. Opret symbolske links til kun de to nødvendige moduler i en separat mappe, og tilføj denne mappe til addons_path, ikke de komplette repositories.
  3. Installér i en testdatabase, og kontrollér, hvilke moduler der får status installeret.
  4. Installér først derefter i produktionsmiljøet med --workers=0, så kommandoen ikke konkurrerer med den kørende tjeneste om HTTP-porten.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init

Efter installationen findes de nye felter og visninger i databasen. Hvis tjenesten kører med flere processer, genindlæser worker-processerne det ændrede registry via databasesignalet. Ændringer i modulets Python-filer træder først i kraft, når tjenesten genstartes.

Brug trin for trin

  1. Opret et sæt: Salg → Produkter → Produktsæt. Giv sættet et navn, vælg Indsæt som afsnit og Udskriftstilstand, og tilføj produkterne med antal. Markér korrektionslinjer med Skjult og Fast pris.
  2. I tilbuddet: Klik på Tilføj sæt, og vælg sæt og antal. Afsnitsnavn og udskriftstilstand kan ændres inden indsættelsen.
  3. Skjul enkelte linjer: Aktivér kolonnen Udskriv ikke i tilbuddet eller fakturaen.
  4. Kontrollér: Åbn forhåndsvisningen eller PDF-filen. Kunderne ser den samme visning i portalen.

Test

Modulet indeholder seks automatiserede test, som vi kører mod en testdatabase, før noget sættes i produktion:

  • Indsættelse som afsnit inklusive udskriftstilstand og underafsnit
  • En skjult linje er ikke med blandt de linjer, der skal udskrives
  • Tilstanden Én linje (summary)
  • Indsættelse uden afsnit, den oprindelige adfærd
  • Fakturaen overtager markeringen hide_in_report, linjen er ikke med i _get_move_lines_to_report(), men indgår fortsat i afsnittets samlede beløb
  • En faktura i tilstanden Kun samlet pris viser ikke den skjulte linje blandt afsnittets linjer, men afsnittets samlede beløb er stadig fuldt medregnet
odoo-bin -d <testdatenbank> -i sale_product_set_layout \
         --test-enable --test-tags=/sale_product_set_layout --stop-after-init

Den tyske oversættelse findes i i18n/de.po.

Begrænsninger og juridisk bemærkning

  • Kun layout. Modulet ændrer ikke, hvordan Odoo beregner priser, rabatter eller afgifter. Det ændrer kun, hvad der udskrives og vises i portalen.
  • Skjulte linjer er synlige i administrationsgrænsefladen. Markeringen gælder udskrift og portal, ikke medarbejdernes skærmvisning.
  • Kontrollér fakturaoplysningerne. Skjulte linjer indgår i det samlede beløb, men vises ikke på bilaget. Inden du bruger funktionen på fakturaer, bør du afklare med din skatterådgiver, om det er foreneligt med fakturakravene i dit land, for eksempel kravet om en beskrivelse af de leverede varer eller ydelser. På tilbud er det mindre problematisk at skjule linjer. Indlægget udgør ikke juridisk rådgivning.
  • Testet til Odoo 19. Vi har udviklet og testet modulet til Odoo 19.0 Community. Det bruger afsnitsindstillinger, som findes i denne form i Odoo 19. Vi har ikke testet det til andre versioner.

Ofte stillede spørgsmål

Hvorfor har jeg brug for modulet, når Odoo 19 allerede understøtter afsnit?

Afsnitsindstillingerne findes i selve Odoo. Det, der manglede, var forbindelsen til produktsættet: Et sæt skulle automatisk indsættes som et afsnit med den udskriftstilstand, der er gemt i sættet. Uden modulet skal man oprette og konfigurere afsnitslinjen manuelt, hver gang man indsætter et sæt.

Hvad er forskellen på »Skjul priser« og »Skjul indhold«?

»Skjul priser« (collapse_prices) lader linjerne stå, men viser ingen beløb på dem, kun afsnittets samlede beløb. »Skjul indhold« (collapse_composition) skjuler linjerne helt, så kunden kun ser afsnittet med det samlede beløb.

Indgår en skjult linje i fakturaens samlede beløb?

Ja. Skjulte linjer indgår fortsat i det samlede beløb, afgiftsberegningen og bogføringen. Markeringen vedrører kun visningen på udskriften og i kundeportalen.

Kan jeg ændre udskriftstilstanden, når jeg indsætter sættet?

Ja. Tilstanden hentes som standard fra sættet, men kan ændres i guiden for den enkelte indsættelse. Selve sættet ændres ikke.

Hvad sker der, hvis jeg slår Indsæt som afsnit fra?

Så fungerer guiden som i OCA-modulet sale_product_set: Sættets linjer indsættes uden afsnit.

Installerer modulet uventet andre moduler?

Ikke selve modulet. Med OCA-repositories kan der dog blive installeret auto_install-moduler, hvis man lægger hele repositories i addons_path. Opret derfor kun links til de to nødvendige moduler (se afsnittet om installation).

Virker modulet med Odoo Enterprise?

Modulet afhænger kun af Community-moduler (sale_management, account) og de to OCA-moduler. Vi bruger det på Odoo 19 Community. Vi har ikke testet det på Enterprise.

Hvilken licens har modulet?

AGPL-3. Du må bruge, tilpasse og videredistribuere det, så længe ændringerne forbliver under samme licens. Fejlrapporter og forbedringsforslag er velkomne som issues i GitHub-repositoriet .

Hjælp fra Biricon IT Services

Vi hjælper små og mellemstore virksomheder med deres Odoo-installationer: fra implementering, tilpasninger og migreringer til den løbende drift. Vi hjælper gerne, hvis du vil tilpasse produktsæt, tilbudsskabeloner eller rapporter i Odoo 19 til dine arbejdsgange, eller hvis du vil have et modul som dette gennemgået og installeret sikkert i dit produktionsmiljø. Du finder flere af vores egenudviklede løsninger på bloggen, for eksempel Sammenlæg fakturakladder og Match automatisk bankbetalinger med fakturaer.

Kontakt os nu

Status: 2. november 2026. Modulversion 19.0.1.0.0. Eksempeldataene i illustrationerne er frit valgt, beløb netto.