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:
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user