入社のご報告&FLEXSCHEをGoogleカレンダーと連携してみた
自己紹介
初めまして、2025年5月に開発として入社したトネガワと申します。
現在入社から約3カ月が過ぎたところで、すでに展示会に参加させていただいたり、メーリングリストで対応させていただいております。至らぬ点も多々あるかと思いますが、どうぞよろしくお願いいたします。
好きなアルゴリズムはKaratsuba法です。基本的な式変形や発想の転換で計算時間が数十倍、数百倍も変わると気持ちが良いですね。
Googleカレンダーと連携
アドインの作り方
FLEXSCHEは標準機能の汎用性の高さがウリですが、新たに自分で機能を作ることもできます。 自分で機能を作る方法には「アクション」や「アドイン」などがあり、C++やPythonなどでコードを書いてより複雑な機能を実装できるのがアドインです。
まずは簡単なアドインを作ってみます。

スクリプト雛形生成ツールからアドインを作成します。ここでアドイン名、言語、実行方法を指定します。 アドインの実行方法はメニューバーから選んで使う、一定時間ごとに実行、作業を動かしたときに実行など多種多様です。
import clr clr.AddReference('FLEXSCHE.Interop.AIM') clr.AddReference('FLEXSCHE.Interop.SData') clr.AddReference('FLEXSCHE.Interop.GUI') clr.AddReference('FLEXSCHE.Interop.SDLib') clr.AddReference('FLEXSCHE.Interop.FSEditor') clr.AddReference('FLEXSCHE.Interop.GPEns') clr.AddReference('FLEXSCHE.Net') clr.AddReference('PyLib') from FLEXSCHE.Interop import AIM, SData, GUI, SDLib, FSEditor, GPEns from FLEXSCHE.XmlHelper import * from PyLib import Script def SelfRegistration(addIns: AIM.AddIns): ## この部分を変更した場合は「アドイン再登録」を実行してください addin = addIns.AddScript("movelog", int(GUI.AddInKeyType.AddInKeyHookAfterMovingOperation)) def movelog(keyEntity: AIM.KeyEntity): ## 不要な行は削除してください operRec = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDOperationRec)), SData.SDOperationRec) timeChart = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDTimeChart)), GUI.TimeChart) movingOperationInfo = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDMovingOperationInfo)), GUI.MovingOperationInfo) ## ここにコードを書いてください project = timeChart.Project dataspace: SData.SDSpace = Script.Cast(project.DataSpace, SData.ISDSpace) utility: SDLib.SDLUtility = Script.Cast(dataspace.SDLUtility, SDLib.ISDLUtility) resBefore = Script.Cast(movingOperationInfo.ResourceBefore, SData.SDResourceRec).Code resAfter = Script.Cast(movingOperationInfo.ResourceAfter, SData.SDResourceRec).Code startTime = utility.TimeToString(operRec.StartTime) endTime = utility.TimeToString(operRec.EndTime) S = f'{operRec.Code}: {resBefore}->{resAfter} {startTime} ~ {endTime} に変更されました' project.Panels.MessagePanel.AddLine('general', S) return True def alert(msg: str): Script.ShowMessageBox(msg, "FLEXSCHE", AIM.MessageBoxType.MBTOk) # モジュール名、アドイン名を"movelog"として登録 # Pythonを使う場合はアドイン設定からPythonのDLLを登録する必要があります
雛形生成ツールでOKを押すと上記のようなテンプレートが作成されます。 26行目~34行目の部分を書き足すと、作業を動かしたときに変更ログを出力するアドインができました。

Googleカレンダーと連携するアドインを作ろう!
休暇や会議の予定をGoogleカレンダーで管理していて、それを考慮して製造計画を立てたいといったシチュエーションは割とあるのではないでしょうか。
これはツールを使って
Googleカレンダー → エクセルファイル → CSVファイル → EDIF取り込み
としても実現できますが、取り込むたびに操作する項目が多く、できる操作も限定的です。
そこで、アドインでFLEXSCHE Components(APIのようなもの)とGoogleカレンダーAPIを使うと両者が直接やりとりできるようになり、より簡単に複雑な操作を実現できる!
というのがこの記事の本題になります。
それでは、早速作っていきましょう!
まずはGoogleカレンダー上の予定を自由カレンダーとして取り込む機能を実装したいと思います。
API周りの設定
APIの設定方法はGoogle Workspaceのチュートリアルに詳しく書かれており、基本的にこれを参考にすれば大丈夫です。
次に、資源ごとにカレンダーを作り、資源名 → カレンダーid の辞書を作っておきます。
カレンダーidはAPIの呼び出しに使います。
{ "作業員1" : "11111111111111111111111@group.calendar.google.com", "作業員2" : "23232323232323232323232@group.calendar.google.com", "作業員3" : "aaaaaaaaaaaaaaaaaaaaaaa@group.calendar.google.com", "作業員4" : "sisisisisisisisisisisii@group.calendar.google.com" } // calendar.json // カレンダーidはGoogleカレンダーの設定から確認できます
アドインの実装
まずはライブラリ部分を作りました。(長いため折りたたんでいます)
大まかに以下のようなことができます。
- カレンダーの予定を取得
- カレンダーの予定を追加
- 通知の時間や個数を設定
- 作業の色を属性(オーダー、工程、資源など)に合わせて指定
- カレンダーの予定を削除
- FLEXSCHEに自由カレンダーを登録
ライブラリ部分(クリックで展開)
# CalendarAPI.py # FLEXSCHEアドイン用 import clr clr.AddReference('FLEXSCHE.Interop.AIM') clr.AddReference('FLEXSCHE.Interop.SData') clr.AddReference('FLEXSCHE.Interop.GUI') clr.AddReference('FLEXSCHE.Interop.SDLib') clr.AddReference('FLEXSCHE.Interop.FSEditor') clr.AddReference('FLEXSCHE.Interop.GPEns') clr.AddReference('FLEXSCHE.Net') clr.AddReference('PyLib') from FLEXSCHE.Interop import AIM, SData, GUI, SDLib, FSEditor, GPEns from FLEXSCHE.XmlHelper import * from PyLib import Script # googleカレンダーapi from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError # インポート import datetime import os.path import inspect import time import datetime import json # ------------------------ パラメータ ------------------------ # スコープ. アクセス権を指定できる(読み込み、書き込みの許可) # 変えたらtoken.jsonを消す SCOPES = ["https://www.googleapis.com/auth/calendar"] # クエリごとのsleep時間(秒, 小数可) # apiを大量に呼ばないようにするため WAITING_TIME = 0.5 # トークン、カレンダーid等 # token, credentials, calendarをコードと同じ階層に置く token_path = os.path.join(os.path.dirname(__file__), "token.json") creds_path = os.path.join(os.path.dirname(__file__), "credentials.json") calendar_path = os.path.join(os.path.dirname(__file__), "calendar.json") __Calendars = {} # タイムゾーンの設定 # 東京の場合 # 'dateTime': '2025-05-28T09:00:00+09:00', 末尾に+9:00を追加する(offset_timezone) # 'timeZone': 'Asia/Tokyo', timezone = "Asia/Tokyo" offset_timezone = "+09:00" # ----------------------------------------------------------- class FLEXSCHEData: def __init__(self, keyEntity: AIM.KeyEntity) -> None: self.env = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDEnvironment)), GUI.Environment) self.project: GUI.Project = self.env.Project self.ds: SData.SDSpace = Script.Cast(self.project.DataSpace, SData.ISDSpace) self.ut: SDLib.SDLUtility = Script.Cast(self.ds.SDLUtility, SDLib.ISDLUtility) # デバッグ用 def alert(msg: any) -> None: Script.ShowMessageBox(str(msg), "FLEXSCHE", AIM.MessageBoxType.MBTOk) # カレンダーid(json)を読み込む def InitCalendar(is_first: bool = True) -> None: if is_first: global __Calendars if calendar_path != '': with open(calendar_path, encoding="utf-8_sig") as f: __Calendars = json.load(f) is_first = False # recourcecodeのカレンダーidを取得(なければ空文字列) def GetCalendarId(rescode: str) -> str: InitCalendar() if rescode in __Calendars: return __Calendars[rescode] else: return "" # calendaridのresidを取得(複数ある場合初めに見つけたやつを返す. なければ空文字列) def GetResId(calendarid: str) -> str: InitCalendar() for k, v in __Calendars.items(): if v == calendarid: return k return "" # カレンダー(の参照)を取得 def GetAllCalendars() -> map: InitCalendar() return __Calendars # tokenをチェックしてcredsを返す def CheckToken(): creds = None if os.path.exists(token_path): creds = Credentials.from_authorized_user_file(token_path, SCOPES) 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(creds_path, SCOPES) creds = flow.run_local_server(port=0) with open(token_path, "w") as token: token.write(creds.to_json()) time.sleep(WAITING_TIME) return creds # 20250528T090000 -> 2025-05-28T09:00:00 def InsertHyphen(s: str) -> str: return s[0:4] + '-' + s[4:6] + '-' + s[6:8] + 'T' + s[9:11] + ':' + s[11:13] + ':00' # 2025-05-28T09:00:00 -> 20250528T090000 def DeleteHyphen(s: str) -> str: return s[0:4] + s[5:7] + s[8:13] + s[14:16] # FLEXSCHE時間 -> GoogleCalendar時間 def ToGoogleTime(data: FLEXSCHEData, T: float) -> str: S = str(data.ut.TimeToISO8601(T)) assert len(S) != 0 if len(S) < 13: S += 'T0000' return InsertHyphen(S) + offset_timezone # GoogleCalendar時間 -> FLEXSCHE時間 def ToFlexscheTime(data: FLEXSCHEData, T: str) -> float: return data.ut.TimeFromISO8601(DeleteHyphen(T)) # 全てのイベントを自由カレンダーとして取り込む (成功したか返す) def ImportSchedulesAsFreeCalendars(data: FLEXSCHEData, calendarid: int) -> bool: creds = CheckToken() try: service = build("calendar", "v3", credentials=creds) events = service.events().list(calendarId=calendarid).execute()["items"] rescode = GetResId(calendarid) res = data.ds.ResourceSet.get_ResourceRecByCode(rescode) for event in events: Ts = event['start']['dateTime'] if 'dateTime' in event['start'] else event['start']['date'] + 'T00:00:00' + offset_timezone Ts = ToFlexscheTime(data, Ts) Te = event['end']['dateTime'] if 'dateTime' in event['end'] else event['end']['date'] + 'T00:00:00' + offset_timezone Te = ToFlexscheTime(data, Te) rr = data.ds.FreeCalendarSet.CreateFreeCalendarRec(SData.SDFreeCalendarType.SDFCTypeAssociatedToTimeSeries, True) rr.set_StartTime(Ts) rr.set_Duration(Te - Ts) rr.ResourceRec = res rr.set_Comment('name', event['summary'] if 'summary' in event else '無題のイベント') time.sleep(WAITING_TIME) return True except HttpError as error: return False # color = {0:資源ごと, 1:オーダーごと, 2:工程ごと} def GetColor(op: SData.SDataType.SDTypeOperation, res: SData.SDataType.SDTypeResource, color: int) -> int: ans = -1 if color == 0: ans = hash(res.Code) elif color == 1: ans = hash(op.SingleOrderRec.Code) elif color == 2: ans = hash(op.Specifier) else: ans = 0 # colorが無効な値なら資源ごとにする return (ans + 2) % 12 # リスト中の作業を全て追加(成功したか返す) def AddAllEvents(data: FLEXSCHEData, ops: list, color: int = 0) -> bool: creds = CheckToken() try: service = build("calendar", "v3", credentials=creds) for op in ops: for i in range(op.CountOfTaskRecs): task = op.get_TaskRec(i) if task.IsAssigned == 0: continue Ts = ToGoogleTime(data, task.AssignmentStartTime) Te = ToGoogleTime(data, task.AssignmentEndTime) # direction = {0:作業追加時から未来方向に, 1:作業開始前から過去方向に} # offset(>=0)分動かした時刻に通知 def AddReminder(event: map, direction: bool, offset: int) -> None: assert offset >= 0 if direction == False: Tn = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))) Sdiff = (datetime.datetime.fromisoformat(Ts) - Tn).total_seconds() - offset * 60 # 過去には通知しない if Sdiff < 0: return event['reminders']['overrides'].append({ 'method': 'popup', 'minutes': Sdiff // 60 }) else: event['reminders']['overrides'].append({ "method": 'popup', "minutes": offset }) # 場所を設定 def SetPlace(event: map, place: str) -> None: event['location'] = place # 説明を設定 def SetDetail(event: map, detail: str) -> None: event['description'] = detail event = { 'summary': op.Code, 'start': { 'dateTime': Ts, 'timeZone': timezone, }, 'end': { 'dateTime': Te, 'timeZone': timezone, }, 'colorId': GetColor(op, task.AssignedResourceRec, color), 'reminders': { 'useDefault': False, 'overrides': [] } } AddReminder(event, False, 0) # 作業登録時に通知 AddReminder(event, True, 1) # 作業開始1分前に通知 SetPlace(event, op.ProcRec.get_Comment('場所')) # 工程マスタのコメント:場所を登録 SetDetail(event, op.ProcRec.get_Comment('説明')) # 工程マスタのコメント:場所を登録 event = service.events().insert(calendarId=GetCalendarId(task.AssignedResourceRec.Code), body=event).execute() time.sleep(WAITING_TIME) return True except HttpError as error: return False # 指定した作業を追加(成功したか返す) def AddEvent(data: FLEXSCHEData, op: SData.SDataType.SDTypeOperation, color: int = 0) -> bool: return AddAllEvents(data.ut, [op], color) # calendaridの全てのイベントを削除(成功したか返す) def DeleteAllEvents(calendarid: str) -> bool: creds = CheckToken() try: service = build("calendar", "v3", credentials=creds) events = service.events().list(calendarId=calendarid).execute()["items"] for event in events: eventid = event['id'] service.events().delete(calendarId=calendarid, eventId=eventid).execute() time.sleep(WAITING_TIME) return True except HttpError as error: return False # 指定したイベントを削除 (成功したか返す) def DeleteEvent(calendarid: str, event: map) -> bool: creds = CheckToken() try: service = build("calendar", "v3", credentials=creds) eventid = event['id'] service.events().delete(calendarId=calendarid, eventId=eventid).execute() return True except HttpError as error: return False
登録するアドインのコードは以下のようになります。
# ImportSchedule.py from CalendarAPI import * def SelfRegistration(addIns: AIM.AddIns): addin = addIns.AddScript("ImportSchedule", int(GUI.AddInKeyType.AddInKeyMenuHelp)) addin.MenuString = "予定取込み(自由カレンダー)" def ImportSchedule(keyEntity: AIM.KeyEntity): data = FLEXSCHEData(keyEntity) ok = True for k, v in GetAllCalendars().items(): ok &= ImportSchedulesAsFreeCalendars(data, v) data.project.FireEvent(GUI.FSEvent.FSEventFreeCalendarRecsAreUpdated) if ok: alert("取り込みに成功しました") else: alert("取り込みに失敗しました") return True
動作
以下のようなカレンダーがあります。
会議は作業員1と3、休みは作業員4が対象です
先ほどのアドインを実行してリスケすると、カレンダー上の予定を考慮して計画を立てることができました。
逆にGoogleカレンダーに作業を反映することもできます。
アドイン:全作業をカレンダーに登録(クリックで展開)
# RegisterSchedule.py from CalendarAPI import * def SelfRegistration(addIns: AIM.AddIns): addin = addIns.AddScript("RegisterSchedule", int(GUI.AddInKeyType.AddInKeyMenuHelp)) addin.MenuString = "全予定登録" addin.Order = 1 def RegisterSchedule(keyEntity: AIM.KeyEntity): data = FLEXSCHEData(keyEntity) opset = data.ds.OperationSet ops = [] for i in range(opset.CountOfRecords): ops.append(opset.get_OperationRec(i)) ok = AddAllEvents(data, ops, 1) if ok: alert("登録に成功しました") else: alert("登録に失敗しました") return True
様々なデバイスと連携
前章ではFLEXSCHEとGoogleカレンダーを連携させましたが、Googleカレンダーを介して様々なデバイスとも連携することができます。
スマートウォッチと連携しました。
作業員それぞれが自身の予定だけ取り込むようにして、作業が変更されたときや開始直前に通知を受け取って作業を進めてもらうというような使い方ができます!