349 lines
16 KiB
JavaScript
349 lines
16 KiB
JavaScript
/**
|
||
* VDA 4913 → DELVRY03 (XML-IDoc) Converter
|
||
* Based on: ifm electronic EDI Guide XML-DELVRY03 v1.0 (30.06.2020)
|
||
* Input: Parsed VDA 4913 object from VDAParser (vda-parser.js)
|
||
* Output: EDIFACT-style IDoc field map object ready for XML serialisation
|
||
* OR a flat key=value string for direct IDoc inbound processing.
|
||
*
|
||
* Segment mapping:
|
||
* VDA 711 → EDI_DC40 + E1EDL20 (delivery header)
|
||
* VDA 712 → E1EDL20 transport fields (carrier, weight)
|
||
* VDA 713 → E1EDL20 delivery note fields + E1ADRM1 (ship-to)
|
||
* VDA 714 → E1EDL24 (delivery item)
|
||
* VDA 715 → E1EDL37 (handling unit)
|
||
*/
|
||
window.EDIBridge = window.EDIBridge || {};
|
||
|
||
class VDA4913ToDELVRY03 {
|
||
|
||
// ── Unit mapping VDA → SAP/IDoc ──────────────────────────────────────────
|
||
static UNIT_MAP = {
|
||
'ST': 'PCE',
|
||
'PCE': 'PCE',
|
||
'KG': 'KG',
|
||
'KGM': 'KG',
|
||
'L': 'L',
|
||
'MTR': 'M',
|
||
'M': 'M',
|
||
'PAL': 'PAL',
|
||
'CTN': 'CTN',
|
||
};
|
||
|
||
// ── Transport type mapping VDA → SAP TRAGY ────────────────────────────────
|
||
static TRANSPORT_MAP = {
|
||
'1': '01', // Sea
|
||
'2': '02', // Rail
|
||
'3': '03', // Road
|
||
'4': '04', // Air
|
||
'5': '05', // Post
|
||
};
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Main entry point
|
||
// @param {object} parsed – result of VDAParser.parse()
|
||
// @param {object} config – { senderId, receiverId, plant, salesOrg, ... }
|
||
// @returns {object} – IDoc structure { control: {}, segments: [] }
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
static convert(parsed, config = {}) {
|
||
const idocs = [];
|
||
|
||
for (const interchange of (parsed.interchanges || [])) {
|
||
const hdr711 = interchange.header || {};
|
||
|
||
for (const transport of (interchange.transports || [])) {
|
||
const hdr712 = transport.data || {};
|
||
|
||
for (const dn of (transport.deliveryNotes || [])) {
|
||
const hdr713 = dn.header || {};
|
||
idocs.push(this._buildIDoc(hdr711, hdr712, hdr713, dn, config));
|
||
}
|
||
}
|
||
}
|
||
|
||
return idocs;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Build one IDoc per delivery note
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
static _buildIDoc(hdr711, hdr712, hdr713, dn, config) {
|
||
const now = new Date();
|
||
const dateNow = this._fmtDate(now);
|
||
const timeNow = this._fmtTime(now);
|
||
|
||
const senderId = config.senderId || hdr711.sourceId || 'SUPPLIER';
|
||
const receiverId = config.receiverId || hdr711.targetId || 'IFM';
|
||
const docNum = hdr713.dnNo || hdr712.refNo || '';
|
||
const delivDate = this._vdaDate(hdr713.date || hdr712.date || dateNow);
|
||
|
||
// ── EDI_DC40 – Control Record ─────────────────────────────────────────
|
||
const control = {
|
||
TABNAM: 'EDI_DC40',
|
||
MANDT: config.client || '600',
|
||
DOCNUM: docNum.padStart(16, '0'),
|
||
DOCREL: config.docrel || '750',
|
||
STATUS: '30',
|
||
DIRECT: '1',
|
||
OUTMOD: '2',
|
||
IDOCTYP: 'DELVRY03',
|
||
MESTYP: 'STPOD',
|
||
STD: 'E',
|
||
STDVRS: '003',
|
||
STDMES: 'DELVRY',
|
||
SNDPOR: config.sndPort || 'EDIPORT',
|
||
SNDPRT: 'LS',
|
||
SNDPRN: senderId.substring(0, 10),
|
||
SNDLAD: senderId.substring(0, 21),
|
||
RCVPOR: config.rcvPort || 'SAPPORT',
|
||
RCVPRT: 'LI',
|
||
RCVPRN: receiverId.substring(0, 10),
|
||
RCVLAD: '1000280',
|
||
CREDAT: dateNow,
|
||
CRETIM: timeNow,
|
||
};
|
||
|
||
const segments = [];
|
||
|
||
// ── E1EDL20 – Delivery Header ─────────────────────────────────────────
|
||
// VDA 712: carrier (16-30), date (30-36), time (36-40)
|
||
// VDA 713: dnNo (5-13), date (13-19), unloadingPoint (19-24)
|
||
const e1edl20 = {
|
||
_SEGMENT: 'E1EDL20',
|
||
VBELN: docNum.padStart(10, '0'), // Delivery note number
|
||
VSTEL: hdr713.unloadingPoint || config.shippingPoint || '', // Shipping point
|
||
VKORG: config.salesOrg || '',
|
||
LGNUM: config.warehouseNo || '',
|
||
ABLAD: hdr713.unloadingPoint || '', // Unloading point
|
||
BTGEW: String(hdr712.grossWeight || ''), // Total weight
|
||
NTGEW: String(hdr712.netWeight || ''), // Net weight
|
||
GEWEI: 'KG',
|
||
BOLNR: hdr712.refNo || '', // Bill of lading
|
||
TRATY: this.TRANSPORT_MAP[hdr712.transportType] || '',
|
||
TRAID: hdr712.carrierName || '', // Carrier name / transport ID
|
||
LIFEX: docNum, // External delivery note no.
|
||
PARID: senderId, // External partner number
|
||
PODAT: this._vdaDate(hdr713.date || hdr712.date || ''), // Proof of delivery date
|
||
POTIM: hdr712.time || '',
|
||
};
|
||
segments.push(e1edl20);
|
||
|
||
// ── E1ADRM1 – Ship-To Address (partner function WE) ───────────────────
|
||
// From VDA 713: qualifier (24-30), orderNo (30-38)
|
||
const e1adrm1 = {
|
||
_SEGMENT: 'E1ADRM1',
|
||
PARTNER_Q: 'WE',
|
||
ADDRESS_T: '1',
|
||
PARTNER_ID: config.shipToId || hdr713.qualifier || '',
|
||
NAME1: config.shipToName || 'ifm electronic gmbh',
|
||
STREET1: config.shipToStreet || '',
|
||
CITY1: config.shipToCity || '',
|
||
POSTL_COD1: config.shipToZip || '',
|
||
COUNTRY1: config.shipToCountry || 'DE',
|
||
};
|
||
segments.push(e1adrm1);
|
||
|
||
// ── E1ADRM1 – Supplier Address (partner function LF) ─────────────────
|
||
const e1adrm1_lf = {
|
||
_SEGMENT: 'E1ADRM1',
|
||
PARTNER_Q: 'LF',
|
||
ADDRESS_T: '1',
|
||
PARTNER_ID: senderId,
|
||
NAME1: config.supplierName || '',
|
||
STREET1: config.supplierStreet || '',
|
||
CITY1: config.supplierCity || '',
|
||
POSTL_COD1: config.supplierZip || '',
|
||
COUNTRY1: config.supplierCountry || 'DE',
|
||
};
|
||
segments.push(e1adrm1_lf);
|
||
|
||
// ── E1EDL24 – Delivery Items ──────────────────────────────────────────
|
||
// VDA 714: custMat (5-27), suppMat (27-49), qty (53-65)/1000, unit (65-67), refNo (77-90)
|
||
let posNr = 10;
|
||
for (const pos of (dn.positions || [])) {
|
||
const d = pos.data || {};
|
||
const unit = this.UNIT_MAP[d.unit] || d.unit || 'PCE';
|
||
|
||
const e1edl24 = {
|
||
_SEGMENT: 'E1EDL24',
|
||
POSNR: String(posNr).padStart(6, '0'),
|
||
MATNR: d.suppMat || '', // Supplier material number
|
||
MATWA: d.suppMat || '', // Material entered
|
||
ARKTX: '', // Short text
|
||
KDMAT: d.custMat || '', // Customer material number
|
||
LGMNG: String(d.qty || 0), // Actual delivery qty
|
||
VRKME: unit, // Sales unit
|
||
MEINS: unit, // Base unit
|
||
VGBEL: hdr713.orderNo || '', // Reference document (order)
|
||
VGPOS: String(posNr).padStart(6, '0'),
|
||
WERKS: config.plant || '1110', // Plant
|
||
};
|
||
segments.push(e1edl24);
|
||
|
||
// ── E1EDL37 – Handling Units (from VDA 715 packaging lines) ───────
|
||
// VDA 715: packMatCust (5-27), packMatSupp (27-49), qty (59-62),
|
||
// labelFrom (78-87), labelTo (87-96)
|
||
let huCounter = 1;
|
||
for (const pack of (pos.packaging || [])) {
|
||
const huId = String(huCounter).padStart(10, '0');
|
||
const e1edl37 = {
|
||
_SEGMENT: 'E1EDL37',
|
||
VPOSNR: String(posNr).padStart(6, '0'),
|
||
VEGR1: pack.packMatSupp || pack.packMatCust || '', // HU type
|
||
ANZPK: String(pack.qty || 1), // Number of packages
|
||
EXIDV: pack.labelFrom || huId, // External HU ID (label from)
|
||
EXIDV2: pack.labelTo || '', // Label to
|
||
};
|
||
segments.push(e1edl37);
|
||
huCounter++;
|
||
}
|
||
|
||
// ── E1EDL14 – DG Control Data (delivery item) – only if needed ───
|
||
// Skipped unless config.includeDG = true
|
||
|
||
posNr += 10;
|
||
}
|
||
|
||
// ── Free texts from VDA 716 ───────────────────────────────────────────
|
||
if (dn.texts && dn.texts.length > 0) {
|
||
const e1txth8 = {
|
||
_SEGMENT: 'E1TXTH8',
|
||
FUNCTION: 'H',
|
||
TDOBJECT: 'VBBK',
|
||
TDOBNAME: docNum,
|
||
TDID: '0001',
|
||
TDSPRAS: '1',
|
||
TDTEXTTYPE: 'GRKO',
|
||
LANGUA_ISO: 'DE',
|
||
};
|
||
segments.push(e1txth8);
|
||
|
||
for (const txt of dn.texts) {
|
||
segments.push({
|
||
_SEGMENT: 'E1TXTP8',
|
||
TDFORMAT: '*',
|
||
TDLINE: txt.substring(0, 132),
|
||
});
|
||
}
|
||
}
|
||
|
||
return { control, segments };
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Serialise IDoc array to flat IDoc text format (for file-based inbound)
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
static toIdocText(idocs) {
|
||
const lines = [];
|
||
let docNum = 1;
|
||
|
||
for (const idoc of idocs) {
|
||
const dn = String(docNum++).padStart(16, '0');
|
||
|
||
// Control record
|
||
const ctrl = { ...idoc.control, DOCNUM: dn };
|
||
lines.push(this._flatRecord('EDI_DC40', ctrl));
|
||
|
||
// Data segments
|
||
let segNum = 1;
|
||
for (const seg of idoc.segments) {
|
||
const segName = seg._SEGMENT;
|
||
const fields = { ...seg };
|
||
delete fields._SEGMENT;
|
||
lines.push(this._flatRecord(segName, fields, dn, segNum++));
|
||
}
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Serialise to structured JSON (for XML generation or further processing)
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
static toJSON(idocs) {
|
||
return JSON.stringify(idocs, null, 2);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Serialise to XML-IDoc format (as expected by ifm DELVRY03 guide)
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
static toXML(idocs) {
|
||
const xmlParts = ['<?xml version="1.0" encoding="UTF-8"?>', '<DELVRY03>'];
|
||
|
||
for (const idoc of idocs) {
|
||
xmlParts.push(' <IDOC BEGIN="1">');
|
||
|
||
// Control record
|
||
xmlParts.push(' <EDI_DC40 SEGMENT="1">');
|
||
for (const [k, v] of Object.entries(idoc.control)) {
|
||
if (v !== undefined && v !== '') {
|
||
xmlParts.push(` <${k}>${this._xmlEsc(v)}</${k}>`);
|
||
}
|
||
}
|
||
xmlParts.push(' </EDI_DC40>');
|
||
|
||
// Data segments
|
||
for (const seg of idoc.segments) {
|
||
const name = seg._SEGMENT;
|
||
xmlParts.push(` <${name} SEGMENT="1">`);
|
||
for (const [k, v] of Object.entries(seg)) {
|
||
if (k === '_SEGMENT') continue;
|
||
if (v !== undefined && v !== '') {
|
||
xmlParts.push(` <${k}>${this._xmlEsc(v)}</${k}>`);
|
||
}
|
||
}
|
||
xmlParts.push(` </${name}>`);
|
||
}
|
||
|
||
xmlParts.push(' </IDOC>');
|
||
}
|
||
|
||
xmlParts.push('</DELVRY03>');
|
||
return xmlParts.join('\n');
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// Helpers
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
/** VDA date YYMMDD → YYYYMMDD */
|
||
static _vdaDate(d) {
|
||
if (!d) return '';
|
||
d = String(d).replace(/\D/g, '');
|
||
if (d.length === 6) return '20' + d; // YYMMDD → YYYYMMDD
|
||
if (d.length === 8) return d; // already YYYYMMDD
|
||
return d;
|
||
}
|
||
|
||
/** Date object → YYYYMMDD */
|
||
static _fmtDate(dt) {
|
||
return dt.toISOString().replace(/-/g, '').substring(0, 8);
|
||
}
|
||
|
||
/** Date object → HHMMSS */
|
||
static _fmtTime(dt) {
|
||
return dt.toISOString().replace(/:/g, '').substring(11, 17);
|
||
}
|
||
|
||
/** Flat IDoc record line */
|
||
static _flatRecord(segName, fields, docNum = '', segNum = 0) {
|
||
const parts = [segName.padEnd(30)];
|
||
if (docNum) parts.push(docNum.padStart(16, '0'));
|
||
if (segNum) parts.push(String(segNum).padStart(6, '0'));
|
||
for (const [k, v] of Object.entries(fields)) {
|
||
parts.push(`${k}=${v}`);
|
||
}
|
||
return parts.join(' ');
|
||
}
|
||
|
||
/** XML escape */
|
||
static _xmlEsc(v) {
|
||
return String(v)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
}
|
||
|
||
window.EDIBridge.VDA4913ToDELVRY03 = VDA4913ToDELVRY03;
|