はじめに
はじめまして。このたびは記事を閲覧いただきありがとうございます。初投稿のため拙い部分も多いかと思いますが、よろしくお願いいたします。まず私の自己紹介もかねて、投稿しようと思った経緯を説明します。私は4月から大学三年生になる代の理系大学生です。大学生といえば2か月近くある長期の春休み。これを2年ほど無駄にしてきたことから、今年こそはと新たなスキルであるプログラミングを習得しようととあるオンラインスクールで学習をはじめました。主にPythonを中心にHTML,CSS,Javascriptなどを学習し、初めてのWebアプリ開発の表現の場としてこの記事を執筆しようと思った次第です。
アプリで解決したい社会課題
私が実装したWebアプリは収入支出管理および税額シミュレーションアプリです。近年、確定申告の時期になると阿鼻叫喚の声が聞こえてくるような現代では、収入支出の管理はもちろん、税額をある程度シミュレーションできた方がよいだろうと考えました。とはいっても初学者であるため、高度なシミュレーションは実装できなかったのでそこは改善の余地ありといった感じです。こうした方がよいなどありましたらコメントいただけると成長につながるためうれしいです。
アプリの機能説明
上の画像はアプリの一番上の部分です。こちらで後ほどの部分で追加する収入や支出の内容を確認することができます。
こちらの画像はこれまでの収入と支出、それから税額を確認することができます。
この画像のグラフではこれまでの収入、支出、税額概算を視覚的にわかりやすく示しています。
上の棒グラフは月別の収入や支出を反映しています。また下にあるフォームからデータを追加することができます。
上のフォームに収入と扶養家族数を入力すると税額をシミュレーションしてくれる機能も実装しました。
実行環境
・PC : Windows10
・開発環境 : Visual Studio Code
・言語 : HTML, Python
・ライブラリ : Flask
コード
app.py
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os
app = Flask(__name__)
# SQLiteのデータベースファイルを設定
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(BASE_DIR, 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# 収入・支出のデータモデル
class Entry(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.String(10), nullable=False)
amount = db.Column(db.Float, nullable=False)
category = db.Column(db.String(10), nullable=False)
memo = db.Column(db.String(100), nullable=True)
# データベースを作成
with app.app_context():
db.create_all()
# 税額計算用の関数
def calculate_tax(income, num_dependents):
# 扶養控除(仮に1人あたり38万円)
dependent_deduction = num_dependents * 380000
# 基本控除や社会保険料控除(仮の金額)
basic_deduction = 480000
social_insurance_deduction = 100000
medical_deduction = 50000
# 課税所得額の計算
total_deductions = basic_deduction + social_insurance_deduction + medical_deduction + dependent_deduction
taxable_income = income - total_deductions
if taxable_income <= 0:
return 0 # 課税所得が0以下の場合、税額は0
# 税額計算(税率)
TAX_BRACKETS = [
(1950000, 0.05), # ~195万円 5%
(3300000, 0.10), # ~330万円 10%
(6950000, 0.20), # ~695万円 20%
(9000000, 0.23), # ~900万円 23%
(18000000, 0.33), # ~1800万円 33%
(40000000, 0.40), # ~4000万円 40%
(float('inf'), 0.45) # 4000万円超 45%
]
tax = 0
for bracket, rate in TAX_BRACKETS:
if taxable_income > bracket:
tax += (min(taxable_income, bracket) * rate)
taxable_income -= bracket
else:
tax += taxable_income * rate
break
return tax
# ホーム画面
@app.route('/', methods=['GET', 'POST'])
def home():
# 収入・支出の集計
entries = Entry.query.all()
income_total = sum(entry.amount for entry in entries if entry.category == 'income')
expense_total = sum(entry.amount for entry in entries if entry.category == 'expense')
tax_estimate = (income_total - expense_total) * 0.1 # 税額概算(10%)
# 月別データの集計
monthly_data = {}
for entry in entries:
month = datetime.strptime(entry.date, '%Y-%m-%d').strftime('%Y-%m') # 年月形式
if month not in monthly_data:
monthly_data[month] = {'income': 0, 'expense': 0}
if entry.category == 'income':
monthly_data[month]['income'] += entry.amount
else:
monthly_data[month]['expense'] += entry.amount
return render_template('index.html', entries=entries, income_total=income_total, expense_total=expense_total, tax_estimate=tax_estimate, monthly_data=monthly_data)
# シミュレーション画面
@app.route('/simulation', methods=['GET', 'POST'])
def simulation():
tax_simulation = None # 初期化
if request.method == 'POST':
# ユーザーが入力した収入額と扶養家族数を取得
income = float(request.form['income'])
num_dependents = int(request.form['dependents'])
tax = calculate_tax(income, num_dependents) # 税額計算
# 税額シミュレーション結果を辞書にまとめる
tax_simulation = {
'income': income,
'dependents': num_dependents,
'tax': tax
}
return render_template('simulation.html', tax_simulation=tax_simulation)
# データ追加
@app.route('/add', methods=['POST'])
def add_entry():
date = request.form['date']
amount = float(request.form['amount'])
category = request.form['category']
memo = request.form['memo']
new_entry = Entry(date=date, amount=amount, category=category, memo=memo)
db.session.add(new_entry)
db.session.commit()
return redirect('/')
# データ削除
@app.route('/delete/<int:id>', methods=['POST'])
def delete_entry(id):
entry = Entry.query.get(id)
if entry:
db.session.delete(entry)
db.session.commit()
return redirect('/')
# 編集ページ
@app.route('/edit/<int:id>', methods=['GET'])
def edit_entry(id):
entry = Entry.query.get(id)
return render_template('edit.html', entry=entry)
# 編集更新
@app.route('/update/<int:id>', methods=['POST'])
def update_entry(id):
entry = Entry.query.get(id)
if entry:
entry.date = request.form['date']
entry.amount = float(request.form['amount'])
entry.category = request.form['category']
entry.memo = request.form['memo']
db.session.commit()
return redirect('/')
# 月別レポート
@app.route('/monthly')
def monthly_report():
entries = Entry.query.all()
# 月ごとの収入と支出を集計
monthly_data = {}
for entry in entries:
month = datetime.strptime(entry.date, '%Y-%m-%d').strftime('%Y-%m') # 年月形式
if month not in monthly_data:
monthly_data[month] = {'income': 0, 'expense': 0}
if entry.category == 'income':
monthly_data[month]['income'] += entry.amount
else:
monthly_data[month]['expense'] += entry.amount
return render_template('monthly_report.html', monthly_data=monthly_data)
# Flaskアプリの実行
if __name__ == '__main__':
app.run(debug=True)
ホームページのHTML(index.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>収支管理</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- Chart.jsの追加 -->
</head>
<body>
<div class="container mt-5">
<header class="text-center mb-4">
<h1>収支管理</h1>
<nav>
<a href="/" class="btn btn-outline-primary">ホーム</a>
<a href="/simulation" class="btn btn-outline-secondary">税額シミュレーション</a>
</nav>
</header>
<!-- 収入・支出リスト -->
<h3>収入・支出</h3>
<table class="table">
<thead>
<tr>
<th>日付</th>
<th>金額</th>
<th>カテゴリ</th>
<th>メモ</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr>
<td>{{ entry.date }}</td>
<td>¥{{ entry.amount }}</td>
<td>{{ entry.category }}</td>
<td>{{ entry.memo }}</td>
<td>
<a href="/edit/{{ entry.id }}" class="btn btn-warning btn-sm">編集</a>
<form action="/delete/{{ entry.id }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-danger btn-sm">削除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 合計部分を強調 -->
<div class="mt-4" style="background-color: #f8f9fa; padding: 20px; border-radius: 8px;">
<h3 class="text-center mb-3">収支合計</h3>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card p-3" style="background-color: #d1ecf1; border-color: #bee5eb;">
<h4 class="text-center">収入合計</h4>
<h5 class="text-center" style="font-size: 24px; font-weight: bold;">¥{{ income_total }}</h5>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card p-3" style="background-color: #f8d7da; border-color: #f5c6cb;">
<h4 class="text-center">支出合計</h4>
<h5 class="text-center" style="font-size: 24px; font-weight: bold;">¥{{ expense_total }}</h5>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card p-3" style="background-color: #fff3cd; border-color: #ffeeba;">
<h4 class="text-center">税額概算</h4>
<h5 class="text-center" style="font-size: 24px; font-weight: bold;">¥{{ tax_estimate }}</h5>
</div>
</div>
</div>
<!-- 円グラフを追加 -->
<h4 class="text-center mt-4">収入・支出 比率</h4>
<div class="row">
<div class="col-md-6 offset-md-3">
<canvas id="pieChart"></canvas> <!-- 円グラフのキャンバス -->
</div>
</div>
<script>
const income = {{ income_total }};
const expense = {{ expense_total }};
const taxEstimate = {{ tax_estimate }};
const ctx = document.getElementById('pieChart').getContext('2d');
const pieChart = new Chart(ctx, {
type: 'pie',
data: {
labels: ['収入', '支出', '税額概算'],
datasets: [{
data: [income, expense, taxEstimate],
backgroundColor: ['#36a2eb', '#ff6384', '#ffcd56'],
hoverBackgroundColor: ['#36a2eb', '#ff6384', '#ffcd56']
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
labels: {
font: {
size: 16
}
}
}
}
}
});
</script>
</div>
<!-- 月別レポートのグラフ -->
<h3 class="mt-4">月別レポート</h3>
<div class="row">
<div class="col-md-12">
<canvas id="monthlyChart"></canvas> <!-- 月別レポートのグラフ用キャンバス -->
</div>
</div>
<script>
const monthlyData = {{ monthly_data | tojson }}; // Pythonから月別データを渡す
const labels = Object.keys(monthlyData);
const incomeData = labels.map(month => monthlyData[month].income);
const expenseData = labels.map(month => monthlyData[month].expense);
// Chart.jsの設定
const ctx2 = document.getElementById('monthlyChart').getContext('2d');
const chart = new Chart(ctx2, {
type: 'bar', // 棒グラフ
data: {
labels: labels,
datasets: [{
label: '収入',
data: incomeData,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}, {
label: '支出',
data: expenseData,
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
label: function(tooltipItem) {
return '¥' + tooltipItem.raw.toLocaleString(); // 通貨形式に変換
}
}
}
}
}
});
</script>
<div class="mt-4">
<h3>データ追加</h3>
<form method="POST" action="/add">
<div class="mb-3">
<label for="date" class="form-label">日付</label>
<input type="date" id="date" name="date" class="form-control" required>
</div>
<div class="mb-3">
<label for="amount" class="form-label">金額</label>
<input type="number" id="amount" name="amount" class="form-control" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">カテゴリ</label>
<select id="category" name="category" class="form-select" required>
<option value="income">収入</option>
<option value="expense">支出</option>
</select>
</div>
<div class="mb-3">
<label for="memo" class="form-label">メモ</label>
<input type="text" id="memo" name="memo" class="form-control">
</div>
<button type="submit" class="btn btn-primary">追加</button>
</form>
</div>
</div>
</body>
</html>
編集画面のHTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>データの編集</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<header class="text-center mb-4">
<h1>データの編集</h1>
</header>
<form action="/update/{{ entry.id }}" method="post">
<div class="mb-3">
<label for="date" class="form-label">日付</label>
<input type="date" id="date" name="date" class="form-control" value="{{ entry.date }}" required>
</div>
<div class="mb-3">
<label for="amount" class="form-label">金額</label>
<input type="number" id="amount" name="amount" class="form-control" value="{{ entry.amount }}" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">種類</label>
<select id="category" name="category" class="form-select">
<option value="income" {% if entry.category == 'income' %}selected{% endif %}>収入</option>
<option value="expense" {% if entry.category == 'expense' %}selected{% endif %}>支出</option>
</select>
</div>
<div class="mb-3">
<label for="memo" class="form-label">メモ</label>
<input type="text" id="memo" name="memo" class="form-control" value="{{ entry.memo }}">
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">更新</button>
<a href="/" class="btn btn-secondary">戻る</a>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
月別収支レポートのHTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月別収支レポート</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- Chart.js を読み込む -->
</head>
<body>
<div class="container mt-5">
<header class="text-center mb-4">
<h1>月別収支レポート</h1>
</header>
<!-- グラフ表示エリア -->
<canvas id="monthlyChart"></canvas>
<!-- 月別収支表 -->
<table class="table table-bordered mt-4">
<thead class="table-dark">
<tr>
<th>年月</th>
<th>収入合計 (¥)</th>
<th>支出合計 (¥)</th>
<th>差額 (¥)</th>
</tr>
</thead>
<tbody>
{% for month, data in monthly_data.items() %}
<tr>
<td>{{ month }}</td>
<td class="text-success">¥{{ data.income }}</td>
<td class="text-danger">¥{{ data.expense }}</td>
<td class="{% if data.income - data.expense >= 0 %}text-primary{% else %}text-warning{% endif %}">
¥{{ data.income - data.expense }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="text-center mt-4">
<a href="/" class="btn btn-secondary">戻る</a>
</div>
</div>
<script>
// Flaskからデータを受け取る
var labels = {{ monthly_data.keys() | list | tojson }};
var incomeData = {{ monthly_data.values() | map(attribute="income") | list | tojson }};
var expenseData = {{ monthly_data.values() | map(attribute="expense") | list | tojson }};
// Chart.js でグラフを描画
var ctx = document.getElementById('monthlyChart').getContext('2d');
var monthlyChart = new Chart(ctx, {
type: 'bar', // 棒グラフ
data: {
labels: labels,
datasets: [
{
label: '収入',
data: incomeData,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
},
{
label: '支出',
data: expenseData,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true }
}
}
});
</script>
</body>
</html>
シミュレーション画面のHTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>税額シミュレーション</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<header class="text-center mb-4">
<h1>税額シミュレーション</h1>
<nav>
<a href="/" class="btn btn-outline-primary">ホーム</a> <!-- ホームへ戻るボタン -->
</nav>
</header>
{% if tax_simulation %}
<div class="alert alert-info mt-4">
<h3>シミュレーション結果</h3>
<p>収入: ¥{{ tax_simulation.income }}</p>
<p>扶養家族数: {{ tax_simulation.dependents }}</p>
<h4>予想税額: ¥{{ tax_simulation.tax }}</h4>
</div>
{% endif %}
<div class="mt-4">
<h3>シミュレーション</h3>
<form method="POST" action="/simulation">
<div class="mb-3">
<label for="income" class="form-label">収入</label>
<input type="number" id="income" name="income" class="form-control" required>
</div>
<div class="mb-3">
<label for="dependents" class="form-label">扶養家族数</label>
<input type="number" id="dependents" name="dependents" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">シミュレーション実行</button>
</form>
</div>
</div>
</body>
</html>
まとめ
初めてのアプリ開発ということでChatGPTなどの生成AIに頼ってしまったり、データ収集を国税庁の公開しているデータで行おうとしたものの税に対する知識の無さやデータ収集の基礎の抜けから断念したり、まだまだ力の無さを感じました。しかし一からこういう形にできたのはある種の達成感を感じます。コードやデータ収集のコツ、追加機能のおすすめなどありましたら、気軽にコメントお願いいたします。