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
+3 -3
View File
@@ -42,11 +42,11 @@ public class FdsDataTests
[Fact]
public void FdsInvoiceData_BuildInvoiceParams_PicksHighestVatAndMapsFields()
{
// two line items with different VAT rates → highest (19) selected
// VAT comes from the sms.vat map (rate → amount); highest rate (19) selected.
var jo = JObject.Parse(
"{ \"admin\": { \"type\": \"R\", \"customerid\": \"7\", \"p13b\": false }, " +
" \"new\": { \"title\": \"Bad\", \"total_gross\": \"119\", \"total_net\": \"100\", \"vat_19_net\": \"100\", \"paymentterm\": \"10d\" }, " +
" \"req\": [ { \"items\": [ { \"vat\": \"7%\" }, { \"vat\": \"19%\" } ] } ] }");
" \"new\": { \"title\": \"Bad\", \"total_gross\": \"119\", \"total_net\": \"100\", \"paymentterm\": \"10d\" }, " +
" \"sms\": { \"vat\": { \"7,0%\": 7.0, \"19,0%\": 19.0 } } }");
var inv = new FdsInvoiceData(jo);
var pl = inv.BuildInvoiceParams(change: false, invId: "");
+111
View File
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Fuchs.intranet;
using Newtonsoft.Json.Linq;
using Xunit;
using static OCORE.OCORE_dictionaries;
namespace Fuchs.Tests;
/// <summary>
/// Verifies that the posted <c>admin</c> flags map to the <c>@InvoiceOptions</c>
/// CSV the backend persists — the channel both §13b and the set-pricing
/// <c>setmode</c> token ride. The PDF reads the mode back via
/// <see cref="InvoiceSetPricing.ModeFromInvoiceOptions"/>.
/// </summary>
public class InvoiceOptionsTests
{
private static string InvoiceOptionsFor(object? admin)
{
var root = new JObject();
if (admin != null) root["admin"] = JObject.FromObject(admin);
var data = new FdsInvoiceData(root);
var p = data.BuildInvoiceParams(change: false, invId: "")
.First(x => x.ParameterName == "@InvoiceOptions");
return p.Value is DBNull or null ? "" : p.Value!.ToString()!;
}
[Fact]
public void NoFlags_EmptyOptions()
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r" }));
[Fact]
public void DefaultSetPrice_OmitsToken()
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "setprice" }));
[Theory]
[InlineData("itemprices", "setmode:itemprices")]
[InlineData("setonly", "setmode:setonly")]
[InlineData("ITEMPRICES", "setmode:itemprices")] // case-insensitive
public void SetMode_EmitsToken(string mode, string expected)
=> Assert.Equal(expected, InvoiceOptionsFor(new { type = "r", setmode = mode }));
[Fact]
public void UnknownSetMode_OmitsToken()
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "garbage" }));
[Fact]
public void P13bOnly_EmitsLegacyToken()
=> Assert.Equal("§13b", InvoiceOptionsFor(new { type = "r", p13b = true }));
[Fact]
public void P13bAndSetMode_EmitsBoth()
=> Assert.Equal("§13b,setmode:setonly", InvoiceOptionsFor(new { type = "r", p13b = true, setmode = "setonly" }));
// ── VAT: highest rate + amount taken from the sms.vat map ─────────────────
private static GenericObjectDictionary SmsWithVat(params (string rate, double amount)[] vat)
{
var map = new Dictionary<string, double>();
foreach (var (r, a) in vat) map[r] = a;
return new GenericObjectDictionary(new Dictionary<string, object> { ["vat"] = JObject.FromObject(map) });
}
[Fact]
public void HighestVat_PicksHighestRate_GermanFormatted()
{
var (rate, amt) = FdsInvoiceData.HighestVat(SmsWithVat(("19,0%", 38.0), ("7,0%", 7.0)));
Assert.Equal("19", rate); // integer, invariant
Assert.Equal("38", amt);
}
[Fact]
public void HighestVat_NonIntegerRate_Preserved()
{
var (rate, amt) = FdsInvoiceData.HighestVat(SmsWithVat(("10,5%", 5.25)));
Assert.Equal("10.5", rate); // invariant decimal point — safe for numeric(5,2)
Assert.Equal("5.25", amt);
}
[Fact]
public void HighestVat_EmptyOrMissing_ReturnsZero()
{
Assert.Equal(("0", "0"), FdsInvoiceData.HighestVat(null));
Assert.Equal(("0", "0"), FdsInvoiceData.HighestVat(new GenericObjectDictionary(new Dictionary<string, object>())));
}
[Fact]
public void HighestVat_FlowsIntoParams()
{
var root = new JObject
{
["admin"] = JObject.FromObject(new { type = "r" }),
["sms"] = JObject.FromObject(new Dictionary<string, object>
{
["vat"] = new Dictionary<string, double> { ["19,0%"] = 38.0 }
})
};
var pars = new FdsInvoiceData(root).BuildInvoiceParams(false, "");
Assert.Equal("19", pars.First(p => p.ParameterName == "@InvoiceVAT_1").Value);
}
[Fact]
public void ModeFromInvoiceOptions_RoundTripsBackendEmission()
{
// The token this side emits must parse back to the same mode on the PDF side.
Assert.Equal(SetDisplayMode.ItemPrices,
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", setmode = "itemprices" })));
Assert.Equal(SetDisplayMode.SetOnly,
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", p13b = true, setmode = "setonly" })));
}
}
+34
View File
@@ -144,6 +144,40 @@ public class InvoiceSetPricingTests
Assert.All(lines, l => Assert.False(l.IsSetHeader));
}
// ── Text/Title lines never show a price (any mode) ────────────────────────
private static Dictionary<string, object?> TextLine(string title) => new()
{
["type"] = "title", ["title"] = title, ["qty"] = "", ["price_net"] = "", ["total_net"] = ""
};
[Fact]
public void Build_TextLine_Standalone_NoPrice()
{
var items = new List<Dictionary<string, object?>>
{
TextLine("Leistungszeitraum 2026"),
Standalone("Anfahrt", "50.00", "50.00")
};
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice);
Assert.False(lines[0].ShowPrice); // heading — blank price
Assert.True(lines[1].ShowPrice); // real item — priced
}
[Fact]
public void Build_TextLine_AsSetMember_NoPriceEvenInItemPrices()
{
var items = new List<Dictionary<string, object?>>
{
SetHeader("10", "Bad-Komplettset", "1000.00"),
new() { ["type"] = "title", ["setId"] = "10", ["title"] = "Hinweis", ["total_net"] = "" },
Member("10", "Waschbecken", "600.00", "600.00")
};
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.ItemPrices);
var note = lines.First(l => l.Title == "Hinweis");
Assert.False(note.ShowPrice); // text member stays blank
Assert.True(lines.First(l => l.Title == "Waschbecken").ShowPrice);
}
// ── Set price equals sum of member prices across modes (no double counting) ─
[Fact]
public void SetPrice_EqualsSumOfMembers()
+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.
+65 -21
View File
@@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.Data.SqlClient;
using Newtonsoft.Json.Linq;
using static OCORE.OCORE_dictionaries;
@@ -113,23 +114,10 @@ public class FdsInvoiceData
internal List<SqlParameter> BuildInvoiceParams(bool change, string invId)
{
_ = change; _ = invId;
var vatsDic = new Dictionary<string, string>();
if (Req != null)
{
foreach (var rq in Req)
{
if (rq.TryGetValue("items", out var itmsObj) && itmsObj is List<object> itms)
{
foreach (var itm in itms.OfType<Dictionary<string, object?>>())
{
string vatKey = itm.nz("vat", "").Replace("%", "").Trim();
if (!string.IsNullOrEmpty(vatKey) && vatKey != "0")
vatsDic.TryAdd(vatKey, vatsDic.TryGetValue(vatKey, out var ve) ? ve : "0");
}
}
}
}
string vathigh = vatsDic.Keys.OrderByDescending(k => double.TryParse(k, out var d) ? d : 0).FirstOrDefault() ?? "19";
// VAT rate + amount come from the editor's computed sms.vat map (rate → amount),
// matching the legacy contract. Item-level VAT strings are German-formatted and
// single-rate procs only store one rate, so the highest rate wins.
var (vatRate, vatNet) = HighestVat(Sms);
return new List<SqlParameter>
{
@@ -137,8 +125,8 @@ public class FdsInvoiceData
SQL_NVarChar("@InvoiceTitle", NewValues?.nz("title") ?? ""),
SQL_Float("@InvoiceBalance", stringvalue: NewValues?.nz("total_gross") ?? "0"),
SQL_Float("@InvoiceBalance_net", stringvalue: NewValues?.nz("total_net") ?? "0"),
SQL_Float("@InvoiceVAT_net1", stringvalue: NewValues?.nz($"vat_{vathigh}_net") ?? "0"),
SQL_VarChar("@InvoiceVAT_1", vathigh),
SQL_Float("@InvoiceVAT_net1", stringvalue: vatNet),
SQL_VarChar("@InvoiceVAT_1", vatRate),
SQL_VarChar("@PaymentTerm", NewValues?.nz("paymentterm") ?? "", dbNull_IfEmpty: true),
SQL_BigInt("@CustomerId", Admin?.nz("customerid") ?? ""),
SQL_VarChar("@SendToAddress", RawInvoiceAddress),
@@ -147,8 +135,64 @@ public class FdsInvoiceData
SQL_NVarChar("@CustomValues", RawCustomValues, dbNull_IfEmpty: true),
SQL_Float("@InvoiceService_net", stringvalue: Sms?.nz("tscn") ?? "0"),
SQL_Float("@InvoiceService_VAT", stringvalue: Sms?.nz("tscvat") ?? "0"),
SQL_VarChar("@InvoiceOptions",
Admin?.no("p13b", false) is true ? "§13b" : "", dbNull_IfEmpty: true)
SQL_VarChar("@InvoiceOptions", BuildInvoiceOptions(), dbNull_IfEmpty: true)
};
}
/// <summary>
/// Builds the InvoiceOptions CSV from the posted admin flags.
/// <c>§13b</c> is the existing reverse-charge flag; <c>setmode:&lt;mode&gt;</c>
/// selects the set-pricing display mode (the default <c>setprice</c> is omitted).
/// Read back by <see cref="InvoiceSetPricing.ModeFromInvoiceOptions"/> (set mode)
/// and the §13b PDF note. Both ride the <c>admin</c> object the editor posts.
/// </summary>
internal string BuildInvoiceOptions()
{
var tokens = new List<string>();
if (Admin?.no("p13b", false) is true) tokens.Add("§13b");
string setmode = (Admin?.nz("setmode") ?? "").Trim().ToLowerInvariant();
if (setmode is "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
return string.Join(",", tokens);
}
/// <summary>
/// Determines the highest VAT rate and its net VAT amount from the editor's
/// computed <c>sms.vat</c> map (rate string → amount), e.g. {"19,0%": 123.45}.
/// Mirrors the legacy contract (VAT comes from the totals block, not line items).
/// Returns invariant numeric strings — rate ("19") and amount ("123.45") — ready
/// for the <c>numeric</c> proc params. Empty/absent map → ("0","0").
/// </summary>
internal static (string rate, string netAmount) HighestVat(GenericObjectDictionary? sms)
{
if (sms == null || !sms.TryGetValue("vat", out var vobj) || vobj is null)
return ("0", "0");
bool found = false; double bestRate = 0, bestAmount = 0;
void Consider(string key, double amount)
{
double r = ParseRate(key);
if (!found || r > bestRate) { found = true; bestRate = r; bestAmount = amount; }
}
if (vobj is JObject jo)
foreach (var p in jo)
Consider(p.Key, p.Value is { } t && t.Type is JTokenType.Float or JTokenType.Integer ? t.Value<double>() : 0);
else if (vobj is IDictionary<string, object?> dict)
foreach (var kv in dict)
Consider(kv.Key, double.TryParse(Convert.ToString(kv.Value, CultureInfo.InvariantCulture),
NumberStyles.Any, CultureInfo.InvariantCulture, out var a) ? a : 0);
if (!found) return ("0", "0");
string rate = bestRate == Math.Floor(bestRate)
? ((long)bestRate).ToString(CultureInfo.InvariantCulture)
: bestRate.ToString(CultureInfo.InvariantCulture);
return (rate, bestAmount.ToString(CultureInfo.InvariantCulture));
}
/// <summary>Parses a VAT rate string ("19,0%", "7%", "19") to a number (German or invariant).</summary>
private static double ParseRate(string? key)
{
string s = (key ?? "").Replace("%", "").Trim().Replace(',', '.');
return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
}
}
+12 -1
View File
@@ -122,6 +122,17 @@ public static class InvoiceSetPricing
private static bool IsSetHeader(Dictionary<string, object?> i) =>
string.Equals(i.nz("type", ""), "set", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Text/Title lines are headings/free text with no price — their price/total
/// cells render blank (matching the editor and the legacy invoice), regardless
/// of the chosen mode.
/// </summary>
private static bool IsNoPriceLine(Dictionary<string, object?> i)
{
string t = i.nz("type", "").ToLowerInvariant();
return t is "text" or "title";
}
private static string? SetIdOf(Dictionary<string, object?> i)
{
string s = i.nz("setId", "");
@@ -165,7 +176,7 @@ public static class InvoiceSetPricing
Qty = i.nz("qty", "1"),
PriceNet = price,
TotalNet = total,
ShowPrice = showPrice,
ShowPrice = showPrice && !IsNoPriceLine(i), // headings/free text print no price
IsSetHeader = false
};
}
+84 -7
View File
@@ -96,6 +96,9 @@ $inv.eM = (r, re, opt) => {
if ((opt || '').split(',').includes('p13b') === true) {
m.push({ lbl: $ict.p13b, fnc: $inv.sp13b });
}
if ((opt || '').split(',').includes('setm') === true) {
m.push({ lbl: $ict.setm, fnc: $inv.ssetmode });
}
if (booln(r, false) === true) {
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
}
@@ -258,7 +261,7 @@ $inv.ccInv = function (ev) { //normale rechnung
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -403,7 +406,7 @@ $inv.cntInv = function (data) { //invoice continuation
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -646,12 +649,19 @@ $inv.invSumUpdate = function () {
};
let bds = tbl.children('tbody');
bds.each((bi, bdy) => {
let b = $(bdy), rx = b.data() || {}, i = [], bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
b.tC('empty', itm.length < 1);
itm.each((ti, tx) => {
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
//console.debug('rrx %o', rrx);
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
Set grouping: an item of Type 'set' is a header that claims the following items in this
block as its members until the next set header (mfr__items has no explicit member link). */
let citem = $inv.itemToContract(rrx);
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
citems.push(citem);
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
rrx.SortOrder = iso;
@@ -663,7 +673,7 @@ $inv.invSumUpdate = function () {
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
//});
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, netval: bnet });
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
});
let nonempty = tbl.find('tbody:not(.empty)').length;
bds.find('tr.isum').tC('hidden', nonempty < 2);
@@ -838,6 +848,53 @@ $inv.sp13b = () => {
}
tbl.trigger('fds.inv');
};
/* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing
/ FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net,
total_net, vat }. Set membership (type:'set' header + setId on members) is added by
the caller. The invoice total comes from the registration balance, so per-item
totals here are purely presentational. */
$inv.itemToContract = function (rrx) {
rrx = rrx || {};
let oHtml = (e) => $$.d().append(e).html();
let type = (rrx.Type || '').toString().toLowerCase();
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
if (rrx.co && rrx.co.typ === 'osum') {
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
ci.desc = rrx.co.t || '';
ci.total_net = (rrx.net_val || 0);
} else if (['text', 'title'].includes(type) && (rrx.net_val || 0) === 0) {
/* heading / free-text line, no price */
ci.desc = rrx.htmltext || ((((rrx.NameOrNumber || '').substr(0, 1) !== '#') ? oHtml($$[0]('p').text(rrx.NameOrNumber || '')) : '') + (rrx.Note || ''));
ci.total_net = '';
} else {
/* normal priced item (incl. set headers, which carry their own set price or 0) */
ci.title = rrx.NameOrNumber || '';
ci.desc = rrx.Note || '';
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
ci.price_net = (rrx.net || 0);
ci.total_net = (rrx.net_val || 0);
}
return ci;
};
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
$inv.ssetmode = () => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
let cur = (d.admin.setmode || 'setprice'), o;
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
o = $ocms.dlg(fr, { width: 800 });
};
$inv.setSetmode = (mode) => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
d.inv.InvoiceOptions = opts.join(',');
};
$inv.sctp = () => {
let flds = $invcol.ctp;
$ocms.dlgform(flds, {
@@ -857,12 +914,32 @@ $inv.sctp = () => {
}, typedvalues: true
});
};
/* Normalises the editor's working model into the exact field names the C# backend
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
new.paymentterm, admin.CustomerId -> admin.customerid.
Originals are kept alongside; the source objects are not mutated. */
$inv.invcPayload = function (d) {
d = d || {};
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
nw.total_net = sms.ttn || 0;
nw.total_gross = sms.ttb || 0;
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
};
$inv.ssave = () => {
var l = $('div.invoice_layout'), d = l.find('table.invi').data();
$inv.t_fds_inv();
l.aC('freeze');
$ocms.postXT({
url: $ocms.url('req/save'), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid || '' }, success: (response) => {
url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => {
$inv.cntInv({ id: response.id });
}, error: () => {
alert($ict.eis);
@@ -884,7 +961,7 @@ $inv.sprev = (change) => {
}
}
$ocms.postXT({
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid ||'' }, success: (response) => {
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => {
l.rC('freeze');
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total;
if (invtp > 10) {
@@ -40,6 +40,12 @@
eis: 'Der Rechnungsentwurf konnte nicht gespeichert werden.',
iss: 'Zwischenstand speichern.',
p13b: 'USt -> §13b',
setm: 'Set-Preisanzeige',
setmo: {
setprice: 'Set mit Preis Positionen ohne Preis',
itemprices: 'Positionen mit Preis Set als Überschrift',
setonly: 'Nur Set mit Preis Positionen ausgeblendet'
},
ctp: 'Ansprechpartner festlegen',
mfr: 'Von MFR neu abrufen',
rq1: 'Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.',
+90 -7
View File
@@ -146,6 +146,12 @@ let $ict = {
eis: 'Der Rechnungsentwurf konnte nicht gespeichert werden.',
iss: 'Zwischenstand speichern.',
p13b: 'USt -> §13b',
setm: 'Set-Preisanzeige',
setmo: {
setprice: 'Set mit Preis Positionen ohne Preis',
itemprices: 'Positionen mit Preis Set als Überschrift',
setonly: 'Nur Set mit Preis Positionen ausgeblendet'
},
ctp: 'Ansprechpartner festlegen',
mfr: 'Von MFR neu abrufen',
rq1: 'Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.',
@@ -637,6 +643,9 @@ $inv.eM = (r, re, opt) => {
if ((opt || '').split(',').includes('p13b') === true) {
m.push({ lbl: $ict.p13b, fnc: $inv.sp13b });
}
if ((opt || '').split(',').includes('setm') === true) {
m.push({ lbl: $ict.setm, fnc: $inv.ssetmode });
}
if (booln(r, false) === true) {
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
}
@@ -799,7 +808,7 @@ $inv.ccInv = function (ev) { //normale rechnung
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -944,7 +953,7 @@ $inv.cntInv = function (data) { //invoice continuation
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1187,12 +1196,19 @@ $inv.invSumUpdate = function () {
};
let bds = tbl.children('tbody');
bds.each((bi, bdy) => {
let b = $(bdy), rx = b.data() || {}, i = [], bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
b.tC('empty', itm.length < 1);
itm.each((ti, tx) => {
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
//console.debug('rrx %o', rrx);
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
Set grouping: an item of Type 'set' is a header that claims the following items in this
block as its members until the next set header (mfr__items has no explicit member link). */
let citem = $inv.itemToContract(rrx);
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
citems.push(citem);
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
rrx.SortOrder = iso;
@@ -1204,7 +1220,7 @@ $inv.invSumUpdate = function () {
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
//});
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, netval: bnet });
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
});
let nonempty = tbl.find('tbody:not(.empty)').length;
bds.find('tr.isum').tC('hidden', nonempty < 2);
@@ -1379,6 +1395,53 @@ $inv.sp13b = () => {
}
tbl.trigger('fds.inv');
};
/* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing
/ FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net,
total_net, vat }. Set membership (type:'set' header + setId on members) is added by
the caller. The invoice total comes from the registration balance, so per-item
totals here are purely presentational. */
$inv.itemToContract = function (rrx) {
rrx = rrx || {};
let oHtml = (e) => $$.d().append(e).html();
let type = (rrx.Type || '').toString().toLowerCase();
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
if (rrx.co && rrx.co.typ === 'osum') {
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
ci.desc = rrx.co.t || '';
ci.total_net = (rrx.net_val || 0);
} else if (['text', 'title'].includes(type) && (rrx.net_val || 0) === 0) {
/* heading / free-text line, no price */
ci.desc = rrx.htmltext || ((((rrx.NameOrNumber || '').substr(0, 1) !== '#') ? oHtml($$[0]('p').text(rrx.NameOrNumber || '')) : '') + (rrx.Note || ''));
ci.total_net = '';
} else {
/* normal priced item (incl. set headers, which carry their own set price or 0) */
ci.title = rrx.NameOrNumber || '';
ci.desc = rrx.Note || '';
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
ci.price_net = (rrx.net || 0);
ci.total_net = (rrx.net_val || 0);
}
return ci;
};
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
$inv.ssetmode = () => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
let cur = (d.admin.setmode || 'setprice'), o;
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
o = $ocms.dlg(fr, { width: 800 });
};
$inv.setSetmode = (mode) => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
d.inv.InvoiceOptions = opts.join(',');
};
$inv.sctp = () => {
let flds = $invcol.ctp;
$ocms.dlgform(flds, {
@@ -1398,12 +1461,32 @@ $inv.sctp = () => {
}, typedvalues: true
});
};
/* Normalises the editor's working model into the exact field names the C# backend
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
new.paymentterm, admin.CustomerId -> admin.customerid.
Originals are kept alongside; the source objects are not mutated. */
$inv.invcPayload = function (d) {
d = d || {};
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
nw.total_net = sms.ttn || 0;
nw.total_gross = sms.ttb || 0;
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
};
$inv.ssave = () => {
var l = $('div.invoice_layout'), d = l.find('table.invi').data();
$inv.t_fds_inv();
l.aC('freeze');
$ocms.postXT({
url: $ocms.url('req/save'), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid || '' }, success: (response) => {
url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => {
$inv.cntInv({ id: response.id });
}, error: () => {
alert($ict.eis);
@@ -1425,7 +1508,7 @@ $inv.sprev = (change) => {
}
}
$ocms.postXT({
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid ||'' }, success: (response) => {
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => {
l.rC('freeze');
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total;
if (invtp > 10) {
File diff suppressed because one or more lines are too long
+90 -7
View File
@@ -146,6 +146,12 @@ let $ict = {
eis: 'Der Rechnungsentwurf konnte nicht gespeichert werden.',
iss: 'Zwischenstand speichern.',
p13b: 'USt -> §13b',
setm: 'Set-Preisanzeige',
setmo: {
setprice: 'Set mit Preis Positionen ohne Preis',
itemprices: 'Positionen mit Preis Set als Überschrift',
setonly: 'Nur Set mit Preis Positionen ausgeblendet'
},
ctp: 'Ansprechpartner festlegen',
mfr: 'Von MFR neu abrufen',
rq1: 'Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.',
@@ -618,6 +624,9 @@ $inv.eM = (r, re, opt) => {
if ((opt || '').split(',').includes('p13b') === true) {
m.push({ lbl: $ict.p13b, fnc: $inv.sp13b });
}
if ((opt || '').split(',').includes('setm') === true) {
m.push({ lbl: $ict.setm, fnc: $inv.ssetmode });
}
if (booln(r, false) === true) {
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
}
@@ -780,7 +789,7 @@ $inv.ccInv = function (ev) { //normale rechnung
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -925,7 +934,7 @@ $inv.cntInv = function (data) { //invoice continuation
rif.tbl.children('tbody').each($inv.bdysort);
rif.tbl.trigger('fds.inv'); /* trigger calculations */
$inv.eM(false, true, 'iss,p13b,ctp');
$inv.eM(false, true, 'iss,p13b,setm,ctp');
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1168,12 +1177,19 @@ $inv.invSumUpdate = function () {
};
let bds = tbl.children('tbody');
bds.each((bi, bdy) => {
let b = $(bdy), rx = b.data() || {}, i = [], bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
b.tC('empty', itm.length < 1);
itm.each((ti, tx) => {
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
//console.debug('rrx %o', rrx);
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
Set grouping: an item of Type 'set' is a header that claims the following items in this
block as its members until the next set header (mfr__items has no explicit member link). */
let citem = $inv.itemToContract(rrx);
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
citems.push(citem);
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
rrx.SortOrder = iso;
@@ -1185,7 +1201,7 @@ $inv.invSumUpdate = function () {
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
//});
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, netval: bnet });
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
});
let nonempty = tbl.find('tbody:not(.empty)').length;
bds.find('tr.isum').tC('hidden', nonempty < 2);
@@ -1360,6 +1376,53 @@ $inv.sp13b = () => {
}
tbl.trigger('fds.inv');
};
/* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing
/ FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net,
total_net, vat }. Set membership (type:'set' header + setId on members) is added by
the caller. The invoice total comes from the registration balance, so per-item
totals here are purely presentational. */
$inv.itemToContract = function (rrx) {
rrx = rrx || {};
let oHtml = (e) => $$.d().append(e).html();
let type = (rrx.Type || '').toString().toLowerCase();
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
if (rrx.co && rrx.co.typ === 'osum') {
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
ci.desc = rrx.co.t || '';
ci.total_net = (rrx.net_val || 0);
} else if (['text', 'title'].includes(type) && (rrx.net_val || 0) === 0) {
/* heading / free-text line, no price */
ci.desc = rrx.htmltext || ((((rrx.NameOrNumber || '').substr(0, 1) !== '#') ? oHtml($$[0]('p').text(rrx.NameOrNumber || '')) : '') + (rrx.Note || ''));
ci.total_net = '';
} else {
/* normal priced item (incl. set headers, which carry their own set price or 0) */
ci.title = rrx.NameOrNumber || '';
ci.desc = rrx.Note || '';
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
ci.price_net = (rrx.net || 0);
ci.total_net = (rrx.net_val || 0);
}
return ci;
};
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
$inv.ssetmode = () => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
let cur = (d.admin.setmode || 'setprice'), o;
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
o = $ocms.dlg(fr, { width: 800 });
};
$inv.setSetmode = (mode) => {
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
d.admin = d.admin || {};
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
d.inv.InvoiceOptions = opts.join(',');
};
$inv.sctp = () => {
let flds = $invcol.ctp;
$ocms.dlgform(flds, {
@@ -1379,12 +1442,32 @@ $inv.sctp = () => {
}, typedvalues: true
});
};
/* Normalises the editor's working model into the exact field names the C# backend
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
new.paymentterm, admin.CustomerId -> admin.customerid.
Originals are kept alongside; the source objects are not mutated. */
$inv.invcPayload = function (d) {
d = d || {};
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
nw.total_net = sms.ttn || 0;
nw.total_gross = sms.ttb || 0;
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
};
$inv.ssave = () => {
var l = $('div.invoice_layout'), d = l.find('table.invi').data();
$inv.t_fds_inv();
l.aC('freeze');
$ocms.postXT({
url: $ocms.url('req/save'), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid || '' }, success: (response) => {
url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => {
$inv.cntInv({ id: response.id });
}, error: () => {
alert($ict.eis);
@@ -1406,7 +1489,7 @@ $inv.sprev = (change) => {
}
}
$ocms.postXT({
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }), id: d.invid ||'' }, success: (response) => {
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => {
l.rC('freeze');
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total;
if (invtp > 10) {
File diff suppressed because one or more lines are too long