added improvements on set proces and minor other improvements

This commit is contained in:
Stefan
2026-07-13 22:40:08 +02:00
parent 5c0fdc6c1d
commit 5f85b75c22
55 changed files with 1170 additions and 357 deletions
+31
View File
@@ -36,11 +36,16 @@ let $bam = {
let fds = bool(ix.fds, false), id = ix.taID, m = [];
m.push({ lbl: $bct.smd, fnc: () => { $bam.smd(id); } });
m.push({ lbl: $bct.ati, fnc: () => { $bam.ati(id); } });
if (bool(ix.IsManual, false) === true) {
m.push({ lbl: $bct.mbc, fnc: () => { $bam.mbc(id); } });
m.push({ lbl: $bct.mbd, fnc: () => { $bam.mbd(id); } });
}
return $('#topbar').ocmsmenu(m);
}, eM : (r, re, opt) => {
let m = [];
if ($fis.isAuth('fds_bam', 2) === true) {
m.push({ lbl: $bct.upl, fnc: $bam.ubs});
m.push({ lbl: $bct.mbe, fnc: $bam.mbe});
}
if (bool(r, false) === true) {
m.push({ lbl: $bct.rel, fnc: $bam.renderLst });
@@ -312,6 +317,32 @@ let $bam = {
}
}
});
}, mbe: () => {
$bam.manForm({});
}, mbc: (taid) => {
$ocms.postXT({
url: $ocms.url('bam/mget'), data: { taid: taid }, success: (dta) => {
$bam.manForm(dta || {});
}
});
}, mbd: (taid) => {
if (confirm($bct.mbdc)) {
$ocms.postXT({
url: $ocms.url('bam/mdel'), data: { taid: taid }, success: () => {
$bam.renderLst();
}
});
}
}, manForm: (dta) => {
let flds = $bcol.man.clone().applyValues(dta || {});
$ocms.dlgform(flds, {
title: (ne(dta.taID, '') !== '') ? $bct.mbc : $bct.mbe,
url: $ocms.url('bam/man'),
typedvalues: true,
success: () => {
$bam.renderLst();
}
});
}
}
+12 -1
View File
@@ -18,10 +18,21 @@
},
in: 'Rechnungsnummer',
bt: 'Buchungstexte',
bv: 'Buchungswert (volle €)'
bv: 'Buchungswert (volle €)',
mbe: 'Manuelle Buchung erfassen',
mbc: 'Manuelle Buchung ändern',
mbd: 'Manuelle Buchung löschen',
mbdc: 'Manuelle Buchung wirklich löschen?\nDieser Vorgang kann nicht rückgängig gemacht werden.'
};
let $bcol = {
man: new fields_definition('Manuelle Buchung', 'Manuelle Buchungen', [
{ name: 'taID', type: 'hidden' },
{ name: 'ValueDate', label: 'Valuta', type: 'date', required: true },
{ name: 'Amount', label: 'Betrag', type: 'number', precision: '0.01', required: true },
{ name: 'NameOfPayer', label: 'Name', type: 'string' },
{ name: 'SepaRemittanceInformation', label: 'Verwendungszweck', type: 'string' }
]),
qtl: new fields_definition('Kontobericht', 'Kontoberichte', [
{ name: 'InvoiceId', label: 'RechnungsNr', type: 'string' },
{ name: 'ValueDate', label: 'Valuta', type: 'date' },
+102 -6
View File
@@ -82,6 +82,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' + (((admin || {}).setmode || '') === '' ? ',setm' : '');
$inv.eM = (r, re, opt) => {
let m = [];
if (booln(r, false) === true || booln(re, false) === true) {
@@ -154,8 +159,24 @@ $inv.d = {
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
tbl.data('dver', state.version).data('serverSums', state.sums);
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
$inv.d.notes(state.notes || []);
$inv.d.validation(state.validation || []);
$inv.d.applyPositions(tbl, state.req || []);
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
/* 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 || {}));
},
/* Renders the backend-authoritative notice paragraphs (ADR: notice texts are
backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF,
so the online editor always shows precisely what will be printed. */
notes: function (notes) {
let frm = $inv.d.layout(); if (frm.length < 1) { return; }
let box = frm.children('.dnotes');
if (box.length < 1) { box = $$.dc('dnotes'); frm.append(box); }
box.empty();
(notes || []).forEach((n) => $$.dc('dnote').aC(n.style || 'InvoiceNotes').text(n.text));
},
/* Push the server's authoritative position numbers back onto the rendered rows so the online
editor and the PDF preview always agree (the server numbers priced lines continuously; the
@@ -167,6 +188,23 @@ $inv.d = {
if (cell.length) { cell.text(co.p != null ? co.p : ''); }
}));
},
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
server says should show no price (set members in SetPrice mode, the set header in
ItemPrices mode), and emphasises the set header line — so the online editor always
shows exactly what the PDF will print, without duplicating the pricing rules client-side.
Rows not present in the map (no set in that block, or standalone items) are left untouched. */
applySetDisplay: function (tbl, setDisplay) {
tbl.find('tr.itm').each((i, tr) => {
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
if (id === '' || !Object.prototype.hasOwnProperty.call(setDisplay, id)) { return; }
let flags = setDisplay[id] || {};
rw.tC('sethdr', bool(flags.h, false));
if (bool(flags.p, true) === false) {
rw.find('td.currency').text('');
}
});
},
/* Send one change to the server; the draftReady signal and this success both refresh. */
sync: function (delta) {
let t = $inv.d.token(); if (t === '') { return; }
@@ -354,8 +392,23 @@ $inv.rd = {
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
$inv.rd.footer(tbl, state.sums || {});
$inv.rd.notes(state.notes || {});
$inv.rd.validation(state.validation || []);
},
/* Renders the backend-authoritative intro/closing notice paragraphs (ADR: notice texts
are backend-authoritative) exactly as FuchsPdf.BuildReminderNotes emits them for the
PDF, so the online editor always shows precisely what will be printed. */
notes: function (notes) {
let frm = $inv.rd.layout(); if (frm.length < 1) { return; }
let rq = frm.find('.srq'); if (rq.length < 1) { return; }
rq.children('.dnote').remove();
let intro = (notes || {}).intro || [], first = rq.children().first();
intro.slice().reverse().forEach((t) => {
let p = $$[0]('p').aC('dnote intro').rwText(t);
if (first.length) { p.insertBefore(first); } else { p.appendTo(rq); }
});
((notes || {}).closing || []).forEach((t) => $$[0]('p').aC('dnote closing').rwText(t).appendTo(rq));
},
/* Send one change to the server; the draftReady signal and this success both refresh. */
sync: function (delta) {
let t = $inv.rd.token(); if (t === '') { return; }
@@ -379,10 +432,13 @@ $inv.rd = {
$inv.rd.sync({ Target: 'amount', Value: (amount != null ? amount : 0).toString() });
$inv.rd.sync({ Target: 'amount_payed', Value: (amount_payed != null ? amount_payed : 0).toString() });
},
/* Render the open-amount footer from the server sums. */
/* Render the open-amount footer from the server sums. Detail rows ($inv.rRemRw) have
6 columns (aux, invoiceid, date, amount, amount_payed, open); span the label over the
4 columns between aux and the open-amount column so the total lands in the same last
column as the per-invoice open amounts. */
footer: function (tbl, sums) {
let ft = tbl.children('tfoot').empty();
let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 3 }).text('Offener Betrag')]);
let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Offener Betrag')]);
$$.tdc('currency', tr, fnum(sums.amount_open || 0, $rct.cst));
},
validation: function (msgs) {
@@ -625,7 +681,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,setm,ctp');
$inv.eM(false, true, $inv.emOpt(admin));
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -770,7 +826,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,setm,ctp');
$inv.eM(false, true, $inv.emOpt(response.admin));
}, complete: () => {
o.c.trigger('modal_close');
}
@@ -970,6 +1026,14 @@ $inv.rrw = function () {
if (ph === false) {
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
/* "Auf Setpreis umstellen": only offered on a Set header item that is not yet
priced from its members (fds__prepInvoice's [SetItmId] is null exactly for a
Set header — either already set-priced, or not yet converted). Irreversible:
sets this row's price to the sum of its members (rows whose [SetItmId] equals
this row's id) and clears each member's price. */
if ((dta.Type || '').toLowerCase() === 'set' && (dta.SetItmId || '') === '') {
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
}
}
let axf = $$.dc('axf').append(bc);
if (ph === true) {
@@ -1015,6 +1079,36 @@ $inv.rrw = function () {
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda);
dta.co = co;
};
/* "Auf Setpreis umstellen" (see $inv.rrw): sets this Set-header row's price to the sum of
its member rows' totals (rows in the same block whose [SetItmId] equals this row's id,
the authoritative grouping from fds__prepInvoice), then clears each member's price so the
member items show without a price — matching the SetPrice presentation, but as an actual,
irreversible data change instead of a display-mode toggle (INVOICE_SET_PRICING.md's
setmode remains display-only and is unaffected by this). */
$inv.toSetPrice = function (ev) {
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
if (id === '') { return; }
if (confirm($rct.toSetPc) === false) { return; }
let bdy = rw.closest('tbody'), members = bdy.find('tr.itm').filter(function () {
return (($(this).data('SetItmId') || '').toString()) === id;
});
let sum_net_val = 0, sum_vat_val = 0, sum_svcnet_val = 0, sum_svcvat_val = 0;
members.each(function () {
let m = $(this).data();
sum_net_val += (m.net_val || 0);
sum_vat_val += (m.vat_val || 0);
sum_svcnet_val += (m.svcnet_val || 0);
sum_svcvat_val += (m.svcvat_val || 0);
$.extend(m, { net: 0, net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, Discount: 0 });
$inv.rrw.call($(this));
});
$.extend(dta, {
net: sum_net_val, quantityhours: 1, Discount: 0,
net_val: sum_net_val, vat_val: sum_vat_val, svcnet_val: sum_svcnet_val, svcvat_val: sum_svcvat_val
});
$inv.rrw.call(rw);
$inv.t_fds_inv();
};
$inv.invSumUpdate = function () {
let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false);
tbl.nextAll('.fnote').remove();
@@ -1541,7 +1635,10 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation
//console.debug(response);
let rem = response.rm || {};
let rq = $$.dc('srq', frm);
$ict.remt[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq));
/* Intro/closing notice paragraphs are backend-authoritative (see $inv.rd.notes,
populated from FuchsPdf.BuildReminderNotes via rem/dstate after seeding below) —
they are appended/prepended into this same .srq container once the draft session
is open, so no client-side wording is constructed here. */
let rif = $$.tblset({ class: 'invi' }, rq);
rif.ft = $$[0]('tfoot');
rif.tbl.data($.extend({ invid: rem.invid, new: {} }, response));
@@ -1549,7 +1646,6 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation
$ict.remHR.forEach(h => $$.th(thr, h));
$inv.rRemRw.call($$.tr(rif.bdy), rif.tbl.data());
rif.ft.appendTo(rif.tbl);
$ict.remt2[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq));
let fm = (cls, txt, nme, hd, dta) => {
let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt);
@@ -62,6 +62,18 @@
height: auto;
cursor: pointer;
.content {
color: #000;
}
.content:empty::before {
content: '-';
min-width: 5rem;
height: 1rem;
display: block;
position: absolute;
}
&:hover {
div.axf {
display: block;
@@ -232,6 +244,12 @@
border-color: #FFF;
border-top: 1.5px solid #000;
}
/* Set-pricing display (see InvoiceSetPricing / $inv.d.applySetDisplay): the set
header line is emphasised the same way the PDF renders it (bold title). */
&.sethdr td.txt {
font-weight: bold;
}
}
> tfoot > tr {
+4 -10
View File
@@ -63,16 +63,10 @@
rovlh: 'Übersicht der bisherigen Mahnungen',
rovl: ['Betreff', 'Betrag', 'Betrag gezahlt', 'fertiggestellt am'],
remHR: ['Rechnung', 'vom', 'Rechnungsbetrag', 'bereits bezahlt', 'noch offen'],
remt: {
f: ['Sehr geehrte Damen und Herren,', 'ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, hängt von Ihnen ab.']
, m: ['Sehr geehrte Damen und Herren,', 'nun müssen wir Sie noch einmal anschreiben.', 'Wahrscheinlich haben Sie triftige Gründe dafür, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir darüber nicht einmal sprechen?', 'Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf.']
, l: ['Sehr geehrte Damen und Herren,', 'Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.", "Wir bitten Sie, den offenen Betrag innerhalb der nächsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.', 'Sollte die Forderung bis dahin nicht beglichen sein, eröffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos.']
},
remt2: {
f: ['Wir bitten Sie, den noch offenen Rechnungsbetrag innerhalb einer Woche auf unser Konto zu überweisen.', 'Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten.']
, m: ['Um Ihnen zusätzliche Kosten für weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die Überweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits fälligen Mahnzinsen und Mahngebühren innerhalb von einer Woche.']
, l: []
}, payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'}
// Reminder intro/closing notice texts are backend-authoritative (see FuchsPdf.BuildReminderNotes /
// $inv.rd.notes) — no client-side copies here.
translatePaymentTerm: (pt) => (pt || '').replace('wd', ' Werktagen').replace('d', ' Tagen').replace('wk', ' Wochen') || '10 Tagen'
, payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'}
}, $invcol = {
datev: new fields_definition('Rechnung', 'Rechnungen', [
{ name: 'Umsatz (ohne Soll/Haben-Kz)', label: 'Umsatz (ohne Soll/Haben-Kz)', type: 'string' },
@@ -34,6 +34,8 @@
svcPart: 'Service-Anteil',
vat: 'Umsatzsteuer',
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?',
iSum: 'Zwischensumme',
dtRel: 'Freigegeben am: ',
dtCr: 'Erstellt am: ',