Bundle POP sales outbound + inspection wiring

- outbound: wire 판매출고 to shipment_instruction with customer_mng JOIN, add customer filter, auto-transition shipment_instruction.status to COMPLETED/IN_PROGRESS based on remaining qty
- inspection: fix is_active filter ('Y' -> '사용') and JOIN inspection_standard to expose judgment_criteria
- POP InspectionModal (7 companies): hide measured-value input when judgment_criteria == CAT_JC_03 (O/X)
- POP SalesOutbound (7 companies): connect to /outbound/source/shipment-instructions, fix stale-closure bug in saveToDb via ref
- COMPANY_16/30 main: temporarily comment out 품질/안전관리 menu entries
This commit is contained in:
kmh
2026-04-30 10:54:21 +09:00
parent 854366453c
commit ce9516802a
18 changed files with 714 additions and 184 deletions

View File

@@ -180,7 +180,7 @@ export async function create(req: AuthenticatedRequest, res: Response) {
item.outbound_status || "대기",
manager_id || item.manager_id || null,
memo || item.memo || null,
item.source_type || null,
item.source_type || item.source_table || null,
item.sales_order_id || null,
item.shipment_plan_id || null,
item.item_info_id || null,
@@ -275,11 +275,12 @@ export async function create(req: AuthenticatedRequest, res: Response) {
);
}
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영 + master status 자동 전환
const itemSourceTable = item.source_type || item.source_table;
if (
item.outbound_type === "판매출고" &&
item.source_id &&
item.source_type === "shipment_instruction_detail"
itemSourceTable === "shipment_instruction_detail"
) {
const outQtyNum = Number(item.outbound_qty) || 0;
await client.query(
@@ -292,9 +293,12 @@ export async function create(req: AuthenticatedRequest, res: Response) {
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
const sidRes = await client.query(
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
`SELECT instruction_id, detail_id
FROM shipment_instruction_detail
WHERE id = $1 AND company_code = $2`,
[item.source_id, companyCode],
);
const instructionId = sidRes.rows[0]?.instruction_id;
const detailId = sidRes.rows[0]?.detail_id;
if (detailId) {
await client.query(
@@ -306,6 +310,27 @@ export async function create(req: AuthenticatedRequest, res: Response) {
[outQtyNum, detailId, companyCode],
);
}
// shipment_instruction master status 자동 전환 (입고의 purchase_detail → purchase_order_mng 패턴)
// 모든 detail 의 잔량이 0 이면 COMPLETED, 아니면 IN_PROGRESS
if (instructionId) {
const unshippedRes = await client.query(
`SELECT COUNT(*)::int AS unshipped
FROM shipment_instruction_detail
WHERE instruction_id = $1 AND company_code = $2
AND COALESCE(plan_qty, 0) > COALESCE(ship_qty, 0)`,
[instructionId, companyCode],
);
const unshipped = unshippedRes.rows[0]?.unshipped ?? 0;
const newStatus = unshipped === 0 ? "COMPLETED" : "IN_PROGRESS";
await client.query(
`UPDATE shipment_instruction
SET status = $1,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[newStatus, instructionId, companyCode],
);
}
}
}
@@ -569,12 +594,18 @@ export async function getShipmentInstructions(
) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
const { keyword, customer } = req.query;
const conditions: string[] = ["si.company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
if (customer) {
conditions.push(`si.partner_id = $${paramIdx}`);
params.push(customer);
paramIdx++;
}
if (keyword) {
conditions.push(
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`,
@@ -591,6 +622,7 @@ export async function getShipmentInstructions(
si.instruction_no,
si.instruction_date,
si.partner_id,
COALESCE(c.customer_name, si.partner_id, '') AS customer_name,
si.status AS instruction_status,
sid.item_code,
sid.item_name,
@@ -605,6 +637,9 @@ export async function getShipmentInstructions(
JOIN shipment_instruction_detail sid
ON si.id = sid.instruction_id
AND si.company_code = sid.company_code
LEFT JOIN customer_mng c
ON si.partner_id = c.customer_code
AND si.company_code = c.company_code
WHERE ${conditions.join(" AND ")}
AND COALESCE(sid.plan_qty, 0) > COALESCE(sid.ship_qty, 0)
ORDER BY si.instruction_date DESC, si.instruction_no`,

View File

@@ -17,30 +17,32 @@ router.get("/info", async (req: Request, res: Response) => {
return res.status(401).json({ success: false, message: "인증 정보 없음" });
}
const conditions: string[] = ["company_code = $1", "is_active = 'Y'"];
const conditions: string[] = ["iii.company_code = $1", "iii.is_active = '사용'"];
const params: unknown[] = [companyCode];
let idx = 2;
if (itemCode) {
conditions.push(`item_code = $${idx++}`);
conditions.push(`iii.item_code = $${idx++}`);
params.push(itemCode);
}
if (itemId) {
conditions.push(`item_id = $${idx++}`);
conditions.push(`iii.item_id = $${idx++}`);
params.push(itemId);
}
if (inspectionType) {
conditions.push(`inspection_type = $${idx++}`);
conditions.push(`iii.inspection_type = $${idx++}`);
params.push(inspectionType);
}
const sql = `
SELECT id, item_id, item_code, item_name,
inspection_type, inspection_item_name, inspection_standard,
inspection_method, pass_criteria, is_required, sort_order, memo
FROM item_inspection_info
SELECT iii.id, iii.item_id, iii.item_code, iii.item_name,
iii.inspection_type, iii.inspection_item_name, iii.inspection_standard,
iii.inspection_method, iii.pass_criteria, iii.is_required, iii.sort_order, iii.memo,
ist.judgment_criteria
FROM item_inspection_info iii
LEFT JOIN inspection_standard ist ON ist.id = iii.inspection_standard_id
WHERE ${conditions.join(" AND ")}
ORDER BY sort_order, inspection_item_name
ORDER BY iii.sort_order, iii.inspection_item_name
`;
try {