[메뉴 마이그레이션 013] - objid=9000404 '전자세금계산서' (parent=9000400 출고/정산) - url: /m/admin/einvoices, seq=13 [거래명세표 [세금계산서 발행] 버튼] - 관리자 발주 상세(/m/admin/orders) 거래명세표 미리보기 하단 - 출고완료(APPROVED/SHIPPED) 또는 입금완료(PAID) 상태에서 노출 - [세금계산서 발행] (과세 TAX) / [계산서(면세)] (TAXFREE) 두 버튼 분리 - 클릭 → 확인 모달 → /api/m/einvoices/issue 호출 → 결과 모달 (승인번호/처리방식 표시) - 발행 후 같은 화면에 "세금계산서 발행됨 (승인번호)" 표시 [현재 흐름 (v0.6)] 1. 거래처: 출고 요청 2. 담당자: 체크 + [출고] 버튼 → 재고 차감 + 거래명세표 메일 자동 발송 (status=APPROVED) 3. 담당자: 거래명세표에서 [세금계산서 발행] 버튼 클릭 → 전자세금계산서 발행 4. 발행 이력은 /m/admin/einvoices 메뉴에서 일괄 조회/엑셀 다운로드 [추후 옵션] - 출고 처리 시 자동 세금계산서 발행 토글 (지금은 명시 발행만) - nts-esero 어댑터 실 통신 활성화 (인증서 + ERP 연계 승인 후) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
-- 013_einvoice_menu.sql
|
||||
-- v0.6 (2026-05-07)
|
||||
-- 전자세금계산서 메뉴 등록 (출고/정산 그룹 9000400 아래)
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM menu_info WHERE objid = 9000404) THEN
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
|
||||
seq, menu_url, status, system_name, regdate)
|
||||
VALUES (9000404, '1', 9000400, '전자세금계산서', 'eTax Invoice',
|
||||
13, '/m/admin/einvoices', 'active', 'PMS', NOW());
|
||||
ELSE
|
||||
UPDATE menu_info
|
||||
SET parent_obj_id = 9000400,
|
||||
menu_name_kor = '전자세금계산서',
|
||||
menu_name_eng = 'eTax Invoice',
|
||||
menu_url = '/m/admin/einvoices',
|
||||
status = 'active'
|
||||
WHERE objid = 9000404;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -512,9 +512,12 @@ function StatementPreview({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(order.STATUS === "APPROVED" || order.STATUS === "SHIPPED") && (
|
||||
<div className="text-[11px] text-emerald-700 pt-2 border-t border-slate-200 inline-flex items-center gap-1">
|
||||
<Check size={12} /> 출고 완료 — {order.APPROVE_DATE}
|
||||
{(order.STATUS === "APPROVED" || order.STATUS === "SHIPPED" || order.STATUS === "PAID") && (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-slate-200 gap-2 flex-wrap">
|
||||
<div className="text-[11px] text-emerald-700 inline-flex items-center gap-1">
|
||||
<Check size={12} /> 출고 완료 {order.APPROVE_DATE && `— ${order.APPROVE_DATE}`}
|
||||
</div>
|
||||
<IssueEinvoiceButton order={order} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -585,3 +588,75 @@ function ExtraRow({ line, onSave, onDelete }: {
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function IssueEinvoiceButton({ order }: { order: DetailOrder }) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [issued, setIssued] = useState<{ ntsInvoiceNo?: string; provider?: string } | null>(null);
|
||||
|
||||
const onIssue = async (kind: "TAX" | "TAXFREE") => {
|
||||
const ok = await Swal.fire({
|
||||
icon: "question",
|
||||
title: `${kind === "TAX" ? "세금계산서" : "계산서(면세)"} 발행`,
|
||||
html: `<div class="text-sm">발주 <b>${order.ORDER_NO}</b><br>합계 ₩${fmt(order.TOTAL_AMOUNT)}<br><br>전자세금계산서를 발행하시겠습니까?</div>`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "발행",
|
||||
cancelButtonText: "취소",
|
||||
confirmButtonColor: "#0f766e",
|
||||
});
|
||||
if (!ok.isConfirmed) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = await fetch("/api/m/einvoices/issue", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, invoiceKind: kind, invoiceType: "NORMAL" }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
setIssued({ ntsInvoiceNo: j.ntsInvoiceNo, provider: j.provider });
|
||||
await Swal.fire({
|
||||
icon: "success",
|
||||
title: "발행 완료",
|
||||
html: `
|
||||
<div class="text-sm">
|
||||
<div>승인번호: <b>${j.ntsInvoiceNo ?? "-"}</b></div>
|
||||
<div>처리방식: <b>${j.provider}</b></div>
|
||||
${j.message ? `<div class="text-xs text-slate-500 mt-2">${j.message}</div>` : ""}
|
||||
</div>`,
|
||||
});
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: "발행 실패", text: j.message ?? "오류" });
|
||||
}
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
if (issued) {
|
||||
return (
|
||||
<span className="text-[11px] text-emerald-700 inline-flex items-center gap-1.5">
|
||||
<Check size={11} /> 세금계산서 발행됨
|
||||
{issued.ntsInvoiceNo && <span className="font-mono text-slate-500">({issued.ntsInvoiceNo})</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onIssue("TAX")}
|
||||
className="inline-flex items-center gap-1 h-8 px-2.5 rounded bg-rose-600 text-white text-[11px] font-bold hover:bg-rose-700 disabled:opacity-50"
|
||||
title="과세 전자세금계산서 발행"
|
||||
>
|
||||
세금계산서 발행
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onIssue("TAXFREE")}
|
||||
className="inline-flex items-center gap-1 h-8 px-2.5 rounded bg-violet-600 text-white text-[11px] font-bold hover:bg-violet-700 disabled:opacity-50"
|
||||
title="면세 계산서 발행"
|
||||
>
|
||||
계산서(면세)
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user