bfc695ed6a
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>
100 lines
5.6 KiB
Markdown
100 lines
5.6 KiB
Markdown
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
|
|
|
|
Customer requirement: items declared as a **set** in `[dbo].[mfr__items]`
|
|
(`[Type] = 'set'`) should normally be shown as a single **set price** on the
|
|
invoice instead of being broken up into their member items and summed.
|
|
|
|
Three display modes (switchable in the invoice editor):
|
|
|
|
| Mode | Set line | Member items | Use as |
|
|
|---|---|---|---|
|
|
| **SetPrice** (default) | shown **with price** | shown **without price** | the new default |
|
|
| **ItemPrices** | shown as a heading **without price** | shown **with price** | the previous behaviour |
|
|
| **SetOnly** | shown **with price** | **removed** | compact |
|
|
|
|
> **Totals are unaffected.** The invoice total is taken from the registration
|
|
> balance (`InvoiceBalance` / `InvoiceBalance_net`), not by summing the rendered
|
|
> lines, so switching modes is purely presentational. The set price always
|
|
> equals the sum of its members (computed as a fallback when the set header
|
|
> carries no own price).
|
|
|
|
## Back-end (implemented + unit-tested)
|
|
|
|
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
|
|
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
|
|
`ShowPrice` and `IsSetHeader`. `ModeFromInvoiceOptions(...)` reads the mode
|
|
from the invoice options. Fully covered by `Fuchs.Tests/InvoiceSetPricingTests.cs`.
|
|
- `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
|
|
unchanged.
|
|
|
|
## Front-end contract (implemented in the invoice editor)
|
|
|
|
Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
|
`wwwroot/web/fis.inv.de.js` via gulp `min:js`):
|
|
|
|
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 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.)
|
|
|
|
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).
|
|
The back-end intentionally stays the single, tested authority for *how* a chosen
|
|
mode maps to printed lines, so the editor only needs to pick the mode and tag the
|
|
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 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.
|