天天看點

S/4 HANA中的MATDOC和MATDOC_EXTRACT

最近做了銷售和物料管理方面的一些需求,是以對S/4中的MM的資料模型有了一定的了解。目前網絡已經有一些介紹物料憑證表MATDOC的文章,内容好像不是很詳細,另外也沒發現介紹庫存變更資料表MATDOC_EXTRACT的文章。我打算把自己知道的一點東西寫下來分享。水準所限,如有不正确的地方,請務必評論指正。

本文将介紹2點内容,

  • 傳統的物料憑證表MKPF,MSEG和新表MATDOC間的關系
  • 傳統的庫存表如MARD, MCHB, MSKU, MSLB, MARDH, MCHBH, MSKUH, MSLBH等和新表MATDOC以及MATDOC_EXTRACT的關系

此外,一些廣為流傳的文章(比如SAP S/4 Hana On-premise Edition 1511做了哪些簡化、SAP S/4HANA 1610後勤之變)中提出S4的SAP中的庫存資料會從MATDOC表中實時計算,本文将證明這種說法是不準确的:至少對于MARD等标準表而言,其中的資料來自于對MATDOC_EXTRACT的彙總,而非MATDOC。

本文連結:https://www.cnblogs.com/hhelibeb/p/9361187.html

原創内容,轉載請注明

replacement object

網絡上的個别文章提到S4中不再存在MKPF和MARD等表,資料都來自于CDS。這種說法不是很準确,事實上MKPF等表依然是存在的,隻不過其中的資料被CDS entity替換掉了,這種CDS entity就是所謂replacement object.。MKPF和MSEG中的資料全部來自MATDOC,MARD中的全部資料來自于MATDOC_EXTRACT。而類似于FAGLFLEXT這種視圖,它們的表則是真的不存在了。SE11中檢視的話可以看出差別:

S/4 HANA中的MATDOC和MATDOC_EXTRACT
S/4 HANA中的MATDOC和MATDOC_EXTRACT

這種差別看似無所謂,實際上對開發人員而言是有差別。使用Open SQL對包含replacement object的表的通路通常會被重定向到replacement object,但在某些情況下存在例外(具體參看文檔)。這種名實不符的情況有時會導緻意外的bug,可以參見筆者之前的踩坑經曆。

物料憑證

MKPF, MSEG和MATDOC之間的關系相對簡單,目前我試着把接到的需求裡的MKPF和MSEG都轉換成MATDOC,開發效率有一些提升,程式的工作情況良好。

MKPF表的replacement object是NSDM_E_MKPF,這個CDS entity是直接從MATDOC中取數的,條件為record_type = 'MDOC'和header_counter = 1,代碼如下,可以重點看where語句部分,

(注:字首NSDM的意思可能是New, Simplified Data Model)

@AbapCatalog.sqlViewName:      'NSDM_V_MKPF'
@EndUserText.label:            'MKPF Compatibility View'
@DataAging.noAgingRestriction: 'false'

@ObjectModel.usageType.sizeCategory: 'L'
@ObjectModel.usageType.serviceQuality: #C
@ObjectModel.usageType.dataClass: #TRANSACTIONAL
@ClientHandling.algorithm: #AUTOMATED
@AccessControl.authorizationCheck: #NOT_ALLOWED
@AbapCatalog.viewEnhancementCategory: #PROJECTION_LIST
define view nsdm_e_MKPF  as
select 
key mandt, 
key mblnr, 
key mjahr,
vgart,
blart,
blaum,
bldat,
budat,
cpudt,
cputm,
aedat,
usnam,
tcode,
xblnr,
bktxt,
frath,
frbnr,
wever,
xabln,
awsys,
bla2d,
tcode2,
bfwms,
exnum,
spe_budat_uhr,
spe_budat_zone,
le_vbeln,
spe_logsys,
spe_mdnum_ewm,
gts_cusref_no,
fls_rsto,
msr_active
/*
Fields which do belong to APPENDs on MSEG will be provided via EXTEND VIEW to this view.
Hence, Industries having also APPENDs can use the same technique 
and just because the alphabetical sequence of APPENDs is also valid for EXTEND VIEW the compability will be ensured.
The name of the IS EXTEND VIEW has to be the same as the APPEND. Rules are:
ABAP Catalog Objekt: <Append>_V
DDL Source <Append>_DDL
CDS View = <Append>_E
The alphabetical order of the EXTEND VIEW in the final proxy view is according to the name of sqlViewAppendName
and not accroding to DDL source name or the EXTEND VIEW name! Hence there is still a restriction on 16 chars!
*/
/*
DDL sources of known extensions so far:
JVMKPF_DDL
MILL_MKPF_DDL
*/
from matdoc
where record_type = 'MDOC'
  and header_counter = 1        

MSEG和它差不多,replacement object是NSDM_E_MSEG,同樣從MATDOC中擷取資料,條件則隻有record_type = 'MDOC',代碼略過不貼。

使用MATDOC代替MKPF和MSEG,可以減少不必要的關聯次數。另外,也有stock_qty和consumption_qty等友善計算的字段,利用它們可以減少相關邏輯處理代碼,提高開發速度。

庫存

一些文章提到S4中的庫存資料是從MATDOC中實時彙總得出的,的确,MATDOC中包含計算庫存所需的全部資料。但是這個表的資料是物料憑證級别的,随着時間的進行,SAP系統内會積累大量的物料憑證,從這樣海量的資料中彙總庫存,似乎有些奢侈。例如FICO子產品中的ACDOCA表,就是從年初餘額開始彙總,來擷取餘額的(見前文:S/4 HANA中的ACDOCT和FAGLFLEXT),而不是彙總曆史上的每一筆憑證。

通過對各個庫存表的replacement object的閱讀追蹤,我發現實際上庫存資料來自于另一個表:MATDOC_EXTRACT。這是一個實在的透明表,并不存在replacement object:

S/4 HANA中的MATDOC和MATDOC_EXTRACT

NOTE 2246602 中提到了這個表:

為了減輕(從MATDOC中進行聚合運算時因資料量大導緻的性能下降的)影響,SAP引入了第二個表MATDOC_EXTRACT,從中計算實際庫存資料。MATDOC_EXTRACT有一小部分來自MATDOC的字段,在物料憑證過賬時,每當有資料被插入到MATDOC中,也會有資料被插入MATDOC_EXTRACT中。MATDOC_EXTRACT裡的資料會被定期壓縮,比如,假設有以下兩條資料:
  • Date:20.11.2015; Material:4711; Plant:0001; StorageLocation:0001; StockIdentifier:01; SpecialStock:blank; Quantity: 10,0; Unit: PC
  • Date:23.11.2015; Material:4711; Plant:0001; StorageLocation:0001; StockIdentifier:01; SpecialStock:blank; Quantity: -3,0; Unit: PC
會被壓縮為:
  • Date:PrecompactingDate; Material:4711; Plant:0001; StorageLocation:0001; StockIdentifier:01; SpecialStock:blank; Quantity: 7,0; Unit: PC

新的壓縮後的資料會被插入MATDOC_EXTRACT,兩條被壓縮的資料會被從中删除。通過這種做法,表

MATDOC_EXTRACT中的總記錄數和用于聚合的記錄數都會定期減少,是以,對庫存的查詢(相比從MATDOC中進行)會有更好的性能。

該過程被稱為“預壓縮”(precompacting),預設作為期末結算(period end closing)的一部分運作。如果是以導緻期末結算性能下降,也可以自行安排定期背景作業。

以下通過MARD的replacement object,NSDM_E_MARD為例,我們來觀察下它是如何從MATDOC_EXTRACT中擷取資料的。源代碼:

@AbapCatalog.sqlViewName:      'NSDM_V_MARD'
@EndUserText.label:            'MARD Compatibility View'
@DataAging.noAgingRestriction: 'false'

@ObjectModel.usageType.sizeCategory: 'L'
@ObjectModel.usageType.serviceQuality: #C
@ObjectModel.usageType.dataClass: #TRANSACTIONAL
@ClientHandling.algorithm: #AUTOMATED
@AccessControl.authorizationCheck: #NOT_ALLOWED
@AbapCatalog.viewEnhancementCategory: #PROJECTION_LIST

define view nsdm_e_mard
  as select from    mard             as t
    left outer join nsdm_e_mard_diff as m on  t.mandt = m.mandt
                                          and t.matnr = m.matnr
                                          and t.werks = m.werks
                                          and t.lgort = m.lgort
  association [0..1] to E_Productstoragelocation as _ActiveExtension on  $projection.matnr = _ActiveExtension.Product
                                                                     and $projection.werks = _ActiveExtension.Plant
                                                                     and $projection.lgort = _ActiveExtension.StorageLocation
{
  key t.mandt,
  key t.matnr,
  key t.werks,
  key t.lgort,

      t.pstat,
      t.lvorm,

      case
      when m.gjper = '0000000'
      or m.gjper is null then
      t.lfgja
      else
      cast(substring(m.gjper, 1, 4) as abap.numc(4))
      end                                                         as lfgja,

      case
      when m.gjper = '0000000'
      or m.gjper is null then
      t.lfmon
      else
      cast(substring(m.gjper, 6, 2) as abap.numc(2))
      end                                                         as lfmon,

      t.sperr,
      case when m.labst is null then 0 else m.labst end           as labst,      // vvv note 2249780
      case when m.umlme is null then 0 else m.umlme end           as umlme,
      case when m.insme is null then 0 else m.insme end           as insme,
      case when m.einme is null then 0 else m.einme end           as einme,
      case when m.speme is null then 0 else m.speme end           as speme,
      case when m.retme is null then 0 else m.retme end           as retme,
      case when m.vmlab is null then 0 else m.vmlab end           as vmlab,
      case when m.vmuml is null then 0 else m.vmuml end           as vmuml,
      case when m.vmins is null then 0 else m.vmins end           as vmins,
      case when m.vmein is null then 0 else m.vmein end           as vmein,
      case when m.vmspe is null then 0 else m.vmspe end           as vmspe,
      case when m.vmret is null then 0 else m.vmret end           as vmret,      // ^^^ note 2249780
      t.kzill,
      t.kzilq,
      t.kzile,
      t.kzils,
      t.kzvll,
      t.kzvlq,
      t.kzvle,
      t.kzvls,
      t.diskz,
      t.lsobs,
      t.lminb,
      t.lbstf,
      t.herkl,
      t.exppg,
      t.exver,
      t.lgpbe,
      case when m.klabs is null then 0 else m.klabs end           as klabs,      // vvv note 2249780
      case when m.kinsm is null then 0 else m.kinsm end           as kinsm,
      case when m.keinm is null then 0 else m.keinm end           as keinm,
      case when m.kspem is null then 0 else m.kspem end           as kspem,      // ^^^ note 2249780
      t.dlinl,
      t.prctl,
      t.ersda,
      case when m.vklab is null then 0 else m.vklab end           as vklab,      // note 2249780
      case when m.vkuml is null then 0 else m.vkuml end           as vkuml,      // note 2249780
      t.lwmkb,
      t.bskrf,
      'X'                                                         as mdrue,
      t.mdjin,
      --field added for extensibility
      t.dummy_stl_incl_eew_ps,
      --Fields added for EA-RETAIL(FASHION)
      t.fsh_salloc_qty_s,
      case when m./cwm/labst is null then 0 else m./cwm/labst end as /cwm/labst, // note 2413597
      case when m./cwm/insme is null then 0 else m./cwm/insme end as /cwm/insme, // note 2413597
      case when m./cwm/einme is null then 0 else m./cwm/einme end as /cwm/einme, // note 2413597
      case when m./cwm/speme is null then 0 else m./cwm/speme end as /cwm/speme, // note 2413597
      case when m./cwm/retme is null then 0 else m./cwm/retme end as /cwm/retme, // note 2413597
      case when m./cwm/umlme is null then 0 else m./cwm/umlme end as /cwm/umlme, // note 2413597
      case when m./cwm/klabs is null then 0 else m./cwm/klabs end as /cwm/klabs, // note 2413597
      case when m./cwm/kinsm is null then 0 else m./cwm/kinsm end as /cwm/kinsm, // note 2413597
      case when m./cwm/keinm is null then 0 else m./cwm/keinm end as /cwm/keinm, // note 2413597
      case when m./cwm/kspem is null then 0 else m./cwm/kspem end as /cwm/kspem, // note 2413597
      case when m./cwm/vmlab is null then 0 else m./cwm/vmlab end as /cwm/vmlab, // note 2413597
      case when m./cwm/vmins is null then 0 else m./cwm/vmins end as /cwm/vmins, // note 2413597
      case when m./cwm/vmein is null then 0 else m./cwm/vmein end as /cwm/vmein, // note 2413597
      case when m./cwm/vmspe is null then 0 else m./cwm/vmspe end as /cwm/vmspe, // note 2413597
      case when m./cwm/vmret is null then 0 else m./cwm/vmret end as /cwm/vmret, // note 2413597
      case when m./cwm/vmuml is null then 0 else m./cwm/vmuml end as /cwm/vmuml // note 2413597

}      

可以看到,labst等度量,完全來自于與透明表MARD左連接配接的CDS entity:nsdm_e_mard_diff,觀察這個entity,可以看到,labst等entity其實原本都是stock_qty,隻是根據lbbsa字段和sobkz字段的值的不同,被分成不同的度量列。

@AbapCatalog.sqlViewName:      'NSDM_V_MARD_DIFF'
@EndUserText.label:            'MARD Compatibility View: Differentiation'
@DataAging.noAgingRestriction: 'false'

@ObjectModel.usageType.sizeCategory: 'L'
@ObjectModel.usageType.serviceQuality: #C
@ObjectModel.usageType.dataClass: #TRANSACTIONAL
@ClientHandling.algorithm: #AUTOMATED
@AccessControl.authorizationCheck: #NOT_ALLOWED
@AbapCatalog.viewEnhancementCategory: #PROJECTION_LIST
define view nsdm_e_mard_diff as
select from nsdm_e_mard_agg 
{
key mandt, 
key matnr, 
key werks, 
key lgort,

max(gjper_max) as gjper,

sum(case lbbsa when '01' then case sobkz when '' then stock_qty else 0 end else 0 end) as labst,
sum(case lbbsa when '04' then case sobkz when '' then stock_qty else 0 end else 0 end) as umlme,
sum(case lbbsa when '02' then case sobkz when '' then stock_qty else 0 end else 0 end) as insme,
sum(case lbbsa when '08' then case sobkz when '' then stock_qty else 0 end else 0 end) as einme,       
sum(case lbbsa when '07' then case sobkz when '' then stock_qty else 0 end else 0 end) as speme,
sum(case lbbsa when '03' then case sobkz when '' then stock_qty else 0 end else 0 end) as retme,

cast(0 as abap.quan(13, 3)) as vmlab,
cast(0 as abap.quan(13, 3)) as vmuml, 
cast(0 as abap.quan(13, 3)) as vmins,
cast(0 as abap.quan(13, 3)) as vmein,
cast(0 as abap.quan(13, 3)) as vmspe,
cast(0 as abap.quan(13, 3)) as vmret,

sum(case lbbsa when '01' then case sobkz when 'K' then stock_qty else 0 end else 0 end) as klabs,           
sum(case lbbsa when '02' then case sobkz when 'K' then stock_qty else 0 end else 0 end) as kinsm,
sum(case lbbsa when '08' then case sobkz when 'K' then stock_qty else 0 end else 0 end) as keinm,
sum(case lbbsa when '07' then case sobkz when 'K' then stock_qty else 0 end else 0 end) as kspem,  

sum(case lbbsa when '01' then case sobkz when '' then stock_vkwrt else 0 end else 0 end) as vklab,
sum(case lbbsa when '04' then case sobkz when '' then stock_vkwrt else 0 end else 0 end) as vkuml,

sum(case lbbsa when '01' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/labst,
sum(case lbbsa when '04' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/umlme,
sum(case lbbsa when '02' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/insme,
sum(case lbbsa when '08' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/einme,       
sum(case lbbsa when '07' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/speme,
sum(case lbbsa when '03' then case sobkz when '' then /cwm/stock_qty else 0 end else 0 end) as /cwm/retme,
cast(0 as abap.quan(13, 3)) as /cwm/vmlab,
cast(0 as abap.quan(13, 3)) as /cwm/vmuml, 
cast(0 as abap.quan(13, 3)) as /cwm/vmins,
cast(0 as abap.quan(13, 3)) as /cwm/vmein,
cast(0 as abap.quan(13, 3)) as /cwm/vmspe,
cast(0 as abap.quan(13, 3)) as /cwm/vmret,
sum(case lbbsa when '01' then case sobkz when 'K' then /cwm/stock_qty else 0 end else 0 end) as /cwm/klabs,           
sum(case lbbsa when '02' then case sobkz when 'K' then /cwm/stock_qty else 0 end else 0 end) as /cwm/kinsm,
sum(case lbbsa when '08' then case sobkz when 'K' then /cwm/stock_qty else 0 end else 0 end) as /cwm/keinm,
sum(case lbbsa when '07' then case sobkz when 'K' then /cwm/stock_qty else 0 end else 0 end) as /cwm/kspem,

'X' as mdrue 

}
group by mandt, matnr, werks, lgort          
        

這個lbbsa字段是什麼意思呢?它是貨物移動的庫存類型(庫存辨別符),域是NSDM_LBBSA,包含固定值(第一行為空值):

        與庫存不相關

01    未限制使用的庫存

02    在檢庫存

03    退貨

04    庫存調撥(存儲位置)

05    庫存調撥(工廠)

06    在途存貨

07    已當機的庫存

08    限制使用的存貨

09    配套空包裝

10    評估收貨已當機庫存

sobkz為特殊庫存辨別。也就是說這是一種行轉列的變換。舉個例子,代碼sum(case lbbsa when '01' then case sobkz when '' then stock_qty else 0 end else 0 end) as labst, 代表着如果庫存類型是“未限制使用的庫存”,特殊庫存辨別為“K”,則它屬于MARD中的LABST,即“未限制使用的估價的庫存”

繼續檢視下一層entity:nsdm_e_mard_agg ,

@AbapCatalog.sqlViewName:      'NSDM_V_MARD_AGG'
@EndUserText.label:            'MARD Compatibility View: Aggregation'
@DataAging.noAgingRestriction: 'false'

@ObjectModel.usageType.sizeCategory: 'L'
@ObjectModel.usageType.serviceQuality: #C
@ObjectModel.usageType.dataClass: #TRANSACTIONAL
@ClientHandling.algorithm: #AUTOMATED
@AccessControl.authorizationCheck: #NOT_ALLOWED
@AbapCatalog.viewEnhancementCategory: #PROJECTION_LIST

define view nsdm_e_mard_agg as
select from matdoc_extract 
{
key mandt, 
key matbf     as matnr, 
key werks, 
key lgort_sid as lgort,

lbbsa_sid     as lbbsa,
sobkz,

sum(stock_qty_l2)      as stock_qty,
sum(stock_vkwrt_l2)    as stock_vkwrt,
max(gjper_curr_per)    as gjper_max,
sum(/cwm/stock_qty_l2) as /cwm/stock_qty
}
where stock_ind_l2 = ''
  and (((sobkz = '' or sobkz = 'K')
         and (lbbsa_sid = '01' or lbbsa_sid = '02' or lbbsa_sid = '07' or lbbsa_sid = '08'))
   or   (sobkz = ''
         and (lbbsa_sid = '03' or lbbsa_sid = '04')))
 
group by mandt, matbf, werks, lgort_sid, lbbsa_sid, sobkz         
  
        

此時透明表MATDOC_EXTRACT終于現身,上文中的stock_qty即該表中stock_qty_12字段的分組彙總結果。

我們可以看到按照一定的條件從MATDOC_EXTRACT中篩選、分組彙總、轉換後得到的,即是MARD中的資料,當然這一切都是實時計算而得到的。其它庫存表中的資料來源也是一樣,計算方法相似。有興趣的話可以檢視相關CDS entity的代碼。

從不同的庫存表中查詢彙總資料做統一處理,會導緻代碼冗長,維護和擴充極為不變。相比之下,使用MATDOC_EXTRACT來進行統一的查詢處理,無論是開發效率還是程式的運作效率,都可能要好不少。

參考閱讀:2238690 - S/4HANA MM-IM migration by SUM

     2206980 - Material Inventory Managment: change of data model in S/4HANA

     2246602 - Precompacting scheduling in case system performance gets slowed down during a posting period

繼續閱讀