Invoice set pricing: wire editor + align editor/backend contract

Front-end (fis.inv_shared.js / fis.inv_txt_de.js, rebuilt bundles):
- 3-way set display switch (setprice/itemprices/setonly) via admin.setmode,
  emitted into InvoiceOptions by FdsInvoiceData.BuildInvoiceOptions.
- Each request block now posts items[] in the backend contract shape
  (title/desc/qty/price_net/total_net + set type/setId tags) via itemToContract.
- invcPayload normalises the editor model to the field names BuildInvoiceParams
  reads (sms totals -> new.total_net/total_gross, invoicetitle->title,
  loc->provisionlocation, admin.paymentterms->new.paymentterm, CustomerId->customerid).

Back-end:
- BuildInvoiceOptions adds the setmode token alongside §13b.
- VAT rate+amount now taken from sms.vat (HighestVat) instead of the broken
  items 'is List<object>' detection that pinned the rate to 19.
- InvoiceSetPricing blanks price/total cells for text/title heading lines.

Tests: set-pricing text-line blanking, HighestVat selection/parsing, updated
the VAT param test to the sms.vat contract. 180 passing.

Note: the second VAT slot (InvoiceVAT_2) stays unused by design; mixed-rate
invoices store only the highest rate (pre-existing, accepted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 10:48:14 +02:00
parent ebdb92713a
commit bfc695ed6a
12 changed files with 552 additions and 67 deletions
+55 -19
View File
@@ -29,23 +29,59 @@ Three display modes (switchable in the invoice editor):
set header line is rendered emphasised. Invoices without sets pass through
unchanged.
## Front-end contract (to wire in the invoice editor)
## Front-end contract (implemented in the invoice editor)
The editor already knows the set structure (from `fds__prepInvoice`) and assembles
the `invc` JSON. To drive the modes it must, when sending the invoice:
Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
`wwwroot/web/fis.inv.de.js` via gulp `min:js`):
1. **Mode** — add a token to `inv.InvoiceOptions` (CSV, alongside `§13b`):
- `setmode:setprice` (default — may be omitted), `setmode:itemprices`, or `setmode:setonly`.
A 3-way switch in the editor sets this token.
1. **Mode** — a 3-way switch (`$inv.ssetmode`, menu entry `setm`, label
`$ict.setm`) writes the choice onto `admin.setmode`
(`setprice` | `itemprices` | `setonly`). The back-end
`FdsInvoiceData.BuildInvoiceOptions` turns that into the
`setmode:<mode>` token inside `@InvoiceOptions` (default `setprice` omitted),
persisted by `fds__createInvoice_Details` and read back by
`InvoiceSetPricing.ModeFromInvoiceOptions`. This rides the **same `admin`
channel as `§13b`** (the posted payload is `{admin, req, sms, new}``inv`
is not sent).
2. **Item flags**in each service request's `items[]`:
- the **set header** item: `type: "set"`, `id: "<setId>"`, and `total_net` =
the set price (or `0` to let the back-end sum the members);
- each **member** item: `setId: "<setId>"` (matching the header's id).
- standalone items need no extra fields.
2. **Item shape**`$inv.invSumUpdate` now posts each request block's
`items[]` in the back-end contract shape via `$inv.itemToContract`:
`{ id, type, title, desc, qty, price_net, total_net, vat }`. (Previously the
editor only posted the legacy on-screen `itm`/`co` objects, which
`FdsInvoiceData.InvoiceItems` does not read — so line items never reached the
C# PDF. This change closes that gap for **all** invoices, not just sets.)
That is the entire contract — the back-end does the rest. The editor's running
**total stays the member sum in every mode**, matching the registration balance.
3. **Set flags**`invSumUpdate` tags items as it builds `items[]`: an item with
`type === 'set'` is a header (`id` = its set id); the **following items in the
same block become its members** (`setId` = the header's id) until the next set
header. `mfr__items` has a `Type='set'` header but **no explicit member link**,
so this "header claims the following items in its block" rule is the convention
— adjust in `invSumUpdate` if mfr later exposes a real grouping.
### Editor → backend field normalization (`$inv.invcPayload`)
The editor's internal model keeps the long-standing key names, but the migrated C#
`BuildInvoiceParams` reads different ones. At post time `$inv.invcPayload(d)` maps the
working model onto the exact field names the back-end reads (non-destructively):
`sms.ttn → new.total_net`, `sms.ttb → new.total_gross`, each `sms.vat` rate →
`new.vat_<rate>_net`, `new.invoicetitle → new.title`, `new.loc → new.provisionlocation`,
`admin.paymentterms → new.paymentterm`, `admin.CustomerId → admin.customerid`. Both
`req/save` and `req/sprep|sedit` post through it, so titles/balances/VAT now reach the
backend correctly.
VAT (rate **and** amount) is taken by the backend directly from the posted `sms.vat`
map via `FdsInvoiceData.HighestVat` (highest rate wins) — `invcPayload` therefore emits
no per-rate `vat_*` keys.
**Back-end fixes applied alongside the wiring:**
- Heading/free-text lines (`type` `text`/`title`) now render a **blank** price/total
(`InvoiceSetPricing.IsNoPriceLine`), instead of `0,00 €`.
- VAT rate detection no longer reads line items (the old `items is List<object>` test
failed on Newtonsoft `JArray` and pinned `@InvoiceVAT_1` to `19`); it now comes from
`sms.vat`, so non-19 % rates are stored correctly. Single-rate procs still store only
the highest rate.
The editor's running **total stays the member sum in every mode**, matching the
registration balance — switching modes is purely presentational.
### Why the switch lives in the editor
Set grouping is only known where the request/item tree is rendered (front-end).
@@ -55,9 +91,9 @@ items — it does not re-implement the pricing rules.
## Persistence note
Draft/preview PDFs render straight from the posted `invc` JSON, so the contract
works end-to-end for previews immediately. For a **finalised** invoice to re-render
in a chosen mode later, the per-item `type`/`setId` flags (and the `setmode`
option) must be persisted with the stored invoice items
(`fds__createInvoice_Details` / `fds__invoice_items`). `setmode` already persists
via `InvoiceOptions`; persisting the per-item set tags is a small SSDT + create-proc
change to make once the editor wiring is confirmed.
works end-to-end for previews and creation. `setmode` persists via
`InvoiceOptions`; the finalised document is rendered once and stored as a file, so
re-rendering from line items is not needed for correctness. Persisting the
per-item `type`/`setId` flags (an SSDT + `fds__createInvoice_Details` change) is
only required if a finalised invoice must be **re-generated** from stored items in
a different mode later — not done here.