Files
wace_rps/backend-node/src/services/inventoryMngService.ts
T
hjjeong aacbb62ad8 자재관리 2메뉴 풀-CRUD + 액션 (자재리스트 + 불출의뢰서)
- 신규 테이블 5종 (운영 11133 → RPS 11134 DDL 1:1):
  inventory_mgmt / inventory_mgmt_in / inventory_mgmt_out /
  inventory_mgmt_out_master / inventory_mgmt_history
- 백엔드 /api/inventory-mng — 리스트·재고등록·자재이동·삭제·이력 +
  불출의뢰 생성·접수·자재불출(재고 차감)·삭제. 채번 Rfw-YYYY-seq.
- 프론트 /COMPANY_16/material/{list, issue-request} +
  StockRegister / MaterialMove / IssueRequestCreate /
  InventoryHistory / IssueDispatch 다이얼로그 5종.
- AdminPageRenderer 등록 + /material/ prefix.
2026-05-19 11:25:15 +09:00

768 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 자재관리 — 자재리스트 + 불출의뢰서 서비스
// 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<T> {
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<ListResult<any>> {
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<any[]> {
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<ListResult<any>> {
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<any[]> {
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<void> {
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<Array<{ code: string; label: string }>> {
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<Array<{ code: string; label: string }>> {
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<Array<{ code: string; label: string }>> {
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<Array<{ code: string; label: string; part_name: string }>> {
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;
}