ABAP

BSID 직접 조회 그만 — CDS로 인보이스 #shorts #SAP #ABAP

▶ YouTube에서 보기

I_CustomerInvoice CDS 뷰 개요 — BSID와 무엇이 다른가

SAP S/4HANA에서 고객 인보이스 데이터를 조회할 때 전통적으로 개발자들은 BSID(미결 고객 항목)와 BSAD(완결 고객 항목) 테이블을 직접 액세스해 왔습니다. 그러나 S/4HANA 이후 이러한 클래식 테이블 구조는 ACDOCA(Universal Journal)로 통합되었고, 이를 의미론적으로 재구성한 뷰가 바로 I_CustomerInvoice입니다. 이 CDS 뷰는 재무회계(FI)의 고객 인보이스 문서 헤더 정보를 비즈니스 관점에서 노출하며, 회사코드·회계연도·문서번호·결제조건·순액 등 실무에서 필요한 필드를 정규화된 형태로 제공합니다.

이 글에서 다룰 체크 항목은 다음과 같습니다.

  • BSID/BSAD 대신 I_CustomerInvoice를 사용해야 하는 이유 파악
  • 주요 필드 구조와 어소시에이션(Association) 이해
  • ABAP CDS SELECT 구문으로 기본/필터/집계 쿼리 작성
  • OData 서비스와 연동해 Fiori/외부 시스템에서 활용
  • 대량 데이터 조회 시 성능 최적화 포인트 습득

필요한 사전 배경

이 글을 원활히 따라가려면 ABAP Open SQL과 CDS View의 기본 문법(DEFINE VIEW, ASSOCIATION), ABAP for Cloud Development 또는 ABAP Development Tools(ADT) 사용 경험이 필요합니다. 또한 SAP FI의 총계정원장/미결항목 개념, S/4HANA의 Universal Journal(ACDOCA) 구조에 대한 개괄적 이해가 있으면 큰 도움이 됩니다. Eclipse 기반 ADT와 Data Preview 기능을 다뤄본 적이 있다면 더욱 좋습니다.

실행 환경과 준비물

본 예제는 다음 환경을 기준으로 작성했습니다.

  • SAP S/4HANA 2022 On-Premise 또는 SAP S/4HANA Cloud Public Edition 2402 이상
  • ABAP Platform 2022 (Steampunk 환경에서도 유사하게 적용)
  • ABAP Development Tools(ADT) for Eclipse 2025-03 이상
  • SAP GUI 8.00 (릴리스노트 확인용)
  • 테스트 권한: S_RFC, S_TABU_DIS 및 재무 데이터 조회용 F_BKPF_BUK

사전 설정으로 ADT에서 시스템 연결을 등록한 뒤, Project ExplorerSystem LibraryDictionaryDDL Sources에서 I_CUSTOMERINVOICE가 조회되는지 먼저 확인합니다. 조회되지 않는다면 릴리스가 낮거나 해당 스코프가 활성화되지 않았을 가능성이 있으므로 Basis 팀에 확인이 필요합니다. 또한 Data Preview 실행 권한이 없으면 CDS 조회 결과를 미리 볼 수 없으므로 S_DEVELOP 권한을 함께 확인하세요.

왜 BSID 직접 조회는 위험한가 — 개념 이해

클래식 ECC 시절 BSID는 "고객 미결 인덱스" 역할을 담당했고, 개발자들은 SELECT * FROM bsid WHERE bukrs = ... 같은 쿼리를 무의식적으로 작성해 왔습니다. 하지만 S/4HANA에서는 회계 문서의 진실 원천(Single Source of Truth)이 ACDOCA로 이동했고, BSID/BSAD/BKPF 등은 호환성 뷰(compatibility view) 또는 Persistency Layer의 잔재로만 남았습니다. 즉, BSID를 직접 조회하는 코드는 다음과 같은 리스크를 안게 됩니다.

BSID는 이제 "인덱스"가 아니라 "결과의 부분집합"이다. 결제 상태가 변하면 BSID → BSAD로 이동하지만, ACDOCA는 결제 상태를 필드로 표현할 뿐 이동이 없다.

비유하자면 BSID는 도서관의 "대출 중 도서 목록"이고, ACDOCA는 "전체 장서 목록 + 상태 컬럼"입니다. 도서가 반납되면 BSID에서는 사라지지만 ACDOCA에서는 상태만 변경됩니다. I_CustomerInvoice는 ACDOCA 기반 뷰이므로 미결/완결 여부에 관계없이 일관된 관점을 제공합니다.

주요 어소시에이션은 아래와 같습니다.

  • _Customer: 고객 마스터(I_Customer)와 연결
  • _CompanyCode: 회사코드 텍스트/통화 정보
  • _AccountingDocument: 원본 회계 전표(I_JournalEntry)로 드릴다운
  • _PaymentTerms: 결제 조건 상세

이 어소시에이션을 활용하면 JOIN을 명시적으로 작성하지 않고도 관련 데이터를 자연스럽게 확장 조회할 수 있습니다.

필드 구조 상세 분석

I_CustomerInvoice의 대표 필드를 살펴봅시다.

필드명타입설명
CustomerInvoiceCHAR(10)고객 인보이스 번호 (BELNR 대응)
CompanyCodeCHAR(4)회사코드 (BUKRS)
FiscalYearNUMC(4)회계연도 (GJAHR)
CustomerCHAR(10)고객 계정 (KUNNR)
DocumentDateDATS문서 일자 (BLDAT)
NetPaymentAmountCURR순 지급액
InvoiceGrossAmountCURR총 인보이스 금액
IsClearedInvoiceCHAR(1)결제 완료 여부 플래그
PaymentTermsCHAR(4)결제 조건 코드

특히 IsClearedInvoice 플래그가 핵심입니다. BSID/BSAD 분기 없이 이 필드 하나로 미결/완결을 구분할 수 있기 때문입니다.

1단계: 기본 조회 예제

가장 단순한 예제부터 시작합시다. 특정 회사코드의 최근 인보이스 100건을 조회하는 코드입니다.

REPORT z_inv_basic_query.

DATA: lt_invoices TYPE STANDARD TABLE OF i_customerinvoice,
      lv_companyCode TYPE bukrs VALUE '1010'.

SELECT CustomerInvoice,
       CompanyCode,
       Customer,
       FiscalYear,
       DocumentDate,
       NetPaymentAmount,
       TransactionCurrency,
       IsClearedInvoice
  FROM i_customerinvoice
  WHERE CompanyCode = @lv_companyCode
  ORDER BY DocumentDate DESCENDING
  INTO TABLE @lt_invoices
  UP TO 100 ROWS.

LOOP AT lt_invoices INTO DATA(ls_inv).
  WRITE: / ls_inv-CustomerInvoice,
           ls_inv-Customer,
           ls_inv-NetPaymentAmount CURRENCY ls_inv-TransactionCurrency,
           ls_inv-IsClearedInvoice.
ENDLOOP.

여기서 눈여겨볼 점은 이스케이프 문자(@)를 사용한 호스트 변수 표기법입니다. S/4HANA에서는 인라인 선언과 함께 @ 접두어가 권장됩니다. 또한 UP TO 100 ROWS로 결과 셋을 제한해 초기 개발 단계에서 대량 데이터 쿼리로 인한 세션 타임아웃을 방지합니다.

2단계: 미결 인보이스만 골라내기 (실무 시나리오)

회계팀이 자주 요청하는 "이번 분기 미수금 리스트"를 만드는 예제입니다. 예외 처리와 로깅을 함께 넣었습니다.

CLASS zcl_open_invoice_reader DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_open_inv,
             invoice_no    TYPE vbeln_va,
             customer      TYPE kunnr,
             net_amount    TYPE dmbtr,
             currency      TYPE waers,
             due_date      TYPE dats,
           END OF ty_open_inv,
           tt_open_inv TYPE STANDARD TABLE OF ty_open_inv WITH EMPTY KEY.

    METHODS get_open_invoices
      IMPORTING iv_company_code TYPE bukrs
                iv_date_from    TYPE dats
                iv_date_to      TYPE dats
      RETURNING VALUE(rt_result) TYPE tt_open_inv
      RAISING   cx_sy_open_sql_db.
ENDCLASS.

CLASS zcl_open_invoice_reader IMPLEMENTATION.
  METHOD get_open_invoices.
    TRY.
        SELECT CustomerInvoice AS invoice_no,
               Customer        AS customer,
               NetPaymentAmount AS net_amount,
               TransactionCurrency AS currency,
               NetDueDate      AS due_date
          FROM i_customerinvoice
          WHERE CompanyCode      = @iv_company_code
            AND DocumentDate     BETWEEN @iv_date_from AND @iv_date_to
            AND IsClearedInvoice = @abap_false
          ORDER BY NetDueDate
          INTO TABLE @rt_result.

        IF sy-subrc <> 0.
          MESSAGE |조건에 해당하는 미결 인보이스가 없습니다.| TYPE 'I'.
        ELSE.
          cl_demo_output=>write(
            |{ lines( rt_result ) }건의 미결 인보이스가 조회되었습니다.| ).
        ENDIF.

      CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
        cl_demo_output=>write( |쿼리 오류: { lx_sql->get_text( ) }| ).
        RAISE EXCEPTION lx_sql.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

IsClearedInvoice = @abap_false 조건으로 BSID에 해당하는 미결 항목만 필터링합니다. 예외 클래스 cx_sy_open_sql_db를 명시적으로 캐치해 상위 계층으로 전파하는 패턴을 사용했습니다.

3단계: 고객별 총 미수금 집계 (프로덕션 레벨)

이번에는 CDS View를 확장(Extend)하지 않고 ABAP 코드 안에서 집계 쿼리를 작성하는 예제입니다. 페이지네이션과 통화 변환까지 고려합니다.

CLASS zcl_receivable_summary DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_receivable,
             customer         TYPE kunnr,
             currency         TYPE waers,
             open_count       TYPE i,
             total_receivable TYPE dmbtr,
           END OF ty_receivable,
           tt_receivable TYPE STANDARD TABLE OF ty_receivable WITH EMPTY KEY.

    CONSTANTS c_page_size TYPE i VALUE 500.

    METHODS aggregate_receivables
      IMPORTING iv_company_code   TYPE bukrs
                iv_customer_group TYPE char10 DEFAULT 'DOMESTIC'
                iv_page_index     TYPE i     DEFAULT 1
      RETURNING VALUE(rt_summary) TYPE tt_receivable.
ENDCLASS.

CLASS zcl_receivable_summary IMPLEMENTATION.
  METHOD aggregate_receivables.
    DATA(lv_offset) = ( iv_page_index - 1 ) * c_page_size.

    SELECT inv~Customer         AS customer,
           inv~TransactionCurrency AS currency,
           COUNT(*)              AS open_count,
           SUM( inv~NetPaymentAmount ) AS total_receivable
      FROM i_customerinvoice AS inv
      INNER JOIN i_customer AS cust
        ON  cust~Customer = inv~Customer
      WHERE inv~CompanyCode      = @iv_company_code
        AND inv~IsClearedInvoice = @abap_false
        AND cust~CustomerGroup   = @iv_customer_group
      GROUP BY inv~Customer, inv~TransactionCurrency
      HAVING SUM( inv~NetPaymentAmount ) > 0
      ORDER BY total_receivable DESCENDING
      INTO TABLE @rt_summary
      OFFSET @lv_offset
      UP TO @c_page_size ROWS.

    cl_demo_output=>write( |Page { iv_page_index }: { lines( rt_summary ) } customers aggregated| ).
  ENDMETHOD.
ENDCLASS.

포인트를 정리하면 다음과 같습니다.

  • OFFSET + UP TO n ROWS로 페이지네이션 구현 (HANA 최적화된 방식)
  • HAVING 절로 잔액이 0 이하인 그룹 제거
  • 고객 마스터와 INNER JOIN해 CustomerGroup이라는 비즈니스 세그먼트 조건 적용
  • 통화별로 그룹핑해 다국통화 환경에서도 오차 없이 집계

OData 서비스와의 연동 시나리오

Fiori 앱이나 외부 시스템에서 이 데이터를 소비하려면 CDS View 위에 @OData.publish: true 애노테이션을 붙이거나 Service Binding을 정의합니다. 커스텀 소비 뷰 예시입니다.

@AbapCatalog.sqlViewName: 'ZOPENINVSUM'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Open Receivables Summary'
@OData.publish: true
define view ZC_OpenReceivableSummary
  as select from I_CustomerInvoice as inv
  association [1] to I_Customer as _Customer
    on $projection.Customer = _Customer.Customer
{
  key inv.Customer                     as Customer,
  key inv.CompanyCode                  as CompanyCode,
      inv.TransactionCurrency          as Currency,
      sum(inv.NetPaymentAmount)        as OpenAmount,
      count(*)                         as InvoiceCount,
      _Customer.CustomerName           as CustomerName,
      _Customer._Country.CountryName   as Country
}
where inv.IsClearedInvoice = ' '
group by inv.Customer,
         inv.CompanyCode,
         inv.TransactionCurrency,
         _Customer.CustomerName,
         _Customer._Country.CountryName;

서비스 활성화 후 Fiori 또는 외부 앱에서 다음과 같이 OData 호출을 구성할 수 있습니다.

const oModel = new sap.ui.model.odata.v2.ODataModel("/sap/opu/odata/sap/ZC_OPENRECEIVABLESUMMARY_CDS/");
oModel.read("/ZC_OpenReceivableSummarySet", {
  urlParameters: {
    "$filter": "CompanyCode eq '1010' and OpenAmount gt 10000",
    "$orderby": "OpenAmount desc",
    "$top": 50
  },
  success: function(oData) {
    console.log("Retrieved " + oData.results.length + " customers");
  },
  error: function(oError) {
    console.error("OData query failed", oError);
  }
});

RAP(ABAP RESTful Application Programming Model) 기반이라면 Service Definition/Service Binding을 별도로 만들어 V4 OData로 노출하는 방식이 일반적으로 권장됩니다.

흔한 실수와 트러블슈팅

Q1. I_CustomerInvoice가 시스템에서 조회되지 않아요.
릴리스 레벨과 스코프 아이템이 활성화되어 있는지 확인하세요. On-Premise 2022 미만이나 특정 산업 솔루션에서는 뷰명이 다를 수 있습니다. I_OperationalAcctgDocItem 또는 I_JournalEntryItem을 대안으로 검토할 수 있습니다.

Q2. 결과 건수가 BSID 대비 훨씬 많거나 적게 나옵니다.
BSID는 미결 항목만, BSAD는 완결 항목만 포함하는 인덱스입니다. I_CustomerInvoice는 두 상태를 모두 포함하므로 반드시 IsClearedInvoice 필터를 붙여야 동등한 결과가 나옵니다. 또한 문서 유형(AccountingDocumentType)이 실제 고객 인보이스(예: DR, RV)인지도 함께 확인하세요.

Q3. 대량 조회 시 응답이 너무 느립니다.
회사코드·회계연도·문서일자 조건을 동시에 사용하는 것이 성능상 유리합니다. HANA에서도 파티셔닝 키가 이들이기 때문입니다. 또한 SELECT * 대신 필요한 필드만 명시하고, 어소시에이션 확장은 실제 사용하는 경우에만 path expression으로 접근하는 것이 좋습니다.

Q4. NetPaymentAmount가 현지통화인지 문서통화인지 헷갈립니다.
필드에 딸린 통화 참조 필드(TransactionCurrency)를 함께 봐야 합니다. 회사코드 통화가 필요하면 InvoiceGrossAmountInCoCodeCrcy 계열 필드를 활용하세요. 잘못된 통화로 SUM을 하면 다국통화 환경에서 값이 왜곡됩니다.

확장 학습 방향

이번 글에서 익힌 개념을 바탕으로 다음 주제로 나아갈 수 있습니다.

  • I_JournalEntry, I_JournalEntryItem을 활용한 전표 레벨 분석
  • RAP 기반 커스텀 Business Object로 미수금 관리 앱 구축
  • Embedded Analytics(Analytical Query)를 통한 Fiori KPI 타일 개발
  • ABAP for Cloud Development(Steampunk) 환경에서의 릴리스 제약 이해
  • Data Aging/Archiving 정책과 ACDOCA 파티셔닝 전략 학습

댓글 0

아직 댓글이 없습니다.