Skip to content

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이벤트분개파생 소스
E5bj-adv-apply-{cc}-{seq}선수금충당차 선수금(0259) / 대 미수관리비(0108) — 현금 무이동useReceipts.advanceLedger(cc)kind==='충당' 엔트리
E-refbj-adv-refund-{cc}-{seq}선수금환급차 선수금(0259) / 대 보통예금(0103)advanceLedger(cc)kind==='환급' 엔트리
E-reclassbj-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전표를 낸다:

  1. E9(bj-sus-{r.id}, 가수금) — 원 미식별 입금 시점의 현금유입. 차 보통예금(0103) / 대 가수금(0257), 금액 r.amount. 재분류 후에도 유지(원 현금유입 사실은 불변). 일자 = r.receivedAt(거래일 — W2-R2): 과거 POSTING_DATE 고정이었으나, 전표쌍(E9↔E-reclass)이 서로 다른 기간에 분열해 반쪽 전기·마감 후 신규 입금 영구 전기불가를 유발(FB-2) — 2026-07-06 receivedAt로 교체.
  2. 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 내부 호출 또는 seedDemoReceiptsopeningAdvances 초기화)스킵 — 이미 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
취소cancelReceiptdebitAdvance(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 불변)openingAdvancesadvanceLedger 적립 엔트리는 §3 표의 스킵 규칙 그대로 적용되고, GL 커버는 vouchersOpeningAdvance()(bj-open-adv-{cc}, W1)가 전담. 이중계상 방지 원리가 수납발 적립과 개시발 적립에 동일 적용(§3 kind 스위치 참조)
r.applied + r.appliedLateFee + r.toAdvance === r.amount(수납 잔액 균형)useReceipts.applyReceiptprincipalCash = 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로 취소하는 경로는 현행 미지원(디퍼). cancelReceiptreceipt.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.receivedAtprincipalCash·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(openingAdvancessrc/composables/useReceipts.js(seedDemoReceiptscreditAdvance(cc, amount, POSTING_DATE, memo)로 초기화)
테스트src/composables/__tests__/useBillingJournal.spec.js(E5·E-ref·가수금 재분류·개시선수금 그룹)
FE 콘솔docs/handoff/frontend/advance-suspense-console.md(선수금·가수금 콘솔 UI)