feat: Enhance outbound and receiving update functionalities with inventory adjustments
- Updated the `update` function in the outbound controller to include detailed inventory adjustments when modifying outbound records, ensuring accurate stock management. - Implemented rollback mechanisms for both outbound and receiving updates to maintain data integrity in case of errors. - Enhanced the `deleteOutbound` function to include inventory recovery and historical logging for deleted outbound records. - Introduced a new utility function `adjustInventory` to handle inventory changes consistently across different controllers. - Improved error handling and logging for better traceability during outbound and receiving operations.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import type { Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import type { AuthenticatedRequest } from "../types/auth";
|
||||
import { adjustInventory } from "../utils/inventoryUtils";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 출고 목록 조회
|
||||
@@ -324,6 +325,9 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
// 출고 수정
|
||||
export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
@@ -341,8 +345,90 @@ export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
memo,
|
||||
} = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 변경 전 값 조회
|
||||
const oldRes = await client.query(
|
||||
`SELECT * FROM outbound_mng WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode],
|
||||
);
|
||||
if (oldRes.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
const old = oldRes.rows[0];
|
||||
const oldQty = Number(old.outbound_qty) || 0;
|
||||
const oldWhCode = old.warehouse_code || null;
|
||||
const oldLocCode = old.location_code || null;
|
||||
const itemCode = old.item_code || old.item_number || null;
|
||||
const outboundNumber = old.outbound_number;
|
||||
|
||||
const newQty =
|
||||
outbound_qty !== undefined && outbound_qty !== null
|
||||
? Number(outbound_qty)
|
||||
: oldQty;
|
||||
const newWhCode =
|
||||
warehouse_code !== undefined ? warehouse_code : oldWhCode;
|
||||
const newLocCode =
|
||||
location_code !== undefined ? location_code : oldLocCode;
|
||||
|
||||
// 재고/이력 반영 (append-only): 수량 또는 창고/위치 변경 시
|
||||
const qtyChanged = newQty !== oldQty;
|
||||
const whChanged =
|
||||
(newWhCode || "") !== (oldWhCode || "") ||
|
||||
(newLocCode || "") !== (oldLocCode || "");
|
||||
|
||||
if (itemCode && (qtyChanged || whChanged)) {
|
||||
if (whChanged) {
|
||||
// 기존 창고 복구
|
||||
if (oldQty > 0) {
|
||||
await adjustInventory(client, {
|
||||
companyCode,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode: oldWhCode,
|
||||
locCode: oldLocCode,
|
||||
delta: +oldQty,
|
||||
transactionType: "출고취소",
|
||||
remark: `출고수정-창고변경 (${outboundNumber}) ${oldWhCode || ""}→${newWhCode || ""}`,
|
||||
});
|
||||
}
|
||||
// 신규 창고 차감 (재고부족 검증)
|
||||
if (newQty > 0) {
|
||||
await adjustInventory(client, {
|
||||
companyCode,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode: newWhCode,
|
||||
locCode: newLocCode,
|
||||
delta: -newQty,
|
||||
transactionType: "출고수정",
|
||||
remark: `출고수정-창고변경 (${outboundNumber}) ${oldWhCode || ""}→${newWhCode || ""}, 수량 ${oldQty}→${newQty}`,
|
||||
validateStockEnough: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 창고 동일, 수량만 변경: 기존 복구(+oldQty) + 신규 차감(-newQty) = delta(+복구/-추가차감)
|
||||
const delta = oldQty - newQty;
|
||||
if (delta !== 0) {
|
||||
await adjustInventory(client, {
|
||||
companyCode,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode: newWhCode,
|
||||
locCode: newLocCode,
|
||||
delta,
|
||||
transactionType: "출고수정",
|
||||
remark: `출고수정 (${outboundNumber}) 수량 ${oldQty}→${newQty}`,
|
||||
validateStockEnough: delta < 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE outbound_mng SET
|
||||
outbound_date = COALESCE($1, outbound_date),
|
||||
outbound_qty = COALESCE($2, outbound_qty),
|
||||
@@ -375,45 +461,95 @@ export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
],
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("출고 수정", { companyCode, userId, id });
|
||||
logger.info("출고 수정", {
|
||||
companyCode,
|
||||
userId,
|
||||
id,
|
||||
oldQty,
|
||||
newQty,
|
||||
oldWhCode,
|
||||
newWhCode,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("출고 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 출고 삭제
|
||||
// 출고 삭제 (재고 복구 + '출고취소' 이력 기록 포함)
|
||||
export async function deleteOutbound(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 대상 출고 조회
|
||||
const oldRes = await client.query(
|
||||
`SELECT * FROM outbound_mng WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode],
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
if (oldRes.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
const old = oldRes.rows[0];
|
||||
const itemCode = old.item_code || old.item_number || null;
|
||||
const whCode = old.warehouse_code || null;
|
||||
const locCode = old.location_code || null;
|
||||
const qty = Number(old.outbound_qty) || 0;
|
||||
const outboundNumber = old.outbound_number;
|
||||
|
||||
logger.info("출고 삭제", { companyCode, id });
|
||||
// 재고 복구 + 이력
|
||||
if (itemCode && qty > 0) {
|
||||
await adjustInventory(client, {
|
||||
companyCode,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode,
|
||||
locCode,
|
||||
delta: +qty,
|
||||
transactionType: "출고취소",
|
||||
remark: `출고 삭제 (${outboundNumber})`,
|
||||
});
|
||||
} else {
|
||||
logger.warn("출고 삭제 - 재고 복구 스킵", {
|
||||
companyCode,
|
||||
id,
|
||||
itemCode,
|
||||
qty,
|
||||
});
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("출고 삭제", { companyCode, userId, id, itemCode, qty });
|
||||
|
||||
return res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("출고 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user