Skip to Content
Technical Articles
Author's profile photo Naoto Sakai

SAP Build Process Automationで自動採番の番号を使用する その1

 SAP Build Process Automationでフォームを使用したワークフローを作成する際、そのフォームに独自の番号を付与したいということがあります。申請時に番号が採番されて、その後はその番号をキーとして処理が行われるという形です。

SAP Build Process Automationではワークフローインスタンスが開始する際はインスタンスIDというGUID値が発行され、内部的にはその番号でワークフロー中の情報を管理していますが、GUIDというのは”dfa3c3b6-2bd2-8845-7ac8-84c98e66000b”のような値です。確かに人間から見たらこの番号で申請などを管理するというのは厳しいでしょう。例えばAAAA00001のような、人間にもわかりやすい番号を使用したいというのは理解できます。

残念ながらこの独自の形式の番号を発行する機能は現在のSAP Build Process Automationには存在しません。したがって外部の機能として採番システムを開発し、それをActionあるいはAutomationでフローと連携させる必要があります。

注意:ここで現在のSAP Build Process Automationではどうしてもできないパターンについて解説します。

現在のSAP Build Process Automationではフォームをトリガーとするワークフローで、「トリガーとなるフォームを表示したときに既に採番した番号が表示されている」というものは開発することができません。現実の例にすると、「紙のフォームで最初から一意の番号が印刷されているものを作成する」ということはできません。フォームに記入する際には番号の部分は空白で、提出すると番号が採番されるというタイプのフローであれば開発することができます。

トリガーフォームを表示した際に最初から番号が表示されているタイプのものを作成したいのであれば、トリガーとなるフォームはSAP Build Appsなどで作成し、APIトリガーとしてワークフローを設計し、フォームからAPIを呼び出す形にする必要があります。

今回作成する自動採番の番号形式

今回は以下のような形式の番号を自動採番させることにします。

<前置詞>-<年号4桁>-<ゼロ埋め8桁の数字>

例)AAA-2023-00000001

前置詞の部分は採番を行う際に指定します。年号は採番時の年を使用します。ゼロ埋め8桁の数字の部分は1から開始し、同じ前置詞&年の組み合わせの番号の取得要求があった場合、カウントアップする形式とすることにします。

必要となるもの

  • 番号を管理するデータベース
    • 今回はHANA Cloudを使用します。CAPでHDIにテーブルを作成します。
  • 上記データベースと連携し、採番を行い新しい番号を返すRESTAPI
    • 今回はCloud Foundry環境でNode.jsで書いたアプリをデプロイして実行します。

*これらを動作・デプロイする先のBTPアカウントは検証・テストのためBTPのFree TierやTrialアカウントでもOKです。また、SAP Build Process AutomationがSubscrideされているBTPアカウントと別の環境でも結構です。

 

それでは開発方法の解説に入ります。

 

1.データベースの作成

今回は以下のようなテーブルを作成します。

// schema.cds
namespace autonum;
using { managed } from '@sap/cds/common';

entity Datatable: managed {
    key prefix : String(40); //前置詞
    key year : String(4); //年
    key number : Int64; //カウントアップする番号
    instanceid : String(40); //ワークフローのインスタンスID
    workflowinfo : String(255); //なにか情報格納用。後で抽出しやすいようにワークフロー名など。(今回未使用)
    workflowuser : String(255); //番号を取得したユーザーのID
    generatedate : Timestamp; //発行日時
}
//number-service.cds
using autonum as autonum from '../db/schema';

service NumberTable {
    entity Autonum as projection on autonum.Datatable;
}
//ODataとして公開するようにしましたが、これは今回必須ではありません。(今後のブログで使うかもしれません。)

 

CAPでHANA Cloudにテーブルを作成する方法は

https://blogs.sap.com/2021/05/26/cap%E3%81%A7sap-hana-cloud%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92odata%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A8%E3%81%97%E3%81%A6%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B/

のブログなどを参考にしていただければと思います。

2.RESTAPIの作成

上記のHDI環境・テーブルと接続し、番号を自動採番するREST APIを構築します。

Node.jsで記述し、Could Foundry Runtimeで動作させることにします。

var express = require("express");
var router = express();
var vcap_services = JSON.parse(process.env.VCAP_SERVICES);
console.log(vcap_services);

// load HANA Client
var hana = require("@sap/hana-client");
const { response } = require("express");
var conn = hana.createConnection();

// HANA Connection Settings
var conn_params = {
  serverNode:
    vcap_services.hana[0].credentials.host +
    ":" +
    vcap_services.hana[0].credentials.port,
  encrypt: true,
  schema: vcap_services.hana[0].credentials.schema,
  sslValidateCertificate: false,
  uid: vcap_services.hana[0].credentials.user,
  pwd: vcap_services.hana[0].credentials.password,
  pooling: true,
  maxPoolSize: 50,
};

router.use(express.json());

// REST API : /getnextvalue
// Method : POST
// INPUT : prefix / String
//         instanceid / String
//         workflowuser / String
// RETURN : JSON
// RETURN EXAMPLE:
//{
//  "newnumber": "1234556-2023-00000004"
//}

router.post("/getnextvalue", (req, res) => {
  var newvalue = 0;

  // Connect to HANA
  conn.connect(conn_params, function (err) {
    if (err) {
      if (err.code != -20004) {
        console.log("DB Error: DB Connection --- ", err);
        var msg = [{ msg: "DB Error: DB Connection" }];
        res.json({ searchResult: msg });
        return;
      }
    }
    //set schema
    var sql0 = "SET SCHEMA " + vcap_services.hana[0].credentials.schema;
    var stmt0 = conn.prepare(sql0);
    try {
      stmt0.exec();
    } catch (err) {
      console.log("set schema error.");
      console.log("SQL=", sql0);
      console.log("DB Error: SQL Execution --- ", err);
    }
    stmt0.drop();

    // get current year
    var currentDate = new Date();
    var currentYear = currentDate.getFullYear();

    //  prefix and year are exists in the data ?
    var sql1 =
      "SELECT COUNT(*) AS COUNT FROM AUTONUM_DATATABLE WHERE PREFIX = ? AND YEAR = ?";
    var stmt1 = conn.prepare(sql1);
    try {
      result = stmt1.exec([req.body.prefix, String(currentYear)]);
    } catch (err) {
      console.log("Get prefix count error.");
      console.log("SQL=", sql1);
      console.log("DB Error: SQL Execution --- ", err);
    }
    stmt1.drop();
    if (Number(result[0].count) == 0) {
      // New prefix and year
      // Insert first value for this prefix and year.
      var sql2 =
        'INSERT INTO AUTONUM_DATATABLE(PREFIX, YEAR, "NUMBER", INSTANCEID ,WORKFLOWUSER ,GENERATEDATE) VALUES(?, ?, 1, ?, ?, CURRENT_TIMESTAMP);';
      var stmt2 = conn.prepare(sql2);
      try {
        stmt2.exec([
          req.body.prefix,
          String(currentYear),
          req.body.instanceid,
          req.body.workflowuser,
        ]);
      } catch (err) {
        console.log("Insert new prefix error.");
        console.log("SQL=", sql2);
        console.log("DB Error: SQL Execution --- ", err);
      }
      stmt2.drop();
      newvalue = 1;
    } else {
      // Exists
      // get current max value
      var sql3 =
        'SELECT MAX("NUMBER") AS MAXNUM FROM AUTONUM_DATATABLE WHERE PREFIX = ? AND YEAR = ?';
      var stmt3 = conn.prepare(sql3);
      try {
        result3 = stmt3.exec([req.body.prefix, String(currentYear)]);
      } catch (err) {
        console.log("Get prefix max error.");
        console.log("SQL=", sql3);
        console.log("DB Error: SQL Execution --- ", err);
      }
      stmt3.drop();
      // MAXNUM+1
      newvalue = Number(result3[0].MAXNUM) + 1;
      // insert new value
      var sql4 =
        'INSERT INTO AUTONUM_DATATABLE(PREFIX, YEAR, "NUMBER", INSTANCEID ,WORKFLOWUSER ,GENERATEDATE) VALUES(?, ?, ?, ?, ?, CURRENT_TIMESTAMP);';
      var stmt4 = conn.prepare(sql4);
      try {
        stmt4.exec([
          req.body.prefix,
          String(currentYear),
          newvalue,
          req.body.instanceid,
          req.body.workflowuser,
        ]);
      } catch (err) {
        console.log("Insert new number error.");
        console.log("SQL=", sql4);
        console.log("DB Error: SQL Execution --- ", err);
      }
      stmt4.drop();
    }

    //Return new number as json
    var newnumber = {};
    newnumber.newnumber =
      req.body.prefix +
      "-" +
      String(currentYear) +
      "-" +
      (Array(8).join("0") + newvalue).slice(-8);
    res.json(newnumber);
    console.log("Return: " + String(newnumber.newnumber));
  });
});
router.listen(process.env.PORT || 4000);

ソースコード中にも記述していますが、このAPIは/getnextvalueで呼び出し、引数としてはPOST形式でprefix,instanceid,workflowuserの3つが必須となります。

prefixは自動採番に使う前置詞です。instanceidはワークフローのインスタンスID、workflowuserはワークフローの実行ユーザー名です。instanceidとworkflowuserがあれば実際のワークフローとあとから紐づけも出来るでしょう。

Cloud Foundry環境にデプロイし、1で作成されたHDIのインスタンスをバインドさせてください。これでNode.jsのアプリがデータベースに接続し、動作します。

3.OpenAPI仕様書の作成

このRestAPIはSAP Build Process Automation上ではActionで呼び出します。Actionで呼び出すためにはOpen APIの仕様書が必要です。

Rest API仕様からOpenAPI仕様書を作成しました。以下のようになります。

{
  "openapi": "3.0.3",
  "info": {
    "title": "Get Number API Sample",
    "version": "1.0.0",
    "description": "Sample API for POST method to get the next value"
  },
  "paths": {
    "/getnextvalue": {
      "post": {
        "summary": "Get the next value",
        "operationId": "getNextValue",
        "requestBody": {
          "description": "Input",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InputSample"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReturnSample"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "InputSample": {
        "type": "object",
        "properties": {
          "prefix": {
            "type": "string"
          },
          "instanceid": {
            "type": "string"
          },
          "workflowuser": {
            "type": "string"
          }
        },
        "required": ["prefix", "instanceid", "workflowuser"],
        "example": {
          "prefix": "ABC",
          "instanceid": "dedewd-dede-dede-dede",
          "workflowuser": "test@test.com"
        }
      },
      "ReturnSample": {
        "type": "object",
        "properties": {
          "newnumber": {
            "type": "string"
          }
        },
        "required": ["newnumber"],
        "example": {
          "newnumber": "ABC-2023-00000001"
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": ["error"]
      }
    }
  }
}

*余談ですが、AutomationからこのAPIを呼び出すこともできます。その場合はOpenAPI仕様書を書く必要はありません。(しかしDesktop Agentが必要になります。)

 

 

次回は3を使用してActionを作成し、ワークフロープロセスに組み込むところを解説しようと思います。

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.