🦔

Google Calendar API を用いた日報作成の半自動化

2024/07/24に公開

はじめに

こんにちは。
クラウドエース株式会社 データソリューション部の髙木です。

私は新卒ですが、入社前から業務の自動化をしたいという思いがありました。
本記事では、私が初めて業務の(半)自動化をしたことについて執筆します。

日報作成を半自動化しようと思ったワケ

退勤前の必須業務である日報。
早く退勤したいのに日報がそれを阻害してきます。

特に「作業詳細」と「作業予定」の欄を埋めるのが大変です。
(「課題」や「所感」は多少なりとも書く気力はあります。)

Google Calendar の予定のタイトルをそのままコピー&ペーストできればいいのに。

もちろん、各予定について

  1. Google Calendar の予定をクリックする。
  2. 予定のタイトルをコピーする。
  3. 日報作成画面にペーストする。

を行えばできます。
しかし、明らかに面倒です。

もっとこう、その日と翌勤務日の予定のタイトルだけをずらーと表示できたらなと。

Google Calendar API を使えばできるのでは?

と思い、調査と実装を行いました。

以下、注意点です。

  • クラウドエース株式会社の日報のテンプレートに準拠しています。
  • 言語は Python を使用しているので、実行環境を用意する必要があります。
  • コードを実行する前に OAuth の設定をする必要があります。
  • 半自動化なので、多少の手作業が生じます。
  • 工数が多い人は使いづらいです。

日報の作成手順

通常の日報作成手順は以下の通りです。

今回は「作業詳細を入力」と「作業予定を入力」の部分を Google Calendar API を使って自動化してみました。

日報の半自動化ツールを使用すると以下のようになります。
作業詳細と作業予定を記載する手間を省きます。

日報の半自動化ツールはCLIで起動します。
出力もCLIで行われます。

日報のフォーマット

日報はメールとして全社員に送信されます。
メール本文のフォーマットは以下の通りです。

作業時間:{HH:MM}-{HH:MM} (休憩時間:{HH:MM})(深夜休憩時間:{HH:MM})
■作業報告
・{工数}({HH:MM})

■作業詳細
・{工数}
  -{作業詳細}

■作業予定
・{工数}
  -{作業詳細}

■課題
・{課題}

■所感
・{所感}

■特記事項
・{特記事項}

工数は「社内研修」や「定例会議」などの作業の分類項目のことです。

弊社が利用している勤怠管理アプリから、上記の日報の本文のうち{工数}と{HH:MM}が記載されたメールの下書きが出力されます。
そのため、今回は Google Calendar から予定を取得し、{作業詳細}を自動記入できるようにしました。

機能

日報の半自動化コードの機能は以下のとおりです。

基本機能

  • 当日 (コード実行日) と翌勤務日の Google Calendar の予定の一覧を表示します。
    • 参加の可否について「承諾」と回答した予定のみを表示します。
    • 表示しない予定を設定できるようにします。
  • 「課題」、「所感」、「特記事項」の欄をデフォルトで「なし」と記入したものを表示します。

追加機能 (コード内の変数を編集することで ON・OFF 切り替えられます)

  • 参加回答: 翌勤務日の予定のうち、Google Calendar の予定の参加の回答を保留にしている予定の可否を確認します。「承諾」または「辞退」を選択した場合は、Google Calendar にそれが反映されます。
  • 工数追加: 変数に代入した工数も表示します。
  • 作業追加: 変数に代入した作業詳細も表示します。

日報作成の半自動化コードの説明

作成したコードに使い方が書いてあるので見ていきます。

作成したコードの全体像
MakeDailyReport.py
from __future__ import print_function
import datetime
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

'''
初めてこれを実行する場合、
1. Google CloudコンソールでGoogle Calendar APIを有効にし、OAuthクライアントのkey(json形式)をダウンロードします。
	・詳しくはhttps://www.coppla-note.net/posts/tutorial/google-calendar-api/に載っています。
	・これに関しては改善の余地ありです。
2. 以下のコマンドでGoogle Client Libraryをインストールします。
	pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
'''

# your key pass and token pass, calendarID
KEYPASS = '保存したOAuthクライアントのkeyのフルパス'
TOKENDIRPASS = list('tokenを保存するディレクトリのフルパス')
CALID = 'カレンダーID(自分のメールアドレス)'
CALIDHOLIDAY = 'ja.japanese#holiday@group.v.calendar.google.com' #日本の休日カレンダー

# 日報に記入しない予定登録
IGNOREEVENT = ['ランチ', 'シャッフルランチ']

# 出欠が未回答または未定の場合に回答するかどうか
ATTENDANCE_CONFIRMATION = True

# 工数も出力に含めるかどうか
# 研修期間やオンボーディング期間など工数が固定されている場合のみ推奨
IS_MAN_HOURS = True
MAN_HOURS = '・990113:クラウドエース株式会社:ディビジョン・ユニット内活動(オンボーディング)\n'
IS_WORK_CONTENTS = False
WORK_CONTENTS = 'オンボーディング'


def print_output(output):
	opstr = ''.join(output)
	print(opstr)

def make_output(eventList):
	output = list()
	output.append('\n'+'-'*100+'\n')
	for day, events in enumerate(eventList):
		if IS_MAN_HOURS:
			output.append('\n')
			if day:
				output.append('■作業予定\n')
			else:
				output.append('■作業詳細\n')
			output.append(f'{MAN_HOURS}\n')
		if IS_WORK_CONTENTS:
			output.append(f'  -{WORK_CONTENTS}\n')
		for event in events:
			output.append(f'  -{event}\n')
		output.append('\n')
	output.append('\n■課題\n・なし\n\n■所感\n・なし\n\n■特記事項\n・なし\n\n')
	output.append('-'*100)
	return output

def change_attendance(isAttend, esum, attendees, myTurn, service, event):
	if isAttend in ['needsAction', 'tentative']:
		print(f'Will you attend {esum} ?(y/n/ON HOLD)', end=': ')
		nA_isAttend = input()
		if nA_isAttend=='y':
			attendees[myTurn]['responseStatus']= 'accepted'
		elif nA_isAttend=='n':
			attendees[myTurn]['responseStatus'] = 'declined'
		service.events().update(calendarId=CALID, 
								eventId=event['id'], 
								body=event).execute()

def get_event_list(service):
	eventList = list()
	daycnt = 0
	day = 0
	while daycnt<2:
		isHoliday, dinit, dend = is_holiday(service, day)
		if isHoliday:
			day += 1
			continue
		events_temp = get_events_temp(service, dinit, dend)
		events = list()
		isWorkingDays = True
		for event in events_temp:
			esum = event['summary']
			eventType = event.get('eventType', 'default')
			attendees = event.get('attendees', [])
			isAttend = 'declined'
			myTurn = 0
			for aidx, p in enumerate(attendees):
				if p.get('self', False):
					myTurn = aidx
					isAttend = p.get('responseStatus', 'needsAction')
			if ATTENDANCE_CONFIRMATION and day==1:
				change_attendance(isAttend, esum, attendees, myTurn, service, event)
			if eventType=='outOfOffice' and event['start']['dateTime']==dinit and event['end']['dateTime']==dend:
				isWorkingDays = False
			if esum not in IGNOREEVENT and eventType!='workingLocation' and isAttend=='accepted':
				events.append(esum)
		if isWorkingDays and len(events):
			eventList.append(events)
			daycnt += 1
		day += 1
	return eventList

def is_holiday(service, day):
	dinit = (datetime.date.today() + datetime.timedelta(days=day)).isoformat() + 'T00:00:00+09:00'
	dend = (datetime.date.today() + datetime.timedelta(days=day+1)).isoformat() + 'T00:00:00+09:00'
	events_result_holiday = service.events().list(calendarId=CALIDHOLIDAY, timeMin=dinit,
											timeMax=dend, singleEvents=True,
											orderBy='startTime').execute()
	return events_result_holiday.get('items', None), dinit, dend

def get_events_temp(service, dinit, dend):
	# Call the Calendar API
	eventsResult = service.events().list(calendarId=CALID, timeMin=dinit,
										timeMax=dend, singleEvents=True,
										orderBy='startTime').execute()
	return eventsResult.get('items', [])

def get_service():
	# If modifying these scopes, delete the file token.json.
	SCOPES = ['https://www.googleapis.com/auth/calendar.events']
	if TOKENDIRPASS[-1] != '/':
		TOKENDIRPASS.append('/')
	TOKENPASS = ''.join(TOKENDIRPASS)+'token.json'
	creds = None
	# The file token.json stores the user's access and refresh tokens, and is
	# created automatically when the authorization flow completes for the first
	# time.
	if os.path.exists(TOKENPASS):
		creds = Credentials.from_authorized_user_file(TOKENPASS, SCOPES)
	# If there are no (valid) credentials available, let the user log in.
	if not creds or not creds.valid:
		if creds and creds.expired and creds.refresh_token:
			creds.refresh(Request())
		else:
			flow = InstalledAppFlow.from_client_secrets_file(KEYPASS, SCOPES)
			creds = flow.run_local_server(port=0)
		# Save the credentials for the next run
		with open(TOKENPASS, 'w') as token:
			token.write(creds.to_json())

	return build('calendar', 'v3', credentials=creds)

def main():
	service = get_service()
	eventList = get_event_list(service)
	output = make_output(eventList)
	print_output(output)

if __name__ == '__main__':
	main()

この日報の半自動化コードはこちらのサイトに載っているコードを元に作成しています。

ライブラリのインポートは飛ばして、コメントアウトしたところ(初めてこれを〜)から見ていきます。

API の有効化・OAuth 認証の設定

MakeDailyReport.py
'''
初めてこれを実行する場合、
1. Google CloudコンソールでGoogle Calendar APIを有効にし、OAuthクライアントのkey(json形式)をダウンロードします。
	・詳しくはhttps://www.coppla-note.net/posts/tutorial/google-calendar-api/に載っています。
	・これに関しては改善の余地ありです。
'''

まず、API の有効化・OAuth 認証の設定をします。
私が作成した OAuth クライアントの key を配布するのが手っ取り早いですが、私が"key の配布"に抵抗があるのでやめました。

こちらの手順に従います。
(GUI の画面が少し異なるかもしれません)

ライブラリのインストール

MakeDailyReport.py
'''
2. 以下のコマンドでGoogle Client Libraryをインストールします。
	pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
'''

こちらのコマンドで Google Client 関連のライブラリをインストールします。

OAuth クライアント key 、カレンダー ID の登録

MakeDailyReport.py
# your key pass and token pass, calendarID
KEYPASS = '保存したOAuthクライアントのkeyのフルパス'
TOKENDIRPASS = list('tokenを保存するディレクトリのフルパス')
CALID = 'カレンダーID(自分のメールアドレス)'
CALIDHOLIDAY = 'ja.japanese#holiday@group.v.calendar.google.com' #日本の休日カレンダー
  • KEYPASS: OAuth クライアントのkeyのフルバスを記入します。
  • TOKENDIRPASSにtokenを保存するディレクトリのフルパスを記入します。
  • CALIDにカレンダーIDを記入します。個人の予定であればCALIDはメールアドレスとなります。

日報に記入しない予定の登録

MakeDailyReport.py
# 日報に記入しない予定登録
IGNOREEVENT = ['ランチ', 'シャッフルランチ']

IGNOREEVENTに日報に記入しない予定('昼休憩'など)を任意で追加します。

追加機能の設定

MakeDailyReport.py
# 出欠が未回答または未定の場合に回答するかどうか
ATTENDANCE_CONFIRMATION = True

# 工数も出力に含めるかどうか
# 研修期間やオンボーディング期間など工数が固定されている場合のみ推奨
IS_MAN_HOURS = False
MAN_HOURS = '・990113:クラウドエース株式会社:ディビジョン・ユニット内活動 (オンボーディング)\n'
IS_WORK_CONTENTS = False
WORK_CONTENTS = 'オンボーディング'

追加機能の設定をします。

  • ATTENDANCE_CONFIRMATION: Trueにすると、参加回答機能がONになります。
    • Will you attend {予定のタイトル} ?(y/n/ON HOLD): に対し、デフォルトは「変更なし」、yと回答すると「承諾」、nと回答すると「辞退」にします。
  • IS_MAN_HOURS: Trueにすると、工数追加機能がONになります。MAN_HOURSに入力した工数を表示します。
    • 工数が日によってあまり変わらない場合に有効です。オンボーディング期間はすべての作業内容はオンボーディング用の工数に付けるので、私は非常に重宝していたりします。
  • MAN_HOURS: 表示する工数を入力します。
    • 勤怠管理アプリに登録する工数が2つ以上の場合は日報のフォーマットをコピー&ペーストし、改行部分で\n\を入力してください。
  • IS_WORK_CONTENTS: Trueにすると、作業追加機能がONになります。カレンダーの予定にはない作業をWORK_CONTENTSに入力することで、それを表示します。
    • Google Calendar に登録しない業務のうち、ほぼ毎日遂行する業務がある場合に有効です。
    • 例えば、"社内研修"や"オンボーディング"などを記入します。
  • WORK_CONTENTS: 表示する作業詳細を入力します。
    • 作業詳細は1つまで入力できます。

処理の流れ

今回、全てのコードの解説は省略します。
main関数を基に処理の流れを確認します。

MakeDailyReport.py
def main():
	service = get_service()
	eventList = get_event_list(service)
	output = make_output(eventList)
	print_output(output)
  1. get_service関数で認証を行います。
  2. get_event_list関数で当日と翌勤務日の全ての予定を取得します。
  3. make_output関数で出力用にフォーマットを整えます。
  4. print_output関数で出力します。

検証

実際に使用するとどのように表示されるのかを試します。

MakeDailyReport.py
# 工数も出力に含めるかどうか
# 研修期間やオンボーディング期間など工数が固定されている場合のみ推奨
IS_MAN_HOURS = True
MAN_HOURS = '・990112:クラウドエース株式会社:ディビジョン・ユニット内活動 (コラボレーション)\n\
  -\n\
\n\
・990113:クラウドエース株式会社:ディビジョン・ユニット内活動 (オンボーディング)'
IS_WORK_CONTENTS = True
WORK_CONTENTS = 'オンボーディング'

OAuth 認証

$ python3 MakeDailyReport.py
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?{認証に関わる情報}

初めてコードを実行するときはOAuth認証を行います。
私はコード実行と同時に認証画面に遷移しました。
認証画面に遷移しない場合はhttps://~のURLをコピーし、ブラウザにペーストしてください。

ブラウザ上で自分のアカウントを選択し、「許可」をクリックすると、

The authentication flow has completed. You may close this window.

の画面になります。

その後、コマンドライン上で API の呼び出しが行われます。

参加確認

翌勤務日に参加の回答をしていない予定がある場合は確認します。

Will you attend Meet AYATOSHI~vol.68~ ?(y/n/ON HOLD):

デフォルトが「変更なし」なので、変更したくない場合は何も打たずにEnterキーで問題ありません。 今回はyと打ち、「承諾」にしました。 実際にカレンダーをみると"Meet AYATOSHI~vol.68~"が「承諾」に変更されていることがわかります。

カレンダーの予定の表示

全ての予定の参加の可否を確認したのちに結果が表示されます。

----------------------------------------------------------------------------------------------------

■作業詳細
・990112:クラウドエース株式会社:ディビジョン・ユニット内活動(コラボレーション)
  -

・990113:クラウドエース株式会社:ディビジョン・ユニット内活動 (オンボーディング)
  -オンボーディング
  -2on2


■作業予定
・990112:クラウドエース株式会社:ディビジョン・ユニット内活動 (コラボレーション)
  -

・990113:クラウドエース株式会社:ディビジョン・ユニット内活動 (オンボーディング)
  -オンボーディング
  -[Analytics&Data3]ユニット定例
  -Data/ML]リリースノートチェック定例


■課題
・なし

■所感
・なし

■特記事項
・なし

----------------------------------------------------------------------------------------------------

あとはこれを日報にコピー&ペーストするだけです。

仮に追加機能を以下のように設定すると…

MakeDailyReport.py
IS_MAN_HOURS = False
IS_WORK_CONTENTS = False

工数が省略され、カレンダーの予定のみが表示されます。

----------------------------------------------------------------------------------------------------
  -2on2

  -[Analytics&Data3]ユニット定例
  -Data/ML]リリースノートチェック定例
  -Meet AYATOSHI~vol.68~


■課題
・なし

■所感
・なし

■特記事項
・なし

----------------------------------------------------------------------------------------------------

この場合は、カレンダーの予定を適切な工数の位置に1つずつコピー&ペーストしていきます。

課題

この日報の半自動化コードには問題があります。

カレンダーの予定と対応する工数に振り分けて出力ができません

検証セクションの例では「ユニット定例」と「リリースノートチェック定例」は「ディビジョン・ユニット内活動(コラボレーション)」の工数ですが、「ディビジョン・ユニット内活動(オンボーディング)」の工数に書かれています。
本来の工数の位置にコピー&ペーストする必要があります。

定期的な予定は対応する工数と紐づけることはできると思いますが、非常に大変なので今回は実装していません。

まとめ

本記事では日報の半自動化コードの紹介をしました。

研修・オンボーディング期間は工数が少ないため、このコードが非常に役立っています。
Google Calendar で業務を管理している人には有用だと考えます。

OAuth や Google Calendar API の仕様を理解するために、Google Cloud プロジェクトを立ち上げて GUI で操作した経験が今でも役に立っていると感じます。

これからも自動化できそうなところを探していきます。

参考記事

https://www.coppla-note.net/posts/tutorial/google-calendar-api/

Discussion