kintoneにサーバーレスで印刷機能を実装する(AWS Lambda + Thinreports)

# 最初に

kintoneに限らず業務系のWebサービスでは、帳票印刷が必要になってきます。
（まだまだ紙で印刷したり、メールに添付したり、ダウンロードしたり）

ここでは、[kintone](https://kintone.cybozu.co.jp/) の印刷機能について調査と独自実装する場合の方法についての考え方をまとめたいと思います。

# 主な [kintone 帳票印刷ソリューション](https://kintone-sol.cybozu.co.jp/integrate/?_ga=2.217224278.1575313049.1572136352-973750221.1496066698&_gac=1.262536702.1570843509.Cj0KCQjw0IDtBRC6ARIsAIA5gWvCvZQJv5_IIkx2pr_sdG-J8KFmxzXUvvAIIWUpxygR7D1l4_EFjEgaAktlEALw_wcB#tab-refine)
（カッコ内は販売会社名）

kintone連携ソリューション公式ページから個人的に使ったり、知っている主な製品をまとめてみました。
（ここでの印刷はPDFで出力することを指しています）

* [プリントクリエイター（トヨクモ）](https://pc.kintoneapp.com/)
    * コース毎の月額定額のサブスクリプションモデル。専用のサービス上で印刷レイアウトをプレビューで確認しながら作成可能。印刷枚数は無制限。
    * 専用サービスにログインして、PDFで作成したテンプレートファイルをアップロード。アップロードしたPDFテンプレート上にkintoneのフィールドをマッピングしていきます。（マウスで矩形を作り、そこにフィールドをマッピングする感じです）
* [RepotoneU PDF(株式会社ソウルウェア)](https://www.repotone.com/)
    * PDF印刷専用プラグイン型買い切り制。プラグイン設定画面にて印刷レイアウトを作成。Excel出力版や毎月一定額を支払うサブスクリプションモデルもあります。
    * プラグイン画面でPDFで作成したテンプレートファイルをアップロード。アップロードしたPDFテンプレート上にkintoneのフィールドをマッピングしていきます。（マウスで矩形を作り、そこにフィールドをマッピングする感じです）
* [ReportsConnect for kintone(株式会社ケーピーエス)](https://kintone-sol.cybozu.co.jp/integrate/kps001.html)
    * 月額定額のサブスクリプション。PCにインストールしたレイアウトツール[iReportデザイナー](https://community.jaspersoft.com/project/ireport-designer)を使ってレイアウトを作成。無料枠有り。
* [gusuku Customine（アールスリーインスティテュート）](https://customine.gusuku.io/)
    * ノーコードでkintoneにJavaScriptのカスタマイズを追加できるサービス。このサービスのできることに「Excel/PDF/CSV」の出力機能があります。Customineでは、Excelでテンプレートを作成し、テンプレートにてkintoneのフィールドコードをマッピングします。そのテンプレートにkintoneのレコードのフィールドの値を流し込むような仕組みです。テンプレートのマッピングは[こちらのマニュアルを参照](https://docs-customine.gusuku.io/ja/excelpdf-template-reference/)。
    * https://support.gusuku.io/ja-JP/support/solutions/articles/36000303634
* [csvラポ　kintone連携プラグイン](https://www.icon-co.jp/csvlapot/kintone/)
    * kintoneの一覧印刷に特化した帳票サービス。レイアウトを必要としないのが特徴。


# Thinreports を使った帳票印刷

サーバーレスが普通になってきた昨今、帳票印刷はサーバーレスで自前で実装することも可能になってきましたね。

## 概要

[Thinreports](http://www.thinreports.org/)は専用のデザイナーアプリ [Thinreports Editor](http://www.thinreports.org/news/2017/09/thinreports-editor-0_10_0-released/)とRubyのSDK [Thinreports Generator](http://www.thinreports.org/news/2017/05/thinreports-generator-0_10_0-released/)を使ってPDFを出力するソリューションです。

デザイナーではドラッグ＆ドロップでレイアウトを作成出来ます。
データとレイアウトファイルを繋ぐコードをRubyで記述します。

* [Thinreports Editor](http://www.thinreports.org/news/2017/09/thinreports-editor-0_10_0-released/)
* [Thinreports Generator](http://www.thinreports.org/news/2017/05/thinreports-generator-0_10_0-released/)

## 専用サーバーで利用する

[Ruby on Rails](https://rubyonrails.org/) 等の Ruby製 WebアプリケーションフレームワークにレイアウトファイルとSDKをインストールすることでWebアプリケーションにPDF出力機能を追加することが出来ます。

## サーバーレスで利用する

AWS Lambda などを利用することで専用のサーバーを自前で用意することなくPDF出力機能を追加出来ます。

# サーバーレスでPDF処理を実装する

## システム構成

システム構成イメージです。

![kintoneAwsLambdaThinreportsPDF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/41324c1f-472f-6301-bf55-92e6572f6ed5.png)

[AWS Lambda](https://aws.amazon.com/jp/lambda/) を利用してPDF印刷を実装する手順です。

## 流れ

1. Thinreportsデザイナーで印刷レイアウトを作成
2. SDK(Thinreports Generator for Ruby) https://rubygems.org/gems/thinreports を利用して印刷プログラムを作成
3. 印刷レイアウトと印刷プログラムを AWS Lambda に登録
4. kintone に設置した JavaScript から Lambda を実行
5. kintone にPDFファイルがアップロードされる

順に説明していきます。

## 1. Thinreportsデザイナーで印刷レイアウトを作成

<img width="1050" alt="スクリーンショット 2019-10-27 1 19.31.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/fbe7f1e6-d1cf-1806-9b9a-2e92a23366b1.png">

下記リンクを参照。

* [クイックスタートガイド](http://www.thinreports.org/documentation/ja/getting-started/quickstart.html)
* [Thinreports Examples](https://github.com/thinreports/thinreports-examples/blob/master/README-ja.md)

## 2. SDK を利用して印刷プログラムを作成

### PDF生成

AWS Lambda にて Thinreports Generator を使って PDFファイルを生成します。

拙稿をご覧ください。

* [AWS Cloud9 に Ruby 2.5.0 と thinreports gem を インストールして Lambda で PDFを生成する方法](https://qiita.com/sy250f/items/aa0befe7b715de551f00)

```hello-world.rb
# coding: utf-8
require 'thinreports'
require 'json'
require 'aws-sdk-s3'
require './file_upload.rb'
require './file.rb'

def lambda_handler(event:, context:)
  app_id = nil
  record_id = nil
  field_code = 'file'
  begin
    print 'JSON.parse event.body => '
    pp JSON.parse(event['body'])
    event_body = JSON.parse(event['body'])
    record = event_body['record']
    app_id = event_body['appId']
    record_id = event_body['recordId']
    print 'record => '
    pp record
    puts '$id => ', record['$id']['value']
    # puts 'text => ', record["text"]["value"]
    puts 'title => ', record['title']['value']
  rescue => e
    puts "#{e.class}: #{e.message}"
    puts e.backtrace
  end

  begin
    # region = 'S3バケットのリージョン'
    # bucket_name = 'バケット名'
    key = 'put-hello-ruby.pdf'
    # S3 Backet Object init
    s3 = Aws::S3::Resource.new(region: region)
    # pp s3
    obj = s3.bucket(bucket_name).object(key)
    # Thinreports Object init
    report = Thinreports::Report.new layout: 'hello_world'
    # 1st page
    report.start_new_page
    report.page.item(:world).value(record["$id"]["value"])
    report.page.item(:thinreports).value(record["text"]["value"])
    report.page.item(:title).value(record["title"]["value"])
    # pp report
    obj.put(body: report.generate)
    
    # /tmpファイルに書き込み
    filename = '/tmp/put-hello-ruby-' + Time.new.localtime("+09:00").strftime("%Y%m%d%H%M%S") + '.pdf'
    puts filename
    File.open(filename, 'w+') { |f| f<< report.generate }
    puts `ls -la /tmp`
    
    # ファイルアップロード
    res = file_upload(filename)
    pp res
    # pp JSON.parse(res.body)
    file_key = res['fileKey']

    # 添付ファイルフィールド更新
    res_file = file_update(app_id, record_id, field_code, file_key)
    pp res_file
  rescue => e
    puts "#{e.class}: #{e.message}"
    puts e.backtrace
  end

  { statusCode: 200, body: JSON.generate('Hello Lambda! Thinreports for Ruby') }
end
```

### ファイルアップロード

Lambda から /tmp に作成した PDFファイルを、kintoneにアップロードします。

下記拙稿も参考にして下さい。

* [ファイルアップロード](https://qiita.com/sy250f/items/82f84904daf7601eeb08#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89-1)

kintoneの領域にファイルアップロードが成功した時に、ファイルキーが返却されます。

```file_upload.rb
require 'rubygems'
require 'bundler/setup'
require 'dotenv/load'
require 'net/http'
require 'uri'
require 'json'
require 'pp'

def file_upload(filename)
  puts __FILE__
  begin
    upload_file = File.open filename, "r"
    data = [["file", upload_file, {"filename" => filename, "content_type" => "application/pdf"}]]
    url = "https://#{ENV['SUBDOMAIN']}.cybozu.com/k/v1/file.json"
    uri = URI.parse(url)
    api_token = ENV['API_TOKEN']
    req = Net::HTTP::Post.new(uri.path)
    req['X-Cybozu-API-Token'] = api_token
    req['Content-Type'] = 'multipart/form-data'
    req.set_form(data, "multipart/form-data")
    pp req # <Net::HTTP::Post POST>
    res = ''
    Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
      res = http.request(req)
      res.each_header do |name, val|
        puts "name = #{name}, val = #{val}"
      end
      case res.code.to_i
      when 200
        pp JSON.parse(res.body) # {"fileKey"=>"b28a1ecc-e8dc-4a10-b030-7cf96b35b272"}
      else
        pp %Q(#{res.code} #{res.message})
        pp JSON.parse(res.body)
      end
    end
    upload_file.close
    JSON.parse(res.body)
  rescue => exception
    pp exception
    upload_file.close
    exception.message
  end
end
```

### アップロードしたファイルとレコードの関連付け

下記拙稿を参考にして下さい。

* [アップロードしたファイルとレコードの関連付け](https://qiita.com/sy250f/items/82f84904daf7601eeb08#%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%97%E3%81%9F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A8%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)

前段で取得したファイルキーを使って、レコードの添付ファイルフィールドを更新します。

```file.rb
require 'rubygems'
require 'bundler/setup'
require 'dotenv/load'
require 'net/http'
require 'uri'
require 'json'
require 'pp'

def file_update(app, id, field_code, file_key)
  puts __FILE__
  begin
    field_code = field_code || 'file'
    url = "https://#{ENV['SUBDOMAIN']}.cybozu.com/k/v1/record.json"
    uri = URI.parse(url)
    api_token = ENV['API_TOKEN']
    req = Net::HTTP::Put.new(uri.path)
    req['X-Cybozu-API-Token'] = api_token
    req['Content-Type'] = 'application/json'
  
    req.body = JSON.generate({
        "app" => app,
        "id" => id,
        "record" => {field_code => {"value" => [{"fileKey"=>file_key}]}}
      })
      Net::HTTP.start(uri.host, uri.port, :use_ssl => true) {|http|
      res = http.request(req)
      res.each_header do |name, val|
        puts "name=#{name}, val=#{val}"
      end
      case res.code.to_i
      when 200
        pp JSON.parse(res.body)
        JSON.parse(res.body)
      else
        pp %Q(#{res.code} #{res.message})
        pp JSON.parse(res.body)
        JSON.parse(res.body)
      end
    }
  rescue => exception
    pp exception
    exception.message
  end
end
```

## 3. 印刷レイアウトと印刷プログラムを AWS Lambda に登録

作成した プログラムファイル及びライブラリ、印刷レイアウトファイルをZip圧縮して Lambda にアップロードします。

下記拙稿を参考にして下さい。

* [AWS Cloud9 に Ruby 2.5.0 と thinreports gem を インストールして Lambda で PDFを生成する方法](https://qiita.com/sy250f/items/aa0befe7b715de551f00)


## 4. kintone に設置した JavaScript から Lambda を実行

下記拙稿を参考にして下さい。

* [AWS Lambda にPOSTしたkintoneのレコードからThinreportsを利用してPDFファイルを生成する](https://qiita.com/sy250f/items/0f2aaf17fd670bf82c84)

アプリに添付ファイルのフィールドを追加しています。

<img width="1220" alt="スクリーンショット 2019-10-27 10.26.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/a3aaf0b8-1870-e437-dd58-ec34eb806dd9.png">

<img width="324" alt="スクリーンショット 2019-10-27 10.29.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/6f0a1923-701c-8be9-e3fa-9a5f166ebd94.png">

## 5. kintone にPDFファイルがアップロードされる

成功すると添付ファイルフィールドにPDFファイルがアップロードされます。
詳細画面を再読み込みして下さい。

<img width="713" alt="スクリーンショット 2019-10-27 19.30.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/3f38823d-4c28-0329-b45d-3ad18bb22f2f.png">

<img width="950" alt="スクリーンショット 2019-10-27 19.30.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/6c6b5f3a-9080-9148-2d74-66b0d3972e3e.png">


# まとめ

AWS Lambdaを始めとするサーバーレスのサービスが一般的になり、フロントエンドとバックエンドを隔てる壁も随分と低く、曖昧になってきたと感じています。

フロントエンドのプログラマもバックエンドの世界を知る機会があると思いますし、バックエンドの方もフロントエンドを理解した上でバックエンドの処理を考える良い機会があるのではと感じています。

---

# 参考リンク

## Thinreports関連

* [Thinreports](http://www.thinreports.org/)
* [Thinreports Examples](https://github.com/thinreports/thinreports-examples/blob/master/README-ja.md)

## AWS関連

* [AWS Lambda](https://aws.amazon.com/jp/lambda/)
* [AWS SDK for Ruby Lambda.invoke](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Lambda/Client.html#invoke-instance_method)
* [AWS Lambda Ruby & Serverless Framework & FFmpeg によるアップロード動画からのサムネ画像生成](https://qiita.com/samuraikun/items/bf0936a5d3386c4914fa)
* [[AWS] API Gateway + Lambda for Ruby + S3 + Slack通知をSAMで構築](https://qiita.com/withelmo/items/9b8575cedc69f6af57e7)
* [AWS Cloud9からSAM Localをためしてみる](https://dev.classmethod.jp/cloud/aws/aws-cloud9-sam-local/)
* [AWS SDK SAM CLI](https://docs.aws.amazon.com/ja_jp/serverless-application-model/?id=docs_gateway)
* [AWS Serverless Application Model (SAM) コマンドラインインターフェイス。サーバレスアプリケーションをローカルで構築、テスト、デバッグする](https://aws.amazon.com/jp/blogs/news/aws-serverless-application-model-sam-command-line-interface-build-test-and-debug-serverless-apps-locally/)
* [AWS ドキュメント](https://docs.aws.amazon.com/index.html)
* [AWS Code Sample Catalog](https://docs.aws.amazon.com/ja_jp/code-samples/latest/catalog/welcome.html?id=docs_gateway)

## kintone関連

* [ファイルアップロード](https://developer.cybozu.io/hc/ja/articles/201941824)
* [レコードの更新（PUT）
](https://developer.cybozu.io/hc/ja/articles/201941784)
* [ファイルアップロードで必須となる3つの手順](https://developer.cybozu.io/hc/ja/articles/200724665-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A7%E5%BF%85%E9%A0%88%E3%81%A8%E3%81%AA%E3%82%8B3%E3%81%A4%E3%81%AE%E6%89%8B%E9%A0%86)

## Ruby関連

* [プログラミング言語 Ruby](https://www.ruby-lang.org/ja/)
* [Ruby ライブラリ](https://www.ruby-lang.org/ja/libraries/)