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
Artiklens indhold
- Problemet: Produktsæt uden indflydelse på udskriftens udseende
- Det gør modulet
- De tre udskriftstilstande i detaljer
- Skjulte linjer og fast pris
- Teknisk implementering
- Installation uden uønskede bivirkninger
- Brug trin for trin
- Test
- Begrænsninger og juridisk bemærkning
- Ofte stillede spørgsmål
- Hjælp fra Biricon IT Services
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
- 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.
- 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.
- 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.
- 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 poster | Afsnitslinje med sættets navn, antal i navnet og linjerne nedenunder |
| Afsnit i sættet forbliver afsnit og bryder strukturen | Afsnit i sættet bliver til underafsnit (line_subsection) |
| Udskriften ser altid ens ud | Udskriftstilstand for hvert sæt, som kan ændres ved indsættelsen |
| Priskorrektion synlig på tilbud og faktura | Linjen kan skjules, men indgår stadig i summen |
| Prisen hentes kun fra prislisten | Mulighed for fast pris på hver sætlinje |
| Layoutet gælder kun tilbuddet | Layout 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),
}
Tilstand (print_mode) | Visning i sættet | Standardindstilling i Odoo | Udskriftens udseende |
|---|---|---|---|
detailed | Hver linje med pris | ingen | Afsnit, alle linjer med beløb |
total_only | Linjer uden priser, kun sættets samlede pris | collapse_prices (Skjul priser) | Afsnit med samlet pris, linjer kun med betegnelse og antal |
summary | Kun én linje med sættets samlede pris | collapse_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.
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.
Udvidede modeller og felter
| Model | Tilføjelse |
|---|---|
product.set | Felterne insert_as_section (boolesk, aktiveret som standard) og print_mode (valgfelt, obligatorisk, standardværdi detailed) |
product.set.line | Felterne hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() overfører dem til tilbudslinjen |
sale.product.set.wizard | Felterne insert_as_section, section_name, print_mode; _prepare_order_lines() opretter afsnitslinjen |
sale.order / sale.order.line | Feltet 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.line | Feltet 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 kunhide_in_reportogprice_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:
- Klon de to OCA-repositories med begrænset historik (
git clone --depth 1 --branch 19.0). - 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. - Installér i en testdatabase, og kontrollér, hvilke moduler der får status installeret.
- 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
- 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.
- I tilbuddet: Klik på Tilføj sæt, og vælg sæt og antal. Afsnitsnavn og udskriftstilstand kan ændres inden indsættelsen.
- Skjul enkelte linjer: Aktivér kolonnen Udskriv ikke i tilbuddet eller fakturaen.
- 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.
Status: 2. november 2026. Modulversion 19.0.1.0.0. Eksempeldataene i illustrationerne er frit valgt, beløb netto.