import { pgTable, text, varchar, timestamp, integer, numeric, date, boolean, unique, uniqueIndex, pgView } from "drizzle-orm/pg-core" import { projects } from "./projects" import { vendorContacts, vendors } from "./vendors" import { eq, sql } from "drizzle-orm"; import { items } from "./items"; /** * PO 상태 << 정의서에 적어놓고 또 다르게 쓰고 있음. 신뢰가 안감 * ENG : KO : 의미 * 1. Contract Transfer : 계약번호생성 : 이 상태는 안 쓰이는 것인가? * 2. Request to Review Conditions : 조건검토요청 : 조건검토를 할 수 있는 기능의 요구사항이 없음. 무시 * 3. Confirm to Review Conditions : 조건검토완료 : 조건검토를 할 수 있는 기능의 요구사항이 없음. 무시 * 4. Contract Accept Request : 계약승인요청 : PO가 들어오면 계약승인 요청으로 처리 * 5. Complete the Contract : 계약체결(승인) : 벤더가 PO 승인하면 이 상태로 변경 * 6. Reject to Accept Contract : 계약승인거절 : 벤더가 PO 거절하면 이 상태로 변경 * 7. Contract Delete : 계약폐기 : 계약폐기를 할 수 있는 기능의 요구사항이 없음. 중공업측에서 변경하거나 무시 * 8. PCR Request : PCR요청 : PCR 요청 시 이 상태로 변경 * 9. VO Request : VO요청 : VO 요청 기능 요구사항 없음. 요구사항이 정의되지 않은 상태임. 무시. (요구사항 정의되면 나중에 VO 요청 시 이 상태로 변경) * 10. PCR Accept : PCR승인 : PCR 요청 승인 시 이 상태로 변경 * 11. PCR Reject : PCR거절 : PCR 요청 거절 시 이 상태로 변경 */ // 계약 상태 enum (영어 키를 타입 안전하게 사용, i18n으로 다국어 지원) export enum ContractStatus { CONTRACT_TRANSFER = "Contract Transfer", REQUEST_TO_REVIEW_CONDITIONS = "Request to Review Conditions", CONFIRM_TO_REVIEW_CONDITIONS = "Confirm to Review Conditions", CONTRACT_ACCEPT_REQUEST = "Contract Accept Request", COMPLETE_THE_CONTRACT = "Complete the Contract", REJECT_TO_ACCEPT_CONTRACT = "Reject to Accept Contract", CONTRACT_DELETE = "Contract Delete", PCR_REQUEST = "PCR Request", VO_REQUEST = "VO Request", PCR_ACCEPT = "PCR Accept", PCR_REJECT = "PCR Reject" } // ============ contracts (계약/PO 정보) ============ export const contracts = pgTable('contracts', { // 주 키 id: integer('id').primaryKey().generatedAlwaysAsIdentity(), // 프로젝트와 협력업체 참조 // .notNull() 제외(0912 구매 프로젝트 id 없는 계약 case 존재-최겸) projectId: integer('project_id').references(() => projects.id, { onDelete: 'cascade', }), vendorId: integer('vendor_id') .notNull() .references(() => vendors.id, { onDelete: 'cascade' }), // 계약/PO 번호(유니크) contractNo: varchar('contract_no', { length: 100 }).notNull().unique(), // EBELN contractName: varchar('contract_name', { length: 255 }).notNull(), // ZTITLE // 계약/PO 상태나 기간 status: varchar('status', { length: 50 }) .notNull() .default(ContractStatus.CONTRACT_ACCEPT_REQUEST), // default value: 'Contract Accept Request' startDate: date('start_date'), // 발주일(혹은 유효 시작일) endDate: date('end_date'), // 계약 종료일/유효 기간 등 // 거절사유 rejectionReason: varchar('rejection_reason', { length: 1000 }), // --- SAP ECC 인터페이스 매핑 필드들 --- // 기본 PO 정보 paymentTerms: text('payment_terms'), // 지급 조건 (ZTERM - 지급조건코드) deliveryTerms: text('delivery_terms'), // 납품 조건 (INCO1 - 인도조건코드) deliveryDate: date('delivery_date'), // 납품 기한 (ZPO_DLV_DT - PO납기일자, 개별 품목별) shippmentPlace: varchar('shippment_place', { length: 255 }), // 선적지 (ZSHIPMT_PLC_CD - 선적지코드) deliveryLocation: varchar('delivery_location', { length: 255 }), // 하역지 (ZUNLD_PLC_CD - 하역지코드) // SAP ECC 추가 필드들 poVersion: integer('po_version'), // PO 버전 (ZPO_VER - 발주버전) purchaseDocType: varchar('purchase_doc_type', { length: 10 }), // 구매문서유형 (BSART) purchaseOrg: varchar('purchase_org', { length: 10 }), // 구매조직 (EKORG - 구매조직코드) 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) defectGuaranteeCode: varchar('defect_guarantee_code', { length: 2 }), // 하자보증코드 (ZDFCT_GRNT_CD) guaranteePeriodCode: varchar('guarantee_period_code', { length: 2 }), // 보증기간코드 (ZGRNT_PRD_CD) advancePaymentYn: varchar('advance_payment_yn', { length: 1 }), // 선급금여부 (ZPAMT_YN) // 금액 관련 (KRW 변환) budgetAmount: numeric('budget_amount', { precision: 17, scale: 2 }), // 예산금액 (ZBGT_AMT) budgetCurrency: varchar('budget_currency', { length: 5 }), // 예산통화 (ZBGT_CURR) totalAmountKrw: numeric('total_amount_krw', { precision: 17, scale: 2 }), // 발주금액KRW (ZPO_AMT_KRW) // 전자계약/승인 관련 electronicContractYn: varchar('electronic_contract_yn', { length: 1 }), // 전자계약필요여부 (ZELC_CNRT_ND_YN) electronicApprovalDate: date('electronic_approval_date'), // 전자승인일자 (ZELC_AGR_DT) electronicApprovalTime: varchar('electronic_approval_time', { length: 6 }), // 전자승인시간 (ZELC_AGR_TM) ownerApprovalYn: varchar('owner_approval_yn', { length: 1 }), // 선주승인필요여부 (ZOWN_AGR_IND_YN) // 기타 plannedInOutFlag: varchar('planned_in_out_flag', { length: 1 }), // 계획내외구분 (ZPLN_INO_GB) settlementStandard: varchar('settlement_standard', { length: 1 }), // 정산기준 (ZECAL_BSE) weightSettlementFlag: varchar('weight_settlement_flag', { length: 1 }), // 중량정산구분 (ZWGT_ECAL_GB) // 연동제 관련 priceIndexYn: varchar('price_index_yn', { length: 1 }), // 납품대금연동제대상여부 (ZDLV_PRICE_T) writtenContractNo: varchar('written_contract_no', { length: 20 }), // 서면계약번호 (ZWEBELN) contractVersion: integer('contract_version'), // 서면계약차수 (ZVER_NO) // 가격/금액 관련 currency: varchar('currency', { length: 10 }).default('KRW'), // 통화 (KRW, USD 등) // ZPO_CURR totalAmount: numeric('total_amount', { precision: 12, scale: 2 }), // 총 계약 금액(아이템 합산 등) // ZPO_AMT discount: numeric('discount', { precision: 12, scale: 2 }), // 전체 할인 // 인터페이스에 없음 (개별 품목별로는 있음) tax: numeric('tax', { precision: 12, scale: 2 }), // 전체 세금 // 인터페이스에 없음 (개별 품목별로는 있음) shippingFee: numeric('shipping_fee', { precision: 12, scale: 2 }), // 배송비 // 인터페이스에 없음 (개별 품목별로는 있음) netTotal: numeric('net_total', { precision: 12, scale: 2 }), // (합계) - (할인) + (세금) + (배송비) // 인터페이스에 없음 (개별 품목별로는 있음) // 부분 납품/부분 결제 가능 여부 partialShippingAllowed: boolean('partial_shipping_allowed').default(false), partialPaymentAllowed: boolean('partial_payment_allowed').default(false), // 추가 메모/비고 remarks: text('remarks'), vendorComment: text('vendor_comment'), shiComment: text('shi_comment'), // PO 계약서 내용 contractContent: text('contract_content'), // 버전 관리 (PO 재발행 등) version: integer('version').default(1), // 생성/수정 시각 createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); // 타입 추론 export type Contract = typeof contracts.$inferSelect // ============ contract_items (1:N 관계) ============ // 한 계약(contracts.id)에 여러 아이템을 연결 export const contractItems = pgTable("contract_items", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), contractId: integer("contract_id") .notNull() .references(() => contracts.id, { onDelete: "cascade" }), itemId: integer("item_id") .notNull() .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), 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"), // 발주비고 (ZPO_RMK) // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (t) => [ // 같은 계약 + 같은 itemId에 대한 중복을 막음 unique().on(t.contractId, t.itemId), uniqueIndex("contract_items_contract_item_idx").on(t.contractId, t.itemId), ]) export type ContractItem = typeof contractItems.$inferSelect // ============ DocuSign 연동용 (전자서명 이력) ============ // Envelope(전자서명 요청) 테이블 export const contractEnvelopes = pgTable("contract_envelopes", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), // 연결된 계약 contractId: integer("contract_id") .notNull() .references(() => contracts.id, { onDelete: "cascade" }), // DocuSign에서 발급되는 Envelope/Document 식별자 envelopeId: varchar("envelope_id", { length: 200 }).notNull(), documentId: varchar("document_id", { length: 200 }), // Envelope 전체 상태 (예: sent, completed, voided ...) envelopeStatus: varchar("envelope_status", { length: 50 }), fileName: varchar("file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 1024 }).notNull(), // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); // 하나의 Envelope에 여러 서명자(사인 요청 대상)가 있을 수 있음 export const contractSigners = pgTable("contract_signers", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), // Envelope와 1:N 관계 envelopeId: integer("envelope_id") .notNull() .references(() => contractEnvelopes.id, { onDelete: "cascade" }), // Reference to vendor_contacts table (optional - if signer is from vendor contacts) vendorContactId: integer("vendor_contact_id") .references(() => vendorContacts.id), // Is this signer from the requester (company) side or vendor side signerType: varchar("signer_type", { length: 20, enum: ["REQUESTER", "VENDOR"] }).notNull().default("VENDOR"), // 서명자 정보 (manual entry or populated from vendor contact) signerEmail: varchar("signer_email", { length: 255 }).notNull(), signerName: varchar("signer_name", { length: 100 }).notNull(), signerPosition: varchar("signer_position", { length: 100 }), // 서명자별 상태 (sent, delivered, signed, declined, etc.) signerStatus: varchar("signer_status", { length: 50 }).default("PENDING"), signedAt: timestamp("signed_at"), // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const contractsDetailView = pgView("contracts_detail_view").as((qb) => { return qb .select({ // Contract primary information id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, // Project information projectId: contracts.projectId, projectCode: projects.code, projectName: projects.name, // Vendor information vendorId: contracts.vendorId, vendorName: vendors.vendorName, // Payment and delivery details paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, deliveryDate: contracts.deliveryDate, deliveryLocation: contracts.deliveryLocation, // Financial information currency: contracts.currency, totalAmount: contracts.totalAmount, discount: contracts.discount, tax: contracts.tax, shippingFee: contracts.shippingFee, netTotal: contracts.netTotal, // Additional settings partialShippingAllowed: contracts.partialShippingAllowed, partialPaymentAllowed: contracts.partialPaymentAllowed, remarks: contracts.remarks, version: contracts.version, // Timestamps createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, // Electronic signature status hasSignature: sql`EXISTS ( SELECT 1 FROM ${contractEnvelopes} WHERE ${contractEnvelopes.contractId} = ${contracts.id} )`.as("has_signature"), // hasItme: sql`EXISTS ( // SELECT 1 // FROM ${contractItems} // WHERE ${contractItems.contractId} = ${contracts.id} // )`.as("has_signature"), // ========================= // 1) contract_items -> JSON // ========================= // 'items' (or "contractItems")라는 필드를 JSON 배열로 가져오기 items: sql`COALESCE(( SELECT json_agg( json_build_object( 'id', ci.id, 'itemId', ci.item_id, 'description', ci.description, 'quantity', ci.quantity, 'unitPrice', ci.unit_price, 'taxRate', ci.tax_rate, 'taxAmount', ci.tax_amount, 'totalLineAmount', ci.total_line_amount, 'remark', ci.remark, 'createdAt', ci.created_at, 'updatedAt', ci.updated_at ) ) FROM ${contractItems} AS ci WHERE ci.contract_id = ${contracts.id} ), '[]')`.as("items"), // ========================= // 2) contract_envelopes -> JSON // ========================= envelopes: sql`COALESCE(( SELECT json_agg( json_build_object( 'id', ce.id, 'envelopeId', ce.envelope_id, 'documentId', ce.document_id, 'envelopeStatus', ce.envelope_status, 'fileName', ce.file_name, 'filePath', ce.file_path, 'createdAt', ce.created_at, 'updatedAt', ce.updated_at, 'signers', ( SELECT json_agg( json_build_object( 'id', cs.id, 'vendorContactId', cs.vendor_contact_id, 'signerType', cs.signer_type, 'signerEmail', cs.signer_email, 'signerName', cs.signer_name, 'signerPosition', cs.signer_position, 'signerStatus', cs.signer_status, 'signedAt', cs.signed_at ) ) FROM ${contractSigners} AS cs WHERE cs.envelope_id = ce.id ) ) ) FROM ${contractEnvelopes} AS ce WHERE ce.contract_id = ${contracts.id} ), '[]')`.as("envelopes"), }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) }); export interface ContractItemParsed { id: number itemId: number description: string | null quantity: number unitPrice: number | null taxRate: number | null taxAmount: number | null totalLineAmount: number | null remark: string | null createdAt: string updatedAt: string } // 기존 Envelope + Signers export interface Envelope { id: number envelopeId: string documentId: string | null envelopeStatus: string | null fileName: string filePath: string createdAt: string updatedAt: string signers?: { id: number vendorContactId: number | null signerType: string signerEmail: string signerName: string signerPosition: string | null signerStatus: string signedAt: string | null }[] } // Drizzle가 만들어준 raw type export type ContractDetail = typeof contractsDetailView.$inferSelect; // 우리가 UI에서 쓰고 싶은 파싱된 타입 export type ContractDetailParsed = Omit & { envelopes: Envelope[] items: ContractItemParsed[] } // ============ poa (Purchase Order Amendment) ============ export const poa = pgTable("poa", { // 주 키 id: integer("id").primaryKey().generatedAlwaysAsIdentity(), // Form code는 원본과 동일하게 유지 contractNo: varchar("contract_no", { length: 100 }).notNull(), // 원본 PO 참조 originalContractNo: varchar("original_contract_no", { length: 100 }) .notNull() .references(() => contracts.contractNo, { onDelete: "cascade" }), // 원본 계약 정보 projectId: integer("project_id") .notNull() .references(() => projects.id, { onDelete: "cascade" }), vendorId: integer("vendor_id") .notNull() .references(() => vendors.id, { onDelete: "cascade" }), originalContractName: varchar("original_contract_name", { length: 255 }).notNull(), originalStatus: varchar("original_status", { length: 50 }).notNull(), // 변경된 납품 조건 deliveryTerms: text("delivery_terms"), // 변경된 납품 조건 deliveryDate: date("delivery_date"), // 변경된 납품 기한 deliveryLocation: varchar("delivery_location", { length: 255 }), // 변경된 납품 장소 // 변경된 가격/금액 관련 currency: varchar("currency", { length: 10 }), // 변경된 통화 totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 변경된 총 금액 discount: numeric("discount", { precision: 12, scale: 2 }), // 변경된 할인 tax: numeric("tax", { precision: 12, scale: 2 }), // 변경된 세금 shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 변경된 배송비 netTotal: numeric("net_total", { precision: 12, scale: 2 }), // 변경된 순 총액 // 변경 사유 changeReason: text("change_reason"), // 승인 상태 approvalStatus: varchar("approval_status", { length: 50 }).default("PENDING"), // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }) // 타입 추론 export type POA = typeof poa.$inferSelect // ============ poa_detail_view ============ export const poaDetailView = pgView("poa_detail_view").as((qb) => { return qb .select({ // POA primary information id: poa.id, contractNo: poa.contractNo, projectId: contracts.projectId, vendorId: contracts.vendorId, changeReason: poa.changeReason, approvalStatus: poa.approvalStatus, // Original PO information originalContractName: sql`${contracts.contractName}`.as('original_contract_name'), originalStatus: sql`${contracts.status}`.as('original_status'), originalStartDate: sql`${contracts.startDate}`.as('original_start_date'), originalEndDate: sql`${contracts.endDate}`.as('original_end_date'), // Changed delivery details deliveryTerms: poa.deliveryTerms, deliveryDate: poa.deliveryDate, deliveryLocation: poa.deliveryLocation, // Changed financial information currency: poa.currency, totalAmount: poa.totalAmount, discount: poa.discount, tax: poa.tax, shippingFee: poa.shippingFee, netTotal: poa.netTotal, // Timestamps createdAt: poa.createdAt, updatedAt: poa.updatedAt, // Electronic signature status hasSignature: sql`EXISTS ( SELECT 1 FROM ${contractEnvelopes} WHERE ${contractEnvelopes.contractId} = ${poa.id} )`.as('has_signature'), }) .from(poa) .leftJoin(contracts, eq(poa.contractNo, contracts.contractNo)) }); // Type inference for the view export type POADetail = typeof poaDetailView.$inferSelect;