Leggere una tabella SAP da Python mediante RFC

Python è comodo per elaborare dati, ma questi dati bisogna procurarseli in qualche modo. Se i dati sono contenuti in una tabella su un sistema SAP S/4, il metodo più diretto è chiamare un modulo RFC.


Requisiti

Per chiamare un modulo RFC da Python è necessario installare nel sistema la Netweawer RFC SDK. Questa libreria è scaricabile da qui, ma il download richiede un S-USER valido (no free trial) e che sia abilitato al download di risorse. Se il vostro S-USER non è abilitato (come accade di solito) dovete chiedere per piacere agli IT della vostra organizzazione.

Si installa e si prende nota della cartella di installazione.

Bisogna poi aggiungere la cartella "lib" della nostra installazione nel path di sistema. Su Windows, si apre il pannello di controllo, voce "Sistema e Sicurezza", voce "Sistema", "Impostazioni di sistema avanzate". Nel tab "Avanzate" si preme il pulsante "Variabili d'ambiente...". Nella parte bassa della finestra (Variabili di Sistema) si fa doppio click su "path", e nella finestra che si apre si aggiunge una riga che punta alla cartella di cui sopra.

Bisogna inoltre installare la libreria Python PYRFC, ad esempio con pip:

pip install pyrfc

A questo punto siamo pronti a cominciare. Usando la libreria pyrfc possiamo chiamare qualunque function module abilitato per la comunicazione RFC. Possiamo crearne uno noi, o possiamo usarne uno standard: nel nostro caso esiste già un modulo che permette di leggere il contenuto di una tabella SAP, e si chiama "RFC_READ_TABLE". Possiamo richiamarlo con questo codice:

from pyrfc import Connection

conn = Connection(
    ashost='xxx.xxx.xxx.xxx', 
    sysnr='00', 
    client='XXX', 
    user='xxxxx', 
    passwd='yyyyy'
)

table='MARA'
delimiter = '|'

response = conn.call(
    "RFC_READ_TABLE", 
    QUERY_TABLE=table, 
    DELIMITER=delimiter,
    FIELDS=[{'FIELDNAME':'MANDT'},{'FIELDNAME':'MATNR'}]
)

conn.close()

Nota: questo FM ha parecchie limitazioni, come ad esempio il fatto che ogni record restituito non può essere più lungo di 512 caratteri. In questo caso, visto che la MARA è una tabella con millemila colonne, abbiamo usato il parametro "FIELDS" per limitare le colonne lette.

A questo punto, nella variabile "response" troviamo un dizionario che contiene, tra le altre, le chiavi DATA e FIELDS.

La chiave FIELDS contiene l'elenco dei campi letti dalla tabella, con il tipo e la posizione all'interno della risposta. Ogni record della risposta, come vedremo dopo, è infatti costruito come stringa in cui i campi sono separati dal separatore specificato al momento della chiamata, e in FIELDS è presente per ciascun campo il carattere di inizio e la lunghezza.

print(response['FIELDS'])

[{'FIELDNAME': 'MANDT',
  'OFFSET': '000000',
  'LENGTH': '000003',
  'TYPE': 'C',
  'FIELDTEXT': 'Mandante'},
 {'FIELDNAME': 'MATNR',
  'OFFSET': '000004',
  'LENGTH': '000040',
  'TYPE': 'C',
  'FIELDTEXT': 'Codice materiale'}]

Nella chiave "DATA" sono invece presenti le righe lette:

print(response['DATA'])

[{'WA': '100|TG10'},
 {'WA': '100|TG11'},
 {'WA': '100|TG12'},
 {'WA': '100|TG13'},
 {'WA': '100|TG14'},
 {'WA': '100|FG129'},
 {'WA': '100|SM0001'},
 {'WA': '100|NS0002'},
 ....

A questo punto abbiamo in mano tutto quello che ci serve per gestire queste informazioni in Python. Ad esempio, potremmo usare le informazioni relative ai campi e alla loro lunghezza per trasformare le righe in dizionari:

data = []

for orig_record in [record['WA'] for record in response['DATA']]:

  parsed_record = {}
  
  for field in response['FIELDS']:
  
    parsed_record[field['FIELDNAME']] = orig_record[int(field['OFFSET']):int(field['OFFSET'])+int(field['LENGTH'])].strip()
    
    data.append(parsed_record)

Potremmo invece trasformare l'output direttamente in un dataframe Pandas, per facilitare successive analisi. Si può ottenere questo risultato con questo comodo snippet:

import pandas as pd

result = pd.DataFrame(
    [[x.strip() for x in response['DATA'][n]['WA'].split(delimiter)] for n in range(len(response['DATA']))],
    columns=[x['FIELDNAME'] for x in response['FIELDS']]
)

Questo il risultato:

Alla prossima!