feat(momo v0.6): 전자세금계산서 메뉴 등록 + 거래명세표에 발행 버튼 추가
Deploy momo-erp / deploy (push) Successful in 51s

[메뉴 마이그레이션 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:
chpark
2026-05-07 16:28:07 +09:00
parent 0b0749cfb1
commit e65ea43429
2 changed files with 103 additions and 3 deletions
+25
View File
@@ -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;
+78 -3
View File
@@ -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>
);
}