📖 개요 및 이 글에서 다루는 내용
커스텀 전표 생성 배치가 밤새 돌다가 "전기기간이 마감되었습니다"로 수백 건이 실패해 있는 아침을 겪어본 적이 있다면 도움이 될 글입니다. BAPI 호출 후 실패를 수습하는 대신, 호출 전에 대상 회계기간이 열려 있는지 선검증하는 구조를 다룹니다.
- 전기기간 통제가 어떤 테이블(T001B, T009B)로 동작하는지
- FI_PERIOD_DETERMINE / FI_PERIOD_CHECK로 사전검증 로직 작성법
- 대량 배치에서 검증 결과를 캐싱하는 패턴, FI/MM 기간 차이
📚 미리 알아두면 좋은 배경
중급 난이도의 글로, ABAP 함수모듈 호출과 예외 처리, 내부 테이블 활용 경험을 전제로 합니다. FI 전표 구조(BKPF/BSEG)와 BAPI_ACC_DOCUMENT_POST를 한 번이라도 호출해 본 경험이 있으면 따라가기 수월합니다.
🔧 환경 및 준비 사항
예제는 SAP ECC 6.0(EhP 이상)과 S/4HANA 온프레미스에서 동일하게 동작하는 클래식 함수모듈을 사용합니다. S/4HANA는 OB52 외에 Fiori 앱 "Manage Posting Periods"로도 관리 가능합니다(내부적으로 동일하게 T001B 갱신).
- 개발 권한이 있는 SE38/SE80 또는 ADT(Eclipse) 환경
- 테스트용 회사코드와 OB52 조회 권한
- 확인용 트랜잭션: OB52(전기기간), OB29(회계연도변형), MMRV(MM 현재기간)
SAP BTP ABAP Environment(Steampunk)에서는 클래식 FM 직접 호출이 제한되므로, 이 글의 패턴은 온프레미스/Private Cloud 클래식 확장 관점입니다.
💡 핵심 개념: 전기기간 통제 구조
전기기간 검증을 건물 출입 통제에 비유하면 이해가 쉽습니다. 회계연도변형(T009B)은 "달력" 역할로, 전기일(BUDAT)이 몇 년도 몇 기(期)에 해당하는지를 결정합니다. 4-4-5 변형이나 이월연도(shifted fiscal year)를 쓰는 회사라면 8월 8일 전표가 반드시 8기가 아닐 수 있으므로, 전기일→기간 변환을 직접 계산하지 말고 시스템에 맡겨야 합니다.
전기기간변형(T001B)은 "출입 허가 명단"입니다. 회사코드에 할당된 전기기간변형(T001-OPVAR) 아래에서, 계정유형별로 열려 있는 기간 구간을 관리합니다.
- 계정유형(MKOAR): + (전체), A(자산), D(고객), K(공급업체), S(G/L), M(자재)
- 구간 1(FRPE1~TOPE1): 권한그룹(BRGRU)으로 보호 가능한 구간. 결산 중 특정 사용자만 전기 허용할 때 사용
- 구간 2(FRPE2~TOPE2): 모든 사용자에게 열린 구간
- 구간 3: S/4HANA에서 CO 기표(FI-CO 통합) 통제용으로 추가된 구간
즉 "기간이 열려 있는가"는 실제로 "이 회사코드·계정유형에 대해, 이 연도·기간이 구간 1 또는 2에 포함되는가"라는 복합 질문입니다. T001B를 직접 SELECT해서 구현하는 것은 권장되지 않습니다. 계정유형 '+' 폴백, 계정번호 구간, 연도 경계 처리 같은 표준 로직을 놓치기 쉽기 때문입니다. 표준 함수모듈 FI_PERIOD_CHECK가 이 판정을 캡슐화하고 있습니다.
혼동하기 쉬운 것이 MM 자재기간입니다. 자재 이동/송장검증은 T001B와 별개로 MARV 테이블을 따르며 MMPV로 월전환합니다. "OB52는 열려 있는데 MIGO가 막힌다"는 상황이 이 때문입니다. 자재 관련 전기가 섞인 전표라면 MM 기간도 별도 확인이 필요합니다.
💻 실전 코드 3단계
1단계 — 기본: 전기일로 기간을 결정하고 오픈 여부 확인
먼저 전기일과 회사코드로 회계연도·기간을 결정하고(회계연도변형 반영), 그 결과로 기간 오픈 여부를 검사합니다.
DATA: lv_gjahr TYPE gjahr,
lv_monat TYPE monat.
" 1) 전기일 -> 회계연도/기간 결정 (T009B 기반, 직접 계산 금지)
CALL FUNCTION 'FI_PERIOD_DETERMINE'
EXPORTING
i_budat = p_budat " 예: 20260808
i_bukrs = p_bukrs " 예: 1000
IMPORTING
e_gjahr = lv_gjahr
e_monat = lv_monat
EXCEPTIONS
fiscal_year = 1
period = 2
t009b_notfound = 3
OTHERS = 4.
IF sy-subrc <> 0.
MESSAGE e001(zfi) WITH p_budat p_bukrs. " 회계연도변형 설정 오류
RETURN.
ENDIF.
" 2) 해당 기간이 계정유형 K(공급업체)에 대해 열려 있는지 확인
CALL FUNCTION 'FI_PERIOD_CHECK'
EXPORTING
i_bukrs = p_bukrs
i_gjahr = lv_gjahr
i_monat = lv_monat
i_koart = 'K'
EXCEPTIONS
error_period = 1 " 기간 자체가 닫힘
error_period_acc = 2 " 해당 계정(유형) 구간에서 닫힘
invalid_input = 3
OTHERS = 4.
IF sy-subrc = 0.
WRITE: / |{ lv_gjahr }/{ lv_monat } 기간 전기 가능|.
ELSE.
WRITE: / |{ lv_gjahr }/{ lv_monat } 기간 마감 - 전표 생성 스킵|.
ENDIF.
공급업체·G/L 라인이 함께 들어간 전표라면 관련 계정유형(K, S) 각각을 검사해야 안전합니다. 하나라도 닫혀 있으면 전기가 실패합니다.
2단계 — 실무 시나리오: 판매오더 후속 전표 배치에서 사전검증과 로깅
정산 대상 판매오더 수천 건을 읽어 BAPI_ACC_DOCUMENT_POST로 전표를 만드는 야간 배치를 가정합니다. 핵심은 건별로 FM을 반복 호출하지 않고, 회사코드+기간 조합 단위로 한 번만 검증해 재사용하고, 마감 건은 로그에 남기고 건너뛰는 것입니다.
TYPES: BEGIN OF ty_period_res,
bukrs TYPE bukrs, gjahr TYPE gjahr,
monat TYPE monat, koart TYPE koart,
is_open TYPE abap_bool,
END OF ty_period_res.
DATA lt_cache TYPE HASHED TABLE OF ty_period_res
WITH UNIQUE KEY bukrs gjahr monat koart.
LOOP AT lt_settle_items INTO DATA(ls_item). " 정산 대상 목록
DATA(lv_gjahr) = VALUE gjahr( ).
DATA(lv_monat) = VALUE monat( ).
CALL FUNCTION 'FI_PERIOD_DETERMINE'
EXPORTING i_budat = ls_item-budat i_bukrs = ls_item-bukrs
IMPORTING e_gjahr = lv_gjahr e_monat = lv_monat
EXCEPTIONS OTHERS = 1.
IF sy-subrc <> 0.
perform_log( 'E' ls_item-vbeln |기간 결정 실패| ).
CONTINUE.
ENDIF.
" 회사코드+연도+기간+계정유형 조합은 캐시에서 먼저 조회
READ TABLE lt_cache INTO DATA(ls_cache)
WITH TABLE KEY bukrs = ls_item-bukrs gjahr = lv_gjahr
monat = lv_monat koart = 'D'.
IF sy-subrc <> 0.
CALL FUNCTION 'FI_PERIOD_CHECK'
EXPORTING i_bukrs = ls_item-bukrs i_gjahr = lv_gjahr
i_monat = lv_monat i_koart = 'D'
EXCEPTIONS error_period = 1 error_period_acc = 2 OTHERS = 3.
ls_cache = VALUE #( bukrs = ls_item-bukrs gjahr = lv_gjahr
monat = lv_monat koart = 'D'
is_open = COND #( WHEN sy-subrc = 0
THEN abap_true ELSE abap_false ) ).
INSERT ls_cache INTO TABLE lt_cache.
ENDIF.
IF ls_cache-is_open = abap_false.
perform_log( 'W' ls_item-vbeln |{ lv_gjahr }/{ lv_monat } 마감 스킵| ).
CONTINUE.
ENDIF.
post_document( ls_item ). " 검증 통과 건만 BAPI 호출
ENDLOOP.
perform_log는 BAL_LOG_CREATE/BAL_LOG_MSG_ADD 기반 애플리케이션 로그(SLG1)로 구현하는 것이 일반적입니다. 어떤 건이 어떤 기간 때문에 스킵됐는지 로그에 남으면, 운영자는 OB52 조치 후 스킵 건만 재실행하면 됩니다.
3단계 — 프로덕션: 재사용 가능한 검증 클래스와 안전장치
여러 배치가 같은 검증을 쓰게 되면 전역 클래스로 캡슐화합니다. 정적 버퍼로 성능을 확보하고, FM 호출부를 protected 메서드로 분리해 단위 테스트에서 테스트 더블로 대체할 수 있게 합니다.
CLASS zcl_fi_period_guard DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS check_open
IMPORTING iv_bukrs TYPE bukrs
iv_budat TYPE budat
iv_koart TYPE koart DEFAULT '+'
RETURNING VALUE(rv_open) TYPE abap_bool
RAISING zcx_fi_period. " 설정 오류는 예외로 구분
PROTECTED SECTION.
METHODS call_period_check " 테스트 더블 대체 지점
IMPORTING iv_bukrs TYPE bukrs iv_gjahr TYPE gjahr
iv_monat TYPE monat iv_koart TYPE koart
RETURNING VALUE(rv_subrc) TYPE syst_subrc.
PRIVATE SECTION.
CLASS-DATA gt_buffer TYPE HASHED TABLE OF ty_period_res
WITH UNIQUE KEY bukrs gjahr monat koart.
ENDCLASS.
사전검증은 필터일 뿐이므로 BAPI_ACC_DOCUMENT_POST의 RETURN 오류 처리는 유지해야 합니다(검증 후 전기 사이 OB52 변경 가능). 장시간 배치는 일정 건수마다 버퍼를 비우고, 구간 1이 권한그룹(BRGRU)으로 보호됐다면 배치 사용자의 F_BKPF_BUP 권한도 점검하십시오.
⚠️ 흔한 실수와 트러블슈팅 FAQ
Q1. FI_PERIOD_CHECK는 통과했는데 실제 전기에서 "기간 마감" 오류가 납니다.
검증에 쓴 계정유형과 실제 전표 라인의 계정유형이 다른지 확인하십시오. '+'만 검사하면 A/D/K/S 개별 구간이 닫힌 경우를 놓칩니다. 검증과 전기 사이 OB52 변경, 또는 자재 라인의 MARV(MM 기간) 차단도 원인일 수 있습니다.
Q2. 전기일에서 기간을 (월 = 기간)으로 직접 계산해도 되지 않나요?
K4(월력) 변형만 쓰는 시스템에서는 우연히 맞지만, 4-4-5 변형·이월연도·특별기간(13~16기)이 있는 회사코드에서는 틀립니다. FI_PERIOD_DETERMINE으로 T009B 기반 변환을 거치는 것이 안전합니다.
Q3. OB52에서는 분명 열려 있는데 특정 사용자만 실패합니다.
구간 1에 권한그룹(BRGRU)이 걸려 있고 구간 2는 이미 다음 기간으로 넘어간 상태가 전형적입니다. F_BKPF_BUP 권한이 없어 구간 1 혜택을 못 받는 경우이니 SU53으로 확인해보십시오.
🚀 더 확장해볼 만한 주제
이 패턴을 익혔다면 BAPI_ACC_DOCUMENT_CHECK 기반 사전 시뮬레이션, SLG1 로그 설계, 병렬(aRFC) 배치의 버퍼 공유 문제로 확장해 보시길 권합니다. S/4HANA라면 구간 3(CO 통제)과 RAP 기반 전표 생성 시 검증 위치도 검토해볼 만합니다.
📚 함께 보면 좋은 문서
댓글 0
아직 댓글이 없습니다.