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
Indhold i dette indlæg
- Problemet: produktsæt uden indflydelse på udskriften
- Hvad modulet gør
- De tre udskriftstilstande i detaljer
- Skjulte linjer og fast pris
- Teknisk implementering
- Installation uden uønskede bivirkninger
- Betjening trin for trin
- Tests
- Begrænsninger og juridisk bemærkning
- Ofte stillede spørgsmål
- Støtte fra Biricon IT Services
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
- 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.
- 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.
- 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.
- 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 positioner | Sektionslinje med sættets navn, mængde i navnet, linjer nedenunder |
| Sektioner i sættet forbliver sektioner og ødelægger strukturen | Sektioner i sættet bliver til undersektioner (line_subsection) |
| Udskriften altid ens | Udskriftstilstand pr. sæt, kan ændres ved indsættelsen |
| Priskorrektion synlig på tilbud og faktura | Linjen kan skjules, men tæller med i summen |
| Pris kun fra prislisten | Valgfri fast pris pr. sætlinje |
| Layoutet ender ved tilbuddet | Layout 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),
}
Tilstand (print_mode) | Visning i sættet | Standardindstilling i Odoo | Udskrift |
|---|---|---|---|
detailed | Hver linje med pris | ingen | Sektion, alle linjer med beløb |
total_only | Linjer uden priser, kun sætsum | collapse_prices (Skjul priser) | Sektion med sum, linjer kun med betegnelse og mængde |
summary | Kun én linje med sætsum | collapse_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.
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.
Udvidede modeller og felter
| Model | Tilføjelse |
|---|---|
product.set | Felterne insert_as_section (boolesk, som standard slået til) og print_mode (valg, obligatorisk felt, standard detailed) |
product.set.line | Felterne hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() videregiver dem til tilbudslinjen |
sale.product.set.wizard | Felterne insert_as_section, section_name, print_mode; _prepare_order_lines() opbygger sektionslinjen |
sale.order / sale.order.line | Feltet hide_in_report (med copy=True); _get_order_lines_to_report() springer skjulte linjer over; _prepare_invoice_line() videregiver 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 |
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ætterhide_in_reportogprice_unitkun, 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:
- Klon de to OCA-repositories fladt (
git clone --depth 1 --branch 19.0). - 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. - Installér på en testdatabase, og kontrollér, hvilke moduler der får status installeret.
- 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
- 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.
- I tilbuddet: Klik på Tilføj sæt, vælg sæt og mængde. Sektionsnavn og udskriftstilstand kan ændres før indsættelsen.
- Skjul enkelte linjer: Aktivér kolonnen Udskriv ikke i tilbuddet eller fakturaen.
- 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.
Status: 2. november 2026. Modulversion 19.0.1.0.0. Eksempeldataene i illustrationerne er frit valgt, beløb netto.