다크모드
BE 참고 — 선수금·가수금 GL 전표 (E5 · E-ref · E-reclass)
독자: BE 개발자/AI. 선수금(0259) 차월충당·환급, 가수금(0257) 재분류의 GL 전표 3종(id·이벤트·계정·파생 소스·불변식)을 정의한다. 표기: as-built(현재 프로토 동작) vs 확정규칙(BE 구현 시 따를 규칙). 다르면 명시한다. 정본 연계:
docs/PROCESS-RECEIPTS-CANON-2026-06-19.md§3-F(선수금 차월충당·환급·가수금 재분류 GL 전표) · §3-G(전표 상태 맵, 본 3종 모두 as-built로 승격). 수납 코어 정본:docs/handoff/backend/collecting-detail.md.
1. 상태: as-built
과거 §3-F는 "GL 전표가 없다"였으나, 2026-07-02 구현 완료. useBillingJournal.vouchersAdvanceMovements()가 선수금 원장에서 E5·E-ref를 파생하고, vouchersCollection()이 재분류 영수증을 E9(원 현금유입)+E-reclass 2전표로 분리한다. 정본 규칙 = 본 문서 + PROCESS-RECEIPTS-CANON §3-F/§3-G.
2. 전표 3종 계약
| 코드 | 전표 id | 이벤트 | 분개 | 파생 소스 |
|---|---|---|---|---|
| E5 | bj-adv-apply-{cc}-{seq} | 선수금충당 | 차 선수금(0259) / 대 미수관리비(0108) — 현금 무이동 | useReceipts.advanceLedger(cc)의 kind==='충당' 엔트리 |
| E-ref | bj-adv-refund-{cc}-{seq} | 선수금환급 | 차 선수금(0259) / 대 보통예금(0103) | advanceLedger(cc)의 kind==='환급' 엔트리 |
| E-reclass | bj-reclass-{receiptId} | 가수금재분류 | 차 가수금(0257) / 대 미수관리비(0108) [· 대 선수금(0259)] — 현금 라인 없음 | 영수증 fromSuspense===true (useReceipts.reclassifySuspense가 설정) |
| 개시선수금 | bj-open-adv-{cc} | 개시선수금 | 차 이월이익잉여금(0375) / 대 선수금(0259) — BS-only(E1 대칭) | openingAdvances 마스터(buildSeed().openingAdvances) |
{cc} = 계약코드(contractCode), {seq} = advanceLedger(cc) 배열 내 0-base index를 4자리 zero-pad(String(i+1).padStart(4,'0')) — 계약 내 원장 엔트리 유니크 인덱스(충당·환급·적립·취소 전부 포함한 전체 배열 위치, 종류별 재넘버링 아님).
개시선수금 전표 (bj-open-adv-{cc}) — W1
개시 시점(온보딩)에 이미 선납·과오납 상태였던 선수금은 openingAdvances 마스터({contractCode, amount, memo}[])에서 직접 전표화한다 — receipts 경로(creditAdvance)를 거치지 않는 별도 소스.
- 일자:
POSTING_DATE(cutover, 2025-12-28) — 시드 하드코딩 일자(과거 2026-06-09)를 쓰지 않는다. - 소스:
useBillingJournal.vouchersOpeningAdvance()—openingAdvances항목별 1전표. - E1과 대칭: E1(
vouchersOpeningReceivable)이 개시미수를차 미수관리비(0108) / 대 이월이익잉여금(0375)로 여는 것과 짝을 이뤄, 개시선수금은차 이월이익잉여금(0375) / 대 선수금(0259)로 연다. 둘 다 0375을 상대계정으로 쓰는 BS-only 개시 전표. billingVouchers()에 편입 — 전체 GL 배출 파이프라인의 일부.
계정코드 소스: useBillingJournal.js 상단 상수 — E4_CASH='0103', E4_RECEIVABLE=toCore('9001')(='0108'), E_ADVANCE='0259'(선수금), E_SUSPENSE='0257'(가수금).
E-reclass 라인 상세
vouchersCollection()이 재분류 영수증(r.fromSuspense===true)을 만나면 2전표를 낸다:
- E9(
bj-sus-{r.id},가수금) — 원 미식별 입금 시점의 현금유입.차 보통예금(0103) / 대 가수금(0257), 금액r.amount. 재분류 후에도 유지(원 현금유입 사실은 불변). 일자 =r.receivedAt(거래일 — W2-R2): 과거POSTING_DATE고정이었으나, 전표쌍(E9↔E-reclass)이 서로 다른 기간에 분열해 반쪽 전기·마감 후 신규 입금 영구 전기불가를 유발(FB-2) — 2026-07-06receivedAt로 교체. - E-reclass(
bj-reclass-{r.id},가수금재분류) — 식별·재분류 시점.차 가수금(0257) r.amount / 대 미수관리비(0108) receivableCredit [· 대 선수금(0259) r.toAdvance].receivableCredit = r.applied + (발생주의 ? r.appliedLateFee : 0)(0보다 클 때만 라인 추가).r.toAdvance도 0보다 클 때만 대변 선수금 라인 추가.
미식별(!r.identified) 또는 미재분류 상태의 가수금은 E9 1전표만 낸다(재분류 전이면 E-reclass 없음).
3. 파생 소스 — 원장 kind 스위치
useReceipts.advanceLedgerByContract[cc]는 계약별 선수금 원장({ kind, amount, balance, at, memo }[]). advanceLedgerContracts()가 원장 보유 계약 코드 목록, advanceLedger(cc)가 해당 계약 원장 스냅샷(카피).
vouchersAdvanceMovements()는 advanceLedgerContracts()를 순회하며 각 계약의 advanceLedger(cc)를 kind로 스위치:
| kind | 발생 함수 | GL 처리 |
|---|---|---|
적립 | creditAdvance(과오납·선납 적립, applyReceipt 내부 호출 또는 seedDemoReceipts의 openingAdvances 초기화) | 스킵 — 이미 E4(수납, bj-rcp-*)·E-reclass 전표의 toAdvance 크레딧 라인, 또는 개시분은 bj-open-adv-{cc}(개시선수금 전표, W1)로 기전표화됨. 별도 E5/E-ref 없음(이중계상 방지). |
충당 | debitAdvance(cc, amt, at, '충당', memo) — autoApplyAdvance(cc)(차월 자동충당) 경로 | E5 |
환급 | refundAdvance(cc, amount, at) → debitAdvance(cc, amount, at, '환급', ...) | E-ref |
취소 | cancelReceipt → debitAdvance(cc, r.toAdvance, ..., '취소', '수납취소') | 스킵 — 영수증 자체가 status==='취소'로 vouchersCollection() 필터에서 제외되므로 원 전표도 이미 배제. 원장의 취소 kind는 잔액 역산 기록일 뿐 별도 GL 라인 아님. |
재분류(E-reclass)는 위 원장 스위치와 별도 경로 — 원장에 kind를 남기지 않고, 영수증 자체의 fromSuspense/identifiedAt 플래그로 vouchersCollection()이 직접 판정한다(§2 참조).
4. 불변식
| # | 불변식 | 근거 |
|---|---|---|
| ① | 재분류 시 현금(0103) 차변은 E9 1회뿐 — 이중계상 없음 | E-reclass 라인에 accountCode==='0103' 없음(가수금↔미수/선수금 대체만). 테스트: useBillingJournal.spec.js(가수금 재분류 그룹) |
| ② | 가수금(0257) 순액 = E9 대변 − E-reclass 차변 = 0(전액 재분류 시) | 둘 다 r.amount 동일 금액이므로 완전 재분류 시 0257 넷 0 |
| ③ | E5는 현금 무이동 | E5 라인에 accountCode==='0103' 없음(0259↔0108 대체만). 테스트: '현금 라인 없음' 단언 |
| ③-a | 개시 '적립' 원장은 개시전표가 커버 — vouchersAdvanceMovements()가 별도 방출하지 않는다(적립 skip 불변) | openingAdvances발 advanceLedger 적립 엔트리는 §3 표의 스킵 규칙 그대로 적용되고, GL 커버는 vouchersOpeningAdvance()(bj-open-adv-{cc}, W1)가 전담. 이중계상 방지 원리가 수납발 적립과 개시발 적립에 동일 적용(§3 kind 스위치 참조) |
| ④ | r.applied + r.appliedLateFee + r.toAdvance === r.amount(수납 잔액 균형) | useReceipts.applyReceipt — principalCash = r.amount − r.appliedLateFee → 미수충당(applied) + 잔여선수금(toAdvance) = principalCash. 연체료 충당분(appliedLateFee)이 있는 입금은 그만큼 원금충당 대상이 줄어든다. 가수금 재분류 영수증은 appliedLateFee===0이라 E-reclass에서 applied + toAdvance === r.amount로 단순화 |
| ⑤ | 전표 일자 = 거래 발생일 | E5/E-ref: l.at(원장 엔트리 시각, debitAdvance/refundAdvance 호출 시 전달한 날짜). E-reclass: `r.identifiedAt |
각 전표는 status:'미전기'로 생성(전기 상태는 후속 워크플로우), 전 라인 balanced(차변합=대변합) 검증 필요.
5. 엣지케이스
- 부분 식별(후속): 현재
reclassifySuspense(receiptId, contractCode, at)는 가수금 항목 전액을 1개 계약으로 재분류한다. 가수금 1건을 여러 계약으로 분할 재분류하는 시나리오는 미구현(디퍼) —suspensePool엔트리는 receipt 1건=1항목 모델. - 재분류 후 취소: 재분류(E-reclass 발행) 후 그 영수증을
cancelReceipt로 취소하는 경로는 현행 미지원(디퍼).cancelReceipt는receipt.identified && receipt.contractCode분기로 일반 식별 수납과 동일 취소 처리를 타지만, E-reclass 전표 자체의 역분개(전표 회수)는 별도 검증 없음 — 실서비스 전에 케이스 확정 필요. - 선수금 여러 충당 엔트리: 한 계약이 여러 차례 자동충당(
autoApplyAdvance)되면advanceLedger(cc)에충당kind 엔트리가 다건 쌓이고, 각각 별도 E5 전표로 방출된다(seq는 원장 배열 내 index라 계약 내 유니크 — 적립/환급/취소와 섞여도 index 겹치지 않음). - 원장 vs 영수증 이중경로: E5/E-ref는 "원장"(
advanceLedgerByContract) 기반, E-reclass는 "영수증"(receipts[].fromSuspense) 기반 — 서로 다른 데이터 소스지만 상호 배타적 이벤트(재분류는 원장에 kind를 남기지 않음)라 전표 중복 없음.
6. as-of 규칙 — 가수금·선수금 시간단면 (W2-R3·FB-14)
GL(전표일)은 identifiedAt 원점(§2 E-reclass)을 쓰는데 서브원장(
suspenseBalance/asOfInflow)이 receivedAt 원점을 쓰면, 과거 as-of 조회에서 재분류가 가수금을 소급 소거해 GL과 서브원장이 분열한다(FB-14). 아래 규칙으로 두 축을 정합.
- 가수금(
useReceipts.suspenseBalance(asOf)): 무인자 호출(현재상태 UI)은 기존suspensePool그대로(동치·무변경).asOf지정 시receipts파생으로 판정:- 미식별(
fromSuspense===false,!identified):receivedAt <= asOf⇒ 가수금에 포함. - 재분류됨(
fromSuspense===true):receivedAt <= asOf < identifiedAt구간만 가수금 —asOf >= identifiedAt이면 이미 계약 귀속(제외). 식별 전 시점의 as-of가 재분류로 소급 변조되지 않는다. status==='취소'는 항상 제외. 재분류취소(cancelReceipt)는fromSuspense=false·identifiedAt=null로 리셋되어 미식별 규칙에 자연 복귀(별도 분기 불요).
- 미식별(
- 계약 미수 유효 편입일(
useCollectingAsOf.asOfInflow):effectiveInflowDate(r) = r.fromSuspense ? r.identifiedAt : r.receivedAt—principalCash·allReceiptsToAdvance필터 모두 이 유효일 기준(재분류 영수증은 식별 시점에 비로소 그 계약의 as-of 미수에 반영, GL의 E-reclass 일자와 정합). - 선수금 적립일(
useReceipts.applyReceipt의 잔여creditAdvance):fromSuspense ? identifiedAt : receivedAt— 재분류로 발생한 잔여 선수금은 식별일에 적립된 것으로 기록(receivedAt 적립 시 선수금 원장 backdate 오류 — Codex G-1 측면 해소).
검증: useReceipts.spec.js·useCollectingAsOf.spec.js 신규 5건(감사 B-3 시나리오 — 12월 입금·익월 식별, 식별 전 asOf 가수금 유지·계약 미수 불변·GL 일자 대사).
7. 관련 파일
| 레이어 | 파일 |
|---|---|
| 선수금·가수금 원장/재분류 | src/composables/useReceipts.js(advanceLedgerByContract·advanceLedger·advanceLedgerContracts·reclassifySuspense·refundAdvance·autoApplyAdvance) |
| GL 전표 파생 | src/composables/useBillingJournal.js(vouchersAdvanceMovements·vouchersCollection·vouchersOpeningAdvance — W1) |
| 개시선수금 마스터/시드 | src/composables/demoSeed.js(openingAdvances)·src/composables/useReceipts.js(seedDemoReceipts가 creditAdvance(cc, amount, POSTING_DATE, memo)로 초기화) |
| 테스트 | src/composables/__tests__/useBillingJournal.spec.js(E5·E-ref·가수금 재분류·개시선수금 그룹) |
| FE 콘솔 | docs/handoff/frontend/advance-suspense-console.md(선수금·가수금 콘솔 UI) |