diff options
Diffstat (limited to 'lib/bidding/actions.ts')
| -rw-r--r-- | lib/bidding/actions.ts | 1065 |
1 files changed, 590 insertions, 475 deletions
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index df9d0dad..b5736707 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -1,475 +1,590 @@ -"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("낙찰된 업체가 없습니다.")
- }
-
- // 일반/매각 입찰의 경우 비율 합계 100% 검증
- const contractType = biddingData.contractType
- if (contractType === 'general' || contractType === 'sale') {
- const totalRatio = winnerCompaniesData.reduce((sum, company) =>
- sum + (Number(company.awardRatio) || 0), 0)
-
- if (totalRatio !== 100) {
- throw new Error(`일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%`)
- }
- }
-
- 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,
- })
- .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 ? totalContractAmount.toString() as any : 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 bidUnitPrice = Number(bid.bidUnitPrice) || 0
-
- const finalQuantity = originalQuantity * awardRatio
- const finalAmount = finalQuantity * bidUnitPrice
-
- await db.insert(generalContractItems).values({
- contractId: contractId,
- itemCode: bid.itemNumber || '',
- itemInfo: bid.itemInfo || '',
- specification: bid.materialDescription || '',
- quantity: finalQuantity || null,
- quantityUnit: bid.quantityUnit || '',
- totalWeight: null, // 중량 정보 제외
- weightUnit: '', // 중량 단위 제외
- contractDeliveryDate: bid.proposedDeliveryDate || null,
- contractUnitPrice: bid.bidUnitPrice || null,
- contractAmount: finalAmount ? finalAmount.toString() as any : null,
- contractCurrency: bid.currency || biddingData.currency || 'KRW',
- })
- }
- 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("낙찰된 업체가 없습니다.")
- }
-
- // 일반/매각 입찰의 경우 비율 합계 100% 검증
- const contractType = bidding.contractType
- if (contractType === 'general' || contractType === 'sale') {
- const totalRatio = winnerCompaniesRaw.reduce((sum, company) =>
- sum + (Number(company.awardRatio) || 0), 0)
-
- if (totalRatio !== 100) {
- throw new Error(`일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%`)
- }
- }
-
- // 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
- finalQuantity: 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,
- })
- .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 bidUnitPrice = Number(bid.bidUnitPrice) || 0
-
- const finalQuantity = originalQuantity * awardRatio
- const finalAmount = finalQuantity * bidUnitPrice
-
- poItems.push({
- ...bid,
- finalQuantity,
- 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. 입찰 정보 조회 (contractType 포함)
- const biddingInfo = await db.select({
- contractType: biddings.contractType,
- })
- .from(biddings)
- .where(eq(biddings.id, biddingId))
- .limit(1)
-
- if (!biddingInfo || biddingInfo.length === 0) {
- return { success: false, error: '입찰 정보를 찾을 수 없습니다.' }
- }
-
- // 2. 낙찰된 업체들 조회
- const winnerCompanies = await db.select({
- id: biddingCompanies.id,
- companyId: biddingCompanies.companyId,
- finalQuoteAmount: biddingCompanies.finalQuoteAmount,
- awardRatio: biddingCompanies.awardRatio,
- vendorName: vendors.vendorName,
- vendorCode: vendors.vendorCode,
- contractType: biddingInfo[0].contractType,
- })
- .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: '낙찰된 업체가 없습니다.' }
- }
-
- // 일반/매각 입찰의 경우 비율 합계 100% 검증
- const contractType = biddingInfo[0].contractType
- if (contractType === 'general' || contractType === 'sale') {
- const totalRatio = winnerCompanies.reduce((sum, company) =>
- sum + (Number(company.awardRatio) || 0), 0)
-
- if (totalRatio !== 100) {
- return { success: false, error: `일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%` }
- }
- }
-
- // 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,
- })
- .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 bidUnitPrice = Number(bid.bidUnitPrice) || 0
-
- // 발주비율에 따른 최종 수량 계산
- const finalQuantity = originalQuantity * awardRatio
- const finalWeight = 0 // 중량 제외
- const finalAmount = finalQuantity * 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: '낙찰 업체 상세 정보 조회에 실패했습니다.'
- }
- }
-}
+"use server" + +import db from "@/db/db" +import { eq, and } from "drizzle-orm" +import { + biddings, + biddingCompanies, + prItemsForBidding, + companyPrItemBids, + vendors, + generalContracts, + generalContractItems, + biddingConditions, + biddingDocuments, + users +} 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" +import { saveFile } from "@/lib/file-stroage" + +// 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("낙찰된 업체가 없습니다.") + } + + // 일반/매각 입찰의 경우 비율 합계 100% 검증 + const contractType = biddingData.contractType + if (contractType === 'general' || contractType === 'sale') { + const totalRatio = winnerCompaniesData.reduce((sum, company) => + sum + (Number(company.awardRatio) || 0), 0) + + if (totalRatio !== 100) { + throw new Error(`일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%`) + } + } + + 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, + }) + .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 ? totalContractAmount.toString() as any : 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 bidUnitPrice = Number(bid.bidUnitPrice) || 0 + + const finalQuantity = originalQuantity * awardRatio + const finalAmount = finalQuantity * bidUnitPrice + + await db.insert(generalContractItems).values({ + contractId: contractId, + itemCode: bid.itemNumber || '', + itemInfo: bid.itemInfo || '', + specification: bid.materialDescription || '', + quantity: finalQuantity || null, + quantityUnit: bid.quantityUnit || '', + totalWeight: null, // 중량 정보 제외 + weightUnit: '', // 중량 단위 제외 + contractDeliveryDate: bid.proposedDeliveryDate || null, + contractUnitPrice: bid.bidUnitPrice || null, + contractAmount: finalAmount ? finalAmount.toString() as any : null, + contractCurrency: bid.currency || biddingData.currency || 'KRW', + }) + } + 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("낙찰된 업체가 없습니다.") + } + + // 일반/매각 입찰의 경우 비율 합계 100% 검증 + const contractType = bidding.contractType + if (contractType === 'general' || contractType === 'sale') { + const totalRatio = winnerCompaniesRaw.reduce((sum, company) => + sum + (Number(company.awardRatio) || 0), 0) + + if (totalRatio !== 100) { + throw new Error(`일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%`) + } + } + + // 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 + finalQuantity: 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, + }) + .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 bidUnitPrice = Number(bid.bidUnitPrice) || 0 + + const finalQuantity = originalQuantity * awardRatio + const finalAmount = finalQuantity * bidUnitPrice + + poItems.push({ + ...bid, + finalQuantity, + 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. 입찰 정보 조회 (contractType 포함) + const biddingInfo = await db.select({ + contractType: biddings.contractType, + }) + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) + + if (!biddingInfo || biddingInfo.length === 0) { + return { success: false, error: '입찰 정보를 찾을 수 없습니다.' } + } + + // 2. 낙찰된 업체들 조회 + const winnerCompanies = await db.select({ + id: biddingCompanies.id, + companyId: biddingCompanies.companyId, + finalQuoteAmount: biddingCompanies.finalQuoteAmount, + awardRatio: biddingCompanies.awardRatio, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + contractType: biddingInfo[0].contractType, + }) + .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: '낙찰된 업체가 없습니다.' } + } + + // 일반/매각 입찰의 경우 비율 합계 100% 검증 + const contractType = biddingInfo[0].contractType + if (contractType === 'general' || contractType === 'sale') { + const totalRatio = winnerCompanies.reduce((sum, company) => + sum + (Number(company.awardRatio) || 0), 0) + + if (totalRatio !== 100) { + return { success: false, error: `일반/매각 입찰의 경우 비율 합계가 100%여야 합니다. 현재 합계: ${totalRatio}%` } + } + } + + // 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, + }) + .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 bidUnitPrice = Number(bid.bidUnitPrice) || 0 + + // 발주비율에 따른 최종 수량 계산 + const finalQuantity = originalQuantity * awardRatio + const finalWeight = 0 // 중량 제외 + const finalAmount = finalQuantity * 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: '낙찰 업체 상세 정보 조회에 실패했습니다.' + } + } +} + +// 폐찰하기 액션 +export async function bidClosureAction( + biddingId: number, + formData: { + description: string + files: File[] + }, + userId: string +) { + try { + const userName = await getUserNameById(userId) + + return await db.transaction(async (tx) => { + // 1. 입찰 정보 확인 + const [existingBidding] = await tx + .select() + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) + + if (!existingBidding) { + return { + success: false, + error: '입찰 정보를 찾을 수 없습니다.' + } + } + + // 2. 유찰 상태인지 확인 + if (existingBidding.status !== 'bidding_disposal') { + return { + success: false, + error: '유찰 상태인 입찰만 폐찰할 수 있습니다.' + } + } + + // 3. 입찰 상태를 폐찰로 변경하고 설명 저장 + await tx + .update(biddings) + .set({ + status: 'bid_closure', + description: formData.description, + updatedAt: new Date(), + updatedBy: userName, + }) + .where(eq(biddings.id, biddingId)) + + // 4. 첨부파일들 저장 (evaluation_doc로 저장) + if (formData.files && formData.files.length > 0) { + for (const file of formData.files) { + try { + const saveResult = await saveFile({ + file, + directory: `biddings/${biddingId}/closure-documents`, + originalName: file.name, + userId + }) + + if (saveResult.success) { + await tx.insert(biddingDocuments).values({ + biddingId, + documentType: 'evaluation_doc', + fileName: saveResult.fileName!, + originalFileName: saveResult.originalName!, + fileSize: saveResult.fileSize!, + mimeType: file.type, + filePath: saveResult.publicPath!, + title: `폐찰 문서 - ${file.name}`, + description: formData.description, + isPublic: false, + isRequired: false, + uploadedBy: userName, + }) + } else { + console.error(`Failed to save closure file: ${file.name}`, saveResult.error) + } + } catch (error) { + console.error(`Error saving closure file: ${file.name}`, error) + } + } + } + + return { + success: true, + message: '폐찰이 완료되었습니다.' + } + }) + + } catch (error) { + console.error('폐찰 실패:', error) + return { + success: false, + error: error instanceof Error ? error.message : '폐찰 중 오류가 발생했습니다.' + } + } +} + +// 사용자 이름 조회 헬퍼 함수 +async function getUserNameById(userId: string): Promise<string> { + try { + const user = await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, parseInt(userId))) + .limit(1) + + return user[0]?.name || userId + } catch (error) { + console.error('Failed to get user name:', error) + return userId + } +} |
