第561話:経費精算クラウドを使わない理由(3)

2017年11月13日

業務:経費の申請と精算


クラウド型ワークフローで「経費申請フロー」のシステム化を実現した!(第559話参照)

しかも「多角的なグルーピング」(クラスタリング分析)を実現するための仕組みも導入した!(第560話参照)

経理チームは、「48時間の滞留で自動的に部長決裁されたものとみなす」というルールが適用された経費申請(案件ステータス115~155)に対して、目を光らせているらしい。

課題:監査法人とのデータ共有

しかし、、、情報量は「多ければ多いほど良い」というモノではない。

ワークフローには、『立替金額』や『計上月』といった基本情報だけでなく『立替申請を行ったヒト・時刻』や『支払を証明する書類』あるいは『プロジェクトの名称』や『取引先企業名』といった関連情報まで、様々なデータが含まれている。たしかに社内の人間であれば、様々なデータから「情報」を抽出することは重要なシゴトと言える。

ただ、例えば、会計監査人にしてみれば「審査過程」や「所要時間」などは無用なデータだ。例えば、社長や会長といった経営トップにしてみれば「誰が多くの経費を使ったか」まで見る時間がない。彼らにとっては「多角的な集計フィルタリング」より、必要なデータが簡潔に一覧されている方が重要なのだ。

う~む、ならば報告用の Spreadsheet を「手作り」するか???(悪夢)

[経費精算フロー-Spreadsheet出力]

解決:Spreadsheet へのリアルタイム追記

この業務プロセス定義では、最下流にある「経理チームの承認工程」の直後に『Append Sheet』という自動工程が配置されています。

ここでは、
  • A. 立て替えた日
  • B. 計上月(YYYY-MM)
  • C. 勘定費目分類
  • D. 精算金額
  • E. 立替人所属組織
  • F. 稟議書ID
の6項目が自動的に Google Spreadsheet へ「リアルタイム追記」されます。

会計監査人は Spreadsheet を参照するだけで「全ての審査が完了した経費案件」について効率よく確認することが可能となっています。ワークフロー基盤にログインする必要はありません。

考察:外部連携の意義

ワークフローに流れたデータを「外部システムである Google Spreadsheet」に自動出力する仕組みは、単に「データの見やすさ」や「リアルタイム共有」を実現するだけのものではありません。

つまり「無人で自動追記される」という特性から、ログデータとしての存在意義があると言えます。

具体的には、業務データの隠蔽削除や改ざんといった不正を抑止し、システム障害時には業務データの代替参照先として活用できます。(業務データ・ミラーリング)

PS:書式や背景色などのカスタマイズ

なお『Google Sheets 行追加』は、各企業文化にあわせたカスタマイズ要求が多い自動工程です。

つまり「負の金額は赤色で書く」や「100万円以下は四捨五入する/切り捨てる」や「四半期の区分を追記する」など、業種や事業規模によって基本的なロギングニーズが異なります。より調和のとれた業務データ環境を実現するために、会社全体での共通利用を積極的に進めた方が良いかも知れません。

<自動工程の設定画面>

<自動工程の定義ファイル>
<?xml version="1.0" encoding="UTF-8"?><service-task-definition>

<label>Google Sheets Append Cells</label>
<label locale="ja">Google Sheets 行追加</label>

<summary>Adds new cells after the last row with data in a sheet, inserting new rows into the sheet if necessary.</summary>
<summary locale="ja">シート末尾に1行追加し、その各セルにデータを入力します。必要あれば行領域を拡大します。</summary>

<help-page-url>https://www.questetra.com/tour/m4/m415/addon-googlesheets-appendcells/</help-page-url>
<help-page-url locale="ja">https://www.questetra.com/ja/tour/m4/m415/addon-googlesheets-appendcells/</help-page-url>


<configs>
  <config name="conf_OAuth2" required="true" form-type="TEXTFIELD">
    <label>V: Set OAuth2 Config Name (at [OAuth 2.0 Setting])</label>
    <label locale="ja">V: OAuth2通信許可の設定名 (←[OAuth 2.0 設定]における設定名)</label>
  </config>
  <config name="conf_DataIdW" required="true" form-type="SELECT" select-data-type="STRING_TEXTFIELD|SELECT_SINGLE">
    <label>W: Select STRING/SELECT DATA for Spreadsheet ID (File ID)</label>
    <label locale="ja">W: Spreadsheet ID (ファイルのID)が格納されている文字列型or選択肢型データを選択してください</label>
  </config>
  <config name="conf_DataIdX" required="true" form-type="SELECT" select-data-type="STRING_TEXTFIELD|SELECT_SINGLE|DECIMAL">
    <label>X: Select STRING/SELECT/NUMERIC DATA for SheetGID (Worksheet ID)</label>
    <label locale="ja">X: Sheet GID (WorksheetのID)が格納されている文字列型or選択肢型or数値型データを選択してください</label>
  </config>
  <config name="conf_DataIdY" required="false" form-type="SELECT" select-data-type="STRING_TEXTAREA">
    <label>Y: Select STRING DATA for Access Log (update)</label>
    <label locale="ja">Y: 通信ログが格納される文字列型データを選択してください (更新)</label>
  </config>
  <config name="conf_DataIdA" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>A: Select DATA for Column-A Value of New Row</label>
    <label locale="ja">A: 末尾行のA列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdB" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>B: Select DATA for Column-B Value of New Row</label>
    <label locale="ja">B: 末尾行のB列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdC" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>C: Select DATA for Column-C Value of New Row</label>
    <label locale="ja">C: 末尾行のC列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdD" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>D: Select DATA for Column-D Value of New Row</label>
    <label locale="ja">D: 末尾行のD列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdE" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>E: Select DATA for Column-E Value of New Row</label>
    <label locale="ja">E: 末尾行のE列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdF" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>F: Select DATA for Column-F Value of New Row</label>
    <label locale="ja">F: 末尾行のF列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdG" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>G: Select DATA for Column-G Value of New Row</label>
    <label locale="ja">G: 末尾行のG列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdH" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>H: Select DATA for Column-H Value of New Row</label>
    <label locale="ja">H: 末尾行のH列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdI" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>I: Select DATA for Column-I Value of New Row</label>
    <label locale="ja">I: 末尾行のI列に追加される値が格納されているデータを選択してください</label>
  </config>
  <config name="conf_DataIdJ" required="false" form-type="SELECT" select-data-type="STRING|DECIMAL|DATE|DATETIME|SELECT_SINGLE|QUSER|QGROUP|FILE">
    <label>J: Select DATA for Column-J Value of New Row</label>
    <label locale="ja">J: 末尾行のJ列に追加される値が格納されているデータを選択してください</label>
  </config>
</configs>


<script><![CDATA[
// Google Sheets Append Cells (ver. 20171031)
// (c) 2017, Questetra, Inc. (the MIT License)
// by spreadsheets.batchUpdate [AppendCellsRequest] (not by spreadsheets.values.append)
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#appendcellsrequest

// OAuth2 config sample at [OAuth 2.0 Setting]
// - Authorization Endpoint URL: https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force
// - Token Endpoint URL: https://accounts.google.com/o/oauth2/token
// - Scope: https://www.googleapis.com/auth/spreadsheets
// - Consumer Key: (Get by Google Developers Console)
// - Consumer Secret: (Get by Google Developers Console)


//// == Config Retrieving / 工程コンフィグの参照 ==
var oauth2 = configs.get( "conf_OAuth2" ) + "";
var dataIdW = configs.get( "conf_DataIdW" ) + "";
var dataIdX = configs.get( "conf_DataIdX" ) + "";
var dataIdY = configs.get( "conf_DataIdY" ) + "";
var dataIdA = configs.get( "conf_DataIdA" ) + "";
var dataIdB = configs.get( "conf_DataIdB" ) + "";
var dataIdC = configs.get( "conf_DataIdC" ) + "";
var dataIdD = configs.get( "conf_DataIdD" ) + "";
var dataIdE = configs.get( "conf_DataIdE" ) + "";
var dataIdF = configs.get( "conf_DataIdF" ) + "";
var dataIdG = configs.get( "conf_DataIdG" ) + "";
var dataIdH = configs.get( "conf_DataIdH" ) + "";
var dataIdI = configs.get( "conf_DataIdI" ) + "";
var dataIdJ = configs.get( "conf_DataIdJ" ) + "";
// convert 'java.lang.String' to 'javascript string'


//// == Data Retrieving / ワークフローデータの参照 ==
var spreadsheetId = "";
if( engine.findDataDefinitionByNumber( dataIdW ).matchDataType( "SELECT_SINGLE" ) ){
    spreadsheetId = engine.findDataByNumber( dataIdW ).get(0).getValue() + "";
}else{
    spreadsheetId = engine.findDataByNumber( dataIdW ) + "";
}
var sheetId       = "";
if( engine.findDataDefinitionByNumber( dataIdX ).matchDataType( "SELECT_SINGLE" ) ){
    sheetId       = engine.findDataByNumber( dataIdX ).get(0).getValue() + "";
}else if( engine.findDataDefinitionByNumber( dataIdX ).matchDataType( "DECIMAL" ) ){
    sheetId       = engine.findDataByNumber( dataIdX ) + "";
    sheetId       = sheetId.replace(/,/g,"").replace(/\./g,""); // Remove dots and commas
}else{
    sheetId       = engine.findDataByNumber( dataIdX ) + "";
}


//// == Calculating / 演算 ==
// preparing for API Request
var apiRequest = httpClient.begin(); // HttpRequestWrapper
// com.questetra.bpms.core.event.scripttask.HttpClientWrapper

// Request HEADER (OAuth2 Token, HTTP Basic Auth, etc)
var token  = httpClient.getOAuth2Token( oauth2 );
apiRequest = apiRequest.bearer( token );

// Request PATH (https://example.com/abc/def/)
var apiUri = "https://sheets.googleapis.com/v4/spreadsheets/";
    apiUri += spreadsheetId + ":batchUpdate";

// Request QUERY (?a=b)
// (no set)

// Request BODY (JSON, Form Parameters, etc)
function cellDataObject( dataId ){
  var dataObj = {};
  dataObj.userEnteredValue = {};
  if( dataId === "" ){
    dataObj.userEnteredValue.stringValue = "";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "SELECT_SINGLE" ) ){
    dataObj.userEnteredValue.stringValue = engine.findDataByNumber( dataId ).get(0).getDisplay() + "";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "FILE" ) ){
    dataObj.userEnteredValue.stringValue = engine.findDataByNumber( dataId ).get(0).getName() + "";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "QUSER" ) ){
    dataObj.userEnteredValue.stringValue = engine.findDataByNumber( dataId ).getName() + "";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "QGROUP" ) ){
    dataObj.userEnteredValue.stringValue = engine.findDataByNumber( dataId ).getName() + "";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "DATE" ) ){
    var serialDays = ( engine.findDataByNumber( dataId ).getTime() + engine.getTimeZoneOffsetInMinutes() * 60000 ) / 86400000 + 25569;
    dataObj.userEnteredValue.numberValue = serialDays;
    dataObj.userEnteredFormat = {};
    dataObj.userEnteredFormat.numberFormat = {};
    dataObj.userEnteredFormat.numberFormat.type = "DATE";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "DATETIME" ) ){
    var serialDays2 = ( engine.findDataByNumber( dataId ).getTime() + engine.getTimeZoneOffsetInMinutes() * 60000 ) / 86400000 + 25569;
    dataObj.userEnteredValue.numberValue = serialDays2;
    dataObj.userEnteredFormat = {};
    dataObj.userEnteredFormat.numberFormat = {};
    dataObj.userEnteredFormat.numberFormat.type = "DATE_TIME";
  }else if( engine.findDataDefinitionByNumber( dataId ).matchDataType( "DECIMAL" ) ){
    dataObj.userEnteredValue.numberValue = engine.findDataByNumber( dataId ) - 0;
  }else{ // for "STRING" 
    dataObj.userEnteredValue.stringValue = engine.findDataByNumber( dataId ) + "";
  }
  return dataObj;
}
var requestObj = {};
  requestObj.requests = [];
  requestObj.requests[0] = {};
  requestObj.requests[0].appendCells = {};
  requestObj.requests[0].appendCells.sheetId = sheetId;
  requestObj.requests[0].appendCells.fields = "*";
  requestObj.requests[0].appendCells.rows = [];
  requestObj.requests[0].appendCells.rows[0] = {};
  requestObj.requests[0].appendCells.rows[0].values = [];

  requestObj.requests[0].appendCells.rows[0].values[0] = cellDataObject( dataIdA );
  requestObj.requests[0].appendCells.rows[0].values[1] = cellDataObject( dataIdB );
  requestObj.requests[0].appendCells.rows[0].values[2] = cellDataObject( dataIdC );
  requestObj.requests[0].appendCells.rows[0].values[3] = cellDataObject( dataIdD );
  requestObj.requests[0].appendCells.rows[0].values[4] = cellDataObject( dataIdE );
  requestObj.requests[0].appendCells.rows[0].values[5] = cellDataObject( dataIdF );
  requestObj.requests[0].appendCells.rows[0].values[6] = cellDataObject( dataIdG );
  requestObj.requests[0].appendCells.rows[0].values[7] = cellDataObject( dataIdH );
  requestObj.requests[0].appendCells.rows[0].values[8] = cellDataObject( dataIdI );
  requestObj.requests[0].appendCells.rows[0].values[9] = cellDataObject( dataIdJ );

apiRequest = apiRequest.body( JSON.stringify( requestObj ), "application/json" );

// Access to the API (POST, GET, PUT, etc)
var response = apiRequest.post( apiUri ); // HttpResponseWrapper
var httpStatus = response.getStatusCode() + "";
var accessLog = "---POST request--- " + httpStatus + "\n";
accessLog += response.getResponseAsString() + "\n";
//var responseObj = JSON.parse( response.getResponseAsString() );

// Retrieve Properties from Response-JSON
// (no action)

// Error Handling - https://stripe.com/docs/api#errors
// (no action)


//// == Data Updating / ワークフローデータへの代入 ==
if ( dataIdY !== "" ){ engine.setDataByNumber( dataIdY, accessLog ); }
]]></script>


<icon>
(省略)
</icon>

</service-task-definition>

<経費精算フロー-Spreadsheet出力:「4.補正/却下」画面>

<データ項目一覧画面>


[雛形ダウンロード (無料)]
<類似プロセス>
≪関連記事≫

[英文記事 (English Entry) ]

0 件のコメント :

コメントを投稿