1.はじめに
学生時代にアルバイトしていた小規模事業者や、知人が経営する動物病院では、タイムカードで打刻した紙の勤怠情報をExcelへ手入力し、給与計算や勤怠管理を行っていました。
その現場では、以下のような課題が発生していました。
- 打刻漏れ
- 転記ミス
- 給与計算ミス
- 管理工数の増大
そこで今回は勤怠打刻→労働時間計算→給与自動計算→Slack通知までを一括で処理できるデスクトップアプリをPythonで作成しました。
2.実装内容の概要
■ 主な機能
- 従業員マスタ読み込み
- 勤務開始ボタンで出勤記録
- 勤務終了ボタンで退勤記録
- 勤務時間自動計算
- 給与自動計算(時給×労働時間)
- Slack通知
- Excel自動保存
■ 使用技術
- Python
- Tkinter
- openpyxl
■ システム構成イメージ
- 勤務開始時
① 該当の従業員を選択し、「勤務開始」ボタンをクリック
② Excelに出勤記録
③ Slack通知
- 勤務終了時
① 該当の従業員を選択し、「勤務終了」ボタンをクリック
② Excelに退勤記録
③ 勤務時間計算
④ 給与計算しExcel転記
⑤ Excel保存
⑥ Slack通知
3.ソースコード
以下に、使用したソースコード全体を掲載します。
import tkinter
from tkinter import ttk
from datetime import datetime
import openpyxl
import slackweb
webhook_url = "SLACK_WEBHOOK_URL"
today = datetime.now().strftime("%Y/%m/%d")
time = datetime.now().strftime("%H:%M")
wb = openpyxl.load_workbook("勤怠管理.xlsx")
ws_pay = wb["pay"]
ws_emp = wb["employee"]
# 従業員マスタを取り込む
max_row = ws_emp.max_row + 1
employees = {}
for row in range(max_row) :
# ヘッダーをスキップ
if row < 2:
continue
# 従業員マスタからIDを取り出す
emp_id = ws_emp.cell(row, 1).value
# 従業員マスタから名前を取り出す
name = ws_emp.cell(row, 2).value
# 画面表示用にID+名前にする
emp_id_name = f"{emp_id:03d} {name}"
# 従業員マスタから時給を取り出す
wage = int(ws_emp.cell(row, 3).value)
employees[emp_id_name] = {"id": emp_id, "name": name, "wage": wage}
# 時給計算するために労働時間を時間に換算する関数
def calc_work_hours(start, end):
start_work = datetime.strptime(start, "%H:%M")
end_work = datetime.strptime(end, "%H:%M")
# 労働時間を秒単位で求める
work_seconds = (end_work - start_work).seconds
# 労働時間を秒単位から時間単位に変換
work_hours = work_seconds / 3600
return round(work_hours,2)
# 勤務開始ボタン押下後の挙動
def start_button_function():
name = employee_box.get()
# slackに送信
slack = slackweb.Slack(url = webhook_url)
slack.notify(text = f"{name} {today} 勤務開始します" )
input_row = ws_pay.max_row + 1
# 現時点の年月日をExcelに記載
ws_pay.cell(row = input_row, column = 1).value = today
# IDをExcelに記載
ws_pay.cell(row = input_row, column = 2).value = name[:3]
# 名前をExcelに記載
ws_pay.cell(row = input_row, column = 3).value = name[4:]
# 出勤時刻をExcelに記載
ws_pay.cell(row = input_row, column = 4).value = time
wb.save("勤怠管理.xlsx")
print("処理完了しました")
# 勤務終了ボタン押下後の挙動
def end_button_function():
name = employee_box.get()
# slackに送信
slack = slackweb.Slack(url = webhook_url)
slack.notify(text = f"{name} {today} 勤務終了します")
# 該当の従業員の列を判定する
target_row = None
for row in range(ws_pay.max_row, 1, -1):
if(ws_pay.cell(row = row, column = 2).value == f"{employees[name]["id"]:03d}" and ws_pay.cell(row = row, column = 5).value is None):
target_row = row
break
if target_row is None:
print("出勤記録が見つかりません。")
return
# 退勤時刻をExcelに記入
ws_pay.cell(row = target_row, column = 5).value = time
# 出勤時刻を取得
start_time = ws_pay.cell(row = target_row, column = 4).value
# 退勤時刻を取得
end_time = time
# 勤務時間を計算
sum_works_hours = calc_work_hours(start_time, end_time)
# 該当の従業員の時給を取得
wage = employees[name]["wage"]
# 給料を計算
salary = int(sum_works_hours * wage)
# 給料をExcelに記入
ws_pay.cell(row = target_row, column = 6).value = salary
# Excelを保存し、ログを出力
wb.save("勤怠管理.xlsx")
print("処理完了しました")
# 入力フォーム作成
foam = tkinter.Tk()
foam.title("勤怠記録アプリ")
foam.geometry("400x200+800+200")
# 従業員選択
employee_box = ttk.Combobox(foam, values = list(employees.keys()))
employee_box.place(x = 30, y = 60)
employee_box.set("従業員選択(ID + 名前)")
# 勤務開始ボタン
start_button = tkinter.Button(foam, text = "勤務開始", command = start_button_function)
start_button.place(x = 250, y = 30)
# 勤務終了ボタン
end_button = tkinter.Button(foam, text = "勤務終了", command = end_button_function)
end_button.place(x = 250, y = 120)
foam.mainloop()
上記のコードのうち、特に設計上重要な部分についてい解説します。
3-1. 従業員マスタの読み込みと辞書構造
本アプリでは従業員マスタをExcelから読み込み、辞書型で管理しています。従業員は勤怠管理.xlsxのemployeeシートから取得し、ID・名前・時給を読み込みます。また画面表示用として、「従業員コード(ID)+名前」を組みあわせています。そして、表示用文字列をキーとして辞書に格納することで、ID・氏名・時給をひとまとめに管理できる構造としました。この構造により、給与計算時にはemployees[name]["wage"]で値を取得することができます。
for row in range(max_row) :
# ヘッダーをスキップ
if row < 2:
continue
# 従業員マスタからIDを取り出す
emp_id = ws_emp.cell(row, 1).value
# 従業員マスタから名前を取り出す
name = ws_emp.cell(row, 2).value
# 画面表示用にID+名前にする
emp_id_name = f"{emp_id:03d} {name}"
# 従業員マスタから時給を取り出す
wage = int(ws_emp.cell(row, 3).value)
employees[emp_id_name] = {"id": emp_id, "name": name, "wage": wage}
3-2. 勤務開始ボタン処理
最初にプルダウンから選択された従業員情報を取得します。次に、Slackへ通知を送信しています。その後Excelへの書き込み処理を行います。既存データの最終行の次の行に新しいレコードを追加します。書き込む内容は以下になります。
- 1列目:日付
- 2列目:従業員ID
- 3列目:名前
- 4列目:出勤時刻
※従業員IDについては表示用文字列の先頭3文字で、名前は4文字目以降を書き込みます。
def start_button_function():
name = employee_box.get()
# slackに送信
slack = slackweb.Slack(url = webhook_url)
slack.notify(text = f"{name} {today} 勤務開始します" )
input_row = ws_pay.max_row + 1
# 現時点の年月日をExcelに記載
ws_pay.cell(row = input_row, column = 1).value = today
# IDをExcelに記載
ws_pay.cell(row = input_row, column = 2).value = name[:3]
# 名前をExcelに記載
ws_pay.cell(row = input_row, column = 3).value = name[4:]
# 出勤時刻をExcelに記載
ws_pay.cell(row = input_row, column = 4).value = time
wb.save("勤怠管理.xlsx")
print("処理完了しました")
3-3. 勤務時間の計算処理
Excelに保存されている出勤時刻と退勤時刻は文字列型であるため、そのままでは引き算をすることが出来ません。そのため、datetime.strptime()を用いて文字列をdatetime型へ変換し、差分を計算しています。差分から勤務時間の秒数が算出されたので、その値を3600で割ることで時間単位へ変換しています。
def calc_work_hours(start, end):
start_work = datetime.strptime(start, "%H:%M")
end_work = datetime.strptime(end, "%H:%M")
# 労働時間を秒単位で求める
work_seconds = (end_work - start_work).seconds
# 労働時間を秒単位から時間単位に変換
work_hours = work_seconds / 3600
return round(work_hours,2)
3-4. 勤務終了ボタン処理
プルダウンから選択された従業員情報を取得します。次に、Slackへ通知を送信します。その後Excelへの書き込み処理を行います。書き込みをするレコードについては、対象従業員であり、退勤時刻が未入力であるものが対象になります。またExcelの最終行から逆順に検索することで、出勤記録を優先的に取得する仕組みにしています。書き込む内容は以下になります。
- 6列目:退勤時刻
- 7列目:給料
def end_button_function():
name = employee_box.get()
# slackに送信
slack = slackweb.Slack(url = webhook_url)
slack.notify(text = f"{name} {today} 勤務終了します")
# 該当の従業員の列を判定する
target_row = None
for row in range(ws_pay.max_row, 1, -1):
if(ws_pay.cell(row = row, column = 2).value == f"{employees[name]["id"]:03d}" and ws_pay.cell(row = row, column = 5).value is None):
target_row = row
break
if target_row is None:
print("出勤記録が見つかりません。")
return
# 退勤時刻をExcelに記入
ws_pay.cell(row = target_row, column = 5).value = time
# 出勤時刻を取得
start_time = ws_pay.cell(row = target_row, column = 4).value
# 退勤時刻を取得
end_time = time
# 勤務時間を計算
sum_works_hours = calc_work_hours(start_time, end_time)
# 該当の従業員の時給を取得
wage = employees[name]["wage"]
# 給料を計算
salary = int(sum_works_hours * wage)
# 給料をExcelに記入
ws_pay.cell(row = target_row, column = 6).value = salary
# Excelを保存し、ログを出力
wb.save("勤怠管理.xlsx")
print("処理完了しました")
4.今後の拡張案について
- 従業員ごとのシート分割および月給自動集計
- 残業・深夜手当の自動計算
- 誤操作防止ロジックの追加
(未退勤レコードが存在する場合は勤務開始を無効化、
同日複数打刻時に確認ダイアログを表示)
5.実装時に苦労した点
今回の実装では、勤怠管理から給与計算、さらにSlackに通知までの一連の流れを実装しました。その中で、特に以下の3点で設計・実装に苦労しました。
①同姓同名を考慮した従業員識別
実務を想定すると、同姓同名の従業員が存在する可能性があります。そのため、単純に「名前」だけで管理するのではなく、「従業員コード(ID)」と「名前」を組み合わせて管理する設計にしました。
その際に悩んだのが、プルダウンにどのような形式でデータを持たせるかという点です。
最初はタプルで保持する方法を検討しましたが、画面表示には「従業員コード(ID)+名前」を表示し、Excelに出力する際は「従業員コード(ID)」と「名前」を別々の列に出力したかったため、どのように紐づけるかで思考錯誤しました。
最終的には、表示用の文字列をキーにして、辞書で管理する構造にしました。表示用の文字列をキーにすることで、Excel出力時は「employees[name]["id"] 」のように個別取得できるようにしました。
②時給取得と給与計算ロジック
給与計算では、employeesに格納した時給を正しく取得し、勤務時間の計算に苦労しました。時給の取得自体は、「employees[name]["wage"] 」のように辞書から取り出すことで解決しました。一方勤務時間の計算はExcelに保存されている出勤時刻と退勤時刻は文字列であるため、そのまま引き算することはできません。そこで文字列をdatetime.strptime()で変換し、差分を秒単位で算出し秒を時間に変換することで正確な勤務時間を算出しました。
③未退勤レコードの特定処理
退勤記録をどのレコードに対して行うかの判定ロジックに苦労しました。ロジックとしては、対象従業員であることと、退勤時刻が未入力であることという条件を満たすレコードを、下から順に検索するロジックを実装しました。
今回の開発を通じて感じたことは、実務を想定すると単純な実装では不十分であるという事です。色々なことを考慮しながら設計をすることで、単なる練習コードではなく実務に近い形へ近づけることができました。
今後は残業計算や月次集計機能を追加し、より実務レベルの業務効率化ツールへ発展させていきたいと考えています。






