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 재구현 — 좌측 리스트 필터)
|
||||
|
||||
@@ -23,6 +23,9 @@ router.get("/process-materials", ctrl.getProcessMaterials);
|
||||
// 외주발주 가능 작업지시 목록 (좌측 리스트 필터, TASK:ERP-019 재구현)
|
||||
router.get("/outsourceable-work-orders", ctrl.listOutsourceableWorkOrders);
|
||||
|
||||
// 외주발주현황 통합 조회 (TASK:ERP-025)
|
||||
router.get("/status", ctrl.listOrderStatus);
|
||||
|
||||
// 사급자재 출고요청
|
||||
router.post("/release-request", ctrl.requestRelease);
|
||||
|
||||
|
||||
@@ -1188,3 +1188,181 @@ async function mapToOpoMaterials(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 외주발주현황 통합 조회 (TASK:ERP-025)
|
||||
// 발주 × 공정 × 자재 × 사급출고 × 입고 단위로 펼친 행 반환
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface OrderStatusFilter {
|
||||
keyword?: string;
|
||||
source_type?: string;
|
||||
order_status?: string;
|
||||
release_status?: string; // '출고완료' | '입고완료'
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}
|
||||
|
||||
export async function listOrderStatus(
|
||||
companyCode: string,
|
||||
opts: OrderStatusFilter,
|
||||
) {
|
||||
const pool = getPool();
|
||||
const params: any[] = [companyCode];
|
||||
let where = `o.company_code = $1`;
|
||||
|
||||
if (opts.source_type) {
|
||||
params.push(opts.source_type);
|
||||
where += ` AND o.source_type = $${params.length}`;
|
||||
}
|
||||
if (opts.order_status) {
|
||||
params.push(opts.order_status);
|
||||
where += ` AND o.status = $${params.length}`;
|
||||
}
|
||||
if (opts.date_from) {
|
||||
params.push(opts.date_from);
|
||||
where += ` AND o.order_date >= $${params.length}`;
|
||||
}
|
||||
if (opts.date_to) {
|
||||
params.push(opts.date_to);
|
||||
where += ` AND o.order_date <= $${params.length}`;
|
||||
}
|
||||
|
||||
// 출고: outsource_purchase_order_material.outbound_id → outbound_mng.id 정참조
|
||||
// 입고: inbound_mng.source_table='outbound_mng' AND source_id=outbound_mng.id 역참조 합산
|
||||
const sql = `
|
||||
SELECT
|
||||
o.id AS order_id,
|
||||
o.order_no,
|
||||
COALESCE(o.source_type, '') AS source_type,
|
||||
COALESCE(o.source_no, '') AS source_no,
|
||||
COALESCE(o.item_code, '') AS item_code,
|
||||
COALESCE(o.item_name, '') AS item_name,
|
||||
COALESCE(o.spec, '') AS spec,
|
||||
COALESCE(o.material, '') AS material,
|
||||
COALESCE(NULLIF(o.quantity::text, '')::numeric, 0) AS quantity,
|
||||
o.order_date,
|
||||
o.due_date,
|
||||
COALESCE(o.status, '') AS order_status,
|
||||
COALESCE(o.manager, '') AS manager,
|
||||
p.id AS process_id,
|
||||
p.seq,
|
||||
COALESCE(p.process_name, '') AS process_name,
|
||||
COALESCE(p.vendor_code, '') AS vendor_code,
|
||||
COALESCE(p.vendor_name, '') AS vendor_name,
|
||||
COALESCE(p.material_needed, false) AS material_needed,
|
||||
m.id AS material_id,
|
||||
COALESCE(m.item_code, '') AS material_item_code,
|
||||
COALESCE(m.item_name, '') AS material_item_name,
|
||||
COALESCE(NULLIF(m.qty::text, '')::numeric, 0) AS material_qty,
|
||||
COALESCE(m.unit, '') AS material_unit,
|
||||
COALESCE(m.release_status, '') AS material_release_status,
|
||||
om.id AS outbound_id,
|
||||
COALESCE(om.outbound_number, '') AS outbound_number,
|
||||
COALESCE(om.outbound_status, '') AS outbound_status,
|
||||
COALESCE(NULLIF(om.outbound_qty, '')::numeric, 0) AS released_qty,
|
||||
COALESCE(om.outbound_date, '') AS request_date,
|
||||
COALESCE(in_sum.received_qty, 0)::numeric AS received_qty
|
||||
FROM outsource_purchase_order o
|
||||
LEFT JOIN outsource_purchase_order_process p
|
||||
ON p.opo_id = o.id
|
||||
LEFT JOIN outsource_purchase_order_material m
|
||||
ON m.opo_process_id = p.id
|
||||
LEFT JOIN outbound_mng om
|
||||
ON om.id = m.outbound_id
|
||||
AND om.company_code = o.company_code
|
||||
LEFT JOIN (
|
||||
SELECT company_code, source_id, SUM(inbound_qty)::numeric AS received_qty
|
||||
FROM inbound_mng
|
||||
WHERE source_table = 'outbound_mng'
|
||||
GROUP BY company_code, source_id
|
||||
) in_sum
|
||||
ON in_sum.source_id = om.id
|
||||
AND in_sum.company_code = o.company_code
|
||||
WHERE ${where}
|
||||
ORDER BY o.order_date DESC NULLS LAST, o.order_no DESC, p.seq NULLS LAST
|
||||
`;
|
||||
|
||||
const r = await pool.query(sql, params);
|
||||
|
||||
// 행 단위 파생 필드 + 키워드/출고상태 필터
|
||||
let rows = r.rows.map((row: any) => {
|
||||
const releasedQty = Number(row.released_qty || 0);
|
||||
const receivedQty = Number(row.received_qty || 0);
|
||||
const remainQty = Math.max(0, releasedQty - receivedQty);
|
||||
const ratePct =
|
||||
releasedQty > 0 ? Math.round((receivedQty / releasedQty) * 100) : 0;
|
||||
|
||||
// 화면 표기용 통합 출고상태
|
||||
let release_status = "";
|
||||
if (releasedQty > 0) {
|
||||
release_status = receivedQty >= releasedQty ? "입고완료" : "출고완료";
|
||||
}
|
||||
|
||||
return {
|
||||
order_id: row.order_id,
|
||||
order_no: row.order_no,
|
||||
source_type: row.source_type,
|
||||
source_no: row.source_no,
|
||||
item_code: row.item_code,
|
||||
item_name: row.item_name,
|
||||
spec: row.spec,
|
||||
material: row.material,
|
||||
quantity: Number(row.quantity || 0),
|
||||
order_date: row.order_date || "",
|
||||
due_date: row.due_date || "",
|
||||
order_status: row.order_status,
|
||||
manager: row.manager,
|
||||
process_id: row.process_id || "",
|
||||
seq: row.seq != null ? Number(row.seq) : null,
|
||||
process_name: row.process_name,
|
||||
vendor_code: row.vendor_code,
|
||||
vendor_name: row.vendor_name,
|
||||
material_needed: !!row.material_needed,
|
||||
material_id: row.material_id || "",
|
||||
material_item_code: row.material_item_code,
|
||||
material_item_name: row.material_item_name,
|
||||
material_qty: Number(row.material_qty || 0),
|
||||
material_unit: row.material_unit,
|
||||
material_release_status: row.material_release_status,
|
||||
outbound_id: row.outbound_id || "",
|
||||
outbound_number: row.outbound_number,
|
||||
outbound_status: row.outbound_status,
|
||||
released_qty: releasedQty,
|
||||
request_date: row.request_date,
|
||||
received_qty: receivedQty,
|
||||
remain_qty: remainQty,
|
||||
rate_pct: ratePct,
|
||||
release_status,
|
||||
};
|
||||
});
|
||||
|
||||
if (opts.keyword) {
|
||||
const kw = opts.keyword.toLowerCase();
|
||||
rows = rows.filter((row: any) => {
|
||||
const hay = [
|
||||
row.order_no,
|
||||
row.source_type,
|
||||
row.source_no,
|
||||
row.item_code,
|
||||
row.item_name,
|
||||
row.spec,
|
||||
row.material,
|
||||
row.process_name,
|
||||
row.vendor_name,
|
||||
row.material_item_code,
|
||||
row.material_item_name,
|
||||
row.manager,
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return hay.includes(kw);
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.release_status) {
|
||||
rows = rows.filter((row: any) => row.release_status === opts.release_status);
|
||||
}
|
||||
|
||||
return { rows, total: rows.length };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user