Skip to Content

A customer, partner or colleague of yours has a nice idea about new web or mobile application, but before starting a bigger project the concept should be quickly validated, fully integrated with SAP backend system. Or, you are just interested for experimenting and deeper understanding how web applications, REST and HTTP work, integrated with SAP ABAP systems.

Using Python RFC connector and Python web framework of choice, you can build the model for the web application prototyping, expose JSON resources or assemble a mini web app, in interactive, fun and convenient way.

Let take the CRM Customer Account as example and suppose two resources with JSON representation are needed, a single customer and the list of customers matching the first name, last name or customer id.

ABAP

Application resources on ABAP side can be implemented by encapsulation of existing BAPIs or by writing new Remote Function Modules (RFM), often called RFCs. There are practically no restrictions how to implement those RFMs, except you should try to keep RFM signatures simple and consistent, because it simplifies the modeling of Python objects for web and makes the communication among Web and ABAP experts easier.

You should also think about performance and returning as much as possible information on errors, like record not found etc., but that depends on available time, budget and goals of your prototype.

Suppose for the purpose of this exercise, two RFMs are implemented and tested: ZFRS_CUSTOMER_ACCOUNT_GET and ZFRS_CUSTOMER_ACCOUNT_GETLIST. From SE37 screenshots below (click to enlarge), you see the consistent naming of these two RFCs because they will soon become methods of Python web object Customer.

Customer GET

Customer GET, returns a single instance of CRM Customer Account

CustomerGet.png

Customer GETLIST

Customer GETLIST, returns a list CRM Customer Accounts, based on search criteria.

CustomerGetList.png

Web

Python objects for web/mobile prototyping, you can create using the editor or IDE of choice, manually or automatically, by generating Python model from ABAP RFM metadata.

In any case, the source code below shows the final result. Pyton is expressive language, which means that using less code lines you can accomplish more, which means the code (if well written) is easy to read and maintain. Experience from projects shows that ABAP experts even with no experience in Python, have no troubles understanding and writing web/mobile prototype models in Python.

Looking into source code below, it is easy to recognize BAPI_RETURN structure and get a clue what the model is doing. In this particular case, the model is even more complicated than the how-to example should be, because of the additional handling of loyalty data, required in a real project.

From the perspective of the web developer (HTML5, JavaScript …), the object Customer looks like a quite standard object for web development, so the web developer can utilize all skills and weapons he/she regularly uses for web development. The difference here, in regard to other web applications, is that this object does not use ORM or another database for the persistence but the leading and only system for the persistence is SAP backend.

The get and search methods of the Customer object are implemented in ABAP system and persistent changes are done there. Although implemented in Python, the majority (ideally 100%) of the business logic remains in ABAP system, exposed via RFMs, as Python object methods. Web/mobile developers normally do not need to care about that, just use those objects like in other web/mobile application.

customermodel.py


#
# CRM Customer GET and GETLIST model
#
import base64
from datetime import datetime
from model    import RfcModel, EnhancedRfcModel
from fields   import Char, Date, Struct, Many, NumC, Decimal, String, Boolean, Time, Exception_, Single
class ETReturn(RfcModel):
    """Error Messages"""
    id_        = Char(20,  name="ID"        ) # Message Class   
    type_      = Char(1,   name="TYPE"      ) # Message type: S Success, E Error, W Warning, I Info, A Abort
    number     = NumC(3,   name="NUMBER"    ) # Message Number
    system     = Char(10,  name="SYSTEM"    ) # Logical system from which message originates
    parameter  = Char(32,  name="PARAMETER" ) # Parameter Name
    field      = Char(30,  name="FIELD"     ) # Field in parameter
    message    = Char(220, name="MESSAGE"   ) # Message Text
    message_v1 = Char(50,  name="MESSAGE_V1") # Message Variable
    message_v2 = Char(50,  name="MESSAGE_V2") # Message Variable
    message_v3 = Char(50,  name="MESSAGE_V3") # Message Variable
    message_v4 = Char(50,  name="MESSAGE_V4") # Message Variable
    log_no     = Char(20,  name="LOG_NO"    ) # Application log: log number
    log_msg_no = NumC(6,   name="LOG_MSG_NO") # Application log: Internal message serial number
    row        = NumC(4,   name="ROW"       ) # Lines in parameter
    def __str__(self):
        return "{message} (Msg {number})".format(**self.__dict__)
class CustomerDetail(RfcModel):
    """Customer Account details"""
    id      = Char(10,  name="KUNNR"    ) # Customer Number
    name    = Char(35,  name="NAME1"    ) # Name 1
    city    = Char(35,  name="ORT01"    ) # City
    zip     = Char(10,  name="PSTLZ"    ) # Postal Code
    street  = Char(35,  name="STRAS"    ) # House number and street
    email   = Char(241, name="SMTP_ADDR") # E-Mail Address
    card_id = Char(25,  name="CARD_ID"  ) # External Card ID
    def __repr__(self):
        return "<CustomerDetail> " + str(self.__dict__)
class CustomerDetails(RfcModel):
    """Customer Account details"""
    id      = Char(10,  name="KUNNR"    )  # Customer Number
    name    = Char(35,  name="NAME1"    )  # Name 1
    city    = Char(35,  name="ORT01"    )  # City
    zip     = Char(10,  name="PSTLZ"    )  # Postal Code
    street  = Char(35,  name="STRAS"    )  # House number and street
    email   = Char(241, name="SMTP_ADDR")  # E-Mail Address
    card_id = Char(25,  name="CARD_ID"  )  # External Card ID
    def __repr__(self):
        return "<CustomerDetails> " + str(self.__dict__)
class CustomerLoyalty(RfcModel):
    """Customer loyalty program"""
    name        = Char   (40, name="PROGRAM"   )  # Text, 40 Characters Long
    tires       = Char   (40, name="TIRES"     )  # Text, 40 Characters Long
    status      = Char   (40, name="STATUS"    )  # Text, 40 Characters Long
    start_date  = Date   (    name="START_DATE")  # Date
    end_date    = Date   (    name="END_DATE"  )  # Date
    balance     = Decimal(6,  name="POINT_BAL" )  # Number of Points
    description = Char   (40, name="PROG_DESCR")  # Language-Dependent Short Text
    def __repr__(self):
        return "<CustomerLoyalty> " + str(self.__dict__)
class Customer(EnhancedRfcModel):
    """ZFRS_CUSTOMER_GET Model """
    id        = Char(10, name="IV_CUSTOMER"              )  # Business Partner ID
    card_id   = Char(25, name="IV_CARD_ID", optional=True)  # External Card ID
    firstname = Char(40, name="IV_NAME_FIRST"            )  # First Name of Business Partner (Person)
    lastname  = Char(40, name="IV_NAME_LAST"             )  # Last Name of Business Partner (Person)
    raw_image      = String(0,               name="EV_FOTO",            optional=True)  # Foto
    url            = Char  (452,             name="EV_URI",             optional=True)  # SAP URL
    details        = Struct(CustomerDetail,  name="ES_CUSTOMER_DETAIL", optional=True)  # Account data
    search_results = Many  (CustomerDetails, name="ET_CUSTOMER_DATA",   optional=True)
    loyalties      = Many  (CustomerLoyalty, name="ET_LOY_DATA",        optional=True)  # Loyalty data
    es_return      = Struct(ETReturn,        name="ES_RETURN"                        )  # Return Parameter
    def __json__(self):
        """One customer details as JSON (Customer GET)"""
        # not found or another error
        if self.es_return.type_ in ['A','E']:
            result={}
            for key in ('id_', 'type_', 'number', 'message'):
                result[key] = getattr(self.es_return, key)
            return result
        # customer account found
        result = {
            'image': len(self.image) if self.image else None,
            'image_type': 'image/jpeg' if self.image else None,
            'url': self.url
        }
        # customer details
        for key in ('id', 'name', 'city', 'zip', 'street', 'email', 'card_id'):
            result[key] = getattr(self.details, key)
        # loyalty infos
        level = ''
        if self.loyalties:
            level = self.loyalties[0].tires # TODO, for now just take the first level found
        result['loyalty'] = {
            'points': 0,
            'level': level,
            'sets': []
        }
        for loy in self.loyalties:
            loyalty = {
                'start_date': datetime.strftime(loy.start_date, '%d.%m.%Y'),
                'end_date': datetime.strftime(loy.end_date, '%d.%m.%Y'),
            }
            for key in ('name', 'description', 'tires', 'status', 'balance'):
                loyalty[key] = getattr(loy, key)
            result['loyalty']['sets'].append(loyalty)
            if loy.balance:
                result['loyalty']['points'] += loy.balance
        return result
    def __json_list__(self):
        """Customer list search result as JSON (Customer GETLIST)"""
        # error check
        if self.es_return.type_ in ['A','E']:
            result={}
            for key in ('id_', 'type_', 'number', 'message'):
                result[key] = getattr(self.es_return, key)
            return result
        result = []
        for customer in self.search_results:
            result.append(
                {key: getattr(customer, key) for key in (
                    'id', 'name', 'city', 'zip', 'street', 'email', 'card_id')})
        return {'customers': result}
    @classmethod
    def get(cls, conn, id='', card_id='', firstname='', lastname='', **params):
        result = conn.call("ZFRS_CUSTOMER_GET",
                           **cls.rfc_fields(id=id,
                                            card_id=card_id,
                                            firstname=firstname,
                                            lastname=lastname,
                                            **params))
        return cls.init_from_rfc(result)
    @classmethod
    def search(cls, conn, id='', firstname='', lastname='', **params):
        result = conn.call("ZFRS_CUSTOMER_GETLIST",
                           **cls.rfc_fields(id=id,
                                            firstname=firstname,
                                            lastname=lastname,
                                            **params))
        return cls.init_from_rfc(result)
    @property
    def image(self):
        return base64.b64encode(self.raw_image.rstrip("\x00"))

App

App can be many different things, a couple of data interfaces, JSON or OData, for the consumption by native, usually mobile app, or the standard web application with HTML templates, rendering, static content, media etc., for the mobile, tablet or desktop device.

This example shows the simplest case, a JSON representation of SAP application resources, basically RESTful data interfaces. However, you can check the web framework documentation (in this example Flask) and easily extend the app with HTML templates, rendering, error handling and so on.

Using a console like IPython (see videos) can be very helpful for  rapid experimenting, prototyping and learning more about REST and HTTP, in a very convenient way, fully integrated with SAP system. It can also help you quickly accomplish an integrated HTML5/REST demo or proof of concept.

Support for OData is not yet implemented, can be provided if needed.

Similar SAP Research prototype example you may find here and more about prototypes done this way you may find here .

app.py


from sapnwrfc2 import Connection
import customermodel
# Connect to SAP CRM
params = {'user'      : 'sb-test',
          'passwd'    : 'sb-test',
          'ashost'    : '10.117.24.153',
          'saprouter' : '/H/203.13.155.17/W/xjkb3d/H/172.19.137.194/H/',
          'sysnr'     : '00',
          'client'    : '765',
          'lang'      : 'EN'}
print 'Connecting ...', params['ashost']
conn = Connection(config={'return_import_params': True}, **params)
# Serve web app
print 'Serve ...'
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def index():
    response = """
       <HTML><HEAD><TITLE>Customer</TITLE></HEAD>
       <BODY>
       <h1>Usage examples</h1>
       <p></p>
       <p>/customer/710160</p>
       <p></p>
<p>/customerlist?lastname=Crimson</p>
       </BODY></HTML>
       """
    return response
@app.route("/customer/<id>")
def get(id=None):
    customer_by_id = Customer.get(conn, id=id)
    response = str(customer_by_id.__json__())
    return response
  
@app.route("/customerlist")
def getlist(lastname=None):
    for header in request.headers:
        print header
    id = request.args.get('id')
    if id == None:
        id = ''
 
    lastname = request.args.get('lastname')
    if lastname == None:
        lastname = ''
    firstname = request.args.get('firstname')
    if firstname == None:
        firstname = ''
    customerlist = Customer.search(conn, id=id, firstname=firstname, lastname=lastname)
    response = str(customerlist.__json_list__())
    return response
if __name__ == "__main__":
    app.run(debug=True, port=5000)

Even without  modeling, direct calling ABAP function modules from  Python requires only couple if lines of code, as shown on screenshot below (click to enlarge). Compact, clean and highly readable code is a great advantage for rapid prototyping, also for the software maintenance and iterative development, often following the prototyping.

ABAP-Python.png

To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

Leave a Reply