入社のご報告&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カレンダーを介して様々なデバイスとも連携することができます。
スマートウォッチと連携しました。
作業員それぞれが自身の予定だけ取り込むようにして、作業が変更されたときや開始直前に通知を受け取って作業を進めてもらうというような使い方ができます!

