Gå til indhold

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

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

Odoo 19-modulet Sale Product Set Layout: produktsæt som sektion i tilbuddet med udskriftstilstand, skjulte linjer og fast pris

Den, der sælger pakker i Odoo, f.eks. et måleinstrument med tilbehør og montering, en arbejdsplads med hardware og opsætning eller en vedligeholdelsespakke bestående af flere ydelser, har brug for to ting på samme tid i tilbuddet: Sammensætningen skal stå i ordren, så lager, teknik og fakturering ved, hvad der leveres. På udskriften til kunden skal hver enkelt position med hver enkelt pris dog ikke i alle tilfælde 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 afgør ikke, hvordan udskriften ser ud. Til det formål har vi udviklet modulet Sale Product Set Layout (sale_product_set_layout). Det er skrevet til Odoo 19 og er tilgængeligt gratis på GitHub som fri software under AGPL-3-licensen:

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

Problemet: produktsæt uden indflydelse på udskriften

Et produktsæt er i Odoo en forberedt liste over produkter med mængder. I stedet for at søge ti varer frem enkeltvis vælger man i tilbuddet Tilføj sæt og får alle linjer på én gang. Det sparer tid og forhindrer glemte positioner. Modulet sale_product_set fra repositoryet OCA/sale-workflow gør netop dette; selve sættene stammer fra product_set i repositoryet OCA/product-attribute.

I praksis er der tre huller:

  • Ingen synlig gruppering. Sætlinjerne havner som løse positioner i tilbuddet. Efter indsættelsen kan man ikke længere se, hvilke linjer der hører sammen, og sættets samlede sum står ingen steder.
  • Ingen indflydelse på udskriften. Nogle gange skal kunden se hver position med pris, andre gange kun pakkeprisen, og andre gange kun én enkelt linje. Det er en beslutning pr. sæt eller pr. kunde, ikke noget, der skal gøres i hånden for hvert enkelt tilbud.
  • Priskorrektioner er uskønne. Skal en pakke have en fast pris, der afviger fra summen af listepriserne, kræver det en korrektionslinje. Den hører hjemme i beregningen, men ikke nødvendigvis på kundens papir.

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.

Hvad modulet gør

  1. Indsæt sæt som sektion. Guiden Tilføj sæt opretter en sektionslinje med sættets navn, f.eks. 2 × Målekit Standard, og placerer sætlinjerne under den. Sektioner inden i sættet bliver til undersektioner.
  2. Udskriftstilstand pr. sæt. I sættet fastlægges én gang, hvordan det skal udskrives. Ved indsættelsen kan tilstanden ændres i guiden for netop dette tilfælde.
  3. Skjul linjer. Enkelte linjer kan markeres som Udskriv ikke (på sættet: Skjult). De tæller med i summen, men bliver hverken udskrevet eller vist i kundeportalen. Markeringen føres videre fra tilbuddet til fakturaen.
  4. Fast pris pr. sætlinje. En sætlinje kan have en fast enhedspris, der erstatter prisen fra prislisten, f.eks. til en skjult priskorrektion.
Uden modulet (kun sale_product_set)Med sale_product_set_layout
Sætlinjer som løse positionerSektionslinje med sættets navn, mængde i navnet, linjer nedenunder
Sektioner i sættet forbliver sektioner og ødelægger strukturenSektioner i sættet bliver til undersektioner (line_subsection)
Udskriften altid ensUdskriftstilstand pr. sæt, kan ændres ved indsættelsen
Priskorrektion synlig på tilbud og fakturaLinjen kan skjules, men tæller med i summen
Pris kun fra prislistenValgfri fast pris pr. sætlinje
Layoutet ender ved tilbuddetLayout og skjulning 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:

# Druckmodus -> (collapse_prices, collapse_composition) der Abschnittszeile
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 OdooUdskrift
detailedHver linje med prisingenSektion, alle linjer med beløb
total_onlyLinjer uden priser, kun sætsumcollapse_prices (Skjul priser)Sektion med sum, linjer kun med betegnelse og mængde
summaryKun én linje med sætsumcollapse_composition (Skjul sammensætning)Kun sektionslinjen med summen

Fordi modulet bruger standardindstillingerne i Odoo 19 og intet eget udskriftsformat, fungerer tilbud, ordrebekræftelse, faktura og kundeportal uden tilpassede rapportskabeloner. Hvis du har egne rapportskabeloner, der tager hensyn til sektionsindstillingerne, får du også den samme adfærd.

Skjulte linjer og fast pris

Et almindeligt tilfælde: En pakke har en pakkepris, der ligger under summen af enkeltpriserne. I stedet for at bøje enkeltpriserne bruger man en korrektionslinje med fast pris (f.eks. −50,00 € pr. sæt) og skjuler den. I backend er fakturaen let at følge, og på udskriften står pakkeprisen.

Priskorrektion synlig og markeret i backend, ikke synlig på udskriften af tilbud, PDF, faktura og portal, summen identisk

Vigtigt for forståelsen: Markeringen hide_in_report ændrer ikke noget ved beregningen. Mængde, pris, rabat, moms, summer og posteringer opstår uændret gennem standard-Odoo. Linjen mangler kun på udskriften og i portalen.

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

Teknisk implementering

Modulet er bevidst lille: Det udvider eksisterende modeller og overskriver kun de steder, hvor Odoo afgør, hvilke linjer der 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, som standard slået til) og print_mode (valg, obligatorisk felt, standard detailed)
product.set.lineFelterne hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() videregiver dem til tilbudslinjen
sale.product.set.wizardFelterne insert_as_section, section_name, print_mode; _prepare_order_lines() opbygger sektionslinjen
sale.order / sale.order.lineFeltet hide_in_report (med copy=True); _get_order_lines_to_report() springer skjulte linjer over; _prepare_invoice_line() videregiver 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

Skjulning i tilbuddet

Odoo 19 finder de tilbudslinjer, der skal udskrives, i _get_order_lines_to_report(). Modulet tilføjer et filter dér. Et indgreb i rapportskabelonen er ikke nødvendigt: 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
        )

Videreførelse til fakturaen

Når fakturalinjen genereres ud fra tilbudslinjen (_prepare_invoice_line()), kopieres markeringen. På fakturasiden er der i Odoo 19 flere veje, som linjer kan nå ind i rapporten ad: hovedlisten, linjerne i en sektion og linjerne i en undersektion. 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: sektion i stedet for løse linjer

Guiden sale.product.set.wizard fra sale_product_set genererer tilbudslinjerne i _prepare_order_lines(). Modulet overskriver denne metode. Er Indsæt som sektion slået fra, kalder det blot den oprindelige metode, og adfærden er så som i OCA-modulet. Ellers oprettes først sektionslinjen med de to standardafbrydere, derefter følger sætlinjerne. Sektioner inden i sættet bliver til undersektioner, så de ikke ødelægger 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

Beslutninger, man ikke ser

  • Adskilte beregningsmetoder i guiden. Sektionsnavn, udskriftstilstand og Indsæt som sektion har hver sin egen compute-metode (store=True, readonly=False). Hvis alle tre delte samme metode, ville en eksplicit angivet værdi for ét felt nulstille de andre. Det er især mærkbart, når guiden oprettes fra kode eller tests med værdier.
  • Sektionsnavn med mængde. Ved en mængde forskellig fra 1 står den i navnet (2 × Målekit Standard, med komma som decimalseparator), så man på udskriften kan se, at der er tale om to sæt og ikke ét sæt med dobbelt mængde.
  • Fast pris kun på produktlinjer. prepare_sale_order_line_values() sætter hide_in_report og price_unit kun, hvis sætlinjen ikke er en sektions- eller notelinje.
  • Standard-Odoo i stedet for egenudvikling. Udskriftstilstand og skjulning bruger eksisterende hooks og felter i Odoo 19. Det gør opdateringer håndterbare: Ændrer Odoo rapportskabelonen, forbliver modulet gyldigt, så længe metoderne findes.

Installation uden uønskede bivirkninger

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

auto_install-fælden ved OCA-moduler. Mange OCA-repositories indeholder moduler med auto_install. Så snart deres afhængigheder er til stede, installerer Odoo dem uopfordret ved næste installation eller opdatering. Den, der derfor lægger hele OCA-repositoryet i addons_path på en produktionsinstans, får muligvis moduler, som ingen ønskede. For OCA/product-attribute gælder det f.eks. purchase_product_template_tags. Vores fremgangsmåde:

  1. Klon de to OCA-repositories fladt (git clone --depth 1 --branch 19.0).
  2. I en separat mappe linkes kun de to nødvendige moduler via symlink, og denne mappe tilføjes til addons_path, ikke de hele repositories.
  3. Installér på en testdatabase, og kontrollér, hvilke moduler der får status installeret.
  4. Først derefter installeres der i produktion, med --workers=0, så kaldet 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

Nye felter og visninger findes i databasen efter installationen. Kører tjenesten i flerprocestilstand, genindlæser workerne den ændrede registry via databasesignalet. Ændringer i modulets Python-filer træder først i kraft efter en genstart af tjenesten.

Betjening trin for trin

  1. Opret sæt: Salg → Produkter → Produktsæt. Navngiv sættet, vælg Indsæt som sektion og Udskriftstilstand, og indtast produkterne med mængder. Forsyn korrektionslinjer med Skjult og Fast pris.
  2. I tilbuddet: Klik på Tilføj sæt, vælg sæt og mængde. Sektionsnavn og udskriftstilstand kan ændres før indsættelsen.
  3. Skjul enkelte linjer: Aktivér kolonnen Udskriv ikke i tilbuddet eller fakturaen.
  4. Kontrollér: Åbn forhåndsvisning eller PDF. Kunderne ser den samme visning i portalen.

Tests

Modulet leveres med seks automatiserede tests, som vi kører mod en testdatabase, før noget går i produktion:

  • Indsættelse som sektion inklusive udskriftstilstand og undersektion
  • Skjult linje mangler i de linjer, der skal udskrives
  • Tilstanden Én linje (summary)
  • Indsættelse uden sektion, den oprindelige adfærd
  • Fakturaen overtager markeringen hide_in_report, linjen mangler i _get_move_lines_to_report(), sektionssummen indeholder den fortsat
  • Faktura i tilstanden Kun samlet pris viser ikke den skjulte linje i sektionslinjerne, sektionssummen forbliver fuldstændig
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 moms. Det ændrer kun, hvad der udskrives og vises i portalen.
  • Skjulte linjer er synlige i backend. Markeringen vedrører udskrift og portal, ikke medarbejdernes skærmvisning.
  • Kontrollér fakturaoplysninger. Skjulte linjer tæller med i summen, men står ikke på bilaget. Om det er foreneligt med kravene til fakturaer i dit land, f.eks. den foreskrevne beskrivelse af leverede varer eller udførte ydelser, bør du afklare med din skatterådgiver før brug på fakturaer. På tilbud er skjulning mindre problematisk. Indlægget er ikke juridisk rådgivning.
  • Testet til Odoo 19. Vi har udviklet og testet modulet til Odoo 19.0 Community. Det bruger sektionsindstillinger, som findes i denne form i Odoo 19. Til andre versioner har vi ikke afprøvet det.

Ofte stillede spørgsmål

Hvorfor har jeg brug for modulet, når Odoo 19 allerede kan sektioner?

Sektionsindstillingerne findes i selve Odoo. Det, der manglede, var forbindelsen til produktsættet: Et sæt skulle automatisk indsættes som sektion, med den udskriftstilstand, der er gemt i sættet. Uden modulet skal man efter hver indsættelse oprette og konfigurere sektionslinjen i hånden.

Hvad er forskellen på »Skjul priser« og »Skjul sammensætning«?

»Skjul priser« (collapse_prices) lader linjerne stå, men viser ingen beløb, kun summen ved sektionen. »Skjul sammensætning« (collapse_composition) skjuler linjerne helt, så kunden kun ser sektionen med summen.

Tæller en skjult linje med i fakturasummen?

Ja. Skjulte linjer forbliver i summen, momsen og posteringen. Markeringen vedrører kun visningen på udskriften og i kundeportalen.

Kan jeg stadig ændre udskriftstilstanden ved indsættelsen?

Ja. Tilstanden kommer som standardværdi fra sættet, og i guiden kan den ændres for den pågældende indsættelse. Selve sættet forbliver uændret.

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

Så opfører guiden sig som i OCA-modulet sale_product_set: Sætlinjerne indsættes uden sektion.

Installerer modulet uventet flere moduler?

Selve modulet gør ikke. Ved OCA-repositories kan der dog komme auto_install-moduler med, hvis man lægger hele repositories i addons_path. Link derfor kun de to nødvendige moduler (se afsnittet Installation).

Kører 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. På Enterprise har vi ikke testet det.

Under hvilken licens udgives modulet?

Under AGPL-3. Du må bruge, tilpasse og videregive det, så længe ændringer forbliver under samme licens. Fejl og forbedringsforslag er velkomne som issue i GitHub-repositoryet .

Støtte fra Biricon IT Services

Vi passer Odoo-installationer for små og mellemstore virksomheder: fra implementering over tilpasninger og migreringer til den løbende drift. 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 sikkert bragt ind i din produktionsinstans, hjælper vi dig gerne. Flere egenudviklinger finder du i vores blog, f.eks. Sammenlægning af fakturakladder og Automatisk tilknytning af bankbetalinger til fakturaer.

Kontakt os nu

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