구매관리 입고 3메뉴 + 프로젝트 발주/입고 통계 빈 그리드 채움

- DDL: purchase_order_part(43 cols) + arrival_plan(37 cols) + POM 보충 컬럼 10개
  (mail_send_yn/date, form_type, purchase_close_date 등)
- 데이터: 운영 sample purchase_order_master 1건 + part 1건 + arrival_plan 1건
- listInbound — wace deliveryMngList_new 매퍼 1:1 (POM + S1 집계, AP × POP)
- listInboundByItem — wace deliveryMngPartList 매퍼 1:1 (품목별)
- listInboundByDate — wace purchaseCloseList 매퍼 1:1 (입고일별 + 매입마감)
- listProjectStatus — PO/DLV 통계 0 → 실데이터 (purchase_order_part + arrival_plan)
- INVENTORY_MGMT / INCOMING_INSPECTION 미존재 → 검사/폐기 0 처리 (확정수량=입고수량)
This commit is contained in:
hjjeong
2026-05-15 15:51:22 +09:00
parent 6b029e20f9
commit c2e364207e
3 changed files with 599 additions and 31 deletions
+407 -31
View File
@@ -380,27 +380,336 @@ export async function listProposal(filter: PurchaseListFilter): Promise<ListResu
}
}
// ─── 4) 입고관리 (wace purchaseOrder.xml deliveryMngAcceptanceList) ──
// purchase_order_master + purchase_order_part(누락) + arrival_plan(누락).
// 누락 의존 — purchase_order_master 단독으로 빈 그리드 처리.
// ─── 4) 입고관리 (wace purchaseOrder.xml deliveryMngList_new 매퍼 1:1) ──
//
// 발주서별 1행 (purchase_order_master + S1 집계: PURCHASE_ORDER_PART × ARRIVAL_PLAN).
// 매퍼 본문: wace_plm/src/com/pms/mapper/purchaseOrder.xml:4381-4580
// 검색: year / customer_cd / project_no / purchase_order_no / part_no / part_name / part_spec /
// partner_objid / sales_mng_user_id / delivery_date 범위 / reg_date 범위 / delivery_status
// 검사현황(IID_AGG/DEFECT_AGG)은 INVENTORY/INSPECTION 테이블 미존재 → 0 처리.
export async function listInbound(filter: PurchaseListFilter): Promise<ListResult<any>> {
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<ListResult<any>> {
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<ListResult<any>> {
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<Lis
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
AND MH2.STATUS = 'Y'), 0) AS total_qty,
-- 발주/입고/미발주/미입고 — purchase_order_part / arrival_plan 누락이라 모두 0
0::int AS po_item_cnt,
0::numeric AS po_qty,
COALESCE((SELECT COUNT(DISTINCT MD.PART_OBJID)::int
FROM MBOM_DETAIL MD
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
AND MH2.STATUS = 'Y'), 0) AS non_po_item_cnt,
COALESCE((SELECT SUM(NULLIF(MD.QTY, '')::numeric)
FROM MBOM_DETAIL MD
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
AND MH2.STATUS = 'Y'), 0) AS non_po_qty,
0::int AS dlv_item_cnt,
0::numeric AS dlv_qty,
0::int AS non_dlv_item_cnt,
0::numeric AS non_dlv_qty
-- 발주 통계: PURCHASE_ORDER_PART × POM(STATUS='create' AND MAIL_SEND_DATE IS NOT NULL)
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) AS po_item_cnt,
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) AS po_qty,
-- 미발주 = BOM 품목수 - 발주 품목수 (음수 방지 GREATEST)
GREATEST(
COALESCE((SELECT COUNT(DISTINCT MD.PART_OBJID)::int
FROM MBOM_DETAIL MD
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
AND MH2.STATUS = 'Y'), 0)
- 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)
, 0) AS non_po_item_cnt,
GREATEST(
COALESCE((SELECT SUM(NULLIF(MD.QTY, '')::numeric)
FROM MBOM_DETAIL MD
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
AND MH2.STATUS = 'Y'), 0)
- 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)
, 0) AS non_po_qty,
-- 입고 통계: ARRIVAL_PLAN × POM (RECEIPT_QTY > 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}
@@ -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';
@@ -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);