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:
kjs
2026-04-20 14:14:24 +09:00
parent 48b9ba3d2a
commit 9737805bf9
48 changed files with 1256 additions and 357 deletions

View File

@@ -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";
// 입고 목록 조회 (헤더-디테일 JOIN, 레거시 호환)
@@ -472,6 +473,45 @@ export async function update(req: AuthenticatedRequest, res: Response) {
await client.query("BEGIN");
// 변경 전 값 조회 (헤더)
const oldHeaderRes = await client.query(
`SELECT * FROM inbound_mng WHERE id = $1 AND company_code = $2`,
[id, companyCode],
);
if (oldHeaderRes.rowCount === 0) {
await client.query("ROLLBACK");
return res
.status(404)
.json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
}
const oldHeader = oldHeaderRes.rows[0];
// 변경 전 값 조회 (디테일, 있을 경우)
let oldDetail: any = null;
if (detail_id) {
const oldDetailRes = await client.query(
`SELECT * FROM inbound_detail WHERE id = $1 AND company_code = $2`,
[detail_id, companyCode],
);
oldDetail = oldDetailRes.rows[0] || null;
}
const oldQty =
Number(oldDetail?.inbound_qty ?? oldHeader.inbound_qty) || 0;
const oldWhCode = oldHeader.warehouse_code || null;
const oldLocCode = oldHeader.location_code || null;
const itemCode = oldDetail?.item_number || oldHeader.item_number || null;
const inboundNumber = oldHeader.inbound_number;
const newQty =
inbound_qty !== undefined && inbound_qty !== null
? Number(inbound_qty)
: oldQty;
const newWhCode =
warehouse_code !== undefined ? warehouse_code : oldWhCode;
const newLocCode =
location_code !== undefined ? location_code : oldLocCode;
// 입고 레코드 업데이트 (헤더 + 품목 필드 모두)
const headerResult = await client.query(
`UPDATE inbound_mng SET
@@ -506,13 +546,6 @@ export async function update(req: AuthenticatedRequest, res: Response) {
],
);
if (headerResult.rowCount === 0) {
await client.query("ROLLBACK");
return res
.status(404)
.json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
}
// 디테일 업데이트 (inbound_detail) — detail_id가 있으면 디테일 레벨 필드 업데이트
let detailRow = null;
if (detail_id) {
@@ -563,9 +596,67 @@ export async function update(req: AuthenticatedRequest, res: Response) {
);
}
// 재고/이력 반영 (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: `입고수정-창고변경 (${inboundNumber}) ${oldWhCode || ""}${newWhCode || ""}`,
});
}
if (newQty > 0) {
await adjustInventory(client, {
companyCode,
userId,
itemCode,
whCode: newWhCode,
locCode: newLocCode,
delta: newQty,
transactionType: "입고수정",
remark: `입고수정-창고변경 (${inboundNumber}) ${oldWhCode || ""}${newWhCode || ""}, 수량 ${oldQty}${newQty}`,
});
}
} else {
const delta = newQty - oldQty;
if (delta !== 0) {
await adjustInventory(client, {
companyCode,
userId,
itemCode,
whCode: newWhCode,
locCode: newLocCode,
delta,
transactionType: "입고수정",
remark: `입고수정 (${inboundNumber}) 수량 ${oldQty}${newQty}`,
});
}
}
}
await client.query("COMMIT");
logger.info("입고 수정", { companyCode, userId, id, detail_id });
logger.info("입고 수정", {
companyCode,
userId,
id,
detail_id,
oldQty,
newQty,
oldWhCode,
newWhCode,
});
return res.json({
success: true,