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:
@@ -199,63 +199,45 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
const locCode = location_code || item.location_code || null;
|
||||
const outQty = Number(item.outbound_qty) || 0;
|
||||
if (itemCode && outQty > 0) {
|
||||
// 재고 사전 검증: 부족 시 즉시 에러 (트랜잭션 ROLLBACK)
|
||||
const stockCheck = 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`,
|
||||
// 매칭 row 탐색 — 사용자 선택 창고/위치 우선, fallback 같은 품목의 첫 row
|
||||
// (사용자가 창고를 선택했지만 inventory_stock에 빈 창고로 입고된 데이터가 있는 경우 대응)
|
||||
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 || ""],
|
||||
);
|
||||
const currentStock = parseFloat(stockCheck.rows[0]?.cur || "0");
|
||||
if (currentStock < outQty) {
|
||||
const stockRow = matchRes.rows[0];
|
||||
const currentStock = parseFloat(stockRow?.cur || "0");
|
||||
if (!stockRow || currentStock < outQty) {
|
||||
throw new Error(
|
||||
`재고 부족: ${item.item_name || itemCode} (창고 ${whCode || "미지정"}) — 현재 재고 ${currentStock}, 요청 출고 ${outQty}`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingStock = 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`,
|
||||
[companyCode, itemCode, whCode || "", locCode || ""],
|
||||
// id 기반 차감
|
||||
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`,
|
||||
[outQty, stockRow.id],
|
||||
);
|
||||
|
||||
if (existingStock.rows.length > 0) {
|
||||
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`,
|
||||
[outQty, existingStock.rows[0].id],
|
||||
);
|
||||
} else {
|
||||
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
|
||||
await client.query(
|
||||
`INSERT INTO inventory_stock (
|
||||
id, company_code, item_code, warehouse_code, location_code,
|
||||
current_qty, safety_qty, last_out_date,
|
||||
created_date, updated_date, writer
|
||||
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`,
|
||||
[companyCode, itemCode, whCode, locCode, userId],
|
||||
);
|
||||
}
|
||||
|
||||
// 재고 이력 기록 (inventory_history)
|
||||
// 차감 후 잔량 조회 (id 기반)
|
||||
const afterStockRes = 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`,
|
||||
[stockRow.id],
|
||||
);
|
||||
const afterQty = afterStockRes.rows[0]?.current_qty || "0";
|
||||
|
||||
// 이력은 실제 차감된 row의 창고/위치를 기록
|
||||
await client.query(
|
||||
`INSERT INTO inventory_history (
|
||||
id, company_code, item_code, warehouse_code, location_code,
|
||||
@@ -265,8 +247,8 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
[
|
||||
companyCode,
|
||||
itemCode,
|
||||
whCode,
|
||||
locCode,
|
||||
stockRow.warehouse_code ?? whCode,
|
||||
stockRow.location_code ?? locCode,
|
||||
String(-outQty),
|
||||
afterQty,
|
||||
resolvedItemOutboundType || "출고",
|
||||
@@ -679,18 +661,20 @@ export async function getPurchaseOrders(
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
// 발주 마스터(purchase_order_mng)는 헤더만 보유, 품목/수량은 detail(purchase_detail)에 있음
|
||||
// 반품출고는 detail 단위로 처리되어야 하므로 detail row를 펼쳐서 반환
|
||||
const conditions: string[] = ["pom.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
// 입고된 것만 (반품 대상)
|
||||
// 입고된 detail만 (반품 대상)
|
||||
conditions.push(
|
||||
`COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`,
|
||||
`COALESCE(CAST(NULLIF(pd.received_qty, '') AS numeric), 0) > 0`,
|
||||
);
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`,
|
||||
`(pom.purchase_no ILIKE $${paramIdx} OR pd.item_name ILIKE $${paramIdx} OR pd.item_code ILIKE $${paramIdx} OR pom.supplier_name ILIKE $${paramIdx})`,
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
@@ -699,15 +683,25 @@ export async function getPurchaseOrders(
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, purchase_no, order_date, supplier_code, supplier_name,
|
||||
item_code, item_name, spec, material,
|
||||
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty,
|
||||
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(unit_price, '') AS numeric), 0) AS unit_price,
|
||||
status, due_date
|
||||
FROM purchase_order_mng
|
||||
pd.id,
|
||||
pom.purchase_no,
|
||||
pom.order_date,
|
||||
pom.supplier_code,
|
||||
pom.supplier_name,
|
||||
pd.item_code,
|
||||
pd.item_name,
|
||||
pd.spec,
|
||||
pd.material,
|
||||
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) AS order_qty,
|
||||
COALESCE(CAST(NULLIF(pd.received_qty, '') AS numeric), 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(pd.unit_price, '') AS numeric), 0) AS unit_price,
|
||||
pom.status, pom.due_date
|
||||
FROM purchase_order_mng pom
|
||||
JOIN purchase_detail pd
|
||||
ON pd.purchase_no = pom.purchase_no
|
||||
AND pd.company_code = pom.company_code
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY order_date DESC, purchase_no`,
|
||||
ORDER BY pom.order_date DESC, pom.purchase_no, pd.item_code`,
|
||||
params,
|
||||
);
|
||||
|
||||
|
||||
@@ -224,6 +224,30 @@ export async function getProcessMaterials(
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 외주발주현황 통합 조회 (TASK:ERP-025)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
export async function listOrderStatus(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const opts: svc.OrderStatusFilter = {
|
||||
keyword: req.query.keyword as string | undefined,
|
||||
source_type: req.query.source_type as string | undefined,
|
||||
order_status: req.query.order_status as string | undefined,
|
||||
release_status: req.query.release_status as string | undefined,
|
||||
date_from: req.query.date_from as string | undefined,
|
||||
date_to: req.query.date_to as string | undefined,
|
||||
};
|
||||
const data = await svc.listOrderStatus(companyCode, opts);
|
||||
return ok(res, data);
|
||||
} catch (e: any) {
|
||||
return fail(res, 500, e?.message || "외주발주현황 조회 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 외주발주 가능 작업지시 목록
|
||||
// (TASK:ERP-019 재구현 — 좌측 리스트 필터)
|
||||
|
||||
Reference in New Issue
Block a user