"Set mit Preis" / "Nur Set mit Preis" collapsed every block into an inserted "Gesamtumfang pauschal" row, nulled/removed all lines and cleared the user-defined set structure. Both modes now convert every still-unconverted top-level set: the set line takes the sum of its subtree, its members render without a price (setprice) or are removed from itm + items (setonly); lines outside a set stay individually priced and several sets stay separate lines. Nested sets are priced once via their top-level set. One conversion primitive (ConvertSetHeader) now backs all three triggers; item.setprice is a no-op on an already-converted set (a repeated delta used to reset the set price to 0). The header's price_net is mirrored too (flat PDF path unit price), and Build treats a null total_net as "no price" like the flat path. The editor checks for a set before offering the modes and the menu entry stays available for sets added later. ADR 0009 is superseded by 0019; INVOICE_SET_PRICING.md rewritten accordingly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7.0 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Accepted | 2026-09-10 |
|
0019 — The two set-price menu modes are keyed on the set, not on the service-request block
Context
ADR 0009 defined the two menu modes
("Set mit Preis" / "Nur Set mit Preis") as per-service-request-block conversions: every block
was collapsed into one inserted "Gesamtumfang pauschal" set row carrying the block's whole sum,
every original line of the block was price-nulled or removed, and any existing set membership was
neutralised (SetItmId/setId cleared, existing set rows demoted). Grouping by SetItmId was
explicitly rejected there.
ADR 0016 then made set membership an explicit, user-editable parent link with a dialog to define exactly which line belongs to which set, and let sets nest. With that in place, the block-wide collapse actively destroys the structure the user just defined: it discards their sets, prices lines that were never meant to be in a set, and can never show two sets of one service request as two lines.
The product owner has therefore redefined the two modes again, this time onto the set structure:
- Choosing "Set mit Preis – Positionen ohne Preis" must give each set one price, show that set's positions without a price, and leave every item that is in no set listed individually with its own price. Sets and single items together make up the invoice total.
- Choosing "Nur Set mit Preis – Positionen ausgeblendet" must hide the positions belonging to the sets, while the sets themselves and the items outside any set stay visible.
- Several sets stay several separate lines — they are never merged into one.
This decision records that redefinition and supersedes ADR 0009. Function 1 (the inline set-item switch) is carried over from 0009 unchanged, as is ADR 0008's rule that every calculation happens server-side and the PDF renders exactly what the editor shows.
Decision
There are still three set-price operations, but they now share one grouping key — the set — and one conversion primitive:
-
Inline set-item switch (
item.setprice) — unchanged from ADR 0009, except that it is now a thin wrapper over the shared primitive and is a no-op on an already-converted set (own price non-zero). Without that guard a repeated delta would re-sum the members the first conversion had already nulled and reset the set price to 0. -
"Set mit Preis" (
block.setprice) — per set, irreversible. For every still-unconverted top-level set of every block: sum the set's transitive descendants onto the set line and set each descendant's price fields tonull(empty cell, excluded from the sum — never0, which would print a genuine0,00 €). The descendant rows are kept. -
"Nur Set mit Preis" (
block.setonly) — per set, irreversible. As above, and additionally remove the whole subtree of every top-level set (its members and any nested set line) from both line shapes (itmanditems). A set that was already converted keeps its price and still has its members removed.
Properties common to modes 2 and 3:
- The set is the grouping key. No row is inserted, no row is demoted, no
SetItmId/setIdis cleared. The user-defined structure (ADR 0016) is what the modes act on and what survives them. - Lines outside every set are untouched — they keep their individual price and stay listed as ordinary positions. Sets and standalone lines together form the total.
- Only top-level sets are priced. A nested set is part of its parent's subtree and is blanked (mode 2) or removed (mode 3) with it, so no value is ever counted twice.
- Empty sets and set-free invoices are no-ops — no version bump, no history entry. The editor therefore checks the authoritative state for a set before opening the mode dialog and otherwise tells the user to define a set first ("Set-Struktur bearbeiten").
- Repeatable, still one-way. Running a mode again converts only sets that are still
unconverted, so the menu entry stays available (the ADR 0009
admin.setmodegate on the entry is gone); no operation converts anything back. - Mutation, not display. The change is written into
InvoiceDraftSession.Reqserver-side; there is no render-time grouping flag. A residualsetmode:InvoiceOptionstoken from the pre-0009 display-mode model still degrades safely (ignored) and is not re-persisted. - Total conserved, editor and PDF identical (ADR 0008): a blanked member contributes nothing, a removed one is gone, and in both cases its value already sits on the set line.
Consequences
InvoiceDraftEditService.ApplyBlockSetPricing(block aggregation, insertedbset_*row,DemoteSet,setIdneutralisation) is gone;ApplySetPricingwalks the set forest throughInvoiceSetStructureand delegates the per-set work toConvertSetHeader, whichApplyItemSetPricenow uses too. One conversion rule, three triggers.- Because membership is preserved,
InvoiceSetPricing.Build(andBuildSetDisplay) governs the rendering of the result again: a converted set's members are price-blanked, non-set lines keep their own price. The PDF path needs no special case for the menu modes. - The "Gesamtumfang pauschal" label and the notion of a service request as an implicit set no longer exist. A user who wants one price for a whole service request creates a set over its lines in the structure dialog first.
- Tests:
InvoiceDraftServiceTestscovers per-set pricing with a standalone item alongside, several sets kept separate, nested sets priced once, member removal in both line shapes, the already-converted set, the repeateditem.setpriceno-op, and the set-free/empty-set no-ops.
Alternatives considered
- Keep the block collapse and let the user pick the block — rejected: it still cannot express two sets in one service request, and it overwrites the structure ADR 0016 exists to let the user define.
- Price nested sets individually as well — rejected: their value is already contained in the top-level set's price, so both being priced would double the invoice's printed sum.
- Fall back to the old block behaviour when an invoice contains no set — rejected: it would make the same menu entry do two very different things. Without a set there is nothing to price as a set, so the modes are a no-op and the editor points the user at the structure dialog.
- Make the modes reversible display toggles again — rejected (as in ADR 0009): the set price must be a real, hand-editable value in the document, and "Nur Set mit Preis" must actually drop the member lines.