Skip to content

매입 P3(선지급)/P6(미식별출금) — BE 참고

골드너스 통합 P-시리즈 두 번째 슬라이스(P1/P2 위에 얹음). 정본 설계: docs/superpowers/specs/2026-07-07-purchasing-p3-p6-design.md. 정본 블루프린트: docs/BLUEPRINT-PURCHASING-ACCOUNTING-2026-07-05.md(§2 P3·P6 행 — 이번 슬라이스로 as-built 승격). P1/P2 계약은 docs/handoff/backend/purchasing-p1-p2.md 참고 — 이 문서는 그 위에 얹힌 확장만 다룬다. E-시리즈 대칭:

useReceipts.creditAdvance/debitAdvance/autoApplyAdvance (E5 선수금 차월충당)  ↔  usePurchases.creditPrepaid/debitPrepaid/applyPrepaid (P3 선급금)
useReceipts.recordReceipt(identified:false)+reclassifySuspense (E9 가수금+E-reclass) ↔ usePayments.recordDisbursement+reclassifyDisbursement (P6+P-reclass)

1. 개요

스코프: P3(선지급 → 선급금 0131 적립·상계) · P6(미식별출금 → 가지급금 0134 · 식별 재분류) · P3/P6/P-reclass GL 전표 · payment 콘솔 확장(선지급 현황·미식별출금 큐·식별 다이얼로그). 스코프 아웃: 은행 자동수집→P6 큐(수동 등록만) · P4(카드) · P5(정정/반품) · 3자통과 · 선급금 환급(P-adv-refund) · 3원대사.

P1/P2 변경(중요): usePayments.recordPayment()의 "초과 지급 차단" 가드({ok:false, reason:'채무 초과'})를 완전히 제거했다 — 초과분은 이제 항상 성공하며 선급금으로 적립된다(§3). 기존 "초과 가드" 테스트는 폐기·재작성됐다(FE 핸드오프 §7 참고).

2. 데이터 모델 확장

2-1. usePurchases.js — 선급금(P3) 신규 상태

prepaidByVendor         // vendorCode → 선급금 잔액(원)
prepaidLedgerByVendor   // vendorCode → [{ kind('적립'|'상계'|'취소'), amount(부호있는 증감),
                         //                balance(그 시점 잔액), at, memo, appliedBreakdown? }]
autoApplyByVendorPrepaid // vendorCode → boolean (미설정 기본 true = 자동, useReceipts.getAutoApply 미러)
  • creditPrepaid(vendorCode, amount, at, memo): 잔액 += amount, 원장에 kind:'적립' 기록.
  • debitPrepaid(vendorCode, amount, at, kind, memo, appliedBreakdown?): use = min(avail, amount) 클램프(무음, 정책 판단 없음) 후 차감, 원장에 기록. appliedBreakdown은 '상계' 이벤트에만 실려 GL 그룹핑에 쓰인다(§3-2).
  • applyPrepaid(vendorCode): 선급금 잔액을 신규 열린 채무에 FIFO 상계(pay() 재사용) — getAutoApplyPrepaid(vendorCode)가 false면 스킵. 온디맨드 호출(recordPurchase에 자동 배선하지 않음 — 원장 append-only 순수성 유지, useReceipts.autoApplyAdvance 선례 동형). 현재 호출부는 payment 콘솔의 [상계] 버튼뿐(§5).
  • prepaidBalanceByVendor() / prepaidLedger(vendorCode) / prepaidLedgerVendors(): 조회 파생.

2-2. usePayments.js — 지급(P3) + 미식별출금(P6) 확장

payment: { ..., applied, toPrepaid, appliedBreakdown, status }  // toPrepaid 신규 필드(P1/P2 스키마 확장)
disbursement: {
  id, amount, disbursementDate, memo, payerHint,
  identified, fromDisbursement, identifiedAt,      // 재분류 상태
  vendorCode, purpose('지급'|'선급'|'경비'|null), accountCode,
  applied, toPrepaid, appliedBreakdown, status
}
suspenseDisbursementPool  // [{id, payerHint, amount, disbursementDate, memo}] — 가지급금 미귀속 풀
  • recordPayment({...}): pay()toPrepaid = amount - applied. toPrepaid > 0이면 creditPrepaid(vendorCode, toPrepaid, ...) 호출. 항상 {ok:true}(amount≤0 가드만 예외) — 구 초과차단 가드 완전 제거.
  • recordDisbursement({amount, disbursementDate, memo, payerHint}): 가지급금 풀에 미귀속 원자 적재. amount>0 가드 없음(콘솔이 선제 검증 — §4 FE 확장 포인트 참고, usePayments.js 헤더 주석에 명시).
  • disbursementBalance(asOf?): asOf 미지정 = 현재 풀(suspenseDisbursementPool) 스냅샷. asOf 지정 시 = disbursements 파생(lte 기반, 이월 시점 정확성 — E9 W2-R3/FB-14 규칙 그대로 미러: 재분류된 건도 identifiedAt 이전 asOf엔 여전히 풀 소속으로 계산).
  • reclassifyDisbursement(id, {vendorCode, purpose, accountCode}, at?): 풀에서 제거 + disbursement 필드 갱신. purpose 분기:
    • '지급': pay(vendorCode, amount) FIFO 충당 → 초과분은 creditPrepaid로 선급금 적립(P3 대칭).
    • '선급': 전액 creditPrepaid(충당 없이 처음부터 선급 의도).
    • '경비': accountCode만 저장(채무/선급 영향 없음 — Task3가 차변 경비계정으로 직접 방출).
    • payments[]에 새 원자를 만들지 않는다 — 현금은 P6 원 전표에서 이미 유출됐다(§3 이중계상 금지).
  • cancelDisbursement(id): fromDisbursement && identified면 재분류만 원복(unpay/debitPrepaid 역산 + 풀 복원, P6 전표는 불변). 미식별 상태 그대로면 status='취소'(전액 취소).

3. GL 전표 계약 확장 (usePurchaseJournal.js)

P2/P3 — 지급 (vouchersPayment()) — ★T1 SEAM FIX

DR payableAccountCode(appliedBreakdown 그룹핑, PAYABLE_ACCOUNT_ORDER=['0251','0253'])
DR 0131(선급금, toPrepaid>0일 때만)
/ CR 0103(보통예금, = pay.amount 전액 = applied+toPrepaid)

교정 이력: 종전엔 CR 0103 = pay.applied만 방출해 toPrepaid(초과·선행 지급분)의 현금유출이 전표에서 누락됐다(0103 과소계상 — 현금총액 대사 불능). E4 복합분개(DR 보통예금/CR 미수+선수금) 매입측 대칭으로 DR 0131 라인을 추가하고 CR을 pay.amount(전액)로 교정했다. totalpay.appliedpay.amount로 변경.

P-adv-apply — 선급금 차월충당(vouchersPrepaidApply())

DR payableAccountCode(appliedBreakdown 채무계정별 그룹핑) / CR 0131

현금 라인 없음(E5 차월충당 미러 — applyPrepaid는 현금이동을 만들지 않는다). prepaidLedger(vendorCode)kind==='상계' 엔트리에서 파생. id = pj-adv-apply-{vendorCode}-{seq}.

P6 — 미식별출금(vouchersDisbursement())

DR 0134(가지급금) / CR 0103(보통예금)

재분류돼도 원 전표는 유지(항상 방출 — 현금 유출 사실은 불변). status==='취소'(미식별 상태 그대로 취소된 것)만 제외. id = pj-dsb-{disbursement.id}.

P-reclass — 가지급금 재분류(vouchersReclassDisbursement())

purpose='지급': DR payableAccountCode(그룹핑) [+ DR 0131(toPrepaid, 초과분)] / CR 0134
purpose='선급': DR 0131(전액) / CR 0134
purpose='경비': DR accountCode / CR 0134

현금 라인 없음(P6에서 이미 유출됨 — 이중 현금계상 금지, 가수금 재분류 모델 정정 감사 교훈 상속). fromDisbursement && identified && status!=='취소'인 것만 방출(재분류취소 시 자동 소멸). id = pj-reclass-{disbursement.id}.

전표 발생 순서(purchaseVouchers())

js
[
  ...vouchersPurchase(),
  ...vouchersPayment(),
  ...vouchersPrepaidApply(),
  ...vouchersDisbursement(),
  ...vouchersReclassDisbursement(),
];

4. 불변식

  1. P2/P3 전표균형: DR(payableAccountCode 그룹핑 + 0131) 합 === CR(0103) 합 === pay.amount.
  2. P-adv-apply/P-reclass 현금無: 두 전표군 어디에도 accountCode==='0103' 라인이 없다.
  3. P6 재분류 현금 1회: 재분류(reclassifyDisbursement) 전후로 vouchersPayment()+vouchersDisbursement()0103 CR 합계가 불변(재분류가 새 현금 라인을 만들지 않는다는 것의 직접 증거 — §7 실측).
  4. 가지급금 순액: 재분류 완료 건은 netOf('0134') = P6(DR d.amount) - P-reclass(CR d.amount) = 0(그 건에 한해). 미식별 잔여 건만 순액이 남는다.
  5. GL 대사(5계정): netOf('0251')+netOf('0253')(CR우세, 부호반전) === payableBalanceByVendor() 합. netOf('0131')(DR우세) === prepaidBalanceByVendor() 합. netOf('0134')(DR우세) === disbursementBalance().total(풀 스냅샷, 재분류된 건 제외). §7 실측 참고.
  6. 현금총액 대사: Σ(vouchersPayment CR0103) + Σ(vouchersDisbursement CR0103) === Σ(비취소 payments.amount) + Σ(비취소 disbursements.amount). 취소된 지급/출금은 자동 제외(전표 자체가 vouchers*()에서 필터링됨).
  7. 회귀0: resetPurchases()/resetPayments()가 신규 상태(prepaid*, disbursement*)도 전부 초기화 — 스펙 beforeEach 필수.

5. GL 대사 실측 (데모 시드, DEMO_CONFIG.householdCount=15 기준, scale=0.05)

seedDemoPurchases()seedDemoPayments()seedDemoDisbursements() 순서 부트스트랩(main.js 순서 그대로) 후 재실행:

계정DRCR잔액(net)서브원장일치
0251 외상매입금0165,000165,000(0253과 합산 대사)
0253 미지급금346,500621,500275,000(0251과 합산 대사)
0251+0253 합계440,000payableBalanceByVendor() 합 = 440,000
0131 선급금8,50008,500prepaidBalanceByVendor() 합 = 8,500(v-svc-security)
0134 가지급금6,00006,000disbursementBalance().total = 6,000(dsb-1, 미식별)
0103 보통예금(CR만 발생)0361,000Σ payments.amount(355,000) + Σ disbursements.amount(6,000) = 361,000

재분류 시나리오 추가 검증(위 시드에 reclassifyDisbursement('dsb-1', {vendorCode:'v-goods-supply', purpose:'지급'}, '2025-12-23') 적용):

  • 재분류 전/후 Σ CR 0103 = 361,000 → 361,000(불변, 현금 라인 신규 생성 없음).
  • 재분류 후 netOf('0134') = 0(가지급금 완전 대체 — P6 DR 6,000 = P-reclass CR 6,000).
  • vouchersReclassDisbursement().length = 1(재분류된 건 1개만 방출).

vendor 상세: payableBalanceByVendor() = [{v-svc-mgmt: 275,000}, {v-goods-supply: 165,000}](v-svc-cleaning·v-svc-security는 완납이라 목록 자체에서 제외 — outstanding<=0 필터). payments 상세 = v-svc-mgmt(applied 165,000/toPrepaid 0), v-svc-cleaning(applied 165,000/toPrepaid 0), v-svc-security(applied 16,500/toPrepaid 8,500 — 이것이 위 0131 8,500의 출처).

이 표는 docs/superpowers/sdd/task-5-report.md에 재실행 스크립트와 함께 원본 로그가 남아있다.

6. As-built vs 후속

As-built(이번 슬라이스): 선급금(0131) 적립·상계·조회 · 미식별출금(0134) 등록·큐·식별 재분류(지급/선급/경비) · P2/P3/P-adv-apply/P6/P-reclass GL 전표 · payment 콘솔 확장(선지급 현황·미식별출금 큐·식별 다이얼로그) · GL 대사(5계정+현금총액, §5).

후속(스코프 아웃):

  • 선급금 환급(P-adv-refund, 차 0103/대 0131) — refund 액션 자체가 코드베이스에 없음(spec §2 조건부 문구와 일치).
  • 은행 거래 자동 수집 → P6 큐 자동 등록(현재는 수동 recordDisbursement만).
  • P4(카드 매입)·P5(매입 정정/반품)·3자통과·3원 대사 리포트 — 전부 미착수.
  • 채무 이력 없는 완전 신규 거래처의 선지급 콘솔 노출: DialogOffsetArtvendorOptionspayableBalanceByVendor()(열린 채무 보유 거래처만)에서 파생돼, 매입 이력이 전혀 없는 vendorCode는 드롭다운에 나타나지 않는다. 컴포저블(usePayments.recordPayment)은 이 케이스를 지원(spec §3 red test 1 — 채무 없는 vendor 전액 선급 성공)하지만 콘솔 UI가 이 경로를 막고 있다(FE 핸드오프 §7 확장 포인트).
  • 미식별출금 재분류 취소 화면 버튼(cancelDisbursement는 구현돼 있으나 미배선).
  • 자동 상계(applyPrepaid)의 자동 트리거화(현재는 [상계] 버튼 수동 호출만 — autoApplyByVendorPrepaid 토글 필드는 존재하나 아직 이 값을 끄는 UI/자동배선 소비처가 없음).