5
1

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で勤怠システム打刻漏れの自動入力

5
Last updated at Posted at 2025-12-30

1.はじめに

各職場の管理者にとって、勤怠情報を管理する事は重要な仕事のひとつといえます。
単純な作業ではありますが、人数も増えてくると、それなりに工数もかかりますので効率化したい部分です。
自分の職場でも使えそうな管理業務の自動化について考えてみました。

2.課題と背景

私はコールセンターで管理者として勤務しています。コールセンターでは短期間業務を行う時に、多ければ100名を超える要員を一気に採用して短期間のうちに業務を立ち上げることがあります。

新しく採用された方々は新しい環境に慣れることや、業務を覚えることで精いっぱいになりますので、どうしても勤怠システムの打刻漏れが発生してしまいます。
一方で、業務の立ち上げで忙しいなかで、管理者は勤怠情報を管理(打刻漏れのチェックおよび、漏れた情報の代理入力)をする必要があります。

そこで、pythonを利用して勤怠システムの打刻漏れと、代理入力を自動化するコードを作成しました。

3.開発の目的

勤怠情報管理の工数を減らすために、pandas、seleniumを使って➀勤怠システムの打刻漏れをチェックし、➁入退室記録(ICカードや生体認証の記録より取得)から必要な情報を勤怠システムに自動入力する。

4.処理の定義

  • 入退室記録を参照し他組織のユーザ情報を除外
  • 処理対象の日付(8桁)を手入力

以下を繰り返し処理

  • 入退室記録のユーザ情報を利用し、各従業員が打刻漏れしていないか勤怠システムをチェック
  • ページで従業員が見つからない場合は、ページ送りをして再検索
  • 全ページで従業員が見つからなければ、該当の従業員が見つからなかったことを記載
  • 勤怠システムに登録された各従業員のシフトと、入退室記録を比べて遅刻・早退がないかチェック
  • 遅刻・早退の場合は入退室記録を参照し、勤怠システムに時間を転記
  • 遅刻・早退がなく、打刻漏れのみの場合は『打刻情報乖離事由』を入力
  • 転記、入力をした場合は登録ボタンをクリック
  • シフトより30分以上早く出社した従業員は処理せずに従業員名をプリント(サービス残業対策)

5.前準備

  • 勤怠システムにログインするID、PWをExcelファイルで用意

image.png

ファイル名:IDPW.xlsx

  • ICカードや生体認証の記録より取得した入退室記録のデータをcsvファイルで取得

image.png

ファイル名:yyyymmdd.csv

  • 関係ない人をまとめて除外リストを作成(他組織の人間や清掃業者など)

image.png

ファイル名:除外emp_list.xlsx

6.コード

from webdriver_manager.chrome import ChromeDriverManager
ChromeDriverManager().install()

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import pandas as pd
from datetime import datetime, timedelta
from time import sleep
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import TimeoutException

#seleniumにChromeDriverManagerを利用するよう指示
service = Service(ChromeDriverManager().install())

sleep(5)

#処理対象の日付設定(input)
start_date = datetime.strptime(input('処理対象の開始年月日を数字8桁で入力して下さい'),"%Y%m%d")
end_date = datetime.strptime(input('処理対象の最終年月日を数字8桁で入力して下さい'),"%Y%m%d")

if start_date>end_date:
    raise ValueError('開始日が終了日より後になっています。再度入力してください。')

dt_start_date = start_date.date()
dt_end_date = end_date.date()

print(f'処理対象:{dt_start_date}{dt_end_date}')

target_days = []
current = dt_start_date

#strftimeはdatetime型を文字列型に変更
while current <= dt_end_date:
    target_days.append(current.strftime("%Y%m%d"))
    current += timedelta(days=1)

#処理対象の日付をtarget_daysとしてリスト化する

results_target_days = []

for day in target_days:
    df = pd.read_csv(f'{day}.csv',encoding='cp932',dtype={'ユーザID':str})
    df = df.dropna(subset=['Room'])
    
    #Roomの列に"OPEN"が含まれる列を抽出
    df_open = df[df['Room'].str.contains("OPEN")]

    #発生日時をdatetime型に変換
    df_open.loc[:,"日時"] = pd.to_datetime(df_open["日時"],errors='coerce')
    
    #ユーザID毎にグループ分け
    groups = df_open.groupby('ユーザID')
                     
    #データ加工の結果を入れるためのリストを作成
    results = []

    for user_id, group in groups:
        #最初の入室日時を探す("入退室"の列が入室となっている"日時"の列を取得)
        in_times = group[group["入退室"]=="入室"]["日時"]
        #データが1件以上あるなら
        if len(in_times)>0:
            #emp_ent_timeはin_timesの最初のデータ
            emp_ent_time = in_times.iloc[0]
        else:
            emp_ent_time = None

        out_times = group[group["入退室"]=="退室"]["日時"]
        #データが1件以上あるなら
        if len(out_times)>0:
            #emp_leave_timeはout_timesの最後のデータ
            emp_leave_time = out_times.iloc[-1]
        else:
            emp_leave_time = None

        #resultsにユーザID、最初の入室時間、最後の退室時間を追加していく
        results.append({
            "発生日時":day,
            "ユーザーID":user_id,
            "入室時間":emp_ent_time,
            "退室時間":emp_leave_time
        })

    #日毎のresultsを統合
    results_target_days.extend(results)

#results_target_daysをDatFrameにする
result_all_df = pd.DataFrame(results_target_days)


#除外リストの読込み
df_exclude = pd.read_excel("除外emp_list.xlsx",encoding='cp932')

#ユーザIDをキーにする
key_cols = ["ユーザID"]

#前処理:データを文字列にする
result_all_df["ユーザID"] = result_all_df["ユーザID"].astype(str)
df_exclude["ユーザID"] = df_exclude["ユーザID"].astype('string')

#前処理:前後の空白を削除
result_all_df["ユーザID"] = result_all_df["ユーザID"].str.strip()
df_exclude["ユーザID"] = df_exclude["ユーザID"].str.strip()

#result_all_dfとdf_excludeを結合して削除行を洗い出す
merged = result_all_df.merge(df_exclude[key_cols],on = key_cols, how="left",indicator=True)

#df_excludeに存在しない(left_only)
result_df = merged[merged["_merge"]=="left_only"].drop(columns="_merge")

#ユーザーIDを文字列に変換してから社員番号の体系へ置換
result_df["ユーザID"] = result_df["ユーザID"].astype('string')
result_df["ユーザID"] = (
    result_df["ユーザID"].str.replace(r'^1','A',regex = True)
        .str.replace(r'^2','K',regex = True).str.replace(r'^3','Y',regex = True)
)

emp_list = result_df["ユーザID"]

#ブラウザを起動
chrome = webdriver.Chrome(service=service)
wait = WebDriverWait(chrome, 10)

sleep(3)

#指定のURLにアクセス
url = '【勤怠システムのURL】'
chrome.get(url)

#ブラウザの表示倍率を80%に(他のボタンと表示が被らないように)
chrome.execute_script("document.body.style.zoom='0.8'")

sleep(3)

df_IDPW = pd.read_excel("IDPW.xlsx")
IDPW_list = df_IDPW["内容"].tolist()

sleep(3)

field = chrome.find_elements(by=By.CLASS_NAME,value='form-control')

username_input = field[0]
password_input = field[1]

#Excelから読み取ったにIDとPWをsend_keysで入力
username_input.send_keys(IDPW_list[0])
password_input.send_keys(IDPW_list[1])

sleep(3)

#ログインボタンをクリック
login = wait.until(EC.element_to_be_clickable((
    By.CSS_SELECTOR, ".m-t-10.btn.btn-primary.btn-cons.btn-block")))
login.click()

sleep(5)

#【ボタン名】の画面へ遷移
lysithea = chrome.find_element(
    by=By.CSS_SELECTOR,
    value='[data-tt-click-send="【ボタン名】"]'
)
lysithea.click()

sleep(5)

# すべてのタブを取得
handles = chrome.window_handles
# 最後に開いたタブへ切り替え
chrome.switch_to.window(handles[-1])

sleep(3)

#承認者画面に遷移
manager_tab = chrome.find_element(By.CSS_SELECTOR, 'a[aria-controls="manager"]')
chrome.execute_script("arguments[0].click();", manager_tab)

sleep(3)

#承認状況一覧画面に遷移
approve_list = chrome.find_element(By.LINK_TEXT, "承認状況一覧")
approve_list.click()

sleep(3)

#emp_list:入退室記録から抽出した社員番号リスト

for emp in emp_list:
    found = False
    while not found:
        try:
            #===従業員の承認画面へ遷移し繰り返し操作====
            emp_approve = wait.until( EC.element_to_be_clickable((By.XPATH, f"//a[text()='{emp}']")))
            emp_approve.click()

            for day in target_days:
                # === ここで対応する入退室時間を取得 ===
                row = result_df[(result_df["ユーザID"] == emp) & (result_df["発生日時"]==day)]  
                
                emp_ent_time = row.iloc[0]["入室時間"]
                emp_leave_time = row.iloc[0]["退室時間"]

                day_dt = datetime.strptime(day,"%Y%m%d").date()
                day_el = day_dt.day
                
                emp_day_approve = wait.until(EC.element_to_be_clickable((
                    By.CSS_SELECTOR,f'[onclick="return doLabelYearMonthDay(\'YEARMONTHDAY\',{day_el});"]')))
                emp_day_approve.click()

                #始業打刻の要素(start_time_el)を取得
                start_time_el = wait.until(EC.visibility_of_element_located((
                    By.XPATH,'//span[@class="lbl" and normalize-space(text())="始業打刻"]/following-sibling::span[@class="time"]')))

                #始業打刻の要素(start_time_el)を取得
                end_time_el = wait.until(EC.visibility_of_element_located((
                    By.XPATH,'//span[@class="lbl" and normalize-space(text())="終業打刻"]/following-sibling::span[@class="time"]')))

                #打刻乖離事由の要素を取得
                select_dif = Select(chrome.find_element(By.NAME,"TimecardCause"))

                #打刻漏れ判定し打刻乖離事由を選択
                if(start_time_el.text=="--")or(end_time_el.text=="--"):
                    select_dif.select_by_visible_text("打刻もれ")

                    #始業時刻の入力欄の要素を取得
                    field_start = wait.until(EC.visibility_of_element_located((By.XPATH,'//input[@name="StartTime"]')))
                    work_rule_start = field_start.get_attribute("value")
                    
                    #終業時刻の入力欄の要素を取得
                    field_end = wait.until(EC.visibility_of_element_located((By.XPATH,'//input[@name="EndTime"]')))
                    work_rule_end = field_end.get_attribute("value")

                    #datetime型に変換
                    work_rule_start_time = datetime.strptime(work_rule_start,'%H%M').time()
                    work_rule_end_time = datetime.strptime(work_rule_end,'%H%M').time()

                    #文字列に変換
                    emp_ent_time_st = emp_ent_time.strftime('%H%M')
                    emp_leave_time_st = emp_leave_time.strftime('%H%M')

                    #始業打刻の入力要否(遅刻・早退)の判定
                    is_late = emp_ent_time.time() > work_rule_start_time
                    is_early_leave = work_rule_end_time > emp_leave_time.time()
                    is_missing_stamp_start = start_time_el.text=="--"
                    is_missing_stamp_end = end_time_el.text=="--"

                    #始業時間より30分以上早く出社(サービス残業対応)
                    base_time = datetime.combine(day_dt,work_rule_start_time)
                    is_too_early = base_time - timedelta(minutes=30) > emp_ent_time

                    if is_too_early:
                        print(f'{emp}がシフトより30分以上早く出社')

                        approve_list = chrome.find_element(By.LINK_TEXT, "承認状況一覧")
                        approve_list.click()
                        
                        continue
                    
                    #正しい始業時刻・終業時刻を入力
                    if is_late:
                        field_start.clear()
                        field_start.send_keys(emp_ent_time_st)

                    if is_early_leave:
                        field_end.clear()
                        field_end.send_keys(emp_leave_time_st)

                    #遅刻・早退発生時のみ登録ボタンを押下
                    if is_late or is_early_leave or is_missing_stamp_start or is_missing_stamp_end:
                        regist = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,".btn.btn-big.btn-primary")))
                        regist.click()

                approve_list = chrome.find_element(By.LINK_TEXT, "承認状況一覧")
                approve_list.click()

            found = True
            
        except TimeoutException:
            try:
                next_page = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".btn.next"))) 
                next_page.click()

            except TimeoutException:
                print(f'{emp}が見つかりませんでした')

                approve_list = chrome.find_element(By.LINK_TEXT, "承認状況一覧")
                approve_list.click()
                
                break

7.成果

150人近くの在籍人数がいるので、人の手で処理する場合少なくとも毎日1時間以上は掛かり、加えて処理した後に疲労感が残る毎日でしたが、自動化することでその時間を本来の業務にあてることができるようになりました。

8.今後の課題

今回のコードでは反映しておりませんでしたが、以下のようなイレギュラーケースも今後は対応できるようにアップデートしていきたいと思います。

  • 勤怠システムに反映されていなかった有休、半休の処理
  • 出社時に誤って退勤として打刻してしまい、前日の退勤打刻を上書きしてしまった場合の処理
5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?