"use server" import db from "@/db/db" import { eq, and } from "drizzle-orm" import { biddings, biddingCompanies, prItemsForBidding, companyPrItemBids, vendors, generalContracts, generalContractItems, biddingConditions } from "@/db/schema" import { createPurchaseOrder } from "@/lib/soap/ecc/send/create-po" import { getCurrentSAPDate } from "@/lib/soap/utils" import { generateContractNumber } from "@/lib/general-contracts/service" // TO Contract export async function transmitToContract(biddingId: number, userId: number) { try { // 1. 입찰 정보 조회 (단순 쿼리) const bidding = await db.select() .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) if (!bidding || bidding.length === 0) { throw new Error("입찰 정보를 찾을 수 없습니다.") } const biddingData = bidding[0] // 2. 입찰 조건 정보 조회 const biddingConditionData = await db.select() .from(biddingConditions) .where(eq(biddingConditions.biddingId, biddingId)) .limit(1) const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null // 3. 낙찰된 업체들 조회 (biddingCompanies.id 포함) const winnerCompaniesData = await db.select({ id: biddingCompanies.id, companyId: biddingCompanies.companyId, finalQuoteAmount: biddingCompanies.finalQuoteAmount, awardRatio: biddingCompanies.awardRatio, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) // 상태 검증 if (biddingData.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } // 낙찰된 업체 검증 if (winnerCompaniesData.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } for (const winnerCompany of winnerCompaniesData) { // winnerCompany에서 직접 정보 사용 const awardRatio = (Number(winnerCompany.awardRatio) || 100) / 100 const biddingCompanyId = winnerCompany.id // 현재 winnerCompany의 입찰 데이터 조회 const companyBids = await db.select({ prItemId: companyPrItemBids.prItemId, proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, bidUnitPrice: companyPrItemBids.bidUnitPrice, bidAmount: companyPrItemBids.bidAmount, currency: companyPrItemBids.currency, // PR 아이템 정보도 함께 조회 itemNumber: prItemsForBidding.itemNumber, itemInfo: prItemsForBidding.itemInfo, materialDescription: prItemsForBidding.materialDescription, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, totalWeight: prItemsForBidding.totalWeight, weightUnit: prItemsForBidding.weightUnit, }) .from(companyPrItemBids) .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id)) .where(eq(companyPrItemBids.biddingCompanyId, biddingCompanyId)) // 발주비율에 따른 최종 계약금액 계산 let totalContractAmount = 0 if (companyBids.length > 0) { for (const bid of companyBids) { const originalQuantity = Number(bid.quantity) || 0 const bidUnitPrice = Number(bid.bidUnitPrice) || 0 const finalQuantity = originalQuantity * awardRatio const finalAmount = finalQuantity * bidUnitPrice totalContractAmount += finalAmount } } // 계약 번호 자동 생성 (실제 규칙에 맞게) const contractNumber = await generateContractNumber(userId.toString(), biddingData.contractType) console.log('Generated contractNumber:', contractNumber) // general-contract 생성 (발주비율 계산된 최종 금액 사용) const contractResult = await db.insert(generalContracts).values({ contractNumber, revision: 0, contractSourceType: 'bid', // 입찰에서 생성됨 status: 'Draft', category: biddingData.contractType || 'general', name: biddingData.title, vendorId: winnerCompany.companyId, linkedBidNumber: biddingData.biddingNumber, contractAmount: totalContractAmount || null, // 발주비율 계산된 최종 금액 사용 startDate: biddingData.contractStartDate || null, endDate: biddingData.contractEndDate || null, currency: biddingData.currency || 'KRW', // 계약 조건 정보 추가 paymentTerm: biddingCondition?.paymentTerms || null, taxType: biddingCondition?.taxConditions || 'V0', deliveryTerm: biddingCondition?.incoterms || 'FOB', shippingLocation: biddingCondition?.shippingPort || null, dischargeLocation: biddingCondition?.destinationPort || null, registeredById: userId, lastUpdatedById: userId, }).returning({ id: generalContracts.id }) console.log('contractResult', contractResult) const contractId = contractResult[0].id // 현재 winnerCompany의 품목정보 생성 (발주비율 적용) if (companyBids.length > 0) { console.log(`Creating ${companyBids.length} contract items for winner company ${winnerCompany.companyId} with award ratio ${awardRatio}`) for (const bid of companyBids) { // 발주비율에 따른 최종 수량/중량 계산 const originalQuantity = Number(bid.quantity) || 0 const originalWeight = Number(bid.totalWeight) || 0 const bidUnitPrice = Number(bid.bidUnitPrice) || 0 const finalQuantity = originalQuantity * awardRatio const finalWeight = originalWeight * awardRatio const finalAmount = finalQuantity > 0 ? finalQuantity * bidUnitPrice : finalWeight * bidUnitPrice await db.insert(generalContractItems).values({ contractId: contractId, itemCode: bid.itemNumber || '', itemInfo: bid.itemInfo || '', specification: bid.materialDescription || '', quantity: finalQuantity || null, quantityUnit: bid.quantityUnit || '', totalWeight: finalWeight || null, weightUnit: bid.weightUnit || '', contractDeliveryDate: bid.proposedDeliveryDate || null, contractUnitPrice: bid.bidUnitPrice || null, contractAmount: finalAmount || null, contractCurrency: bid.currency || biddingData.currency || 'KRW', } as any) } console.log(`Created ${companyBids.length} contract items for winner company ${winnerCompany.companyId}`) } else { console.log(`No bid data found for winner company ${winnerCompany.companyId}`) } } return { success: true, message: `${winnerCompaniesData.length}개의 계약서가 생성되었습니다.` } } catch (error) { console.error('TO Contract 실패:', error) throw new Error(error instanceof Error ? error.message : '계약서 생성에 실패했습니다.') } } // TO PO export async function transmitToPO(biddingId: number) { try { // 1. 입찰 정보 조회 const biddingData = await db.select() .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) if (!biddingData || biddingData.length === 0) { throw new Error("입찰 정보를 찾을 수 없습니다.") } const bidding = biddingData[0] if (bidding.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } // 2. 입찰 조건 정보 조회 const biddingConditionData = await db.select() .from(biddingConditions) .where(eq(biddingConditions.biddingId, biddingId)) .limit(1) const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null // 3. 낙찰된 업체들 조회 (발주비율 포함) const winnerCompaniesRaw = await db.select({ id: biddingCompanies.id, companyId: biddingCompanies.companyId, finalQuoteAmount: biddingCompanies.finalQuoteAmount, awardRatio: biddingCompanies.awardRatio, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) if (winnerCompaniesRaw.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } // 4. 낙찰된 업체들의 입찰 데이터 조회 (발주비율 적용) type POItem = { prItemId: number proposedDeliveryDate: string | null bidUnitPrice: string | null bidAmount: string | null currency: string | null itemNumber: string | null itemInfo: string | null materialDescription: string | null quantity: string | null quantityUnit: string | null totalWeight: string | null weightUnit: string | null finalQuantity: number finalWeight: number finalAmount: number awardRatio: number vendorCode: string | null vendorName: string | null companyId: number } const poItems: POItem[] = [] for (const winner of winnerCompaniesRaw) { const awardRatio = (Number(winner.awardRatio) || 100) / 100 const companyBids = await db.select({ prItemId: companyPrItemBids.prItemId, proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, bidUnitPrice: companyPrItemBids.bidUnitPrice, bidAmount: companyPrItemBids.bidAmount, currency: companyPrItemBids.currency, // PR 아이템 정보 itemNumber: prItemsForBidding.itemNumber, itemInfo: prItemsForBidding.itemInfo, materialDescription: prItemsForBidding.materialDescription, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, totalWeight: prItemsForBidding.totalWeight, weightUnit: prItemsForBidding.weightUnit, }) .from(companyPrItemBids) .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id)) .where(eq(companyPrItemBids.biddingCompanyId, winner.id)) // 발주비율 적용하여 PO 아이템 생성 for (const bid of companyBids) { const originalQuantity = Number(bid.quantity) || 0 const originalWeight = Number(bid.totalWeight) || 0 const bidUnitPrice = Number(bid.bidUnitPrice) || 0 const finalQuantity = originalQuantity * awardRatio const finalWeight = originalWeight * awardRatio const finalAmount = finalQuantity > 0 ? finalQuantity * bidUnitPrice : finalWeight * bidUnitPrice poItems.push({ ...bid, finalQuantity, finalWeight, finalAmount, awardRatio, vendorCode: winner.vendorCode, vendorName: winner.vendorName, companyId: winner.companyId, } as POItem) } } // 5. PO 데이터 구성 (bidding condition 정보와 발주비율 적용된 데이터 사용) const poData = { T_Bidding_HEADER: winnerCompaniesRaw.map((company) => ({ ANFNR: bidding.biddingNumber, LIFNR: company.vendorCode || `VENDOR${company.companyId}`, ZPROC_IND: 'A', // 구매 처리 상태 ANGNR: bidding.biddingNumber, WAERS: bidding.currency || 'KRW', ZTERM: biddingCondition?.paymentTerms || '0001', // 지급조건 INCO1: biddingCondition?.incoterms || 'FOB', // Incoterms INCO2: biddingCondition?.destinationPort || biddingCondition?.shippingPort || 'Seoul, Korea', MWSKZ: biddingCondition?.taxConditions || 'V0', // 세금 코드 LANDS: 'KR', ZRCV_DT: getCurrentSAPDate(), ZATTEN_IND: 'Y', IHRAN: getCurrentSAPDate(), TEXT: `PO from Bidding: ${bidding.title}`, })), T_Bidding_ITEM: poItems.map((item, index) => ({ ANFNR: bidding.biddingNumber, ANFPS: (index + 1).toString().padStart(5, '0'), LIFNR: item.vendorCode || `VENDOR${item.companyId}`, NETPR: item.bidUnitPrice?.toString() || '0', PEINH: '1', BPRME: item.quantityUnit || 'EA', NETWR: item.finalAmount?.toString() || '0', BRTWR: (Number(item.finalAmount || 0) * 1.1).toString(), // 10% 부가세 가정 LFDAT: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate).toISOString().split('T')[0] : getCurrentSAPDate(), })), T_PR_RETURN: [{ ANFNR: bidding.biddingNumber, ANFPS: '00001', EBELN: `PR${bidding.biddingNumber}`, EBELP: '00001', MSGTY: 'S', MSGTXT: 'Success' }] } // 3. SAP으로 PO 전송 console.log('SAP으로 PO 전송할 poData', poData) const result = await createPurchaseOrder(poData) if (!result.success) { throw new Error(result.message) } return { success: true, message: result.message } } catch (error) { console.error('TO PO 실패:', error) throw new Error(error instanceof Error ? error.message : 'PO 전송에 실패했습니다.') } } // 낙찰된 업체들의 상세 정보 조회 (발주비율에 따른 계산 포함) export async function getWinnerDetails(biddingId: number) { try { // 1. 낙찰된 업체들 조회 const winnerCompanies = await db.select({ id: biddingCompanies.id, companyId: biddingCompanies.companyId, finalQuoteAmount: biddingCompanies.finalQuoteAmount, awardRatio: biddingCompanies.awardRatio, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) if (winnerCompanies.length === 0) { return { success: false, error: '낙찰된 업체가 없습니다.' } } // 2. 각 낙찰 업체의 입찰 품목 정보 조회 const winnerDetails = [] for (const winner of winnerCompanies) { // 업체의 입찰 품목 정보 조회 const companyBids = await db.select({ prItemId: companyPrItemBids.prItemId, proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, bidUnitPrice: companyPrItemBids.bidUnitPrice, bidAmount: companyPrItemBids.bidAmount, currency: companyPrItemBids.currency, // PR 아이템 정보 itemNumber: prItemsForBidding.itemNumber, itemInfo: prItemsForBidding.itemInfo, materialDescription: prItemsForBidding.materialDescription, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, totalWeight: prItemsForBidding.totalWeight, weightUnit: prItemsForBidding.weightUnit, }) .from(companyPrItemBids) .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id)) .where(eq(companyPrItemBids.biddingCompanyId, winner.id)) // 발주비율에 따른 계산 (백분율을 실제 비율로 변환) const awardRatio = (Number(winner.awardRatio) || 100) / 100 const calculatedItems = companyBids.map(bid => { const originalQuantity = Number(bid.quantity) || 0 const originalWeight = Number(bid.totalWeight) || 0 const bidUnitPrice = Number(bid.bidUnitPrice) || 0 // 발주비율에 따른 최종 수량/중량 계산 const finalQuantity = originalQuantity * awardRatio const finalWeight = originalWeight * awardRatio // 최종 견적가 계산 (수량 또는 중량에 따른) const finalAmount = finalQuantity > 0 ? finalQuantity * bidUnitPrice : finalWeight * bidUnitPrice return { ...bid, finalQuantity, finalWeight, finalAmount, awardRatio, } }) // 업체 총 견적가 계산 const totalFinalAmount = calculatedItems.reduce((sum, item) => sum + item.finalAmount, 0) winnerDetails.push({ ...winner, items: calculatedItems, totalFinalAmount, awardRatio: Number(winner.awardRatio) || 1, }) } return { success: true, data: winnerDetails } } catch (error) { console.error('Winner details 조회 실패:', error) return { success: false, error: '낙찰 업체 상세 정보 조회에 실패했습니다.' } } }