/** * VDA 4905 Generator * Strictly follows BSH VDA 4905 Spec (Feb 2017) */ window.EDIBridge = window.EDIBridge || {}; class VDA4905Generator { static generate(parsed) { const segments = parsed.segments; const records = []; let count511 = 0, count512 = 0, count513 = 0, count514 = 0; // --- Helpers --- 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; // Fallback for flat elements: if asking for comp 1 of el 0, but el 0 is flat, maybe it's in el 1 if (elIdx === 0 && compIdx === 1) return seg.elements[1] || ''; return ''; }; const findSeg = (tag, qual) => segments.find(s => s.tag === tag && getVal(s, 0, 0) === qual); // --- 1. Header (Record 511) --- const nadBY = findSeg('NAD', 'BY'); const nadSE = findSeg('NAD', 'SE'); const custId = getVal(nadBY, 1, 0) || getVal(nadBY, 2, 0); // Try el 1 or 2 const suppId = getVal(nadSE, 1, 0) || getVal(nadSE, 2, 0); const bgm = segments.find(s => s.tag === 'BGM'); const docNum = bgm ? (getVal(bgm, 1, 0) || getVal(bgm, 0, 1)) : ''; const dtm137 = findSeg('DTM', '137'); const docVerify = dtm137 ? getVal(dtm137, 0, 1) : ''; const docDateYYMMDD = docVerify.length === 8 ? docVerify.slice(2, 8) : (docVerify || '000000'); // Old Release Number: Prioritize AIF (Previous) let rffOld = segments.find(s => s.tag === 'RFF' && ['AIF', 'AAN', 'AAL', 'PL'].includes(getVal(s, 0, 0))); let oldDocNum = rffOld ? getVal(rffOld, 0, 1) : '00000'; // Old Release Date (DTM+192) const dtm192 = findSeg('DTM', '192'); const oldDocVerify = dtm192 ? getVal(dtm192, 0, 1) : ''; const oldDocDateYYMMDD = oldDocVerify.length === 8 ? oldDocVerify.slice(2, 8) : (oldDocVerify || '000000'); records.push(this.build511(custId, suppId, docNum, docDateYYMMDD, oldDocNum)); count511++; // --- 2. Loop through Articles (LIN) --- let currentLinData = null; for (let i = 0; i < segments.length; i++) { const s = segments[i]; if (s.tag === 'LIN') { if (currentLinData) { const added514 = this.processLin(currentLinData, records, custId, docNum, docDateYYMMDD, oldDocNum, oldDocDateYYMMDD); count512++; count513++; count514 += added514; } else { for (let k = i + 1; k < segments.length && segments[k].tag !== 'LIN'; k++) { if (segments[k].tag === 'RFF' && getVal(segments[k], 0, 0) === 'AIF') { oldDocNum = getVal(segments[k], 0, 1); records[0] = this.build511(custId, suppId, docNum, docDateYYMMDD, oldDocNum); break; } } } currentLinData = { lin: s, qtyDtms: [], rffs: [], pia: null, imd: null, locs: [], dtms: [] }; } else if (currentLinData) { if (s.tag === 'QTY') { let date = ''; let rffs = []; for (let j = i + 1; j < segments.length; j++) { const next = segments[j]; if (next.tag === 'QTY' || next.tag === 'LIN' || next.tag === 'UNS' || next.tag === 'CNT') break; if (next.tag === 'DTM') { const q = getVal(next, 0, 0); if (['10', '2', '11', '64', '63', '69', '171', '50', '137', '131', '242'].includes(q)) { date = getVal(next, 0, 1); } } if (next.tag === 'RFF' || next.tag === 'DOC') { rffs.push(next); } } currentLinData.qtyDtms.push({ qtySeg: s, date, rffs }); } else if (s.tag === 'RFF') currentLinData.rffs.push(s); else if (s.tag === 'PIA') currentLinData.pia = s; else if (s.tag === 'IMD') currentLinData.imd = s; else if (s.tag === 'LOC') currentLinData.locs.push(s); else if (s.tag === 'DTM') currentLinData.dtms.push(s); } } if (currentLinData) { const added514 = this.processLin(currentLinData, records, custId, docNum, docDateYYMMDD, oldDocNum, oldDocDateYYMMDD); count512++; count513++; count514 += added514; } // --- 3. Trailer (Record 519) --- records.push(this.build519(count511, count512, count513, count514)); return records.map(r => r + '\r\n').join(''); } static processLin(data, records, custId, docNum, docDate, headerOldDoc, headerOldDate) { 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 loc11 = data.locs.find(l => getVal(l, 0, 0) === '11'); const plant = loc11 ? getVal(loc11, 1, 0) : ' '; const custMat = getVal(data.lin, 2, 0); const rffON = data.rffs.find(r => getVal(r, 0, 0) === 'ON'); const orderNo = rffON ? getVal(rffON, 0, 1) : ''; const rffAif = data.rffs.find(r => getVal(r, 0, 0) === 'AIF'); const oldDocNum = rffAif ? getVal(rffAif, 0, 1) : headerOldDoc; const dtm192 = data.dtms.find(d => getVal(d, 0, 0) === '192'); let oldDocDate = dtm192 ? getVal(dtm192, 0, 1) : headerOldDate; if (oldDocDate.length === 8) oldDocDate = oldDocDate.slice(2); const loc159 = data.locs.find(l => getVal(l, 0, 0) === '159'); let unloading = loc159 ? getVal(loc159, 1, 0) : ''; if (!unloading && loc11) unloading = getVal(loc11, 1, 0); let unit = 'ST'; if (data.qtyDtms.length > 0) { const u = getVal(data.qtyDtms[0].qtySeg, 0, 2); if (u === 'PCE' || u === 'C62') unit = 'ST'; else if (u === 'KGM') unit = 'KG'; else if (u === 'MTR') unit = 'M '; } records.push(this.build512(plant, docNum, docDate, custMat, orderNo, unloading, unit, oldDocNum, oldDocDate)); const schedules = []; for (const item of data.qtyDtms) { const qual = getVal(item.qtySeg, 0, 0); const qty = getVal(item.qtySeg, 0, 1); if (['1', '12', '11', '21', '113'].includes(qual)) { let d = item.date; if (!d) d = docDate; if (d.length === 8) d = d.slice(2); // VDA 4905: Quantities are usually whole numbers or scaled as needed. // Removed implicit x1000 scaling as per user feedback (15000 should be 15000). const mappedQty = String(Math.round(parseFloat(qty) || 0)); schedules.push({ date: d || '000000', qty: mappedQty, qual }); } } schedules.sort((a, b) => { if (a.qual === '12' && b.qual !== '12') return -1; if (a.qual !== '12' && b.qual === '12') return 1; return 0; }); const chunk513 = schedules.slice(0, 5); const chunkRest = schedules.slice(5); // EFZ (Eingangs-Fortschrittszahl) = QTY+70 // QTY+194 sind einzelne Lieferschein-Mengen, NICHT die Fortschrittszahl! // VDA 4905: Quantities are usually whole numbers as per user requirements const qty70 = data.qtyDtms.find(q => getVal(q.qtySeg, 0, 0) === '70'); const efzRaw = qty70 ? (parseFloat(getVal(qty70.qtySeg, 0, 1)) || 0) : 0; const efz = String(Math.round(efzRaw)); // Pick latest candidate for head of 513 // 48=Received, 194=Received (alt), 12=In-Transit let candidates = data.qtyDtms.filter(q => ['48', '194', '12'].includes(getVal(q.qtySeg, 0, 0))); if (candidates.length === 0) candidates = data.qtyDtms.filter(q => getVal(q.qtySeg, 0, 0) === '1'); const lastRec = candidates.length > 0 ? candidates[candidates.length - 1] : null; let weDate = lastRec && lastRec.date ? lastRec.date : ''; if (!weDate && data.dtms) { const dtmRec = data.dtms.find(d => ['131', '50', '171', '137', '11'].includes(getVal(d, 0, 0))); weDate = dtmRec ? getVal(dtmRec, 0, 1) : ''; } if (weDate.length === 8) weDate = weDate.slice(2); // lastQty likewise without x1000 scaling const lastQtyRaw = lastRec ? (parseFloat(getVal(lastRec.qtySeg, 0, 1)) || 0) : 0; const lastQty = String(Math.round(lastQtyRaw)); let lastDn = ''; const dnQuals = ['AAU', 'AAK', 'VS', 'VN', 'IF', 'SI', 'RE']; if (lastRec && lastRec.rffs && lastRec.rffs.length > 0) { const rr = lastRec.rffs.find(seg => dnQuals.includes(getVal(seg, 0, 0))); if (rr) lastDn = getVal(rr, 0, 1); } if (!lastDn) { const rff = data.rffs.find(r => dnQuals.includes(getVal(r, 0, 0))); lastDn = rff ? getVal(rff, 0, 1) : ''; } let lastDate = lastRec && lastRec.date ? lastRec.date : ''; if (!lastDate && data.dtms) { const dtmL = data.dtms.find(d => ['11', '171', '137', '131', '50'].includes(getVal(d, 0, 0))); lastDate = dtmL ? getVal(dtmL, 0, 1) : ''; } if (lastDate.length === 8) lastDate = lastDate.slice(2); // Null-Stellung: DTM+51 Datum let nullStellung = ''; if (qty70) { // DTM+51 folgt direkt nach QTY+70 in der DELFOR const qty70idx = data.qtyDtms.indexOf(qty70); // Suche DTM+51 in den Artikel-DTMs const dtm51 = data.dtms ? data.dtms.find(d => getVal(d, 0, 0) === '51') : null; if (dtm51) { nullStellung = getVal(dtm51, 0, 1); if (nullStellung.length === 8) nullStellung = nullStellung.slice(2); } } records.push(this.build513(chunk513, weDate, lastDn, lastDate, lastQty, efz, nullStellung)); let count514 = 0; for (let i = 0; i < chunkRest.length; i += 8) { const chunk = chunkRest.slice(i, i + 8); records.push(this.build514(chunk)); count514++; } return count514; } // --- Format Helpers --- static padN(val, len) { let v = String(val || '').replace(/[^0-9]/g, ''); if (v.length > len) v = v.replace(/^0+/, ''); return v.padStart(len, '0').slice(-len); } static padA(val, len) { return String(val || '').padEnd(len, ' ').slice(0, len); } // --- Builders --- static build511(cust, supp, docNum, date, oldDocNum) { let r = '51101'; r += this.padA(cust, 9); r += this.padA(supp, 9); r += this.padN(oldDocNum || '00000', 5); // Old Trans No r += this.padN(docNum, 5); // New Trans No r += this.padN(date, 6); r += ''.padEnd(89, ' '); return r; } static build512(plant, docNum, docDate, custMat, orderNo, unloading, unit, oldDocNum, oldDocDate) { let r = '51201'; r += this.padA(plant, 3); r += this.padN(docNum, 9); r += this.padN(docDate, 6); r += this.padN(oldDocNum || '000000000', 9); r += this.padN(oldDocDate || '000000', 6); r += this.padA(custMat, 22); r += this.padA('', 22); r += this.padA(orderNo, 12); r += this.padA(unloading, 5); r += ' '; r += this.padA(unit, 2); r += 'L'; r += ' '; r += 'S'; r += 'D '; r += '01 '; r += ' '; return r; } static build513(schedules5, weDate, lastDn, lastDate, lastQty, efz, nullStellung) { let r = '51301'; r += this.padN(weDate || '000000', 6); r += this.padN(lastDn || '00000000', 8); r += this.padN(lastDate || '000000', 6); r += this.padN(lastQty || '0', 12); r += this.padN(efz || '0', 10); for (let i = 0; i < 5; i++) { if (i < schedules5.length) { const s = schedules5[i]; r += this.padN(s.date, 6); r += this.padN(s.qty, 9); } else { r += ' '; r += ' '; } } // Null-Stellung (6 Zeichen JJMMTT) am Ende von 513 r += this.padN(nullStellung || '000000', 6); return r; } static build514(schedules8) { let r = '51401'; for (let i = 0; i < 8; i++) { if (i < schedules8.length) { const s = schedules8[i]; r += this.padN(s.date, 6); r += this.padN(s.qty, 9); } else { r += ' '; r += ' '; } } r += ' '; return r; } static build519(c511, c512, c513, c514) { let r = '51901'; r += this.padN(c511, 7); r += this.padN(c512, 7); r += this.padN(c513, 7); r += this.padN(c514, 7); r += '0000000'; r += '0000000'; r += '0000001'; r += ''.padEnd(74, ' '); return r; } } window.EDIBridge.VDA4905Generator = VDA4905Generator;