ABAP

EKKO 직접 조회 90%가 잘못된 이유 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. I_SchedulingAgreement가 해결하는 문제

구매 스케줄링 어그리먼트(Scheduling Agreement)는 장기 공급 계약을 체결하고 그 아래에 반복적인 납기 일정(Delivery Schedule Line)을 관리하는 SAP 표준 프로세스입니다. 전통적으로 개발자는 EKKO(문서 헤더) 테이블에서 BSTYP = 'L' 조건을 걸어 스케줄링 어그리먼트만 골라내고, 다시 EKPO(아이템), EKET(딜리버리 스케줄), LFA1(공급업체) 등을 수동으로 조인해 왔습니다. 그러나 S/4HANA 환경에서는 이런 방식이 필드 매핑 오류, 삭제 플래그 누락, 조직 권한 체크 미적용 등 다양한 리스크를 만들어냅니다.

I_SchedulingAgreement는 이 문제를 표준 CDS View 계층으로 흡수합니다. 이 글에서 도달할 지점은 다음과 같습니다.

  • EKKO 원시 조회 대비 I_SchedulingAgreement가 제공하는 시맨틱 이점 파악
  • 헤더-아이템-딜리버리 스케줄 3계층 CDS 조합 방식 이해
  • 유효기간(Validity), 삭제 코드, 릴리스 상태를 반영한 실무 SELECT 작성
  • 납기 지연·초과 건 집계 로직을 CDS + ABAP 조합으로 구현
  • 권한 오류·성능 저하·필드 명명 혼동 등 자주 만나는 함정 회피

2. 시작 전에 갖춰야 할 배경

이 글은 S/4HANA 구매 모듈(MM-PUR) 데이터 모델과 ABAP CDS 문법을 이미 다뤄본 개발자를 대상으로 합니다. 최소한 SELECT ... FROM cds_entity 문법, Association의 _Association[...] 참조 표기, @ObjectModel.readOnly 등 어노테이션의 의미를 알고 있어야 합니다. 또한 EKKO/EKPO/EKET 테이블 구조와 문서 유형(BSTYP, BSART) 개념, 구매조직(EKORG)·구매그룹(EKGRP) 조직 코드에 대한 이해가 필요합니다.

3. 환경과 사전 준비 항목

이 글의 코드는 다음 환경에서 검증되었으며, 릴리스에 따라 필드 명칭이 소폭 달라질 수 있습니다.

  • SAP S/4HANA 2022 FPS02 (On-Premise) 또는 S/4HANA Cloud Public 2023
  • ABAP Platform 2022, ADT(Eclipse) 2023-06 이상
  • SAP GUI 8.00 (참고용 SE16, SE11)
  • 권한: S_TABU_DIS(그룹 ME), M_BEST_EKO(구매조직), M_BEST_BSA(문서 유형 L)
  • 테스트 데이터: 공급업체 VENDOR-5501, 구매조직 1710, 플랜트 2010, 자재 RM-20045

ADT의 Data PreviewI_SchedulingAgreement를 열어 필드 목록과 Association 이름을 먼저 확인하면 이후 코드 작성이 수월합니다. On-Premise 환경에서 CDS 명칭이 보이지 않을 경우 SAP Note를 참고해 관련 BC Set 활성화 여부를 점검하십시오.

4. 스케줄링 어그리먼트 개념과 CDS 계층

스케줄링 어그리먼트는 "장기 계약 헤더 + 반복 납기 라인"이라는 이중 구조를 가집니다. 이를 창고 임대 계약에 비유하면, 임대차 계약서(헤더)가 있고 그 안에 매월 보관 물량 배정표(딜리버리 스케줄)가 붙는 형태입니다. SAP는 이 구조를 세 계층의 CDS로 노출합니다.

  • I_SchedulingAgreement — 헤더(EKKO 기반, BSTYP='L')
  • I_SchedulingAgreementItem — 아이템(EKPO 기반, 자재·플랜트·수량·단가)
  • I_SchedgAgreementDeliverySchd — 딜리버리 스케줄(EKET 기반, 납기일·계획수량)

세 뷰는 Association(_SchedulingAgreementItem, _DeliverySchedule)으로 연결되어 있어 SQL JOIN을 명시하지 않고도 트리 형태로 접근할 수 있습니다. 도식으로 표현하면 다음과 같습니다.

I_SchedulingAgreement (헤더)
   |-- _Supplier              --> I_Supplier
   |-- _PurchasingOrganization --> I_PurchasingOrganization
   |-- _SchedulingAgreementItem (0..N)
          |-- _Material        --> I_Product
          |-- _DeliverySchedule (0..N) --> I_SchedgAgreementDeliverySchd

주요 헤더 필드는 다음과 같이 EKKO 원본 컬럼과 매핑됩니다. 필드명이 시맨틱하게 변경되어 있음에 주목하십시오.

CDS 필드EKKO 원본의미
SchedulingAgreementEBELN문서번호
SchedulingAgreementTypeBSART문서 유형(LP, LPA 등)
PurchasingOrganizationEKORG구매조직
SupplierLIFNR공급업체
ValidityStartDate / ValidityEndDateKDATB / KDATE유효기간
PurchasingDocumentDeletionCodeLOEKZ삭제 플래그

5. EKKO 대신 CDS를 우선 선택해야 하는 이유

기술적으로 SELECT * FROM ekko WHERE bstyp = 'L'도 동일한 결과를 낼 수 있으나, 다음과 같은 이유로 신규 개발에서는 CDS 사용이 일반적으로 권장됩니다.

  • 시맨틱 명칭: LIFNR 대신 Supplier를 씀으로써 코드 가독성이 극적으로 향상됩니다.
  • 필터 캡슐화: BSTYP='L' 필터가 뷰 내부에 이미 걸려 있어 실수로 다른 문서 유형을 섞을 위험이 없습니다.
  • Association 재사용: JOIN 조건을 매번 작성하지 않고 _Supplier.SupplierName처럼 참조합니다.
  • Fiori/OData 연계: 동일 뷰가 표준 Fiori 앱과 OData 서비스에서도 사용되어 UI-백엔드 정합성이 유지됩니다.
  • 확장성: 필드 확장(Extend View)이 표준 방식으로 제공됩니다.

6. 헤더 조회 실전 예제 — 유효한 어그리먼트 뽑기

첫 단계로 특정 구매조직에서 오늘 시점 기준 유효(Not Deleted, In Validity)한 스케줄링 어그리먼트 헤더를 조회합니다.

REPORT zr_sa_header_scan.

DATA lt_agreement TYPE TABLE OF i_schedulingagreement.

SELECT SchedulingAgreement,
       SchedulingAgreementType,
       PurchasingOrganization,
       PurchasingGroup,
       Supplier,
       ValidityStartDate,
       ValidityEndDate,
       PurchasingDocumentDeletionCode
  FROM i_schedulingagreement
  WHERE PurchasingOrganization  = '1710'
    AND Supplier                = 'VENDOR-5501'
    AND ValidityStartDate      <= @sy-datum
    AND ValidityEndDate        >= @sy-datum
    AND PurchasingDocumentDeletionCode = ''
  ORDER BY ValidityEndDate ASCENDING
  INTO TABLE @lt_agreement.

cl_demo_output=>display( lt_agreement ).

포인트는 PurchasingDocumentDeletionCode = ''로 삭제된 문서를 걸러낸 점입니다. EKKO 직조회 시 자주 누락되는 조건이며, 이걸 빼면 이미 종료된 계약이 결과에 섞여 리포트 신뢰도가 크게 떨어집니다.

7. 딜리버리 스케줄 결합 — 3계층 JOIN

두 번째 단계에서는 헤더-아이템-딜리버리 스케줄을 한 번에 결합해 "특정 자재의 향후 3개월 납기 계획"을 조회합니다.

TYPES: BEGIN OF ty_schedule,
         agreement       TYPE i_schedulingagreement-schedulingagreement,
         item            TYPE i_schedulingagreementitem-schedulingagreementitem,
         material        TYPE i_schedulingagreementitem-material,
         plant           TYPE i_schedulingagreementitem-plant,
         delivery_date   TYPE i_schedgagreementdeliveryschd-scheduleline_deliverydate,
         planned_qty     TYPE i_schedgagreementdeliveryschd-scheduleline_scheduledquantity,
         net_price       TYPE i_schedulingagreementitem-netpriceamount,
       END OF ty_schedule.

DATA lt_result TYPE TABLE OF ty_schedule.

SELECT sa~schedulingagreement       AS agreement,
       it~schedulingagreementitem   AS item,
       it~material                  AS material,
       it~plant                     AS plant,
       ds~scheduleline_deliverydate AS delivery_date,
       ds~scheduleline_scheduledquantity AS planned_qty,
       it~netpriceamount            AS net_price
  FROM i_schedulingagreement AS sa
   INNER JOIN i_schedulingagreementitem      AS it ON it~schedulingagreement = sa~schedulingagreement
   INNER JOIN i_schedgagreementdeliveryschd  AS ds ON ds~schedulingagreement     = it~schedulingagreement
                                                  AND ds~schedulingagreementitem = it~schedulingagreementitem
  WHERE sa~purchasingorganization  = '1710'
    AND sa~supplier                = 'VENDOR-5501'
    AND it~material                = 'RM-20045'
    AND it~plant                   = '2010'
    AND ds~scheduleline_deliverydate BETWEEN @sy-datum AND @( sy-datum + 90 )
    AND it~purchasingdocumentitemdeletioncode = ''
  ORDER BY delivery_date ASCENDING
  INTO TABLE @lt_result.

Association 표기(sa\_SchedulingAgreementItem)를 활용하면 위 JOIN을 훨씬 짧게 쓸 수 있으나, 명시적 JOIN이 성능 튜닝 관점에서 더 예측 가능한 실행 계획을 만들기 때문에 대량 조회 리포트에서는 명시 JOIN을 선호하는 실무 팀이 많습니다.

8. 납기 현황 분석 — 지연 건 집계 리포트

세 번째 단계에서는 프로덕션 등급 리포트를 목표로 합니다. 공급업체별로 "약속 납기가 오늘 이전인데 아직 GR 미완료(잔여 계획수량 > 0)"인 라인을 뽑아 SLA 지표를 계산합니다.

CLASS zcl_sa_delivery_monitor DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_overdue,
             supplier         TYPE i_schedulingagreement-supplier,
             agreement        TYPE i_schedulingagreement-schedulingagreement,
             item             TYPE i_schedulingagreementitem-schedulingagreementitem,
             material         TYPE i_schedulingagreementitem-material,
             delivery_date    TYPE i_schedgagreementdeliveryschd-scheduleline_deliverydate,
             open_qty         TYPE i_schedgagreementdeliveryschd-scheduleline_scheduledquantity,
             overdue_days     TYPE i,
           END OF ty_overdue.

    METHODS collect_overdue
      IMPORTING iv_purch_org  TYPE i_schedulingagreement-purchasingorganization
                iv_cutoff     TYPE d DEFAULT sy-datum
      RETURNING VALUE(rt_data) TYPE STANDARD TABLE OF ty_overdue.
ENDCLASS.

CLASS zcl_sa_delivery_monitor IMPLEMENTATION.
  METHOD collect_overdue.
    TRY.
        AUTHORITY-CHECK OBJECT 'M_BEST_EKO'
          ID 'EKORG' FIELD iv_purch_org
          ID 'ACTVT' FIELD '03'.
        IF sy-subrc <> 0.
          RAISE EXCEPTION TYPE cx_sy_no_handler.
        ENDIF.

        SELECT sa~supplier,
               sa~schedulingagreement                       AS agreement,
               it~schedulingagreementitem                   AS item,
               it~material                                  AS material,
               ds~scheduleline_deliverydate                 AS delivery_date,
               ( ds~scheduleline_scheduledquantity - ds~scheduleline_deliveredquantity ) AS open_qty,
               CAST( @iv_cutoff - ds~scheduleline_deliverydate AS abap.int4 ) AS overdue_days
          FROM i_schedulingagreement AS sa
           INNER JOIN i_schedulingagreementitem     AS it ON it~schedulingagreement = sa~schedulingagreement
           INNER JOIN i_schedgagreementdeliveryschd AS ds ON ds~schedulingagreement     = it~schedulingagreement
                                                        AND ds~schedulingagreementitem = it~schedulingagreementitem
          WHERE sa~purchasingorganization         = @iv_purch_org
            AND sa~purchasingdocumentdeletioncode = ''
            AND it~purchasingdocumentitemdeletioncode = ''
            AND ds~scheduleline_deliverydate    < @iv_cutoff
            AND ds~scheduleline_scheduledquantity > ds~scheduleline_deliveredquantity
          INTO TABLE @rt_data.

        SORT rt_data BY overdue_days DESCENDING supplier ASCENDING.

      CATCH cx_sy_no_handler.
        cl_bali_message_setter=>create( )->set_message(
          iv_severity = 'E' iv_text = |권한 없음: { iv_purch_org }| ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

이 코드에서 주목할 점은 세 가지입니다. 첫째, 조회 이전에 M_BEST_EKO 권한 오브젝트를 명시 체크하여 결과 유출을 방지합니다. 둘째, ScheduleLine_ScheduledQuantity - ScheduleLine_DeliveredQuantity를 CDS 표현식으로 계산해 잔여 수량을 산출합니다. 셋째, 지연 일수를 CAST로 정수 변환하여 정렬·집계에 활용합니다.

9. 자주 마주치는 실수와 해결 방법

Q1. I_SchedulingAgreement에서 SELECT하면 계약(Contract)이 함께 나옵니다. — 이 CDS는 내부적으로 BSTYP='L' 필터가 걸려 있어 계약(BSTYP='K')이 나올 수 없습니다. 만약 섞여 보인다면 실제로는 I_PurchasingDocument나 EKKO를 잘못 조회했을 가능성이 큽니다.

Q2. 딜리버리 스케줄 필드명이 SCHEDULELINE_DELIVERYDATE처럼 이상합니다.I_SchedgAgreementDeliverySchd는 딜리버리 스케줄 라인 필드에 접두어 ScheduleLine_을 붙입니다. 헤더/아이템 필드와 명명 규칙이 다르므로 오토 컴플리션을 반드시 활용하십시오.

Q3. 성능이 느립니다. — 세 계층을 한 번에 조인하면 EKET 대용량으로 인해 쿼리 시간이 늘어납니다. PurchasingOrganization·Supplier를 필수 필터로 강제하고, 딜리버리 날짜 범위를 좁히세요. 사용자 정의 확장 필드가 많은 시스템에서는 헤더만 먼저 뽑아 2단 조회로 나누는 방식이 안전합니다.

Q4. 삭제된 어그리먼트 라인이 남습니다. — 헤더 삭제 코드(PurchasingDocumentDeletionCode)와 아이템 삭제 코드(PurchasingDocumentItemDeletionCode)는 별개입니다. 두 조건을 모두 걸어야 완전히 살아있는 라인만 남습니다.

Q5. 릴리스 상태를 확인하고 싶습니다. — 릴리스 전략이 켜진 시스템에서는 ReleaseIsCompleted 관련 필드가 존재합니다. 릴리스 미완료 문서는 실제로 발주로 넘어가지 않으므로 리포트에서 별도 표시하십시오.

10. 이어서 살펴볼 주제

이 글에서 다룬 I_SchedulingAgreement는 헤더 계층의 진입점입니다. 이를 기반으로 다음 주제로 확장해 나갈 수 있습니다.

  • C_SchedulingAgreement류 Consumption View — Fiori 앱과 OData 서비스 노출
  • RAP(RESTful ABAP Programming Model) — 어그리먼트 조회/변경 서비스 개발
  • I_PurchaseOrder와의 비교 — 문서 유형별 데이터 모델 차이
  • SAP Analytics Cloud에서 CDS View를 직접 소비하여 SLA 대시보드 구축
  • Behavior Definition을 통한 스케줄 라인 삭제/등록 이벤트 처리

11. 더 파고들 때 도움되는 자료

댓글 0

아직 댓글이 없습니다.