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>
5.6 KiB
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)→ orderedInvoiceSetLines, each withShowPriceandIsSetHeader.ModeFromInvoiceOptions(...)reads the mode from the invoice options. Fully covered byFuchs.Tests/InvoiceSetPricingTests.cs.FuchsPdf.ApplyInvoicerenders throughInvoiceSetPricing.Build: lines withShowPrice == falserender blank price/total cells (not0,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):
-
Mode — a 3-way switch (
$inv.ssetmode, menu entrysetm, label$ict.setm) writes the choice ontoadmin.setmode(setprice|itemprices|setonly). The back-endFdsInvoiceData.BuildInvoiceOptionsturns that into thesetmode:<mode>token inside@InvoiceOptions(defaultsetpriceomitted), persisted byfds__createInvoice_Detailsand read back byInvoiceSetPricing.ModeFromInvoiceOptions. This rides the sameadminchannel as§13b(the posted payload is{admin, req, sms, new}—invis not sent). -
Item shape —
$inv.invSumUpdatenow posts each request block'sitems[]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-screenitm/coobjects, whichFdsInvoiceData.InvoiceItemsdoes not read — so line items never reached the C# PDF. This change closes that gap for all invoices, not just sets.) -
Set flags —
invSumUpdatetags items as it buildsitems[]: an item withtype === '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__itemshas aType='set'header but no explicit member link, so this "header claims the following items in its block" rule is the convention — adjust ininvSumUpdateif 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 (
typetext/title) now render a blank price/total (InvoiceSetPricing.IsNoPriceLine), instead of0,00 €. - VAT rate detection no longer reads line items (the old
items is List<object>test failed on NewtonsoftJArrayand pinned@InvoiceVAT_1to19); it now comes fromsms.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.