aacbb62ad8
- 신규 테이블 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.
768 lines
32 KiB
TypeScript
768 lines
32 KiB
TypeScript
// ============================================================
|
||
// 자재관리 — 자재리스트 + 불출의뢰서 서비스
|
||
// 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;
|
||
}
|