FlaskとSQLiteでバーコード付顧客管理簿をつくる

FlaskとSQLAlchemyとpython-barcodeをインストール

pip install Flask
pip install sqlalchemy
pip install python-barcode

ファイルの階層

customerbook-demo
|   customerFlask.py
|   customer.sqlite
|
+---static
|   +---css
|   |       skeleton.css
|   |       semantic.min.css
|   |
|   \---js
|           ajaxzip3.js
|           jquery-3.5.1.min.js
|           jquery.autoKana.js
|
\---templates
        check.html
        index.html
        resister.html

CSSフレームワーク

cssのことはよく分からないので、フレームワークのskeleton.cssSemantic UIを使います。

skeleton.cssのダウンロードは、こちら
Semantic UIのダウンロードは、こちら

運用環境

セキュリティに詳しくないので、ローカル環境で運用します。

ロリポップ!エックスサーバーで運用する場合は、以下の記事を参考にファイルを追加してください。

完成イメージ

入力フォーム

登録完了画面

sqlite3でデータベースを作成

以前つくった住所録は、SQLAlchemyでデータベースを作成しましたが、SQLAlchemyでsqlite_sequenceテーブルを作成する方法がわからなかったので、sqlite3でデータベースを作成します。

sqlite3の場合、INTEGER PRIMARY KEY AUTOINCREMENTを含むテーブルを作成すると、sqlite_sequenceが自動で作成されます。

idの番号でバーコードを作成するときに、桁数を揃えたいのでidの開始値を100,001に設定しています。

import sqlite3

db = 'customer.sqlite'
con = sqlite3.connect(db)
cur = con.cursor()

# customerテーブルがない場合、テーブルを作成
cur.execute('''create table if not exists customer(
    id integer primary key autoincrement,
    sei text,
    mei text,
    sei_kana text,
    mei_kana text,
    zip01 text,
    pref01 text,
    addr01 text,
    barcode text,
    datetime text)
    ''')

# autoincrementの開始値を100,001に設定
# ("テーブル名", 開始値-1)
cur.execute('insert into sqlite_sequence values("customer",100000)')

con.commit()
con.close()

カラムの設定

カラム名内容
idID(プライマリーキー、自動入力)
sei
mei
sei_kana姓のフリガナ
mei_kana名のフリガナ
zip01郵便番号
pref01都道府県名
addr01都道府県名以降の住所
barcodeバーコード(SVG形式)
datetime作成日

ソースコード

基本的には、以前つくった住所録にSQLAlchemyの【データベース作成】機能を削除、【日付表示】【バーコード作成】機能を追加し、登録完了画面に表示するだけです。

index.htmlとcheck.htmlに変更はありません。

customerFlask.py

SECRET_KEYは、duckduckgoで検索バーにpw 24 strongと入力し適当に生成しています。

バーコードは【code128】形式で作成します。どの形式でもよかったのですが、桁数指定がなく手持ちのバーコードリーダーで読み取りやすかったのがcode128でした。

python-barcodeの使い方は、関連記事を見てください。

ビジコムのバーコードリーダーは、消音することができ、読み取ると振動で教えてくれるので気に入っています。音で読み取りを知らせるタイプは、狭い部屋で使うと電子音がかなり耳障りです。

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, Text
import datetime
import barcode
import io
import re

app = Flask(__name__)

# SECRET KEYは適当(要変更)
app.config['SECRET_KEY'] = 'vyoU2(GqrMVXDN6QdncpQGxp'

# sqlite3で作成したデータベースを指定
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///customer.sqlite'

db = SQLAlchemy(app)

# SQLAlchemyでモデルを作成
class Customer(db.Model):
    __tablename__ = 'customer'

    id = db.Column(Integer, primary_key = True, autoincrement = True)
    sei = db.Column(Text)
    mei = db.Column(Text)
    sei_kana = db.Column(Text)
    mei_kana = db.Column(Text)
    zip01 = db.Column(Text)
    pref01 = db.Column(Text)
    addr01 = db.Column(Text)
    barcode = db.Column(Text)
    datetime = db.Column(Text)

# 入力フォームのname属性をリスト化
name_att = ['sei','mei','sei_kana','mei_kana','zip01','pref01','addr01']

@app.route('/')
def index():
    return render_template('index.html', name_att = name_att)

@app.route('/check', methods=['POST'])
def check():
    # index.htmlのフォームから受け取るデータ用リストを作成
    check_list = []

    # 受け取ったデータをリストに追加
    for att in name_att:
        check_list.append(request.form.get(att))

    return render_template('check.html', check_list = check_list, name_att = name_att)

@app.route('/resister', methods=['POST'])
def resister():
    # check.htmlのフォームから受け取るデータ用リストを作成
    resister_list = []

    # 受け取ったデータをリストに追加
    for att in name_att:
        resister_list.append(request.form.get(att))

    # データベースのカラムに受け取ったデータを代入   
    # datetimeは本日の日付を代入(yyyy-mm-dd)
    book = Customer(
        sei = resister_list[0], 
        mei = resister_list[1],
        sei_kana = resister_list[2],
        mei_kana = resister_list[3],
        zip01 = resister_list[4],
        pref01 = resister_list[5],
        addr01 = resister_list[6],
        datetime = datetime.date.today())
    
    # データベースへ登録
    db.session.add(book)

    # ID番号を取得するためにflushが必要
    db.session.flush()

    # 最後に登録されたデータのIDを取得
    for r in db.session.execute('select last_insert_rowid()'):
        lastid = r[0]

    # バーコードのタイプをcode128形式に
    CODE128 = barcode.get_barcode_class('code128')

    # 取得したIDを文字列に変換してバーコードを作成
    id_barcode = CODE128(str(lastid))

    # メモリ上でバイナリデータを扱うためにBytesIOを使用
    fp = io.BytesIO()

    # バーコード作成のオプションを辞書型で指定
    options = dict(module_width=0.8)

    # バーコードをSVG形式で作成
    id_barcode.write(fp, options)

    # 作成したバーコードをテキスト形式にdecode
    svgfile = fp.getvalue().decode('utf-8')
    
    # 正規表現で<svg>から</svg>の間を抽出
    m = re.search(r'<svg(.*)/svg>', svgfile, flags=re.DOTALL)
    barcode_svg = m.group()

    # 作成したバーコードのsvgタグの間だけをテキストでデータベースに登録
    book.barcode = barcode_svg

    # データベースに書込処理
    db.session.commit()
    db.session.close()

    # 最後に登録したIDと一致するカラムをすべて抽出
    customer_infos = db.session.query(Customer).filter(Customer.id == lastid).all()

    # resister.htmlに登録したデータを渡す
    return render_template('resister.html', customer_infos = customer_infos)

if __name__ == '__main__':
    app.run()

render template と jinja2 で表示

index.html

郵便番号から住所を自動入力するためにajaxzip3.jsを、名前のフリガナを自動入力するためにjquery.autoKana.jsを使用します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>住所録入力</title>
    <script src="./static/js/jquery-3.5.1.min.js"></script>
    <script src="./static/js/ajaxzip3.js"></script>
    <script src="./static/js/jquery.autoKana.js"></script>
    <script>
        $(document).ready(function(){
            $.fn.autoKana('input[name="{{name_att[0]}}"]', 'input[name="{{name_att[2]}}"]', {katakana:true});
            $.fn.autoKana('input[name="{{name_att[1]}}"]', 'input[name="{{name_att[3]}}"]', {katakana:true});
        });
    </script>
    <link rel="stylesheet" type="text/css" href="./static/css/skeleton.css">
</head>
<body>
    <form action="/check" method="POST">
        <label>名前</label>
        <input type="text"  name="{{name_att[0]}}" placeholder="姓">
        <input type="text"  name="{{name_att[1]}}" placeholder="名">
        <label>フリガナ</label>
        <input type="text" name="{{name_att[2]}}">
        <input type="text" name="{{name_att[3]}}">
        <label>郵便番号</label>
        <input type="text" name="{{name_att[4]}}" size="10" maxlength="8" onKeyUp="AjaxZip3.zip2addr(this,'','{{name_att[5]}}','{{name_att[6]}}');" placeholder="1234567">
        <label>都道府県</label>
        <input type="text" name="{{name_att[5]}}" size="20">
        <label>以降の住所</label>
        <input type="text" name="{{name_att[6]}}" size="40">
        <input class="button-primary" type="submit" value="確認">
    </form>
</body>
</html>

check.html

入力項目の確認画面は、jinja2のfor文を使って簡潔に表示しています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登録確認</title>
    <link rel="stylesheet" type="text/css" href="../static/css/skeleton.css">
</head>
<body>
    <form action="/resister" method="POST">
        {% for i in range(check_list | length) %}
            {% if i == 0 or i == 2 %}
                <input type="text" name="{{name_att[i]}}" value="{{check_list[i]}}" readonly>
            {% elif i == 6 %}
                <input type="text" name="{{name_att[i]}}" value="{{check_list[i]}}" readonly size="40"><br>
            {% else %}
                <input type="text" name="{{name_att[i]}}" value="{{check_list[i]}}" readonly><br>
            {% endif %}
        {% endfor %}

        <button type="button" onclick="history.back()">戻る</button>
        <input class="button-primary" type="submit" value="登録">
    </form>

</body>
</html>

resister.html

CSSはよく分からないので、フレームワークのSemanticUIを使っています。

jinja2で登録完了画面を表示するのに、苦労した所が2つあります。

  • データベースから抽出したデータの表示方法
  • SVG形式のバーコードをhtmlに埋め込む方法
jinja2でデータベースから抽出したデータの表示方法
{% for info in customer_infos %}
    {{ info.sei }}
{% endfor %}
  • for文を使い【.カラム名】で表示するカラムを指定します。
jinja2でSVG形式のバーコードをhtmlに埋め込む方法
{% autoescape false %}
    {{ info.barcode }}
{% endautoescape %}
  • jinja2では、タグの部分【<>】はエスケープ処理されるようです。
  • タグが含まれた部分をそのまま埋め込むには、autoescapeflaseにして囲みます。

jinja2の詳しい説明はこちら

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登録完了</title>
    <link rel="stylesheet" type="text/css" href="./static/css/semantic.min.css">
</head>
<body>
    <div class="ui text container" style="margin-top: 20px;">
        <h2 class="ui center aligned header">登録完了</h2>
        {% for info in customer_infos %}
            <table class="ui celled striped table">
                <tbody>
                    <tr>
                        <td>作成日</td>
                        <td colspan="2">{{ info.datetime }}</td>
                    </tr>
                    <tr>
                        <td>ID</td>
                        <td colspan="2" class="ui center aligned">
                            {% autoescape false %}
                                {{ info.barcode }}
                            {% endautoescape %}
                        </td>
                    </tr>
                    <tr>
                        <td>名前</td>
                        <td>{{ info.sei }}</td>
                        <td>{{ info.mei }}</td>
                    </tr>
                    <tr>
                        <td>フリガナ</td>
                        <td>{{ info.sei_kana }}</td>
                        <td>{{ info.mei_kana }}</td>
                    </tr>
                    <tr>
                        <td>郵便番号</td>
                        <td colspan="2">{{ info.zip01 }}</td>
                    </tr>
                    <tr>
                        <td>都道府県名</td>
                        <td colspan="2">{{ info.pref01 }}</td>
                    </tr>
                    <tr>
                        <td>以降の住所</td>
                        <td colspan="2">{{ info.addr01 }}</td>
                    </tr>
                </tbody>
            </table>
        {% endfor %}
    </div>  
</body>
</html>

設置デモ

https://dattesar.com/customer-demo/

  データベース機能は削除してあります。ボタンを押してもデータは保存されません