Add user-defined invoice set structure dialog (ADR 0016)
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
Lets a back-office user correct/define which invoice line belongs to which "set" and how deep it is nested, instead of relying only on the implicit "everything after a Set header until the next Set" grouping fds__prepInvoice derives. Sets may contain sets; every change applies immediately to the cached draft session and is reflected in the editor and PDF (ADR 0006/0008). Backend: - InvoiceSetStructure: pure parent-link forest (assign, change level, create, place/move-sibling, depth-first linearise + level recompute). - InvoiceDraftSetStructureDeltas: new inv/dpatch targets set.assign, set.level, set.create, set.move. - InvoiceSetPricing.Build is now order-preserving and nesting-aware (governed by the nearest converted ancestor); the set-item switch sums a header's transitive descendants so nested conversions never double-count. - FuchsPdf indents nested set members per level, mirroring the editor. Frontend: - New fis.inv_sets.js: "Set-Struktur bearbeiten" dialog — per-block table of all lines with drag & drop reorder (before/after/into), ▲▼ order and ◀▶ level buttons, fold/unfold per set (view-only), and a create bar for new Set/Text lines. Posts one delta per action; the dialog is a pure view of the server state (ADR 0006). - _Layout.cshtml / fis_main.js: cache-bust the dynamically imported per-module bundles (web/fis.<mdl>.<locale>.js/.css) with an asset version derived from the newest bundle write time, so a gulp rebuild is picked up without a stale cached module. Docs: ADR 0016, live-draft-editing concept, INVOICE_SET_PRICING.md, USER_GUIDE.md, ARCHITECTURE.md updated. Tests: InvoiceSetStructureTests (new) + additions to InvoiceDraftServiceTests/InvoiceSetPricingTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
|
||||
|
||||
> Governed by ADR [0009](Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)
|
||||
> (the two menu modes) and ADR [0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md)
|
||||
> (everything server-side, PDF == editor). ADR 0009 **redefined** the two menu modes from the
|
||||
> (the two menu modes), ADR [0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md)
|
||||
> (everything server-side, PDF == editor) and ADR
|
||||
> [0016](Decisions/0016-user-defined-set-structure.md) (set membership is an explicit,
|
||||
> user-editable parent link; sets may nest — see "Set structure" below). ADR 0009 **redefined** the two menu modes from the
|
||||
> reversible, non-mutating display toggles this document previously described into irreversible,
|
||||
> per-service-request-block mutations — the text below reflects the redefinition.
|
||||
|
||||
@@ -40,6 +42,37 @@ for the full breakdown.
|
||||
> or removes (mode 3), and the set-item switch (mode 1) writes exactly the members' sum onto the
|
||||
> header.
|
||||
|
||||
## Set structure (ADR 0016): which line belongs to which set
|
||||
|
||||
`SetItmId` is no longer only what `fds__prepInvoice` derived from row order — it is the
|
||||
**explicit parent link** of every cached line, and the user can edit it:
|
||||
|
||||
- **Initial structure** = the implicit derivation (every item after a zero-priced Set header
|
||||
until the next one). Nothing changes for drafts the user never touches.
|
||||
- **"Set-Struktur bearbeiten"** (editor menu → `$inv.sets`, `fis.inv_sets.js`) shows every
|
||||
line per service-request block (Pos., Typ, Menge, Bezeichnung, indented by level). Drag a
|
||||
line onto a set → `set.assign { parent }` (it becomes the set's last member); drag it onto
|
||||
the block heading / the "oberste Ebene" zone → `set.assign { parent: "" }` (pulled out, placed
|
||||
after the subtree it came from). Drag a line to the **top/bottom edge** of another row →
|
||||
`set.move { anchor, where: before|after }` (reorder: it becomes that row's sibling; a set
|
||||
travels with its members, folded or not); ▲ / ▼ post `set.move` −1 / +1 (one step among
|
||||
siblings). The ◀ / ▶ buttons post `set.level` −1 / +1 (outdent to the
|
||||
grandparent / indent under the nearest preceding sibling set). Sets can be folded via the
|
||||
triangle (view state only). The per-block bar creates a
|
||||
new **Set** (zero-priced header, convertible via function 1) or **Text** line inside any set
|
||||
or at the top level → `set.create { typ, title, parent }`.
|
||||
- **Sets may contain sets.** A set dropped onto another set keeps its own members (one level
|
||||
deeper). Function 1 (`item.setprice`) sums the header's **transitive** descendants and nulls
|
||||
them all; converting inner-then-outer or outer directly conserves the total either way.
|
||||
- **Only sets are drop targets; a line never leaves its block.** Cycles (a set onto its own
|
||||
descendant) and no-op moves are rejected server-side (`InvoiceSetStructure`), the browser
|
||||
merely greys them out.
|
||||
- **The server linearises** after each structural delta (depth-first: set, then its members,
|
||||
recursively; siblings keep their order) and publishes `lvl`/`level`; `InvoiceSetPricing.Build`
|
||||
is order-preserving, so editor and PDF show the same sequence and the same per-level indent.
|
||||
Every change is applied immediately to the cached session and pushed back via
|
||||
`draftReady`/`dstate`; the dialog re-renders from that state.
|
||||
|
||||
## Back-end
|
||||
|
||||
> **Migration status (ADR 0009).** The code below still reflects the previous
|
||||
@@ -51,9 +84,14 @@ for the full breakdown.
|
||||
> migration lands so it stays a faithful description of the code.
|
||||
|
||||
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
|
||||
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
|
||||
`ShowPrice` and `IsSetHeader`. `ModeFromInvoiceOptions(...)` reads the mode
|
||||
`SetDisplayMode` + `Build(items, mode)` → `InvoiceSetLine`s **in input order**, each with
|
||||
`ShowPrice`, `IsSetHeader` and `Level` (nesting depth). A line is governed by its nearest
|
||||
*converted* ancestor set (blank in SetPrice, dropped in SetOnly); an unconverted header is
|
||||
blank with individually priced members. `ModeFromInvoiceOptions(...)` reads the mode
|
||||
from the invoice options. Fully covered by `Fuchs.Tests/InvoiceSetPricingTests.cs`.
|
||||
- `Fuchs/code/InvoiceSetStructure.cs` — the pure set forest (parent links, levels, depth-first
|
||||
linearisation, assign / change level / create); `Fuchs/Services/InvoiceDraftSetStructureDeltas.cs`
|
||||
maps the `set.*` patch targets onto it. Covered by `InvoiceSetStructureTests` + `InvoiceDraftServiceTests`.
|
||||
- `FuchsPdf.ApplyInvoice` renders through `InvoiceSetPricing.Build`: lines with
|
||||
`ShowPrice == false` render **blank** price/total cells (not `0,00 €`), and the
|
||||
set header line is rendered emphasised. Invoices without sets pass through
|
||||
@@ -82,11 +120,12 @@ Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
||||
C# PDF. This change closes that gap for **all** invoices, not just sets.)
|
||||
|
||||
3. **Set flags (function 1 only)** — `invSumUpdate` tags items as it builds `items[]`:
|
||||
an item with `type === 'set'` is a header (`id` = its set id); a member item's
|
||||
`setId` is taken directly from the server-computed `SetItmId` field on the row
|
||||
(`rrx.SetItmId`, populated by `fds__prepInvoice`'s `[SetItmID]` window function,
|
||||
anchored on the still-unconverted, zero-priced `'set'` header that owns it) —
|
||||
**not** re-derived from row order in the browser. `mfr__items` itself still has no
|
||||
an item with `type === 'set'` is a header (`id` = its set id); a member's
|
||||
`setId` is taken directly from the `SetItmId` field on the row
|
||||
(`rrx.SetItmId` — initially populated by `fds__prepInvoice`'s `[SetItmID]` window function,
|
||||
anchored on the still-unconverted, zero-priced `'set'` header that owns it, and thereafter
|
||||
maintained by the server per ADR 0016, including for a **nested set**, which is a member of
|
||||
its parent set) — **not** re-derived from row order in the browser. `mfr__items` itself still has no
|
||||
explicit member link; `fds__prepInvoice` computes `SetItmId` per request from the
|
||||
item list, so only items the server actually attributes to a set are tagged, and
|
||||
unrelated items following a set in the list are never swept in. The header row's
|
||||
|
||||
Reference in New Issue
Block a user