// ============================================================ // 자재관리 — 자재리스트 + 불출의뢰서 서비스 // wace_plm inventoryMng.xml + InventoryMngService.java 1:1 베이스 // // 테이블: // inventory_mgmt (자재 마스터: contract_objid+unit+part_objid PK, objid는 별도) // inventory_mgmt_in (입고/이동 라인: parent_objid → inventory_mgmt.objid) // inventory_mgmt_out (불출 라인: parent_objid → inventory_mgmt.objid, // inventory_request_master_objid → inventory_mgmt_out_master.objid) // inventory_mgmt_out_master (불출의뢰 마스터: inventory_out_no = Rfw-YYYY-seq) // inventory_mgmt_history (자재 투입 이력 — 본 메뉴에서는 조회용) // // 보유수량 공식: // USE_CNT = SUM(receipt_qty - move_qty) FROM inventory_mgmt_in WHERE parent=IM.objid // - SUM(request_qty) FROM inventory_mgmt_out WHERE parent=IM.objid // // 불출 흐름: 의뢰(request_qty) → 접수(reception_status=reception) → 불출(out_qty, outstatus=complete) // ============================================================ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; import { createObjId } from "../utils/objidUtil"; export interface InventoryListFilter { project_objid?: string; unit_code?: string; part_no?: string; part_name?: string; part_type?: string; location?: string; cls_cd?: string; cau_cd?: string; writer?: string; page?: number; page_size?: number; } export interface IssueRequestFilter { part_no?: string; part_name?: string; request_start_date?: string; request_end_date?: string; request_user?: string; reception_status?: string; reception_user?: string; reception_start_date?: string; reception_end_date?: string; out_status?: string; page?: number; page_size?: number; } interface ListResult { rows: T[]; totalCount: number; page: number; pageSize: number; } function paging(f: { page?: number; page_size?: number }) { const page = Math.max(1, Number(f.page ?? 1)); const pageSize = Math.max(1, Math.min(500, Number(f.page_size ?? 50))); return { page, pageSize, limit: pageSize, offset: (page - 1) * pageSize }; } // ─── 자재리스트 그리드 ───────────────────────────────────────── // wace `inventoryMngNewGridList` 매퍼 1:1 베이스 // USE_CNT = inbound − move − out_request (각 부모 행 단위 합산) // USE_CNT_ALL = 같은 part_objid 전체 가상 합계 (윈도우 함수) // REQUEST_QTY = 누적 불출의뢰 수량 // LOCATION_NAME = inventory_mgmt_in.location distinct 목록 (콤마) export async function listInventory(filter: InventoryListFilter): Promise> { const pool = getPool(); const { page, pageSize, limit, offset } = paging(filter); const where: string[] = []; const params: any[] = []; const addP = (v: any) => { params.push(v); return `$${params.length}`; }; if (filter.project_objid) where.push(`IM.contract_objid = ${addP(filter.project_objid)}`); if (filter.unit_code) where.push(`IM.unit = ${addP(filter.unit_code)}`); if (filter.part_no) where.push(`P.part_no ILIKE ${addP(`%${filter.part_no}%`)}`); if (filter.part_name) where.push(`P.part_name ILIKE ${addP(`%${filter.part_name}%`)}`); if (filter.part_type) where.push(`P.part_type = ${addP(filter.part_type)}`); if (filter.location) where.push(`IM.location = ${addP(filter.location)}`); if (filter.cls_cd) where.push(`IM.cls_cd = ${addP(filter.cls_cd)}`); if (filter.cau_cd) where.push(`IM.cau_cd = ${addP(filter.cau_cd)}`); if (filter.writer) where.push(`IM.writer = ${addP(filter.writer)}`); const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : ""; const dataSql = ` WITH IN_SUM AS ( SELECT parent_objid, SUM(COALESCE(NULLIF(receipt_qty,'')::numeric,0) - COALESCE(NULLIF(move_qty,'')::numeric,0)) AS in_qty FROM inventory_mgmt_in GROUP BY parent_objid ), OUT_SUM AS ( SELECT parent_objid, SUM(COALESCE(NULLIF(request_qty,'')::numeric,0)) AS req_qty, SUM(COALESCE(NULLIF(out_qty,'')::numeric,0)) AS out_qty FROM inventory_mgmt_out GROUP BY parent_objid ), LOC_LIST AS ( SELECT parent_objid, STRING_AGG(DISTINCT NULLIF(location,''), ',') AS loc_names FROM inventory_mgmt_in GROUP BY parent_objid ) SELECT IM.objid AS objid, IM.contract_objid AS contract_objid, COALESCE(PJ.project_no, CT.contract_no, '') AS project_no, IM.unit AS unit, COALESCE(WT.task_name, '') AS unit_name, IM.part_objid AS part_objid, P.part_no AS part_no, P.part_name AS part_name, P.material AS material, P.spec AS spec, P.part_type AS part_type, P.part_type AS part_type_name, IM.cls_cd AS cls_cd, IM.cau_cd AS cau_cd, IM.location AS location, IM.sub_location AS sub_location, COALESCE(LL.loc_names, IM.location, '') AS location_name, IM.reg_date AS reg_date, IM.price AS price, IM.writer AS writer, COALESCE(user_name(IM.writer), IM.writer, '') AS writer_name, COALESCE(IS_.in_qty, 0) - COALESCE(OS.req_qty, 0) AS use_cnt, COALESCE(OS.req_qty, 0) AS request_qty, COALESCE(OS.out_qty, 0) AS out_qty_total, SUM(COALESCE(IS_.in_qty, 0) - COALESCE(OS.req_qty, 0)) OVER (PARTITION BY IM.part_objid) AS use_cnt_all, '' AS remark FROM inventory_mgmt IM LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid LEFT JOIN contract_mgmt CT ON CT.objid = IM.contract_objid LEFT JOIN project_mgmt PJ ON PJ.contract_objid = IM.contract_objid LEFT JOIN pms_wbs_task WT ON WT.contract_objid = IM.contract_objid AND WT.task_seq = IM.unit LEFT JOIN IN_SUM IS_ ON IS_.parent_objid = IM.objid LEFT JOIN OUT_SUM OS ON OS.parent_objid = IM.objid LEFT JOIN LOC_LIST LL ON LL.parent_objid = IM.objid ${whereSql} ORDER BY IM.reg_date DESC NULLS LAST, IM.objid DESC LIMIT ${addP(limit)} OFFSET ${addP(offset)} `; const countSql = ` SELECT COUNT(*)::int AS cnt FROM inventory_mgmt IM LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid ${whereSql} `; 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("listInventory 실패", { error: e.message }); return { rows: [], totalCount: 0, page, pageSize }; } } // ─── 재고 등록 (자재 신규/추가입고) ─────────────────────────── // 1) inventory_mgmt: (contract_objid+unit+part_objid) UNIQUE → 존재 시 재사용, 없으면 INSERT // 2) inventory_mgmt_in: 입고 1건 INSERT export interface SaveInventoryInput { project_objid: string; // contract_objid unit?: string; part_objid: string; qty: number; price?: string; location: string; sub_location?: string; cls_cd?: string; cau_cd?: string; receipt_date?: string; writer?: string; order_objid?: string; } export async function saveInventory(input: SaveInventoryInput): Promise<{ objid: string; in_objid: string }> { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const probe = await client.query( `SELECT objid FROM inventory_mgmt WHERE contract_objid = $1 AND unit = $2 AND part_objid = $3 LIMIT 1`, [input.project_objid, input.unit ?? "", input.part_objid], ); let objid = probe.rows[0]?.objid as string | undefined; if (!objid) { objid = createObjId(); await client.query( `INSERT INTO inventory_mgmt (objid, contract_objid, unit, part_objid, cls_cd, cau_cd, qty, location, sub_location, reg_date, price, writer) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9, TO_CHAR(NOW(),'YYYY-MM-DD'), $10, $11)`, [objid, input.project_objid, input.unit ?? "", input.part_objid, input.cls_cd ?? "", input.cau_cd ?? "", String(input.qty), input.location, input.sub_location ?? "", input.price ?? "", input.writer ?? ""], ); } const inObjid = createObjId(); await client.query( `INSERT INTO inventory_mgmt_in (objid, parent_objid, receipt_qty, location, sub_location, writer, regdate, contract_mgmt_objid, purchase_order_master_objid, receipt_date) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9)`, [inObjid, objid, String(input.qty), input.location, input.sub_location ?? "", input.writer ?? "", input.project_objid, input.order_objid ?? "", input.receipt_date ?? new Date().toISOString().slice(0, 10)], ); await client.query("COMMIT"); return { objid, in_objid: inObjid }; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 재고 삭제 ───────────────────────────────────────────── export async function deleteInventory(objidArr: string[]): Promise<{ deleted: number }> { if (!objidArr.length) return { deleted: 0 }; const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); await client.query(`DELETE FROM inventory_mgmt_in WHERE parent_objid = ANY($1::varchar[])`, [objidArr]); await client.query(`DELETE FROM inventory_mgmt_out WHERE parent_objid = ANY($1::varchar[])`, [objidArr]); const r = await client.query(`DELETE FROM inventory_mgmt WHERE objid = ANY($1::varchar[])`, [objidArr]); await client.query("COMMIT"); return { deleted: r.rowCount ?? 0 }; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 자재이동: inventory_mgmt_in 행에 move_qty/move_date/move_user 누적 ── export interface MoveInventoryInput { in_objid: string; move_qty: number; location?: string; sub_location?: string; move_date?: string; move_user?: string; writer?: string; } export async function moveInventoryBulk(items: MoveInventoryInput[]): Promise<{ updated: number }> { if (!items.length) return { updated: 0 }; const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); for (const it of items) { const prev = await client.query( `SELECT COALESCE(NULLIF(move_qty,''),'0')::numeric AS mq, parent_objid FROM inventory_mgmt_in WHERE objid = $1`, [it.in_objid], ); const parent = prev.rows[0]?.parent_objid as string | undefined; if (!parent) continue; const newMove = Number(prev.rows[0].mq) + Number(it.move_qty); await client.query( `UPDATE inventory_mgmt_in SET move_qty = $1, move_date = COALESCE($2, move_date), move_user = COALESCE($3, move_user) WHERE objid = $4`, [String(newMove), it.move_date ?? null, it.move_user ?? null, it.in_objid], ); // 이동 이력 라인 추가 await client.query( `INSERT INTO inventory_mgmt_in (objid, parent_objid, receipt_qty, location, sub_location, writer, regdate, move_qty, move_date, move_user) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9)`, [createObjId(), parent, String(it.move_qty), it.location ?? "", it.sub_location ?? "", it.writer ?? it.move_user ?? "", String(it.move_qty), it.move_date ?? null, it.move_user ?? null], ); } await client.query("COMMIT"); return { updated: items.length }; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 자재 입출고 이력 ───────────────────────────────────────── export async function getInventoryHistory(parentObjid: string): Promise { const pool = getPool(); const sql = ` -- 입고 SELECT II.objid AS objid, P.part_no AS part_no, P.part_name AS part_name, '입고' AS gubun, COALESCE(II.receipt_qty, '') AS qty, II.location AS location_name, II.sub_location AS sub_location_name, II.regdate AS regdate, II.writer AS writer, COALESCE(user_name(II.writer), II.writer,'') AS writer_name FROM inventory_mgmt_in II LEFT JOIN inventory_mgmt IM ON IM.objid = II.parent_objid LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE II.parent_objid = $1 AND COALESCE(II.move_date, '') = '' UNION ALL -- 이동 SELECT II.objid, P.part_no, P.part_name, '이동', COALESCE(II.move_qty, II.receipt_qty, ''), II.location, II.sub_location, II.regdate, II.move_user, COALESCE(user_name(II.move_user), II.move_user, '') FROM inventory_mgmt_in II LEFT JOIN inventory_mgmt IM ON IM.objid = II.parent_objid LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE II.parent_objid = $1 AND COALESCE(II.move_date, '') <> '' UNION ALL -- 출고 SELECT IO.objid, P.part_no, P.part_name, '출고', COALESCE(IO.out_qty, IO.request_qty, ''), '', '', IO.regdate, IO.writer, COALESCE(user_name(IO.writer), IO.writer, '') FROM inventory_mgmt_out IO LEFT JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.parent_objid = $1 ORDER BY regdate DESC NULLS LAST `; const r = await pool.query(sql, [parentObjid]); return r.rows; } // ─── 불출의뢰서 그리드 ───────────────────────────────────────── export async function listIssueRequest(filter: IssueRequestFilter): Promise> { const pool = getPool(); const { page, pageSize, limit, offset } = paging(filter); const where: string[] = []; const params: any[] = []; const addP = (v: any) => { params.push(v); return `$${params.length}`; }; if (filter.request_user) where.push(`OM.request_id = ${addP(filter.request_user)}`); if (filter.reception_user) where.push(`OM.reception_id = ${addP(filter.reception_user)}`); if (filter.reception_status) { if (filter.reception_status === "reception") where.push(`OM.reception_status = 'reception'`); else if (filter.reception_status === "AA") where.push(`COALESCE(OM.reception_status,'') <> 'reception'`); } if (filter.out_status) { if (filter.out_status === "complete") where.push(`OM.outstatus = 'complete'`); else if (filter.out_status === "NG") where.push(`COALESCE(OM.outstatus,'') <> 'complete'`); } if (filter.request_start_date) where.push(`OM.request_date >= ${addP(filter.request_start_date)}`); if (filter.request_end_date) where.push(`OM.request_date <= ${addP(filter.request_end_date)}`); if (filter.reception_start_date) where.push(`OM.reception_date >= ${addP(filter.reception_start_date)}`); if (filter.reception_end_date) where.push(`OM.reception_date <= ${addP(filter.reception_end_date)}`); if (filter.part_no) { where.push(`EXISTS ( SELECT 1 FROM inventory_mgmt_out IO JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.inventory_request_master_objid = OM.objid AND P.part_no ILIKE ${addP(`%${filter.part_no}%`)} )`); } if (filter.part_name) { where.push(`EXISTS ( SELECT 1 FROM inventory_mgmt_out IO JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.inventory_request_master_objid = OM.objid AND P.part_name ILIKE ${addP(`%${filter.part_name}%`)} )`); } const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : ""; const dataSql = ` SELECT OM.objid AS objid, OM.inventory_out_no AS inventory_out_no, OM.request_date AS request_date, OM.request_id AS request_id, COALESCE(user_name(OM.request_id), OM.request_id,'') AS request_user_name, OM.reception_status AS reception_status, CASE WHEN OM.reception_status='reception' THEN '접수' ELSE '미접수' END AS reception_status_title, OM.reception_id AS reception_id, COALESCE(user_name(OM.reception_id), OM.reception_id,'') AS reception_user_name, OM.reception_date AS reception_date, OM.outstatus AS outstatus, CASE WHEN OM.outstatus='complete' THEN '완료' ELSE '미완료' END AS outstatus_title, OM.remark AS remark, OM.writer AS writer, OM.regdate AS regdate, COALESCE(( SELECT STRING_AGG(P.part_no, ',' ORDER BY P.part_no) FROM inventory_mgmt_out IO JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.inventory_request_master_objid = OM.objid ), '') AS part_no_arr, COALESCE(( SELECT STRING_AGG(P.part_name, ',' ORDER BY P.part_no) FROM inventory_mgmt_out IO JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.inventory_request_master_objid = OM.objid ), '') AS part_name_arr, COALESCE(( SELECT SUM(COALESCE(NULLIF(IO.request_qty,'')::numeric,0)) FROM inventory_mgmt_out IO WHERE IO.inventory_request_master_objid = OM.objid ), 0) AS request_qty_total, COALESCE(( SELECT SUM(COALESCE(NULLIF(IO.out_qty,'')::numeric,0)) FROM inventory_mgmt_out IO WHERE IO.inventory_request_master_objid = OM.objid ), 0) AS out_qty_total FROM inventory_mgmt_out_master OM ${whereSql} ORDER BY OM.regdate DESC NULLS LAST, OM.objid DESC LIMIT ${addP(limit)} OFFSET ${addP(offset)} `; const countSql = `SELECT COUNT(*)::int AS cnt FROM inventory_mgmt_out_master OM ${whereSql}`; 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("listIssueRequest 실패", { error: e.message }); return { rows: [], totalCount: 0, page, pageSize }; } } // ─── 불출의뢰 상세: 마스터 + 라인 ──────────────────────────── export async function getIssueRequest(masterObjid: string): Promise<{ master: any; lines: any[] }> { const pool = getPool(); const master = await pool.query( `SELECT OM.*, COALESCE(user_name(OM.request_id), OM.request_id, '') AS request_user_name, COALESCE(user_name(OM.reception_id),OM.reception_id,'') AS reception_user_name FROM inventory_mgmt_out_master OM WHERE OM.objid = $1`, [masterObjid], ); const lines = await pool.query( `SELECT IO.objid AS objid, IO.parent_objid AS parent_objid, IO.request_qty AS request_qty, IO.out_qty AS out_qty, IO.out_date AS out_date, IO.writer AS writer, COALESCE(user_name(IO.writer), IO.writer, '') AS writer_name, IO.acq_user AS acq_user, COALESCE(user_name(IO.acq_user), IO.acq_user, '') AS acq_user_name, IO.sign AS sign, IO.unit AS unit, IO.contract_mgmt_objid AS contract_mgmt_objid, IM.location AS location, IM.sub_location AS sub_location, IM.part_objid AS part_objid, P.part_no AS part_no, P.part_name AS part_name, P.material AS material, P.spec AS spec, P.part_type AS part_type FROM inventory_mgmt_out IO LEFT JOIN inventory_mgmt IM ON IM.objid = IO.parent_objid LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid WHERE IO.inventory_request_master_objid = $1 ORDER BY P.part_no NULLS LAST, IO.objid`, [masterObjid], ); return { master: master.rows[0] ?? null, lines: lines.rows }; } // ─── 불출 가능 자재 후보 (자재리스트에서 선택한 OBJID들 기반) ──── export async function getIssueRequestCandidates(parentObjids: string[]): Promise { if (!parentObjids.length) return []; const pool = getPool(); const r = await pool.query( `SELECT IM.objid AS objid, IM.contract_objid AS contract_objid, COALESCE(PJ.project_no, CT.contract_no, '') AS project_no, IM.unit AS unit, COALESCE(WT.task_name, '') AS unit_name, P.part_no AS part_no, P.part_name AS part_name, P.material AS material, P.spec AS spec, P.part_type AS part_type_name, IM.location AS location, IM.sub_location AS sub_location, ( COALESCE((SELECT SUM( COALESCE(NULLIF(receipt_qty,'')::numeric,0) - COALESCE(NULLIF(move_qty,'')::numeric,0)) FROM inventory_mgmt_in WHERE parent_objid = IM.objid), 0) - COALESCE((SELECT SUM(COALESCE(NULLIF(request_qty,'')::numeric,0)) FROM inventory_mgmt_out WHERE parent_objid = IM.objid), 0) ) AS use_cnt FROM inventory_mgmt IM LEFT JOIN part_mng P ON P.objid::varchar = IM.part_objid LEFT JOIN contract_mgmt CT ON CT.objid = IM.contract_objid LEFT JOIN project_mgmt PJ ON PJ.contract_objid = IM.contract_objid LEFT JOIN pms_wbs_task WT ON WT.contract_objid = IM.contract_objid AND WT.task_seq = IM.unit WHERE IM.objid = ANY($1::varchar[])`, [parentObjids], ); return r.rows; } // ─── 불출의뢰 생성/수정 ─────────────────────────────────────── export interface IssueRequestSaveInput { master_objid?: string; // 수정 시 기존 키 contract_mgmt_objid?: string; request_date?: string; request_id?: string; remark?: string; writer: string; lines: Array<{ parent_objid: string; request_qty: number; unit?: string; }>; } export async function saveIssueRequest(input: IssueRequestSaveInput): Promise<{ master_objid: string; inventory_out_no: string }> { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const today = new Date().toISOString().slice(0, 10); let masterObjid = input.master_objid; let inventoryOutNo = ""; if (masterObjid) { await client.query( `UPDATE inventory_mgmt_out_master SET remark = COALESCE($1, remark), request_date = COALESCE($2, request_date), contract_mgmt_objid = COALESCE($3, contract_mgmt_objid) WHERE objid = $4`, [input.remark ?? null, input.request_date ?? today, input.contract_mgmt_objid ?? null, masterObjid], ); const m = await client.query( `SELECT inventory_out_no FROM inventory_mgmt_out_master WHERE objid = $1`, [masterObjid], ); inventoryOutNo = m.rows[0]?.inventory_out_no ?? ""; await client.query( `DELETE FROM inventory_mgmt_out WHERE inventory_request_master_objid = $1`, [masterObjid], ); } else { masterObjid = createObjId(); const seq = await client.query( `SELECT 'Rfw-' || TO_CHAR(NOW(),'YYYY') || '-' || (COALESCE(MAX(NULLIF(SPLIT_PART(inventory_out_no, '-', 3),'')::int), 0) + 1)::text AS no FROM inventory_mgmt_out_master WHERE inventory_out_no LIKE 'Rfw-' || TO_CHAR(NOW(),'YYYY') || '-%'`, ); inventoryOutNo = seq.rows[0]?.no ?? `Rfw-${new Date().getFullYear()}-1`; await client.query( `INSERT INTO inventory_mgmt_out_master (objid, inventory_out_no, request_date, request_id, writer, regdate, remark, contract_mgmt_objid) VALUES ($1, $2, $3, $4, $5, NOW(), $6, $7)`, [masterObjid, inventoryOutNo, input.request_date ?? today, input.request_id ?? input.writer, input.writer, input.remark ?? "", input.contract_mgmt_objid ?? ""], ); } for (const ln of input.lines) { if (!ln.parent_objid || !ln.request_qty || ln.request_qty <= 0) continue; const ioObjid = createObjId(); await client.query( `INSERT INTO inventory_mgmt_out (objid, parent_objid, request_qty, writer, regdate, inventory_request_master_objid, contract_mgmt_objid, unit) VALUES ($1, $2, $3, $4, NOW(), $5, $6, $7)`, [ioObjid, ln.parent_objid, String(ln.request_qty), input.writer, masterObjid, input.contract_mgmt_objid ?? "", ln.unit ?? ""], ); } await client.query("COMMIT"); return { master_objid: masterObjid!, inventory_out_no: inventoryOutNo }; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 불출의뢰 삭제 ───────────────────────────────────────── export async function deleteIssueRequest(masterObjid: string): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); await client.query(`DELETE FROM inventory_mgmt_out WHERE inventory_request_master_objid = $1`, [masterObjid]); await client.query(`DELETE FROM inventory_mgmt_out_master WHERE objid = $1`, [masterObjid]); await client.query("COMMIT"); } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 접수 처리 ──────────────────────────────────────────── export async function receiveIssueRequest(objids: string[], receptionId: string): Promise<{ updated: number }> { if (!objids.length) return { updated: 0 }; const pool = getPool(); const today = new Date().toISOString().slice(0, 10); const r = await pool.query( `UPDATE inventory_mgmt_out_master SET reception_status = 'reception', reception_date = $1, reception_id = $2 WHERE objid = ANY($3::varchar[]) AND COALESCE(reception_status,'') <> 'reception'`, [today, receptionId, objids], ); return { updated: r.rowCount ?? 0 }; } // ─── 자재불출 처리 (실제 불출 + 재고 차감) ────────────────── export interface DispatchInput { master_objid: string; lines: Array<{ objid: string; // inventory_mgmt_out.objid out_qty: number; out_date: string; acq_user: string; sign?: string; writer?: string; }>; } export async function dispatchIssueRequest(input: DispatchInput): Promise<{ updated: number }> { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); for (const ln of input.lines) { await client.query( `UPDATE inventory_mgmt_out SET out_qty = $1, out_date = $2, acq_user = $3, sign = COALESCE($4, sign), writer = COALESCE($5, writer) WHERE objid = $6`, [String(ln.out_qty), ln.out_date, ln.acq_user, ln.sign ?? null, ln.writer ?? null, ln.objid], ); } await client.query( `UPDATE inventory_mgmt_out_master SET outstatus = 'complete' WHERE objid = $1`, [input.master_objid], ); await client.query("COMMIT"); return { updated: input.lines.length }; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } // ─── 옵션: 프로젝트 / 유닛 / Location / 사용자 ────────────── export async function listProjectOptions(): Promise> { const pool = getPool(); const r = await pool.query( `SELECT DISTINCT PM.contract_objid AS code, COALESCE(PM.project_no, CT.contract_no, PM.contract_objid) AS label FROM project_mgmt PM LEFT JOIN contract_mgmt CT ON CT.objid = PM.contract_objid WHERE PM.contract_objid IS NOT NULL ORDER BY label`, ); return r.rows; } export async function listUnitOptions(contractObjid: string): Promise> { if (!contractObjid) return []; const pool = getPool(); const r = await pool.query( `SELECT task_seq AS code, task_name AS label FROM pms_wbs_task WHERE contract_objid = $1 AND COALESCE(task_seq,'') <> '' ORDER BY task_seq`, [contractObjid], ); return r.rows; } export async function listUserOptions(): Promise> { const pool = getPool(); const r = await pool.query( `SELECT user_id AS code, COALESCE(user_name, user_id) AS label FROM user_info WHERE COALESCE(status,'') <> 'inactive' ORDER BY label`, ); return r.rows; } export async function listPartOptions(keyword: string, limit: number = 30): Promise> { const pool = getPool(); const kw = keyword?.trim() ?? ""; const params: any[] = []; const conds: string[] = []; if (kw) { params.push(`%${kw}%`); conds.push(`(part_no ILIKE $1 OR part_name ILIKE $1)`); } params.push(limit); const r = await pool.query( `SELECT objid::varchar AS code, part_no AS label, part_name FROM part_mng ${conds.length ? `WHERE ${conds.join(" AND ")}` : ""} ORDER BY part_no LIMIT $${params.length}`, params, ); return r.rows; }