diff --git a/backend-node/src/services/purchaseService.ts b/backend-node/src/services/purchaseService.ts index 1fa02e9c..6a8b22ab 100644 --- a/backend-node/src/services/purchaseService.ts +++ b/backend-node/src/services/purchaseService.ts @@ -380,27 +380,336 @@ export async function listProposal(filter: PurchaseListFilter): Promise> { - const { page, pageSize } = clampPaging(filter); - logger.warn("listInbound: purchase_order_part / arrival_plan 미존재 — 빈 응답"); - return { rows: [], totalCount: 0, page, pageSize }; + const pool = getPool(); + const { limit, offset, page, pageSize } = clampPaging(filter); + + const where: string[] = [ + `POM.MAIL_SEND_DATE IS NOT NULL`, + `POM.STATUS = 'create'`, + `(POM.MULTI_MASTER_YN = 'Y' OR COALESCE(POM.MULTI_MASTER_YN, '') <> 'Y' AND COALESCE(POM.MULTI_YN, '') <> 'Y')`, + ]; + const params: any[] = []; + const addParam = (val: any) => { params.push(val); return `$${params.length}`; }; + + if (filter.year) where.push(`TO_CHAR(POM.REGDATE, 'YYYY') = ${addParam(String(filter.year))}`); + if (filter.customer_cd) where.push(`CM.CUSTOMER_OBJID = REPLACE(${addParam(filter.customer_cd)}, 'C_', '')`); + if (filter.project_no) where.push(`CM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`); + if (filter.purchase_order_no) where.push(`POM.PURCHASE_ORDER_NO ILIKE ${addParam(`%${filter.purchase_order_no}%`)}`); + if (filter.partner_objid) where.push(`POM.PARTNER_OBJID = REPLACE(${addParam(filter.partner_objid)}, 'C_', '')`); + if (filter.sales_mng_user_id) where.push(`POM.WRITER = ${addParam(filter.sales_mng_user_id)}`); + if (filter.delivery_start_date) where.push(`POM.DELIVERY_DATE >= ${addParam(filter.delivery_start_date)}`); + if (filter.delivery_end_date) where.push(`POM.DELIVERY_DATE <= ${addParam(filter.delivery_end_date)}`); + if (filter.reg_start_date) where.push(`TO_CHAR(POM.REGDATE, 'YYYY-MM-DD') >= ${addParam(filter.reg_start_date)}`); + if (filter.reg_end_date) where.push(`TO_CHAR(POM.REGDATE, 'YYYY-MM-DD') <= ${addParam(filter.reg_end_date)}`); + if (filter.part_no) where.push(`EXISTS (SELECT 1 FROM PURCHASE_ORDER_PART POPX WHERE POPX.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID AND POPX.PART_NO ILIKE ${addParam(`%${filter.part_no}%`)})`); + if (filter.part_name) where.push(`EXISTS (SELECT 1 FROM PURCHASE_ORDER_PART POPX WHERE POPX.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID AND POPX.PART_NAME ILIKE ${addParam(`%${filter.part_name}%`)})`); + if (filter.part_spec) where.push(`EXISTS (SELECT 1 FROM PURCHASE_ORDER_PART POPX WHERE POPX.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID AND POPX.SPEC ILIKE ${addParam(`%${filter.part_spec}%`)})`); + + const whereSql = `WHERE ${where.join(" AND ")}`; + const havingSql = + filter.delivery_status + ? `HAVING (CASE WHEN COALESCE(S1.TOTAL_PO_QTY,0) - COALESCE(S1.TOTAL_DELIVERY_QTY,0) <= 0 THEN '입고완료' + WHEN TO_CHAR(NOW(),'YYYY-MM-DD') > POM.DELIVERY_DATE THEN '지연' + ELSE '입고중' END) = ${addParam(filter.delivery_status)}` + : ""; + + const fromSql = ` + FROM PURCHASE_ORDER_MASTER POM + LEFT JOIN PROJECT_MGMT CM ON CM.OBJID = POM.CONTRACT_MGMT_OBJID + LEFT JOIN ( + SELECT POP.PURCHASE_ORDER_MASTER_OBJID, + SUM(COALESCE(POP.ORDER_QTY::NUMERIC, 0)) AS TOTAL_PO_QTY, + MAX(AP_AGG.MAX_RECEIPT_DATE) AS CUR_DELIVERY_DATE, + SUM(COALESCE(AP_AGG.SUM_RECEIPT_QTY, 0)) AS TOTAL_DELIVERY_QTY, + SUM(COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * COALESCE(POP.ORDER_QTY::NUMERIC, 0)) AS TOTAL_SUPPLY_PRICE, + SUM(COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * COALESCE(AP_AGG.SUM_RECEIPT_QTY, 0)) AS TOTAL_DELIVERY_PRICE, + SUM(COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * + (COALESCE(POP.ORDER_QTY::NUMERIC, 0) - COALESCE(AP_AGG.SUM_RECEIPT_QTY, 0))) AS TOTAL_NOT_DELIVERY_PRICE + FROM PURCHASE_ORDER_PART POP + LEFT JOIN ( + SELECT PARENT_OBJID, PART_OBJID, + SUM(COALESCE(RECEIPT_QTY::NUMERIC, 0)) AS SUM_RECEIPT_QTY, + MAX(RECEIPT_DATE) AS MAX_RECEIPT_DATE + FROM ARRIVAL_PLAN + GROUP BY PARENT_OBJID, PART_OBJID + ) AP_AGG ON AP_AGG.PARENT_OBJID = POP.PURCHASE_ORDER_MASTER_OBJID + AND AP_AGG.PART_OBJID = POP.PART_OBJID + GROUP BY POP.PURCHASE_ORDER_MASTER_OBJID + ) S1 ON POM.OBJID = S1.PURCHASE_ORDER_MASTER_OBJID + ${whereSql} + `; + + const groupBySql = havingSql ? `GROUP BY POM.OBJID, S1.TOTAL_PO_QTY, S1.TOTAL_DELIVERY_QTY, POM.DELIVERY_DATE` : ""; + + const dataSql = ` + SELECT + POM.OBJID AS objid, + POM.PURCHASE_ORDER_NO AS purchase_order_no, + POM.STATUS AS status, + (SELECT REQUEST_MNG_NO FROM SALES_REQUEST_MASTER SRM WHERE SRM.OBJID = POM.SALES_REQUEST_OBJID LIMIT 1) AS proposal_no, + CM.PROJECT_NO AS project_no, + -- 첫 품번/품명 + "외 N건" + (SELECT CASE WHEN COUNT(*) > 1 THEN MIN(PART_NO) || ' 외 ' || (COUNT(*) - 1) || '건' ELSE MIN(PART_NO) END + FROM PURCHASE_ORDER_PART POPN WHERE POPN.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID) AS part_no, + (SELECT CASE WHEN COUNT(*) > 1 THEN MIN(PART_NAME) || ' 외 ' || (COUNT(*) - 1) || '건' ELSE MIN(PART_NAME) END + FROM PURCHASE_ORDER_PART POPN WHERE POPN.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID) AS part_name, + POM.PARTNER_OBJID AS partner_objid, + (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID = POM.PARTNER_OBJID LIMIT 1) AS partner_name, + (SELECT CC.CODE_NAME FROM COMM_CODE CC + WHERE CC.CODE_ID = (SELECT POP2.CURRENCY FROM PURCHASE_ORDER_PART POP2 + WHERE POP2.PURCHASE_ORDER_MASTER_OBJID = POM.OBJID + AND POP2.CURRENCY IS NOT NULL AND POP2.CURRENCY <> '' LIMIT 1) + LIMIT 1) AS currency_name, + POM.WRITER AS writer, + COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = POM.WRITER LIMIT 1), POM.WRITER, '') AS writer_name, + (SELECT COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = AP.WRITER), AP.WRITER, '') + FROM ARRIVAL_PLAN AP + WHERE AP.PARENT_OBJID = POM.OBJID + AND AP.RECEIPT_QTY IS NOT NULL AND AP.RECEIPT_QTY::NUMERIC > 0 + ORDER BY AP.RECEIPT_DATE DESC LIMIT 1) AS delivery_writer_name, + (SELECT AP.RECEIPT_DATE FROM ARRIVAL_PLAN AP + WHERE AP.PARENT_OBJID = POM.OBJID + AND AP.RECEIPT_QTY IS NOT NULL AND AP.RECEIPT_QTY::NUMERIC > 0 + ORDER BY AP.RECEIPT_DATE DESC LIMIT 1) AS delivery_regdate, + COALESCE(S1.TOTAL_PO_QTY, 0) AS total_po_qty, + COALESCE(S1.TOTAL_DELIVERY_QTY, 0) AS total_delivery_qty, + COALESCE(S1.TOTAL_PO_QTY, 0) - COALESCE(S1.TOTAL_DELIVERY_QTY, 0) AS non_delivery_qty, + COALESCE(S1.TOTAL_SUPPLY_PRICE, 0) AS total_supply_price, + COALESCE(S1.TOTAL_DELIVERY_PRICE, 0) AS total_delivery_price, + COALESCE(S1.TOTAL_NOT_DELIVERY_PRICE, 0) AS total_not_delivery_price, + (SELECT COUNT(1)::int FROM ATTACH_FILE_INFO AF + WHERE AF.TARGET_OBJID = POM.OBJID + AND AF.DOC_TYPE = 'INSPECTION_FILE' + AND UPPER(COALESCE(AF.STATUS, 'Active')) = 'ACTIVE') AS inspection_file_cnt, + CASE WHEN COALESCE(S1.TOTAL_PO_QTY, 0) - COALESCE(S1.TOTAL_DELIVERY_QTY, 0) <= 0 THEN '입고완료' + WHEN TO_CHAR(NOW(),'YYYY-MM-DD') > POM.DELIVERY_DATE THEN '지연' + ELSE '입고중' + END AS delivery_status, + POM.PURCHASE_CLOSE_DATE AS purchase_close_date + ${fromSql} + ${groupBySql} + ${havingSql} + ORDER BY POM.REGDATE DESC + LIMIT ${addParam(limit)} OFFSET ${addParam(offset)} + `; + const countSql = `SELECT COUNT(*)::int AS cnt ${fromSql} ${groupBySql} ${havingSql}`; + try { + const [d, c] = await Promise.all([ + pool.query(dataSql, params), + pool.query(countSql, params.slice(0, params.length - 2)), + ]); + return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize }; + } catch (e: any) { + logger.error("listInbound 실패", { error: e.message }); + return { rows: [], totalCount: 0, page, pageSize }; + } } -// ─── 5) 품목별 입고관리 (wace deliveryMngPartList) ── +// ─── 5) 품목별 입고관리 (wace deliveryMngPartList 매퍼 1:1) ── +// 매퍼 본문: wace_plm/src/com/pms/mapper/purchaseOrder.xml:6309-6543 +// PURCHASE_ORDER_PART 행별 1행 + AP_AGG (입고집계) + IID_AGG/DEFECT_AGG (검사 — RPS 미존재로 0). + export async function listInboundByItem(filter: PurchaseListFilter): Promise> { - const { page, pageSize } = clampPaging(filter); - logger.warn("listInboundByItem: purchase_order_part / arrival_plan 미존재 — 빈 응답"); - return { rows: [], totalCount: 0, page, pageSize }; + const pool = getPool(); + const { limit, offset, page, pageSize } = clampPaging(filter); + + const where: string[] = [ + `POM.MAIL_SEND_DATE IS NOT NULL`, + `POM.STATUS = 'create'`, + `(POM.MULTI_MASTER_YN = 'Y' OR COALESCE(POM.MULTI_MASTER_YN, '') <> 'Y' AND COALESCE(POM.MULTI_YN, '') <> 'Y')`, + ]; + const params: any[] = []; + const addParam = (val: any) => { params.push(val); return `$${params.length}`; }; + + if (filter.year) where.push(`TO_CHAR(POM.REGDATE, 'YYYY') = ${addParam(String(filter.year))}`); + if (filter.customer_cd) where.push(`CM.CUSTOMER_OBJID = REPLACE(${addParam(filter.customer_cd)}, 'C_', '')`); + if (filter.project_no) where.push(`CM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`); + if (filter.purchase_order_no) where.push(`POM.PURCHASE_ORDER_NO ILIKE ${addParam(`%${filter.purchase_order_no}%`)}`); + if (filter.partner_objid) where.push(`POM.PARTNER_OBJID = REPLACE(${addParam(filter.partner_objid)}, 'C_', '')`); + if (filter.sales_mng_user_id) where.push(`POM.WRITER = ${addParam(filter.sales_mng_user_id)}`); + if (filter.delivery_start_date) where.push(`POP.DELIVERY_REQUEST_DATE >= ${addParam(filter.delivery_start_date)}`); + if (filter.delivery_end_date) where.push(`POP.DELIVERY_REQUEST_DATE <= ${addParam(filter.delivery_end_date)}`); + if (filter.reg_start_date) where.push(`TO_CHAR(POM.REGDATE, 'YYYY-MM-DD') >= ${addParam(filter.reg_start_date)}`); + if (filter.reg_end_date) where.push(`TO_CHAR(POM.REGDATE, 'YYYY-MM-DD') <= ${addParam(filter.reg_end_date)}`); + if (filter.part_no) where.push(`POP.PART_NO ILIKE ${addParam(`%${filter.part_no}%`)}`); + if (filter.part_name) where.push(`POP.PART_NAME ILIKE ${addParam(`%${filter.part_name}%`)}`); + if (filter.part_spec) where.push(`POP.SPEC ILIKE ${addParam(`%${filter.part_spec}%`)}`); + if (filter.delivery_status) { + where.push(`(CASE WHEN COALESCE(POP.ORDER_QTY::NUMERIC, 0) - COALESCE(AP_AGG.DELIVERY_QTY, 0) <= 0 THEN '입고완료' + WHEN TO_CHAR(NOW(),'YYYY-MM-DD') > POM.DELIVERY_DATE THEN '지연' + ELSE '입고중' END) = ${addParam(filter.delivery_status)}`); + } + + const whereSql = `WHERE ${where.join(" AND ")}`; + const fromSql = ` + FROM PURCHASE_ORDER_PART POP + JOIN PURCHASE_ORDER_MASTER POM ON POM.OBJID = POP.PURCHASE_ORDER_MASTER_OBJID + LEFT JOIN PROJECT_MGMT CM ON CM.OBJID = POM.CONTRACT_MGMT_OBJID + LEFT JOIN ( + SELECT PARENT_OBJID, PART_OBJID, SUM(COALESCE(RECEIPT_QTY::NUMERIC, 0)) AS DELIVERY_QTY + FROM ARRIVAL_PLAN + GROUP BY PARENT_OBJID, PART_OBJID + ) AP_AGG ON AP_AGG.PARENT_OBJID = POM.OBJID AND AP_AGG.PART_OBJID = POP.PART_OBJID + ${whereSql} + `; + + const dataSql = ` + SELECT + POP.OBJID AS objid, + POP.OBJID AS purchase_order_part_objid, + POM.OBJID AS purchase_order_master_objid, + POM.STATUS AS status, + (SELECT REQUEST_MNG_NO FROM SALES_REQUEST_MASTER SRM WHERE SRM.OBJID = POM.SALES_REQUEST_OBJID LIMIT 1) AS proposal_no, + POM.PURCHASE_ORDER_NO AS purchase_order_no, + CM.PROJECT_NO AS project_no, + -- 부품품번 (sales_request_part 미존재 → POP.PART_NO fallback) + POP.PART_NO AS component_part_no, + POP.PART_NO AS part_no, + POP.PART_NAME AS part_name, + POM.PARTNER_OBJID AS partner_objid, + (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID = POM.PARTNER_OBJID LIMIT 1) AS partner_name, + (SELECT CC.CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = POP.CURRENCY LIMIT 1) AS currency_name, + POP.DELIVERY_REQUEST_DATE AS delivery_request_date, + COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = POM.WRITER LIMIT 1), POM.WRITER, '') AS writer_name, + (SELECT COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = AP.WRITER), AP.WRITER, '') + FROM ARRIVAL_PLAN AP + WHERE AP.PARENT_OBJID = POM.OBJID + AND AP.PART_OBJID = POP.PART_OBJID + AND AP.RECEIPT_QTY IS NOT NULL AND AP.RECEIPT_QTY::NUMERIC > 0 + ORDER BY AP.RECEIPT_DATE DESC LIMIT 1) AS delivery_writer_name, + (SELECT AP.RECEIPT_DATE FROM ARRIVAL_PLAN AP + WHERE AP.PARENT_OBJID = POM.OBJID + AND AP.PART_OBJID = POP.PART_OBJID + AND AP.RECEIPT_QTY IS NOT NULL AND AP.RECEIPT_QTY::NUMERIC > 0 + ORDER BY AP.RECEIPT_DATE DESC LIMIT 1) AS delivery_regdate, + COALESCE(POP.ORDER_QTY::NUMERIC, 0) AS order_qty, + COALESCE(AP_AGG.DELIVERY_QTY, 0) AS delivery_qty, + COALESCE(POP.ORDER_QTY::NUMERIC, 0) - COALESCE(AP_AGG.DELIVERY_QTY, 0) AS non_delivery_qty, + COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * COALESCE(POP.ORDER_QTY::NUMERIC, 0) AS total_supply_price, + COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * COALESCE(AP_AGG.DELIVERY_QTY, 0) AS total_delivery_price, + COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * + (COALESCE(POP.ORDER_QTY::NUMERIC, 0) - COALESCE(AP_AGG.DELIVERY_QTY, 0)) AS total_not_delivery_price, + -- 검사현황/폐기/확정수량 (incoming_inspection_* 미존재 → 0) + '' AS inspection_status, + 0 AS defect_qty, + COALESCE(AP_AGG.DELIVERY_QTY, 0) AS confirmed_qty, + CASE WHEN COALESCE(POP.ORDER_QTY::NUMERIC, 0) - COALESCE(AP_AGG.DELIVERY_QTY, 0) <= 0 THEN '입고완료' + WHEN TO_CHAR(NOW(),'YYYY-MM-DD') > POM.DELIVERY_DATE THEN '지연' + ELSE '입고중' + END AS delivery_status + ${fromSql} + ORDER BY POM.REGDATE DESC, POP.OBJID + LIMIT ${addParam(limit)} OFFSET ${addParam(offset)} + `; + const countSql = `SELECT COUNT(*)::int AS cnt ${fromSql}`; + try { + const [d, c] = await Promise.all([ + pool.query(dataSql, params), + pool.query(countSql, params.slice(0, params.length - 2)), + ]); + return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize }; + } catch (e: any) { + logger.error("listInboundByItem 실패", { error: e.message }); + return { rows: [], totalCount: 0, page, pageSize }; + } } -// ─── 6) 입고일별 입고관리 (wace purchaseCloseList) ── +// ─── 6) 입고일별 입고관리 (wace purchaseCloseList 매퍼 1:1) ── +// 매퍼 본문: wace_plm/src/com/pms/mapper/purchaseOrder.xml:6549-6765 +// ARRIVAL_PLAN 행별 (RECEIPT_QTY > 0) + 매입마감/관세/세금계산서 컬럼. + export async function listInboundByDate(filter: PurchaseListFilter): Promise> { - const { page, pageSize } = clampPaging(filter); - logger.warn("listInboundByDate: arrival_plan / purchase_order_part 미존재 — 빈 응답"); - return { rows: [], totalCount: 0, page, pageSize }; + const pool = getPool(); + const { limit, offset, page, pageSize } = clampPaging(filter); + + const where: string[] = [ + `POM.MAIL_SEND_DATE IS NOT NULL`, + `POM.STATUS = 'create'`, + `COALESCE(AP.RECEIPT_QTY, '0')::NUMERIC > 0`, + ]; + const params: any[] = []; + const addParam = (val: any) => { params.push(val); return `$${params.length}`; }; + + if (filter.year) where.push(`TO_CHAR(POM.REGDATE, 'YYYY') = ${addParam(String(filter.year))}`); + if (filter.customer_cd) where.push(`CM.CUSTOMER_OBJID = REPLACE(${addParam(filter.customer_cd)}, 'C_', '')`); + if (filter.project_no) where.push(`CM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`); + if (filter.purchase_order_no) where.push(`POM.PURCHASE_ORDER_NO ILIKE ${addParam(`%${filter.purchase_order_no}%`)}`); + if (filter.partner_objid) where.push(`POM.PARTNER_OBJID = REPLACE(${addParam(filter.partner_objid)}, 'C_', '')`); + if (filter.sales_mng_user_id) where.push(`POM.WRITER = ${addParam(filter.sales_mng_user_id)}`); + if (filter.part_no) where.push(`POP.PART_NO ILIKE ${addParam(`%${filter.part_no}%`)}`); + if (filter.part_name) where.push(`POP.PART_NAME ILIKE ${addParam(`%${filter.part_name}%`)}`); + if (filter.part_spec) where.push(`POP.SPEC ILIKE ${addParam(`%${filter.part_spec}%`)}`); + if (filter.receipt_date_start) where.push(`AP.RECEIPT_DATE >= ${addParam(filter.receipt_date_start)}`); + if (filter.receipt_date_end) where.push(`AP.RECEIPT_DATE <= ${addParam(filter.receipt_date_end)}`); + if (filter.close_status === "Y") where.push(`AP.PURCHASE_CLOSE_DATE IS NOT NULL AND AP.PURCHASE_CLOSE_DATE <> ''`); + if (filter.close_status === "N") where.push(`(AP.PURCHASE_CLOSE_DATE IS NULL OR AP.PURCHASE_CLOSE_DATE = '')`); + + const whereSql = `WHERE ${where.join(" AND ")}`; + const fromSql = ` + FROM ARRIVAL_PLAN AP + JOIN PURCHASE_ORDER_MASTER POM ON POM.OBJID = AP.PARENT_OBJID + LEFT JOIN PURCHASE_ORDER_PART POP + ON POP.PURCHASE_ORDER_MASTER_OBJID = AP.PARENT_OBJID + AND POP.PART_OBJID = AP.PART_OBJID + LEFT JOIN PROJECT_MGMT CM ON CM.OBJID = POM.CONTRACT_MGMT_OBJID + ${whereSql} + `; + const dataSql = ` + SELECT + AP.OBJID AS objid, + AP.OBJID AS arrival_plan_objid, + POP.OBJID AS purchase_order_part_objid, + POM.OBJID AS purchase_order_master_objid, + (SELECT REQUEST_MNG_NO FROM SALES_REQUEST_MASTER SRM WHERE SRM.OBJID = POM.SALES_REQUEST_OBJID LIMIT 1) AS proposal_no, + POM.PURCHASE_ORDER_NO AS purchase_order_no, + CM.PROJECT_NO AS project_no, + POP.PART_NO AS component_part_no, + POP.PART_NO AS part_no, + POP.PART_NAME AS part_name, + (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID = POM.PARTNER_OBJID LIMIT 1) AS partner_name, + (SELECT CC.CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = POP.CURRENCY LIMIT 1) AS currency_name, + AP.RECEIPT_DATE AS receipt_date, + COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = POM.WRITER LIMIT 1), POM.WRITER, '') AS writer_name, + COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = AP.WRITER LIMIT 1), AP.WRITER, '') AS delivery_writer_name, + COALESCE(AP.RECEIPT_QTY::NUMERIC, 0) AS receipt_qty, + COALESCE(POP.PARTNER_PRICE::NUMERIC, 0) * COALESCE(AP.RECEIPT_QTY::NUMERIC, 0) AS total_delivery_price, + '' AS inspection_status, + 0 AS defect_qty, + COALESCE(AP.RECEIPT_QTY::NUMERIC, 0) AS confirmed_qty, + AP.SUB_LOCATION AS sub_location_name, + (SELECT CC.CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = AP.FOREIGN_TYPE LIMIT 1) AS foreign_type_name, + AP.EXCHANGE_RATE AS exchange_rate, + (SELECT CC.CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = AP.TAX_TYPE LIMIT 1) AS tax_type_name, + AP.TAX_INVOICE_DATE AS tax_invoice_date, + AP.EXPORT_DECL_NO AS export_decl_no, + AP.LOADING_DATE AS loading_date, + AP.DUTY AS duty, + AP.IMPORT_VAT AS import_vat, + AP.PURCHASE_CLOSE_DATE AS purchase_close_date + ${fromSql} + ORDER BY AP.RECEIPT_DATE DESC NULLS LAST, AP.OBJID + LIMIT ${addParam(limit)} OFFSET ${addParam(offset)} + `; + const countSql = `SELECT COUNT(*)::int AS cnt ${fromSql}`; + try { + const [d, c] = await Promise.all([ + pool.query(dataSql, params), + pool.query(countSql, params.slice(0, params.length - 2)), + ]); + return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize }; + } catch (e: any) { + logger.error("listInboundByDate 실패", { error: e.message }); + return { rows: [], totalCount: 0, page, pageSize }; + } } // ─── 7) 프로젝트별 발주/입고 현황 (wace projectPurchaseDeliveryStatus) ── @@ -451,23 +760,90 @@ export async function listProjectStatus(filter: PurchaseListFilter): Promise 0) + COALESCE((SELECT COUNT(DISTINCT AP.PART_OBJID)::int + FROM ARRIVAL_PLAN AP + JOIN PURCHASE_ORDER_MASTER POM3 ON POM3.OBJID = AP.PARENT_OBJID + WHERE POM3.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM3.MAIL_SEND_DATE IS NOT NULL + AND POM3.STATUS = 'create' + AND COALESCE(AP.RECEIPT_QTY::numeric, 0) > 0), 0) AS dlv_item_cnt, + COALESCE((SELECT SUM(COALESCE(AP.RECEIPT_QTY::numeric, 0)) + FROM ARRIVAL_PLAN AP + JOIN PURCHASE_ORDER_MASTER POM3 ON POM3.OBJID = AP.PARENT_OBJID + WHERE POM3.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM3.MAIL_SEND_DATE IS NOT NULL + AND POM3.STATUS = 'create'), 0) AS dlv_qty, + -- 미입고 = 발주 - 입고 (음수 방지) + GREATEST( + COALESCE((SELECT COUNT(DISTINCT POP.PART_OBJID)::int + FROM PURCHASE_ORDER_PART POP + JOIN PURCHASE_ORDER_MASTER POM2 ON POM2.OBJID = POP.PURCHASE_ORDER_MASTER_OBJID + WHERE POM2.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM2.MAIL_SEND_DATE IS NOT NULL + AND POM2.STATUS = 'create'), 0) + - COALESCE((SELECT COUNT(DISTINCT AP.PART_OBJID)::int + FROM ARRIVAL_PLAN AP + JOIN PURCHASE_ORDER_MASTER POM3 ON POM3.OBJID = AP.PARENT_OBJID + WHERE POM3.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM3.MAIL_SEND_DATE IS NOT NULL + AND POM3.STATUS = 'create' + AND COALESCE(AP.RECEIPT_QTY::numeric, 0) > 0), 0) + , 0) AS non_dlv_item_cnt, + GREATEST( + COALESCE((SELECT SUM(COALESCE(POP.ORDER_QTY::numeric, 0)) + FROM PURCHASE_ORDER_PART POP + JOIN PURCHASE_ORDER_MASTER POM2 ON POM2.OBJID = POP.PURCHASE_ORDER_MASTER_OBJID + WHERE POM2.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM2.MAIL_SEND_DATE IS NOT NULL + AND POM2.STATUS = 'create'), 0) + - COALESCE((SELECT SUM(COALESCE(AP.RECEIPT_QTY::numeric, 0)) + FROM ARRIVAL_PLAN AP + JOIN PURCHASE_ORDER_MASTER POM3 ON POM3.OBJID = AP.PARENT_OBJID + WHERE POM3.CONTRACT_MGMT_OBJID = PM.OBJID + AND POM3.MAIL_SEND_DATE IS NOT NULL + AND POM3.STATUS = 'create'), 0) + , 0) AS non_dlv_qty FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CTR ON CTR.OBJID = PM.CONTRACT_OBJID ${whereSql} diff --git a/docs/migration/purchase/data-sync/02_purchase_inbound_sync.sql b/docs/migration/purchase/data-sync/02_purchase_inbound_sync.sql new file mode 100644 index 00000000..671511ce --- /dev/null +++ b/docs/migration/purchase/data-sync/02_purchase_inbound_sync.sql @@ -0,0 +1,61 @@ +-- ============================================================ +-- 발주/입고 운영 sample 데이터 → RPS 이관 +-- 운영: 211.115.91.141:11133/waceplm +-- purchase_order_master 1건 / purchase_order_part 1건 / arrival_plan 1건 +-- 대상: 211.115.91.141:11134/vexplor_rps +-- +-- FK 매칭 (확인): +-- sales_request_objid='-233034270' → RPS sales_request_master.objid (있음) +-- contract_mgmt_objid='-1752090174' → 운영DB project_mgmt.objid (RPS contract_mgmt 미매칭, project_mgmt 매칭) +-- part_objid=1868260552 → RPS part_mng (있음) +-- partner_objid='0000000007' → RPS client_mng 서울반도체(주) (있음) +-- +-- 멱등성: ON CONFLICT DO NOTHING +-- ============================================================ + +-- ── purchase_order_master (RPS 이미 존재하면 mail_send_* 만 보강) ── +-- PK constraint 없어 ON CONFLICT 사용 불가 → WHERE NOT EXISTS 패턴 +INSERT INTO purchase_order_master + (objid, purchase_order_no, partner_objid, contract_mgmt_objid, sales_request_objid, + regdate, writer, status, mail_send_yn, mail_send_date, + sales_mng_user_id, payment_terms) +SELECT + '-2135417309','RPS26-0401-01','0000000007','-1752090174','-233034270', + '2026-04-01 07:20:58.687075','ady1225','create','Y','2026-04-03', + 'ish0312','0001069' +WHERE NOT EXISTS (SELECT 1 FROM purchase_order_master WHERE objid='-2135417309'); + +-- 이미 있던 행에는 매퍼 필수 필드(mail_send_*) 보강 +UPDATE purchase_order_master + SET mail_send_yn='Y', mail_send_date='2026-04-03' + WHERE objid='-2135417309' + AND COALESCE(mail_send_yn,'') = ''; + +-- ── purchase_order_part ─────────────────────────────────────── +INSERT INTO purchase_order_part + (objid, purchase_order_master_objid, part_objid, order_qty, partner_price, + remark, writer, regdate, part_name, spec, supply_unit_price, unit, + part_no, qty, part_delivery_place, delivery_request_date) +VALUES + ('-192149597','-2135417309',1868260552,'1','10000', + 'W/M ASSY (RWMR1070-NO07 LH) / HOLDER','ady1225','2026-04-01 07:20:58.687075', + 'Ti(GR5)','Ø50*22','10000','0001400','C3P50L22','1','RPS','2026-04-03') +ON CONFLICT (objid) DO NOTHING; + +-- ── arrival_plan ────────────────────────────────────────────── +INSERT INTO arrival_plan + (objid, parent_objid, order_part_objid, part_objid, + arrival_qty, receipt_qty, receipt_date, location, + writer, group_seq, seq, inventory_status, sub_location, receiver_id) +VALUES + ('1030275443','-2135417309','-192149597',1868260552, + '1','1','2026-04-01','L101', + 'ady1225','1','1','Y','1490000','ady1225') +ON CONFLICT (objid) DO NOTHING; + +-- 검증: 매퍼 WHERE (mail_send_date IS NOT NULL AND status='create') 통과 여부 +-- SELECT pom.purchase_order_no, pop.part_no, ap.receipt_date +-- FROM purchase_order_master pom +-- JOIN purchase_order_part pop ON pop.purchase_order_master_objid = pom.objid +-- LEFT JOIN arrival_plan ap ON ap.parent_objid = pom.objid AND ap.part_objid = pop.part_objid +-- WHERE pom.mail_send_date IS NOT NULL AND pom.status = 'create'; diff --git a/docs/migration/purchase/ddl-extracted/501_purchase_inbound.sql b/docs/migration/purchase/ddl-extracted/501_purchase_inbound.sql new file mode 100644 index 00000000..2b11facb --- /dev/null +++ b/docs/migration/purchase/ddl-extracted/501_purchase_inbound.sql @@ -0,0 +1,131 @@ +-- ============================================================ +-- 발주서 + 입고관리 — 구매관리 입고 3메뉴 + 발주서관리 의존 테이블 +-- 원본: 운영DB 211.115.91.141:11133/waceplm +-- purchase_order_master 1건 (mail_send_yn='Y', status='create') +-- purchase_order_part 1건 (RPS26-0401-01 / C3P50L22) +-- arrival_plan 1건 (receipt_qty=1, receipt_date=2026-04-01) +-- 추출일: 2026-05-15 +-- 적용대상: vexplor_rps (11134) +-- +-- 운영 ↔ RPS 타입 차이: +-- part_objid: 운영 varchar(64) → RPS bigint (part_mng.objid bigint 호환) +-- +-- 매퍼: +-- deliveryMngPartList: wace_plm/src/com/pms/mapper/purchaseOrder.xml:6309-6543 +-- purchaseCloseList: wace_plm/src/com/pms/mapper/purchaseOrder.xml:6549-6765 +-- projectPurchaseStat: wace_plm/src/com/pms/mapper/purchaseOrder.xml:6768-6951 +-- +-- 함정: +-- 1) wace 매퍼는 PROJECT_MGMT.OBJID = POM.CONTRACT_MGMT_OBJID 로 LEFT JOIN +-- (즉 contract_mgmt_objid 컬럼명이 실제로는 project_mgmt 키를 저장) +-- 2) WHERE: POM.MAIL_SEND_DATE IS NOT NULL AND POM.STATUS='create' +-- ============================================================ + +-- ── 1. purchase_order_master 보충 컬럼 (10개) ─────────────────── +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS mail_send_yn varchar; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS mail_send_date varchar; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS form_type varchar(20); +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS sales_mng_user_id2 varchar(50); +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS request_content text; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS purchase_close_date varchar(10); +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS shipment varchar; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS packing varchar; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS validity varchar; +ALTER TABLE purchase_order_master ADD COLUMN IF NOT EXISTS attn_to varchar; + +-- ── 2. purchase_order_part (운영 43 cols 1:1, part_objid 만 bigint) ─ +CREATE TABLE IF NOT EXISTS purchase_order_part ( + objid varchar(64) NOT NULL, + purchase_order_master_objid varchar(64), + part_objid bigint, + order_qty varchar, + partner_price varchar, + remark varchar, + writer varchar, + regdate timestamp, + status varchar, + part_name varchar, + do_no varchar, + thickness varchar, + width varchar, + height varchar, + out_diameter varchar, + length varchar, + in_diameter varchar, + inven_total_qty varchar, + ld_part_objid varchar, + spec varchar, + maker varchar, + supply_unit_price varchar, + unit varchar, + price1 varchar, + price2 varchar, + price3 varchar, + part_no varchar, + supply_unit_vat_price varchar, + price4 varchar, + supply_unit_vat_sum_price varchar, + total_order_qty varchar, + stock_qty varchar, + real_order_qty varchar, + update_date timestamp, + modifier varchar, + real_supply_price varchar, + bom_qty varchar, + qty varchar, + part_delivery_place varchar(50), + product_name varchar(200), + work_order_no varchar(50), + delivery_request_date varchar(20), + currency varchar, + CONSTRAINT purchase_order_part_pkey PRIMARY KEY (objid) +); + +CREATE INDEX IF NOT EXISTS idx_pop_master ON purchase_order_part (purchase_order_master_objid); +CREATE INDEX IF NOT EXISTS idx_pop_part ON purchase_order_part (part_objid); + +-- ── 3. arrival_plan (운영 37 cols 1:1, part_objid bigint) ─────── +CREATE TABLE IF NOT EXISTS arrival_plan ( + objid varchar(64) NOT NULL, + parent_objid varchar(64), + order_part_objid varchar(64), + part_objid bigint, + arrival_plan_date varchar, + re_arrival_plan_date varchar, + arrival_qty varchar, + receipt_qty varchar, + genuine_qty varchar, + receipt_date varchar, + inspection_date varchar, + location varchar, + error_qty varchar, + error_reason varchar, + attribution varchar, + status varchar, + assembly_status varchar, + writer varchar, + group_seq varchar, + seq varchar, + defect_content varchar, + defect_action varchar, + defect_note varchar, + defect_action_date varchar, + defect_action_title varchar, + inventory_status varchar, + sub_location varchar, + receiver_id varchar, + purchase_close_date varchar, + foreign_type varchar(10), + exchange_rate numeric(15,2), + duty numeric(15,2), + import_vat numeric(15,2), + tax_invoice_date varchar(10), + export_decl_no varchar(100), + loading_date varchar(10), + tax_type varchar(20), + CONSTRAINT arrival_plan_pkey PRIMARY KEY (objid) +); + +CREATE INDEX IF NOT EXISTS idx_arrival_parent ON arrival_plan (parent_objid); +CREATE INDEX IF NOT EXISTS idx_arrival_order_part ON arrival_plan (order_part_objid); +CREATE INDEX IF NOT EXISTS idx_arrival_part ON arrival_plan (part_objid);