やりたいこと
kintone(キントーン)に登録した顧客情報から、ご契約サービスの利用開始案内通知書を作りたいです。
顧客情報には、顧客コードと契約種類で決まる一意のURL、各サービスのご契約状況フラグが含まれています。
通知書のイメージはこんな感じで、URLから生成するQRコードの画像を帳票に埋め込む必要がありました。(会社名やURLは架空のものです)
kintone は、同じローコードツールの Salesforce に比べると(標準では)帳票系に弱く、実現にはプラグインや外部サービスを頼ることになりそうです。
今回は自前でやりたかったので、帳票作成サーバを立ててみました。
サーバ構成
- Ubuntu 20.04 (Amazon EC2)
- Apache 2.4
- Python 3.8
使用したオープンソース
APIサーバの構築
kintone もしくは人を通して、帳票作成リクエストを受けつけるのがAPIサーバの役割です。
Python実行環境(WindowsやMac、Linuxなど)から帳票作成スクリプトを起動するなら不要です。
各種インストール
Apache2 / Python3 は導入済みとします。
Pythonで書かれたWebアプリケーションをWebサーバ(Apache2)と接続するために、WSGI(Web Server Gateway Interface)を導入します。
apt update
apt -y upgrade
apt install apache2-dev
apt install libapache2-mod-wsgi-py3
pip3 install mod_wsgi
依存関係でエラーになったら、
pip3 uninstall importlib_metadata
pip3 install importlib_metadata --force-reinstall
で強制的に再インストールしてみて下さい。
最後にサードパーティのライブラリを入れます。
Flask はシンプルで軽量な、Pythonで動くWebアプリケーションフレームワークで、プロトタイプ開発などでよく活用されています。
よく比較されるDjango(ジャンゴ)は、大規模かつ複雑なアプリケーション開発に適したフルスタックフレームワーク、という棲み分けです。
pip3 install flask, reportlab, qrcode
環境設定
Flask の設定です。
mkdir /var/www/flask
chown -R www-data:www-data flask
import sys
sys.path.insert(0, '/var/www/flask/qrapp')
from app import app as application
テスト用のサンプルアプリケーションです。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Apache2 と接続するための設定をします。
マルチバイト文字を含むファイル名などを作成すると UnicodeEncodeError: 'ascii' codec can't encode characters in position ...
になる場合、locale=ja_JP.UTF-8
も必要です。
WSGIDaemonProcess qrapp user=www-data group=www-data threads=5 locale=ja_JP.UTF-8
WSGIScriptAlias /qrapp /var/www/flask/qrapp/wsgi.py
<Directory /var/www/flask/qrapp>
WSGIProcessGroup qrapp
WSGIApplicationGroup %{GLOBAL}
AllowOverride All
Require all granted
</Directory>
Apache2 を再起動し、https://example.com/qrapp
で Hello World!
と表示されたらOK
systemctl restart apache2
実行してみよう
まず、完動するモジュールを先に示します。
""" Generate PDF files guiding companies to product URLs with QR codes """
__author__ = "MindWood"
__version__ = "1.0.0"
import os
import datetime
import json
import tempfile
import zipfile
import requests
import qrcode
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, portrait
from reportlab.lib.units import mm
from reportlab.platypus import Table, TableStyle
CONFIG_PATH = 'config.json'
API_URL = 'https://foo.cybozu.com/k/guest/99/v1/records.json'
APP_ID = 1234
FONT_NAME = "HeiseiKakuGo-W5"
class PdfQrGenerator:
""" Class to generate PDF files with QR codes and archive them into a ZIP file """
def __init__(self):
""" Load the API token from a JSON configuration file """
if not os.path.exists(CONFIG_PATH):
raise FileNotFoundError(f"Configuration file '{CONFIG_PATH}' not found.")
with open(CONFIG_PATH, 'r') as config_file:
config = json.load(config_file)
if 'api_token' not in config:
raise KeyError("API token not found in config.json.")
PdfQrGenerator.__api_token = config['api_token']
pdfmetrics.registerFont(UnicodeCIDFont(FONT_NAME))
def generate(self, zip_file):
""" Generate a zip file to store the pdf files """
self.zip_file = zip_file
with tempfile.TemporaryDirectory() as tmpd:
self.tmp_dir = tmpd
self._fetch_kintone_record()
self._create_archive()
def _fetch_kintone_record(self):
""" Use kintone api to get properties of all registered companies """
headers = {
'X-Cybozu-API-Token': PdfQrGenerator.__api_token,
}
params = {
'app': APP_ID,
}
response = requests.get(API_URL, headers=headers, params=params)
response.raise_for_status()
records = response.json().get('records', [])
for record in records:
values = {key: value['value'] for key, value in record.items()}
self._generate_pdf_file(values)
def _generate_pdf_file(self, values):
""" Create a PDF file of the target company and embed the QR code image """
customer_name = f"【{values['顧客コード']}】{values['会社名']}様"
file_path = os.path.join(self.tmp_dir, f"{customer_name}_利用開始案内通知書.pdf")
# PDF File Initialization
cc = canvas.Canvas(file_path, pagesize=portrait(A4), bottomup=False)
width, height = A4
cc.setTitle("利用開始案内通知書")
# Header
cc.setFont(FONT_NAME, 9)
cc.drawRightString(width - 10*mm, 10*mm, "{:%Y年%-m月%-d日} 発行".format(datetime.datetime.now()))
cc.setFont(FONT_NAME, 12)
cc.drawCentredString(width / 2, 30*mm, f"{customer_name} 利用開始案内通知書")
# Footer
cc.setFont(FONT_NAME, 9)
cc.drawRightString(width - 10*mm, height - 16*mm, "MindWood Inc.")
# Prepare QR Code generator
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M)
def _insert_table(product_name, field_name):
""" Inner function for outputting tables """
url = values[field_name]
data = [
['契約名', product_name],
['QRコード', ],
['URL', url],
]
styles = [
('FONT', (0, 0), (-1, -1), FONT_NAME, 12),
('FONT', (1, 2), (1, 2), FONT_NAME, 7),
('BOX', (0, 0), (-1, -1), 1, colors.black),
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.black),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
]
table = Table(data, (25*mm, 170*mm), (8*mm, 40*mm, 8*mm), TableStyle(styles))
table.wrapOn(cc, 8*mm, pos*mm)
table.drawOn(cc, 8*mm, pos*mm)
qr.clear()
qr.add_data(url)
qr.make(fit=True)
cc.drawInlineImage(qr.make_image(), 100*mm, pos*mm - 10, 100, 100)
contracts = [
{'flag': '契約フラグ1', 'name': '利用サービス名1', 'url': '契約1 URL'},
{'flag': '契約フラグ2', 'name': '利用サービス名2', 'url': '契約2 URL'},
{'flag': '契約フラグ3', 'name': '利用サービス名3', 'url': '契約3 URL'},
]
pos = 100 # Starting vertical position
for contract in contracts:
if values[contract['flag']] is not None:
_insert_table(values[contract['name']], values[contract['url']])
pos += 80 # Increment position for next table
# Save the PDF file
cc.save()
def _create_archive(self):
""" Create zip archive file """
with zipfile.ZipFile(self.zip_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file in os.listdir(self.tmp_dir):
file_path = os.path.join(self.tmp_dir, file)
if os.path.isfile(file_path):
zipf.write(file_path, file)
kintone のAPIトークンは設定ファイルに記述します。
設定ファイルは.gitignore
に追加します。
{
"api_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
メソッドの説明
__init__
コンストラクタです。
config.json
からAPIトークンを取得します。
generate
引数で受け取ったZIPファイルにPDFファイルを生成します。
一時ディレクトリ作成し、privateメソッドを呼びます。
_fetch_kintone_record
kintone API の「複数のレコードを取得する」を使って顧客情報を取得します。
取得できたら、顧客ごとに、次のメソッドに渡してPDFファイルの作成を指示します。
_generate_pdf_file
受け取った顧客情報を基にPDFファイルを生成します。
契約フラグをみて、契約中なら、その契約名、URL、URLから生成したQRコードからなる表を作成し、PDFに挿入します。
PDF作成ライブラリ ReportLab の使い方は、Qiitaを始め各サイトで解説されており、情報量は豊富です。
Canvasを作成するときにbottomup=False
を指定しているので原点は左上になることに注意して下さい。
_create_archive
複数のPDFファイルをZIP形式に圧縮します。
モジュールの使い方
generateメソッドにZIPファイルを指定するだけです。
端末から実行するには
import os
import pdf_qr_generator
zip_file = os.path.join(os.getcwd(), "foo.zip")
pq = pdf_qr_generator.PdfQrGenerator()
pq.generate(zip_file)
Webから実行するには
Flask のファイルを2つ追加します。
import datetime
import tempfile
from flask import Flask, render_template, request, send_file
import pdf_qr_generator
app = Flask(__name__)
@app.route('/')
def main():
return render_template("main.html")
@app.route('/submit', methods=['POST'])
def submit():
if request.form['password'] != "hogehoge":
return render_template("main.html", message="パスワードが違います")
with tempfile.NamedTemporaryFile(delete=True) as tmpf:
try:
pq = pdf_qr_generator.PdfQrGenerator()
pq.generate(tmpf.name)
return send_file(
tmpf.name,
as_attachment=True,
download_name="pdf_files-{:%Y%m%d-%H%M%S}.zip".format(datetime.datetime.now()),
mimetype="application/zip"
)
except Exception as e:
return render_template("main.html", message=f"内部でエラーが発生しました: {e}")
if __name__ == '__main__':
app.run()
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パスワードを入力して下さい</title>
</head>
<body>
<form action="/qrapp/submit" method="POST">
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required autofocus>
<button type="submit">ダウンロード</button>
</form>
<p style="color:#f00">{{ message }}</p>
</body>
</html>
パスワードを入力するだけのシンプルなフォームです。
パスワードが一致していれば、ZIPファイル(複数のPDFファイルを圧縮したもの)がダウンロードされます。