Key the set-price menu modes on the set instead of the service-request block (ADR 0019)

"Set mit Preis" / "Nur Set mit Preis" collapsed every block into an inserted
"Gesamtumfang pauschal" row, nulled/removed all lines and cleared the user-defined
set structure. Both modes now convert every still-unconverted top-level set:
the set line takes the sum of its subtree, its members render without a price
(setprice) or are removed from itm + items (setonly); lines outside a set stay
individually priced and several sets stay separate lines. Nested sets are priced
once via their top-level set. One conversion primitive (ConvertSetHeader) now
backs all three triggers; item.setprice is a no-op on an already-converted set
(a repeated delta used to reset the set price to 0). The header's price_net is
mirrored too (flat PDF path unit price), and Build treats a null total_net as
"no price" like the flat path. The editor checks for a set before offering the
modes and the menu entry stays available for sets added later.

ADR 0009 is superseded by 0019; INVOICE_SET_PRICING.md rewritten accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 22:19:01 +02:00
co-authored by Claude Fable 5.1
parent 22d2ef9c1a
commit 5fa7a2b50f
16 changed files with 678 additions and 447 deletions
+47 -37
View File
@@ -39,7 +39,8 @@ let $rct = {
combP: 'Positionen zusammenfassen',
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
toSetMc: 'Jedes Set erhält die Summe seiner Positionen als Set-Preis; die Positionen des Sets werden ohne Preis dargestellt bzw. ausgeblendet. Positionen ausserhalb eines Sets bleiben unverändert einzeln aufgeführt. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
toSetMn: 'Diese Rechnung enthält kein Set. Legen Sie zuerst über "Set-Struktur bearbeiten" ein Set an bzw. ordnen Sie Positionen einem Set zu.',
iSum: 'Zwischensumme',
dtRel: 'Freigegeben am: ',
dtCr: 'Erstellt am: ',
@@ -657,11 +658,11 @@ $inv.iMn = (ix) => {
}
return $('#topbar').ocmsmenu(m);
}
/* Menu option CSV builder: 'setm' (Set-Preisanzeige) is only offered while the invoice has not
yet been explicitly switched to a set-display mode (admin.setmode empty/undefined) — once set,
changing it again is not supported (the user edits the line items directly instead); it
reappears only if 'Änderungen verwerfen' reverts to a DB state that was never switched. */
$inv.emOpt = (admin) => 'iss,p13b,ctp,setst' + (((admin || {}).setmode || '') === '' ? ',setm' : '');
/* Menu option CSV builder. 'setm' (Set-Preisanzeige) is always offered: since ADR 0019 the two
modes convert every still-unconverted SET (not the whole invoice), so running them again after
the user has added or re-assigned a set converts just that one — and is a plain no-op when there
is nothing left to convert. The legacy admin.setmode flag no longer gates the entry. */
$inv.emOpt = () => 'iss,p13b,ctp,setst,setm';
$inv.eM = (r, re, opt) => {
let m = [];
if (booln(r, false) === true || booln(re, false) === true) {
@@ -757,10 +758,7 @@ $inv.d = {
$inv.d.applyItems(tbl, state.req || []);
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
if ($inv.sets && $inv.sets.isOpen()) { $inv.sets.render(state); } /* the set-structure dialog is a view of the same state (ADR 0016) */
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
hides it right after a mode switch, and restores it if a discard reverted the draft to
a DB state that was never switched (ADR 0006 backend-authoritative refresh). */
$inv.eM(false, true, $inv.emOpt(state.admin || {}));
$inv.eM(false, true, $inv.emOpt());
},
/* Renders the backend-authoritative notice paragraphs (ADR: notice texts are
backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF,
@@ -777,10 +775,10 @@ $inv.d = {
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
/* Reconciles each block's rendered rows against the authoritative server line list
(`req[].itm`, the co shape). The server may not only change a line's values but also
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
ADD lines (e.g. a set/text line created in the structure dialog) and REMOVE lines (a set's
members under "Nur Set mit Preis"/block.setonly), and reorder them — the browser never
computes any of that (ADR 0006/0008/0019), it just mirrors the server. For each server line:
patch/insert the matching row, preserving null price fields verbatim (null = empty cell, kept
distinct from 0). Rows the server no longer lists are removed; order follows the server.
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
applyItems: function (tbl, req) {
@@ -1343,7 +1341,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, $inv.emOpt(admin));
$inv.eM(false, true, $inv.emOpt());
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1488,7 +1486,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, $inv.emOpt(response.admin));
$inv.eM(false, true, $inv.emOpt());
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1886,7 +1884,7 @@ $inv.rrw = function () {
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
} else {
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. a set line) */
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
co.v = dta.net
co.vt = dta.net_val
@@ -1905,8 +1903,8 @@ $inv.rrw = function () {
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
} else {
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
members, and the set row's own unit-price), distinct from a real 0,00 €. */
/* ADR 0009/0019: a null/undefined price renders an EMPTY cell (the members of a set
converted to a set price, and a set line's own unit price), distinct from a real 0,00 €. */
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
Array.prototype.push.apply(tda, [
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
@@ -1915,7 +1913,7 @@ $inv.rrw = function () {
]);
}
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows */
.aC((co.lvl || 0) > 0 ? 'lvl' + Math.min(co.lvl, 6) : '') /* indent nested set members (ADR 0016) */
.tC('hidenote', hn).append(tda);
dta.co = co;
@@ -2163,7 +2161,7 @@ $inv.itemToContract = function (rrx) {
let oHtml = (e) => $$.d().append(e).html();
let type = (rrx.Type || '').toString().toLowerCase();
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
nulled set members and the set line round-trip unchanged when the block is re-posted. */
let nz = (x) => (x == null ? null : x);
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
if (rrx.co && rrx.co.typ === 'osum') {
@@ -2191,26 +2189,38 @@ $inv.itemToContract = function (rrx) {
}
return ci;
};
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
always just the implicit pre-conversion default and is not a state the user can switch back to
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
/* Whether the authoritative state carries at least one SET line that owns members — the two
set-price modes only ever touch sets (ADR 0019), so without one there is nothing to convert. */
$inv.hasSets = (state) => (((state || {}).req) || []).some((b) => {
let itms = ((b || {}).itm || []).filter((co) => co && (co.id || '') !== ''), byId = {};
itms.forEach((co) => { byId[(co.id || '').toString()] = co; });
let isSet = (co) => ((co.typ || '').toString().toLowerCase() === 'set');
return itms.some((co) => {
let p = (co.SetItmId == null ? '' : co.SetItmId).toString();
return p !== '' && p !== (co.id || '').toString() && byId[p] && isSet(byId[p]);
});
});
/* 2-way set-pricing switch (SetPrice / SetOnly), ADR 0019: both modes give EVERY set the sum of
its positions as one set price; "Set mit Preis" keeps those positions without a price, "Nur Set
mit Preis" removes them. Positions outside any set stay individually priced either way, and
several sets stay several separate lines. ItemPrices ("Positionen mit Preis – Set als
Überschrift") was removed: it is just the pre-conversion default and not a state one can switch
back to, since every conversion is one-way (see $inv.toSetPrice /
InvoiceDraftEditService.ApplyItemSetPrice). */
$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 l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(), o;
if (d.dstate && $inv.hasSets(d.dstate) === false) { alert($rct.toSetMn); return; } /* no state yet -> let the server decide (no-op) */
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
o = $ocms.dlg(fr, { width: 800 });
};
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
set row per block (block sum) and either nulls each member's price (setprice) or removes the
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0019, superseding the per-block collapse of ADR 0009):
NOT a display toggle — an irreversible, backend-authoritative mutation grouped by SET. The server
sums each top-level set's positions onto the set line and either nulls those positions' prices
(setprice) or removes them (setonly); lines outside a set are untouched. The normal draftReady
refresh re-renders both via $inv.d.applyItems. The browser only posts the chosen mode. Flush
pending local edits first so a delayed block.replace can't land after the conversion and
overwrite it (same race guard as $inv.toSetPrice). */
$inv.setSetmode = (mode) => {
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
File diff suppressed because one or more lines are too long
+47 -37
View File
@@ -39,7 +39,8 @@ let $rct = {
combP: 'Positionen zusammenfassen',
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
toSetMc: 'Jedes Set erhält die Summe seiner Positionen als Set-Preis; die Positionen des Sets werden ohne Preis dargestellt bzw. ausgeblendet. Positionen ausserhalb eines Sets bleiben unverändert einzeln aufgeführt. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
toSetMn: 'Diese Rechnung enthält kein Set. Legen Sie zuerst über "Set-Struktur bearbeiten" ein Set an bzw. ordnen Sie Positionen einem Set zu.',
iSum: 'Zwischensumme',
dtRel: 'Freigegeben am: ',
dtCr: 'Erstellt am: ',
@@ -638,11 +639,11 @@ $inv.iMn = (ix) => {
}
return $('#topbar').ocmsmenu(m);
}
/* Menu option CSV builder: 'setm' (Set-Preisanzeige) is only offered while the invoice has not
yet been explicitly switched to a set-display mode (admin.setmode empty/undefined) — once set,
changing it again is not supported (the user edits the line items directly instead); it
reappears only if 'Änderungen verwerfen' reverts to a DB state that was never switched. */
$inv.emOpt = (admin) => 'iss,p13b,ctp,setst' + (((admin || {}).setmode || '') === '' ? ',setm' : '');
/* Menu option CSV builder. 'setm' (Set-Preisanzeige) is always offered: since ADR 0019 the two
modes convert every still-unconverted SET (not the whole invoice), so running them again after
the user has added or re-assigned a set converts just that one — and is a plain no-op when there
is nothing left to convert. The legacy admin.setmode flag no longer gates the entry. */
$inv.emOpt = () => 'iss,p13b,ctp,setst,setm';
$inv.eM = (r, re, opt) => {
let m = [];
if (booln(r, false) === true || booln(re, false) === true) {
@@ -738,10 +739,7 @@ $inv.d = {
$inv.d.applyItems(tbl, state.req || []);
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
if ($inv.sets && $inv.sets.isOpen()) { $inv.sets.render(state); } /* the set-structure dialog is a view of the same state (ADR 0016) */
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
hides it right after a mode switch, and restores it if a discard reverted the draft to
a DB state that was never switched (ADR 0006 backend-authoritative refresh). */
$inv.eM(false, true, $inv.emOpt(state.admin || {}));
$inv.eM(false, true, $inv.emOpt());
},
/* Renders the backend-authoritative notice paragraphs (ADR: notice texts are
backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF,
@@ -758,10 +756,10 @@ $inv.d = {
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
/* Reconciles each block's rendered rows against the authoritative server line list
(`req[].itm`, the co shape). The server may not only change a line's values but also
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
ADD lines (e.g. a set/text line created in the structure dialog) and REMOVE lines (a set's
members under "Nur Set mit Preis"/block.setonly), and reorder them — the browser never
computes any of that (ADR 0006/0008/0019), it just mirrors the server. For each server line:
patch/insert the matching row, preserving null price fields verbatim (null = empty cell, kept
distinct from 0). Rows the server no longer lists are removed; order follows the server.
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
applyItems: function (tbl, req) {
@@ -1324,7 +1322,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, $inv.emOpt(admin));
$inv.eM(false, true, $inv.emOpt());
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1469,7 +1467,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, $inv.emOpt(response.admin));
$inv.eM(false, true, $inv.emOpt());
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -1867,7 +1865,7 @@ $inv.rrw = function () {
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
} else {
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. a set line) */
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
co.v = dta.net
co.vt = dta.net_val
@@ -1886,8 +1884,8 @@ $inv.rrw = function () {
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
} else {
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
members, and the set row's own unit-price), distinct from a real 0,00 €. */
/* ADR 0009/0019: a null/undefined price renders an EMPTY cell (the members of a set
converted to a set price, and a set line's own unit price), distinct from a real 0,00 €. */
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
Array.prototype.push.apply(tda, [
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
@@ -1896,7 +1894,7 @@ $inv.rrw = function () {
]);
}
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows */
.aC((co.lvl || 0) > 0 ? 'lvl' + Math.min(co.lvl, 6) : '') /* indent nested set members (ADR 0016) */
.tC('hidenote', hn).append(tda);
dta.co = co;
@@ -2144,7 +2142,7 @@ $inv.itemToContract = function (rrx) {
let oHtml = (e) => $$.d().append(e).html();
let type = (rrx.Type || '').toString().toLowerCase();
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
nulled set members and the set line round-trip unchanged when the block is re-posted. */
let nz = (x) => (x == null ? null : x);
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
if (rrx.co && rrx.co.typ === 'osum') {
@@ -2172,26 +2170,38 @@ $inv.itemToContract = function (rrx) {
}
return ci;
};
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
always just the implicit pre-conversion default and is not a state the user can switch back to
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
/* Whether the authoritative state carries at least one SET line that owns members — the two
set-price modes only ever touch sets (ADR 0019), so without one there is nothing to convert. */
$inv.hasSets = (state) => (((state || {}).req) || []).some((b) => {
let itms = ((b || {}).itm || []).filter((co) => co && (co.id || '') !== ''), byId = {};
itms.forEach((co) => { byId[(co.id || '').toString()] = co; });
let isSet = (co) => ((co.typ || '').toString().toLowerCase() === 'set');
return itms.some((co) => {
let p = (co.SetItmId == null ? '' : co.SetItmId).toString();
return p !== '' && p !== (co.id || '').toString() && byId[p] && isSet(byId[p]);
});
});
/* 2-way set-pricing switch (SetPrice / SetOnly), ADR 0019: both modes give EVERY set the sum of
its positions as one set price; "Set mit Preis" keeps those positions without a price, "Nur Set
mit Preis" removes them. Positions outside any set stay individually priced either way, and
several sets stay several separate lines. ItemPrices ("Positionen mit Preis – Set als
Überschrift") was removed: it is just the pre-conversion default and not a state one can switch
back to, since every conversion is one-way (see $inv.toSetPrice /
InvoiceDraftEditService.ApplyItemSetPrice). */
$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 l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(), o;
if (d.dstate && $inv.hasSets(d.dstate) === false) { alert($rct.toSetMn); return; } /* no state yet -> let the server decide (no-op) */
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
o = $ocms.dlg(fr, { width: 800 });
};
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
set row per block (block sum) and either nulls each member's price (setprice) or removes the
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0019, superseding the per-block collapse of ADR 0009):
NOT a display toggle — an irreversible, backend-authoritative mutation grouped by SET. The server
sums each top-level set's positions onto the set line and either nulls those positions' prices
(setprice) or removes them (setonly); lines outside a set are untouched. The normal draftReady
refresh re-renders both via $inv.d.applyItems. The browser only posts the chosen mode. Flush
pending local edits first so a delayed block.replace can't land after the conversion and
overwrite it (same race guard as $inv.toSetPrice). */
$inv.setSetmode = (mode) => {
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
File diff suppressed because one or more lines are too long