diff options
Diffstat (limited to 'lib/soap/ecc')
| -rw-r--r-- | lib/soap/ecc/mapper/po-mapper.ts | 254 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 3 |
2 files changed, 187 insertions, 70 deletions
diff --git a/lib/soap/ecc/mapper/po-mapper.ts b/lib/soap/ecc/mapper/po-mapper.ts index 4f8b8034..fef85662 100644 --- a/lib/soap/ecc/mapper/po-mapper.ts +++ b/lib/soap/ecc/mapper/po-mapper.ts @@ -42,21 +42,33 @@ export async function mapECCPOHeaderToBusiness( debugLog('ECC PO 헤더 매핑 시작', { ebeln: eccHeader.EBELN }); // projectId 찾기 (PSPID → projects.code 기반) + // PSPID가 없으면 불완전 데이터로 처리하고 에러 발생 let projectId: number | null = null; - if (eccHeader.PSPID) { - try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, eccHeader.PSPID), + if (!eccHeader.PSPID) { + debugError('PSPID 누락 - 불완전 데이터', { + ebeln: eccHeader.EBELN, + ztitle: eccHeader.ZTITLE + }); + throw new Error(`PSPID가 없는 불완전 데이터: EBELN=${eccHeader.EBELN}`); + } + + try { + const project = await db.query.projects.findFirst({ + where: eq(projects.code, eccHeader.PSPID), + }); + if (project) { + projectId = project.id; + debugLog('프로젝트 ID 찾음', { pspid: eccHeader.PSPID, projectId }); + } else { + debugError('프로젝트를 찾을 수 없음 - 불완전 데이터', { + pspid: eccHeader.PSPID, + ebeln: eccHeader.EBELN }); - if (project) { - projectId = project.id; - debugLog('프로젝트 ID 찾음', { pspid: eccHeader.PSPID, projectId }); - } else { - debugError('프로젝트를 찾을 수 없음', { pspid: eccHeader.PSPID }); - } - } catch (error) { - debugError('프로젝트 조회 중 오류', { pspid: eccHeader.PSPID, error }); + throw new Error(`프로젝트를 찾을 수 없습니다: PSPID=${eccHeader.PSPID}, EBELN=${eccHeader.EBELN}`); } + } catch (error) { + debugError('프로젝트 조회 중 오류', { pspid: eccHeader.PSPID, error }); + throw error; } // vendorId 찾기 (LIFNR 기반) @@ -104,14 +116,13 @@ export async function mapECCPOHeaderToBusiness( } }; - // projectId와 vendorId 필수 체크 - if (!projectId) { - debugError('프로젝트를 찾을 수 없어 매핑을 건너뜁니다', { pspid: eccHeader.PSPID }); - throw new Error(`프로젝트를 찾을 수 없습니다: PSPID=${eccHeader.PSPID}`); - } + // vendorId 필수 체크 (projectId는 위에서 이미 체크됨) if (!vendorId) { - debugError('벤더를 찾을 수 없어 매핑을 건너뜁니다', { lifnr: eccHeader.LIFNR }); - throw new Error(`벤더를 찾을 수 없습니다: LIFNR=${eccHeader.LIFNR}`); + debugError('벤더를 찾을 수 없어 매핑을 건너뜁니다', { + lifnr: eccHeader.LIFNR, + ebeln: eccHeader.EBELN + }); + throw new Error(`벤더를 찾을 수 없습니다: LIFNR=${eccHeader.LIFNR}, EBELN=${eccHeader.EBELN}`); } // 계약서 내용 구성 (ZMM_NOTE에서 가져옴) @@ -134,10 +145,12 @@ export async function mapECCPOHeaderToBusiness( contractNo: eccHeader.EBELN || '', // EBELN - 구매오더번호 contractName: eccHeader.ZTITLE || eccHeader.EBELN || '', // ZTITLE - 발주제목 contractContent, // 계약서 내용 - status: eccHeader.ZPO_CNFM_STAT || 'ACTIVE', // ZPO_CNFM_STAT - 구매오더확인상태 + // TODO: ZPO_CNFM_STAT 값을 ContractStatus enum으로 매핑하는 로직 필요 + // 현재는 ECC에서 받은 값을 그대로 사용하되, null인 경우 undefined로 변환 + status: eccHeader.ZPO_CNFM_STAT || undefined, // ZPO_CNFM_STAT - 구매오더확인상태 startDate: parseDate(eccHeader.ZPO_DT || null), // ZPO_DT - 발주일자 - endDate: null, // ZMM_DT에서 가져와야 함 - deliveryDate: null, // ZMM_DT에서 가져와야 함 + endDate: null, // TODO: ZMM_DT의 ZPLN_ED_DT(예정종료일자) 중 최대값으로 계산 필요 + deliveryDate: null, // TODO: ZMM_DT의 ZPO_DLV_DT(PO납기일자) 중 최대값으로 계산 필요 // SAP ECC 기본 필드들 paymentTerms: eccHeader.ZTERM || null, // ZTERM - 지급조건코드 @@ -151,7 +164,8 @@ export async function mapECCPOHeaderToBusiness( purchaseOrg: eccHeader.EKORG || null, // EKORG - 구매조직코드 purchaseGroup: eccHeader.EKGRP || null, // EKGRP - 구매그룹코드 exchangeRate: eccHeader.WKURS ? parseAmount(eccHeader.WKURS) : null, // WKURS - 환율 - poConfirmStatus: eccHeader.ZPO_CNFM_STAT || null, // ZPO_CNFM_STAT - 구매오더확인상태 + poConfirmStatus: eccHeader.ZPO_CNFM_STAT || null, // ZPO_CNFM_STAT - 구매오더확인상태 (기존 필드) + ZPO_CNFM_STAT: eccHeader.ZPO_CNFM_STAT || null, // SAP 구매오더확인상태 원본값 // 계약/보증 관련 contractGuaranteeCode: eccHeader.ZCNRT_GRNT_CD || null, // ZCNRT_GRNT_CD - 계약보증코드 @@ -162,7 +176,7 @@ export async function mapECCPOHeaderToBusiness( // 금액 관련 budgetAmount: parseAmount(eccHeader.ZBGT_AMT || null), // ZBGT_AMT - 예산금액 budgetCurrency: eccHeader.ZBGT_CURR || null, // ZBGT_CURR - 예산금액 통화키 - currency: eccHeader.ZPO_CURR || 'KRW', // ZPO_CURR - 통화키 + currency: eccHeader.ZPO_CURR || null, // ZPO_CURR - 통화키 (null 가능) totalAmount: parseAmount(eccHeader.ZPO_AMT || null), // ZPO_AMT - 발주금액 totalAmountKrw: parseAmount(eccHeader.ZPO_AMT_KRW || null), // ZPO_AMT_KRW - 발주금액 KRW @@ -185,12 +199,12 @@ export async function mapECCPOHeaderToBusiness( netTotal: parseAmount(eccHeader.ZPO_AMT || null), // ZPO_AMT와 동일 remarks: eccHeader.ETC_2 || null, // ETC_2 - 확장2 - // 기본값들 - discount: null, - tax: null, - shippingFee: null, - partialShippingAllowed: false, - partialPaymentAllowed: false, + // 기본값들 (ECC 인터페이스에서 제공되지 않는 필드들) + discount: null, // TODO: 개별 품목별로는 할인 정보 있을 수 있음 (ZPDT_EXDS_AMT - 할인/할증금액) + tax: null, // TODO: 개별 품목별 세금 합산 필요 + shippingFee: null, // TODO: 운송비 정보 (ZTRNS_UPR - 운송단가)가 있으나 헤더 레벨 집계 로직 필요 + partialShippingAllowed: false, // ECC에서 제공되지 않음 + partialPaymentAllowed: false, // ECC에서 제공되지 않음 version: 1, }; @@ -231,25 +245,25 @@ export async function mapECCPODetailToBusiness( // 2. 아이템이 없으면 새로 생성 debugLog('아이템이 없어서 새로 생성', { matnr: eccDetail.MATNR }); - // 프로젝트 정보 설정 - const projectNo = eccDetail.PSPID || 'DEFAULT'; - const packageCode = 'AUTO_GENERATED'; // 기본값으로 설정 + // PSPID를 ProjectNo로 사용 (projects.code와 매핑됨) + // PSPID가 없으면 불완전 데이터이므로 이미 상위에서 에러 발생되어 여기까지 오지 않음 + const projectNo = eccDetail.PSPID || 'UNKNOWN'; // notNull 필드이므로 기본값 필요 const newItemData = { ProjectNo: projectNo, itemCode: eccDetail.MATNR, - itemName: eccDetail.MAKTX || eccDetail.MATNR || 'Unknown Item', - packageCode: packageCode, - smCode: null, // SM 코드는 ECC 데이터에서 제공되지 않음 + itemName: eccDetail.MAKTX || eccDetail.MATNR, // notNull 필드 + packageCode: null, // nullable 필드 - ECC에서 제공되지 않으므로 null + smCode: null, // ECC 데이터에서 제공되지 않음 description: eccDetail.MAKTX || null, parentItemCode: null, itemLevel: null, deleteFlag: 'N', - unitOfMeasure: null, - steelType: null, - gradeMaterial: null, + unitOfMeasure: eccDetail.ZPO_UNIT || null, // ZPO_UNIT - 구매오더수량단위 + steelType: null, // ECC 데이터에서 제공되지 않음 + gradeMaterial: null, // ECC 데이터에서 제공되지 않음 changeDate: null, - baseUnitOfMeasure: null, + baseUnitOfMeasure: eccDetail.BPRME || null, // BPRME - 구매단가단위 }; const [insertedItem] = await db.insert(items).values(newItemData).returning({ id: items.id }); @@ -291,49 +305,117 @@ export async function mapECCPODetailToBusiness( } }; - // 세율 계산 (MWSKZ 기반) - const calculateTaxRate = (mwskz: string | null): string | null => { - if (!mwskz) return null; - // 일반적인 한국 세율 매핑 (실제 비즈니스 로직에 따라 조정 필요) - switch (mwskz) { - case '10': - return '10.00'; - case '00': - return '0.00'; - default: - return '10.00'; // 기본값 - } - }; - const quantity = parseQuantity(eccDetail.MENGE || null); const unitPrice = parseAmount(eccDetail.NETPR || null); - const taxRate = calculateTaxRate(eccDetail.MWSKZ || null); - const totalLineAmount = parseAmount(eccDetail.NETWR || null); + + // SAP ECC 금액 필드 + // 금액 관계: NETWR = BRTWR + ZPDT_EXDS_AMT + // - BRTWR: 기본 오더총액 + // - ZPDT_EXDS_AMT: 조정금액 (할인은 음수, 할증은 양수) + // - NETWR: 최종 오더정가 + const NETWR = parseAmount(eccDetail.NETWR || null); // 오더정가 (최종 정가) + const BRTWR = parseAmount(eccDetail.BRTWR || null); // 오더총액 (기본 총액) + const ZPDT_EXDS_AMT = parseAmount(eccDetail.ZPDT_EXDS_AMT || null); // 할인/할증금액 (조정금액) + + // 세율과 세액 - SAP에서 계산된 값이 있으면 그대로 사용 + // SAP에서 세금 정보를 별도로 제공하지 않으므로, 일단 null로 설정 + const taxRate: string | null = null; + const taxAmount: string | null = null; + + // 세금코드 (MWSKZ - 매출부가가치세코드: V1, V2 등의 두자리 코드) + const taxType = eccDetail.MWSKZ || null; + + // totalLineAmount는 최종 정가(NETWR)를 사용 + const totalLineAmount = NETWR; + + // 금액은 SAP에서 이미 계산/검증되어 오므로 그대로 저장 + // 금액 관계: NETWR = BRTWR + ZPDT_EXDS_AMT + + // MWSKZ(매출부가가치세코드)를 taxType에 매핑 + // 각 코드(V1, V2 등)별 taxRate는 추후 별도로 관리될 예정 + + // 날짜 파싱 + const parseDate = (dateStr: string | null): string | null => { + if (!dateStr || dateStr.length !== 8) return null; + try { + const year = dateStr.substring(0, 4); + const month = dateStr.substring(4, 6); + const day = dateStr.substring(6, 8); + return `${year}-${month}-${day}`; + } catch (error) { + debugError('날짜 파싱 오류', { dateStr, error }); + return null; + } + }; - // 세액 계산 - let taxAmount: string | null = null; - if (unitPrice && taxRate) { + // 정수 파싱 + const parseInteger = (intStr: string | null): number | null => { + if (!intStr) return null; try { - const unitPriceNum = parseFloat(unitPrice); - const taxRateNum = parseFloat(taxRate); - const calculatedTaxAmount = (unitPriceNum * quantity * taxRateNum) / 100; - taxAmount = calculatedTaxAmount.toString(); + const num = parseInt(intStr); + return isNaN(num) ? null : num; } catch (error) { - debugError('세액(taxAmount) 계산 오류((unitPriceNum * quantity * taxRateNum) / 100)', { unitPrice, taxRate, quantity, error }); + debugError('정수 파싱 오류', { intStr, error }); + return null; } - } + }; // 매핑 const mappedData: ContractItemData = { contractId, itemId: itemId!, // 아이템이 없으면 자동 생성되므로 null이 될 수 없음 + itemNo: eccDetail.EBELP || null, // EBELP - 구매오더품목번호 (품번) + prNo: eccDetail.BANFN || null, // BANFN - 구매요청번호 (PR번호) + prItemNo: eccDetail.BNFPO || null, // BNFPO - 구매요청품목번호 (PR 품번) + materialGroup: eccDetail.MATKL || null, // MATKL - 자재그룹 + weight: parseAmount(eccDetail.NTGEW || null), // NTGEW - 순중량 + weightUnit: eccDetail.GEWEI || null, // GEWEI - 중량단위 + totalWeight: parseAmount(eccDetail.BRGEW || null), // BRGEW - 총중량 description: eccDetail.MAKTX || null, quantity, - unitPrice, + ZPO_UNIT: eccDetail.ZPO_UNIT || null, // 구매오더수량단위 + unitPrice, // NETPR - 구매단가 + + // 가격 관련 추가 필드 + PEINH: parseInteger(eccDetail.PEINH || null), // 가격단위값 (예: 1, 10, 100) + BPRME: eccDetail.BPRME || null, // 구매단가단위 (EA, KG 등) + ZNETPR: parseAmount(eccDetail.ZNETPR || null), // 발주단가 + ZREF_NETPR: parseAmount(eccDetail.ZREF_NETPR || null), // 참조단가 + taxRate, taxAmount, + taxType, // MWSKZ - 매출부가가치세코드 + NETWR, // 오더정가 (최종 정가) + BRTWR, // 오더총액 (기본 총액) + ZPDT_EXDS_AMT, // 할인/할증금액 (조정금액) totalLineAmount, - remark: eccDetail.ZPO_RMK || null, + + // 위치 정보 + WERKS: eccDetail.WERKS || null, // 플랜트코드 + LGORT: eccDetail.LGORT || null, // 저장위치 + + // RFQ 추적 + ANFNR: eccDetail.ANFNR || null, // RFQ번호 + ANFPS: eccDetail.ANFPS || null, // RFQ품목번호 + + // 자재 추적 + ZPO_LOT_NO: eccDetail.ZPO_LOT_NO || null, // Steel Material Marking No + + // 볼륨 정보 + VOLUM: parseAmount(eccDetail.VOLUM || null), // 볼륨 + VOLEH: eccDetail.VOLEH || null, // 볼륨단위 + + // 날짜 정보 + ZPO_DLV_DT: parseDate(eccDetail.ZPO_DLV_DT || null), // PO납기일자 + ZPLN_ST_DT: parseDate(eccDetail.ZPLN_ST_DT || null), // 예정시작일자 + ZPLN_ED_DT: parseDate(eccDetail.ZPLN_ED_DT || null), // 예정종료일자 + LFDAT: parseDate(eccDetail.LFDAT || null), // PR Delivery Date + ZRCV_DT: parseDate(eccDetail.ZRCV_DT || null), // 구매접수일자 + + // 기타 + ZCON_IND: eccDetail.ZCON_IND || null, // 시리즈구분 (SS 등) + + remark: eccDetail.ZPO_RMK || null, // 발주비고 }; debugSuccess('ECC PO 상세 매핑 완료', { @@ -365,6 +447,39 @@ export async function mapAndSaveECCPOData( // 1. 헤더 매핑 및 저장 const contractData = await mapECCPOHeaderToBusiness(header); + // Details에서 날짜 정보 집계 + const parseDate = (dateStr: string | null): Date | null => { + if (!dateStr || dateStr.length !== 8) return null; + try { + const year = dateStr.substring(0, 4); + const month = dateStr.substring(4, 6); + const day = dateStr.substring(6, 8); + return new Date(`${year}-${month}-${day}`); + } catch { + return null; + } + }; + + // 모든 details의 납기일자 중 최대값을 deliveryDate로 설정 + const deliveryDates = details + .map(d => parseDate(d.ZPO_DLV_DT || null)) + .filter((date): date is Date => date !== null); + + if (deliveryDates.length > 0) { + const maxDeliveryDate = new Date(Math.max(...deliveryDates.map(d => d.getTime()))); + contractData.deliveryDate = maxDeliveryDate.toISOString().split('T')[0]; + } + + // 모든 details의 예정종료일자 중 최대값을 endDate로 설정 + const endDates = details + .map(d => parseDate(d.ZPLN_ED_DT || null)) + .filter((date): date is Date => date !== null); + + if (endDates.length > 0) { + const maxEndDate = new Date(Math.max(...endDates.map(d => d.getTime()))); + contractData.endDate = maxEndDate.toISOString().split('T')[0]; + } + // 중복 체크 (contractNo 기준) const existingContract = await tx.query.contracts.findFirst({ where: eq(contracts.contractNo, contractData.contractNo), @@ -429,10 +544,10 @@ export async function mapAndSaveECCPOData( } processedCount++; - } catch (error) { + } catch (err) { debugError('PO 데이터 처리 중 오류', { ebeln: header.EBELN, - error + error: err }); // 개별 PO 처리 실패 시 해당 PO만 스킵하고 계속 진행 continue; @@ -509,3 +624,4 @@ export function validateECCPOData( errors, }; } + diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts index c0557d0c..c1c56cf3 100644 --- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts @@ -155,6 +155,7 @@ export async function mapECCRfqHeaderToRfqLast( // 담당자 찾기 const inChargeUserInfo = await findUserInfoByEKGRP(eccHeader.EKGRP || null); const inChargeUserId = inChargeUserInfo?.userId || null; + const inChargeUserName = inChargeUserInfo?.userName || null; // 담당자명 추가 // 대표 PR Item 기반으로 projectId, itemCode, itemName, 설계담당자 설정 (없으면 첫번째 PR Item 사용) let projectId: number | null = null; @@ -219,7 +220,7 @@ export async function mapECCRfqHeaderToRfqLast( remark: null, pic: inChargeUserId, // 담당자 ID picCode: eccHeader.EKGRP || null, // 구매그룹코드 - picName: null, // 담당자명은 별도 조회 필요 + picName: inChargeUserName, // 담당자명 (EKGRP로 조회) sentBy: null, createdBy: inChargeUserId || 1, updatedBy: inChargeUserId || 1, |
