SAP MM 모듈에서 송장(Invoice) 처리는 구매 프로세스의 마지막 정산 단계입니다. 잘못된 송장을 발견했을 때 화면에서는 MR8M 으로 취소를 수행하지만, 인터페이스나 자동화 배치에서는 표준 BAPI 인 BAPI_INCOMINGINVOICE_CANCEL 을 호출해서 처리해야 합니다.
이 BAPI 는 단순히 송장 전표를 "삭제" 하는 것이 아니라, 원본 송장과 반대 부호의 취소 전표(Reversal Document)를 새로 생성 해서 두 전표를 자동 청산(clearing) 시키는 구조입니다. 그래서 호출 후 원본 전표번호와 취소 전표번호가 각각 살아남고, 회계 추적이 끝까지 가능해집니다.
이 글에서는 송장 취소 BAPI 의 시그니처·취소 사유 코드·실제 호출 패턴·COMMIT 처리·자주 발생하는 오류를 정리합니다. 송장 생성 BAPI(BAPI_INCOMINGINVOICE_CREATE) 와 같이 사용해야 송장 등록·취소의 양방향 인터페이스가 완성됩니다.
핵심 — BAPI_INCOMINGINVOICE_CANCEL 시그니처
| 📋 구분 | 파라미터 | 타입 | 설명 |
|---|---|---|---|
| IMPORTING | INVOICEDOCNUMBER |
BAPI_INCINV_FLD-INV_DOC_NO |
취소할 송장 전표번호 (RBKP-BELNR) |
| IMPORTING | FISCALYEAR |
BAPI_INCINV_FLD-FISC_YEAR |
회계연도 (RBKP-GJAHR) |
| IMPORTING | REASONREVERSAL |
BAPI_INCINV_FLD-REASON_REV |
취소 사유 코드 (T041C 참조) |
| IMPORTING | POSTINGDATE (Optional) |
BAPI_INCINV_FLD-PSTNG_DATE |
취소 전표 전기일자 (생략 시 시스템 일자) |
| EXPORTING | INVOICEDOCNUMBER_REVERSAL |
BAPI_INCINV_FLD-INV_DOC_NO |
생성된 취소 전표번호 |
| EXPORTING | FISCALYEAR_REVERSAL |
BAPI_INCINV_FLD-FISC_YEAR |
취소 전표 회계연도 |
| TABLES | RETURN |
BAPIRET2 |
처리 결과 메시지 (Type E 가 하나라도 있으면 실패) |
송장 생성 BAPI(BAPI_INCOMINGINVOICE_CREATE) 가 헤더·아이템·세금·계정분개까지 수십 개 파라미터를 받는 것과 대조적으로, 취소 BAPI 는 "전표번호 + 회계연도 + 사유" 단 3가지 면 충분합니다. 취소 시 필요한 헤더·아이템 정보를 SAP 가 원본 전표에서 그대로 읽어와서 자동으로 반대 부호로 뒤집어 새 전표를 만들어주기 때문입니다.
1단계 — 취소 사유 코드 (REASONREVERSAL)
SAP 표준 취소 사유 코드는 T041C 테이블에 정의되어 있습니다. 회사 시스템에 따라 추가·변경될 수 있으니 운영 환경의 코드 마스터를 먼저 확인하시기 바랍니다.
| 코드 | 의미 | 설명 |
|---|---|---|
| 01 | 현 회계기간 내 취소 | 원본 전표와 동일 기간에 취소 (가장 일반적) |
| 02 | 전기 회계기간 취소 | 이전 기간 전표 취소 — 별도 전기일자 지정 필요 |
| 03 | 실제 취소 | 송장 자체가 잘못 발행된 경우 |
| 04 | 정정 전 취소 | 송장을 정정하기 위해 먼저 취소 |
| 05 | 회계 결산 정정 | 연도·반기 결산 과정에서 발생한 정정용 취소 |
자동화 배치에서 가장 많이 쓰는 값은 01 또는 03 입니다. 이전 기간 송장을 현재 기간에 취소하는 경우라면 반드시 02 와 함께 POSTINGDATE 를 명시해야 회계 기간 마감 오류를 피할 수 있습니다.
2단계 — 호출 전 사전 체크 (취소 가능 여부)
BAPI 는 내부적으로 reversal_allowed_check 라는 검증 PERFORM 을 수행해서 취소 가능 여부를 먼저 판단합니다. 다음 조건 중 하나라도 걸리면 RETURN 에 에러 메시지가 담겨 돌아오므로 호출 전 미리 검증해두면 불필요한 시도를 줄일 수 있습니다.
* 사전 체크 패턴
SELECT SINGLE rbstat " 송장 상태
stblg " 이미 취소된 전표번호
stjah " 이미 취소된 회계연도
zfbdt " 지급예정일
INTO (lv_rbstat, lv_stblg, lv_stjah, lv_zfbdt)
FROM rbkp
WHERE belnr = lv_invoice_doc
AND gjahr = lv_fiscal_year.
IF lv_stblg IS NOT INITIAL.
" 이미 취소된 전표 — 재취소 불가
MESSAGE 'Invoice already cancelled' TYPE 'E'.
ENDIF.
IF lv_rbstat = 'X'.
" 차단 상태 — 취소 불가
MESSAGE 'Invoice blocked' TYPE 'E'.
ENDIF.
RBKP-STBLG 가 채워져 있으면 이미 취소된 전표라는 뜻입니다. 같은 송장을 두 번 취소하려고 시도하면 표준 화면에서는 막혀 있지만, BAPI 직접 호출 시에는 RETURN 메시지로만 알려주므로 인터페이스 로직에서 이 컬럼을 먼저 확인하는 게 안전합니다.
3단계 — BAPI 호출 기본 패턴
가장 단순한 호출 형태입니다. 단일 전표 취소 + 시스템 일자 사용 + 사유 01.
DATA: lt_return TYPE TABLE OF bapiret2,
ls_return TYPE bapiret2,
lv_rev_doc TYPE bapi_incinv_fld-inv_doc_no,
lv_rev_year TYPE bapi_incinv_fld-fisc_year.
CALL FUNCTION 'BAPI_INCOMINGINVOICE_CANCEL'
EXPORTING
invoicedocnumber = '5100000001' " 취소할 송장 번호
fiscalyear = '2026'
reasonreversal = '01'
* postingdate = sy-datum " 생략 시 시스템 일자
IMPORTING
invoicedocnumber_reversal = lv_rev_doc
fiscalyear_reversal = lv_rev_year
TABLES
return = lt_return.
" 에러 체크
READ TABLE lt_return INTO ls_return
WITH KEY type = 'E'.
IF sy-subrc = 0.
" 에러 발생 — 롤백
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ELSE.
" 정상 처리 — 커밋
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING wait = 'X'.
WRITE: / '취소 전표번호:', lv_rev_doc,
/ '회계연도:', lv_rev_year.
ENDIF.
핵심은 호출 직후 RETURN 테이블의 Type 'E' 메시지 유무 검증 입니다. BAPI 자체는 호출 성공/실패 여부를 SY-SUBRC 로 알려주지 않고 RETURN 메시지로만 표현하므로 이 체크를 빼먹으면 실패한 취소를 성공으로 오인할 수 있습니다.
4단계 — COMMIT 처리 (필수)
표준 BAPI 패턴과 동일하게 BAPI_INCOMINGINVOICE_CANCEL 도 호출 자체로는 DB 에 반영되지 않습니다. BAPI_TRANSACTION_COMMIT 을 명시적으로 호출해야 취소 전표가 실제로 저장됩니다.
" 정상 처리 시
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = 'X'. " 동기 커밋 (DB 반영 완료까지 대기)
wait = 'X' 옵션은 DB 반영이 완료될 때까지 기다린 후 다음 라인으로 진행하라는 뜻입니다. 이 옵션 없이 커밋하면 호출은 비동기로 처리되어 직후 SELECT 시 아직 데이터가 보이지 않을 수 있습니다. 인터페이스에서 취소 후 결과를 즉시 다른 시스템에 송신해야 한다면 반드시 wait = 'X' 를 넣어야 합니다.
5단계 — 다건 일괄 취소 처리
여러 송장을 한 배치에서 취소할 때 한 건씩 호출하면서 건별 단위로 RETURN 검증·로그 적재 하는 패턴을 권장합니다. 한 건이 실패해도 나머지 송장 취소가 영향받지 않게 하기 위함입니다.
LOOP AT lt_cancel_target INTO ls_target.
REFRESH lt_return.
CLEAR: lv_rev_doc, lv_rev_year.
CALL FUNCTION 'BAPI_INCOMINGINVOICE_CANCEL'
EXPORTING
invoicedocnumber = ls_target-belnr
fiscalyear = ls_target-gjahr
reasonreversal = ls_target-reason
postingdate = ls_target-pst_date
IMPORTING
invoicedocnumber_reversal = lv_rev_doc
fiscalyear_reversal = lv_rev_year
TABLES
return = lt_return.
READ TABLE lt_return WITH KEY type = 'E' TRANSPORTING NO FIELDS.
IF sy-subrc = 0.
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
" 로그 적재 (실패)
APPEND VALUE #( belnr = ls_target-belnr
status = 'FAIL'
msg = ls_return-message ) TO lt_log.
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
" 로그 적재 (성공)
APPEND VALUE #( belnr = ls_target-belnr
rev_belnr = lv_rev_doc
status = 'OK' ) TO lt_log.
ENDIF.
ENDLOOP.
한 LUW 안에서 COMMIT 을 여러 번 호출하지 않는 것 도 가능하지만, 그 경우 한 건이 실패해서 ROLLBACK 을 하면 같은 LUW 의 이전 성공 건도 함께 롤백됩니다. 정책상 건별 독립 처리가 필요하면 위와 같이 매 건마다 COMMIT/ROLLBACK 을 명시하는 패턴이 안전합니다.
흔히 빠뜨리는 함정
이미 지급된 송장은 취소 불가
송장이 이미 지급 처리(F-53 · F110) 되었다면 BAPI 가 거부합니다. RETURN 에 "Document already paid" 메시지가 담겨 오므로 먼저 지급 취소(FBRA) 를 한 후 송장 취소를 수행해야 합니다.
회계 기간 마감
원본 전표의 POSTINGDATE 가 속한 회계 기간이 마감된 경우, 같은 기간으로 취소 전표를 끊으려고 하면 "Posting period locked" 오류가 발생합니다. 이 경우 REASON 02 + 현재 열려 있는 기간의 POSTINGDATE 를 명시해서 호출해야 합니다.
회계연도 누락 시 검색 실패
전표번호만 같고 회계연도가 다른 경우가 있을 수 있습니다(연도가 바뀌면서 번호 범위 재시작). FISCALYEAR 를 잘못 넘기면 BAPI 가 다른 전표를 취소하거나 "Document not found" 로 실패합니다. 호출 전 RBKP 에서 BELNR + GJAHR 조합으로 정확히 한 건이 조회되는지 확인하시기 바랍니다.
COMMIT 누락
가장 흔한 실수입니다. BAPI 호출만 하고 BAPI_TRANSACTION_COMMIT 을 빼먹으면 RETURN 에는 성공이 떠도 DB 에는 반영되지 않습니다. 다음 LUW 가 시작되면(예: 새 화면 진입) 변경분이 그대로 폐기됩니다.
한 LUW 안에서 COMMIT/SELECT 순서
COMMIT 직후 같은 프로그램에서 RBKP 를 SELECT 해서 취소 전표를 확인하려면 wait = 'X' 가 필수입니다. 비동기 COMMIT 직후 SELECT 하면 아직 update task 가 끝나지 않아 데이터가 안 보일 수 있습니다.
MR8M (수기) vs BAPI 차이
화면 MR8M 은 BAdI MRM_HEADER_DEFAULT · INVOICE_UPDATE 같은 화면 흐름용 BAdI 들이 함께 호출되는데, BAPI 직접 호출 시에는 일부 BAdI 가 트리거되지 않는 경우가 있습니다. 사내 커스텀 BAdI 가 화면에만 의존해서 동작하도록 짜여 있다면 BAPI 전환 시 누락 분기를 점검해야 합니다.
전체 코드 — 복사용 통합본
*&---------------------------------------------------------------------*
*& Report Z_XX_INVOICE_CANCEL
*&---------------------------------------------------------------------*
*& 송장 취소 BAPI 호출 샘플
*&---------------------------------------------------------------------*
REPORT z_xx_invoice_cancel.
DATA: lt_return TYPE TABLE OF bapiret2,
ls_return TYPE bapiret2,
lv_rev_doc TYPE bapi_incinv_fld-inv_doc_no,
lv_rev_year TYPE bapi_incinv_fld-fisc_year.
DATA: lv_belnr TYPE bapi_incinv_fld-inv_doc_no VALUE '5100000001',
lv_gjahr TYPE bapi_incinv_fld-fisc_year VALUE '2026',
lv_reason TYPE bapi_incinv_fld-reason_rev VALUE '01',
lv_pst_date TYPE bapi_incinv_fld-pstng_date.
DATA: lv_stblg TYPE rbkp-stblg,
lv_rbstat TYPE rbkp-rbstat.
* ★ 사전 체크: 이미 취소되었거나 차단된 전표 검증
SELECT SINGLE stblg rbstat
INTO (lv_stblg, lv_rbstat)
FROM rbkp
WHERE belnr = lv_belnr
AND gjahr = lv_gjahr.
IF sy-subrc <> 0.
WRITE: / '대상 송장 없음:', lv_belnr, lv_gjahr.
EXIT.
ENDIF.
IF lv_stblg IS NOT INITIAL.
WRITE: / '이미 취소된 전표 — 재취소 불가. 취소 전표:', lv_stblg.
EXIT.
ENDIF.
* ★ 핵심: BAPI 호출
CALL FUNCTION 'BAPI_INCOMINGINVOICE_CANCEL'
EXPORTING
invoicedocnumber = lv_belnr
fiscalyear = lv_gjahr
reasonreversal = lv_reason
* postingdate = lv_pst_date " 필요 시 활성화
IMPORTING
invoicedocnumber_reversal = lv_rev_doc
fiscalyear_reversal = lv_rev_year
TABLES
return = lt_return.
* ★ RETURN 검증
READ TABLE lt_return INTO ls_return
WITH KEY type = 'E'.
IF sy-subrc = 0.
" 에러 발생 — 롤백
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
WRITE: / '취소 실패:', ls_return-message.
LOOP AT lt_return INTO ls_return.
WRITE: / ls_return-type, ls_return-id,
ls_return-number, ls_return-message.
ENDLOOP.
ELSE.
" 정상 처리 — 동기 커밋
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = 'X'.
WRITE: / '취소 성공',
/ '원본 송장: ', lv_belnr, '/', lv_gjahr,
/ '취소 전표번호:', lv_rev_doc, '/', lv_rev_year.
ENDIF.
같이 보면 좋은 글은 "BAPI_INCOMINGINVOICE_CREATE — 송장 생성 BAPI 호출 패턴" 입니다. 송장 생성 → 잘못 등록 발견 → 취소 → 정정 송장 재등록 순서가 인터페이스에서 가장 흔한 시나리오이므로 두 BAPI 를 한 세트로 익혀두시면 좋습니다.
요약
| 단계 | 처리 내용 | 핵심 포인트 |
|---|---|---|
| 1 | 사전 체크 | RBKP-STBLG 확인 — 재취소 차단 |
| 2 | 취소 사유 결정 | 현 기간은 01, 이전 기간은 02 + 별도 일자 |
| 3 | BAPI 호출 | BELNR + GJAHR + REASON 3개로 트리거 |
| 4 | RETURN 검증 | Type 'E' 있으면 ROLLBACK |
| 5 | COMMIT | wait = 'X' 동기 처리 권장 |
송장 취소 BAPI 는 시그니처가 단순한 만큼 사전 체크와 RETURN 검증이 품질을 좌우합니다. 특히 인터페이스 / 배치에서 사용할 때는 "이미 취소된 전표 재취소 방지"·"COMMIT/ROLLBACK 명시"·"한 건 실패가 전체에 영향 없게 분리" 세 가지를 패턴으로 박아두면 운영 중 사고가 거의 발생하지 않습니다.
Disclaimer — 이 포스트는 실무 정리 노트를 바탕으로 AI 보조로 정리되었습니다.
BAPI 시그니처·취소 사유 코드(T041C)·RBKP 필드 구조는 SAP MM 표준(패키지 MRM · ECC 6.0 / S/4HANA on-premise) 기준이며, 사내 추가 커스터마이징·BAdI 구현·결재 흐름에 따라 일부 동작이 다를 수 있으니 운영 시스템 적용 전 개발·QA 환경에서 검증하시기 바랍니다.