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 });

View File

@@ -945,10 +945,23 @@ async function getBomChildItems(
${leadTimeCol} AS child_lead_time
FROM bom b
JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code
LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code
JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code
WHERE b.company_code = $1
AND b.item_code = $2
AND COALESCE(b.status, 'active') = 'active'
-- 반제품 계획은 BOM 자식 중 품목구분이 '반제품'인 것만 대상 (TASK:ERP-node-077)
-- item_info.type 은 회사/저장경로에 따라 라벨('반제품') 또는 카테고리 코드(CAT_xxx)로 들어옴.
-- category_values 로 코드↔라벨을 함께 해석하여 양쪽 모두 매칭. (TASK:ERP-node-077 후속)
AND (
TRIM(COALESCE(ii.type, '')) = '반제품'
OR ii.type IN (
SELECT value_code FROM category_values
WHERE company_code = $1
AND table_name = 'item_info'
AND column_name = 'type'
AND TRIM(COALESCE(value_label, '')) = '반제품'
)
)
`;
const result = await client.query(bomQuery, [companyCode, itemCode]);
return result.rows;