Implement Order Status Integration for Outsourcing Purchase Management

- Added a new endpoint `listOrderStatus` in the `outsourcePurchaseController` to retrieve integrated order status information, including filtering options for source type, order status, and date range.
- Updated the `outsourcePurchaseService` to handle the new order status retrieval logic, ensuring proper filtering and data aggregation.
- Introduced a new route for accessing the order status information in `outsourcePurchaseRoutes`.
- Created a detailed modal for viewing outsourcing purchase order details, enhancing the user interface for better data presentation.
- Developed a registration modal for creating and editing outsourcing purchase orders, featuring a tabbed interface for improved user experience.

(TASK: ERP-025, ERP-019)
This commit is contained in:
kjs
2026-05-07 18:33:38 +09:00
parent ec1c95b8c5
commit 2e686f059d
28 changed files with 13714 additions and 168 deletions

View File

@@ -12,6 +12,17 @@ export interface AdjustInventoryParams {
validateStockEnough?: boolean;
}
/**
* inventory_stock 차감/증가 + inventory_history 기록.
*
* 매칭 우선순위:
* 1) 사용자 선택 창고+위치 정확히 일치 (strict)
* 2) 창고만 일치
* 3) 같은 품목의 첫 row (위치/창고 빈값 데이터 fallback)
*
* 즉, 사용자가 창고를 선택했지만 해당 row가 없고 빈 창고 row만 있는 경우라도
* 첫 매칭 row에서 차감되도록 한다. inventory_history에는 실제 차감된 row의 위치 기록.
*/
export async function adjustInventory(
client: PoolClient,
params: AdjustInventoryParams,
@@ -30,96 +41,96 @@ export async function adjustInventory(
if (!itemCode || delta === 0) return;
if (validateStockEnough && delta < 0) {
const stockRes = await client.query(
`SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) AS cur
FROM inventory_stock
WHERE company_code = $1 AND item_code = $2
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
AND ($4 IS NULL OR $4 = '' OR COALESCE(location_code, '') = $4)
LIMIT 1`,
[companyCode, itemCode, whCode || "", locCode || ""],
);
const cur = parseFloat(stockRes.rows[0]?.cur || "0");
if (cur + delta < 0) {
throw new Error(
`재고 부족: 품목 ${itemCode} (창고 ${whCode || "미지정"}) — 현재 재고 ${cur}, 차감 요청 ${-delta}`,
);
}
}
const existing = await client.query(
`SELECT id FROM inventory_stock
WHERE company_code = $1 AND item_code = $2
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
AND ($4 IS NULL OR $4 = '' OR COALESCE(location_code, '') = $4)
LIMIT 1`,
// 1) 매칭 row 탐색 (우선순위 정렬 + LIMIT 1)
const matchRes = await client.query(
`SELECT id, warehouse_code, location_code,
COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) AS cur
FROM inventory_stock
WHERE company_code = $1 AND item_code = $2
ORDER BY
CASE WHEN COALESCE(warehouse_code, '') = COALESCE($3, '') THEN 0 ELSE 1 END,
CASE WHEN COALESCE(location_code, '') = COALESCE($4, '') THEN 0 ELSE 1 END
LIMIT 1`,
[companyCode, itemCode, whCode || "", locCode || ""],
);
if (existing.rows.length > 0) {
let stockId: string | null = matchRes.rows[0]?.id || null;
const matchedWh: string | null = matchRes.rows[0]?.warehouse_code ?? null;
const matchedLoc: string | null = matchRes.rows[0]?.location_code ?? null;
const cur = parseFloat(matchRes.rows[0]?.cur || "0");
// 2) 재고 부족 검증 (차감 케이스)
if (validateStockEnough && delta < 0 && cur + delta < 0) {
throw new Error(
`재고 부족: 품목 ${itemCode} (창고 ${whCode || "미지정"}) — 현재 재고 ${cur}, 차감 요청 ${-delta}`,
);
}
// 3) row가 있으면 UPDATE, 없으면 신규 INSERT (입고 시에만)
if (stockId) {
if (delta >= 0) {
await client.query(
`UPDATE inventory_stock
SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text),
last_in_date = NOW(),
updated_date = NOW()
WHERE id = $2`,
[delta, existing.rows[0].id],
SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text),
last_in_date = NOW(),
updated_date = NOW()
WHERE id = $2`,
[delta, stockId],
);
} else {
await client.query(
`UPDATE inventory_stock
SET current_qty = CAST(GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1, 0) AS text),
last_out_date = NOW(),
updated_date = NOW()
WHERE id = $2`,
[delta, existing.rows[0].id],
SET current_qty = CAST(GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1, 0) AS text),
last_out_date = NOW(),
updated_date = NOW()
WHERE id = $2`,
[delta, stockId],
);
}
} else {
const initQty = Math.max(delta, 0);
await client.query(
const insRes = await client.query(
`INSERT INTO inventory_stock (
id, company_code, item_code, warehouse_code, location_code,
current_qty, safety_qty, last_in_date, last_out_date,
created_date, updated_date, writer
) VALUES (
gen_random_uuid()::text, $1, $2, $3, $4,
$5, '0',
${delta > 0 ? "NOW()" : "NULL"},
${delta < 0 ? "NOW()" : "NULL"},
NOW(), NOW(), $6
)`,
id, company_code, item_code, warehouse_code, location_code,
current_qty, safety_qty, last_in_date, last_out_date,
created_date, updated_date, writer
) VALUES (
gen_random_uuid()::text, $1, $2, $3, $4,
$5, '0',
${delta > 0 ? "NOW()" : "NULL"},
${delta < 0 ? "NOW()" : "NULL"},
NOW(), NOW(), $6
) RETURNING id`,
[companyCode, itemCode, whCode, locCode, String(initQty), userId],
);
stockId = insRes.rows[0].id;
}
// 4) 차감 후 잔량 (이력 기록용)
const afterRes = await client.query(
`SELECT current_qty FROM inventory_stock
WHERE company_code = $1 AND item_code = $2
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
AND ($4 IS NULL OR $4 = '' OR COALESCE(location_code, '') = $4)
LIMIT 1`,
[companyCode, itemCode, whCode || "", locCode || ""],
`SELECT current_qty FROM inventory_stock WHERE id = $1`,
[stockId],
);
const afterQty = afterRes.rows[0]?.current_qty || "0";
// 5) inventory_history 기록 — 실제 차감/증가된 row의 창고/위치를 기록
const historyWh = matchedWh ?? whCode;
const historyLoc = matchedLoc ?? locCode;
await client.query(
`INSERT INTO inventory_history (
id, company_code, item_code, warehouse_code, location_code,
transaction_type, transaction_date, quantity, balance_qty, remark,
writer, created_date
) VALUES (
gen_random_uuid()::text, $1, $2, $3, $4,
$5, NOW(), $6, $7, $8,
$9, NOW()
)`,
id, company_code, item_code, warehouse_code, location_code,
transaction_type, transaction_date, quantity, balance_qty, remark,
writer, created_date
) VALUES (
gen_random_uuid()::text, $1, $2, $3, $4,
$5, NOW(), $6, $7, $8,
$9, NOW()
)`,
[
companyCode,
itemCode,
whCode,
locCode,
historyWh,
historyLoc,
transactionType,
(delta > 0 ? "+" : "") + String(delta),
afterQty,