2024年4月27日

GASを使って電子取引データファイル名から索引簿を作成するスクリプトを作ってみました。

松山観光港の夕日
松山観光港の夕日



2024年1月1日より電子帳簿保存法による電子取引データの電子保存義務化が実質的に始まりました。

実際には,2022年1月1日から義務化されていたのですが,2023年12月31日まで宥恕措置がありましたので,2023年度までは印刷書面による保存も可能でした。

しかし,2024年度からは電子データとして受発信した電子取引データは,原本のまま保存しなければなりません。

これについては,ほとんどの方が対応を終えられていると存じます。当研究会でも電子帳簿保存法および令和5年6月版電子帳簿保存法一問一答【電子取引関係】に従い,規程を整備して検索しやすいファイル名を付した電子取引データとして保存しています。

今回は,GoogleAppsScript(GAS)学習の一環として,この電子取引データについて索引簿作成スクリプトを作ってみましたので参考にご紹介します。

企業であれば,それなりの経済力があるため如何なる対応も可能でしょうが,小規模事業者では費用も要員も限られるため,小さな工夫ですが参考になれば幸いです。

具体的には,国税庁の電子帳簿保存法一問一答【電子取引関係】令和5年6月版や電子帳簿保存法の関係法令を参考に検討しましたので,調査するのに多くの時間を要しました。

それでは,学習を始めます。




  1.電子帳簿保存法とは


ほとんどの方が既に電子帳簿保存法をご存じですから,簡単におさらいします。

電子帳簿保存法とは,税務関係帳簿書類のデータ保存を可能とする法律で、同法に基づく各種法令の総称です。これらの法律を活用することで経理のデジタル化を図ることができます。

主な関連法令は以下のとおりです。




これらの法令には,電子計算機を使用した国税関係帳簿書類の保存方法が定められていますが,加えて電子取引の取引情報に係る電磁的記録の保存方法も定められている点に注意が必要です。

この電子取引の取引情報の保存方法については,「令和5年6月電子帳簿保存法一問一答【電子取引関係】」で具体的に説明されています。

この解説書の中で,示されている帳簿書類等の各種保存方法が以下の図になります。


帳票・書類等の各種保存方法
帳票・書類等の各種保存方法
【出典:令和5年6月電子帳簿保存法一問一答【電子取引関係】】

この中で,電帳法第7条に示されている電子取引データの保存ファイルが今回のターゲットになります。


  2.電子帳簿保存法(電子取引関係)の内容


この「電子帳簿保存法一問一答【電子取引関係】」では,検索機能を確保するため電子取引データのファイル名に規則性を持って内容を表示することが示されています。

具体的には,「取引年月日_取引先_金額」というファイル名が記載されており,こうすれば,特殊な検索ソフトを使わなくても要件を満たすということです。

仮に,この例を参考に電子取引データのファイルを次のようにしたと仮定します。

ファイル名の命名規則 : 取引年月日_取引先_金額_取引内容.pdf

具体的なファイル名の例

  「20240401_(株)ITリテラシー商事_10000円_電帳法請求書デモ.pdf

      20240410_(株)ITリテラシー商事_20000円_電帳法請求書デモ.pdf

このような請求書が2ファイルあったとして,Googleドライブの適当なフォルダに格納します。

下図はGoogleドライブのマイドライブ下に「電子取引データ保存」というフォルダを作成し,そのフォルダ下に上記2つの請求書ファイルをアップロードしたところです。


電子取引データの格納図
電子取引データのgoogleドライブへの格納図


次に,このファイル名群から索引簿を作るため,下図のような「電子取引データ索引簿」というスプレッドシートをフォルダ内に作ります。

索引簿スプレッドシート
索引簿スプレッドシート図1

このスプレッドシートのファイル名は「電子取引データ索引簿」です。

セル(”F1")に更新日付を設定します。

3行目にヘッダーとして,番号("A3"),取引年月日(”B3"),取引先名(”C3"),金額(”D3"),取引内容(”E3")を設定します。

シート名は「シート1」です。

このスプレッドシートに付随する「コンテナバインドスクリプト」を作成して行きます。

番号(”A3”)は,電子取引データファイル名を当該フォルダから抽出しシートに貼り付けた後,取引先年月日(昇順),取引先名(昇順),金額(昇順)のソートKeyで並べ替えて,1番から順に付与します。

なお,金額は視認性を良くするため,ファイル名には”円”を付けていますが,索引簿では円を取って数字として検索性を良くすることにします。

令和5年6月電子帳簿保存法一問一答【電子取引関係】」では,規則性のあるファイル名を採用した場合,索引簿は必要ないようですが,GoogleAppsScript(GAS)の学習も兼ねて,検索性を高めるための索引簿を作成するスクリプトを作ります。


  3.電子取引データファイル名から索引簿を作成するスクリプト


コンテナバインドスクリプト」とは,スプレッドシート(コンテナ)に付随するスクリプトという意味です。

コーディング方法は,スプレッドシートの「拡張機能タブ」をクリックして,その中の「Apps Script」を選択してエディターを開きます。

スクリプトのFunction名は,仮に「myindex_book_make()」とし,次の順序で作成していきます。

DriveAppクラスとSpreadsheetAppクラスに係るSpreadsheetクラスを取得します。
日付を取得し,西暦年月日に整形してセル(”F1")に出力します。
スプレッドシートの書き出し部分をクリアします。
当該フォルダ下の対象ファイルタイプ(pdf,text,jpeg)を全てコレクションとして抽出します。
コレクションを一つずつ読み出して,アンダーバー(”_”)でファイル名を分解し配列に代入して,その配列毎に該当するスプレッドシートのセルに貼り付けます。金額は”円”を削除して数字としてセルに貼り付けます。
貼り付けが完了したら,取引先年月日(昇順),取引先名(昇順),金額(昇順)のソートKeyで並べ替えを行います。
並び替えが完了したら,順番に番号を付与します。

以上がスクリプトの流れになります。

それでは,一つずつコーディングして行きます。

①【DriveAppクラスとSpreadsheetAppクラスおよびSpreadsheetクラスの取得】

 具体的なコーディングは次のとおりです。


コード

function myindex_book_make() {
 
 const folderId = "*******ここにフォルダーIDを入れる**************"
 const folder = DriveApp.getFolderById(folderId); //Idからフォルダーオブジェクト取得
 //const folder = DriveApp.getRootFolder();       //マイドライブのフォルダーオブジェクト取得
 console.log(folder.getName()); //現在のドライブ
 
 const ssActive = SpreadsheetApp.getActiveSpreadsheet(); //ActiveAppオブジェクト取得
 console.log(ssActive.getName()); //シート名を出力
 const sheet = ssActive.getSheetByName("シート1");         //Spreadsheetクラス取得

 //----- 以下にコーディングを追加して行く -----

}


該当するフォルダは,「マイドライブ」>「電子取引データ保存」ですから,コーディング内の****部分に「電子取引データ保存」フォルダのIdを設定します。

フォルダIDは該当フォルダのURLの「d/」直後の英数字(”_”や”-”を含む)になります。

このフォルダIdを使ってDriveApp.getFolderById()関数により,GoogleドライブフォルダのDriveAppクラスオブジェクトを固定値「folder」に取得します。

また,フォルダがマイドライブの場合は,ルートドライブなのでDriveApp.getRootFolder()関数を利用します。この場合は,フォルダIdはありません。

console.log(folder.getName())はフォルダー名が表示され,実行時のデバックで利用します。

次に,スプレッドシートのActiveAppオブジェクトを「SpreadsheetApp.getActiveSpreadsheet()」関数で固定値「ssActive」に取得します。

この固定値「ssActive」を使って,現在開いているシート1のSpreadsheetクラスオブジェクトを「ssActive.getSheetByName("シート1")」関数で固定値「sheet」に取得します。

これにより,シート1上の操作が出来るようになります。


②【日付を取得し,西暦年月日に整形してセル(”F1")に出力します。】

次に,日付を取得して,西暦年月日に整形します。

具体的なコーディングは次のとおりです。


コード

 let today = new Date(); //日付データを取得
 sheet.getRange(1,6).setHorizontalAlignment('center'); //日付を出力するシェル書式設定
                                                       //日付を書式付きフォーマット出力
 sheet.getRange(1,6).setValue(Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy年MM月dd日'));


ここでは,Date関数を使って当日のデータを取得し,Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy年MM月dd日')において,西暦年月日表示に整形しています。

それを,getRange(1,6).setValue()関数を使って,セル(”F1")に設定しています。

日本式表示にユーティリティー(Utilities)があるのは助かります。


③【スプレッドシートの書き出し部分をクリアします。】

さて,次はシートの書き出し分のクリアです。これは,先程,Spreadsheetクラスオブジェクトで取得した固定値「sheet」を使って次のようにコーディングします。


コード

 let row_count = sheet.getLastRow() -3; //HEADER3行分を除く
 let col_count = sheet.getLastColumn(); //利用カラムの最大数

 if(row_count > 0){
   sheet.getRange(4, 1, row_count, col_count).clear(); //4行目から最終行までをクリア
 }


シート1の最大行を「sheet.getLastRow()」関数で取得して,それからヘッダー部分の3行分を引いた値を,変数「row_count」に代入します。

同様に,シート1の最大桁を「sheet.getLastColumn()」関数で取得して,それを変数「col_count」に代入します。

変数「row_count」が1以上の時は,索引簿内にファイルが表示されているので,その表示範囲を「sheet.getRange(4, 1, row_count, col_count).clear()」でクリアしています。

こうすることで,何回でも繰り返しファイル名称から索引簿を作り替えることが出来ます。


④【当該フォルダ下の対象ファイル(pdf,text,jpeg)をコレクションとして全抽出】

いよいよ,当該フォルダ下の対象ファイルをコレクションに呼び出します。コーディングは以下のとおりです。


コード

 //const files = folder.getFilesByType(MimeType.PDF);  //PDFファイルだけを出力
                                                       //pdf,text,jpgファイルをコレクション出力
 const files = folder.searchFiles('mimeType = "application/pdf" or mimeType = "text/plain" or mimeType = "image/jpeg"');


ここでは,「 folder.searchFiles('mimeType = "application/pdf" or mimeType = "text/plain" or mimeType = "image/jpeg"')」を使って,該当するファイルタイプを一括して固定値「files」にコレクション値として代入しています。

ファイルタイプが1種類の場合は,「getFilesByType(MimeType)」を使うことも可能です。

あとはこのコレクション値を一つずつ読めば,ファイル名称が取得できます。


⑤【コレクションを一つずつ読み出して,アンダーバー(”_”)でファイル名を分解し,配列に格納して,その配列毎に該当するスプレッドシートのセルに貼付】

このコレクションを読むには,「next()」メソッドを使って読むことになりますが,ファイルが見つからない場合に,例外エラーがスローされます。

そのため,次のファイルが存在するかを確かめるために「hasnext()」メソッドを使うようにします。具体的には以下のとおりです。


コード

 let row = 4;
 let col = 2;

 while (files.hasNext()){       //コレクションファイルが存在する間繰り返す
   var file =files.next();      //コレクションファイルを読む
   console.log(file.getName()); //ファイル名を出力する
   var str = file.getName();    //ファイル名を取得する
   var urllink = file.getUrl(); //ファイルURLを取得する
   
   var items = str.split('_');  //ファイル名を’_’で配列分割する
   console.log(items);          //配列全体を出力する

   i=0;
   var transcont = "";          //取引内容ストレージ
   while (i < items.length){    //配列の数だけ繰り返す
    //console.log(items[i]);
    if(i < 3){                  //取引年月日,取引先,金額を編集する
      sheet.getRange(row,col).setBorder(true,true,true,true,true,true); //セル上下左右罫線書く
      sheet.getRange(row,col).setHorizontalAlignment('center'); //セル水平中央に設定
      sheet.getRange(row,col).setVerticalAlignment('middle');   //セル垂直中央に設定
      if(col == 4){  //金額を数字に変換し出力する
        sheet.getRange(row,col).setNumberFormat('0');
        sheet.getRange(row,col).setValue(items[i].replace(/円/g, "")); //円を取って数字に変換
      }else{
        sheet.getRange(row,col).setWrap(true);                    //セル内折り返し設定
        sheet.getRange(row,col).setValue(items[i]);               //配列のi番目をセルに出力
      }
      col++;
    }
    else
    {
      if(transcont == ""){        //取引内容を整える
        transcont = items[i];     //取引内容をそのまま格納
      }else{
        transcont = transcont + "_" + items[i];  //取引内容に加えて格納
      }
    }
    i++;
   }
   //取引内容をセルに出力する。
   sheet.getRange(row,col).setBorder(true,true,true,true,true,true); //URLセル上下左右罫線書く
   sheet.getRange(row,col).setHorizontalAlignment('left'); //セル水平左寄設定
   sheet.getRange(row,col).setVerticalAlignment('middle'); //セル垂直中央設定
   sheet.getRange(row,col).setWrap(true);                  //セル内折り返し設定
   sheet.getRange(row,col).setValue(transcont);            //取引内容をセルに出力する   
   
   //URLLINKをセルに出力する。
   sheet.getRange(row,col+1).setBorder(true,true,true,true,true,true); //URLセル上下左右罫線書く
   sheet.getRange(row,col+1).setHorizontalAlignment('left'); //セル水平左寄設定
   sheet.getRange(row,col+1).setVerticalAlignment('middle'); //セル垂直中央設定
   sheet.getRange(row,col+1).setWrap(true);                  //セル内折り返し設定
   sheet.getRange(row,col+1).setValue(urllink);              //URLLINKをセルに出力する
   row++;
   col = 2;
 }


最初に,「let row = 4;」と「let col = 2;」で電子取引データファイル名を出力するセルの最初の行番号と桁番号を与えています。

続いて,While文でコレクションファイルを一つずつ読んで,「getName()」関数と「getUrl」関数で,ファイル名とURLを取得しています。

次に読んだファイル名を「split('_')」メソッドでアンダーバー「”_”」で配列に分解します。

var変数「transcont」は,分割した配列を取引内容として再構成するためのエリアです。

続いて,While文で変数「i」を使って,一つずつ配列を処理していきます。

取引年月日,取引先,金額(i < 3)については,各セルの罫線,水平垂直位置,セル内折り返しなどの書式を与えています。

金額(col=4の場合)は,replace(/円/g, "")メソッドで円の文字を削除してsetNumberFormat('0')で数字の書式を与えています。

取引年月日,取引先,金額以外の配列データは,var変数「transcont」に蓄積しています。

配列の処理が終わったら,蓄積されていた取引内容変数「transcont」とURL値「Urllink」を該当セルに文字配置位置や罫線などの書式付きで出力しています。

この一連の処理をコレクションファイルが無くなるまで繰り返します。


⑥【取引先年月日(昇順),取引先名(昇順),金額(昇順)で並べ替え】

スプレッドシートへの出力が完了しましたので,取引先年月日(昇順),取引先名(昇順),金額(昇順)でソートします。具体的なコードは以下のとおりです。


コード

 row_count = sheet.getLastRow() -3; //HEADER3行分を除く行数
 col_count = sheet.getLastColumn(); //利用カラムの最大数
 //console.log(row_count);

 if(row_count > 0){  //出力行数が1以上ならソートを行う
   const range = sheet.getRange(4, 1, row_count, col_count); //索引簿出力エリアを指定
   range.sort([ //ソートする
     {column: 2, ascending: true}, //2カラム(取引年月日)上昇順
     {column: 3, ascending: true}, //3カラム(取引先)上昇順
     {column: 4, ascending: true}  //4カラム(金額)上昇順
   ])
 } 

変数「row_count]と「col_count]に,「getLastRow()」と「getLastColumn()」関数を使って,再度,最大行数と最大桁数を与えます。

最大行数はヘッダー部分の3行を引いて,1行以上あればソート範囲を「getRange(4, 1, row_count, col_count)」で特定して,「range.sort()」を実施します。


⑦【順番に番号を付与】

最後に,順番に番号を付与します。コードは以下のとおりです。


コード

let counter = 1;
 row = 4;  //行番号を付与する。
 while(sheet.getRange(row,2).getValue() != ""){  //セルにデータがある限り繰り返す
  sheet.getRange(row, 1).setBorder(true,true,true,true,true,true); //セル上下左右罫線を書く
  sheet.getRange(row, 1).setHorizontalAlignment('center');  //セル水平中央設定
  sheet.getRange(row, 1).setVerticalAlignment('middle');    //セル垂直中央設定
  sheet.getRange(row, 1).setValue(counter);                 //番号を出力する
  counter++;
  row++; 
 }

変数「counter」に初期値1を与え,順番に加算して,シート1の番号セルに貼り付けます。

最後に全体を通してのコードを掲載します。


コード

function myindex_book_make() {
 
 const folderId = "1_2XXQX7WQySu-VLIE3YarYyvkSJFBJiK"
 const folder = DriveApp.getFolderById(folderId); //IDからフォルダー指定
 //const folder = DriveApp.getRootFolder();   //マイドライブのフォルダー指定
 console.log(folder.getName()); //現在のドライブ
 
 const ssActive = SpreadsheetApp.getActiveSpreadsheet(); //Activeシートオブジェクト指定
 console.log(ssActive.getName()); //シート名を出力
 const sheet = ssActive.getSheets()[0]; //スプレッドシートクラス指定

 let today = new Date(); //日付データを取得
 sheet.getRange(1,6).setHorizontalAlignment('center'); //日付を出力するシェル書式設定
                                                       //日付を書式付きフォーマット出力
 sheet.getRange(1,6).setValue(Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy年MM月dd日'));

 let row_count = sheet.getLastRow() -3; //HEADER3行分を除く
 let col_count = sheet.getLastColumn(); //利用カラムの最大数

 if(row_count > 0){
   sheet.getRange(4, 1, row_count, col_count).clear(); //4行目から最終行までをクリア
 }
 
 //const files = folder.getFilesByType(MimeType.PDF);  //PDFファイルだけを出力
                                                       //pdf,text,jpgファイルをコレクション出力
 const files = folder.searchFiles('mimeType = "application/pdf" or mimeType = "text/plain" or mimeType = "image/jpeg"');

 let row = 4;
 let col = 2;

 while (files.hasNext()){         //コレクションファイルが存在する間繰り返す
   var file =files.next();       //コレクションファイルを読む
   console.log(file.getName());    //ファイル名を出力する
   var str = file.getName();     //ファイル名を取得する
   var urllink = file.getUrl();  //ファイルURLを取得する
   
   var items = str.split('_');   //ファイル名を’_’で配列分割する
   console.log(items);             //配列全体を出力する

   i=0;
   var transcont = "";          //取引内容ストレージ
   while (i < items.length){    //配列の数だけ繰り返す
    //console.log(items[i]);
    if(i < 3){                  //取引年月日,取引先,金額を編集する
      sheet.getRange(row,col).setBorder(true,true,true,true,true,true); //セル上下左右罫線書く
      sheet.getRange(row,col).setHorizontalAlignment('center'); //セル水平中央に設定
      sheet.getRange(row,col).setVerticalAlignment('middle');   //セル垂直中央に設定
      if(col == 4){  //金額を数字に変換し出力する
        sheet.getRange(row,col).setNumberFormat('0');
        sheet.getRange(row,col).setValue(items[i].replace(/円/g, "")); //円を取って数字に変換
      }else{
        sheet.getRange(row,col).setWrap(true);                    //セル内折り返し設定
        sheet.getRange(row,col).setValue(items[i]);               //配列のi番目をセルに出力
      }
      col++;
    }
    else
    {
      if(transcont == ""){        //取引内容を整える
        transcont = items[i];     //取引内容をそのまま格納
      }else{
        transcont = transcont + "_" + items[i];  //取引内容に加えて格納
      }
    }
    i++;
   }
   //取引内容をセルに出力する。
   sheet.getRange(row,col).setBorder(true,true,true,true,true,true); //URLセル上下左右罫線書く
   sheet.getRange(row,col).setHorizontalAlignment('left'); //セル水平左寄設定
   sheet.getRange(row,col).setVerticalAlignment('middle'); //セル垂直中央設定
   sheet.getRange(row,col).setWrap(true);                  //セル内折り返し設定
   sheet.getRange(row,col).setValue(transcont);            //取引内容をセルに出力する   
   
   //URLLINKをセルに出力する。
   sheet.getRange(row,col+1).setBorder(true,true,true,true,true,true); //URLセル上下左右罫線書く
   sheet.getRange(row,col+1).setHorizontalAlignment('left'); //セル水平左寄設定
   sheet.getRange(row,col+1).setVerticalAlignment('middle'); //セル垂直中央設定
   sheet.getRange(row,col+1).setWrap(true);                  //セル内折り返し設定
   sheet.getRange(row,col+1).setValue(urllink);              //URLLINKをセルに出力する
   row++;
   col = 2;
 }
 
 row_count = sheet.getLastRow() -3; //HEADER3行分を除く行数
 col_count = sheet.getLastColumn(); //利用カラムの最大数
 //console.log(row_count);

 if(row_count > 0){  //出力行数が1以上ならソートを行う
   const range = sheet.getRange(4, 1, row_count, col_count); //索引簿出力エリアを指定
   range.sort([ //ソートする
     {column: 2, ascending: true}, //2カラム(取引年月日)上昇順
     {column: 3, ascending: true}, //3カラム(取引先)上昇順
     {column: 4, ascending: true}  //4カラム(金額)上昇順
   ])
 } 

 let counter = 1;
 row = 4;  //行番号を付与する。
 while(sheet.getRange(row,2).getValue() != ""){  //セルにデータがある限り繰り返す
  sheet.getRange(row, 1).setBorder(true,true,true,true,true,true); //セル上下左右罫線を書く
  sheet.getRange(row, 1).setHorizontalAlignment('center');  //セル水平中央設定
  sheet.getRange(row, 1).setVerticalAlignment('middle');    //セル垂直中央設定
  sheet.getRange(row, 1).setValue(counter);                 //番号を出力する
  counter++;
  row++; 
 }
}


  4.テスト結果(電子取引関係)

このスクリプトを実行してみます。実行結果は以下のとおりです。


索引簿スプレッドシート図2
索引簿スプレッドシート図2


ファイルのURLリンクも表示しましたので,検索機能は十分だと思います。電子取引データが増えるたびに,GoogleAppsScript(GAS)を起動します。

このスクリプトを毎回開いて実行することも可能ですが,面倒なのでスプレッドシード上に実行ボタンを配置して,そのボタンをクリックして実行するように改良します。

実行ボタンの配置方法は,図形タブから適当な図形を作成し,「右クリック」>「左クリック」で操作メニューが出ますので「スクリプトを割り当て」を選びます。以下の画面が出ますので,スクリプト関数名を入力します。

スクリプト割り当て画面
スクリプト割り当て画面

この実行ボタンを押したときの処理が,以下の図になります。


索引簿スクリプト実行図
索引簿スクリプト実行図



  5.まとめ


以上で,電子取引データの索引簿を作成するスクリプトの説明は終わりです。今回は,GASを習得する意味合いで「コンテナバインドスクリプト」を作ってみました。

電子帳簿保存法の電子取引データの保存では,これらの電子取引データの訂正・削除を防止するための規程類も整備する必要があります。

詳しくは,「令和5年6月電子帳簿保存法一問一答【電子取引関係】」をご覧ください。分からない点は税理士や所轄税務署にお尋ね下さい。

また,利用できるアプリなどは,公益社団法人日本文書情報マネジメント協会(JIIMA)が審査する「電子取引ソフト法的要件認証」の電子取引ソフト法的要件認証製品一覧をご覧ください。GoogleWorkspaceなども登録されていました。

(606200-00 Google Workspace 2022年1月版 グーグル・クラウド・ジャパン合同会社)


最後に,GoogleAppsScript(GAS)の注意点について申しあげます。このGASはクラウドで動作するスクリプトであるため,動作時間と動作回数に制限があります。また,ローカルのPCファイルを直接操作することは原則出来ません。

動作制限時間は,無料版でスクリプト実行時間が6分,関数の実行時間が30秒,トリガーによる総実行時間が一日あたり90分に制限されています。有料版はもう少し実行時間が増えるようです。

100~200ファイル程度の電子取引データ処理に6分もかかることはないと思いますが,もし,オーバーする場合は,処理フローを工夫する必要があります。

このスクリプト実行時間が制限される問題は,インターネット上でも話題になっており,制限を回避するためのテクニックサイトが多数存在します。

詳しくはそちらのサイトをご覧ください。

それでは次回まで。


(注意)情報の正確性を期していますが,実施される場合には自己責任でお願いします。


0 件のコメント: