개요와 이 글에서 다루는 범위
판매 계약(Sales Contract)은 고객과 일정 기간 동안 정해진 조건으로 거래하기로 약속한 'Outline Agreement'의 한 종류입니다. S/4HANA에서는 전통적인 VBAK/VBAP 테이블 위에 Virtual Data Model(VDM) 계층이 얹혀 있어, I_SalesContract CDS View를 통해 의미가 명확한 필드명으로 계약 헤더를 조회할 수 있습니다. 이 글은 해당 View의 구조와 VBAK 컬럼 매핑, 일반 수주(Sales Order)에서 계약을 참조하는 방식, 그리고 실무에서 자주 등장하는 '만료 임박 계약 추출' 시나리오까지 다룹니다.
- I_SalesContract View의 핵심 필드와 카테고리 구분 이해
- VBAK 테이블 컬럼과 CDS 필드명 매핑 관계 파악
- Sales Contract와 Scheduling Agreement의 문서 유형 차이 식별
- I_SalesOrder.SalesContract 참조 필드로 계약-오더 연결 추적
- 유효기간 기반 만료 계약 조회 CDS/ABAP 예제 작성
- DCL(Data Control Language)과 권한 체크 시 주의점 정리
먼저 알아두면 좋은 사전 지식
이 글을 따라가려면 ABAP CDS View의 기본 문법(define view, association, annotation)과 SD(Sales & Distribution) 모듈의 문서 흐름(견적-수주-납품-청구)을 어느 정도 알고 있어야 합니다. 또한 VBAK/VBAP 테이블 구조에 익숙하다면 VDM의 필드 리네이밍 규칙을 빠르게 익힐 수 있습니다. ADT(ABAP Development Tools in Eclipse) 환경과 View Browser 사용 경험도 권장됩니다.
실행 환경과 사전 준비 사항
이 글의 예제는 일반적으로 SAP S/4HANA 2022 이상 온프레미스 또는 S/4HANA Cloud Private Edition 환경을 기준으로 작성되었습니다. 클라우드 Public Edition에서도 동일 View가 제공되지만, RAP(Released API) 등급에 따라 외부 ABAP 코드에서의 접근 가능 여부가 달라질 수 있습니다.
- SAP S/4HANA 2022 FPS01 이상(2023 권장)
- ABAP Development Tools(ADT) 3.30 이상이 설치된 Eclipse
- SE16/SE16N 또는 Data Preview 권한
- SD 모듈 트랜잭션 권한: VA41(계약 생성), VA43(계약 조회)
- I_SalesContract, I_SalesOrder View 조회 권한 및 DCL 역할
핵심 개념: Outline Agreement와 I_SalesContract의 위치
판매 영역에서 'Outline Agreement(외형 합의)'는 두 가지 큰 줄기로 나뉩니다. 첫째는 Sales Contract(판매 계약)로, 일정 기간 동안 정해진 금액·수량 한도 안에서 고객이 자유롭게 인출할 수 있도록 한 약속입니다. 둘째는 Scheduling Agreement(스케줄 동의)로, 납기 일정을 미리 확정해 자동으로 출하가 트리거되는 유형입니다. 두 문서 모두 VBAK 헤더 테이블에 저장되지만 VBTYP(SD Document Category)과 AUART(Sales Document Type)로 구분됩니다.
대표적인 문서 유형 코드는 다음과 같습니다.
| AUART | 의미 | VBTYP |
|---|---|---|
| WK1 | Quantity Contract(수량 계약) | G |
| WK2 | Value Contract(금액 계약) | G |
| MV | Rental Contract(임대 계약) | G |
| LZ / LZM | Scheduling Agreement | E |
| OR / TA | Standard Sales Order | C |
VDM 관점에서 보면, I_SalesDocument가 모든 SD 문서를 포괄하는 최상위 View이고, 그 아래 I_SalesContract, I_SalesOrder, I_SalesQuotation, I_SalesSchedulingAgreement 등이 카테고리별로 분리되어 있습니다.
핵심적인 사실은 일반 수주가 계약을 참조하면 계약 한도가 차감(Release)된다는 점입니다. I_SalesOrder의 SalesContract(VBAK-VGBEL 매핑) 필드와 I_SalesOrderItem의 SalesContractItem(VBAP-VGPOS) 필드가 이 참조 관계를 표현합니다.
실전 예제 1단계 — I_SalesContract 기본 조회
먼저 가장 단순한 형태로 I_SalesContract를 조회하는 Consumption View를 만들어봅니다. 아래 예제는 계약 헤더의 주요 필드를 노출하면서, 만료일과 유효기간 시작일을 함께 가져오는 구조입니다.
@AbapCatalog.sqlViewName: 'ZCSALESCONHDR'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Contract Header Browser'
@VDM.viewType: #CONSUMPTION
define view ZC_SalesContractBrowse
as select from I_SalesContract
{
key SalesContract, // VBAK-VBELN
SalesContractType, // VBAK-AUART (예: WK1)
SalesOrganization, // VBAK-VKORG
DistributionChannel, // VBAK-VTWEG
OrganizationDivision, // VBAK-SPART
SoldToParty, // VBAK-KUNNR
PurchaseOrderByCustomer, // VBAK-BSTNK
SalesContractDate, // VBAK-AUDAT
ContractStartDate, // VBAK-GUEBG
ContractEndDate, // VBAK-GUEEN
TotalNetAmount, // VBAK-NETWR
TransactionCurrency, // VBAK-WAERK
OverallSDProcessStatus, // VBAK-GBSTK
CreationDate,
CreatedByUser
}
여기서 주목할 부분은 ContractStartDate와 ContractEndDate입니다. 클래식 VBAK에서는 각각 GUEBG, GUEEN 컬럼이지만, CDS에서는 의미가 명확한 이름으로 노출되어 코드 가독성이 크게 좋아집니다. 또한 SalesContractType 필드를 통해 수량 계약(WK1)과 금액 계약(WK2)을 구분할 수 있습니다.
실전 예제 2단계 — 만료 임박 계약 조회와 오류 처리
실무에서 자주 요구되는 시나리오는 "앞으로 30일 안에 만료될 계약을 영업 조직별로 뽑아 담당자에게 알림을 보내라"는 것입니다. 이를 위해 View에 파라미터를 추가하고 ABAP에서 결과를 소비하는 흐름을 만들어 봅니다.
@AbapCatalog.sqlViewName: 'ZCSALESCONEXP'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Expiring Sales Contracts'
@VDM.viewType: #CONSUMPTION
define view ZC_ExpiringSalesContract
with parameters
p_DaysAhead : abap.int2,
p_SalesOrg : vkorg
as select from I_SalesContract as Contract
association [0..1] to I_Customer as _Customer
on $projection.SoldToParty = _Customer.Customer
{
key Contract.SalesContract,
Contract.SalesContractType,
Contract.SalesOrganization,
Contract.SoldToParty,
_Customer.CustomerName,
Contract.ContractStartDate,
Contract.ContractEndDate,
dats_days_between(
$session.system_date,
Contract.ContractEndDate
) as DaysUntilExpiry,
Contract.TotalNetAmount,
Contract.TransactionCurrency,
_Customer
}
where Contract.SalesOrganization = :p_SalesOrg
and Contract.ContractEndDate between $session.system_date
and dats_add_days(
$session.system_date,
:p_DaysAhead,
'NULL'
)
and Contract.OverallSDProcessStatus <> 'C'
다음은 이 View를 호출하는 ABAP 클래스의 처리 로직입니다.
METHOD fetch_expiring_contracts.
DATA lt_result TYPE STANDARD TABLE OF zc_expiringsalescontract.
TRY.
SELECT *
FROM zc_expiringsalescontract( p_daysahead = @iv_days,
p_salesorg = @iv_sales_org )
INTO TABLE @lt_result
UP TO 500 ROWS.
IF sy-subrc <> 0 OR lt_result IS INITIAL.
MESSAGE i208(00) WITH '만료 예정 계약이 존재하지 않습니다'.
RETURN.
ENDIF.
LOOP AT lt_result ASSIGNING FIELD-SYMBOL(<fs_row>).
IF <fs_row>-daysuntilexpiry <= 7.
mo_notifier->send_urgent(
iv_contract = <fs_row>-salescontract
iv_customer = <fs_row>-customername ).
ENDIF.
ENDLOOP.
rt_contracts = lt_result.
CATCH cx_sy_open_sql_db INTO DATA(lx_db).
RAISE EXCEPTION TYPE zcx_contract_query_failed
EXPORTING previous = lx_db.
ENDTRY.
ENDMETHOD.
실전 예제 3단계 — 계약-오더 참조 분석 View
"이 계약을 통해 실제로 얼마나 인출(Release)되었는가?"를 분석하려면 I_SalesContract와 I_SalesOrder를 조인해야 합니다. 다음은 계약별 누적 인출 금액과 잔여 한도를 계산하는 분석용 View입니다.
@AbapCatalog.sqlViewName: 'ZCCONTRACTREL'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Contract Release Analytics'
@VDM.viewType: #COMPOSITE
@Analytics.dataCategory: #FACT
define view ZC_SalesContractRelease
as select from I_SalesContract as Header
left outer join I_SalesOrderItem as ReleasedItem
on Header.SalesContract = ReleasedItem.SalesContract
{
key Header.SalesContract,
Header.SalesContractType,
Header.SoldToParty,
Header.ContractStartDate,
Header.ContractEndDate,
Header.TotalNetAmount as ContractAmount,
@DefaultAggregation: #SUM
ReleasedItem.NetAmount as ReleasedAmount,
@DefaultAggregation: #SUM
cast( Header.TotalNetAmount - coalesce(ReleasedItem.NetAmount, 0)
as abap.curr( 15, 2 ) ) as RemainingAmount,
Header.TransactionCurrency
}
group by
Header.SalesContract,
Header.SalesContractType,
Header.SoldToParty,
Header.ContractStartDate,
Header.ContractEndDate,
Header.TotalNetAmount,
Header.TransactionCurrency
프로덕션 환경에서는 다음과 같은 DCL 파일을 함께 배포해 권한 체크를 강제합니다.
@MappingRole: true
define role ZC_SalesContractRelease_Role {
grant select on ZC_SalesContractRelease
where (SalesOrganization) =
aspect pfcg_auth(V_VBAK_VKO, VKORG, ACTVT = '03');
}
현장에서 자주 마주치는 함정과 해결법
Q1. I_SalesContract에서 특정 계약이 보이지 않습니다.
가장 흔한 원인은 권한입니다. @AccessControl.authorizationCheck: #CHECK가 활성화된 상태에서 V_VBAK_VKO, V_VBAK_AAT 등 SD 권한 객체가 없는 사용자가 호출하면 결과가 필터링됩니다. 디버그 시 #NOT_REQUIRED로 임시 변경하는 것은 운영 코드에선 금물입니다.
Q2. Sales Contract와 Scheduling Agreement를 한 View에서 동시에 다뤄도 되나요?
가능은 하지만 권장되지 않습니다. 두 문서 유형은 비즈니스 의미가 다르고 VBTYP(G vs E)도 구분되어 있어, SAP는 I_SalesContract와 I_SalesSchedulingAgreement를 분리해 제공합니다. 통합 분석이 필요하면 상위 I_SalesDocument를 사용하거나 UNION으로 묶는 것이 일반적입니다.
Q3. I_SalesOrder.SalesContract 필드는 항상 채워지나요?
아닙니다. 이 필드는 오더 생성 시 계약을 명시적으로 참조한 경우에만 채워집니다. VA01에서 'Create with Reference' 기능으로 계약을 선택했거나, BAPI_SALESORDER_CREATEFROMDAT2에서 참조 문서를 지정한 경우입니다. "계약을 통해 인출되지 않은 일반 오더"는 이 필드가 공란이라는 점을 쿼리 설계에 반영해야 합니다.
Q4. ContractEndDate가 빈 날짜로 나오는 경우가 있어요.
계약 유형에 따라 유효기간이 필수가 아닌 경우 빈 날짜가 들어갑니다. dats_days_between 같은 날짜 함수에 빈 값이 들어가면 예외가 발생할 수 있으므로 where ContractEndDate is not initial 또는 CASE 식으로 가드해야 합니다.
이어서 살펴볼 주제
I_SalesContract를 충분히 이해했다면, 다음 단계로는 RAP 기반의 'Managed Sales Contract' 비즈니스 객체를 살펴보길 권합니다. 또한 Embedded Analytics 관점에서는 I_SalesContractItem과 I_PricingElement를 결합한 가격 분석 View, Fiori 앱 'Manage Sales Contracts'의 OData 백엔드인 C_SalesContractManageProc도 좋은 학습 자료입니다.
댓글 0
아직 댓글이 없습니다.