1. I_PurchaseContractItem이 해결하는 문제
구매 계약(Purchasing Contract)은 특정 자재나 서비스를 정해진 기간·조건에 반복 조달하기 위한 아웃라인 협약(Outline Agreement)입니다. SAP ERP 시절에는 EKKO(헤더)/EKPO(아이템) 테이블을 직접 조회하고 BSTYP = 'K' 같은 필터로 계약만 걸러내야 했지만, S/4HANA에서는 I_PurchaseContractItem이라는 CDS View를 통해 "이미 계약만 정제된" 데이터 셋을 바로 소비할 수 있습니다.
- 계약(Contract) 아이템만 골라내는 필터 로직을 개발자가 직접 짜지 않아도 됨
- Association을 통해 자재·플랜트·공급업체·계약 헤더에 손쉽게 조인
- Fiori 앱, RAP 서비스, CDP(Consumption View)에서 공통으로 재사용 가능
이 글에서 다루는 목표 체크리스트:
- I_PurchaseContractItem의 필드 구조와 EKPO와의 매핑 이해
- 기본 SELECT부터 JOIN·집계까지 실전 쿼리 작성
- ABAP RAP/보고서에서 안전하게 소비하는 패턴 습득
- 유효기간 필터·성능 튜닝 시 주의 사항 확인
2. 시작 전에 알아두어야 할 배경
이 글은 다음 지식을 전제로 합니다.
- ABAP Open SQL 및 CDS View 기본 문법(
SELECT ... FROM, Association_Assoc) - SAP MM 구매 계약 개념: Value Contract(WK), Quantity Contract(MK)
- EKKO/EKPO 테이블의 대략적인 역할
- ADT(Eclipse) 환경에서 CDS View를 열람하고 F2로 정의를 확인하는 방법
3. 사용 환경과 준비물
본 예제는 SAP S/4HANA 2022 On-Premise 및 S/4HANA Cloud Public Edition 2402 기준으로 작성했습니다. Private Cloud에서도 동일 View명이 유지되나, 릴리스별로 필드 추가/Deprecated가 발생할 수 있으므로 SE11 또는 ADT의 CDS Repository View에서 실제 활성 버전을 확인하는 것이 권장됩니다.
- 백엔드: S/4HANA 2021 이상 (Foundation/Core Data Service 릴리스)
- 클라이언트: ABAP Development Tools (Eclipse 2024-03 이상)
- 권한:
S_TABU_NAM(EKKO, EKPO),S_RS_COMP조회 권한 - 테스트 데이터: 계약 문서번호(예:
4600001234), 자재MAT-10023, 플랜트1010 - 도구:
Data Preview(F8),ST05(SQL Trace),SAT(런타임 분석)
클라우드 에디션에서는 릴리스된 화이트리스트 View만 소비 가능하며, I_PurchaseContractItem은 일반적으로 C1(Public) 릴리스 상태로 노출되어 있습니다.
4. CDS View 구조와 핵심 필드 해부
I_PurchaseContractItem은 EKPO 계열 테이블을 Base로 하되, 계약 유형(BSTYP = 'K')을 사전 필터링한 Interface View입니다. "Interface"라는 접두어(I_)는 재사용을 전제로 한 시맨틱 계층이라는 뜻으로, 앱/서비스 계층에서는 이 View를 조합해 Consumption View(C_)를 만드는 방식이 표준입니다.
비유하자면, EKPO가 창고에 뒤섞여 있는 원자재 상자라면 I_PurchaseContractItem은 "구매 계약"이라는 라벨이 붙은 정돈된 팔레트입니다. 개발자는 어떤 상자가 계약인지 매번 헤더를 뒤져 확인할 필요 없이, 이미 분류된 팔레트를 가져다 쓸 수 있습니다.
핵심 필드 요약:
| 필드 | 의미 | EKPO 원본 |
|---|---|---|
| PurchaseContract | 계약 문서번호 | EBELN |
| PurchaseContractItem | 계약 아이템번호 | EBELP |
| Material | 자재번호 | MATNR |
| Plant | 플랜트 | WERKS |
| TargetQuantity | 목표 수량 | KTMNG |
| NetPriceAmount | 단가 | NETPR |
| OrderPriceUnit | 가격 단위 | BPRME |
| PurgDocItemCategory | 아이템 카테고리 | PSTYP |
| IsDeleted | 삭제 플래그 | LOEKZ |
주의할 점은 유효기간(ValidityStartDate/EndDate)은 헤더(I_PurchaseContractHeader)에 있다는 것입니다. 아이템 View에서 만료 여부를 판정하려면 헤더 Association(_PurchaseContract)을 통해 결합해야 합니다. 또한 통화·단위는 각각 DocumentCurrency, PurchaseOrderQuantityUnit 필드로 노출되며, @Semantics.amount.currencyCode 애노테이션이 붙어 있어 Fiori에서 자동으로 통화 포맷을 인식합니다.
5. EKPO와의 관계 — 왜 CDS 소비를 우선하는가
EKPO를 직접 SELECT해도 결과는 얻을 수 있지만, 실무에서는 다음 이유로 CDS View 사용이 일반적으로 권장됩니다.
- 시맨틱 필드명:
MATNR대신Material처럼 의미 있는 이름 사용 - 변환 로직 내장: 계약 유형/삭제 플래그 등 사전 필터 처리
- Association 재사용:
_Material,_Plant,_PurchaseContract로 즉시 조인 - 클라우드 호환: Public Cloud에서는 EKPO 직접 접근이 제한되므로 CDS가 유일한 진입점
- 확장성: 사이드카/Extensibility(
@Metadata.allowExtensions: true)로 커스텀 필드 통합
6. 기본 SELECT 실전 예제 — 1단계
먼저 특정 플랜트에서 사용 중인 계약 아이템을 조회하는 가장 기본적인 형태입니다.
REPORT zr_pcontract_item_basic.
DATA: lt_contract_items TYPE STANDARD TABLE OF i_purchasecontractitem.
SELECT PurchaseContract,
PurchaseContractItem,
Material,
Plant,
TargetQuantity,
PurchaseOrderQuantityUnit,
NetPriceAmount,
DocumentCurrency
FROM I_PurchaseContractItem
WHERE Plant = '1010'
AND Material = 'MAT-10023'
AND IsDeleted = ''
INTO TABLE @lt_contract_items
UP TO 100 ROWS.
cl_demo_output=>display( lt_contract_items ).
포인트: IsDeleted = ''로 삭제 플래그가 없는 아이템만 필터링합니다. EKPO를 직접 조회할 때 매번 신경 써야 했던 LOEKZ = '' 조건이 필드명에서 명확해집니다.
7. INNER JOIN으로 헤더 결합 — 2단계 실무 시나리오
실무에서 가장 자주 마주치는 요구사항은 "만료가 임박한 계약 아이템 목록"입니다. 유효기간은 헤더에 있으므로 I_PurchaseContractHeader와 조인이 필요합니다. 로깅과 예외 처리를 포함한 형태입니다.
CLASS zcl_pcontract_expiry_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_expiring,
contract TYPE ebeln,
item TYPE ebelp,
material TYPE matnr,
supplier TYPE lifnr,
valid_end TYPE datum,
target_qty TYPE ktmng,
net_price TYPE bprei,
currency TYPE waers,
days_left TYPE i,
END OF ty_expiring.
METHODS get_expiring_contracts
IMPORTING iv_plant TYPE werks_d
iv_horizon_day TYPE i DEFAULT 30
RETURNING VALUE(rt_data) TYPE STANDARD TABLE OF ty_expiring
RAISING cx_sy_open_sql_db.
ENDCLASS.
CLASS zcl_pcontract_expiry_report IMPLEMENTATION.
METHOD get_expiring_contracts.
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
DATA(lv_horizon) = lv_today + iv_horizon_day.
TRY.
SELECT itm~PurchaseContract AS contract,
itm~PurchaseContractItem AS item,
itm~Material AS material,
hdr~Supplier AS supplier,
hdr~ValidityEndDate AS valid_end,
itm~TargetQuantity AS target_qty,
itm~NetPriceAmount AS net_price,
itm~DocumentCurrency AS currency,
( hdr~ValidityEndDate - @lv_today ) AS days_left
FROM I_PurchaseContractItem AS itm
INNER JOIN I_PurchaseContractHeader AS hdr
ON itm~PurchaseContract = hdr~PurchaseContract
WHERE itm~Plant = @iv_plant
AND itm~IsDeleted = ''
AND hdr~ValidityEndDate BETWEEN @lv_today AND @lv_horizon
AND hdr~PurchasingDocumentDeletionCode = ''
ORDER BY hdr~ValidityEndDate
INTO CORRESPONDING FIELDS OF TABLE @rt_data.
IF sy-subrc <> 0.
MESSAGE |플랜트 { iv_plant } 에서 { iv_horizon_day }일 이내 만료 계약 없음|
TYPE 'I'.
ENDIF.
CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
" 애플리케이션 로그에 기록
cl_bali_message_setter=>create( lx_sql->get_text( ) ).
RAISE EXCEPTION lx_sql.
ENDTRY.
ENDMETHOD.
ENDCLASS.
날짜 계산은 CDS 표현식으로 위임하고, 예외는 cx_sy_open_sql_db로 잡아 애플리케이션 로그(BAL)에 전달합니다. iv_horizon_day를 기본 30일로 두어 배치/온디맨드 양쪽에서 재사용 가능합니다.
8. 필터링·집계와 프로덕션 패턴 — 3단계
공급업체별 유효 계약 금액을 집계하고, 결과를 캐싱·페이징하는 프로덕션 레벨 패턴입니다. RAP(Restful Application Programming) 서비스에서 소비하는 것을 가정합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Active Purchase Contract Value by Supplier'
define view entity ZC_ActivePContractValue
as select from I_PurchaseContractItem as itm
association [1..1] to I_PurchaseContractHeader as _Hdr
on $projection.PurchaseContract = _Hdr.PurchaseContract
{
key itm.PurchaseContract,
key itm.PurchaseContractItem,
_Hdr.Supplier,
itm.Plant,
itm.Material,
itm.TargetQuantity,
itm.NetPriceAmount,
@Semantics.amount.currencyCode: 'DocumentCurrency'
cast( itm.TargetQuantity * itm.NetPriceAmount as abap.curr(15,2) )
as OpenContractValue,
itm.DocumentCurrency,
_Hdr.ValidityStartDate,
_Hdr.ValidityEndDate,
_Hdr
}
where itm.IsDeleted = ''
and _Hdr.ValidityEndDate >= $session.system_date
and _Hdr.ValidityStartDate <= $session.system_date;
ABAP에서 이 Consumption View를 페이징하며 집계 사용:
METHOD read_supplier_summary.
DATA(lv_page_size) = 500.
DATA(lv_offset) = 0.
DO.
SELECT Supplier,
DocumentCurrency,
SUM( OpenContractValue ) AS total_value,
COUNT( * ) AS item_count
FROM ZC_ActivePContractValue
WHERE Plant = @iv_plant
GROUP BY Supplier, DocumentCurrency
ORDER BY Supplier
INTO TABLE @DATA(lt_page)
OFFSET @lv_offset
UP TO @lv_page_size ROWS.
IF lt_page IS INITIAL.
EXIT.
ENDIF.
APPEND LINES OF lt_page TO rt_summary.
lv_offset += lv_page_size.
ENDDO.
ENDMETHOD.
보안 측면에서는 CDS 자체의 @AccessControl.authorizationCheck: #CHECK가 DCL(Data Control Language)과 연동되어 사용자별 조직단위(Purchasing Organization 등) 필터를 자동 적용합니다. 별도 AUTHORITY-CHECK를 뿌리지 않아도 되지만, 커스텀 뷰라면 DCL(define role)을 반드시 짝지어 배포해야 합니다.
9. 흔한 실수와 트러블슈팅
Q1. 계약이 분명 있는데 결과가 나오지 않아요.
A. IsDeleted = ''를 넣었는지, 헤더의 PurchasingDocumentDeletionCode도 비어 있는지 확인하세요. 헤더가 삭제된 계약은 아이템이 살아 있어도 실무적으로 유효하지 않습니다. 또 계약 유형이 WK(Value Contract)일 때는 TargetQuantity가 0이고 NetPriceAmount만 채워지는 경우가 많으므로, 수량 기반 필터에 걸리지 않는지 검토합니다.
Q2. 집계 값이 EKPO 직접 조회와 다릅니다.
A. I_PurchaseContractItem은 계약 전용 필터(BSTYP = 'K') 및 릴리스 상태 필터가 걸린 시맨틱 뷰입니다. 반면 EKPO 직접 SELECT는 PO/스케줄 협약까지 포함될 수 있습니다. 수치 검증 시에는 SELECT ... WHERE BSTYP = 'K' AND LOEKZ = '' 조건으로 EKPO를 필터한 후 비교하는 것이 일반적입니다.
Q3. 성능이 느립니다. Fiori 목록 화면이 20초 이상 걸려요.
A. ST05로 실제 실행 SQL을 확인하세요. Association을 남발하면 HANA가 불필요한 컬럼까지 프로젝션합니다. Consumption View에서 필요한 필드만 노출하고, WHERE 절에 반드시 Plant 또는 PurchasingOrganization처럼 선택도가 높은 컬럼을 포함하세요. 대량 조회 시에는 위 예제처럼 OFFSET/UP TO 페이징을 적용합니다.
추가로 자주 마주치는 문제:
- 필드명 대소문자 오류 — CDS는 대소문자 구분 없이 인식하지만 ADT 자동완성 이름을 그대로 쓰는 것이 안전
- 통화/단위 필드 누락 시 Fiori 통화 포맷 오류 발생
- DCL을 배포하지 않은 커스텀 뷰는
#NOT_ALLOWED로 모든 조회가 차단됨
10. 이어서 살펴보면 좋은 주제
계약 아이템을 어느 정도 다룰 수 있게 되었다면 다음 영역으로 확장해 보세요.
- I_PurchaseContractHeader — 헤더 레벨 유효기간·공급업체·구매조직 필드 심화
- I_PurchasingDocumentItem — PO/계약/스케줄 협약 통합 뷰
- RAP Business Object — 계약을 CRUD 가능한 서비스로 노출(Managed/Unmanaged)
- Analytical Query(CDS @Analytics.query) — 계약 소진율 대시보드 구성
- Extensibility —
@Metadata.allowExtensions: true로 커스텀 필드 확장
11. 더 파고들 때 도움이 되는 자료
댓글 0
아직 댓글이 없습니다.