diff options
| -rw-r--r-- | app/[lng]/partners/(partners)/po/page.tsx | 1 | ||||
| -rw-r--r-- | components/contract/contract-items-card.tsx | 44 | ||||
| -rw-r--r-- | db/schema/contract.ts | 55 | ||||
| -rw-r--r-- | db/schema/items.ts | 2 | ||||
| -rw-r--r-- | lib/po/vendor-table/service.ts | 127 | ||||
| -rw-r--r-- | lib/po/vendor-table/types.ts | 53 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-actions.tsx | 7 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-columns.tsx | 9 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-items-dialog.tsx | 123 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 2 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/po-mapper.ts | 254 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 3 | ||||
| -rw-r--r-- | lib/utils.ts | 2 |
13 files changed, 542 insertions, 140 deletions
diff --git a/app/[lng]/partners/(partners)/po/page.tsx b/app/[lng]/partners/(partners)/po/page.tsx index c21d5e35..709b975c 100644 --- a/app/[lng]/partners/(partners)/po/page.tsx +++ b/app/[lng]/partners/(partners)/po/page.tsx @@ -2,7 +2,6 @@ import * as React from "react" import { type SearchParams } from "@/types/table" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { redirect } from "next/navigation" import { getValidFilters } from "@/lib/data-table" import { Skeleton } from "@/components/ui/skeleton" diff --git a/components/contract/contract-items-card.tsx b/components/contract/contract-items-card.tsx index 5bf5a927..82d273d4 100644 --- a/components/contract/contract-items-card.tsx +++ b/components/contract/contract-items-card.tsx @@ -7,8 +7,19 @@ interface ContractItem { specification?: string quantity?: number quantityUnit?: string + ZPO_UNIT?: string unitPrice?: number | string contractAmount?: number | string + + // SAP ECC 금액 필드 + NETWR?: number + BRTWR?: number + ZPDT_EXDS_AMT?: number + + // SAP 날짜 필드 + ZPO_DLV_DT?: string + ZPLN_ST_DT?: string + ZPLN_ED_DT?: string } interface ContractItemsCardProps { @@ -36,7 +47,10 @@ export function ContractItemsCard({ items, currency }: ContractItemsCardProps) { <th className="px-4 py-3 text-left font-medium">규격</th> <th className="px-4 py-3 text-right font-medium">수량</th> <th className="px-4 py-3 text-right font-medium">단가</th> - <th className="px-4 py-3 text-right font-medium">금액</th> + <th className="px-4 py-3 text-right font-medium">기본총액(BRTWR)</th> + <th className="px-4 py-3 text-right font-medium">조정금액(ZPDT)</th> + <th className="px-4 py-3 text-right font-medium">최종정가(NETWR)</th> + <th className="px-4 py-3 text-center font-medium">납기일자</th> </tr> </thead> <tbody> @@ -46,7 +60,7 @@ export function ContractItemsCard({ items, currency }: ContractItemsCardProps) { <td className="px-4 py-3">{item.itemDescription || "-"}</td> <td className="px-4 py-3">{item.specification || "-"}</td> <td className="px-4 py-3 text-right"> - {item.quantity} {item.quantityUnit || ""} + {item.quantity} {item.ZPO_UNIT || item.quantityUnit || ""} </td> <td className="px-4 py-3 text-right font-mono"> {item.unitPrice @@ -58,16 +72,30 @@ export function ContractItemsCard({ items, currency }: ContractItemsCardProps) { : formatNumber(parseFloat(item.unitPrice.toString())) : "-"} </td> + <td className="px-4 py-3 text-right font-mono"> + {item.BRTWR + ? currency + ? formatCurrency(item.BRTWR, currency) + : formatNumber(item.BRTWR) + : "-"} + </td> + <td className="px-4 py-3 text-right font-mono"> + {item.ZPDT_EXDS_AMT + ? currency + ? formatCurrency(item.ZPDT_EXDS_AMT, currency) + : formatNumber(item.ZPDT_EXDS_AMT) + : "-"} + </td> <td className="px-4 py-3 text-right font-mono font-medium"> - {item.contractAmount + {item.NETWR ? currency - ? formatCurrency( - parseFloat(item.contractAmount.toString()), - currency - ) - : formatNumber(parseFloat(item.contractAmount.toString())) + ? formatCurrency(item.NETWR, currency) + : formatNumber(item.NETWR) : "-"} </td> + <td className="px-4 py-3 text-center"> + {item.ZPO_DLV_DT || "-"} + </td> </tr> ))} </tbody> diff --git a/db/schema/contract.ts b/db/schema/contract.ts index e2a070ec..a04bb44b 100644 --- a/db/schema/contract.ts +++ b/db/schema/contract.ts @@ -90,6 +90,7 @@ export const contracts = pgTable('contracts', { purchaseGroup: varchar('purchase_group', { length: 10 }), // 구매그룹 (EKGRP - 구매그룹코드) exchangeRate: numeric('exchange_rate', { precision: 9, scale: 5 }), // 환율 (WKURS) poConfirmStatus: varchar('po_confirm_status', { length: 10 }), // PO확인상태 (ZPO_CNFM_STAT) + ZPO_CNFM_STAT: varchar('ZPO_CNFM_STAT', { length: 255 }), // SAP 구매오더확인상태 원본값 // 계약/보증 관련 contractGuaranteeCode: varchar('contract_guarantee_code', { length: 2 }), // 계약보증코드 (ZCNRT_GRNT_CD) @@ -163,17 +164,65 @@ export const contractItems = pgTable("contract_items", { .references(() => items.id, { onDelete: "cascade" }) , + // --- SAP ECC 인터페이스 매핑 필드 --- + itemNo: varchar("item_no", { length: 10 }), // 품번 (EBELP - 구매오더품목번호) + prNo: varchar("pr_no", { length: 10 }), // PR번호 (BANFN - 구매요청번호) + prItemNo: varchar("pr_item_no", { length: 5 }), // PR 품번 (BNFPO - 구매요청품목번호) + materialGroup: varchar("material_group", { length: 9 }), // 자재그룹 (MATKL) + weight: numeric("weight", { precision: 13, scale: 3 }), // 순중량 (NTGEW) + weightUnit: varchar("weight_unit", { length: 3 }), // 중량단위 (GEWEI) + totalWeight: numeric("total_weight", { precision: 15, scale: 3 }), // 총중량 (BRGEW) + // --- 품목(아이템) 단위 정보 --- description: text("description"), // 품목 설명 (스펙, 모델명 등) quantity: integer("quantity").notNull().default(1), - unitPrice: numeric("unit_price", { precision: 10, scale: 2 }), - + ZPO_UNIT: varchar("ZPO_UNIT", { length: 10 }), // 구매오더수량단위 (ZPO_UNIT) + unitPrice: numeric("unit_price", { precision: 10, scale: 2 }), // 구매단가 (NETPR) + + // 가격 관련 추가 필드 + PEINH: integer("PEINH"), // 가격단위값 (예: 1, 10, 100) + BPRME: varchar("BPRME", { length: 3 }), // 구매단가단위 (EA, KG 등) + ZNETPR: numeric("ZNETPR", { precision: 17, scale: 2 }), // 발주단가 + ZREF_NETPR: numeric("ZREF_NETPR", { precision: 17, scale: 2 }), // 참조단가 + + taxType: varchar("tax_type", { length: 10 }), // 세금코드( MWSKZ - 매출부가가치세코드이며, V1, V2 같은 두자리 코드를 가짐, 각 코드별 taxRate가 있음. 코드별 taxRate는 아직 받아오기 전임) taxRate: numeric("tax_rate", { precision: 5, scale: 2 }), // % (예: 10.00) taxAmount: numeric("tax_amount", { precision: 10, scale: 2 }), // 계산된 세금 + + // SAP ECC 금액 필드 (금액 관계: NETWR = BRTWR + ZPDT_EXDS_AMT) + NETWR: numeric("NETWR", { precision: 17, scale: 2 }), // 오더정가 (최종 정가) + BRTWR: numeric("BRTWR", { precision: 17, scale: 2 }), // 오더총액 (기본 총액) + ZPDT_EXDS_AMT: numeric("ZPDT_EXDS_AMT", { precision: 17, scale: 2 }), // 할인/할증금액 (조정금액: 할인은 음수, 할증은 양수) + totalLineAmount: numeric("total_line_amount", { precision: 12, scale: 2 }), // (수량×단가±할인+세금) 등 + // 위치 정보 + WERKS: varchar("WERKS", { length: 4 }), // 플랜트코드 + LGORT: varchar("LGORT", { length: 10 }), // 저장위치 + + // RFQ 추적 + ANFNR: varchar("ANFNR", { length: 10 }), // RFQ번호 + ANFPS: varchar("ANFPS", { length: 5 }), // RFQ품목번호 + + // 자재 추적 + ZPO_LOT_NO: varchar("ZPO_LOT_NO", { length: 50 }), // Steel Material Marking No + + // 볼륨 정보 + VOLUM: numeric("VOLUM", { precision: 15, scale: 3 }), // 볼륨 + VOLEH: varchar("VOLEH", { length: 3 }), // 볼륨단위 + + // 날짜 정보 + ZPO_DLV_DT: date("ZPO_DLV_DT"), // PO납기일자 + ZPLN_ST_DT: date("ZPLN_ST_DT"), // 예정시작일자 + ZPLN_ED_DT: date("ZPLN_ED_DT"), // 예정종료일자 + LFDAT: date("LFDAT"), // PR Delivery Date + ZRCV_DT: date("ZRCV_DT"), // 구매접수일자 + + // 기타 + ZCON_IND: varchar("ZCON_IND", { length: 1 }), // 시리즈구분 (SS 등) + // 비고 - remark: text("remark"), + remark: text("remark"), // 발주비고 (ZPO_RMK) // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), diff --git a/db/schema/items.ts b/db/schema/items.ts index e9c10058..fefb8e08 100644 --- a/db/schema/items.ts +++ b/db/schema/items.ts @@ -7,7 +7,7 @@ export const items = pgTable("items", { ProjectNo: varchar("project_no", { length: 100 }).notNull(), itemCode: varchar("item_code", { length: 100 }).notNull(), itemName: varchar("item_name", { length: 255 }).notNull(), - packageCode: varchar("package_code", { length: 255 }).notNull(), + packageCode: varchar("package_code", { length: 255 }), // nullable로 변경 (ECC에서 제공되지 않을 수 있음) smCode: varchar("sm_code", { length: 255 }), description: text("description"), parentItemCode: varchar("parent_item_code", { length: 18 }), // PRNT_CLAS_CD - 부모 아이템 코드 (부모 클래스 코드) diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts index 224dd2f1..bc4a693b 100644 --- a/lib/po/vendor-table/service.ts +++ b/lib/po/vendor-table/service.ts @@ -366,14 +366,60 @@ export async function getVendorPOItems(contractId: number): Promise<VendorPOItem id: contractItems.id, contractId: contractItems.contractId, itemId: contractItems.itemId, + itemNo: contractItems.itemNo, // EBELP - 구매오더품목번호 (품번) + prNo: contractItems.prNo, // BANFN - 구매요청번호 (PR번호) + prItemNo: contractItems.prItemNo, // BNFPO - 구매요청품목번호 (PR 품번) + materialGroup: contractItems.materialGroup, // MATKL - 자재그룹 + weight: contractItems.weight, // NTGEW - 순중량 + weightUnit: contractItems.weightUnit, // GEWEI - 중량단위 + totalWeight: contractItems.totalWeight, // BRGEW - 총중량 description: contractItems.description, quantity: contractItems.quantity, - unitPrice: contractItems.unitPrice, + unitPrice: contractItems.unitPrice, // NETPR - 구매단가 + + // 가격 관련 추가 필드 + PEINH: contractItems.PEINH, // 가격단위값 + BPRME: contractItems.BPRME, // 구매단가단위 + ZNETPR: contractItems.ZNETPR, // 발주단가 + ZREF_NETPR: contractItems.ZREF_NETPR, // 참조단가 + taxRate: contractItems.taxRate, taxAmount: contractItems.taxAmount, + taxType: contractItems.taxType, // MWSKZ - 매출부가가치세코드 totalLineAmount: contractItems.totalLineAmount, remark: contractItems.remark, + // SAP ECC 추가 필드들 + ZPO_UNIT: contractItems.ZPO_UNIT, // 구매오더수량단위 + NETWR: contractItems.NETWR, // 오더정가 (최종 정가) + BRTWR: contractItems.BRTWR, // 오더총액 (기본 총액) + ZPDT_EXDS_AMT: contractItems.ZPDT_EXDS_AMT, // 할인/할증금액 + + // 위치 정보 + WERKS: contractItems.WERKS, // 플랜트코드 + LGORT: contractItems.LGORT, // 저장위치 + + // RFQ 추적 + ANFNR: contractItems.ANFNR, // RFQ번호 + ANFPS: contractItems.ANFPS, // RFQ품목번호 + + // 자재 추적 + ZPO_LOT_NO: contractItems.ZPO_LOT_NO, // Steel Material Marking No + + // 볼륨 정보 + VOLUM: contractItems.VOLUM, // 볼륨 + VOLEH: contractItems.VOLEH, // 볼륨단위 + + // 날짜 정보 + ZPO_DLV_DT: contractItems.ZPO_DLV_DT, // PO납기일자 + ZPLN_ST_DT: contractItems.ZPLN_ST_DT, // 예정시작일자 + ZPLN_ED_DT: contractItems.ZPLN_ED_DT, // 예정종료일자 + LFDAT: contractItems.LFDAT, // PR Delivery Date + ZRCV_DT: contractItems.ZRCV_DT, // 구매접수일자 + + // 기타 + ZCON_IND: contractItems.ZCON_IND, // 시리즈구분 + // contracts 테이블 필드들 contractNo: contracts.contractNo, @@ -393,33 +439,74 @@ export async function getVendorPOItems(contractId: number): Promise<VendorPOItem .orderBy(contractItems.id); // VendorPOItem 타입으로 변환 + // SAP에서 제공하지 않는 데이터는 임의로 만들지 않고 그대로 표시 (FE에서 '-' 처리) const vendorPOItems: VendorPOItem[] = rawItems.map(row => ({ contractNo: row.contractNo || '', - itemNo: row.itemCode || 'AUTO-ITEM', // mock 데이터용 - prNo: `PR-${new Date().getFullYear()}-${String(row.id).padStart(4, '0')}`, // mock 데이터용 - materialGroup: row.packageCode || 'Unknown Group', - priceStandard: 'FOB', // mock 데이터용 기본값 + itemNo: row.itemNo || '', // EBELP - 구매오더품목번호 (품번) + prNo: row.prNo || '', // BANFN - 구매요청번호 (PR번호) + prItemNo: row.prItemNo || '', // BNFPO - 구매요청품목번호 (PR 품번) + materialGroup: row.materialGroup || '', // MATKL - 자재그룹 + priceStandard: '', // SAP에서 제공되지 않음 - FE에서 '-' 처리 materialNo: row.itemCode || '', itemDescription: row.itemName || '', materialSpec: row.description || '', - fittingNo: undefined, // contract_items에 없는 필드 - cert: undefined, // contract_items에 없는 필드 + fittingNo: undefined, // SAP에서 제공되지 않음 + cert: undefined, // SAP에서 제공되지 않음 material: row.gradeMaterial || undefined, specification: row.description || '', - quantity: row.quantity || 1, - quantityUnit: row.unitOfMeasure || 'EA', - weight: undefined, // contract_items에 없는 필드 - weightUnit: undefined, // contract_items에 없는 필드 - totalWeight: undefined, // contract_items에 없는 필드 - unitPrice: row.unitPrice ? Number(row.unitPrice) : 0, - priceUnit: 'KRW', // 기본값 - priceUnitValue: '원/EA', // 기본값 - contractAmount: row.totalLineAmount ? Number(row.totalLineAmount) : 0, - adjustmentAmount: undefined, // contract_items에 없는 필드 - deliveryDate: new Date().toISOString().split('T')[0], // 기본값 (오늘 날짜) - vatType: row.taxRate && Number(row.taxRate) > 0 ? '과세' : '면세', + quantity: row.quantity ?? 0, // null/undefined면 0, 0이면 0 표시 + quantityUnit: row.ZPO_UNIT || row.unitOfMeasure || '', // SAP 단위 우선, 없으면 items 테이블 단위 + ZPO_UNIT: row.ZPO_UNIT || undefined, // SAP 구매오더수량단위 + weight: row.weight ? Number(row.weight) : undefined, // NTGEW - 순중량 + weightUnit: row.weightUnit || undefined, // GEWEI - 중량단위 + totalWeight: row.totalWeight ? Number(row.totalWeight) : undefined, // BRGEW - 총중량 + unitPrice: row.unitPrice ? Number(row.unitPrice) : 0, // NETPR - 구매단가 + + // 가격 관련 추가 필드 + PEINH: row.PEINH ?? undefined, // 가격단위값 + BPRME: row.BPRME || undefined, // 구매단가단위 + ZNETPR: row.ZNETPR ? Number(row.ZNETPR) : undefined, // 발주단가 + ZREF_NETPR: row.ZREF_NETPR ? Number(row.ZREF_NETPR) : undefined, // 참조단가 + + priceUnit: row.BPRME || '', // BPRME을 priceUnit으로 사용 + priceUnitValue: row.PEINH ? String(row.PEINH) : '', // PEINH를 priceUnitValue로 사용 + contractAmount: row.NETWR ? Number(row.NETWR) : (row.totalLineAmount ? Number(row.totalLineAmount) : 0), // NETWR 우선, 없으면 totalLineAmount + adjustmentAmount: row.ZPDT_EXDS_AMT ? Number(row.ZPDT_EXDS_AMT) : undefined, // SAP 조정금액 + + // SAP ECC 금액 필드 + NETWR: row.NETWR ? Number(row.NETWR) : undefined, // 오더정가 (최종 정가) + BRTWR: row.BRTWR ? Number(row.BRTWR) : undefined, // 오더총액 (기본 총액) + ZPDT_EXDS_AMT: row.ZPDT_EXDS_AMT ? Number(row.ZPDT_EXDS_AMT) : undefined, // 할인/할증금액 + + // 위치 정보 + WERKS: row.WERKS || undefined, // 플랜트코드 + LGORT: row.LGORT || undefined, // 저장위치 + + // RFQ 추적 + ANFNR: row.ANFNR || undefined, // RFQ번호 + ANFPS: row.ANFPS || undefined, // RFQ품목번호 + + // 자재 추적 + ZPO_LOT_NO: row.ZPO_LOT_NO || undefined, // Steel Material Marking No + + // 볼륨 정보 + VOLUM: row.VOLUM ? Number(row.VOLUM) : undefined, // 볼륨 + VOLEH: row.VOLEH || undefined, // 볼륨단위 + + deliveryDate: row.ZPO_DLV_DT || '', // SAP 납기일자, 없으면 빈 문자열 (FE에서 '-' 처리) + ZPO_DLV_DT: row.ZPO_DLV_DT || undefined, // SAP PO납기일자 + ZPLN_ST_DT: row.ZPLN_ST_DT || undefined, // SAP 예정시작일자 + ZPLN_ED_DT: row.ZPLN_ED_DT || undefined, // SAP 예정종료일자 + LFDAT: row.LFDAT || undefined, // PR Delivery Date + ZRCV_DT: row.ZRCV_DT || undefined, // 구매접수일자 + + // 기타 + ZCON_IND: row.ZCON_IND || undefined, // 시리즈구분 (SS 등) + + vatType: row.taxType || '', // MWSKZ - 매출부가가치세코드 (V1, V2 등) steelSpec: row.steelType || undefined, - prManager: 'AUTO-MANAGER', // mock 데이터용 기본값 + prManager: '', // SAP에서 제공되지 않음 - FE에서 '-' 처리 + remark: row.remark || undefined, })); return vendorPOItems; diff --git a/lib/po/vendor-table/types.ts b/lib/po/vendor-table/types.ts index f8bc3ea2..e318ff39 100644 --- a/lib/po/vendor-table/types.ts +++ b/lib/po/vendor-table/types.ts @@ -76,8 +76,9 @@ export interface VendorPO { export interface VendorPOItem { contractNo: string // PO/계약번호 - itemNo: string // 품번 - prNo: string // P/R번호 + itemNo: string // 품번 (EBELP) + prNo: string // P/R번호 (BANFN) + prItemNo: string // PR 품번 (BNFPO) materialGroup: string // 자재그룹(명) priceStandard: string // 단가기준 materialNo: string // 자재번호 @@ -89,16 +90,54 @@ export interface VendorPOItem { material?: string // 재질 specification: string // 규격 quantity: number // 수량 - quantityUnit: string // 수량단위 + quantityUnit: string // 수량단위 (기존) + ZPO_UNIT?: string // SAP 구매오더수량단위 weight?: number // 중량 weightUnit?: string // 중량단위 totalWeight?: number // 총중량 - unitPrice: number // 단가기준 (단가) + unitPrice: number // 단가기준 (단가 - NETPR) + + // 가격 관련 추가 필드 + PEINH?: number // 가격단위값 (예: 1, 10, 100) + BPRME?: string // 구매단가단위 (EA, KG 등) + ZNETPR?: number // 발주단가 + ZREF_NETPR?: number // 참조단가 + priceUnit: string // 단가단위 priceUnitValue: string // 가격단위값 - contractAmount: number // PO계약금액 - adjustmentAmount?: number // 조정금액 - deliveryDate: string // 납기일자 + contractAmount: number // PO계약금액 (기존 필드 - 호환성 유지) + adjustmentAmount?: number // 조정금액 (기존 필드 - 호환성 유지) + + // SAP ECC 금액 필드 (금액 관계: NETWR = BRTWR + ZPDT_EXDS_AMT) + NETWR?: number // 오더정가 (최종 정가) + BRTWR?: number // 오더총액 (기본 총액) + ZPDT_EXDS_AMT?: number // 할인/할증금액 (조정금액: 할인은 음수, 할증은 양수) + + // 위치 정보 + WERKS?: string // 플랜트코드 + LGORT?: string // 저장위치 + + // RFQ 추적 + ANFNR?: string // RFQ번호 + ANFPS?: string // RFQ품목번호 + + // 자재 추적 + ZPO_LOT_NO?: string // Steel Material Marking No + + // 볼륨 정보 + VOLUM?: number // 볼륨 + VOLEH?: string // 볼륨단위 + + deliveryDate: string // 납기일자 (기존) + ZPO_DLV_DT?: string // SAP PO납기일자 + ZPLN_ST_DT?: string // SAP 예정시작일자 + ZPLN_ED_DT?: string // SAP 예정종료일자 + LFDAT?: string // PR Delivery Date + ZRCV_DT?: string // 구매접수일자 + + // 기타 + ZCON_IND?: string // 시리즈구분 (SS 등) + vatType: string // VAT구분 steelSpec?: string // 철의장 SPEC prManager: string // P/R 담당자 diff --git a/lib/po/vendor-table/vendor-po-actions.tsx b/lib/po/vendor-table/vendor-po-actions.tsx index 329d91fd..08fe3f88 100644 --- a/lib/po/vendor-table/vendor-po-actions.tsx +++ b/lib/po/vendor-table/vendor-po-actions.tsx @@ -160,13 +160,6 @@ export function VendorPOActions({ row, setRowAction }: VendorPOActionsProps) { <DropdownMenuContent align="end"> <DropdownMenuLabel>액션</DropdownMenuLabel> <DropdownMenuItem - onClick={() => setRowAction({ row, type: "view-items" })} - > - <FileTextIcon className="mr-2 h-4 w-4" /> - 상세품목 보기 - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem onClick={handlePcrCreate} disabled={isLoading || !canCreatePcr} > diff --git a/lib/po/vendor-table/vendor-po-columns.tsx b/lib/po/vendor-table/vendor-po-columns.tsx index c954b872..de44adce 100644 --- a/lib/po/vendor-table/vendor-po-columns.tsx +++ b/lib/po/vendor-table/vendor-po-columns.tsx @@ -9,6 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { VendorPO, VendorPOActionType } from "./types" import { VendorPOActions } from "./vendor-po-actions" +import { formatNumber } from "@/lib/utils" interface GetVendorColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<{ row: { original: VendorPO }; type: VendorPOActionType } | null>> @@ -193,7 +194,13 @@ export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect ), cell: ({ row }) => { const amount = row.getValue("totalAmount") as string | number - return <div className="text-sm text-right font-mono">{amount || '-'}</div> + const currency = row.getValue("currency") as string + + // 통화별 소수점 자리수 결정 (KRW, JPY: 0자리, 나머지: 2자리) + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + const formattedAmount = formatNumber(amount, decimals) + + return <div className="text-sm text-right font-mono">{formattedAmount}</div> }, size: 120, }, diff --git a/lib/po/vendor-table/vendor-po-items-dialog.tsx b/lib/po/vendor-table/vendor-po-items-dialog.tsx index 647950c4..d88d88d1 100644 --- a/lib/po/vendor-table/vendor-po-items-dialog.tsx +++ b/lib/po/vendor-table/vendor-po-items-dialog.tsx @@ -16,10 +16,10 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" import { VendorPO, VendorPOItem } from "./types" import { getVendorPOItemsByContractNo } from "./service" +import { formatNumber } from "@/lib/utils" interface VendorPOItemsDialogProps { open: boolean @@ -99,8 +99,9 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableHead className="min-w-[120px] whitespace-nowrap">PO/계약번호</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">품번</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">P/R번호</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">PR 품번</TableHead> <TableHead className="min-w-[120px] whitespace-nowrap">자재그룹(명)</TableHead> - <TableHead className="min-w-[100px] whitespace-nowrap">단가기준</TableHead> + {/* <TableHead className="min-w-[100px] whitespace-nowrap">단가기준</TableHead> */} <TableHead className="min-w-[100px] whitespace-nowrap">자재번호</TableHead> <TableHead className="min-w-[200px] whitespace-nowrap">품목/자재내역</TableHead> {/* <TableHead className="min-w-[200px] whitespace-nowrap">자재내역사양</TableHead> @@ -114,15 +115,31 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableHead className="min-w-[80px] text-right whitespace-nowrap">중량</TableHead> <TableHead className="min-w-[80px] whitespace-nowrap">중량단위</TableHead> <TableHead className="min-w-[100px] text-right whitespace-nowrap">총중량</TableHead> - <TableHead className="min-w-[100px] text-right whitespace-nowrap">단가기준</TableHead> - <TableHead className="min-w-[80px] whitespace-nowrap">단가단위</TableHead> - <TableHead className="min-w-[100px] whitespace-nowrap">가격단위값</TableHead> - <TableHead className="min-w-[120px] text-right whitespace-nowrap">PO계약금액</TableHead> - <TableHead className="min-w-[100px] text-right whitespace-nowrap">조정금액</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">단가(NETPR)</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">단가단위(BPRME)</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">가격단위값(PEINH)</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">참조단가</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">발주단가</TableHead> + <TableHead className="min-w-[120px] text-right whitespace-nowrap">기본총액(BRTWR)</TableHead> + <TableHead className="min-w-[120px] text-right whitespace-nowrap">조정금액(ZPDT_EXDS_AMT)</TableHead> + <TableHead className="min-w-[120px] text-right whitespace-nowrap">최종정가(NETWR)</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">예정시작일자</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">납기일자</TableHead> - <TableHead className="min-w-[80px] whitespace-nowrap">VAT구분</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">예정종료일자</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">PR납기일</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">구매접수일</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">세금코드</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">플랜트</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">저장위치</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">RFQ번호</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">RFQ품번</TableHead> + <TableHead className="min-w-[150px] whitespace-nowrap">LOT No.</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">볼륨</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">볼륨단위</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">시리즈구분</TableHead> + <TableHead className="min-w-[150px] whitespace-nowrap">비고</TableHead> {/* <TableHead className="min-w-[120px] whitespace-nowrap">철의장 SPEC</TableHead> */} - <TableHead className="min-w-[100px] whitespace-nowrap">P/R 담당자</TableHead> + {/* <TableHead className="min-w-[100px] whitespace-nowrap">P/R 담당자</TableHead> */} </TableRow> </TableHeader> <TableBody> @@ -131,8 +148,9 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableCell className="font-medium">{item.contractNo || '-'}</TableCell> <TableCell>{item.itemNo || '-'}</TableCell> <TableCell>{item.prNo || '-'}</TableCell> + <TableCell>{item.prItemNo || '-'}</TableCell> <TableCell>{item.materialGroup || '-'}</TableCell> - <TableCell>{item.priceStandard || '-'}</TableCell> + {/* <TableCell>{item.priceStandard || '-'}</TableCell> */} <TableCell className="font-mono text-sm">{item.materialNo || '-'}</TableCell> <TableCell className="max-w-[200px]"> <div className="truncate" title={item.itemDescription || ''}> @@ -151,7 +169,7 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableCell>{item.material || '-'}</TableCell> <TableCell>{item.specification || '-'}</TableCell> */} <TableCell className="text-right font-mono"> - {item.quantity?.toLocaleString() || '-'} + {item.quantity !== undefined && item.quantity !== null ? item.quantity.toLocaleString() : '-'} </TableCell> <TableCell>{item.quantityUnit || '-'}</TableCell> <TableCell className="text-right font-mono"> @@ -162,20 +180,76 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia {item.totalWeight ? item.totalWeight.toLocaleString() : '-'} </TableCell> <TableCell className="text-right font-mono"> - {item.unitPrice?.toLocaleString() || '-'} + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return formatNumber(item.unitPrice, decimals) + })()} </TableCell> - <TableCell>{item.priceUnit || '-'}</TableCell> - <TableCell>{item.priceUnitValue || '-'}</TableCell> - <TableCell className="text-right font-mono font-semibold"> - {item.contractAmount?.toLocaleString() || '-'} + <TableCell>{item.BPRME || '-'}</TableCell> + <TableCell className="text-right">{item.PEINH ?? '-'}</TableCell> + <TableCell className="text-right font-mono"> + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return item.ZREF_NETPR ? formatNumber(item.ZREF_NETPR, decimals) : '-' + })()} + </TableCell> + <TableCell className="text-right font-mono"> + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return item.ZNETPR ? formatNumber(item.ZNETPR, decimals) : '-' + })()} </TableCell> <TableCell className="text-right font-mono"> - {item.adjustmentAmount ? item.adjustmentAmount.toLocaleString() : '-'} + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return item.BRTWR ? formatNumber(item.BRTWR, decimals) : '-' + })()} + </TableCell> + <TableCell className="text-right font-mono"> + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return item.ZPDT_EXDS_AMT ? formatNumber(item.ZPDT_EXDS_AMT, decimals) : '-' + })()} + </TableCell> + <TableCell className="text-right font-mono font-semibold"> + {(() => { + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + return item.NETWR ? formatNumber(item.NETWR, decimals) : '-' + })()} </TableCell> - <TableCell>{item.deliveryDate || '-'}</TableCell> + <TableCell>{item.ZPLN_ST_DT || '-'}</TableCell> + <TableCell>{item.ZPO_DLV_DT || item.deliveryDate || '-'}</TableCell> + <TableCell>{item.ZPLN_ED_DT || '-'}</TableCell> + <TableCell>{item.LFDAT || '-'}</TableCell> + <TableCell>{item.ZRCV_DT || '-'}</TableCell> <TableCell>{item.vatType || '-'}</TableCell> + <TableCell>{item.WERKS || '-'}</TableCell> + <TableCell>{item.LGORT || '-'}</TableCell> + <TableCell>{item.ANFNR || '-'}</TableCell> + <TableCell>{item.ANFPS || '-'}</TableCell> + <TableCell className="max-w-[150px]"> + <div className="truncate" title={item.ZPO_LOT_NO || ''}> + {item.ZPO_LOT_NO || '-'} + </div> + </TableCell> + <TableCell className="text-right font-mono"> + {item.VOLUM ? item.VOLUM.toLocaleString() : '-'} + </TableCell> + <TableCell>{item.VOLEH || '-'}</TableCell> + <TableCell>{item.ZCON_IND || '-'}</TableCell> + <TableCell className="max-w-[150px]"> + <div className="truncate" title={item.remark || ''}> + {item.remark || '-'} + </div> + </TableCell> {/* <TableCell>{item.steelSpec || '-'}</TableCell> */} - <TableCell>{item.prManager || '-'}</TableCell> + {/* <TableCell>{item.prManager || '-'}</TableCell> */} </TableRow> ))} </TableBody> @@ -190,7 +264,16 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia 총 {items.length}개 품목 </div> <div className="text-sm font-medium"> - 총 계약금액: {items.reduce((sum, item) => sum + item.contractAmount, 0).toLocaleString()} 원 + {(() => { + // NETWR(최종정가) 합계 계산 + const totalNETWR = items.reduce((sum, item) => sum + (item.NETWR || item.contractAmount || 0), 0) + // 통화별 소수점 자리수 결정 (KRW, JPY: 0자리, 나머지: 2자리) + const currency = po?.currency || '' + const decimals = (currency === 'KRW' || currency === 'JPY') ? 0 : 2 + const formattedAmount = formatNumber(totalNETWR, decimals) + + return `총 계약금액(NETWR): ${currency} ${formattedAmount}` + })()} </div> </div> )} diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 52d67280..7ebec795 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -2645,7 +2645,7 @@ export async function getRfqFullInfo(rfqId: number): Promise<RfqFullInfo> { picCode: rfq.picCode, picName: rfq.picName, picUserName: picUser?.name ?? null, - picTeam: picUser?.department ?? null, // users 테이블에 department 필드가 있다고 가정 + picTeam: picUser?.deptName ?? null, // users 테이블의 deptName 필드 사용 // 설계 담당자 engPicName: rfq.EngPicName, 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, diff --git a/lib/utils.ts b/lib/utils.ts index 98142389..8c6d2396 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -291,7 +291,7 @@ export function compareItemNumber(a?: string, b?: string) { } /** - * 숫자를 3자리마다 콤마(,)로 구분하여 포맷팅합니다. + * 숫자를 3자리마다 콤마(,)로 구분하여 포맷팅합니다. (금액 등 자리수 구분 필요한 경우, KRW 및 JPY는 소수점 0자리, 나머지는 2자리) * * @param value 포맷팅할 숫자 (number, string, null, undefined 허용) * @param decimals 소수점 자릿수 (선택, 지정하지 않으면 원본 소수점 유지) |
