Emit invoices as validated ZUGFeRD (DATEV) and XRechnung (B2G)
Playwright Tests / test (pull_request) Has been cancelled
Playwright Tests / test (pull_request) Has been cancelled
Completes the eRechnung output path: finalized invoices are emitted as a ZUGFeRD/Factur-X EN 16931 hybrid (DATEV) or, when a Leitweg-ID is present, as XRechnung 3.0 for B2G. Both are externally validated as ACCEPTED (0 errors) and PDF/A-3B COMPLIANT against the ProcessWeb eInvoice service. - ERechnungMapper: FdsInvoiceData -> EN 16931 model. Seller master data now from Fuchs:ERechnung:Seller config (VAT id BT-31, Steuernummer BT-32, optional Handelsregister BT-30, IBAN/BIC, contact). Adds payment terms BT-20/BT-9 (BR-CO-25), buyer VAT id, §13b reverse charge, and the service date/period (BT-72 / BG-14) parsed from ProvisionPeriod (BR-DE-TMP-32). B2G -> XRechnung profile with the required electronic addresses/contact. - ERechnungValidator: client for POST /validatepdf (EN 16931 XML + PDF/A-3 in one call). A pure "scenario not matched" with zero errors is not treated as a hard failure; real errors optionally withhold the hybrid (FailOnError). - ERechnungSettings: seller + validation config; wired in Program.cs; ServiceUrl in appsettings. - Online editor: structured German-only dialogs for the recipient address (incl. Leitweg-ID) and the service date/period (single date or range). - Bumps the eRechnungLib submodule to the CII rsm-namespace / PDF-A subtype fix. Fuchs.Tests 514/514, eRechnungLib 97/97. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1569,12 +1569,14 @@ $inv.eAddress = function (ev) {
|
||||
{ name: 'postalCode', label: 'PLZ', type: 'text', value: cur.postalCode || '' },
|
||||
{ name: 'city', label: 'Ort', type: 'text', value: cur.city || '' },
|
||||
{ name: 'countryCode', label: 'Land', type: 'select', url: $inv.adrCountries, value: (cur.countryCode || 'DE') },
|
||||
{ name: 'vatId', label: 'USt-IdNr. (nur bei Firmen)', type: 'text', value: cur.vatId || '' }
|
||||
{ name: 'vatId', label: 'USt-IdNr. (nur bei Firmen)', type: 'text', value: cur.vatId || '' },
|
||||
{ name: 'leitwegId', label: 'Leitweg-ID (nur Behörden / B2G)', type: 'text', value: cur.leitwegId || '' }
|
||||
];
|
||||
let hint = $$.dc('adr-conformity').append([
|
||||
$$.dc('hd').text('Hinweis zur DATEV-/eRechnungs-Konformität'),
|
||||
$$.dc('tx').text('Pflicht: Name, Straße, PLZ, Ort und Land. Die USt-IdNr. nur bei Firmen angeben — '
|
||||
+ 'für eine Rechnung an eine Privatperson einfach leer lassen.')
|
||||
+ 'für eine Rechnung an eine Privatperson einfach leer lassen. Eine Leitweg-ID nur bei '
|
||||
+ 'Behörden (B2G) — dann wird die Rechnung automatisch als XRechnung erzeugt.')
|
||||
]);
|
||||
$ocms.dlgform(flds, {
|
||||
title: 'Rechnungsempfänger',
|
||||
@@ -1583,7 +1585,8 @@ $inv.eAddress = function (ev) {
|
||||
let a = {
|
||||
name: res.name || '', contact: res.contact || '', line2: res.line2 || '',
|
||||
street: res.street || '', postalCode: res.postalCode || '', city: res.city || '',
|
||||
countryCode: (res.countryCode || 'DE'), vatId: ('' + (res.vatId || '')).trim()
|
||||
countryCode: (res.countryCode || 'DE'), vatId: ('' + (res.vatId || '')).trim(),
|
||||
leitwegId: ('' + (res.leitwegId || '')).trim()
|
||||
};
|
||||
let txt = $inv.composeAddress(a);
|
||||
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
||||
@@ -1594,12 +1597,46 @@ $inv.eAddress = function (ev) {
|
||||
});
|
||||
};
|
||||
|
||||
/* Leistungsdatum / -zeitraum: structured German-date entry only (no free text). A single date =
|
||||
Leistungsdatum (BT-72); a from+to date = Leistungszeitraum (BG-14). The composed value is a
|
||||
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. */
|
||||
$inv.provToIso = function (dmy) {
|
||||
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)) : '';
|
||||
};
|
||||
$inv.eProvisionPeriod = function (ev) {
|
||||
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) || [];
|
||||
let flds = [
|
||||
{ name: 'von', label: 'Leistungsdatum', type: 'date', value: $inv.provToIso(dates[0] || '') },
|
||||
{ name: 'bis', label: 'Leistungszeitraum – bis (optional)', type: 'date', value: $inv.provToIso(dates[1] || '') }
|
||||
];
|
||||
let hint = $$.dc('adr-conformity').append([
|
||||
$$.dc('hd').text('Leistungsdatum / -zeitraum'),
|
||||
$$.dc('tx').text('Für ein einzelnes Datum nur „Leistungsdatum" ausfüllen; für einen Zeitraum '
|
||||
+ 'zusätzlich das Bis-Datum. Nur strukturierte Datumsauswahl — für die E-Rechnung empfohlen.')
|
||||
]);
|
||||
$ocms.dlgform(flds, {
|
||||
title: 'Leistungsdatum / -zeitraum',
|
||||
addcontent: hint,
|
||||
success: function (res) {
|
||||
let v = ('' + (res.von || '')).trim(), b = ('' + (res.bis || '')).trim();
|
||||
let txt = v === '' ? '' : ((b === '' || b === v) ? v : (v + ' - ' + b));
|
||||
if (ev.data && ev.data.t) { ev.data.t.rwText(txt); }
|
||||
if (typeof (ev.data || {}).change === 'function') { ev.data.change(txt); }
|
||||
$inv.d.sync({ Target: 'provisionperiod', Value: txt });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$inv.eHtml = function (ev) {
|
||||
/* The recipient address is edited through the structured dialog whenever an invoice draft is
|
||||
active; reminders (no invoice draft token) keep the plain free-text editor. */
|
||||
if (ev.data && !(ev.data instanceof jQuery) && ev.data.nme === 'invoiceaddress'
|
||||
&& $inv.d && typeof $inv.d.token === 'function' && $inv.d.token() !== '') {
|
||||
return $inv.eAddress.call(this, ev);
|
||||
/* The recipient address and the service date/period are edited through structured dialogs
|
||||
whenever an invoice draft is active; reminders (no invoice draft token) keep the plain
|
||||
free-text editor. */
|
||||
if (ev.data && !(ev.data instanceof jQuery) && $inv.d && typeof $inv.d.token === 'function' && $inv.d.token() !== '') {
|
||||
if (ev.data.nme === 'invoiceaddress') { return $inv.eAddress.call(this, ev); }
|
||||
if (ev.data.nme === 'provisionperiod') { return $inv.eProvisionPeriod.call(this, ev); }
|
||||
}
|
||||
let t = $(this), frmct = ev.data instanceof jQuery ? ev.data : ev.data.t;
|
||||
/* Single-line fields must stay plain text — the TinyMCE/html editor wraps the value in <p>
|
||||
|
||||
Reference in New Issue
Block a user