フォームから自動でエクセル・PDFを作成し、メールに添付し送信するサービス

# これは何？
Google app scriptを使用して、フォーム入力から配送依頼内容を入力し、自動で配送明細のエクセルファイルと納品書（PDF）を作成し、倉庫会社にメールで送る、というシステムを作成しました。

<img width="712" alt="スクリーンショット 2016-01-03 2.24.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/95460/8e1bfbba-99d8-3d4e-f997-15a8e1b1b77a.png">


倉庫の会社が古い体制で、オリジナルフォーマットのエクセルと、PDFの納品書しか受け付けてくれないので、今までは渋々、１つ１つ手作業で作成していました。

今回のシステムの効果で、この面倒な作業が随分と改善されると思います。

#こんな人におすすめ！

データーを保存したり、定期的に何かやってほしいなど、サーバー的な仕組みは欲しいのだけれど、自分で構築したり、メンテしたりは敷居が高いなぁ、と思っている人に最適だと思います。私もサーバーサイドの仕組みを作ったのは初めてでした。

# Google app scriptとは？
無料で利用ができるサーバーサイドの仕組みです。Gmail等、Googleアカウントを持っている人なら誰でも使えます。

###参考
ドットインストール
http://dotinstall.com/lessons/basic_google_apps_script
チュートリアル
https://developers.google.com/apps-script/overview
リファレンス（spreadsheet）
https://developers.google.com/apps-script/reference/spreadsheet/

##開発環境

ブラウザで行います。
表記はjavascriptですが、サーバーサイドなので、クライアント用の一部の機能が使えません。開発は、Google app scriptのリファレンスを参照しつつ、その命令文を主にして使用するイメージです。下記、チュートリアルから一部転記します。コメントは日本語にしています。

```lang:YourFirstScript
  // 'Hello, world!'という名前のドキュメントを作成（指定がない場合、一番上の階層に作成される）
  var doc = DocumentApp.create('Hello, world!');
  // ドキュメントのBody要素に対し、パラグラフを作成。
  doc.getBody().appendParagraph('This document was created by Google Apps Script.');

  // 作ったドキュメントのURLを取得
  var url = doc.getUrl();

  // 自分のアカウントのgmailアドレスを取得
  var email = Session.getActiveUser().getEmail();

  // メール用の件名をドキュメントのタイトルにする
  var subject = doc.getName();

  // メールの本文にドキュメントのURLを記載する
  var body = 'Link to your doc: ' + url;

  // 自分のアドレスにドキュメントのURLを記載したメールを送る
  GmailApp.sendEmail(email, subject, body);
```

ちなみにスクリプトの作成は単独のファイルとして作る方法と、ファイルの中に内包して作る方法があって、今回はSpreadSheet内に作成しました。
詳しくは [チュートリアル](https://developers.google.com/apps-script/overview)に書いてあります。

###注意

サーバーの挙動を書くものなので、ブラウザ（クライアント）で使用する機能が使えません。（例：console.log,alert,setTimeout等）

当たり前ですが、スクロール位置やウィンドウサイズを調べることもできません。
（そもそもないから）

#メリット（良かった点）

###1) サーバー的な機能が無料で使える
何かを集計したり、自動でメールを送ったりなど、サーバーを持たなくてはできない機能があると思います。ただサーバーを借りたり、データベースをインストールしたりしなくてはいけないので、少なくても僕には敷居が高いものでした。それがお手軽に使えるのでとても便利です。特に特定の時間に機能したり、１分ごとにスクリプトを動かす機能は重宝しています。

###2) 表記が簡単
リファレンスの命令を１行貼り付ければ動きます。

`GmailApp.sendEmail("who@email.com", "件名",  "本文　改行は\nで");`

これだけでメールが送信できます。
起動のトリガーを１分ごとにすれば、１分ごとに送られます。
for文と組み合わせることもできるので、１分ごとに、１０通ずつ送るとかもできますが、そのうちにGoolgeさんから怒られるのではないでしょうか…

###3) 高度な権限管理
Google apps 全体に言えることですが、アカウントによっての権限を細かく決めることができます。spreadsheetに限ってはセルごとに編集権限を与えることもできます。さらに編集記録も残るので安心です。

<img width="494" alt="スクリーンショット 2016-01-03 3.11.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/95460/84a0ac25-624c-435a-ebcf-fd7a44cd20fa.png">

#デメリット（実装で困った点）

##1) 考え方に適応する

Google driveのファイルとフォルダは階層構造に見えて実は単独です。すべてにユニークIDとURLが振られています。対象の指定は、このIDやおそらくフォルダは、他のファイルやフォルダのIDを知っているだけで内包はしていないものだと思います。そのためフォルダ構成を変えてもIDやURLは変わりません。すべてのフォルダやファイルに対してアクセス権限の指定があります。スクリプトから参照しているだけのファイルにも事前にアクサス権限を指定する必要があります。

##2) どこにスクリプトがあるのかわからない…

上記にも書いたとおり、スクリプトを内包する方法だとどこにスクリプトがあるのかが非常にわかりにくいです。内包すると、内包元のデーターが操作しやすくなりますが、分かりやすくするという観点から単独タイプにしておくのも良いと思いました。

<img width="318" alt="スクリーンショット 2016-01-03 3.01.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/95460/d9034092-f695-059e-f5d2-2241269e42bc.png">

どこにスクリプトがあるのか見えない…

##ファイルの作成とメールの送信は同時に行えない

ファイルを操作するスクリプトの実行は大変遅く、処理に数秒かかることが普通です。
ドキュメントを作成し、それをすぐさま送信すると、何も記入されていないファイルが添付され送られてきます。

おそらく命令は非同期で処理され、ファイルに情報が書かれる前に送信されているためだと思われます。残念ながら処理の完了をお知らせしてくれるコールバックの機能は発見することができませんでした。（誰か知っていたら教えて下さい！）

非常に悩んだ末、無理やり実装したのが、１分ごと起動するスクリプトを用意して、送っていないファイルを見つけ次第、送るといったものです。今のところ問題はありませんが、運悪く書き込み中にこのスクリプトが動作すると、うまくいかないのではと思うところもあり、やや心配です。

<img width="805" alt="スクリーンショット 2016-01-03 4.01.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/95460/f45e6319-fe11-aa21-0f33-c7a4768118c1.png">

##グループメールが使えない…？

メールアドレスにグループメール（例：all@email.com）などを記載すると、なぜか送れません。スパム対策でしょうか？しかたがないので、カンマ区切りで送るべき全員のメールアドレスを記載しました。

###例

`GmailApp.sendEmail("hogehoge@email.com,mogemoge@email.com,chomechome@email.com", "件名",  "本文");`

#実装
```
// フォームが送信された時に起動　eの中にはフォームの情報が入っているが使っていない
function submitForm(e){
  //ファイル名の取得　時間になる
  var version = getVersionString();
  //コピーする配送表のフォーマット　事前にIDを調べておく
  var template = DriveApp.getFileById("***");
  //配送表を格納するフォルダ　事前にIDを調べておく
  var folder = DriveApp.getFolderById("***");
  // フォーマットからコピーを作成　上のフォルダの中に置く
  var newfile = template.makeCopy(version, folder);
  // リンクを知っている人はアクセスできるように
  // これをしないとアクセス権限によりエクセルの添付できない
  // この複雑なリンクはサーバーでしか見ることができないのでセキュリティ的に大丈夫だと思う
  newfile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.EDIT);
  // フォームの情報はmaster_sheetに書き入れられている
  var master_sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  // 必要な情報を倉庫会社の特殊フォーマットに転記する
  var order_sheet = SpreadsheetApp.openById(newfile.getId()).getSheets()[0];  
  setValue(2,2);
  // メールと納品書で必要な情報を格納
  var name = setValue(3,3);
  setValue(4,5);
  setValue(5,6);
  setValue(6,9);
  setValue(7,10);
  setValue(8,11);
  // 納品書で必要な情報を格納
  var products = setValue(9,13);
  // 納品書を作成
  createDelivery (name, products);
  // 転記用関数
  function setValue(from_col, to_col) {
    var value = master_sheet.getRange(master_sheet.getLastRow(),from_col).getValue();
    order_sheet.getRange(4, to_col).setValue(value);
    return value;
  }
  // タイムスタンプ発行関数
  function getVersionString (is_text) {
    var now = new Date();
    var year = now.getYear();
    var month = now.getMonth() + 1;
    var day = now.getDate();
    var hour = now.getHours();
    var min = now.getMinutes();
    var sec = now.getSeconds();
    if(is_text) return year + "年 "+ month + "月" + day + "日";
    else return "version_" + year + month + day + hour + min + sec;
  }
  // 納品書作成関数　中身は上の注文書とほぼ同じ
  function createDelivery (name, products) {
    var template = DriveApp.getFileById("***"); 
    var folder = DriveApp.getFolderById("***");
    var newfile = template.makeCopy(version, folder);
    var data = getVersionString(true);
    var body = DocumentApp.openById(newfile.getId());
    // フォーマットの中身を書き変え
    var date_text = rewrite("date",getVersionString(true));
    var name_text = rewrite("name",name + " 様");
    var products = rewrite("products",products);
    // 文章の見た目を調整
    name_text.setFontSize(16).setBold(true).setUnderline(true);
    products.setBold(true);
    // 書き換えよう関数
    function rewrite (orignal, rewrite) {
      var range = body.getBody().findText("--" + orignal + "--");
      if(range) {
        var text = range.getElement().asText();
        text.setText(rewrite);
        return text;
      }
    return 0;
    }
  }
}

// 一定時間で起動される　この場合は１分ごと
function sendEmail () {
  // 配送表が格納されているファイルだ内を検索
  var files_s = DriveApp.getFolderById("***").getFiles();
  // フォルダ内のデーターを一度に取ることはできない
  while(files_s.hasNext()){
    var target_s = files_s.next()
    var name_s = target_s.getName();
    // 送信が終わったファイルには_sendがつく
    if(!name_s.match("_send")) {
      // 今度は、納品書が格納されているフォルダを検索
      var files_d = DriveApp.getFolderById("***").getFiles();
      while(files_d.hasNext()){
        var target_d = files_d.next()
        var name_d = target_d.getName();
        // _sendがない、かつ・・・
        if(!name_d.match("_send")) {
          // バージョン名が一致している　＝　セット
          if(name_s == name_d) {
            var attachments = new Array();
            var id_s = target_s.getId();
            // エクセル形式で取得するURL!
            var fetchUrl = "https://docs.google.com/feeds/download/spreadsheets/Export?key=" + id_s + "&amp;exportFormat=xlsx";
            // BlobというGoogleの「なんでもデータ」形式に変換、名前をつけるattachments.push(UrlFetchApp.fetch(fetchUrl).getBlob().setName(target_s.getName()));
            // 今度は納品書　ドキュメントはBlobにするとPDFとしてダウンロードできるみたい
            var id_d = target_d.getId();
            attachments.push(DocumentApp.openById(id_d).getBlob().setName("納品書"));
            // いよいよメールを送ります。。
            MailApp.sendEmail({
              to: "hogehoge@email.com",
              cc:"mogemoge@email.com",
              subject: "配送依頼",
              name:"自動メール配信システム",
              body:"お世話になっています。\n" +
              "・・・\n\n" +
              "・・・\n" +
              "・・・\n" +
              "以上",
              // これが添付ファイル 配列
              attachments: attachments
            });
            // _sendつけて送ったことを名前に直接記録
            target_s.setName(target_s.getName() + "_send");
            target_d.setName(target_d.getName() + "_send");
          }
        }
      }
    }
  }
}
```