본문 바로가기
BAPI · BADI · RFC · Interface

[SAP ABAP] BAPI_PO_CHANGE — 구매오더 변경·삭제 BAPI 호출 방법 (ME22N 자동화 · X-구조 · DELETE_IND)

by Song.sh 2026. 5. 21.

SAP MM 모듈에서 이미 생성된 구매오더(PO) 의 단가·수량·납기·라인 추가·라인 삭제·계정 분개 등을 수정하려면 화면에서는 ME22N 으로 처리하지만, 인터페이스나 자동 정정 배치에서는 BAPI_PO_CHANGE 를 호출합니다.

 

이 BAPI 의 가장 큰 특징은 X-구조 패턴(POHEADERX·POITEMX·POSCHEDULEX 등) 입니다. 생성 BAPI 와 달리 "어떤 필드를 변경할 것인지 X 플래그로 명시" 해야 하며, 라인 삭제·신규 라인 추가도 X-구조의 플래그로 표현합니다. 이 X 의미를 정확히 이해하지 않으면 의도와 다른 필드까지 같이 바뀌어 운영 사고로 이어집니다.

 

이 글에서는 BAPI 시그니처·X-구조 동작 원리·필드 변경·라인 신규 추가·라인 삭제·결재 상태 처리·COMMIT 처리·자주 발생하는 함정까지 정리합니다.

핵심 — X-구조가 결정한다

생성 BAPI(BAPI_PO_CREATE1) 는 POHEADER 에 채운 값을 그대로 사용하면 됐지만, 변경 BAPI 는 POHEADER 에 새 값을 채워도 POHEADERX 의 같은 필드가 'X' 가 아니면 무시 됩니다. X 가 변경 의도를 명시하는 신호입니다.

🔧 시나리오 POITEM POITEMX
필드 변경 (단가) PO_ITEM='00010' + NET_PRICE='6000' PO_ITEM='00010' + PO_ITEMX=' ' + NET_PRICE='X'
라인 신규 추가 PO_ITEM='00030' + 자재·수량·단가 전부 PO_ITEM='00030' + PO_ITEMX='X' + 모든 필드 'X'
라인 삭제 PO_ITEM='00020' + DELETE_IND='X' PO_ITEM='00020' + DELETE_IND='X'

X-구조의 핵심 규칙: POITEMX 의 PO_ITEM 은 어떤 라인을 가리키는지 알려주는 키이고, PO_ITEMX 는 "이 라인이 신규인지(X)·기존인지(공백)" 를 의미. 나머지 필드명에 대응하는 X-필드(NET_PRICEX·QUANTITYX 등) 는 "이 필드를 변경할지 여부" 를 결정합니다.


1단계 — 필드 변경 (단가·수량·납기)

기존 라인의 단가만 변경하는 가장 단순한 패턴.

DATA: lt_item  TYPE TABLE OF bapimepoitem,
      lt_itemx TYPE TABLE OF bapimepoitemx,
      ls_item  TYPE bapimepoitem,
      ls_itemx TYPE bapimepoitemx.

" 변경할 라인 + 새 단가
ls_item-po_item   = '00010'.
ls_item-net_price = '6000'.            " 5000 → 6000
APPEND ls_item TO lt_item.

" X-구조 — NET_PRICE 만 'X'
ls_itemx-po_item    = '00010'.
ls_itemx-po_itemx   = ' '.             " 기존 라인
ls_itemx-net_price  = 'X'.             " 단가 변경
APPEND ls_itemx TO lt_itemx.

CALL FUNCTION 'BAPI_PO_CHANGE'
  EXPORTING
    purchaseorder = '4500000001'
  TABLES
    return        = lt_return
    poitem        = lt_item
    poitemx       = lt_itemx.

POITEM 에 단가만 채우고 다른 필드를 비워두면, POITEMX 가 NET_PRICE 만 'X' 이므로 그 필드만 변경됩니다. 자재·수량·납기는 그대로 유지됩니다.

수량 변경도 동일한 패턴 — POITEM-QUANTITY + POITEMX-QUANTITY = 'X'. 단, 수량 변경 시 POSCHEDULE 의 일정 수량도 함께 맞춰야 BAPI 가 통과합니다.

" 수량 + 일정 수량 동시 변경
ls_item-po_item    = '00010'.
ls_item-quantity   = '120'.            " 100 → 120
APPEND ls_item TO lt_item.

ls_itemx-po_item   = '00010'.
ls_itemx-po_itemx  = ' '.
ls_itemx-quantity  = 'X'.
APPEND ls_itemx TO lt_itemx.

DATA: lt_sched  TYPE TABLE OF bapimeposchedule,
      lt_schedx TYPE TABLE OF bapimeposchedulx,
      ls_sched  TYPE bapimeposchedule,
      ls_schedx TYPE bapimeposchedulx.

ls_sched-po_item       = '00010'.
ls_sched-sched_line    = '0001'.
ls_sched-quantity      = '120'.
APPEND ls_sched TO lt_sched.

ls_schedx-po_item      = '00010'.
ls_schedx-sched_line   = '0001'.
ls_schedx-po_itemx     = ' '.
ls_schedx-sched_linex  = ' '.
ls_schedx-quantity     = 'X'.
APPEND ls_schedx TO lt_schedx.

2단계 — 라인 신규 추가

기존 PO 에 새 자재 라인을 추가하는 패턴. POITEMX 의 PO_ITEMX = 'X' 가 핵심.

" 신규 라인 — 자재·플랜트·수량·단가 전부 채움
ls_item-po_item    = '00030'.           " 신규 아이템 번호
ls_item-material   = 'TEST-MAT-003'.
ls_item-plant      = '1000'.
ls_item-stge_loc   = '0001'.
ls_item-quantity   = '30'.
ls_item-po_unit    = 'EA'.
ls_item-net_price  = '7500'.
ls_item-price_unit = '1'.
ls_item-tax_code   = 'V1'.
APPEND ls_item TO lt_item.

" X-구조 — PO_ITEMX='X' (신규 라인 마킹) + 채운 필드 전부 'X'
ls_itemx-po_item    = '00030'.
ls_itemx-po_itemx   = 'X'.             " ★ 신규 라인
ls_itemx-material   = 'X'.
ls_itemx-plant      = 'X'.
ls_itemx-stge_loc   = 'X'.
ls_itemx-quantity   = 'X'.
ls_itemx-po_unit    = 'X'.
ls_itemx-net_price  = 'X'.
ls_itemx-price_unit = 'X'.
ls_itemx-tax_code   = 'X'.
APPEND ls_itemx TO lt_itemx.

" 신규 라인의 일정라인도 추가
ls_sched-po_item       = '00030'.
ls_sched-sched_line    = '0001'.
ls_sched-delivery_date = sy-datum + 14.
ls_sched-quantity      = '30'.
APPEND ls_sched TO lt_sched.

ls_schedx-po_item      = '00030'.
ls_schedx-sched_line   = '0001'.
ls_schedx-po_itemx     = 'X'.          " 신규 라인의 신규 일정
ls_schedx-sched_linex  = 'X'.
ls_schedx-delivery_date = 'X'.
ls_schedx-quantity     = 'X'.
APPEND ls_schedx TO lt_schedx.

신규 라인의 POSCHEDULE 도 함께 채우는 것이 핵심. PO 의 라인은 일정라인이 최소 1개는 있어야 유효하므로, 라인만 추가하고 일정라인을 빠뜨리면 BAPI 가 거부합니다.


3단계 — 라인 삭제

라인을 PO 에서 제거하려면 DELETE_IND = 'X' 를 POITEM 과 POITEMX 양쪽에 설정.

ls_item-po_item     = '00020'.
ls_item-delete_ind  = 'X'.             " ★ 삭제 플래그
APPEND ls_item TO lt_item.

ls_itemx-po_item    = '00020'.
ls_itemx-po_itemx   = ' '.
ls_itemx-delete_ind = 'X'.             " ★ X-구조에도 명시
APPEND ls_itemx TO lt_itemx.

CALL FUNCTION 'BAPI_PO_CHANGE'
  EXPORTING
    purchaseorder = '4500000001'
  TABLES
    return        = lt_return
    poitem        = lt_item
    poitemx       = lt_itemx.

실제 DB 동작: 라인이 EKPO 에서 물리적으로 사라지는 것이 아니라 EKPO-LOEKZ = 'L'(삭제 플래그) 로 변경됩니다. 이후 PO 조회에서 그 라인은 더 이상 활성으로 잡히지 않지만 이력은 남습니다.

이미 GR(입고) 이 발생한 라인은 단순 삭제 불가능합니다. 입고 취소 → 라인 삭제 순서가 필요하거나, 입고 완료(NO_MORE_GR = 'X') 처리로 라인을 비활성화할 수 있습니다.


4단계 — 입고 완료 마킹 (NO_MORE_GR)

라인 자체는 살려두되 "더 이상 입고하지 않음" 으로 마킹하는 경우. 부분 입고 후 잔량을 청산할 때 사용합니다.

ls_item-po_item    = '00010'.
ls_item-no_more_gr = 'X'.              " 입고 완료 (EKPO-ELIKZ)
APPEND ls_item TO lt_item.

ls_itemx-po_item    = '00010'.
ls_itemx-po_itemx   = ' '.
ls_itemx-no_more_gr = 'X'.
APPEND ls_itemx TO lt_itemx.

화면 ME22N 에서 "Delivery Complete" 체크와 같은 동작입니다. 잔량이 있어도 추가 입고가 안 생기게 막아 PO 라이프사이클을 정리할 수 있습니다.


5단계 — 헤더 변경 (지급조건·텍스트·결재자)

헤더 변경도 동일한 X-구조 패턴.

DATA: ls_header  TYPE bapimepoheader,
      ls_headerx TYPE bapimepoheaderx.

ls_header-pmnttrms  = '0002'.          " 지급조건 변경
ls_headerx-pmnttrms = 'X'.

CALL FUNCTION 'BAPI_PO_CHANGE'
  EXPORTING
    purchaseorder = '4500000001'
    poheader      = ls_header
    poheaderx     = ls_headerx
  TABLES
    return        = lt_return.

거래처·회사코드·구매조직 같이 PO 의 기본 정체성을 정의하는 필드는 변경 불가능합니다. 이런 변경이 필요하면 PO 자체를 새로 생성해야 합니다.


6단계 — TESTRUN + COMMIT

생성 BAPI 와 동일하게 TESTRUN = 'X' 로 시뮬레이션 가능. 변경의 영향이 클 때(대량 단가 일괄 변경 등) 사전 검증 권장.

CALL FUNCTION 'BAPI_PO_CHANGE'
  EXPORTING
    purchaseorder = '4500000001'
    testrun       = 'X'                " 시뮬레이션
  TABLES
    return        = lt_return
    poitem        = lt_item
    poitemx       = lt_itemx.

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'.
ENDIF.

흔히 빠뜨리는 함정

X-구조 누락

가장 흔한 실수입니다. POITEM 에 새 값만 채우고 POITEMX 에 'X' 를 안 박으면 BAPI 가 그 필드를 변경 의도로 인식하지 않아 변경이 무시 됩니다. 에러 없이 그냥 변경이 안 됩니다. 호출 후 EKPO 를 SELECT 해서 실제 변경 여부를 검증하는 것이 안전합니다.

PO_ITEMX = 'X' 잘못 사용

기존 라인 변경 시 POITEMX-PO_ITEMX = ' '(공백). 신규 라인 추가 시에만 'X'. 기존 라인에 PO_ITEMX = 'X' 를 넣으면 BAPI 가 그 PO_ITEM 번호로 라인을 새로 만들려고 시도해 "Item already exists" 에러가 발생합니다.

일정라인 동기화

수량을 변경하면 POSCHEDULE 의 일정 수량 합계도 새 수량과 일치해야 합니다. POITEM 만 바꾸고 POSCHEDULE 을 안 바꾸면 BAPI 가 거부합니다. 동일하게 신규 라인 추가 시 POSCHEDULE 도 함께 채워야 합니다.

결재 상태 (Release Strategy)

PO 가 결재 완료 상태라면 일부 필드(단가·수량) 변경 시 결재가 자동 초기화될 수 있습니다. 회사 정책에 따라 결재 재상신이 필요하므로 인터페이스에서 변경 후 EKKO-FRGKE 상태를 확인하는 로직이 필요합니다.

이미 입고된 라인

GR 이 발생한 라인은 단가·수량·자재 같은 핵심 필드 변경이 제약됩니다. 환경에 따라 "단가는 입고 전까지만 변경 가능", "수량은 입고 수량 이상으로만 변경 가능" 같은 룰이 적용됩니다. 변경 실패 메시지를 보고 분기 처리하시기 바랍니다.

송장 처리된 라인

송장이 끊긴 라인은 변경 제약이 훨씬 큽니다. 사실상 신규 변경이 어렵고, 잘못된 라인은 송장 취소 → 입고 취소 → PO 라인 정리 순서로 처리해야 합니다.

라인 삭제 시 결재 영향

활성 라인을 삭제하면 PO 의 합계 금액이 바뀌어 결재 한도 룰이 다시 적용될 수 있습니다. 한도 초과 / 미달 분기에 따라 결재 흐름이 재시작되므로 운영에서 한 번에 대량 삭제는 피하는 게 안전합니다.

COMMIT 누락

BAPI 자체 호출 성공이라도 BAPI_TRANSACTION_COMMIT 누락 시 DB 미반영. 변경 BAPI 는 한 호출에 여러 라인을 다루는 경우가 많아 COMMIT 누락 시 변경 전체가 폐기되므로 더 위험합니다.


전체 코드 — 복사용 통합본

*&---------------------------------------------------------------------*
*& Report  Z_XX_PO_CHANGE
*&---------------------------------------------------------------------*
*& 구매오더 변경 / 삭제 BAPI 호출 샘플
*&---------------------------------------------------------------------*
REPORT z_xx_po_change.

DATA: lt_item    TYPE TABLE OF bapimepoitem,
      lt_itemx   TYPE TABLE OF bapimepoitemx,
      ls_item    TYPE bapimepoitem,
      ls_itemx   TYPE bapimepoitemx,
      lt_sched   TYPE TABLE OF bapimeposchedule,
      lt_schedx  TYPE TABLE OF bapimeposchedulx,
      ls_sched   TYPE bapimeposchedule,
      ls_schedx  TYPE bapimeposchedulx,
      lt_return  TYPE TABLE OF bapiret2,
      ls_return  TYPE bapiret2.

DATA lv_ebeln TYPE bapimepoheader-po_number VALUE '4500000001'.

* ★ 시나리오 1: 기존 라인 00010 의 단가 변경
ls_item-po_item    = '00010'.
ls_item-net_price  = '6000'.
APPEND ls_item TO lt_item.

ls_itemx-po_item    = '00010'.
ls_itemx-po_itemx   = ' '.             " 기존 라인
ls_itemx-net_price  = 'X'.
APPEND ls_itemx TO lt_itemx.

* ★ 시나리오 2: 신규 라인 00030 추가
CLEAR: ls_item, ls_itemx, ls_sched, ls_schedx.

ls_item-po_item    = '00030'.
ls_item-material   = 'TEST-MAT-003'.
ls_item-plant      = '1000'.
ls_item-stge_loc   = '0001'.
ls_item-quantity   = '30'.
ls_item-po_unit    = 'EA'.
ls_item-net_price  = '7500'.
ls_item-price_unit = '1'.
ls_item-tax_code   = 'V1'.
APPEND ls_item TO lt_item.

ls_itemx-po_item    = '00030'.
ls_itemx-po_itemx   = 'X'.             " 신규 라인
ls_itemx-material   = 'X'.
ls_itemx-plant      = 'X'.
ls_itemx-stge_loc   = 'X'.
ls_itemx-quantity   = 'X'.
ls_itemx-po_unit    = 'X'.
ls_itemx-net_price  = 'X'.
ls_itemx-price_unit = 'X'.
ls_itemx-tax_code   = 'X'.
APPEND ls_itemx TO lt_itemx.

ls_sched-po_item        = '00030'.
ls_sched-sched_line     = '0001'.
ls_sched-delivery_date  = sy-datum + 14.
ls_sched-quantity       = '30'.
APPEND ls_sched TO lt_sched.

ls_schedx-po_item       = '00030'.
ls_schedx-sched_line    = '0001'.
ls_schedx-po_itemx      = 'X'.
ls_schedx-sched_linex   = 'X'.
ls_schedx-delivery_date = 'X'.
ls_schedx-quantity      = 'X'.
APPEND ls_schedx TO lt_schedx.

* ★ 시나리오 3: 라인 00020 삭제
CLEAR: ls_item, ls_itemx.

ls_item-po_item     = '00020'.
ls_item-delete_ind  = 'X'.
APPEND ls_item TO lt_item.

ls_itemx-po_item    = '00020'.
ls_itemx-po_itemx   = ' '.
ls_itemx-delete_ind = 'X'.
APPEND ls_itemx TO lt_itemx.

* ★ BAPI 호출
CALL FUNCTION 'BAPI_PO_CHANGE'
  EXPORTING
    purchaseorder = lv_ebeln
*   testrun       = 'X'                " 시뮬레이션 시 활성화
  TABLES
    return        = lt_return
    poitem        = lt_item
    poitemx       = lt_itemx
    poschedule    = lt_sched
    poschedulex   = lt_schedx.

* ★ RETURN 검증
READ TABLE lt_return INTO ls_return
     WITH KEY type = 'E'.

IF sy-subrc = 0.
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
  WRITE: / 'PO 변경 실패'.
  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: / 'PO 변경 성공:', lv_ebeln.
ENDIF.

같이 보면 좋은 글은 "BAPI_PO_CREATE1 — 구매오더 생성 BAPI 호출 방법" 입니다. 생성 → 변경 → 삭제까지 PO 라이프사이클 전체를 한 세트로 익혀두면 인터페이스 자동화에 그대로 적용 가능합니다.


요약

시나리오 POITEM POITEMX
필드 변경 PO_ITEM + 변경 필드 PO_ITEMX=' ' + 해당 필드 'X'
신규 라인 PO_ITEM + 자재·수량·단가 전부 PO_ITEMX='X' + 모든 필드 'X'
라인 삭제 PO_ITEM + DELETE_IND='X' PO_ITEM + DELETE_IND='X'
입고 완료 마킹 PO_ITEM + NO_MORE_GR='X' PO_ITEM + NO_MORE_GR='X'
수량 변경 QUANTITY + 일정라인 QUANTITY QUANTITY='X' + POSCHEDULEX 동기화

PO 변경 BAPI 의 핵심은 "X-구조가 변경 의도를 결정한다" 입니다. POITEMX 의 PO_ITEMX 플래그(신규 vs 기존)·필드별 X 플래그·DELETE_IND 세 요소만 정확히 채우면 단가 변경·라인 추가·라인 삭제·입고 완료 마킹까지 모든 시나리오를 한 BAPI 로 처리할 수 있습니다. 입고 / 송장이 이미 발생한 라인은 변경 제약을 사전 확인하고, 결재 흐름 영향까지 인터페이스에 박아두면 운영 중 사고가 거의 발생하지 않습니다.


Disclaimer — 이 포스트는 실무 정리 노트를 바탕으로 AI 보조로 정리되었습니다.

BAPI 시그니처·구조체 필드(BAPIMEPOITEMX·BAPIMEPOSCHEDULX) · X-구조 동작 규칙은 SAP MM 표준(패키지 ME · ECC 6.0 / S/4HANA on-premise) 기준이며, 사내 추가 BAdI · 결재 흐름 · 가격 결정 룰에 따라 일부 동작이 다를 수 있으니 운영 시스템 적용 전 개발·QA 환경에서 검증하시기 바랍니다.