ABAP

VBAK vs I_SalesOrder — 판매 오더 조회 차이 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 다루는 내용

SAP S/4HANA의 SD(Sales & Distribution) 영역에서 판매 오더를 다루다 보면 가장 먼저 마주치게 되는 테이블이 VBAK(판매 오더 헤더)입니다. 그러나 S/4HANA 환경에서는 더 이상 VBAK를 직접 SELECT 하기보다 I_SalesOrder라는 CDS Interface View를 통해 접근하는 패턴이 권장됩니다. 이 글은 VBAK 직접 조회의 한계를 짚고, I_SalesOrder의 필드 매핑·association 구조를 분석한 뒤, 실무에서 자주 발생하는 "고객별 미결 오더 집계", "가격 조건(Pricing Element) 분석"까지 단계적으로 풀어냅니다.

  • VBAK ↔ I_SalesOrder 필드 매핑과 추상화 의도 이해
  • I_Customer, I_PricingElement와의 association 활용
  • 미결 오더(open order) 판별 로직과 집계 패턴
  • 대용량 트랜잭션 환경에서의 성능·권한·테스트 고려사항

읽기 전 알아두면 좋은 배경

이 글은 ABAP Open SQL과 기본 CDS 구문(define view, association, annotation)을 한 번이라도 작성해 본 개발자를 가정합니다. SD 모듈의 판매 오더 라이프사이클(생성 → 출하 → 청구), VBAK/VBAP/VBKD/KONV 같은 핵심 테이블 명칭, ABAP의 SELECT ... FOR ALL ENTRIES 또는 WITH 절 사용 경험이 있으면 이해가 빠릅니다.

환경, 릴리스, 준비물

I_SalesOrder는 S/4HANA 표준 Virtual Data Model(VDM)에 포함된 Interface View로, 일반적으로 S/4HANA 1709 이후 릴리스부터 사용 가능하며 릴리스 업그레이드에 따라 필드와 association이 점진적으로 확장되어 왔습니다. 본문 예제는 다음 환경을 기준으로 합니다.

  • SAP S/4HANA 2022(또는 그 이후) on-premise / Private Cloud Edition
  • ABAP Platform 2022, ADT 3.32 이상 (Eclipse 2023-09 권장)
  • DB: SAP HANA 2.0 SPS06 이상
  • 권한: S_RFC, S_DEVELOP(개발자), 데이터 접근을 위한 V_VBAK_VKO(판매 조직 권한) 등

Cloud(Public Edition) 환경에서는 ABAP Cloud 모델로 인해 직접 VBAK SELECT는 차단되며, I_SalesOrder(또는 C1/C2 등급의 Released API)만 사용 가능하다는 점을 유의해야 합니다.

핵심 개념: VBAK에서 I_SalesOrder로

VBAK는 1990년대 R/3 시절부터 사용된 물리 테이블입니다. 컬럼명이 VBELN(오더 번호), KUNNR(고객), VKORG(판매 조직), NETWR(순액)처럼 4~6자리 독일어 약어로 되어 있어 가독성이 낮습니다. 또한 통화 단위(WAERK)와 금액(NETWR)의 연결, 삭제 플래그(LOEKZ) 등 보일러플레이트 로직이 매번 반복됩니다.

I_SalesOrder는 이를 다음과 같은 의미 기반 필드명으로 추상화합니다.

VBAK 필드I_SalesOrder 필드의미
VBELNSalesOrder판매 오더 번호
AUARTSalesOrderType오더 유형
KUNNRSoldToParty판매처 고객
VKORGSalesOrganization판매 조직
NETWRTotalNetAmount순액 총계
WAERKTransactionCurrency거래 통화
ERDATCreationDate생성일

여기에 더해 I_SalesOrder_SoldToParty(→ I_Customer), _SalesOrderItem(→ I_SalesOrderItem), _PricingElement(→ I_PricingElement, KONV 기반) 같은 association을 노출합니다.

일반적으로 VBAK 직접 조회는 (1) 필드 의미 파악 비용, (2) 권한 체크 누락, (3) Cloud 호환성 부재, (4) 향후 필드 확장 시 코드 깨짐 등의 리스크가 있어 신규 개발에서는 지양하는 것이 권장됩니다.

실전 예제 1단계: 기본 SELECT와 association 따라가기

가장 단순한 형태로, 특정 판매 조직에 속한 최근 30일간의 오더 헤더와 판매처 고객 이름을 한 번에 가져오는 예제입니다.

REPORT zr_so_basic_lookup.

SELECT FROM I_SalesOrder AS so
  FIELDS so~SalesOrder,
         so~SalesOrderType,
         so~SoldToParty,
         so~_SoldToParty-CustomerName AS sold_to_name,
         so~TotalNetAmount,
         so~TransactionCurrency,
         so~CreationDate
  WHERE  so~SalesOrganization = @'1010'
    AND  so~CreationDate     >= @( cl_abap_context_info=>get_system_date( ) - 30 )
  ORDER BY so~CreationDate DESCENDING
  INTO TABLE @DATA(lt_orders).

cl_demo_output=>display( lt_orders ).

여기서 주목할 부분은 so~_SoldToParty-CustomerName입니다. 이는 I_Customer로 가는 association을 자동 JOIN으로 변환합니다. 동일한 결과를 VBAK + KNA1 조합으로 작성하면 약 두 배 길이의 코드가 되며, 클라이언트 필드, 삭제 플래그, 권한 체크를 모두 직접 다뤄야 합니다.

실전 예제 2단계: 고객별 미결 오더 집계와 예외 처리

실무에서 가장 빈번한 시나리오 중 하나는 "특정 고객이 아직 출하/청구되지 않은 오더가 얼마나 있는가?"를 빠르게 보여주는 것입니다. 헤더만으로는 미결 여부를 알 수 없고, 아이템 레벨의 전체 처리 상태(OverallSDProcessStatus)를 확인해야 합니다.

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Open Sales Orders by Customer'
define view entity ZC_OpenOrdersByCustomer
  as select from I_SalesOrder as so
  association [0..1] to I_Customer as _Customer
    on $projection.SoldToParty = _Customer.Customer
{
  key so.SoldToParty                              as Customer,
      _Customer.CustomerName                      as CustomerName,
      count( distinct so.SalesOrder )             as OpenOrderCount,
      sum( so.TotalNetAmount )                    as OpenNetAmount,
      so.TransactionCurrency                      as Currency
}
where so.OverallSDProcessStatus in ( 'A', 'B' )
group by so.SoldToParty,
         _Customer.CustomerName,
         so.TransactionCurrency
METHOD report_open_orders.

  TRY.
      SELECT FROM zc_openordersbycustomer
        FIELDS Customer, CustomerName, OpenOrderCount, OpenNetAmount, Currency
        WHERE  Customer IN @it_customer_range
        ORDER BY OpenNetAmount DESCENDING
        INTO TABLE @DATA(lt_result).

      IF lt_result IS INITIAL.
        RETURN.
      ENDIF.

      LOOP AT lt_result ASSIGNING FIELD-SYMBOL().
        IF -openNetAmount > 100000.
          send_alert( iv_customer = -customer
                      iv_amount   = -openNetAmount ).
        ENDIF.
      ENDLOOP.

    CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
      cl_aff_log=>add_message(
        EXPORTING iv_msgid = 'ZSD'
                  iv_msgno = '010'
                  iv_msgty = 'E'
                  iv_msgv1 = lx_sql->get_text( ) ).
      RAISE EXCEPTION TYPE cx_zsd_open_orders
        EXPORTING previous = lx_sql.
  ENDTRY.

ENDMETHOD.

이 단계에서 중요한 포인트는 두 가지입니다. 첫째, 미결 판정 로직(OverallSDProcessStatus in ('A','B'))을 CDS 뷰 안에 캡슐화해서 모든 호출자가 동일한 정의를 공유합니다. 둘째, ABAP 호출부는 비즈니스 분기와 로깅에만 집중합니다.

실전 예제 3단계: 가격 조건 분석과 운영 품질 고려

전통적으로 KONV(또는 PRCD_ELEMENTS) 테이블을 직접 조회해 조건 유형별(예: PR00=기본가, K007=할인) 금액을 추출했지만, I_SalesOrder_PricingElement association을 사용하면 헤더·아이템 키 매칭과 통화 변환 부담을 줄일 수 있습니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Discount Ratio per Sales Order'
define view entity ZC_OrderDiscountRatio
  as select from I_SalesOrder as so
{
  key so.SalesOrder                                            as SalesOrder,
      so.SoldToParty                                           as Customer,
      so.TotalNetAmount                                        as NetAmount,
      so.TransactionCurrency                                   as Currency,
      cast(
        coalesce(
          ( select sum( p.ConditionAmount )
              from I_PricingElement as p
              where p.SalesOrder      = so.SalesOrder
                and p.ConditionType   = 'K007' ),
          0 )
        as abap.dec( 15, 2 ) )                                 as TotalDiscount,
      case when so.TotalNetAmount > 0
           then division(
                  coalesce( ( select sum( p.ConditionAmount )
                                from I_PricingElement as p
                                where p.SalesOrder    = so.SalesOrder
                                  and p.ConditionType = 'K007' ), 0 ),
                  so.TotalNetAmount, 4 )
           else cast( 0 as abap.dec( 5, 4 ) )
      end                                                      as DiscountRatio
}

운영 단계에서는 다음 사항을 함께 고려합니다.

  • 성능: 가격 조건은 오더당 수십 행이 되기 쉬우므로, 분석 대상 기간을 반드시 CreationDate 등으로 제한합니다.
  • 테스트: ABAP Unit + CDS Test Double Framework로 I_SalesOrder를 더블링하면 실제 트랜잭션 데이터 없이도 시나리오를 재현할 수 있습니다.
  • 보안: @AccessControl.authorizationCheck: #CHECK는 DCL에서 정의된 판매 조직/영업처 권한을 자동 적용합니다.
  • 릴리스 호환: Released API 사용 여부를 ADT의 API State 탭에서 확인합니다.

자주 마주치는 함정과 트러블슈팅 FAQ

Q1. VBAK에는 있는 필드가 I_SalesOrder에는 없어 보입니다.
일부 운영용 필드는 Interface View에 노출되지 않습니다. 이런 경우 ZI_SalesOrderExt 뷰에서 I_SalesOrder를 base로 두고 추가 association을 만드는 패턴이 일반적으로 권장됩니다.

Q2. 미결 상태를 헤더 한 필드로만 판단해도 되나요?
헤더의 OverallSDProcessStatus는 아이템 상태의 집계입니다. 부분 출하·부분 청구가 빈번한 환경에서는 아이템 레벨(I_SalesOrderItem)의 DeliveryStatus를 함께 확인하는 것이 더 안전합니다.

Q3. CDS에서 association을 썼는데 실제로 JOIN이 발생하지 않는 것 같습니다.
association은 SELECT 절·WHERE 절에서 실제 필드를 참조해야 JOIN으로 펼쳐집니다. ST05 SQL 트레이스로 확인하고, LOOP 안에서 매번 참조하는 N+1 패턴에 주의합니다.

이어서 보면 좋은 주제

  • I_SalesOrderItem, I_SalesOrderScheduleLine을 활용한 아이템·납기 분석
  • RAP(ABAP RESTful Application Programming Model)로 판매 오더 조회용 OData 서비스 노출
  • Analytical Query(C_SalesOrderQry 계열)로 Fiori Analytical List Page 구성
  • CDS Test Double Framework로 단위 테스트 자동화

댓글 0

아직 댓글이 없습니다.