毎月の請求書作成をGoogleサービスのみで無料で自動化する

# なぜ請求書作成サービスを利用しないのか

- 毎月多少なりともお金がかかる
- 請求書のテンプレートがあまり好みじゃなかったりする
- 可能であればログインして利用するサービスはあまり増やしたくない

個人的には毎月の請求対象が相当な数あって、入金管理が必須で、入金ステータスの自動取得が出来るサービスを使いたいとかでなければspreadsheetからのPDF出力で十分簡潔に管理出来ると感じたため、そのような運用にしてみました。

# 請求書発行までの流れ

以下のエントリを参考にしました。
[GoogleフォームとGASで請求書のPDFを自動生成する](https://qiita.com/non-programmer/items/7accd90bbcb8630a66e7)

Googleフォームへの入力からPDFを生成というフローは個人的には微妙だったので、Spreadsheetで毎月の請求項目をまとめたタブを作成管理し、ボタンクリックで指定しておいたGoogleドライブにPDF保存されるような形にしました。

# DEMO

[Spreadsheet サンプル](https://docs.google.com/spreadsheets/d/152vSzTgc_sl56Bfr4GQIsni_qvs00Xe_p4xzo6Ex_Jw/edit#gid=719554900)

Spreadsheetでは最終的にPDFとして出力したい請求書のフォーマットをテンプレ化したものと、クライアント情報をそれぞれのタブに入力しておきます。

また、請求書を発行したい月の（サンプルの場合は2019年6月）名前をつけたタブで項目と金額、クライアントを入力していきます。（クライアントはclientsタブに設定したクライアント名をドロップダウンで選択します。）

[GoogleDrive サンプル](https://drive.google.com/drive/u/0/folders/1jDT37QNqK7c61kveNfP2XMiBqrPftvB6)

指定したGoogleDriveのフォルダの中に該当月（サンプルの場合は201906）のフォルダが作成され、その中にPDFが保存されます。

<b>▽上記のサンプルは閲覧のみ許可にしているため実際の動きをキャプチャしてみました。</b>
[![Image from Gyazo](https://i.gyazo.com/f8db8b492a82f95d00f318e08c01696c.gif)](https://gyazo.com/f8db8b492a82f95d00f318e08c01696c)
201906タブの右上にあるアイコンをクリックすると処理が開始するように設定しています。
処理が始まると、templateをコピーして各クライアント分の請求書タブを用意し、そこに請求項目を追加していきます。
全ての項目の追加が完了したらPDFとしてドライブに保存し、各クライアント用の請求書タブは削除します。

# GAS
以下、作成したコードです。

```js:create.gs
function CreateInvoice() {
  
  targetym = SpreadsheetApp.getActiveSheet().getName(); //ボタンを押したタブ名から対象年月を取得
  
  ssid = "1m8O53AC2SHd4tXLYgp_fkeP5qWUQuLbjJoaGmt6nneg"; //spreadsheetのID
  gdid = "1lBDb4RpdaURBru5wo4SabhgEMqw8PjXX"; //driveのID
  ss = SpreadsheetApp.openById(ssid);
  
  var mkdirflg = true;
  var tagetfolderid = '';
  
  //保存する年月フォルダが無ければ作成
  var testfolders = DriveApp.getFolderById(gdid).getFolders();
  while(testfolders.hasNext()) {
    var testfolder = testfolders.next();
    Logger.log(testfolder.getName());
    if (testfolder.getName() == targetym) {
      mkdirflg = false;
      var tagetfolders = DriveApp.getFolderById(gdid).getFoldersByName(targetym);
      while(tagetfolders.hasNext()) {
        var temp = tagetfolders.next();
        tagetfolderid = temp.getId();
      }
    }
  }
  
  if (mkdirflg) {
    tagetfolderid = DriveApp.getFolderById(gdid).createFolder(targetym).getId();
  }
  
  Logger.log(tagetfolderid);
  
  datass = ss.getSheetByName(targetym);
  pdfss =  ss.getSheetByName("template");
  clientss =  ss.getSheetByName("clients");
  
  //登録クライアント情報を取得
  var clientsdata = getClients(clientss);
  
  CreateInvoicebyc(clientsdata);
  
  WrittingInvoice();
  
  CreatePDF(clientsdata,tagetfolderid)

}

function CreatePDF(clientsdata,tagetfolderid) {
  
  var folder = DriveApp.getFolderById(tagetfolderid);
  
  //以下PDF化のためのプログラムです。今回はA4サイズでPDF化するため、オプション設定も下記のようになります。
  var url = "https://docs.google.com/spreadsheets/d/SSID/export?".replace("SSID", ssid);
  
  Logger.log(url);
  
  clientsdata.forEach(function(values, index){
    
    var thisclient = ss.getSheetByName(values[0]);
    
    var url_ext = 'exportFormat=pdf&format=pdf'        // export as pdf / csv / xls / xlsx
    + '&size=letter'                       // paper size legal / letter / A4
    + '&portrait=true'                     // orientation, false for landscape
    + '&fitw=true&source=labnol'           // fit to page width, false for actual size
    + '&sheetnames=false&printtitle=false' // hide optional headers and footers
    + '&pagenumbers=false&gridlines=false' // hide page numbers and gridlines
    + '&fzr=false'                         // do not repeat row headers (frozen rows) on each page
    + '&gid=';                             // the sheet's Id

    //https://gist.github.com/Spencer-Easton/78f9867a691e549c9c70 （参考
  
    var token = ScriptApp.getOAuthToken();
    
    var response = UrlFetchApp.fetch(url + url_ext + thisclient.getSheetId(), {
      headers: {
        'Authorization': 'Bearer ' +  token
      }
    });
    
    var blob = response.getBlob().setName(thisclient.getRange("N2").getValue() + ".pdf");
    
    folder.createFile(blob);
    
    //シートを削除
    ss.deleteSheet(thisclient);
    
    Utilities.sleep(1000);
    
  });
  
}

function getClients(clientss) {
  const columnBVals = clientss.getRange('B2:B').getValues();
  const LastRow = columnBVals.filter(String).length;
  
  const returnval = columnBVals.slice(0,LastRow);

  Logger.log(returnval);
  return returnval;
}

function CreateInvoicebyc(clientsdata) {
  
  clientsdata.forEach(function(values, index){
    
    ss.insertSheet(values[0],{template: pdfss});
    
    var thisclient = ss.getSheetByName(values[0]);
    
    var tagetyear = parseInt(targetym.slice(0,4));
    var tagetmonth = parseInt(targetym.slice(4));
    
    var paydate = tagetyear + '/' + (tagetmonth+1) + '/' + new Date(tagetyear,tagetmonth+1,0).getDate();
    if (tagetmonth == 12) {
      paydate = (tagetyear + 1) + '/01/' + new Date(tagetyear + 1,1,0).getDate();
    }
    
    Logger.log(tagetmonth);
    
    var lastdate = new Date(tagetyear,tagetmonth,0).getDate();
    
    var rownum = (parseInt(index)+2).toFixed();
    
    var comaddress = clientss.getRange('C' + rownum).getValue();
    var comtel = clientss.getRange('D' + rownum).getValue();
    
    thisclient.getRange("N2").setValue(targetym + index);
    thisclient.getRange("M3").setValue(tagetyear + '/' + tagetmonth + '/' + lastdate );
    thisclient.getRange("C4").setValue(values[0] + ' 御中');
    thisclient.getRange("R34").setValue('お支払い期限 : ' + paydate);
    thisclient.getRange("C5").setValue(comaddress);
    thisclient.getRange("C7").setValue('TEL : ' + comtel);
    
    Logger.log(values[0]);
    
  });
  
}

function WrittingInvoice() {
  
  var getItems = datass.getRange('B2:B').getValues();
  
  getItems.forEach(function(values, index){
    
    var rownum = (parseInt(index)+2).toFixed();
    
    if (values[0] == '') {
      return true;
    }
    
    var item = values[0];
    var sprice = datass.getRange("C" + rownum).getValue();
    var quantity = datass.getRange("D" + rownum).getValue();
    var tprice = datass.getRange("E" + rownum).getValue();
    var client = datass.getRange("F" + rownum).getValue();
    
    var targetclient = ss.getSheetByName(client);
    var targetclientrow = targetclient.getRange('B12:B18').getValues();
    
    for(var ii = 12; ii < 19; ii++){
      
      if (targetclient.getRange("B" + ii).getValue()) {
        continue;
      } else {
        targetclient.getRange("B" + ii).setValue(item);
        targetclient.getRange("G" + ii).setValue(quantity);
        targetclient.getRange("H" + ii).setValue(sprice);
        break;
      }
      
    }
    
  });
  
}
```
※ 命名がかなり適当ですがご勘弁ください。

# 最後に
あくまで自分にとって必要十分な範囲でしか実装していないので、

- 請求書フォーマットが7項目しかないので１つのクライアントに対して項目が8個以上あると多分変になる
- 同じクライアントでも内容によって請求書を分けて欲しいという要望があったりしますが、それに対応していない（対応するつもりでsuffixという列を設けたのですが面倒になりました。。）

など、まだまだやりようはいくらでもあるのですが、ひとまずこれで良しとしました。
また、GASだと毎月X日に実行とか簡単に実装できると思うので、本当に毎月同じ金額同じ内容で請求しているようなものは完全自動化でDriveに保存しつつクライアントにメール送信してしまうのが良いですね。
この記事を元に各自の運用などに合わせてカスタマイズは割と楽に出来るかなと思うので参考にしていただけたら嬉しいです。またデザイナーさんなどで、実際に使ってみたいけどよく分からんという方がいらっしゃいましたら、最低限同じ動きをさせるまでの部分を補足しようかなと思いますのでコメントいただけましたら幸いです。