fix(invoices): Leistungsdatum editor lost or mis-formatted the service date
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
- serializeObject: an empty optional date field was run through parseDt, became null and invalidated the whole form, so leaving "bis" empty made dlgform call success with only loginaccount and wiped the stored period. Empty optional date fields are now sent as ''. - dlgform: an invalid form no longer calls success with partial data; it stops the spinner and keeps the dialog open. - eProvisionPeriod: the date inputs return yyyy-MM-dd, which was saved verbatim; it is now stored as German dd.MM.yyyy as documented (PDF, eRechnung mapping). ISO values saved by the old editor are accepted when reopening, so their fields are prefilled instead of empty. - Add a Playwright regression test (e2e_tests/invoice-provisionperiod.cjs) driving the real form builder, serializer and dialog submit handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e2faf52cd2
commit
4145ab699c
@@ -1114,12 +1114,14 @@ $inv.eAddress = function (ev) {
|
|||||||
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
||||||
backend maps to the eRechnung service date/period. */
|
backend maps to the eRechnung service date/period. */
|
||||||
$inv.provToIso = function (dmy) {
|
$inv.provToIso = function (dmy) {
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dmy || '')) { return dmy; }
|
||||||
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
||||||
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
||||||
};
|
};
|
||||||
$inv.eProvisionPeriod = function (ev) {
|
$inv.eProvisionPeriod = function (ev) {
|
||||||
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
||||||
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}/g) || [];
|
// Accept ISO values saved by older editor versions as well as German dates.
|
||||||
|
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}|\d{4}-\d{2}-\d{2}/g) || [];
|
||||||
let flds = [
|
let flds = [
|
||||||
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
||||||
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
||||||
@@ -1133,7 +1135,8 @@ $inv.eProvisionPeriod = function (ev) {
|
|||||||
title: 'Leistungsdatum / -zeitraum',
|
title: 'Leistungsdatum / -zeitraum',
|
||||||
addcontent: hint,
|
addcontent: hint,
|
||||||
success: function (res) {
|
success: function (res) {
|
||||||
let v = ('' + (res.von || '')).trim(), b = ('' + (res.bis || '')).trim();
|
let germanDate = (iso) => iso ? fdt(iso + 'T00:00:00', 'dd.MM.yyyy') : '';
|
||||||
|
let v = germanDate(res.von), b = germanDate(res.bis);
|
||||||
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
||||||
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
||||||
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ $ocms.vbl_send = function (ev) {
|
|||||||
if (rcheckableType.test(type) === true) {
|
if (rcheckableType.test(type) === true) {
|
||||||
val = e.checked ? (val !== '' ? val : 'true') : '';
|
val = e.checked ? (val !== '' ? val : 'true') : '';
|
||||||
}
|
}
|
||||||
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1) {
|
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1 && (val !== '' || req)) {
|
||||||
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
||||||
if (typeof val === 'boolean') {
|
if (typeof val === 'boolean') {
|
||||||
val = null;
|
val = null;
|
||||||
|
|||||||
@@ -613,7 +613,9 @@
|
|||||||
} else if (typeof options.success === 'function') {
|
} else if (typeof options.success === 'function') {
|
||||||
cfm = function (e) {
|
cfm = function (e) {
|
||||||
var c = $(this).ldng(1);
|
var c = $(this).ldng(1);
|
||||||
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) }));
|
var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) });
|
||||||
|
if (values === null) { c.ldng(0); return; }
|
||||||
|
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
|
||||||
if ((options.url || '') !== ''){
|
if ((options.url || '') !== ''){
|
||||||
$ocms.postXT({
|
$ocms.postXT({
|
||||||
url: options.url, data: qs, success: function (response) {
|
url: options.url, data: qs, success: function (response) {
|
||||||
|
|||||||
@@ -1696,12 +1696,14 @@ $inv.eAddress = function (ev) {
|
|||||||
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
||||||
backend maps to the eRechnung service date/period. */
|
backend maps to the eRechnung service date/period. */
|
||||||
$inv.provToIso = function (dmy) {
|
$inv.provToIso = function (dmy) {
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dmy || '')) { return dmy; }
|
||||||
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
||||||
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
||||||
};
|
};
|
||||||
$inv.eProvisionPeriod = function (ev) {
|
$inv.eProvisionPeriod = function (ev) {
|
||||||
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
||||||
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}/g) || [];
|
// Accept ISO values saved by older editor versions as well as German dates.
|
||||||
|
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}|\d{4}-\d{2}-\d{2}/g) || [];
|
||||||
let flds = [
|
let flds = [
|
||||||
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
||||||
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
||||||
@@ -1715,7 +1717,8 @@ $inv.eProvisionPeriod = function (ev) {
|
|||||||
title: 'Leistungsdatum / -zeitraum',
|
title: 'Leistungsdatum / -zeitraum',
|
||||||
addcontent: hint,
|
addcontent: hint,
|
||||||
success: function (res) {
|
success: function (res) {
|
||||||
let v = ('' + (res.von || '')).trim(), b = ('' + (res.bis || '')).trim();
|
let germanDate = (iso) => iso ? fdt(iso + 'T00:00:00', 'dd.MM.yyyy') : '';
|
||||||
|
let v = germanDate(res.von), b = germanDate(res.bis);
|
||||||
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
||||||
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
||||||
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -884,7 +884,7 @@ $ocms.vbl_send = function (ev) {
|
|||||||
if (rcheckableType.test(type) === true) {
|
if (rcheckableType.test(type) === true) {
|
||||||
val = e.checked ? (val !== '' ? val : 'true') : '';
|
val = e.checked ? (val !== '' ? val : 'true') : '';
|
||||||
}
|
}
|
||||||
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1) {
|
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1 && (val !== '' || req)) {
|
||||||
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
||||||
if (typeof val === 'boolean') {
|
if (typeof val === 'boolean') {
|
||||||
val = null;
|
val = null;
|
||||||
@@ -2392,7 +2392,9 @@ class NumArray extends Array {
|
|||||||
} else if (typeof options.success === 'function') {
|
} else if (typeof options.success === 'function') {
|
||||||
cfm = function (e) {
|
cfm = function (e) {
|
||||||
var c = $(this).ldng(1);
|
var c = $(this).ldng(1);
|
||||||
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) }));
|
var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) });
|
||||||
|
if (values === null) { c.ldng(0); return; }
|
||||||
|
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
|
||||||
if ((options.url || '') !== ''){
|
if ((options.url || '') !== ''){
|
||||||
$ocms.postXT({
|
$ocms.postXT({
|
||||||
url: options.url, data: qs, success: function (response) {
|
url: options.url, data: qs, success: function (response) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1677,12 +1677,14 @@ $inv.eAddress = function (ev) {
|
|||||||
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
German dd.MM.yyyy string ("18.06.2026" or "01.06.2026 - 30.06.2026") that the PDF shows and the
|
||||||
backend maps to the eRechnung service date/period. */
|
backend maps to the eRechnung service date/period. */
|
||||||
$inv.provToIso = function (dmy) {
|
$inv.provToIso = function (dmy) {
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dmy || '')) { return dmy; }
|
||||||
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
let m = ('' + (dmy || '')).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
||||||
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
return m ? (m[3] + '-' + ('0' + m[2]).slice(-2) + '-' + ('0' + m[1]).slice(-2)) : '';
|
||||||
};
|
};
|
||||||
$inv.eProvisionPeriod = function (ev) {
|
$inv.eProvisionPeriod = function (ev) {
|
||||||
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
let cur = ('' + (ev.data && ev.data.t ? ev.data.t.text() : '')).trim();
|
||||||
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}/g) || [];
|
// Accept ISO values saved by older editor versions as well as German dates.
|
||||||
|
let dates = cur.match(/\d{1,2}\.\d{1,2}\.\d{4}|\d{4}-\d{2}-\d{2}/g) || [];
|
||||||
let flds = [
|
let flds = [
|
||||||
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
||||||
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
||||||
@@ -1696,7 +1698,8 @@ $inv.eProvisionPeriod = function (ev) {
|
|||||||
title: 'Leistungsdatum / -zeitraum',
|
title: 'Leistungsdatum / -zeitraum',
|
||||||
addcontent: hint,
|
addcontent: hint,
|
||||||
success: function (res) {
|
success: function (res) {
|
||||||
let v = ('' + (res.von || '')).trim(), b = ('' + (res.bis || '')).trim();
|
let germanDate = (iso) => iso ? fdt(iso + 'T00:00:00', 'dd.MM.yyyy') : '';
|
||||||
|
let v = germanDate(res.von), b = germanDate(res.bis);
|
||||||
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
||||||
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
||||||
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -701,7 +701,7 @@ $ocms.vbl_send = function (ev) {
|
|||||||
if (rcheckableType.test(type) === true) {
|
if (rcheckableType.test(type) === true) {
|
||||||
val = e.checked ? (val !== '' ? val : 'true') : '';
|
val = e.checked ? (val !== '' ? val : 'true') : '';
|
||||||
}
|
}
|
||||||
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1) {
|
if (dtf[0].substr(0, 4) === 'date' && dtf.length > 1 && (val !== '' || req)) {
|
||||||
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
val = parseDt(val, dtf.slice(1).join(':')); /* should result in date object */
|
||||||
if (typeof val === 'boolean') {
|
if (typeof val === 'boolean') {
|
||||||
val = null;
|
val = null;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,89 @@
|
|||||||
|
// Run with: node --test e2e_tests/invoice-provisionperiod.cjs
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
|
||||||
|
test('service dates and periods survive entry, editing and reopening', async () => {
|
||||||
|
const browser = await chromium.launch({ headless: true,
|
||||||
|
channel: process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined) });
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage({ timezoneId: 'Europe/Berlin' });
|
||||||
|
await page.setContent('<html><body></body></html>');
|
||||||
|
for (const file of [
|
||||||
|
'Fuchs/wwwroot/lib/jquery/jquery.js',
|
||||||
|
'Fuchs/js/intranet/oci_texts_basic_de.js',
|
||||||
|
'Fuchs/js/intranet/oci_basic.js',
|
||||||
|
'Fuchs/js/intranet/oci_mainbase.js',
|
||||||
|
'Fuchs/js/intranet/oci_main.js'
|
||||||
|
]) {
|
||||||
|
await page.addScriptTag({ content: read(file) });
|
||||||
|
}
|
||||||
|
// Use the production form builder/serializer and dialog submit handler;
|
||||||
|
// only the modal shell and server transport are replaced locally.
|
||||||
|
const source = read('Fuchs/js/intranet/modules/fis.inv_shared.js');
|
||||||
|
await page.addScriptTag({ content: 'var $inv = { d: { sync: function(delta) { window.lastDelta = delta; } } };\n'
|
||||||
|
+ source.slice(source.indexOf('$inv.provToIso ='), source.indexOf('/* Main recipient + CC')) });
|
||||||
|
const results = await page.evaluate(() => {
|
||||||
|
let dialog;
|
||||||
|
$ocms.dlg = (content, options) => { dialog = { content, options }; return dialog; };
|
||||||
|
function open(value) {
|
||||||
|
const t = $('<span>').text(value);
|
||||||
|
const changes = [];
|
||||||
|
$inv.eProvisionPeriod({ data: { t, change: (value) => changes.push(value) } });
|
||||||
|
return { t, changes, fields: dialog.content.find('input').map((i, e) => e.value).get() };
|
||||||
|
}
|
||||||
|
const cases = [
|
||||||
|
['', '2026-10-06', '', '06.10.2026'],
|
||||||
|
['', '2026-10-01', '2026-10-06', '01.10.2026 - 06.10.2026'],
|
||||||
|
['18.06.2026', '2026-10-06', '', '06.10.2026'],
|
||||||
|
['01.06.2026 - 30.06.2026', '2026-10-01', '2026-10-06', '01.10.2026 - 06.10.2026'],
|
||||||
|
['18.06.2026', '2026-10-01', '2026-10-06', '01.10.2026 - 06.10.2026'],
|
||||||
|
['01.06.2026 - 30.06.2026', '2026-10-06', '', '06.10.2026'],
|
||||||
|
['2026-06-18', '2026-10-06', '', '06.10.2026'],
|
||||||
|
['2026-06-01 - 2026-06-30', '2026-10-01', '2026-10-06', '01.10.2026 - 06.10.2026'],
|
||||||
|
['18.06.2026', '', '', ''],
|
||||||
|
['', '2026-10-06', '2026-10-06', '06.10.2026']
|
||||||
|
];
|
||||||
|
const dates = cases.map(([initial, von, bis, expected]) => {
|
||||||
|
const entry = open(initial);
|
||||||
|
dialog.content.find('[name=von]').val(von);
|
||||||
|
dialog.content.find('[name=bis]').val(bis);
|
||||||
|
dialog.options.confirm.call(dialog.content[0]);
|
||||||
|
const saved = window.lastDelta;
|
||||||
|
return { initial, von, bis, expected, saved, displayed: entry.t.text(), changes: entry.changes,
|
||||||
|
prefilled: entry.fields, reopened: open(saved.Value).fields };
|
||||||
|
});
|
||||||
|
const optional = $('<div><input name="date" type="date" data-format="date:dd.MM.yyyy;yyyy-MM-dd"></div>');
|
||||||
|
const emptyOptional = optional.serializeObject(true);
|
||||||
|
const emptyOptionalTyped = optional.serializeObject(true, { typedvalues: true });
|
||||||
|
optional.find('input').prop('required', true);
|
||||||
|
const emptyRequired = optional.serializeObject(true);
|
||||||
|
let submitted = false;
|
||||||
|
$ocms.dlgform([{ name: 'date', type: 'date', required: true }], { success: () => { submitted = true; } });
|
||||||
|
dialog.options.confirm.call(dialog.content[0]);
|
||||||
|
return { dates, emptyOptional, emptyOptionalTyped, emptyRequired, submitted };
|
||||||
|
});
|
||||||
|
for (const row of results.dates) {
|
||||||
|
const label = JSON.stringify([row.initial, row.von, row.bis]);
|
||||||
|
assert.deepEqual(row.saved, { Target: 'provisionperiod', Value: row.expected }, label);
|
||||||
|
assert.equal(row.displayed, row.expected, label);
|
||||||
|
assert.deepEqual(row.changes, [row.expected], label);
|
||||||
|
assert.deepEqual(row.reopened, row.von ? [row.von, row.bis === row.von ? '' : row.bis] : ['', ''], label);
|
||||||
|
}
|
||||||
|
assert.deepEqual(results.dates[2].prefilled, ['2026-06-18', '']);
|
||||||
|
assert.deepEqual(results.dates[3].prefilled, ['2026-06-01', '2026-06-30']);
|
||||||
|
assert.deepEqual(results.dates[6].prefilled, ['2026-06-18', '']);
|
||||||
|
assert.deepEqual(results.dates[7].prefilled, ['2026-06-01', '2026-06-30']);
|
||||||
|
assert.deepEqual(results.emptyOptional, { date: '' });
|
||||||
|
assert.deepEqual(results.emptyOptionalTyped, { date: '' });
|
||||||
|
assert.equal(results.emptyRequired, null);
|
||||||
|
assert.equal(results.submitted, false, 'invalid forms must not invoke success and clear existing values');
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user