fix(orders): 수기 출고 — 라벨 변경 + 현재고 표시 + 삭제 + admin-panel 정리
Deploy momo-erp / deploy (push) Successful in 2m13s

1) '수기 발주' → '수기 출고' 라벨 변경 (버튼/타이틀).
2) detail STOCK_QTY: 거래처 default_wh_objid 분기 제거 → 항상 STOCK 류
   전체 합산 표시. customer=admin 또는 김포 시장 등 default 가 빈 창고일
   때 현재고 0 으로 표시되던 버그 fix. 실제 출고 차감은 approve 시
   default_wh_objid 또는 STOCK 첫 창고 기준 그대로.
3) /api/m/orders/delete (admin) — REQUESTED 상태 발주만 hard delete.
   수기 출고로 잘못 생성한 빈 발주 정리용. einvoice/items/orders 일괄.
4) 출고관리 detail (REQUESTED) 에 '삭제' 버튼 추가 — 반려 옆.
5) admin-panel 의 '공급업체관리' 메뉴 제거 (m/admin/vendors 별도 메뉴 사용).
This commit is contained in:
chpark
2026-05-14 23:45:29 +09:00
parent 470fa4884d
commit ea21dced45
4 changed files with 82 additions and 23 deletions
+35 -9
View File
@@ -804,13 +804,39 @@ function StatementPreview({
<span className="text-[11px] text-slate-400">
[] .
</span>
<button
onClick={() => onCancel(order)}
disabled={busy || shipping}
className="px-3 h-9 rounded-lg border border-rose-200 text-rose-700 text-xs font-semibold hover:bg-rose-50 disabled:opacity-50 inline-flex items-center gap-1"
>
<X size={12} />
</button>
<div className="flex items-center gap-2">
<button
onClick={async () => {
const ok = await Swal.fire({
icon: "warning",
title: `발주 ${order.ORDER_NO} 삭제`,
text: "수기 출고 등 잘못 만든 출고건을 완전히 삭제합니다. (취소 이력 없음)",
showCancelButton: true,
confirmButtonColor: "#dc2626",
confirmButtonText: "삭제",
});
if (!ok.isConfirmed) return;
const res = await fetch("/api/m/orders/delete", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objid: order.OBJID }),
});
const j = await res.json();
if (j.success) { onReloadList(); }
else Swal.fire({ icon: "error", title: "삭제 실패", text: j.message });
}}
disabled={busy || shipping}
className="px-3 h-9 rounded-lg border border-rose-300 bg-white text-rose-700 text-xs font-bold hover:bg-rose-50 disabled:opacity-50 inline-flex items-center gap-1"
>
<X size={12} />
</button>
<button
onClick={() => onCancel(order)}
disabled={busy || shipping}
className="px-3 h-9 rounded-lg border border-rose-200 text-rose-700 text-xs font-semibold hover:bg-rose-50 disabled:opacity-50 inline-flex items-center gap-1"
>
<X size={12} />
</button>
</div>
</div>
)}
{(order.STATUS === "APPROVED" || order.STATUS === "PAID") && (
@@ -1186,9 +1212,9 @@ function ManualOrderButton({ onCreated }: { onCreated: (newObjid: string) => voi
onClick={onClick}
disabled={busy}
className="h-9 px-3 rounded-lg bg-white border border-amber-300 text-amber-700 text-sm font-bold hover:bg-amber-50 disabled:opacity-50 inline-flex items-center gap-1.5"
title="빈 발주를 생성하여 오른쪽에서 거래처/품목 채워가는 흐름"
title="빈 출고건을 생성하여 오른쪽에서 거래처/품목 채워가는 흐름"
>
<PhoneCall size={14} />
<PhoneCall size={14} />
</button>
);
}
-1
View File
@@ -33,7 +33,6 @@ const ADMIN_MENUS = [
{
label: "기준정보관리", icon: Database,
items: [
{ key: "supply" as AdminTab, label: "공급업체관리" },
{ key: "template" as AdminTab, label: "템플릿 관리" },
{ key: "exchange" as AdminTab, label: "환율관리" },
],
+40
View File
@@ -0,0 +1,40 @@
// 발주 hard delete — admin 만, REQUESTED 상태만 (수기 출고 정리용).
// 출고 처리(APPROVED) 이후는 재고/이력 정합성 때문에 cancel(CANCELED) 흐름 사용.
import { NextRequest, NextResponse } from "next/server";
import { pool } from "@/lib/db";
import { requireMomoAdmin } from "@/lib/momo-guard";
export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const { objid } = await req.json() as { objid?: string };
if (!objid) return NextResponse.json({ success: false, message: "objid 누락" }, { status: 400 });
const client = await pool.connect();
try {
await client.query("BEGIN");
const cur = await client.query(`SELECT status FROM momo_orders WHERE objid = $1 FOR UPDATE`, [objid]);
if (cur.rowCount === 0) {
await client.query("ROLLBACK");
return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 });
}
if (cur.rows[0].status !== "REQUESTED") {
await client.query("ROLLBACK");
return NextResponse.json({ success: false, message: "출고요청 상태만 삭제 가능합니다. 출고된 발주는 취소 처리하세요." }, { status: 400 });
}
// 연관 데이터 정리 (REQUESTED 단계라 stock_moves/payments 없음)
await client.query(`DELETE FROM momo_einvoice_items WHERE einvoice_objid IN (SELECT objid FROM momo_einvoices WHERE order_objid = $1)`, [objid]);
await client.query(`DELETE FROM momo_einvoices WHERE order_objid = $1`, [objid]);
await client.query(`DELETE FROM momo_order_items WHERE order_objid = $1`, [objid]);
await client.query(`DELETE FROM momo_orders WHERE objid = $1`, [objid]);
await client.query("COMMIT");
return NextResponse.json({ success: true });
} catch (err) {
await client.query("ROLLBACK");
console.error("[orders/delete]", err);
return NextResponse.json({ success: false, message: err instanceof Error ? err.message : "오류" }, { status: 500 });
} finally {
client.release();
}
}
+7 -13
View File
@@ -65,20 +65,14 @@ export async function POST(req: NextRequest) {
I.unit AS "UNIT",
I.image_url AS "IMAGE_URL",
-- 현재고: 거래처에 default_wh_objid 가 있으면 그 창고 재고, 없으면 STOCK 류 전체 합
-- (출고가 실제로 일어날 창고 기준 — approve 로직과 일관)
-- 거래처 default 창고가 비어있을 수 있어, 항상 STOCK 류 전체 합산 표시.
-- (실제 출고는 approve 시 default 창고 또는 STOCK 첫 창고에서 차감)
COALESCE(
CASE
WHEN UD.default_wh_objid IS NOT NULL THEN
(SELECT SUM(S.qty) FROM momo_stocks S
WHERE S.item_objid = OI.item_objid
AND S.wh_objid = UD.default_wh_objid::text)
ELSE
(SELECT SUM(S.qty) FROM momo_stocks S
JOIN momo_warehouses W ON W.objid = S.wh_objid
WHERE S.item_objid = OI.item_objid
AND W.wh_type IN ('STOCK','HQ_STOCK','KIMPO_STOCK')
AND COALESCE(W.is_del,'N') != 'Y')
END,
(SELECT SUM(S.qty) FROM momo_stocks S
JOIN momo_warehouses W ON W.objid = S.wh_objid
WHERE S.item_objid = OI.item_objid
AND W.wh_type IN ('STOCK','HQ_STOCK','KIMPO_STOCK')
AND COALESCE(W.is_del,'N') != 'Y'),
0
) AS "STOCK_QTY"
FROM momo_order_items OI