2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[初心者向け]Python×Tkinterを使って残業時間、残業代を自動で計算、保存する残業管理ツール(GUIアプリ)を作成した

2
Posted at

はじめに

私は2025年12月からPythonの学習を始めています。

今回は主にPython、Tkinterを使用して残業代や残業時間を自動で計算するアプリを作成しました。正直このアウトプットはExcelで全て事足りるのですが、学習の一環Excel操作やパソコンが苦手な方でも使えるようにと思い、実用的なデスクトップアプリを作成しました。

1.作成に至った経緯

私は現在、地方の公共交通機関に勤めています。地方企業は人員不足により残業が増えている傾向にあります。そこで、社員個人で残業時間や残業代を容易に把握できるようなツールがあれば便利になるのではと思いました。

少しコードを改良すれば、勤怠管理や見積書の計算、副業の収支管理等、様々なものに応用できるので、汎用性も含めて作成をしました。

2.アウトプットの内容

2-1.想定する利用方法

今回は公共交通機関での勤務を想定して作成しています。我が社の残業(早出)は勤務番号(早番や泊まり等)、若しくは本勤務終了後(前)、数回乗務する乗継ぎの2パターンがあります。

本記事ではこれらをTkinterで残業内容を選択して自動でExcelに保存していく仕組みについてまとめます。最後にPyinstallerでexe化してワンクリックでGUIアプリが起動できるようにします。

完成イメージ

  • 基本給と手当を入力
  • 反映を押下
  • 残業代(/時間)が自動計算、入力される
  • 勤務番号 or 乗継情報を選択
  • 登録ボタンを押下
  • 合計残業代が自動計算され、Excelに記録される

[GUIアプリイメージ]

スクリーンショット (10).png

[Excelの保存イメージ]

スクリーンショット (15).png

2-2.アプリのターゲット層

 冒頭でも書きましたが、パソコンやExcel操作が苦手な方に向けて作成しました。そのため中身はなるべく簡潔に、誰が見ても分かりやすくを意識しています。

3.使用するライブラリ

使用目的 ライブラリ バージョン
GUIアプリ Tkinter 8.6
メッセージ表示 Tkinter-Messagebox 8.6
日付表示 datetime Python 3.13.9
データ保存 json 3.2.1
ファイルの存在確認 os Python 3.13.9
Excel自動編集 openpyxl 3.1.5
exeファイル化 Pyinstaller 6.18.0

4.実装コード

4-1.ライブラリのインポート

今回使用するライブラリをインストールします。

library_import
import tkinter as tk
from tkinter import messagebox
from datetime import datetime
import json
import os
import openpyxl

4-2. 初期設定

setup
root = tk.Tk()
root.title('残業管理ツール')
root.geometry('600x500+500+100')
root.resizable(0, 0)

today = datetime.now().strftime('%m/%d')
filename = '残業代管理シート.xlsx'

4-3. 基本給・手当の保存(json)

salary・allowance
def save_salary():
    salary_data = {
        'salary': salary_entry.get(),
        'allowance': allowance_entry.get()
    }

    with open('data_salary.json', 'w') as f:
        json.dump(salary_data, f)

jsonを使った理由

  • アプリを閉じてもデータを残したい
  • データが軽く扱いやすい
    :ok_hand: 設定情報の保存に便利

4-3.残業代の計算

今回は1時間当たりの残業代を
(基本給+中休手当) x 0.00768
で算出します。

overtime_rate
def calculate_overtime_rate():
    salary = float(salary_entry.get())
    allowance = float(allowance_entry.get())
    overtime_rate = (salary + allowance) * 0.00768

4-4. jsonファイルの処理

初回登録情報がない場合(反映ボタンの処理)

save_salary
def save_salary():
    with open('data_salary.json','w') as f:
        json.dump({
            'salary': salary_entry.get(),
            'allowance': allowance_entry.get()
        }, f)
    calculate_overtime_rate()
  • data_salary.jsonが存在しない場合に新しく作成し、入力した基本給と手当の情報を保存

2回目以降のjsonファイル読み込み処理

load_salary
def load_salary():
    if os.path.exists('data_salary.json'):
        with open('data_salary.json', 'r') as f:
            data = json.load(f)
            salary_entry.insert(0, data["salary"])
            allowance_entry.insert(0, data["allowance"])
        calculate_overtime_rate()
  • jsonファイルから基本給と手当を自動入力し、残業代を計算

4-5. 入力情報から残業代の計算処理

[処理の順序]
1.入力チェック
2.勤務 or 乗継の判定
👉今回は勤務番号と乗継は同時に選択しないという過程で行います。そのため両方選択した際、もしくは両方未選択の際にエラーメッセージを表示するようにしました。

[エラーメッセージイメージ]

3.残業代計算
4.Excelに保存
5.画面に結果表示
👉コードではコメントアウトで区切っています

register_overtime
def register_overtime():
    overtime_rate = float(overtime_rate_entry.get())
    overtime_rate_minutes = overtime_rate / 60 #残業代/分

 # ==================================================
# 各リストボックスから選択した情報を取得
# ==================================================
    selected_work_index = work_number_listbox.curselection()
    selected_transit1_index = transit1_listbox.curselection()
    selected_transit2_index = transit2_listbox.curselection()
    transit1_count_index = transit1_count_listbox.curselection()
    transit1_count = transit1_count_listbox.get(transit1_count_index[0])
    transit2_count_index = transit2_count_listbox.curselection()
    transit2_count = transit2_count_listbox.get(transit2_count_index[0])

# ==================================================
# 誤った選択をした際にメッセージを表示
# ==================================================
    if selected_work_index == (0,) and selected_transit1_index == (0,):
        messagebox.showerror("エラー", "選択がされていません", parent=root)
        return
    elif selected_work_index != (0,) and selected_transit1_index != (0,):
        messagebox.showerror("エラー", "どちらかのみ選択してください", parent=root)
        return

# ==================================================
# 選択、未選択の勤務から表示の仕方、残業代の計算処理
# ==================================================
    total_work_amount = 0
    total_transit_amount = 0

    if selected_work_index != (0,):
        work_key = work_number_listbox.get(selected_work_index[0])
        work_rate = float(work_number_rates.get(work_key))
        total_work_amount = overtime_rate_minutes * work_rate
    else:
      #未選択時、Excelの表示を空にする
        work_key = '' 
      #Error防止
        work_rate = 0
        
    if selected_transit1_index != (0,):
        transit1_key = transit1_listbox.get(selected_transit1_index[0])
        transit1_rate = float(transit1_rates.get(transit1_key)) 
        
        if selected_transit2_index !=(0,):
            transit2_key = transit2_listbox.get(selected_transit2_index[0])
            transit2_rate = float(transit2_rates.get(transit2_key))
        else:
            transit2_key = ''
            transit2_rate = 0
        total_transit_amount = (
            overtime_rate_minutes * transit1_rate * transit1_count +
            overtime_rate_minutes * transit2_rate * transit2_count
        )
    else:
        transit1_key = ''
        transit2_key = ''

# ==================================================
# 選択された勤務をアクティブにする
# ==================================================
    if total_work_amount == 0:
        active_total = total_transit_amount
        active_rate = transit1_rate * transit1_count + transit2_rate * transit2_count
        
    else:
        active_total = total_work_amount
        active_rate = work_rate

    #未選択時、Excelの表示を空にする
    if transit1_key == '':
        transit1_count = ''

    if transit2_key == '':
        transit2_count = ''

# ==================================================
# Excelファイル読み込み or 新規Excelファイルを作成
# ==================================================
    if os.path.exists(filename):
        wb = openpyxl.load_workbook(filename)
    else:
        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "残業管理"
        ws.append(['日付', '勤務番号', '乗継1', '回数1','乗継2','回数2','残業時間','残業代','累計残業時間','累計残業代'])
        
        wb.save(filename)
        
    ws=wb.worksheets[0]

    column_width = {'A':6.4,'B':8.4,'C':6.5,'D':5.8,'E':6.5,'F':5.8,'G':10.3,'H':8.7,'I':13,'J':12}

    for col, width in column_width.items():
        ws.column_dimensions[col].width = width

    #入力情報をExcelへ記載
    input_row=ws.max_row+1
    ws.cell(row=input_row, column=1).value= today_entry.get()
    ws.cell(row=input_row, column=2).value= work_key
    ws.cell(row=input_row, column=3).value= transit1_key
    ws.cell(row=input_row, column=4).value= transit1_count
    ws.cell(row=input_row, column=5).value= transit2_key
    ws.cell(row=input_row, column=6).value= transit2_count
    ws.cell(row=input_row, column=7).value = active_rate / (24 * 60)
    ws.cell(row=input_row, column=7).number_format = '[h]"時間"mm"分"'
    ws.cell(row=input_row, column=8).value= active_total
    ws.cell(row=input_row, column=8).number_format = '#,##0"円"'
    ws.cell(row=2, column=9).value= '=SUM(G:G)'
    ws.cell(row=2, column=9).number_format = '[h]"時間"mm"分"'
    ws.cell(row=2, column=10).value= '=SUM(H:H)'
    ws.cell(row=2, column=10).number_format= '#,##0"円"'
    
    wb.save(filename)

    #本日の残業代に表示
    read_only_text.configure(state='normal')
    read_only_text.delete(1.0, tk.END)
    read_only_text.insert(tk.END, f'{int(active_total):,}円')
    read_only_text.configure(state='disabled')

4-6. 画面パーツ(Tkinter)

parts
# 今日の日付
today_label = tk.Label(root, text='今日の日付')
today_label.place(x=15, y=5)

today_entry = tk.Entry(root)
today_entry.insert(0, today)
today_entry.place(x=20, y=35, width=55)

# 基本給
salary_label = tk.Label(root, text='基本給を入力')
salary_label.place(x=20, y=100)

salary_entry = tk.Entry(root)
salary_entry.place(x=27, y=120, width=55)

# 中休手当
allowance_label = tk.Label(root, text = '中休手当を入力')
allowance_label.place(x=20, y=150)

allowance_entry = tk.Entry(root)
allowance_entry.place(x=27, y=170, width=55)

# 残業代
overtime_label = tk.Label(root, text='残業代(/時間)')
overtime_label.place(x=25, y=250)

overtime_rate_entry = tk.Entry(root)
overtime_rate_entry.place(x=27, y=270, width=55)

# 勤務番号
work_number_label = tk.Label(root, text='<勤務番号>')
work_number_label.place(x=195, y=80)

work_number_rates = {'--未選択--':0, '早番1':120, '早番2':150, '早番3':180,'遅番1':65,'遅番2':95,'遅番3':115,'泊まり':290}
work_number_var = tk.StringVar(value=list(work_number_rates))
work_number_listbox = tk.Listbox(
    root, height=10, width=10,
    listvariable=work_number_var, exportselection=False
)
work_number_listbox.place(x=200, y=100)
work_number_listbox.selection_set(0)

# 乗継1
transit1_label = tk.Label(root, text='<乗継ぎ ・ 回数>')
transit1_label.place(x=300, y=80)

transit1_rates = {'--未選択--': 0, '新宿': 55, '渋谷': 40, '池袋': 65, '品川':70}
transit1_var = tk.StringVar(value=list(transit1_rates))
transit1_listbox = tk.Listbox(
    root, height=5, width=10,
    listvariable=transit1_var, exportselection=False
)
transit1_listbox.place(x=300, y=100)
transit1_listbox.selection_set(0)

transit1_count_values = (1,2,3,4,5)
transit1_count_var = tk.StringVar(value=transit1_count_values)
transit1_count_listbox = tk.Listbox(
    root, height=5, width=5,
    listvariable=transit1_count_var, exportselection=False
)
transit1_count_listbox.place(x=370, y=100)
transit1_count_listbox.selection_set(0)

# 乗継2
transit2_label = tk.Label(root, text='※乗継ぎが複数の場合選択')
transit2_label.place(x=280, y=240)

transit2_rates = {'--未選択--': 0, '新宿': 55, '渋谷': 40, '池袋': 65, '品川':70}
transit2_var = tk.StringVar(value=list(transit2_rates))
transit2_listbox = tk.Listbox(
    root, height=5, width=10,
    listvariable=transit2_var, exportselection=False
)
transit2_listbox.place(x=300, y=260)
transit2_listbox.selection_set(0)

transit2_count_values = (1,2,3,4,5)
transit2_count_var = tk.StringVar(value=transit2_count_values)
transit2_count_listbox = tk.Listbox(
    root, height=5, width=5,
    listvariable=transit2_count_var, exportselection=False
)
transit2_count_listbox.place(x=370, y=260)
transit2_count_listbox.selection_set(0)

# 反映ボタン
button_reflect = tk.Button(root, text='反映', command=save_salary)
button_reflect.place(x=33, y=200)

# 登録ボタン
button_register = tk.Button(root, text='登録', command=register_overtime)
button_register.place(x=300, y=400, width=55)


# 合計残業代
read_only_label = tk.Label(root, text='<本日の残業代>')
read_only_label.place(x=485, y=280)
read_only_text = tk.Text(root)
read_only_text.insert(1.0, '')
read_only_text.configure(state='disabled')
read_only_text.place(x=500, y=300, height=20, width=60)
  • ratesのvalueには残業代/分を入力
  • 勤務番号や乗継は(仮)です

アプリ起動

load
load_salary()
root.mainloop()
  • load_salary()→前回の設定を復元

4-7.Pyinstallerでexeファイル化

  • 最後にpython環境が整っていないパソコンでも利用できるようにコマンドプロンプトでexeファイル化
cd %USERPROFILE%\Desktop
pyinstaller overtime.py --onefile --clean --collect-all openpyxl --noconsole
  • 無事にデスクトップへアプリを作成することができました!

5.開発を通しての振り返り

5-1.苦労したこと

エラーメッセージを出す条件

デフォルトでは何も選択していない状態になるため、その場合の処理をどうするかばかり考えてしまい、コードがぐちゃぐちゃになってしまいました。

そこでアプリ起動時に未選択(値0)を選択するようにしてコードを整理しました。開発を通して1つの考え方にとらわれず、発想の柔軟性が必要だと改めて思いました。

register_overtime()の処理

登録ボタンの処理を一気に仕上げようとしてしまったことが開発を難しくしていました。

まずは切り分けをして動作がうまくいったら次の処理を、またうまくいったら次の処理を、、、と進めていくことでスムーズに開発できたと反省しています。

if文

どの条件下でどの処理をするかというのは頭では理解していても、いざコードにするとエラーのオンパレードでした。

if文が多岐にわたるときはメモやコメントアウトでどのコードがなんの処理をしているかを一目でわかるようにしておくことで解決できたと振り返っています。

5-2.学んだこと

開発を通して、TkinterでGUIアプリを作成する方法やPyinstallerを使ってデスクトップアプリにするやり方を学びました。

まだPythonの基本のコードや処理内容も完璧に把握しているわけではないのですが、本記事でまとめたことによって整理することができました。

アプリの内容自治はシンプルで簡単なものですが、いざ開発するとたくさんエラーが出て、それを解決するの繰り返しでした。ですがそれが良い勉強になったと思います。

6.今後の展望

アプリを利用していく中で改良点は必ず出てくると思います。まずは実際に使ってみて改善するべき箇所を洗い出していこうと思います。

また、今回のアウトプットはパソコンが苦手な方に向けたアプリにもかかわらず、利用できるのがパソコンのみという本末転倒を招いています。私はこれからFlaskの勉強も進めていく予定です。今はデスクトップの簡単なアプリしか作れないですが、今回のアウトプットを活かしてスマホでも使えるWebアプリを開発したいです。

感想

最後まで本記事をご覧いただきありがとうございました!

今回で2回目のアウトプットになります。Qiitaに記事を投稿することで学習内容がまとまって良い時間になりました。改良の余地はたくさんあると思いますが、着実に成長している実感ができて嬉しいです:blush:

今後もまだまだアウトプットを公開していく予定ですので、またご覧いただけると幸いです!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?