【0からGASを学ぶ】GASを用いてスプレッドシート+ドキュメント+メール送信を自動化しよう

# はじめに
本シリーズでは、GASの始め方や便利な使い方、ビジネス活用まで幅広く解説します。シリーズをひと通り読んでいただければ、あなたもきっとGASマスターになれるはずです。

# シリーズの対象者
* そもそもGASってなんだか**わからない**方
* GASを学びたいけど**何から始めればいいか**わからない方
* GASはわかり始めたけど、**もっと活用ができないか**と模索している方
* とにかく**Googleが好き！** という方

# 前回記事
https://qiita.com/gas-suke/items/3c7c88e23e74388e3186

# スプレッドシートで遊ぶ
では早速始めていきましょう。【0からGASを学ぶ】シリーズの第7回は「GASを用いてスプレッドシート+ドキュメント+メール送信を自動化しよう」です。今回は少し複雑なことをやってみようと思います。これまで、`Googleドキュメント`や`Gmail`をGASから操作しました。また<a href="https://qiita.com/gas-suke/items/928fdd8d01fafc4aed1b" target="_blank">第5回</a>では、スプレッドシートのデータを評価し、メールを自動送信しました。これらを組み合わせてみましょう。

## 今回やること
1. スプレッドシート内のすべてのデータを取得し、行ごとに評価する。
1. 発注が必要な場合は、注文書をGoogleドキュメントで作成する。
1. GoogleドキュメントをPDF変換する。
1. 変換したPDFを添付ファイルとして相手先にメールを送信する。

## 事前準備
今回はサンプルでいくつかファイルを作ります。もちろんサンプルなのでこの通りである必要はありませんが、これらを前提にプログラムは書かせてもらいます。
### 在庫管理表の準備
スプレッドシートのデータを取得しなければ始まらないので、**取得元となるスプレッドシート**を作りましょう。私は以下のようなスプレッドシートを作成しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/3e1b0e82-fd9f-df88-7eda-ea482b05f6fc.png" width="600">

### 注文書テンプレートの準備
Googleドキュメントで注文書テンプレートを適当に作成しましょう。どんな様式でも構いません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/8ee4bf73-8183-6685-4dfe-22256d0f9f46.png" width="600">

:::note info
枠で囲った箇所を**可変文字部**と呼ばせてもらいますが、今回のポイントはここです。
:::

### PDF出力場所の作成
「今回やること」にも記載しましたが、途中でGoogleドキュメントをPDF変換するため、その出力先を作成しておきましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/c6992c0b-b840-b373-de7c-60d8c2117aee.png" width="600">

### IDの取得
スプレッドシートはコンテナバインド型で開くため、IDは不要ですが、`注文書テンプレート`と`PDF出力場所`のIDは事前に取得しておきましょう。

## プログラム開始
### GASエディタを開く
今回も在庫管理表に対するコンテナバインド型でGASプログラムを記述していきましょう。<a href="https://qiita.com/gas-suke/items/5cf7bd4c6c3f15a25838#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%90%E3%82%A4%E3%83%B3%E3%83%89%E5%9E%8B" target="_blank">こちら</a>を参考にGASエディタを起動してください。では、どんどんいきますよ、ついてきてください。

### 結論
今回はまず最終形をお見せします。そのうえで、解説していきましょう。
```javascript:Qiita007.gs
// Qiita007_簡易在庫管理表のヘッダーカラム
const COL = {
  NUM:1
  , PRODUCT_NAME:2
  , REQUIRED_QUANTITY:3
  , INVENTORY_CNT:4
  , PRICE:5
  , COMPANY:6
  , STAFF:7
  , MAIL:8
}

// Qiita007_注文書のテンプレートファイル
const ORDER_FORM_TEMPLATE = DriveApp.getFileById('15ZqXoJxpuq96NaPJBbWinEYx4H-Qn2lxkRCNr2dgnKM');

// PDF出力先
const PDF_OUTDIR = DriveApp.getFolderById('1DNlhIKHlFVJ419UGh3RR3eCZkC_xyUfi');

function Qiita007_SpreadSheet() {
  // コンテナバインド型なので、getActiveSpreadsheetにより自分自身を取得する
  const wSpread = SpreadsheetApp.getActiveSpreadsheet()

  // スプレッドシート内の1番目のシートを取得する
  const wSheet = wSpread.getSheets()[0];

  // シート内の全データを取得する。
  // getDataRangeはデータが存在する範囲のみを取得してくれるため非常に便利
  let wVals = wSheet.getDataRange().getValues();

  // 今回はforEachでデータを回したいので、shiftメソッドを使って配列の最初の要素を削除する
  wVals.shift();

  // データを走査
  wVals.forEach(function(wVal) {
    // [必要数]>[在庫数]の行を特定する
    if (wVal[COL.REQUIRED_QUANTITY-1]>wVal[COL.INVENTORY_CNT-1]) {
      // PDF変換する元ファイルを作成する
      let wFileRtn = createGDoc(wVal);
      // PDF変換してファイルIDを取得する
      let wPdfId = createPdf(wFileRtn[0], wFileRtn[1]);
      // PDF変換したあとは元ファイルを削除する
      DriveApp.getFileById(wFileRtn[0]).setTrashed(true);
      // 今回はPDFファイルを添付してメールを送信する
      GmailApp.sendEmail(
        wVal[COL.MAIL-1]
        , '【自動送信メール】発注依頼'
        , `${wVal[COL.PRODUCT_NAME-1]}の在庫がなくなりました。
必要数：${wVal[COL.REQUIRED_QUANTITY-1]}
在庫数：${wVal[COL.INVENTORY_CNT-1]}`
        , {attachments: DriveApp.getFileById(wPdfId).getBlob()}
      )
    } else {
      // 何もしない
    }
  });
}


function createGDoc(rowVal) {
  // テンプレートファイルをコピーする
  const wCopyFile = ORDER_FORM_TEMPLATE.makeCopy()
        , wCopyFileId = wCopyFile.getId()
        , wCopyDoc = DocumentApp.openById(wCopyFileId); // コピーしたファイルをGoogleドキュメントとして開く
  let wCopyDocBody = wCopyDoc.getBody(); // Googleドキュメント内の本文を取得する

  // 注文書ファイル内の可変文字部（として用意していた箇所）を変更する
  wCopyDocBody = wCopyDocBody.replaceText('{{date}}', Utilities.formatDate(new Date(), 'JST', 'yyyy年MM月dd日'));
  wCopyDocBody = wCopyDocBody.replaceText('{{company_name}}', rowVal[COL.COMPANY-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{staff_name}}', rowVal[COL.STAFF-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{company_from}}', 'GAS大好きカンパニー');
  wCopyDocBody = wCopyDocBody.replaceText('{{name_from}}', 'gas-suke');
  wCopyDocBody = wCopyDocBody.replaceText('{{product_name}}', rowVal[COL.PRODUCT_NAME-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{count}}', rowVal[COL.REQUIRED_QUANTITY-1]-rowVal[COL.INVENTORY_CNT-1]+100);
  wCopyDocBody = wCopyDocBody.replaceText('{{price}}', rowVal[COL.PRICE-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{total}}', rowVal[COL.PRICE-1]*(rowVal[COL.REQUIRED_QUANTITY-1]-rowVal[COL.INVENTORY_CNT-1]+100));
  wCopyDoc.saveAndClose();

  // ファイル名を変更する
  let fileName = rowVal[COL.COMPANY-1]+'宛注文書_'+Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd');
  wCopyFile.setName(fileName);
  // コピーしたファイルIDとファイル名を返却する（あとでこのIDをもとにPDFに変換するため）
  return [wCopyFileId, fileName];
}


function createPdf(docId, fileName){
  // PDF変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/document/d/${docId}/export?exportFormat=pdf`;

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  }; 
  // PDFを作成する
  let wBlob = UrlFetchApp.fetch(wUrl, wOtions).getBlob().setName(fileName + '.pdf');

  //PDFを指定したフォルダに保存する
  return PDF_OUTDIR.createFile(wBlob).getId();
}
```
実行すると、対象に**添付付きメールが送信**されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/30f97060-e66f-492d-6751-fbad7a20ff87.png" width="400">

また、添付されたファイルは以下のように出力されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/e7924916-d5bc-dae9-1399-8759a3068303.png" width="400">
無事に**可変文字部**が在庫管理表内のデータを用いて編集されていることを確認できます。

### 解説
`Qiita007_SpreadSheet`自体にはそこまで目新しいものはないため、`createGDoc`と`createPdf`を中心に見ていきましょう。
#### createGDoc
```javascript:createGDoc
function createGDoc(rowVal) {
  // テンプレートファイルをコピーする
  const wCopyFile = ORDER_FORM_TEMPLATE.makeCopy()
        , wCopyFileId = wCopyFile.getId()
        , wCopyDoc = DocumentApp.openById(wCopyFileId); // コピーしたファイルをGoogleドキュメントとして開く
  let wCopyDocBody = wCopyDoc.getBody(); // Googleドキュメント内の本文を取得する

  // 注文書ファイル内の可変文字部（として用意していた箇所）を変更する
  wCopyDocBody = wCopyDocBody.replaceText('{{date}}', Utilities.formatDate(new Date(), 'JST', 'yyyy年MM月dd日'));
  wCopyDocBody = wCopyDocBody.replaceText('{{company_name}}', rowVal[COL.COMPANY-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{staff_name}}', rowVal[COL.STAFF-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{company_from}}', 'GAS大好きカンパニー');
  wCopyDocBody = wCopyDocBody.replaceText('{{name_from}}', 'gas-suke');
  wCopyDocBody = wCopyDocBody.replaceText('{{product_name}}', rowVal[COL.PRODUCT_NAME-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{count}}', rowVal[COL.REQUIRED_QUANTITY-1]-rowVal[COL.INVENTORY_CNT-1]+100);
  wCopyDocBody = wCopyDocBody.replaceText('{{price}}', rowVal[COL.PRICE-1]);
  wCopyDocBody = wCopyDocBody.replaceText('{{total}}', rowVal[COL.PRICE-1]*(rowVal[COL.REQUIRED_QUANTITY-1]-rowVal[COL.INVENTORY_CNT-1]+100));
  wCopyDoc.saveAndClose();

  // ファイル名を変更する
  let fileName = rowVal[COL.COMPANY-1]+'宛注文書_'+Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd');
  wCopyFile.setName(fileName);
  // コピーしたファイルIDとファイル名を返却する（あとでこのIDをもとにPDFに変換するため）
  return [wCopyFileId, fileName];
}
```
ここでポイントとなるのは、冒頭にも申しあげた通り、`{{XXXXXX}}`で示した可変文字部です。`Googleドキュメント`においてはスプレッドシートと異なり、**セル位置がないため特定の場所を更新する（例えばスプレッドシートでC11セルとか）ことは不得意**です。そのため、特定の場所にあらかじめ**変更するための目印をつけておく**ことが必要になります。
そのうえで、`getBody()`によりGoogleドキュメントのBodyを取得し、`replaceText`を用いて編集します。

:::note info
`{{}}`をお決まりのような書き方をしていますが、区切り文字はなんだってかまいません。ご自身の中で**分かりやすいマーク**であり、**明確に可変文字部とわかれば**それでいいです。
:::

#### createPdf
```javascript:createPdf
function createPdf(docId, fileName){
  // PDF変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/document/d/${docId}/export?exportFormat=pdf`;

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  }; 
  // PDFを作成する
  let wBlob = UrlFetchApp.fetch(wUrl, wOtions).getBlob().setName(fileName + '.pdf');

  //PDFを指定したフォルダに保存する
  return PDF_OUTDIR.createFile(wBlob).getId();
}
```
ここでポイントとなるのは、`https://docs.google.com/document/d/${docId}/export?exportFormat=pdf`のURLとなります。
1. `https://docs.google.com/document/d/${docId}`までが**対象のGoogleドキュメント**を指しています。
1. そのあとの`export?`で**エクスポートを指示**しており、GETパラメタへ続きます。
1. `exportFormat=pdf`は読んで字のごとく、**エクスポートの形式を指示**します。

:::note info
#### Bearer認証
Bearer認証はアクセストークン認証といい、**ログインID・パスワードなどでユーザ認証を行なった後**に、サービスから発行される**アクセストークン（GASにおいては、スクリプト自体のアクセストークン`getOAuthToken()`を使用）** を受け取って、APIのリクエスト時に送信する方式です。

ヘッダー項目を
`Authorization: Bearer {アクセストークン}`
で設定して、HTTPリクエストを送信します。
:::

### さらっとfor文をforEach文に書き換えました
```javascript:前回
let wVals = wSheet.getDataRange().getValues();

// 1行目（index:0）はヘッダー列なので、2行目からデータを走査
for (let rIdx=1; rIdx<wVals.length; rIdx++) {
  // [必要数]>[在庫数]の行を特定する
  if (wVals[rIdx][COL.REQUIRED_QUANTITY-1]>wVals[rIdx][COL.INVENTORY_CNT-1]) {
    …
  }
}
```
```javascript:今回
let wVals = wSheet.getDataRange().getValues();

// 今回はforEachでデータを回したいので、shiftメソッドを使って配列の最初の要素を削除する
wVals.shift();

// データを走査
wVals.forEach(function(wVal) {
  // [必要数]>[在庫数]の行を特定する
  if (wVal[COL.REQUIRED_QUANTITY-1]>wVal[COL.INVENTORY_CNT-1]) {
    …
  }
});
```
一見すると同様のことが行えているため、書き方のちょっとした違いのようにも見えますが、**`for`と`forEach`はまるで異なり**ます。
`for`は**構文**でありいつでもどこでも使えます。しかし、`forEach`は**配列要素に対するメソッド**であり、使用箇所は限定的となります。ただし、見ていただくとわかる通り、`for`の場合に行っていた**indexの管理が不要**であり、配列データを扱う場合には**非常に美しくプログラムを記載**することができます。

:::note info
`forEach`でもindexを扱うことは可能です。
```javascript
wVals.forEach(function(wVal, index) {
  console.log(index);
}
```
実際には第3引数も指定できますが、この辺りの詳細は**また今度！**
:::

### 
# おわりに
お疲れ様でした。
第7回は「GASを用いてスプレッドシート+ドキュメント+メール送信を自動化しよう」ということで、少し**複雑**なことをしてみました。しかし、一つ一つを見てみると**これまで行ったことを組み合わせただけ**であることに気づくかと思います。つまり、**いかなるプログラムやシステムも基本的には基礎を組み合わせて出来上がったもの**です。これまで学んだ**GASスキルを少しずつ組み合わせ**て、ぜひ業務効率化を進めてみてください。次回も引き続き、スプレッドシートで遊んでいきましょう。
記事を読んで、「良いな」や「今後に期待できる！」と感じて頂けたら**いいね**や**フォロー**、**コメント**いただけると幸いです。それではまた次回をお楽しみに！

## ブログでより詳しく解説しています！
以下画像をクリックしてブログにアクセス！！
<a href="https://gas-4u.com/gas06/" target="_blank"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/23153bcb-75d8-ac1e-a2af-5ac799aa08d5.png" width="600"/></a>