feat: 입고/자재현황/분석리포트 컨트롤러 및 프론트엔드 개선
- receivingController: 헤더-디테일 JOIN 구조로 변경, 검색/조회 로직 개선 - materialStatusController: work_instruction 테이블 기반으로 쿼리 수정 - analyticsReportController: 구매 리포트 company_code 필터링 로직 개선 - material-status 페이지: COMPANY_29/COMPANY_7 프론트엔드 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 입고 목록 조회
|
||||
// 입고 목록 조회 (헤더-디테일 JOIN, 레거시 호환)
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
@@ -50,7 +50,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
if (search_keyword) {
|
||||
conditions.push(
|
||||
`(im.inbound_number ILIKE $${paramIdx} OR im.item_name ILIKE $${paramIdx} OR im.item_number ILIKE $${paramIdx} OR im.supplier_name ILIKE $${paramIdx} OR im.reference_number ILIKE $${paramIdx})`
|
||||
`(im.inbound_number ILIKE $${paramIdx} OR COALESCE(id.item_name, im.item_name) ILIKE $${paramIdx} OR COALESCE(id.item_number, im.item_number) ILIKE $${paramIdx} OR COALESCE(id.supplier_name, im.supplier_name) ILIKE $${paramIdx} OR COALESCE(id.reference_number, im.reference_number) ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${search_keyword}%`);
|
||||
paramIdx++;
|
||||
@@ -72,14 +72,37 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
im.*,
|
||||
im.id, im.company_code, im.inbound_number, im.inbound_type, im.inbound_date,
|
||||
im.warehouse_code, im.location_code, im.inspector, im.manager,
|
||||
im.inbound_status, im.memo AS header_memo, im.source_table, im.source_id,
|
||||
im.created_date, im.created_by, im.updated_date, im.updated_by,
|
||||
im.writer, im.status, im.prev_inbound_qty, im.remark,
|
||||
COALESCE(id.reference_number, im.reference_number) AS reference_number,
|
||||
COALESCE(id.supplier_code, im.supplier_code) AS supplier_code,
|
||||
COALESCE(id.supplier_name, im.supplier_name) AS supplier_name,
|
||||
COALESCE(id.item_number, im.item_number) AS item_number,
|
||||
COALESCE(id.item_name, im.item_name) AS item_name,
|
||||
COALESCE(id.spec, im.spec) AS spec,
|
||||
COALESCE(id.material, im.material) AS material,
|
||||
COALESCE(id.unit, im.unit) AS unit,
|
||||
COALESCE(id.inbound_qty, im.inbound_qty) AS inbound_qty,
|
||||
COALESCE(id.unit_price, im.unit_price) AS unit_price,
|
||||
COALESCE(id.total_amount, im.total_amount) AS total_amount,
|
||||
COALESCE(id.lot_number, im.lot_number) AS lot_number,
|
||||
COALESCE(id.inspection_status, im.inspection_status) AS inspection_status,
|
||||
COALESCE(id.memo, im.memo) AS memo,
|
||||
id.id AS detail_id,
|
||||
id.seq_no,
|
||||
id.inbound_type AS detail_inbound_type,
|
||||
wh.warehouse_name
|
||||
FROM inbound_mng im
|
||||
LEFT JOIN inbound_detail id
|
||||
ON id.inbound_id = im.inbound_number AND id.company_code = im.company_code
|
||||
LEFT JOIN warehouse_info wh
|
||||
ON im.warehouse_code = wh.warehouse_code
|
||||
AND im.company_code = wh.company_code
|
||||
${whereClause}
|
||||
ORDER BY im.created_date DESC
|
||||
ORDER BY im.created_date DESC, id.seq_no ASC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
@@ -97,7 +120,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// 입고 등록 (다건)
|
||||
// 입고 등록 (헤더 1건 + 디테일 N건)
|
||||
export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
@@ -111,41 +134,70 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
return res.status(400).json({ success: false, message: "입고 품목이 없습니다." });
|
||||
}
|
||||
|
||||
// 첫 번째 아이템에서 inbound_type 추출 (헤더용)
|
||||
const inboundType = items[0].inbound_type || null;
|
||||
const inboundNumber = inbound_number || items[0].inbound_number;
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
const insertedRows: any[] = [];
|
||||
// 1. 헤더 INSERT (inbound_mng) — 품목 컬럼은 NULL
|
||||
const headerResult = await client.query(
|
||||
`INSERT INTO inbound_mng (
|
||||
id, company_code, inbound_number, inbound_type, inbound_date,
|
||||
warehouse_code, location_code,
|
||||
inbound_status, inspector, manager, memo,
|
||||
created_date, created_by, writer, status
|
||||
) VALUES (
|
||||
gen_random_uuid()::text, $1, $2, $3, $4::date,
|
||||
$5, $6,
|
||||
$7, $8, $9, $10,
|
||||
NOW(), $11, $11, '입고'
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
inboundNumber,
|
||||
inboundType,
|
||||
inbound_date || items[0].inbound_date,
|
||||
warehouse_code || items[0].warehouse_code || null,
|
||||
location_code || items[0].location_code || null,
|
||||
items[0].inbound_status || "대기",
|
||||
inspector || items[0].inspector || null,
|
||||
manager || items[0].manager || null,
|
||||
memo || items[0].memo || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO inbound_mng (
|
||||
company_code, inbound_number, inbound_type, inbound_date,
|
||||
reference_number, supplier_code, supplier_name,
|
||||
const headerRow = headerResult.rows[0];
|
||||
const insertedDetails: any[] = [];
|
||||
|
||||
// 2. 디테일 INSERT (inbound_detail) + 재고/발주 업데이트
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const seqNo = i + 1;
|
||||
|
||||
// 2a. inbound_detail INSERT
|
||||
const detailResult = await client.query(
|
||||
`INSERT INTO inbound_detail (
|
||||
id, company_code, inbound_id, seq_no, inbound_type,
|
||||
item_number, item_name, spec, material, unit,
|
||||
inbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, manager, memo,
|
||||
source_table, source_id,
|
||||
lot_number, reference_number, supplier_code, supplier_name,
|
||||
inspection_status, memo, item_id,
|
||||
created_date, created_by, writer, status
|
||||
) VALUES (
|
||||
$1, $2, $3, $4::date,
|
||||
$5, $6, $7,
|
||||
$8, $9, $10, $11, $12,
|
||||
$13, $14, $15,
|
||||
$16, $17, $18,
|
||||
$19, $20,
|
||||
$21, $22, $23,
|
||||
$24, $25,
|
||||
NOW(), $26, $26, '입고'
|
||||
gen_random_uuid()::text, $1, $2, $3, $4,
|
||||
$5, $6, $7, $8, $9,
|
||||
$10, $11, $12,
|
||||
$13, $14, $15, $16,
|
||||
$17, $18, $19,
|
||||
NOW(), $20, $20, '입고'
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
inbound_number || item.inbound_number,
|
||||
item.inbound_type,
|
||||
inbound_date || item.inbound_date,
|
||||
item.reference_number || null,
|
||||
item.supplier_code || null,
|
||||
item.supplier_name || null,
|
||||
inboundNumber,
|
||||
seqNo,
|
||||
item.inbound_type || inboundType,
|
||||
item.item_number || null,
|
||||
item.item_name || null,
|
||||
item.spec || null,
|
||||
@@ -155,22 +207,19 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
item.unit_price || 0,
|
||||
item.total_amount || 0,
|
||||
item.lot_number || null,
|
||||
warehouse_code || item.warehouse_code || null,
|
||||
location_code || item.location_code || null,
|
||||
item.inbound_status || "대기",
|
||||
item.reference_number || null,
|
||||
item.supplier_code || null,
|
||||
item.supplier_name || null,
|
||||
item.inspection_status || "대기",
|
||||
inspector || item.inspector || null,
|
||||
manager || item.manager || null,
|
||||
memo || item.memo || null,
|
||||
item.source_table || null,
|
||||
item.source_id || null,
|
||||
item.memo || null,
|
||||
item.item_id || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
insertedRows.push(result.rows[0]);
|
||||
insertedDetails.push(detailResult.rows[0]);
|
||||
|
||||
// 재고 업데이트 (inventory_stock): 입고 수량 증가
|
||||
// 2b. 재고 업데이트 (inventory_stock): 입고 수량 증가 — 기존 로직 유지
|
||||
const itemCode = item.item_number || null;
|
||||
const whCode = warehouse_code || item.warehouse_code || null;
|
||||
const locCode = location_code || item.location_code || null;
|
||||
@@ -206,7 +255,7 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// 구매입고인 경우 발주의 received_qty 업데이트
|
||||
// 2c. 구매입고인 경우 발주의 received_qty 업데이트 — 기존 로직 유지
|
||||
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_order_mng") {
|
||||
await client.query(
|
||||
`UPDATE purchase_order_mng
|
||||
@@ -229,29 +278,34 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
);
|
||||
}
|
||||
|
||||
// 구매입고인 경우 purchase_detail 기반 발주의 헤더 상태 업데이트
|
||||
// 구매입고인 경우 purchase_detail 품목별 입고수량 업데이트
|
||||
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_detail") {
|
||||
// 해당 디테일의 발주번호 조회
|
||||
// 1. 해당 purchase_detail의 received_qty 누적 업데이트
|
||||
await client.query(
|
||||
`UPDATE purchase_detail SET
|
||||
received_qty = CAST(
|
||||
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text
|
||||
),
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[item.inbound_qty || 0, item.source_id, companyCode]
|
||||
);
|
||||
|
||||
// 2. 발주 헤더 상태 업데이트
|
||||
const detailInfo = await client.query(
|
||||
`SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`,
|
||||
[item.source_id, companyCode]
|
||||
);
|
||||
if (detailInfo.rows.length > 0) {
|
||||
const purchaseNo = detailInfo.rows[0].purchase_no;
|
||||
// 해당 발주의 모든 디테일 잔량 확인
|
||||
// 잔량 있는 디테일이 있는지 확인
|
||||
const unreceived = await client.query(
|
||||
`SELECT pd.id
|
||||
FROM purchase_detail pd
|
||||
LEFT JOIN (
|
||||
SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received
|
||||
FROM inbound_mng
|
||||
WHERE source_table = 'purchase_detail' AND company_code = $1
|
||||
GROUP BY source_id
|
||||
) r ON r.source_id = pd.id
|
||||
WHERE pd.purchase_no = $2 AND pd.company_code = $1
|
||||
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(r.total_received, 0) > 0
|
||||
`SELECT id FROM purchase_detail
|
||||
WHERE purchase_no = $1 AND company_code = $2
|
||||
AND COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
||||
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0
|
||||
LIMIT 1`,
|
||||
[companyCode, purchaseNo]
|
||||
[purchaseNo, companyCode]
|
||||
);
|
||||
const newStatus = unreceived.rows.length === 0 ? '입고완료' : '부분입고';
|
||||
await client.query(
|
||||
@@ -268,14 +322,15 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
logger.info("입고 등록 완료", {
|
||||
companyCode,
|
||||
userId,
|
||||
count: insertedRows.length,
|
||||
inbound_number,
|
||||
headerCount: 1,
|
||||
detailCount: insertedDetails.length,
|
||||
inbound_number: inboundNumber,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: insertedRows,
|
||||
message: `${insertedRows.length}건 입고 등록 완료`,
|
||||
data: { header: headerRow, details: insertedDetails },
|
||||
message: `${insertedDetails.length}건 입고 등록 완료`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
@@ -286,8 +341,11 @@ 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;
|
||||
@@ -297,71 +355,253 @@ export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, manager: mgr, memo,
|
||||
detail_id,
|
||||
} = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 헤더 업데이트 (inbound_mng) — 헤더 레벨 필드만
|
||||
const headerResult = await client.query(
|
||||
`UPDATE inbound_mng SET
|
||||
inbound_date = COALESCE($1::date, inbound_date),
|
||||
inbound_qty = COALESCE($2, inbound_qty),
|
||||
unit_price = COALESCE($3, unit_price),
|
||||
total_amount = COALESCE($4, total_amount),
|
||||
lot_number = COALESCE($5, lot_number),
|
||||
warehouse_code = COALESCE($6, warehouse_code),
|
||||
location_code = COALESCE($7, location_code),
|
||||
inbound_status = COALESCE($8, inbound_status),
|
||||
inspection_status = COALESCE($9, inspection_status),
|
||||
inspector = COALESCE($10, inspector),
|
||||
manager = COALESCE($11, manager),
|
||||
memo = COALESCE($12, memo),
|
||||
warehouse_code = COALESCE($2, warehouse_code),
|
||||
location_code = COALESCE($3, location_code),
|
||||
inbound_status = COALESCE($4, inbound_status),
|
||||
inspector = COALESCE($5, inspector),
|
||||
manager = COALESCE($6, manager),
|
||||
memo = COALESCE($7, memo),
|
||||
updated_date = NOW(),
|
||||
updated_by = $13
|
||||
WHERE id = $14 AND company_code = $15
|
||||
updated_by = $8
|
||||
WHERE id = $9 AND company_code = $10
|
||||
RETURNING *`,
|
||||
[
|
||||
inbound_date, inbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
inbound_status, inspection_status,
|
||||
inspector, mgr, memo,
|
||||
inbound_date, warehouse_code, location_code,
|
||||
inbound_status, inspector, mgr, memo,
|
||||
userId, id, companyCode,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
if (headerResult.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("입고 수정", { companyCode, userId, id });
|
||||
// 디테일 업데이트 (inbound_detail) — detail_id가 있으면 디테일 레벨 필드 업데이트
|
||||
let detailRow = null;
|
||||
if (detail_id) {
|
||||
const detailResult = await client.query(
|
||||
`UPDATE inbound_detail SET
|
||||
inbound_qty = COALESCE($1, inbound_qty),
|
||||
unit_price = COALESCE($2, unit_price),
|
||||
total_amount = COALESCE($3, total_amount),
|
||||
lot_number = COALESCE($4, lot_number),
|
||||
inspection_status = COALESCE($5, inspection_status),
|
||||
memo = COALESCE($6, memo),
|
||||
updated_date = NOW(),
|
||||
updated_by = $7
|
||||
WHERE id = $8 AND company_code = $9
|
||||
RETURNING *`,
|
||||
[
|
||||
inbound_qty, unit_price, total_amount,
|
||||
lot_number, inspection_status, memo,
|
||||
userId, detail_id, companyCode,
|
||||
]
|
||||
);
|
||||
detailRow = detailResult.rows[0] || null;
|
||||
} else {
|
||||
// 레거시 데이터: detail_id 없이 inbound_mng 자체에 품목 정보 업데이트
|
||||
await client.query(
|
||||
`UPDATE inbound_mng SET
|
||||
inbound_qty = COALESCE($1, inbound_qty),
|
||||
unit_price = COALESCE($2, unit_price),
|
||||
total_amount = COALESCE($3, total_amount),
|
||||
lot_number = COALESCE($4, lot_number),
|
||||
inspection_status = COALESCE($5, inspection_status)
|
||||
WHERE id = $6 AND company_code = $7`,
|
||||
[
|
||||
inbound_qty, unit_price, total_amount,
|
||||
lot_number, inspection_status,
|
||||
id, companyCode,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("입고 수정", { companyCode, userId, id, detail_id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: { header: headerResult.rows[0], detail: detailRow },
|
||||
});
|
||||
} 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 deleteReceiving(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 헤더 정보 조회 (inbound_number, warehouse_code 등)
|
||||
const headerResult = await client.query(
|
||||
`SELECT * FROM inbound_mng WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
if (headerResult.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("입고 삭제", { companyCode, id });
|
||||
const header = headerResult.rows[0];
|
||||
const inboundNumber = header.inbound_number;
|
||||
|
||||
// 디테일 조회 (재고/발주 롤백용)
|
||||
const detailResult = await client.query(
|
||||
`SELECT * FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`,
|
||||
[inboundNumber, companyCode]
|
||||
);
|
||||
|
||||
// 디테일이 있으면 디테일 기반으로 롤백, 없으면 헤더(레거시) 기반으로 롤백
|
||||
const rollbackItems = detailResult.rows.length > 0
|
||||
? detailResult.rows.map((d: any) => ({
|
||||
item_number: d.item_number,
|
||||
inbound_qty: d.inbound_qty,
|
||||
inbound_type: d.inbound_type || header.inbound_type,
|
||||
source_table: header.source_table,
|
||||
source_id: header.source_id,
|
||||
}))
|
||||
: [{
|
||||
item_number: header.item_number,
|
||||
inbound_qty: header.inbound_qty,
|
||||
inbound_type: header.inbound_type,
|
||||
source_table: header.source_table,
|
||||
source_id: header.source_id,
|
||||
}];
|
||||
|
||||
const whCode = header.warehouse_code || null;
|
||||
const locCode = header.location_code || null;
|
||||
|
||||
for (const item of rollbackItems) {
|
||||
const itemCode = item.item_number || null;
|
||||
const inQty = Number(item.inbound_qty) || 0;
|
||||
|
||||
// 재고 롤백: 입고 수량만큼 차감
|
||||
if (itemCode && inQty > 0) {
|
||||
await client.query(
|
||||
`UPDATE inventory_stock
|
||||
SET current_qty = CAST(
|
||||
GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) - $1, 0) AS text
|
||||
),
|
||||
updated_date = NOW()
|
||||
WHERE company_code = $2 AND item_code = $3
|
||||
AND COALESCE(warehouse_code, '') = COALESCE($4, '')
|
||||
AND COALESCE(location_code, '') = COALESCE($5, '')`,
|
||||
[inQty, companyCode, itemCode, whCode || '', locCode || '']
|
||||
);
|
||||
}
|
||||
|
||||
// 구매입고 발주 롤백: purchase_order_mng 기반
|
||||
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_order_mng") {
|
||||
await client.query(
|
||||
`UPDATE purchase_order_mng
|
||||
SET received_qty = CAST(
|
||||
GREATEST(COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text
|
||||
),
|
||||
remain_qty = CAST(
|
||||
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
||||
- GREATEST(COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text
|
||||
),
|
||||
status = CASE
|
||||
WHEN GREATEST(COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) <= 0
|
||||
THEN '발주확정'
|
||||
ELSE '부분입고'
|
||||
END,
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[inQty, item.source_id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
// 구매입고 발주 롤백: purchase_detail 기반
|
||||
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_detail") {
|
||||
const detailInfo = await client.query(
|
||||
`SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`,
|
||||
[item.source_id, companyCode]
|
||||
);
|
||||
if (detailInfo.rows.length > 0) {
|
||||
const purchaseNo = detailInfo.rows[0].purchase_no;
|
||||
// 삭제 후 재계산을 위해 현재 입고 건 제외한 미입고 확인
|
||||
const unreceived = await client.query(
|
||||
`SELECT pd.id
|
||||
FROM purchase_detail pd
|
||||
LEFT JOIN (
|
||||
SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received
|
||||
FROM inbound_mng
|
||||
WHERE source_table = 'purchase_detail' AND company_code = $1
|
||||
AND inbound_number != $3
|
||||
GROUP BY source_id
|
||||
) r ON r.source_id = pd.id
|
||||
WHERE pd.purchase_no = $2 AND pd.company_code = $1
|
||||
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(r.total_received, 0) > 0
|
||||
LIMIT 1`,
|
||||
[companyCode, purchaseNo, inboundNumber]
|
||||
);
|
||||
// 잔량 있으면 부분입고, 전량 미입고면 발주확정
|
||||
const hasAnyReceived = await client.query(
|
||||
`SELECT 1 FROM inbound_mng
|
||||
WHERE source_table = 'purchase_detail' AND company_code = $1
|
||||
AND inbound_number != $2
|
||||
LIMIT 1`,
|
||||
[companyCode, inboundNumber]
|
||||
);
|
||||
const newStatus = hasAnyReceived.rows.length > 0
|
||||
? (unreceived.rows.length === 0 ? '입고완료' : '부분입고')
|
||||
: '발주확정';
|
||||
await client.query(
|
||||
`UPDATE purchase_order_mng SET status = $1, updated_date = NOW()
|
||||
WHERE purchase_no = $2 AND company_code = $3`,
|
||||
[newStatus, purchaseNo, companyCode]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 디테일 삭제
|
||||
await client.query(
|
||||
`DELETE FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`,
|
||||
[inboundNumber, companyCode]
|
||||
);
|
||||
|
||||
// 헤더 삭제
|
||||
await client.query(
|
||||
`DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("입고 삭제", { companyCode, id, inboundNumber });
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,14 +627,8 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
}
|
||||
|
||||
const baseQuery = `
|
||||
WITH detail_received AS (
|
||||
SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received
|
||||
FROM inbound_mng
|
||||
WHERE source_table = 'purchase_detail' AND company_code = $1
|
||||
GROUP BY source_id
|
||||
),
|
||||
combined AS (
|
||||
-- 디테일 기반 발주 데이터 (신규 헤더-디테일 구조, 헤더 없는 디테일도 포함)
|
||||
WITH combined AS (
|
||||
-- 디테일 기반 발주 데이터 (purchase_detail.received_qty로 잔량 계산)
|
||||
SELECT
|
||||
pd.id,
|
||||
COALESCE(po.purchase_no, pd.purchase_no) AS purchase_no,
|
||||
@@ -406,8 +640,8 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
COALESCE(NULLIF(pd.spec, ''), ii.size) AS spec,
|
||||
COALESCE(NULLIF(pd.material, ''), ii.material) AS material,
|
||||
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) AS order_qty,
|
||||
COALESCE(dr.total_received, 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(dr.total_received, 0) AS remain_qty,
|
||||
COALESCE(CAST(NULLIF(pd.received_qty, '') AS numeric), 0) AS received_qty,
|
||||
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(pd.received_qty, '') AS numeric), 0) AS remain_qty,
|
||||
COALESCE(CAST(NULLIF(pd.unit_price, '') AS numeric), 0) AS unit_price,
|
||||
COALESCE(po.status, '') AS status,
|
||||
COALESCE(pd.due_date, po.due_date) AS due_date,
|
||||
@@ -416,9 +650,8 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
LEFT JOIN purchase_order_mng po
|
||||
ON pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
|
||||
LEFT JOIN item_info ii ON pd.item_id = ii.id
|
||||
LEFT JOIN detail_received dr ON dr.source_id = pd.id
|
||||
WHERE pd.company_code = $1
|
||||
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(dr.total_received, 0) > 0
|
||||
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(pd.received_qty, '') AS numeric), 0) > 0
|
||||
AND COALESCE(pd.approval_status, '') NOT IN ('반려')
|
||||
AND COALESCE(po.status, '') NOT IN ('입고완료', '취소')
|
||||
${keywordConditionDetail}
|
||||
|
||||
Reference in New Issue
Block a user