Files
Fuchs_Intranet/Fuchs/Docs/INVOICE_SET_PRICING.md
T
Stefan bfc695ed6a 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>
2026-06-06 10:48:14 +02:00

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) → ordered InvoiceSetLines, 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 flagsinvSumUpdate 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.