Enhance Work Instruction and Production Plan Functionality

- Added automatic migration to include a new column `batch_use` in the `item_info` table, allowing for batch usage management.
- Implemented logic to prevent deletion of work instructions that are in progress or completed, ensuring data integrity.
- Enhanced the `getBomBaseQtyMap` function to return batch usage status for items, defaulting to 'Y' if not specified.
- Introduced warnings for overdue items and insufficient production time in the production plan management, allowing users to proceed with caution.

(TASK: ERP-node-074, ERP-node-075, ERP-node-076)
This commit is contained in:
kjs
2026-05-19 16:12:44 +09:00
parent 6731ca4183
commit ffd5ffc4c0
14 changed files with 387 additions and 39 deletions

View File

@@ -25,6 +25,19 @@ async function ensureDetailRoutingColumn() {
} catch { /* 이미 존재하거나 권한 문제 시 무시 */ }
}
// 자동 마이그레이션: item_info에 batch_use(배치사용여부) 컬럼 추가 (TASK:ERP-node-074)
// 'Y'=사용(현행 유지, 기본) / 'N'=미사용(작업지시 자동 배치분할 안 함)
let _batchUseMigrationDone = false;
async function ensureItemInfoBatchUseColumn() {
if (_batchUseMigrationDone) return;
try {
const pool = getPool();
await pool.query("ALTER TABLE item_info ADD COLUMN IF NOT EXISTS batch_use VARCHAR(1) DEFAULT 'Y'");
await pool.query("UPDATE item_info SET batch_use = 'Y' WHERE batch_use IS NULL OR batch_use = ''");
_batchUseMigrationDone = true;
} catch { /* 이미 존재하거나 권한 문제 시 무시 */ }
}
// ─── 작업지시 목록 조회 (detail 기준 행 반환) ───
export async function getList(req: AuthenticatedRequest, res: Response) {
try {
@@ -361,6 +374,31 @@ export async function remove(req: AuthenticatedRequest, res: Response) {
if (!ids || ids.length === 0) return res.status(400).json({ success: false, message: "삭제할 항목을 선택해주세요" });
const pool = getPool();
// 진행중/완료 작업지시는 삭제 불가 (데이터 무결성 가드, TASK:ERP-node-075)
// progress_status: in_progress/completed → 차단. NULL이라도 실적(completed_qty)이 있으면 진행으로 간주.
const guard = await pool.query(
`SELECT work_instruction_no,
progress_status,
CASE WHEN completed_qty ~ '^[0-9]+(\\.[0-9]+)?$' THEN completed_qty::numeric ELSE 0 END AS completed_qty
FROM work_instruction
WHERE id = ANY($1) AND company_code = $2`,
[ids, companyCode]
);
const blocked = guard.rows.filter((r: any) => {
const ps = String(r.progress_status || "").toLowerCase();
if (ps === "in_progress" || ps === "completed") return true;
if (!ps || ps === "pending") return Number(r.completed_qty) > 0;
return false;
});
if (blocked.length > 0) {
const nos = blocked.map((r: any) => r.work_instruction_no).filter(Boolean).join(", ");
return res.status(409).json({
success: false,
message: `진행중이거나 완료된 작업지시는 삭제할 수 없습니다.${nos ? ` (${nos})` : ""}`,
});
}
const client = await pool.connect();
try {
await client.query("BEGIN");
@@ -1008,8 +1046,9 @@ export async function getBomBaseQtyMap(req: AuthenticatedRequest, res: Response)
try {
const companyCode = req.user!.companyCode;
const itemCodes: string[] = Array.isArray(req.body?.itemCodes) ? req.body.itemCodes.filter(Boolean) : [];
if (itemCodes.length === 0) return res.json({ success: true, data: {} });
if (itemCodes.length === 0) return res.json({ success: true, data: {}, batchUse: {} });
await ensureItemInfoBatchUseColumn();
const pool = getPool();
// bom.item_code 우선 매칭, 없으면 item_info.id 경유 매칭
const result = await pool.query(
@@ -1032,7 +1071,23 @@ export async function getBomBaseQtyMap(req: AuthenticatedRequest, res: Response)
if (map[code] == null) map[code] = base;
}
}
return res.json({ success: true, data: map });
// 품목별 배치사용여부 일괄 조회 (BOM 유무와 무관하게 item_info 기준, TASK:ERP-node-074)
// 빈 값/NULL/미등록 품목은 'Y'(사용, 현행 유지)로 간주
const batchUse: Record<string, "Y" | "N"> = {};
for (const code of itemCodes) batchUse[code] = "Y";
const buResult = await pool.query(
`SELECT item_number, COALESCE(NULLIF(batch_use, ''), 'Y') AS batch_use
FROM item_info
WHERE company_code = $1 AND item_number = ANY($2::text[])`,
[companyCode, itemCodes]
);
for (const row of buResult.rows) {
if (!row.item_number) continue;
batchUse[row.item_number] = String(row.batch_use).toUpperCase() === "N" ? "N" : "Y";
}
return res.json({ success: true, data: map, batchUse });
} catch (error: any) {
logger.error("BOM 기준수 일괄 조회 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });