Python

PythonでGoogleカレンダーから交通費精算書を作成する

こんにちは Tomoです。

今回はGoogle Calendarに交通費を登録してその情報をPythonを使って表計算ソフトに書き込むコーディングについて投稿します。

事前準備

類似コーディングとして、勤務表のコーディングを行っています。その内容がベースとなっていますので最初にこちらをご確認下さい。


Google Calendarに交通費を登録

Google Calendarに交通費を登録します。

タイトルを『通勤』とします。

日時に指定はありませんが1つだけ予定を登録して下さい。

当初、『通勤(出社)』『通勤(退社)』と2つに分けていましたがコーディングが難しくなりそうなので断念しました。

繰り返しは『平日(月~金)』を指定して下さい。

説明欄は以下のように登録してください。

    
利用路線=ⅩⅩ線;出発駅=□□駅;乗換駅=△△駅;往片=往復;片道料金=210;往復料金=420
利用路線=○○線;乗換駅=△△駅;到着駅=○○駅;往片=往復;片道料金=130;往復料金=260
    

各項目の区切り文字を「;」とし項目のタイトルと値の区切り文字を「=」とします。

後は保存してファイルをエクスポートして下さい。

エクスポートの方法も過去の投稿をご確認下さい

ちなみに、登録するカレンダーは前回使用した勤怠用のカレンダーを使っています。

コーディング

ここから実際のコーディングを行います。

前回モジュールやクラスを整理したのでその単位で記載します。

googlecalendarモジュール改修

googlecalendarモジュールを改修します。

改修内容は『イベントクラスに説明の項目を追加する』です。

その前に、カレンダーが説明をどのように出力するか記載します。

    
DESCRIPTION:利用路線=ⅩⅩ線\;出発駅=□□駅\;乗換駅=△△駅\;往片=往復\;片道料金=210\;往復料金=420\n利用路線=○○線
 \;乗換駅=△△駅\;到着駅=○○駅\;往片=往復\;片道料金=130\;往復料金=260
LAST-MODIFIED:20200104T154313Z
    

2行で出力されていますが、変なところで改行が行われています。

1の文字列の長さが決まっているのかもしれません。

なお、正しい改行位置は「\n」で出力されています。

CalendarEventClass改修

__init__メソッドを改修します。

    

    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
        self.description    = none

    

「description」を追加しました。

set_description追加

「description」を追加することで、値をセットするメソッドを追加します。

    

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

        例) DESCRIPTION:利用路線=ⅩⅩ線\;出発駅=□□駅\;乗換駅=△△駅\;片道料金=420\n利用路線=○○線\;乗換駅=△△駅\;到着駅=○○

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

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

        # descriptionを格納
        description = description_list[1]

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

    

特に難しい箇所はなく、以前作成した「summary」と類似した処理になります。

set_description_add追加

「description」は2行以上出力されることがあるため2行目以降を処理するメソッドを追加しました。

    

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

        例) DESCRIPTION:利用路線=ⅩⅩ線\;出発駅=□□駅\;乗換駅=△△駅\;片道料金=420\n利用路線=○○線\;乗換駅=△△駅\;到着駅=○○

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

        """
        # インスタンス変数に格納
        self.description = self.description + ical_data.strip()

    

「description」のインスタンス変数を呼び出して2行目を『strip()』で空白除去して文字列を結合します。

その内容をそのまま「description」のインスタンス変数に格納しています。

以上が「CalendarEventClass」の改修となります。

CalendarReadClass改修

CalendarReadClassを改修します。

    

class CalendarReadClass():

    def calendar_read_zip(self, file_path_zip):
        """
        zip形式のカレンダーを取得してlistに格納します.
        """
        with zipfile.ZipFile(file_path_zip) as cal_zip:

            for file_name in cal_zip.namelist():
                with cal_zip.open(file_name) as cal_file:

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

・・・略・・・

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

                            # (LAST-MODIFIED)を探す
                            if 'MODIFIED' in line_decode:
                                description_flag = False

                            # DESCRIPTIONは改行されて出力される
                            if description_flag:
                                ce.set_description_add(line_decode)

                            # 説明(DESCRIPTION)を探す
                            if 'DESCRIPTION' in line_decode:
                                # DESCRIPTIONは改行されて出力されるためつなげる
                                description_flag = True  
                                ce.set_description(line_decode)

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

            return cal_eve_list

    

1か所クラス整理の時に修正しています。

zipファイルの中のファイルを指定していましたが、中身のファイル名を取得してそのファイル名を渡す様に変更しています。

「cal_zip.namelist()」このファイル名のリストを「For」で繰り返し処理するように改修しています。

このクラスの改修ポイントは『LAST-MODIFIE』~『DESCRIPTION』を探すところのロジックになります。

『DESCRIPTION』で説明の内容を先ほど作成したメソッド「set_description」に渡します。このとき2行目を処理できるように「escription_flag」を「True」にしています。これにより、Trueの間は「set_description_add」を処理します。

解除は、「LAST-MODIFIED」で行います。個の文言を探したら「escription_flag」を「False」にしています。

読み込みの改修は以上です。

transportationexpensesモジュール新規作成

交通費作成用のモジュールを新規に作成します。

とはいいつつ、勤怠管理用のモジュール「attendancemanagement」をベースにしているためコピーして名前を付けて変更しています。

クラスの改修を行う前に表計算ソフトの出力ファイル名を変更します。

    
# 交通費管理ファイル
ATTENDANCE_FILE_PATH = 'D:\\python_workspace\\output_data\\交通費.xlsx'
    

RouteClasst追加

descriptionの内容を細かく変数に格納できるように対応したクラスを作成します。

    

class RouteClass():

    def ___init___(self):
        self.use_route      = none
        self.departure      = none
        self.arrival        = none
        self.transfer       = none
        self.round_trip     = none
        self.one_way_fee    = none
        self.round_trip_fee = none

    

descriptionの項目のインスタンス変数を定義しています。

CalendarReadClass改修

CalendarReadClassoを改修します。

___init___改修

___init___を改修します。

    

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

    

「route_list」を追加します。

set_description追加

descriptionを細かく分解するメソッドを作成します

    

        # 改行コードがあるか確認
        description_list = in_data.split('\\n')

        route_list = []
        # 路線数分処理を繰り返す
        for description in description_list:

            # ルート分割
            route_list_split =  description.split('\\;')

            # ルートクラスのインスタンス生成
            rc = RouteClass()

            # ルート分繰り返す
            for route in route_list_split:

                if '利用路線' in route:

                    # 「=」の前後をlistに格納
                    use_route_list = route.split('=')

                    # 利用路線を格納
                    use_route = use_route_list[1]

                    # インスタンス変数に格納
                    rc.use_route = use_route

                if '出発駅' in route:

                    # 「=」の前後をlistに格納
                    departure_list = route.split('=')

                    # 出発駅を格納
                    departure = departure_list[1]

                    # インスタンス変数に格納
                    rc.departure = departure

                if '乗換駅' in route:

                    # 「=」の前後をlistに格納
                    transfer_list = route.split('=')

                    # 乗換駅を格納
                    transfer = transfer_list[1]

                    # インスタンス変数に格納
                    rc.transfer = transfer

                if '到着駅' in route:

                    # 「=」の前後をlistに格納
                    arrival_list = route.split('=')

                    # 乗換駅を格納
                    arrival = arrival_list[1]

                    # インスタンス変数に格納
                    rc.arrival = arrival

                if '往片' in route:

                    # 「=」の前後をlistに格納
                    round_trip_list = route.split('=')

                    # 往片を格納
                    round_trip = round_trip_list[1]

                    # インスタンス変数に格納
                    rc.round_trip = round_trip

                if '片道料金' in route:

                    # 「=」の前後をlistに格納
                    one_way_fee_list = route.split('=')

                    # 往片を格納
                    one_way_fee = one_way_fee_list[1]

                    # インスタンス変数に格納
                    rc.one_way_fee = one_way_fee

                if '往復料金' in route:

                    # 「=」の前後をlistに格納
                    round_trip_fee_list = route.split('=')

                    # 往片を格納
                    round_trip_fee = round_trip_fee_list[1]

                    # インスタンス変数に格納
                    rc.round_trip_fee = round_trip_fee

            route_list.append(rc)

        self.route_list = route_list

    

改行コードがあるか確認をして改行数分処理を繰り返します。

「;」毎に項目を切り出します。注意点として切り出すときに「\\;」を指定して下さい。

「\\」は「\」をエスケープしています。

あとは切り出した数分繰り返しています。

基本的に「=」で切り出して「RouteClass」のインスタンス変数に格納しています。

なお、行数分処理を繰り返してRouteClassをリストに詰めています

route_list.append(rc)

最後にリストそのものをroute_listのインスタンス変数に格納しています。

SpreadSheetWriteClass改修

SpreadSheetWriteClassを改修します。が基本ロジック(繰り返しなどは勤怠のロジックが使えますのでちょっとだけ変更します。

    

class SpreadSheetWriteClass(spreadsheet.SpreadSheetWriteAbstractClass):

    def write_spreadsheet(self, cal_eve_list, input_date):

        self.file_path = ATTENDANCE_FILE_PATH

        super().write_spreadsheet(cal_eve_list, input_date)


    def create_plan(self, cd, calendar_attendance, target_month_start, target_month_end):

        # タイトルが通勤の場合に処理をする
        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.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.date():
                                        ex_no_flg = False

                            # 出勤日
                            if ex_no_flg:

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

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

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

                                            #  カレンダー予定クラスのインスタンスを生成
                                            cpc = CalendarPlanClass()

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

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

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

                                            # 説明をインスタンス変数に格納
                                            cpc.set_description(cd.description)

                                            # 交通費クラスのインスタンスをlistに格納
                                            calendar_attendance.append(cpc)

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

                                        #  カレンダー予定クラスのインスタンスを生成
                                        cpc = CalendarPlanClass()

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

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

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

                                        # 説明をインスタンス変数に格納
                                        cpc.set_description(cd.description)
                                        print(cpc.route_list)

                                        # 交通費クラスのインスタンスをlistに格納
                                        calendar_attendance.append(cpc)

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

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

                    #  カレンダー予定クラスのインスタンスを生成
                    cpc = CalendarPlanClass()

                    # 対象日をインスタンス変数に格納
                    cpc.date = cd.stat_date_time.date()

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

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

                    # 説明をインスタンス変数に格納
                    cpc.set_description(cd.description)

                    # 交通費クラスのインスタンスをlistに格納
                    calendar_attendance.append(cpc)

    

タイトル(summary)を通勤に変えます。

レンダー予定クラスのインスタンスを生成をした後にインスタンスを格納する場所に以下を追加します。

3か所ありますのでそれぞれ対応します。

    

# 説明をインスタンス変数に格納
cpc.set_description(cd.description)

    

※インスタンスの格納部分は内部メソッドにする方が変更は楽かもしれませんが今回はこのままにします。

※ブログを書いているときに気が付きました。

改修は以上です。

write_planの改修

表計算ソフトに書き出すメソッドを改修します。

    

    def write_plan(self, calendar_plan, work_sheet, target_month_start, target_month_end):

        work_sheet.cell(1, 1).value = '日付'
        work_sheet.cell(1, 2).value = '交通機関'
        work_sheet.cell(1, 3).value = '経路'
        work_sheet.cell(1, 4).value = '往/片'
        work_sheet.cell(1, 5).value = '片道料金'
        work_sheet.cell(1, 6).value = '往復料金'

        row_counter = 2
        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(row_counter, 1).value = target_date.strftime(FORMAT_DATE)

            # カレンダー交通費クラス(繰り返す)
            for cal_plan in calendar_plan:

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

                   departure_flg = False
                   row_counter_sub = 0
                   for route in cal_plan.route_list:

                       row_counter = row_counter + row_counter_sub
                       # 利用路線を出力する
                       work_sheet.cell(row_counter, 2).value = route.use_route

                       if hasattr(route, 'departure'):
                           # 出発駅を出力する
                           start_departure = route.departure
                           departure_flg = True


                       if departure_flg:

                           if hasattr(route, 'transfer'):
                               # 乗換駅を出力する
                               work_sheet.cell(row_counter, 3).value = start_departure + ' - ' + route.transfer

                           if hasattr(route, 'arrival'):
                               # 到着駅を出力する
                               work_sheet.cell(row_counter, 3).value = start_departure + ' - ' + route.arrival

                           departure_flg = False

                       else:
                           if hasattr(route, 'transfer'):
                               # 乗換駅を出力する
                               start_transfer = route.transfer

                           if hasattr(route, 'arrival'):
                               # 到着駅を出力する
                               work_sheet.cell(row_counter, 3).value = start_transfer  + ' - ' + route.arrival

                       if hasattr(route, 'round_trip'):
                           # 往片を出力する
                           work_sheet.cell(row_counter, 4).value = route.round_trip

                       if hasattr(route, 'one_way_fee'):
                           # 片道料金を出力する
                           work_sheet.cell(row_counter, 5).value = route.one_way_fee

                       if hasattr(route, 'round_trip_fee'):
                           # 往復料金を出力する
                           work_sheet.cell(row_counter, 6).value = route.round_trip_fee

                       row_counter_sub = row_counter_sub + 1

            row_counter = row_counter + 1

    

最初にヘッダを出力します。説明は省略します。

交通費を出力する箇所ですが・・・

    

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

    

ここまでは勤怠と同じです。

ここから先は「description」の改行数分(利用路線数分)繰り返します

利用路線の次に出力するのが「出発駅」か「乗換駅」かのいずれかになります。

1行目は必ず「出発駅」にします。このときdeparture_flgでフラグを「True」にすることで1行目を表します。

    

if departure_flg:

   if hasattr(route, 'transfer'):
       # 乗換駅を出力する
       work_sheet.cell(row_counter, 3).value = start_departure + ' - ' + route.transfer

   if hasattr(route, 'arrival'):
       # 到着駅を出力する
       work_sheet.cell(row_counter, 3).value = start_departure + ' - ' + route.arrival

   departure_flg = False

    

この処理が1行目の処理になります。

「乗換駅」か「到着駅」のいずれかを判定して登録します。

この時「出発駅-乗換駅」「出発駅-到着駅」と文字連結を行いいずれかが出力されます。

そのあとフラグを「False」にします。

    

else:
   if hasattr(route, 'transfer'):
       # 乗換駅を出力する
       start_transfer = route.transfer

   if hasattr(route, 'arrival'):
       # 到着駅を出力する
       work_sheet.cell(row_counter, 3).value = start_transfer  + ' - ' + route.arrival

    

この処理が2行目の処理になります。

この処理をみてお気づきかもしれませんが・・・乗り換え1回のみの対応となっています。

2回以上の乗り換えについては、別途対応が必要です。

あとはそのまま出力する処理のため省略します。

main処理の改修

最後にメイン処理の改修を行います

    

import attendancemanagement
import transportationexpenses
import googlecalendar
import sys

# カレンダーZIPファイル
CALENDAR_ZIP_FILE_PATH = 'D:\\python_workspace\\imput_data\\{Google アカウント}@gmail.com.ical.zip'

def create_spreatsheet(pattern):
    """
    勤怠または交通費のインスタンスを生成し返却します.

    Parameters
    ----------
    pattern : 
        pattern
    """
    if pattern is '1':
        # 勤怠管理のインスタンスを生成する
        return attendancemanagement.SpreadSheetWriteClass()
    if pattern is '2':
        # 交通日のインスタンスを生成する
        return transportationexpenses.SpreadSheetWriteClass()


if __name__ == '__main__':

    param = sys.argv

    if len(param) != 3: 
        print('引数を選択して下さい. [1番目の引数:(勤怠管理:1/交通費:2)][2番目の引数:年月]')
        exit()

    # 関数呼び出し
    # カレンダー情報取得
    gcc = googlecalendar.CalendarReadClass()
    cal_eve_list = gcc.calendar_read_zip(CALENDAR_ZIP_FILE_PATH)


    # 表計算ソフトに書き込み
    sp_ins = create_spreatsheet(param[1])
    sp_ins.write_spreadsheet(cal_eve_list, param[2])

    

改修のポイントは引数を切り替えて勤怠か交通費の何れかのインスタンスを生成しているところです。

呼び出しは以下の通りです。

    
python main.py 2 202001
    

まとめ

今回は、前回作成した勤怠管理のコーディングをベースに交通費のコーディングを行いました。

ある程度クラス設計をしていたので改修範囲はそこまで大きくなく1日程度でコーディングできました。

勤怠管理の方は4日程度かかっています。

カレンダー機能の「説明」の箇所はいろいろと応用ができると思います。

また、地図の機能もあるので地図をつかっても何かできるのではないでしょうか?

なんとか年末年始で勤怠と交通費の出力ができるようになりました。

罫線や変数名が共通の名前になっていない。ロジックが分かり難い等改修の必要性はありますが今回の資産をベースに今後改良したいと思います。

今回の改修内容については、別途配置したいと思います。

コメント

0 件のコメント:

コメントを投稿

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