Refactor Outbound and Work Instruction Controllers for Source Table Updates
- Updated the outbound and outsourcing outbound controllers to replace `source_type` with `source_table` for improved clarity and consistency in data handling. - Enhanced the work instruction controller to include automatic migration for the `work_instruction_info` table, allowing for better management of work instruction notes. - Implemented new logic to handle material input types in the work instruction detail modal, supporting both automatic and manual input methods. - Added new routes for retrieving work instruction information, facilitating better data retrieval for editing purposes. (TASK: ERP-node-095, ERP-node-096)
This commit is contained in:
@@ -3,6 +3,34 @@ import type { Pool, PoolClient } from "pg";
|
||||
import { getPool } from "../database/db";
|
||||
import type { AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||
import logger from "../utils/logger";
|
||||
import {
|
||||
onAcceptCancelled,
|
||||
onProcessAccept,
|
||||
onProcessCompleted,
|
||||
onProcessMove,
|
||||
onResultSaved,
|
||||
} from "../services/wipStockService";
|
||||
|
||||
/**
|
||||
* user_id → user_name(한글명) 조회 헬퍼 — wip_stock_history.manager_name 기록용.
|
||||
* 조회 실패 시 user_id 를 fallback 으로 반환.
|
||||
*/
|
||||
async function resolveUserName(
|
||||
exec: { query: (text: string, values?: any[]) => Promise<any> },
|
||||
userId: string,
|
||||
companyCode: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const r = await exec.query(
|
||||
`SELECT COALESCE(NULLIF(user_name, ''), user_id) AS user_name
|
||||
FROM user_info WHERE user_id = $1 AND company_code = $2 LIMIT 1`,
|
||||
[userId, companyCode],
|
||||
);
|
||||
return r.rows[0]?.user_name || userId;
|
||||
} catch {
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
|
||||
// 불량 상세 항목 타입
|
||||
interface DefectDetailItem {
|
||||
@@ -978,7 +1006,7 @@ export const controlTimer = async (
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
AND status != 'completed'
|
||||
RETURNING id, status, completed_at, completed_by, actual_work_time, good_qty, defect_qty`,
|
||||
RETURNING id, status, completed_at, completed_by, actual_work_time, good_qty, defect_qty, wop_id, equipment_code`,
|
||||
[
|
||||
work_order_process_id,
|
||||
companyCode,
|
||||
@@ -999,6 +1027,26 @@ export const controlTimer = async (
|
||||
});
|
||||
}
|
||||
|
||||
// [WIP 적재 — 트리거 4] 타이머 완료(complete) 시 wip_stock status='completed' 전이
|
||||
if (action === "complete" && result.rows[0]?.wop_id) {
|
||||
try {
|
||||
const row = result.rows[0];
|
||||
const managerName = await resolveUserName(pool, userId, companyCode);
|
||||
await onProcessCompleted(
|
||||
pool,
|
||||
companyCode,
|
||||
row.wop_id,
|
||||
work_order_process_id,
|
||||
userId,
|
||||
row.equipment_code || null,
|
||||
managerName,
|
||||
"타이머 완료(상태 전이)",
|
||||
);
|
||||
} catch (wipErr) {
|
||||
logger.error("[pop/production] WIP 적재 오류 (타이머 완료는 유지):", wipErr);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
@@ -1527,6 +1575,36 @@ export const saveResult = async (req: AuthenticatedRequest, res: Response) => {
|
||||
await checkAndCompleteWorkInstruction(client, csWoId, companyCode, userId);
|
||||
}
|
||||
|
||||
// [WIP 적재 — 트리거 2] 실적 저장분(양품/불량 증분)을 wip_stock 에 반영
|
||||
// SAVEPOINT 로 감싼다: 공유 트랜잭션(client)에서 WIP 쿼리가 throw 하면
|
||||
// PG 가 트랜잭션 전체를 aborted 로 만들어 이후 COMMIT 이 ROLLBACK 처리된다.
|
||||
// SAVEPOINT 까지만 되돌려 트랜잭션을 건강하게 살려 본작업(실적저장)을 보존한다.
|
||||
if ((currentSeq.rowCount ?? 0) > 0) {
|
||||
await client.query("SAVEPOINT wip_save_result");
|
||||
try {
|
||||
const cs = currentSeq.rows[0];
|
||||
const managerName = await resolveUserName(client, userId, companyCode);
|
||||
await onResultSaved(
|
||||
client,
|
||||
companyCode,
|
||||
cs.wop_id,
|
||||
work_order_process_id,
|
||||
addGood,
|
||||
addDefect,
|
||||
userId,
|
||||
cs.equipment_code || null,
|
||||
managerName,
|
||||
);
|
||||
await client.query("RELEASE SAVEPOINT wip_save_result");
|
||||
} catch (wipErr) {
|
||||
// WIP 부분쓰기만 되돌리고 본작업 트랜잭션은 살린다 (데이터는 후속 백필로 보정 가능)
|
||||
await client
|
||||
.query("ROLLBACK TO SAVEPOINT wip_save_result")
|
||||
.catch(() => {});
|
||||
logger.error("[pop/production] WIP 적재 오류 (실적 저장은 유지):", wipErr);
|
||||
}
|
||||
}
|
||||
|
||||
const latestData = await client.query(
|
||||
`SELECT id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail,
|
||||
result_note, result_status, status, input_qty,
|
||||
@@ -1808,7 +1886,7 @@ export const confirmResult = async (
|
||||
writer = $3,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
RETURNING id, status, result_status, total_production_qty, good_qty, defect_qty, wop_id`,
|
||||
RETURNING id, status, result_status, total_production_qty, good_qty, defect_qty, wop_id, equipment_code`,
|
||||
[work_order_process_id, companyCode, userId],
|
||||
);
|
||||
|
||||
@@ -1819,6 +1897,23 @@ export const confirmResult = async (
|
||||
});
|
||||
}
|
||||
|
||||
// [WIP 적재 — 트리거 3] 실적 확정 시 wip_stock status='completed' 전이
|
||||
try {
|
||||
const managerName = await resolveUserName(pool, userId, companyCode);
|
||||
await onProcessCompleted(
|
||||
pool,
|
||||
companyCode,
|
||||
result.rows[0].wop_id,
|
||||
work_order_process_id,
|
||||
userId,
|
||||
result.rows[0].equipment_code || null,
|
||||
managerName,
|
||||
"실적 확정(상태 전이)",
|
||||
);
|
||||
} catch (wipErr) {
|
||||
logger.error("[pop/production] WIP 적재 오류 (실적 확정은 유지):", wipErr);
|
||||
}
|
||||
|
||||
// 작업지시 완료 캐스케이드
|
||||
const wopLookup = await pool.query(
|
||||
`SELECT wo_id FROM work_order_process WHERE id = $1 AND company_code = $2`,
|
||||
@@ -2358,6 +2453,63 @@ export const acceptProcess = async (
|
||||
{ skipAStrategy: true },
|
||||
);
|
||||
|
||||
// [WIP 적재 — 트리거 1·6] 공정 접수 시 wip_stock UPSERT + 직전 공정 이동 반영.
|
||||
// 공유 트랜잭션(client) 이므로 WIP 쿼리가 throw 하면 트랜잭션 전체가 aborted 되어
|
||||
// 이후 COMMIT 이 ROLLBACK 처리된다 → SAVEPOINT 로 감싸 본작업(접수)을 보존한다.
|
||||
// 트리거1·6 을 각각 별도 SAVEPOINT 로 감싸 트리거6 실패가 트리거1 적재를 되돌리지 않게 한다.
|
||||
const wipManagerName = await resolveUserName(client, userId, companyCode);
|
||||
|
||||
// 트리거 1: 이번 공정 접수분 적재 (input_qty 누적, status='in_progress')
|
||||
await client.query("SAVEPOINT wip_accept");
|
||||
try {
|
||||
await onProcessAccept(
|
||||
client,
|
||||
companyCode,
|
||||
masterId,
|
||||
inserted.id,
|
||||
qty,
|
||||
userId,
|
||||
req.body.equipment_code || null,
|
||||
wipManagerName,
|
||||
);
|
||||
await client.query("RELEASE SAVEPOINT wip_accept");
|
||||
} catch (wipErr) {
|
||||
await client.query("ROLLBACK TO SAVEPOINT wip_accept").catch(() => {});
|
||||
logger.error("[pop/production] WIP 적재 오류 (공정 접수는 유지):", wipErr);
|
||||
}
|
||||
|
||||
// 트리거 6: 후속 공정(seq>최소) 접수면 직전 공정 wip_stock 행에 이동 반영
|
||||
if (seqNum > 1) {
|
||||
await client.query("SAVEPOINT wip_move");
|
||||
try {
|
||||
const prevWopRes = await client.query(
|
||||
`SELECT id FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2
|
||||
AND NULLIF(regexp_replace(COALESCE(seq_no, ''), '[^0-9-]', '', 'g'), '')::int < $3
|
||||
ORDER BY NULLIF(regexp_replace(COALESCE(seq_no, ''), '[^0-9-]', '', 'g'), '')::int DESC
|
||||
LIMIT 1`,
|
||||
[row.wo_id, companyCode, seqNum],
|
||||
);
|
||||
if ((prevWopRes.rowCount ?? 0) > 0) {
|
||||
await onProcessMove(
|
||||
client,
|
||||
companyCode,
|
||||
prevWopRes.rows[0].id,
|
||||
masterId,
|
||||
inserted.id,
|
||||
qty,
|
||||
userId,
|
||||
req.body.equipment_code || null,
|
||||
wipManagerName,
|
||||
);
|
||||
}
|
||||
await client.query("RELEASE SAVEPOINT wip_move");
|
||||
} catch (wipErr) {
|
||||
await client.query("ROLLBACK TO SAVEPOINT wip_move").catch(() => {});
|
||||
logger.error("[pop/production] WIP 이동 적재 오류 (공정 접수는 유지):", wipErr);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("[pop/production] accept-process 접수 완료", {
|
||||
@@ -2420,7 +2572,7 @@ export const cancelAccept = async (
|
||||
|
||||
const current = await pool.query(
|
||||
`SELECT wr.id, wr.status, wr.input_qty, wr.total_production_qty, wr.result_status,
|
||||
wr.wop_id, wr.good_qty, wr.concession_qty,
|
||||
wr.wop_id, wr.good_qty, wr.concession_qty, wr.equipment_code,
|
||||
wop.wo_id, wop.seq_no, wop.process_name,
|
||||
wop.target_warehouse_id, wop.target_location_code
|
||||
FROM work_order_process_result wr
|
||||
@@ -2538,6 +2690,34 @@ export const cancelAccept = async (
|
||||
}
|
||||
}
|
||||
|
||||
// [WIP 적재 — 트리거 5] 접수 취소 시 wip_stock 미소진 접수분 롤백.
|
||||
// 공유 트랜잭션(client) 이므로 WIP 쿼리 throw 시 트랜잭션 전체가 aborted 되어
|
||||
// 이후 COMMIT 이 ROLLBACK 처리된다 → SAVEPOINT 로 감싸 본작업(접수 취소)을 보존한다.
|
||||
await client.query("SAVEPOINT wip_cancel");
|
||||
try {
|
||||
const managerName = await resolveUserName(client, userId, companyCode);
|
||||
await onAcceptCancelled(
|
||||
client,
|
||||
companyCode,
|
||||
proc.wop_id,
|
||||
work_order_process_id,
|
||||
cancelledQty,
|
||||
userId,
|
||||
totalProduced === 0, // 실적 0 → result 행 전체 삭제(fullRemove)
|
||||
proc.equipment_code || null,
|
||||
managerName,
|
||||
);
|
||||
await client.query("RELEASE SAVEPOINT wip_cancel");
|
||||
} catch (wipErr) {
|
||||
await client
|
||||
.query("ROLLBACK TO SAVEPOINT wip_cancel")
|
||||
.catch(() => {});
|
||||
logger.error(
|
||||
"[pop/production] WIP 적재 오류 (접수 취소는 유지):",
|
||||
wipErr,
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (txErr) {
|
||||
await client.query("ROLLBACK");
|
||||
|
||||
Reference in New Issue
Block a user