Pythonで勤怠を管理する(Google Calendarからを取得して表計算ソフトに出力するのソースコード)

Pythonで勤怠を管理する(Google Calendarからを取得して表計算ソフトに出力するのソースコード)を記述します。

    
import zipfile
import datetime
import dateutil.parser
import openpyxl

# カレンダーZIPファイル
CALENDAR_ZIP_FILE_PATH = 'D:\\python_workspace\\imput_data\\{Google アカウント}@gmail.com.ical.zip'
# カレンダーICSファイル
CALENDAR_FILE_NAME = '{xxxxxxxx}@group.calendar.google.com.ics'
# 勤怠管理ファイル
ATTENDANCE_FILE_PATH = 'D:\\python_workspace\\output_data\\test.xlsx'

# 日時フォーマット
FORMAT_DATE_TIME = '%Y/%m/%d %H:%M:%S7'
# 日付フォーマット
FORMAT_DATE = '%Y/%m/%d'
# 時分フォーマット
FORMAT_HM = '%H:%M'

target_month_start = datetime.date(2020, 1, 1)
target_month_end = datetime.date(2020, 1, 31)

WEEKRY_EN_LIST = ['MO','TH','TU','WE','FR','ST','SN']




class CalendarAttendanceClass():

    def ___init___(self):
        self.date = none
        self.stat_date_time = none
        self.end_date_time  = none




class CalendaEventClass():

    def ___init___(self):

        self.summary        = none
        self.stat_date_time = none
        self.end_date_time  = none
        self.repeat         = False
        self.freq           = none
        self.byday          = none
        self.until          = none
        self.exdate         = none

    def set_summary(self, ical_data):
        """
        ical形式のSUMMARYをString型で設定する.

        例) SUMMARY:勤怠

        Parameters
        ----------
        ical_data : String
             ical形式のデータ

        """
        # 「:」の前後をlistに格納
        summary_list = ical_data.split(':')

        # summaryを格納
        summary = summary_list[1]

        # インスタンス変数に格納
        self.summary = summary


    def set_dtstart(self, ical_data):
        """
        ical形式の開始時間DTSTARTをdatetime型で設定する.
        また、繰り返しの場合に繰り返し用のフラグを有効にする.

        「;」繰り返しのイベント
        例) DTSTART;TZID=Asia/Tokyo:20200106T093000

        「:」単体イベント
        例) DTSTART:20200105T003000Z

        Parameters
        ----------
        ical_data : String
             ical形式のデータ

        """

        if 'DTSTART:' in ical_data:

            # boolean型の繰り返しなしでインスタンス変数に格納
            self.repeat = False

            # 「:」の前後をlistに格納
            dt_list = ical_data.split(':')

            # 開始時刻を格納
            dt_start = dt_list[1]

            # datetime型に変換しインスタンス変数に格納
            self.stat_date_time = self.__dt_to_date(dt_start)

        else:

            # boolean型の繰り返しありでインスタンス変数に格納
            self.repeat = True

            # 「;」の前後をlistに格納
            dt_list = ical_data.split(';')

            # 「TZID」が含まれているか確認
            if 'TZID' in dt_list[1]:

                # 「:」の前後をlistに格納
                dt_list = ical_data.split(':')

                # 「=」の前後をlistに格納
                dt_tz_list = dt_list[0].split('=')

                # タイムゾーンを格納
                dt_tz = dt_tz_list[0]

                # 開始時刻を格納
                dt_start = dt_list[1]

                # datetime型に変換しインスタンス変数に格納
                self.stat_date_time = self.__dt_to_date(dt_start, dt_tz)

            else:

                # 「:」の前後をlistに格納
                dt_list = ical_data.split(':')


                # 開始時刻を格納
                dt_start = dt_list[1]

                # datetime型に変換しインスタンス変数に格納
                self.stat_date_time = self.__dt_to_date(dt_start)


    def set_dtend(self, ical_data):
        """
        ical形式のルールを取得し値を設定する.
        また、繰り返しの場合に繰り返し用のフラグを有効にする.

        「;」繰り返しのイベント
        例) DTEND;TZID=Asia/Tokyo:20200106T183000

        「:」単体イベント
        例) DTEND:20200105T093000Z

        Parameters
        ----------
        ical_data : String
             ical形式のデータ

        """

        if 'DTEND:' in ical_data:

            # 「:」の前後をlistに格納
            dt_list = ical_data.split(':')

            # 終了時刻を格納
            dt_end = dt_list[1]

            # datetime型に変換しインスタンス変数に格納
            self.end_date_time = self.__dt_to_date(dt_end)

        else:

            # 「;」の前後をlistに格納
            dt_list = ical_data.split(';')

            # 「TZID」が含まれているか確認
            if 'TZID' in dt_list[1]:

                # 「:」の前後をlistに格納
                dt_list = ical_data.split(':')

                # 「=」の前後をlistに格納
                dt_tz_list = dt_list[0].split('=')

                # タイムゾーンを格納
                dt_tz = dt_tz_list[0]

                # 開始時刻を格納
                dt_end = dt_list[1]

                # datetime型に変換しインスタンス変数に格納
                self.end_date_time = self.__dt_to_date(dt_end, dt_tz)

            else:

                # 「:」の前後をlistに格納
                dt_list = ical_data.split(':')

                # 開始時刻を格納
                dt_end = dt_list[1]

                # datetime型に変換しインスタンス変数に格納
                self.end_date_time = self.__dt_to_date(dt_end)

    def set_exdate(self, ical_data):
        """
        ical形式のEXDATAを取得し値を設定する.

        例) EXDATE;TZID=Asia/Tokyo:20200113T093000

        Parameters
        ----------
        ical_data : String
             ical形式のデータ

        """
        # 「;」の前後をlistに格納
        dt_list = ical_data.split(';')

        # 「TZID」が含まれているか確認
        if 'TZID' in dt_list[1]:

            # 「:」の前後をlistに格納
            dt_list = ical_data.split(':')

            # 「=」の前後をlistに格納
            dt_tz_list = dt_list[0].split('=')

            # タイムゾーンを格納
            dt_tz = dt_tz_list[0]

            # EXDATEを格納
            dt_exdate = dt_list[1]

            #クラスに変数が定義されているか
            if hasattr(self, 'exdate'):
                # datetime型に変換しインスタンス変数に格納
                self.exdate.append(self.__dt_to_date(dt_exdate, dt_tz))
            else:
                # datetime型に変換しインスタンス変数に格納(最初にlist初期化して格納)
                self.exdate = [self.__dt_to_date(dt_exdate, dt_tz)]

        else:

            # 「:」の前後をlistに格納
            dt_list = ical_data.split(':')

            # EXDATEを格納
            dt_exdate = dt_list[1]

            #クラスに変数が定義されているか
            if hasattr(self, 'exdate'):
                # datetime型に変換しインスタンス変数に格納
                self.exdate.append(self.__dt_to_date(dt_exdate))

            else:
                # datetime型に変換しインスタンス変数に格納(list初期化)
                self.exdate = [self.__dt_to_date(dt_exdate)]

    def set_rule(self, ical_data):
        """
        ical形式の終了時間DTENDをdatetime型で設定する.
        また、FREEQ(頻度)  BYDAY UNTIL(期限)を分解して格納する

        繰り返しのイベント(平日)
        例) RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE

        繰り返しのイベント(平日 ある日以降削除)
        例) RRULE:FREQ=WEEKLY;UNTIL=20200129T145959Z;BYDAY=FR,MO,TH,TU,WE

        繰り返しのイベント(毎日)
        例) RRULE:FREQ=DAILY

        それ以外については別途必要に応じて検討

        Parameters
        ----------
        ical_data : String
       """

        # 「:」の前後をlistに格納
        rule_list = ical_data.split(':')

        # 「;」の前後をlistに格納
        element_list = rule_list[1].split(';')

        # 要素数分処理を繰り返す
        for element in element_list:

            # FREQの場合
            if  'FREQ' in element:
                print(element)
                # freq_listを取得「=」の前後をlistに格納
                freq_list = element.split('=')

                # freqを取得「=」の右側を格納
                freq = freq_list[1]

                # インスタンス変数に格納
                self.freq = freq

            # UNTILの場合
            if 'UNTIL' in element:

                # until_listを取得「=」の前後をlistに格納
                until_list = element.split('=')

                # unitlを取取得「=」の右側を格納
                until = until_list[1]

                # datetime型に変換しインスタンス変数に格納
                self.until = self.__dt_to_date(until)

            # BYDAYの場合
            if 'BYDAY' in element:

                # byday_listを取得「=」の前後をlistに格納
                byday_list = element.split('=')

                # unitlを取取得「=」の右側を格納
                until = byday_list[1]

                # list型の状態でインスタンス変数に格納
                self.byday = until.split(',')


    def __dt_to_date(self, dt, tz=None):
        """
        ISO 8601形式の時間をdatetime型で設定する.

        Parameters
        ----------
        dt : str
             ISO 8601形式のデータ

        tz : str
             TimeZone形式(未使用)
             例) Asia/Tokyo
        """
        # タイムゾーンを設定
        jst = datetime.timezone(datetime.timedelta(hours=+9), 'JST')

        # タイムゾーンで変換した時刻を返却
        return dateutil.parser.parse(dt).astimezone(jst)




def read_calendar_zip():
    """
    zip形式のカレンダーを取得してlistに格納します.
    """
    with zipfile.ZipFile(CALENDAR_ZIP_FILE_PATH) as cal_zip:
        with cal_zip.open(CALENDAR_FILE_NAME) as cal_file:

            cal_eve_list = []
            event_flag = False
            for line in cal_file:
                 line_decode = line.decode('utf-8')
                 line_decode = line_decode.rstrip('\r\n')

                 # イベントの開始(BEGIN:VEVENT)を探す
                 if 'BEGIN:VEVENT' in line_decode:
                     event_flag = True
                     cd = CalendaEventClass()

                 if event_flag:
                     # 開始時間(DTSTART)を探す
                     if 'DTSTART' in line_decode:
                         cd.set_dtstart(line_decode)

                     # 終了時間(DTEND)を探す
                     if 'DTEND' in line_decode:
                         cd.set_dtend(line_decode)

                     # ルール(RRULE)を探す
                     if 'RRULE' in line_decode:
                         cd.set_rule(line_decode)

                     # 除外日(EXDATE)を探す
                     if 'EXDATE' in line_decode:
                         cd.set_exdate(line_decode)

                     # タイトル(SUMMARY)を探す
                     if 'SUMMARY:' in line_decode:
                         cd.set_summary(line_decode)

                     # イベントの終了(END:VEVENT)を探す
                     if 'END:VEVENT' in line_decode:
                         event_flag = False
                         cal_eve_list.append(cd)

            return cal_eve_list




def write_attendance(cal_ate_list):
    """
    勤怠データを出力します.
    """

    # カレンダー勤怠クラスをlistに格納する為に初期化
    calendar_attendance = []

    # イベントの数分処理を繰りす
    for cd in cal_ate_list:

        # タイトルが勤怠の場合に処理をする
        if cd.summary in '勤怠':

            if cd.repeat:
                # 繰り返し予定の場合はこのルートを通る

                # 出力範囲の日数を繰り返す
                # 例)2020年1月1日から2020年1月31日まで1日づつ処理を繰り返して勤怠用のクラスに格納する
                for count_date in range((target_month_end - target_month_start).days + 1):

                    # ターゲット日(繰り返しのカレント日付)
                    target_date = target_month_start + datetime.timedelta(count_date)

                    # イベントの開始日 <= ターゲット日 
                    # 繰り返しの場合ターゲット日以前に開始日があ場合はすべて出力する(例外あり)
                    if cd.stat_date_time.date() <= target_date:

                        # 週の繰り返しであること
                        if cd.freq is not None and cd.freq in 'WEEKLY':

                            # 出勤しない日か判定するフラグ(True:出勤/False/休み)
                            ex_no_flg = True

                            # 出勤しない日を格納するインスタンスの存在をチェックし存在する場合以下を処理
                            if hasattr(cd, 'exdate'):

                                # 出勤しない日はlistで保持されるため繰り返す。
                                # このデータが多いと処理が遅くなる
                                for exdate in cd.exdate:

                                    # 出勤しない日 == ターゲット日 の場合
                                    # カレンダー勤怠のインスタンスに書き込まないように出勤しない日か判定するフラグを無効にする
                                    if exdate.date() == target_date:
                                        ex_no_flg = False

                            # 出勤日
                            if ex_no_flg:

                                if hasattr(cd, 'until'):
                                    # 繰り返しの期限が設定されているか

                                    # 繰り返し期限日 >= ターゲット日 (期限日以前の時は出力する)
                                    if cd.until.date() >= target_date:

                                        # 勤怠を出力する曜日か判定する
                                        if WEEKRY_EN_LIST[target_date.weekday()] in cd.byday:

                                            #  カレンダー勤怠クラスのインスタンスを生成
                                            cac = CalendarAttendanceClass()

                                            # 対象日をインスタンス変数に格納
                                            cac.date = target_date

                                            # 開始日時をインスタンス変数に格納
                                            cac.stat_date_time = cd.stat_date_time

                                            # 終了日時をインスタンス変数に格納
                                            cac.end_date_time = cd.end_date_time

                                            # カレンダー勤怠クラスのインスタンスをlistに格納
                                            calendar_attendance.append(cac)

                                else:
                                    # 繰り返しの期限が設定されていないため出力
                                    # 勤怠を出力する曜日か判定する
                                    if WEEKRY_EN_LIST[target_date.weekday()] in cd.byday:

                                        #  カレンダー勤怠クラスのインスタンスを生成
                                        cac = CalendarAttendanceClass()

                                        # 対象日をインスタンス変数に格納
                                        cac.date = target_date

                                        # 開始日時をインスタンス変数に格納
                                        cac.stat_date_time = cd.stat_date_time

                                        # 終了日時をインスタンス変数に格納
                                        cac.end_date_time = cd.end_date_time

                                        # カレンダー勤怠クラスのインスタンスをlistに格納
                                        calendar_attendance.append(cac)

            else:
                # 繰り返し予定以外の場合はこのルートを通る

                # 出力対象開始日 <= イベントクラスの開始日 <= 出力対象終了日
                # 例) 2020年1月1日 <= 2020年1月5日 <= 2020年1月31日 出力する
                # 例) 2020年2月1日 <= 2020年2月5日 <= 2020年2月31日 出力しない
                if target_month_start <= cd.stat_date_time.date() <= target_month_end:

                    #  カレンダー勤怠クラスのインスタンスを生成
                    cac = CalendarAttendanceClass()

                    # 対象日をインスタンス変数に格納
                    cac.date = target_date

                    # 開始日時をインスタンス変数に格納
                    cac.stat_date_time = cd.stat_date_time

                    # 終了日時をインスタンス変数に格納
                    cac.end_date_time = cd.end_date_time

                    # カレンダー勤怠クラスのインスタンスをlistに格納
                    calendar_attendance.append(cac)

    """
    ここから表計算に書き込みを行う
    """
    # 表計算ワークブック作成
    work_book = openpyxl.Workbook()

    # 表計算ワークシートをアクティブ
    work_sheet = work_book.active

    # 出力範囲の日数を繰り返す
    # 例)2020年1月1日から2020年1月31日まで1日づつ処理を繰り返して表計算のセルに書き込む
    for count_date in range((target_month_end - target_month_start).days + 1):

        # ターゲット日(繰り返しのカレント日付)
        target_date = target_month_start + datetime.timedelta(count_date)

        # 勤怠表の日付を出力する(日付だけを出力する)
        work_sheet.cell(count_date + 1, 1).value = target_date.strftime(FORMAT_DATE)

        # カレンダー勤怠クラス(出勤日を探すために繰り返す)
        for cal_ate in calendar_attendance:

            # ターゲット日と出勤日が一致し場合 出勤情報をセルに格納する
            if cal_ate.date == target_date:

                # 開始時間
                work_sheet.cell(count_date + 1, 2).value = cal_ate.stat_date_time.strftime(FORMAT_HM)

                # 終了時間
                work_sheet.cell(count_date + 1, 3).value = cal_ate.end_date_time.strftime(FORMAT_HM)

                # 作業時間(昼休憩1時間を引く)
                work_sheet.cell(count_date + 1, 4).value = cal_ate.end_date_time - cal_ate.stat_date_time - datetime.timedelta(hours=1)

    # 表計算ソフトに書き込み
    work_book.save(ATTENDANCE_FILE_PATH)




# 関数呼び出し
# カレンダー情報取得
cal_eve_list = read_calendar_zip()

# 表計算ソフトに書き込み
write_attendance(cal_eve_list)

    

コメント

0 件のコメント:

コメントを投稿

コメントをお待ちしています。