Files
vda-to-edifact-converter/js/invrpt-to-vda4913.js
2026-03-13 09:53:40 +01:00

399 lines
18 KiB
JavaScript

/**
* INVRPT D13A → VDA 4913 EDL36/35 Converter
*
* Converts EDIFACT INVRPT (Inventory Report) messages into
* VDA 4913 format with EDL36 (delivery note) and EDL35 (stock) records.
*
* Record structure produced:
* 711 Header
* 712 Transport (one per INVRPT message)
* 713 Delivery note header (EDL36 or EDL35)
* 714 Position (material / qty)
* 719 Trailer
*/
window.EDIBridge = window.EDIBridge || {};
class InvrptToVDA4913 {
// ─── Public API ──────────────────────────────────────────────────
static async convertAsync(edifactContent, config) {
let parsed;
// Try rigorous parsing via ts-edifact if available
if (window.electronAPI && window.electronAPI.parseEdifactLib) {
try {
const libResult = await window.electronAPI.parseEdifactLib(edifactContent);
if (libResult.success && libResult.data) {
parsed = { segments: libResult.data };
}
} catch (e) {
console.warn('ts-edifact parsing fall-back to basic parser:', e);
}
}
// Fallback to basic parser
if (!parsed) {
const parser = window.EDIBridge.DelforParser;
if (!parser) throw new Error('DelforParser nicht geladen.');
parsed = parser.parse(edifactContent);
}
return this.processParsedData(parsed.segments, config);
}
// For backwards compatibility if called synchronously without await
static convert(edifactContent, config) {
const parser = window.EDIBridge.DelforParser;
if (!parser) throw new Error('DelforParser nicht geladen.');
const parsed = parser.parse(edifactContent);
return this.processParsedData(parsed.segments, config);
}
static processParsedData(segments, config) {
const getVal = (seg, elIdx, compIdx) => {
if (!seg || !seg.elements) return '';
const el = seg.elements[elIdx];
if (!el) return '';
if (Array.isArray(el)) return el[compIdx] || '';
if (compIdx === 0) return el;
if (elIdx === 0 && compIdx === 1) return seg.elements[1] || '';
return '';
};
const groups = this.splitMessages(segments, getVal);
const firstGroup = groups[0] || [];
const nadWH = firstGroup.find(s => s.tag === 'NAD' && getVal(s, 0, 0) === 'WH');
const nadGM = firstGroup.find(s => s.tag === 'NAD' && getVal(s, 0, 0) === 'GM');
const dtm137 = firstGroup.find(s => s.tag === 'DTM' && getVal(s, 0, 0) === '137');
const targetId = nadWH ? (getVal(nadWH, 1, 0) || getVal(nadWH, 0, 1)) : '';
const sourceId = nadGM ? (getVal(nadGM, 1, 0) || getVal(nadGM, 0, 1)) : '';
const msgDateRaw = dtm137 ? getVal(dtm137, 0, 1) : '';
const msgDate = msgDateRaw.length === 8 ? msgDateRaw.slice(2) : (msgDateRaw || '000000');
const unz = segments.find(s => s.tag === 'UNZ');
const unzRef = unz ? getVal(unz, 1, 0) : '00001';
const transNo1 = unzRef;
const transNo2 = String((parseInt(unzRef) || 0) + 1);
const records = [];
records.push(this.build711(targetId, sourceId, transNo1, transNo2, msgDate, config));
let totalRecords712 = 0;
let totalRecords713 = 0;
let totalRecords714 = 0;
for (const group of groups) {
const bgm = group.find(s => s.tag === 'BGM');
const bgmType = bgm ? getVal(bgm, 0, 0) : '';
// We now process BGM+35 (Inventory Report) and BGM+78 (Inventory Movement)
// Re-group into LIN blocks
let currentLin = null;
let currentInv = null;
let linBlocks = [];
for (const seg of group) {
if (seg.tag === 'LIN') {
currentLin = { seg, invs: [] };
linBlocks.push(currentLin);
currentInv = null;
} else if (seg.tag === 'INV' && currentLin) {
currentInv = { seg, details: [] };
currentLin.invs.push(currentInv);
} else if (currentInv) {
currentInv.details.push(seg);
} else if (currentLin) {
currentLin.invs.push({ seg: null, details: [seg] }); // segments under LIN but before INV
}
}
for (const linBlock of linBlocks) {
const lin = linBlock.seg;
const material = getVal(lin, 2, 0);
for (const invBlock of linBlock.invs) {
const inv = invBlock.seg;
const details = invBlock.details;
const qty156 = details.find(s => s.tag === 'QTY' && getVal(s, 0, 0) === '156');
const qty145 = details.find(s => s.tag === 'QTY' && getVal(s, 0, 0) === '145');
const qtyOnHand = qty156 ? (parseFloat(getVal(qty156, 0, 1)) || 0) : 0;
const qtyCumulative = qty145 ? (parseFloat(getVal(qty145, 0, 1)) || 0) : 0;
let unitObj = qty156 || qty145;
const unit = unitObj ? (getVal(unitObj, 0, 2) || 'PCE') : 'PCE';
const vdaUnit = (unit === 'PCE' || unit === 'C62' || unit === 'EA') ? 'ST' :
(unit === 'KGM' ? 'KG' : (unit === 'MTR' ? 'M ' : 'ST'));
const dtm179 = details.find(s => s.tag === 'DTM' && getVal(s, 0, 0) === '179');
const invDateRaw = dtm179 ? getVal(dtm179, 0, 1) : msgDateRaw;
const invDate = invDateRaw.length === 8 ? invDateRaw.slice(2) : (invDateRaw || '000000');
// Global or detail refs
const rffDQ = details.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'DQ') || group.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'DQ');
const rffTN = details.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'TN') || group.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'TN');
const rffAAU = details.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'AAU') || group.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'AAU');
const rffON = details.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'ON') || group.find(s => s.tag === 'RFF' && getVal(s, 0, 0) === 'ON');
const dqNo = rffDQ ? getVal(rffDQ, 0, 1) : '';
const tnNo = rffTN ? getVal(rffTN, 0, 1) : '';
const aauNo = rffAAU ? getVal(rffAAU, 0, 1) : '';
const onNo = rffON ? getVal(rffON, 0, 1) : '';
// Lieferschein-Nr (current reference)
const currentDn = aauNo || tnNo || dqNo;
const groupLoc11 = group.find(s => s.tag === 'LOC' && getVal(s, 0, 0) === '11');
const unloadingPoint = groupLoc11 ? (getVal(groupLoc11, 1, 0) || '').replace(/^W/, '') : '';
// Get specific storage location for this stock
const loc18 = details.find(s => s.tag === 'LOC' && getVal(s, 0, 0) === '18');
const storageLocation = loc18 ? getVal(loc18, 1, 0) : '';
// EDL36 (Movements) logic
if (qty156 || currentDn) {
// Determine transaction key (Vorgangsschlüssel) based on INV and QTY
// Default to 36 (Abgangsmeldung)
let vdaKey = '36';
if (inv) {
const direction = getVal(inv, 0, 0); // DE 4501
const reason = getVal(inv, 2, 0); // DE 4499
if (direction === '2' || reason === '1') {
vdaKey = '30'; // Eingangsmeldung
} else if (reason === '4' || reason === '3') {
vdaKey = '32'; // Differenz-/Verlustmeldung
} else if (reason === '2' || reason === '11') {
vdaKey = '36'; // Abgangsmeldung
}
}
records.push(this.build712(msgDate));
totalRecords712++;
records.push(this.build713(currentDn || '00000000', invDate, unloadingPoint, vdaKey, targetId, onNo));
totalRecords713++;
records.push(this.build714(material, qtyOnHand, vdaUnit, dqNo || currentDn));
totalRecords714++;
}
// EDL35 (Stock balances) generated for QTY+145
if (qty145) {
records.push(this.build712(msgDate));
totalRecords712++;
// Stock reports use Vorgangsschlüssel 35
records.push(this.build713('00000000', invDate, storageLocation || unloadingPoint, '35', targetId, onNo));
totalRecords713++;
records.push(this.build714(material, qtyCumulative, vdaUnit, dqNo));
totalRecords714++;
}
}
}
}
records.push(this.build719(
totalRecords712, totalRecords713, totalRecords714,
totalRecords712, totalRecords713, totalRecords714
));
return records.join('\r\n') + '\r\n';
}
// ─── Split EDIFACT segments into UNH message groups ──────────────
static splitMessages(segments, getVal) {
const groups = [];
let current = null;
for (const seg of segments) {
if (seg.tag === 'UNH') {
if (current) groups.push(current);
current = [seg];
} else if (seg.tag === 'UNT') {
if (current) {
current.push(seg);
groups.push(current);
current = null;
}
} else if (current) {
current.push(seg);
}
}
if (current) groups.push(current);
return groups;
}
// ─── Format Helpers ──────────────────────────────────────────────
static padN(val, len) {
let v = String(val || '').replace(/[^0-9]/g, '');
return v.padStart(len, '0').slice(-len);
}
static padA(val, len) {
return String(val || '').padEnd(len, ' ').slice(0, len);
}
static padID(val, len) {
let v = String(val || '').replace(/[^0-9]/g, '');
// Prefer the front of the ID if it exceeds VDA limits
return v.padEnd(len, ' ').slice(0, len).trim().padStart(len, '0');
}
// ─── Record Builders ─────────────────────────────────────────────
/**
* 711 - Vorsatz Lieferschein- und Transportdaten
* Version 03
* Pos 01: 1-3 (3) Konstant "711"
* Pos 02: 4-5 (2) Version "03"
* Pos 03: 6-14 (9) Daten-Empfänger-Nummer
* Pos 04: 15-23 (9) Daten-Sender-Nummer
* Pos 05: 24-28 (5) Übertragungs-Nummer-Alt
* Pos 06: 29-33 (5) Übertragungs-Nummer-Neu
* Pos 07: 34-39 (6) Übertragungsdatum (JJMMTT)
*/
static build711(targetId, sourceId, transNo1, transNo2, date, config) {
let r = '71102';
// Custom override for targetId (e.g. "10305") via Mapping or Setting
let target = targetId;
if (config && window.EDIBridge.ConfigParser) {
const parser = window.EDIBridge.ConfigParser;
// 1. Try specific mapping (e.g. 8810 -> 10305)
const mapped = parser.findInSection(config, 'VDA711_ID_MAPPING', targetId);
if (mapped) {
target = mapped;
} else {
// 2. Fallback to global setting if no specific mapping exists
target = parser.lookupGeneral(config, 'OWN_CUSTOMER_NUMBER', targetId);
}
}
r += this.padA(target, 9); // Daten-Empfänger
r += this.padA(sourceId, 9); // Daten-Sender
r += this.padN(transNo1, 5);
r += this.padN(transNo2, 5);
r += this.padN(date, 6);
r += ''.padEnd(128 - r.length, ' ');
return r.slice(0, 128);
}
/**
* 712 - Einmalige Daten des Transports
* Version 03
* Pos 01: 1-3 (3) Konstant "712"
* Pos 02: 4-5 (2) Version "03"
* Pos 03: 6-13 (8) Sendungs-Ladungs-Bezugs-Nummer
* Pos 04: 14-16 (3) Werk Lieferant
* Pos 05: 17-30 (14) Frachtführer Name/Nummer
* Pos 06: 31-36 (6) Übergabedatum (JJMMTT)
* Pos 07: 37-40 (4) Übergabezeit (HHMM)
* Pos 08: 41-47 (7) Sendungs-Gewicht Brutto (kg)
* Pos 09: 48-54 (7) Sendungs-Gewicht Netto (kg)
*/
static build712(date) {
let r = '71202';
r += this.padN('0', 8); // Sendungs-Ladungs-Bezug-Nr
r += this.padA('', 3); // Werk Lieferant
r += this.padA('', 14); // Frachtführer
r += this.padN(date, 6); // Datum
r += this.padN('0', 4); // Zeit
r += this.padN('0', 7); // Brutto
r += this.padN('0', 7); // Netto
r += ''.padEnd(128 - r.length, ' ');
return r.slice(0, 128);
}
/**
* 713 - Einmalige Daten des Lieferscheins (Kopf)
* Version 03
* Pos 01: 1-3 (3) Konstant "713"
* Pos 02: 4-5 (2) Version "03"
* Pos 03: 6-13 (8) Lieferschein-Nummer
* Pos 04: 14-19 (6) Versanddatum (JJMMTT)
* Pos 05: 20-24 (5) Abladestelle
* Pos 06: 25-26 (2) Versandart
* Pos 08: 31-42 (12) Bestellnummer
* Pos 09: 43-44 (2) Vorgangs-Schlüssel (EDL)
* Pos 11: 49-51 (3) Werk Kunde
* Pos 16: 77-85 (9) Lieferanten-Nummer
*/
static build713(dnNo, date, unloadingPoint, edlType, targetId, orderNo) {
let r = '713';
// User requested shifting DN left by 2 chars (occupying the version field area to allow 10 digits)
r += this.padID(dnNo, 10); // Lieferschein-Nr (4-13)
r += this.padN(date, 6); // Datum (14-19)
r += this.padA(unloadingPoint, 5); // Abladestelle (20-24)
r += '00'; // Versandart (25-26)
r += ' '; // Filler (27-30)
r += this.padA(orderNo, 12); // Bestellnummer (31-42)
const validKeys = ['30', '32', '33', '35', '36', '40'];
r += this.padN(validKeys.includes(edlType) ? edlType : '00', 2); // Vorgangsschlüssel (43-44)
r += ' '; // Filler (45-48)
r += this.padA(targetId.slice(0, 3), 3); // Werk Kunde (49-51)
r += ''.padEnd(25, ' '); // Filler to index 76 (52-76)
r += this.padA(targetId, 9); // Lieferanten-Nummer (Pos 16, index 76-84)
r += ''.padEnd(25, ' '); // Filler to index 110 (85-109)
r += this.padA(dnNo, 14); // Dokument-Nr. Kunde (Pos 20, index 110-123)
r += ''.padEnd(4, ' '); // Filler to 128
return r.slice(0, 128);
}
/**
* 714 - Lieferscheinpositionsdaten
* Version 03
* Pos 01: 1-3 (3) Konstant "714"
* Pos 02: 4-5 (2) Version "03"
* Pos 03: 6-27 (22) Sachnummer-Kunde
* Pos 04: 28-49 (22) Sachnummer-Lieferant
* Pos 05: 50-52 (3) Ursprungsland
* Pos 06: 53-65 (13) Liefermenge (mit 3 impliziten Dezimalstellen, VDA Standard)
* Pos 07: 66-67 (2) Mengeneinheit
* Pos 12: 87-89 (3) Positions-Nummer Lieferschein
* Pos 14: 91-105 (15) Chargen-Nummer
* Pos 22: 121-128 (8) Ursprung-Lieferschein-Nr
*/
static build714(custMat, qty, unit, refNo) {
const qtyEncoded = Math.round(qty * 1000);
let r = '71403';
r += this.padA(custMat, 22); // Sachnummer-Kunde
r += this.padA('', 22); // Sachnummer-Lieferant
r += '000'; // Ursprungsland
r += this.padN(String(qtyEncoded), 13); // Liefermenge (13 Stellen)
r += this.padA(unit, 2); // ME
r += ''.padEnd(19, ' '); // Filler to 86
r += this.padN('0', 3); // Pos-Nr (87-89)
r += ' '; // Filler
r += this.padA('', 15); // Charge (91-105)
r += ' '; // Filler to 120
r += this.padID(refNo, 8); // Ursprung-LS (121-128)
return r.slice(0, 128);
}
/**
* 719 - Nachsatz Lieferschein- und Transportdaten
* Version 02
*/
static build719(cnt712, cnt713, cnt714, tot712, tot713, tot714) {
let r = '71902';
r += this.padN('0', 8);
r += this.padN(String(1), 7); // Zähler 711
r += this.padN(String(cnt712), 7);
r += this.padN(String(cnt713), 7);
r += this.padN(String(cnt714), 7);
r += this.padN('0', 7); // Zähler 715
r += this.padN('0', 7); // Zähler 716
r += this.padN('0', 7); // Zähler 718
r += this.padN(String(1), 7); // Zähler 719
r += this.padN('0', 7); // Zähler 717
r += ''.padEnd(128 - r.length, ' ');
return r.slice(0, 128);
}
}
window.EDIBridge.InvrptToVDA4913 = InvrptToVDA4913;