2

AI Powered Invoice Management with SAP RAP and ABAP on Cloud – Part 2

 11 months ago
source link: https://blogs.sap.com/2023/06/16/ai-powered-invoice-management-with-sap-rap-and-abap-on-cloud-part-2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
June 16, 2023 8 minute read

AI Powered Invoice Management with SAP RAP and ABAP on Cloud – Part 2

All Blogs in this Series –

AI Powered Invoice Management with SAP RAP and ABAP on Cloud

AI Powered Invoice Management with SAP RAP and ABAP on Cloud – Part 1

AI Powered Invoice Management with SAP RAP and ABAP on Cloud – Part 2

AI Powered Invoice Management with SAP RAP and ABAP on Cloud – Part 3

This is the continuation of the main blog. Please go through it for better understanding.

If you are coming from the reference from the initial blog. Congrats you might have already done the ABAP RAP developments. Lets jump to the point –

We need to create a Invoice Process class. Where we will modularize the functionality for API Calls and also use latest Class methods available to call external apis.

CLASS zsaba_inv_process DEFINITION
 PUBLIC
 FINAL
 CREATE PUBLIC .
 PUBLIC SECTION.
 TYPES: inv_doc TYPE c LENGTH 10.
 INTERFACES: if_oo_adt_classrun.
 CLASS-DATA: out TYPE REF TO if_oo_adt_classrun_out.
 TYPES: BEGIN OF ty_post_document_body,
 fileName TYPE string,
 format TYPE string,
 rawData TYPE string,
 END OF ty_post_document_body.
 TYPES: BEGIN OF ty_line,
 material TYPE string,
 description TYPE string,
 quantity TYPE string,
 amount TYPE string,
 unitprice TYPE string,
 END OF ty_line.
 TYPES: tt_items TYPE TABLE OF ty_line.
 TYPES: BEGIN OF ty_extracted,
 receivercontact TYPE string,
 grossamount TYPE string,
 comments TYPE string,
 invoicedate TYPE string,
 refpurchaseorder TYPE string,
 END OF ty_extracted.
 CLASS-METHODS: analyze_doc 
 IMPORTING iv_inv_doc TYPE inv_doc 
 EXPORTING et_items TYPE tt_items 
 es_header TYPE ty_extracted,
 analyze_doc_dummy 
 IMPORTING iv_inv_doc TYPE inv_doc 
 EXPORTING et_items TYPE tt_items 
 es_header TYPE ty_extracted,
 create_client 
 IMPORTING url TYPE string 
 RETURNING VALUE(result) TYPE REF TO if_web_http_client 
 RAISING cx_static_check.
 PROTECTED SECTION.
 PRIVATE SECTION.
 CONSTANTS:
 base_url TYPE string VALUE 'https://inv_<INV_RAP> 
_rap.cfapps.XXXXXXXX.hana.ondemand.com/upload', "Prd-BASF Account
 content_type TYPE string VALUE 'Content-type',
 txt_content TYPE string VALUE 'plain/txt',
 json_content TYPE string VALUE 'application/json; charset=UTF-8'.
ENDCLASS.
CLASS zsaba_inv_process IMPLEMENTATION.
 METHOD if_oo_adt_classrun~main.
 me->out = out.
 analyze_doc( iv_inv_doc = '0002001321' ).
 ENDMETHOD.
 METHOD analyze_doc_dummy.
 et_items = VALUE #( ( amount = '200' description = 'Test' material = 
'1002' quantity = '10' unitprice = '10' )
 ( amount = '2000' description = '2nd Material'
material = '1003' quantity = '100' unitprice = '20' ) ).
 es_header = VALUE #( comments = 'Test' grossamount = '2200' invoicedate
= '2023.04.27' receivercontact = '8013865207' ).
 ENDMETHOD.
 METHOD create_client.
 DATA(dest) = cl_http_destination_provider=>create_by_url( url ).
 result = cl_web_http_client_manager=>create_by_http_destination( dest ).
 ENDMETHOD.
 METHOD analyze_doc.
 DATA: post_document_body TYPE ty_post_document_body.
 " Create the body..
 SELECT SINGLE attachment, mimetype, filename 
 FROM zinvtable_sab 
 WHERE invoice = @iv_inv_doc 
 INTO @DATA(ls_pdf_data).
 TRY.
 post_document_body = VALUE #( fileName = ls_pdf_data-filename 
 format = 'jpg'
rawData = ls_pdf_data-attachment ).
 DATA(json_post) = xco_cp_json=>data->from_abap( post_document_body
)->apply(
 VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) 
)->to_string( ).
 DATA(url) = |{ base_url }|.
 DATA(client) = create_client( url ).
 DATA(req) = client->get_http_request( ).
 req->set_text( json_post ).
 req->set_header_field( i_name = content_type i_value = json_content
).
 DATA(result) = client->execute( if_web_http_client=>post )-
>get_text( ).
 DATA: ls_extracted_head TYPE ty_extracted.
 DATA: lo_data TYPE REF TO data,
 lo_head TYPE REF TO data,
 lo_item TYPE REF TO data,
 lo_items TYPE REF TO data.
 FIELD-SYMBOLS: <lfs_data> TYPE any,
 <lfs_header> TYPE any,
 <lfs_items> TYPE any.
 CALL METHOD /ui2/cl_json=>deserialize 
 EXPORTING
 json = result 
 pretty_name = /ui2/cl_json=>pretty_mode-user 
 assoc_arrays = abap_true 
 CHANGING
 data = lo_data.
 ASSIGN lo_data->* TO <lfs_data>.
 ASSIGN COMPONENT 'HEADER' OF STRUCTURE <lfs_data> TO <lfs_header>.
 ASSIGN COMPONENT 'ITEMS' OF STRUCTURE <lfs_data> TO <lfs_items>.
 IF <lfs_header> IS ASSIGNED.
 lo_head = <lfs_header>.
 ASSIGN lo_head->* TO FIELD-SYMBOL(<lfs_structure>).
 IF <lfs_structure> IS ASSIGNED.
 ASSIGN COMPONENT 'COMMENTS' OF STRUCTURE <lfs_structure> TO 
FIELD-SYMBOL(<lfs_any>).
 IF <lfs_any> IS ASSIGNED.
 es_header-comments = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'GROSSAMOUNT' OF STRUCTURE <lfs_structure> TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 es_header-grossamount = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'INVOICEDATE' OF STRUCTURE <lfs_structure> TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 es_header-invoicedate = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'RECEIVERCONTACT' OF STRUCTURE <lfs_structure>
TO <lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 es_header-receivercontact = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'REFPURCHASEORDER' OF STRUCTURE <lfs_structure>
TO <lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 es_header-refpurchaseorder = <lfs_any>->*.
 ENDIF.
 ENDIF.
 ENDIF.
 DATA: ls_item TYPE ty_line.
 IF <lfs_items> IS ASSIGNED.
 lo_items = <lfs_items>.
 ASSIGN lo_items->* TO FIELD-SYMBOL(<lfs_table>).
 LOOP AT <lfs_table> ASSIGNING FIELD-SYMBOL(<lfs_line>).
 ASSIGN COMPONENT 'AMOUNT' OF STRUCTURE <lfs_line>->* TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 ls_item-amount = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'MATERIAL' OF STRUCTURE <lfs_line>->* TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 ls_item-material = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'DESCRIPTION' OF STRUCTURE <lfs_line>->* TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 ls_item-description = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'QUANTITY' OF STRUCTURE <lfs_line>->* TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 ls_item-quantity = <lfs_any>->*.
 ENDIF.
 ASSIGN COMPONENT 'UNITPRICE' OF STRUCTURE <lfs_line>->* TO 
<lfs_any>.
 IF <lfs_any> IS ASSIGNED.
 ls_item-unitprice = <lfs_any>->*.
 ENDIF.
 APPEND ls_item TO et_items.
 ENDLOOP.
 ENDIF.
 CATCH cx_root INTO DATA(err).
 ENDTRY.
 ENDMETHOD.
ENDCLASS.

This Code is having reference to call external APIs and JSON Formatter codes in ABAP.

Using the same aspects, you can create your method for sent_for_payment.

SAP Document Information Extraction Service Configuration

Enable Document Information extraction services

Document-Information-Extraction-Service-1.jpg

Create service key for ‘Document Information Extraction Trial’.

Document-Information-Extraction-Service-2.jpg

SAP BTP NodeJS Express Server and CF Deployments( API for AI enabled Invoice Processes )

Create a basic npm server by using the below package file.

{ 
 "name": "inv_image_scan", 
 "version": "1.0.0", 
 "description": "", 
 "main": "index.js", 
 "scripts": { 
 "start": "node index.js", 
 "test": "echo \"Error: no test specified\" && exit 1" 
 }, 
 "author": "Sabarna Chatterjee", 
 "license": "ISC", 
 "dependencies": { 
 "body-parser": "^1.20.2", 
 "btoa-atob": "^0.1.2", 
 "express": "^4.18.2", 
 "express-fileupload": "^1.4.0", 
 "image-to-pdf": "^2.0.0", 
 "nodemon": "^2.0.20", 
 "sap-cf-axios": "^0.4.8" 
 } 
}

Now create Index.js

var express = require('express'); 
var bodyParser = require('body-parser'); 
var app = express(); 
var PORT = 8080; 
var lib = require('./lib'); 
// app.use(express.json()); 
app.use(bodyParser.json({limit: '500mb'})); 
app.use(bodyParser.urlencoded({limit: '500mb', extended: true})); 
app.post('/upload', async function (req, res) { 
 console.log('Upload Triggered') 
 await lib.upload(req, res); 
}); 
app.get('/', async function (req, res) { 
 await lib.base(req, res); 
}); 
app.listen(PORT, function (err) { 
 if (err) console.log(err); 
 console.log("Server listening on PORT", PORT); 
});

Now implement your code block in lib.js

const axios = require('axios') 
var FormData = require('form-data'); 
var fs = require('fs'); 
const { resolve } = require('path'); 
var inv_ocr_json = require('./inv_ocr_config.json'); 
var inv_ocr_str = JSON.stringify( inv_ocr_json ) 
function loadSecret() { 
 return require('./.secret.doc_extract_inv.json') // The json file with the service key. 
} 
const _getDocAPIToken = async function(){ 
 var secret_json = loadSecret(); 
 var configToken = { 
 'headers': { 
 'Authorization': 'Basic ' + Buffer.from(secret_json.uaa.clientid + ':' + 
secret_json.uaa.clientsecret).toString('base64') 
 } 
 } 
 var oauthURL = secret_json.uaa.url + '/oauth/token?grant_type=client_credentials'; 
 const token = await _getToken(oauthURL, configToken); 
 return token; 
} 
const _document = async function (configPost) { 
 console.log('Posting Started'); 
 return new Promise((resolve, reject) => { 
 axios(configPost) 
 .then(function (response) { 
 resolve(response.data); 
 console.log('I am here'); 
 }) 
 .catch(function (error) { 
 console.log('Error Happened'); 
 console.log(error); 
 }) 
 }); 
} 
const _execDocPost = async function(filename) { 
 var secret_json = loadSecret(); 
 var token = await _getDocAPIToken() 
 var data = new FormData(); 
 data.append('file', fs.createReadStream(filename)); //'./invoice_2001321.pdf')); 
 data.append('options', inv_ocr_str ); 
 var configPost = { 
 'method': 'post', 
 'url': secret_json.endpoints.backend.url + secret_json.swagger + 'document/jobs', 
 'headers': { 
 'Authorization': 'Bearer ' + token 
 }, 
 'data': data 
 } 
 
 var postedData = await _document(configPost) 
 // console.log(postedData); 
 var response = { 
 "id": postedData.id, 
 "token": token 
 } 
 return response; 
} 
const _execDocGetLoop = async function(id,token) { 
 const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 
 
 for(var i=0;i<30;i++){ 
 console.log('Loop Iteration -> ', i) 
 var secret_json = loadSecret(); 
 var configGet = { 
 'method': 'get', 
 'url': secret_json.endpoints.backend.url + secret_json.swagger + 'document/jobs/' + id, 
 'headers': { 
 'Authorization': 'Bearer ' + token 
 } 
 } 
 var result = await new Promise((resolve, reject) => { axios(configGet) 
 .then(function (response) { 
 resolve(response.data) 
 
 }) 
 .catch(function (error) { 
 console.log(error); 
 }) }); 
 if( result.status === "DONE"){ 
 return result 
 } 
 else{ 
 await delay(1000) 
 } 
 } 
 return result 
} 
const _getToken = async function (url, header) { 
 return new Promise((resolve, reject) => { 
 axios.get(url, header) 
 .then(response => { 
 resolve(response.data.access_token) 
 }) 
 .catch(function (error) { 
 if (error.response) { 
 // Request made and server responded 
 console.log(error.response.data); 
 console.log(error.response.status); 
 console.log(error.response.headers); 
 } else if (error.request) { 
 // The request was made but no response was received 
 console.log(error.request); 
 } else { 
 // Something happened in setting up the request that triggered an Error 
 console.log('Error', error.message); 
 } 
 }) 
 }) 
} 
const _fileSave = async function fileSave(fileName,format,rawData){ 
 console.log('my-filename---->', fileName); 
 require("fs").writeFile(fileName, rawData, 'hex', function(err) { 
 console.log(err); 
 return false; 
 }); 
 return true; 
} 
module.exports = { 
 base: async function base(req, res) { 
 res.send(JSON.stringify({ 'uri': '/upload' })); 
 }, 
 upload: async function base(req, res) { 
 var data = JSON.stringify(req.body); 
 var date = new Date(); 
 function pad2(n) { return n < 10 ? '0' + n : n } 
 var ts = date.getFullYear().toString() + pad2(date.getMonth() + 1) + pad2( date.getDate()) + pad2( 
date.getHours() ) + pad2( date.getMinutes() ) + pad2( date.getSeconds() ) 
 var docData = {} 
 res.setHeader("Content-Type", "application/json"); 
 
 
 var jsondata = JSON.parse(data); 
 console.log('my-filename---->', jsondata.filename); 
 try{ 
 if( await _fileSave(jsondata.filename + ts + '.' + 
jsondata.format,jsondata.format,jsondata.rawdata)) 
 { 
 var response = await _execDocPost(jsondata.filename + ts + '.' + jsondata.format); 
 docData = await _execDocGetLoop(response.id, response.token); 
 console.log(docData.extraction.lineItems); 
 var lineitem = { 
 'material': '', 
 'description': '', 
 'quantity': '', 
 'amount': '', 
 'unitprice': '', 
 } 
 var docDataFinal = { 
 'header': { 'receivercontact': '', 'grossamount': '', 'invoicedate': '', 'comments': '' }, 
 'items' : [] 
 } 
 docData.extraction.headerFields.forEach(element => { 
 switch (element.name){ 
 case 'receiverContact': 
 docDataFinal.header.receivercontact = element.rawValue; 
 break; 
 case 'grossAmount': 
 docDataFinal.header.grossamount = element.rawValue; 
 break; 
 case 'documentDate': 
 docDataFinal.header.invoicedate = element.rawValue; 
 break; 
 case 'paymentTerms': 
 docDataFinal.header.comments = element.rawValue; 
 break; 
 } 
 }); 
 docData.extraction.lineItems.forEach( item =>{ 
 lineitem = {}; 
 item.forEach( column =>{ 
 switch(column.name){ 
 case 'description': 
 lineitem.description = column.rawValue; 
 break; 
 case 'netAmount': 
 lineitem.amount = column.rawValue; 
 break; 
 case 'quantity': 
 lineitem.quantity = column.rawValue; 
 break; 
 case 'unitPrice': 
 lineitem.unitprice = column.rawValue; 
 break; 
 case 'materialNumber': 
 lineitem.material = column.rawValue; 
 break; 
 } 
 }) 
 if(!lineitem.material){ 
 lineitem.material = '' 
 } 
 docDataFinal.items.push(lineitem); 
 }); 
 // docData.receivercontact = 
 console.log(docDataFinal); 
 res.send(JSON.stringify(docDataFinal)); 
 } 
 } 
 catch (error){ 
 docData = {'process_status': "error"}; 
 res.send(JSON.stringify({ 'message': docData })); 
 } 
 } 
} 

Next is pushing your development to SAP Cloud Foundry. Use a manifest.yml and use cf8 push to deploy your code to SAP Cloud Foundry. Also you can deploy the code in Kyma Runtime environment.

--- 
applications: 
- name: inv_<INV_RAP>_rap 
 random-route: false 
 path: ./ 
 memory: 256M 
 buildpack: nodejs_buildpack

If you have followed the blog till now, you must have a better understanding now, how this program is skilled enough to process the Raw file and convert it into structured data using AI and OCR powered APIs.

  1. Configuration of Document Extraction Service in SAP BTP
  2. Creation of a NodeJS based wrapper interface and host it in SAP BTP
  3. Calling of the NodeJS API from the Custom Invoice Process Class – zsaba_inv_process->analyze_doc_dummy

Please visit Part 3.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK