System Settings REST API で組織図の変更を反映する

4 月の組織再編で、次のように組織を変えることになったとします。

  • 営業部の下に、西日本営業課を新設する
  • 開発部の下の品質保証室を、品質管理部に改称して会社の直下へ移す
  • 総務部の下の情報システム室を廃止する

この記事では、このうち新設・改称・親組織の付け替えと、組織ごとの所属メンバの入れ替えを、REST API で反映する方法を説明します。組織の廃止は「System Settings REST API で組織を廃止する」(以下、組織廃止の記事)で扱います。

組織の登録や編集は、[システム設定]>[組織一覧]でもできます。[組織一括登録]を使えば、CSV から一度に 300 組織まで登録できます。1 回かぎりであれば、そちらのほうが手軽です。

REST API を使うのは、この作業が毎年くり返される場合です。人事システムから出力した組織図をそのまま流し込めるようにしておけば、次回からは CSV を差し替えて実行するだけで済みます。

所属メンバは、組織ごとの名簿で入れ替えます。 組織再編では、旧組織の所属メンバがそのまま新しい組織へ移るとは限らず、一部が別の部署へ移ることもあります。組織ごとに再編後の名簿を用意して入れ替えれば、移り方によって手順が変わりません。ユーザの登録と、人ひとりの異動は「System Settings REST API で入社と異動を反映する」で説明しています。

コード例は Python 3 で、HTTP リクエストの送信に Requests パッケージを使います。スクリプトは 1 本で、接続先の設定と組織一覧の取得のあと、改称・組織図・名簿の 3 つの CSV(rename.csv・orgchart.csv・roster.csv)をすべて読んで照合し、問題がなければ順に反映します。全体は「コード全体」に載せています。

組織に起きる 5 つのこと

組織再編で組織に起きることは、次の 5 つに分けられます。どれが起きたかで、書く CSV と、使う API と、組織 ID が変わるかどうかが決まります。

組織に起きたこと書く CSV使う API組織 ID
新しく組織が追加されるorgchart.csvQgroup/add新しく採番される
組織ツリー上の位置が変わるorgchart.csvQgroup/update(parentQgroupId)変わらない
位置は変わらず、組織名が変わるrename.csvQgroup/update(name)変わらない
位置も組織名も変わるrename.csv と orgchart.csvQgroup/update(name と parentQgroupId)変わらない
組織が廃止される組織廃止の記事組織廃止の記事—

冒頭の例では、西日本営業課が表の 1 行目、品質保証室が 4 行目、情報システム室が 5 行目にあたります。5 行目の廃止だけは、[削除予定]の設定と削除日の削除の 2 段階になり、元に戻せない操作を含むので、組織廃止の記事で説明します。

所属メンバの入れ替えは、この 5 つとは別に、組織ごとの名簿 roster.csv で反映します。

位置や組織名が変わる 3 つの行には、もう 1 つのやり方があります。 新しい組織を追加して、旧組織を廃止する方法です。組織図は同じ形になりますが、組織 ID は別のものになります。どちらにするかは、CSV を書く前に、次の「既存の組織を直すか、新しい組織を作るか」で決めます。

既存の組織を直すか、新しい組織を作るか

選ぶ基準は、次の 3 つです。

  • 過去の記録の見え方。 既存の組織を直すと、組織 ID が変わらないので、すでに終わったケースに残っている組織も、新しい組織名・新しい位置のものとして表示されます。新しい組織を作れば、過去の記録は旧組織のまま残ります。再編の前後を別の組織として残したいかどうかで決めます。
  • 進行中のケースとアプリの設定。 既存の組織を直せば、その組織に割り当てられている進行中のケース、組織型のデータ項目に入っている値、その組織を指しているアプリの設定は、そのまま引き継がれます。新しい組織を作った場合、旧組織を指しているアプリの設定は、新しい組織に直すことになります。旧組織に割り当て済みの進行中のケースは、旧組織のまま処理されます。
  • 作業の量。 既存の組織を直すのは、rename.csv と orgchart.csv だけで済みます。新しい組織を作る場合は、組織廃止の記事の 2 段階が加わります。子組織を持つ組織では、作業の量の差がさらに大きくなります。既存の組織を直せば子組織はそのまま下に残りますが、新しく作る場合は下位の組織も作り直すことになります。

新しい組織を作る場合に問題になるのは、旧組織を指しているアプリの設定です。 各アプリのアプリ管理者に直してもらうほか、組織廃止の記事の「処理担当者設定の一括の付け替え」で、まとめて付け替えることもできます。

手間は多くなりますが、新しい組織を作るほうが安全とも言えます。 動いているケースに影響を与えないためです。既存の組織の所属メンバを入れ替えると、その組織を処理担当者に指定しているアプリでは、すでに生成されているタスクの担当者は変わりませんが、入れ替えたあとに生成されるタスクから担当者が変わります。進行中のケースも例外ではありません。それが望ましい場合もありますが、基本的には慎重に行う作業です。

既存の組織を直すほうを選び、廃止する組織がなければ、作業はこの記事だけで完結します。 新しい組織を作るほうを選んだ場合と、廃止する組織がある場合は、このスクリプトのあとに組織廃止の記事の作業が続きます。

新しい組織を作るほうを選んだ場合は、CSV の書き方に次の 2 点が加わります。

  • 新しい組織に旧組織と同じ名前を付ける場合は、先に旧組織を 営業部(旧) のような名前に改称します。その改称も rename.csv に書きます。
  • 新組織の名簿は roster.csv に書き、旧組織は書きません。所属メンバを新組織へ移す場合も、旧組織の名簿はそのままにします。旧組織を処理担当者に指定しているアプリで、削除日まで担当者がいなくなるのを防ぐためです。旧組織を[削除予定]にしてから削除するまでの間に、アプリ管理者が処理担当者設定を新組織に直します。その流れは、組織廃止の記事で説明します。

用意する CSV

CSV は、改称・組織図・名簿の 3 つに分けて用意します。

改称する組織は、旧組織名と新組織名を並べ、rename.csv として保存します。

old_name,new_name
品質保証室,品質管理部

新しい組織図は、組織名と親組織名を並べ、orgchart.csv として保存します。組織名は、改称を先に反映するので、改称した後の名前で書きます。

name,parent
営業部,株式会社サンプル
西日本営業課,営業部
品質管理部,株式会社サンプル

parent には、親組織の名前を書きます。 最上位の組織(会社全体を表す組織)の下に部を作る場合は、[システム設定]>[組織一覧]の先頭に表示されている組織の名前を書きます。新設する組織を親組織にする場合は、親になる行を先に書いてください。

組織の親子関係は[組織一覧]で確認できる

組織図の CSV には、変える組織だけでなく、変えない組織も書けます。 上の例では、営業部が変えない組織です。スクリプトは、各行の組織を組織名で探し、次のように扱います。

  • 見つからなければ、新設します。
  • 見つかって、親組織が違えば、付け替えます。
  • 見つかって、親組織が同じであれば、何もしません。

どの行も、何度実行しても結果は変わりません。そのため、人事システムから出力した組織図を、変える組織だけに絞らずにそのまま使えます。parent に書いた親組織が見つからない行があると、新設はせず、どの行も反映せずに止まります。

組織図の CSV に書かなかった組織は、そのまま残ります。 廃止するかどうかは、組織廃止の記事で別に指定します。組織図に無いことを廃止と見なすと、書き漏らした組織まで廃止することになるためです。廃止する組織に子組織があり、子組織を残す場合は、子組織の親組織をこの CSV で付け替えておきます。子組織を持つ組織は削除できないためです。

再編後の名簿は、roster.csv として保存します。1 行目は見出し行で、スクリプトは読み飛ばします。2 行目からは、1 行に 1 つの組織を書き、組織名のあとに、その組織に所属する人のメールアドレスを並べます。組織名は、orgchart.csv と同じく、改称した後の名前で書きます。同じ組織を 2 行以上書くと、照合の段階で止まります。

qgroup,emails
営業部,taro.tanaka@example.com
西日本営業課,hanako.suzuki@example.com,jiro.sato@example.com

名簿に書いた組織の所属は、その行に書いた人に置き換わります。 その組織に残る人も含めて、再編後の全員を書いてください。行に無い人はその組織から外れ、メールアドレスを 1 つも書かない行は、その組織の所属を 0 人にします。1 人が複数の組織に所属する場合は、それぞれの組織の行に書きます。所属が変わらない組織は、名簿に書かなければそのまま残ります。

名簿に書くユーザは、登録済みである必要があります。 入社するユーザは、「System Settings REST API で入社と異動を反映する」の手順で先に登録しておきます。

廃止する組織にしか所属していないユーザがいれば、移動先の組織の行に、そのユーザも書きます。組織を削除すると、そのユーザはどの組織にも所属しない状態になるためです。

API を呼び出す準備

実行するユーザには、ユーザ管理権限と、Basic 認証による API アクセスの許可が必要です。 その設定の手順、API パスワードの調べ方、接続先と認証情報の用意、呼び出しをまとめる check と call の 2 つの関数は、「System Settings REST API で入社と異動を反映する」の「API を呼び出す準備」と同じです。この記事のコードも、その check と call を使います。

スクリプトは、ユーザ管理権限だけで動きます。 システム管理権限が要るのは、作業のあとの「アプリ設定の見直し」で[組織を参照しているアプリ一覧]を使う場合だけです。

組織一覧の取得

API が使うのは組織の ID で、CSV にあるのは組織名です。 先に組織の一覧を取得して、組織名から組織を引く辞書を作ります。組織名はワークフロー基盤の中で一意なので、組織名で引けます。/API/User/Qgroup/list を limit と start でくり返し呼ぶ書き方は、「System Settings REST API で入社と異動を反映する」の「組織一覧の取得」と同じです。

辞書には、組織 ID だけでなく、応答の 1 件をまるごと入れます。 groups['品質保証室']['id'] が組織 ID、groups['品質保証室']['parentQgroupId'] が親組織の ID です。親組織を付け替えるかどうかの判断に使います。

照合:組織とユーザの把握

組織や所属を変える前に、3 つの CSV をすべて読み、照合を済ませます。 名簿に書いたユーザの ID も、ここで控えておきます。改称・組織図の反映・所属の入れ替えを、あいだにユーザの検索を挟まずに続けて実行するためです。照合で見つけた問題は errors に集め、最後にまとめて表示します。

with open('rename.csv', encoding='utf-8') as f:
    renames = list(csv.DictReader(f))
with open('orgchart.csv', encoding='utf-8') as f:
    orgchart = list(csv.DictReader(f))
with open('roster.csv', encoding='utf-8') as f:
    reader = csv.reader(f)
    next(reader)
    rows = [(row[0], [email for email in row[1:] if email])
            for row in reader if row]

errors = []

照合は、組織一覧の組織名を集めた known に対して行います。 改称と新設で組織名が増減するので、照合を進めながら known も直し、組織図と名簿は改称後・新設後の名前で確かめます。組織一覧から作った辞書 groups はここでは書き換えず、反映の節で使います。

改称は、旧組織名が known に無い行を止めます。 ただし、新組織名の組織があれば、改称済みとして通します。どちらも無い行を残したまま先へ進むと、あとの「組織図の反映」で、改称後の名前の組織が新しく作られてしまうためです。

新組織名が、すでにある組織の名前と同じ行も、照合で止めます。 組織名はワークフロー基盤の中で一意なので、改称の API がエラーを返すためです。同じ CSV の前の行の改称で空く名前であれば、通します。

known = set(groups)
for row in renames:
    if row['old_name'] in known:
        if row['new_name'] in known:
            errors.append((row['new_name'], 'すでにある組織名です'))
        known.discard(row['old_name'])
        known.add(row['new_name'])
    elif row['new_name'] not in known:
        errors.append((row['old_name'], '組織一覧にありません'))

組織図は、親組織が見つかるかを確かめます。 親組織は、改称後の組織一覧にあるか、同じ CSV のその行より前に name として出てくれば、見つかったものとして扱います。見つからない原因には、親組織になる行が後ろにある場合と、人事システムとワークフロー基盤とで組織名の表記が食い違っている場合があります。どちらも、行の組織名と親組織名を並べて出力します。

for row in orgchart:
    if row['parent'] not in known:
        errors.append((row['name'], '親組織が見つかりません:', row['parent']))
    known.add(row['name'])

名簿は、組織とユーザがすべて見つかるかと、同じ組織が 2 行以上ないかを確かめます。 組織は、組織図で新設する組織も含めて探します。ユーザは、メールアドレスで探します。ユーザ名には同姓同名があり得るためです。見つかったユーザの ID を組織ごとに roster に入れ、「所属メンバの入れ替え」でそのまま渡します。/API/User/Quser/find は、未登録のメールアドレスには 400 を返します。それだけを「見つからない」として扱い、認証エラーなどほかのエラーは check で止めます。

names = [name for name, _ in rows]
for name in sorted({name for name in names if names.count(name) > 1}):
    errors.append((name, '2 行以上あります'))

roster = {}
for name, emails in rows:
    if name not in known:
        errors.append((name, '組織が見つかりません'))
    memberships = []
    for email in emails:
        found = requests.get(f'{BASE}/API/User/Quser/find', auth=AUTH,
                             params={'email': email})
        if found.status_code != 400:
            check(found)
        if not found.ok:
            errors.append((email, 'ユーザが見つかりません'))
            continue
        memberships.append({'quserId': found.json()['quser']['id']})
    roster[name] = memberships

問題が 1 件でもあれば、何も反映せずに止まります。 一部だけ反映された状態になると、どこまで進んだか分からなくなるためです。

if errors:
    for error in errors:
        print(*error)
    raise SystemExit('何も反映しませんでした')

改称:組織名の変更

組織を更新する API は /API/UGA/Qgroup/update です。id が必須で、指定しなかった項目は変更されません。組織名だけを渡せば、親組織も所属も組織 ID もそのまま残ります。

for row in renames:
    old = groups.get(row['old_name'])
    if not old:
        print(row['new_name'], '改称済みです')
        continue
    call('POST', '/API/UGA/Qgroup/update',
         data={'id': old['id'], 'name': row['new_name']})
    groups[row['new_name']] = groups.pop(row['old_name'])
    print(row['old_name'], '→', row['new_name'])

辞書 groups のキーも、新しい組織名に直します。 次の節では、改称後の名前で組織を探すためです。

新設と位置の変更:組織図の反映

組織を追加する API は /API/UGA/Qgroup/add で、name と parentQgroupId が必須です。応答の qgroup に、追加した組織の内容が入ります。親組織を付け替えるときは、/API/UGA/Qgroup/update に id と parentQgroupId を渡します。

次の関数は、組織名で辞書を探し、見つからなければ追加し、親組織が違えば付け替えます。追加した組織は辞書にも入れ、同じ CSV の後ろの行が親組織として指せるようにします。新設した組織には、まだ誰も所属していません。親組織を付け替えた組織には、所属しているユーザがそのまま残ります。どちらも、所属は次の「所属メンバの入れ替え」で反映します。

def ensure_group(name, parent_id):
    group = groups.get(name)
    if not group:
        r = call('POST', '/API/UGA/Qgroup/add',
                 data={'name': name, 'parentQgroupId': parent_id})
        groups[name] = r.json()['qgroup']
        return '新設しました'
    if group['parentQgroupId'] != parent_id:
        call('POST', '/API/UGA/Qgroup/update',
             data={'id': group['id'], 'parentQgroupId': parent_id})
        group['parentQgroupId'] = parent_id
        return '親組織を付け替えました'
    return '変更はありません'
for row in orgchart:
    print(row['name'], ensure_group(row['name'], groups[row['parent']]['id']))

所属メンバの入れ替え

組織の所属ユーザを一括で設定する API は /API/UGA/Membership/set です。qgroupId と、その組織に所属するユーザの一覧を渡すと、指定した組織の所属が、渡した一覧に置き換わります。

組織ごとに、再編後の名簿を渡します。 roster.csv の 1 行が、1 回の呼び出しになります。

主として属する組織は、この API では指定しませんが、変わることがあります。 名簿で加わった人は、それまでどの組織にも所属していなければ、その組織が主として属する組織になります。すでにほかの組織に所属していれば変わりません。名簿から外れた人は、その組織が主として属する組織だった場合に、別の組織に変わります。ほかに所属する組織が無ければ、どの組織にも所属しない状態になり、主として属する組織もなくなります。実行したあとで、名簿から外した人の主として属する組織を確認してください。

for name, memberships in roster.items():
    call('POST', '/API/UGA/Membership/set',
         json={'qgroupId': groups[name]['id'], 'memberships': memberships})
    print(name, len(memberships), '人にしました')

作業の順序

スクリプトは、3 つの CSV をすべて照合してから、この順に反映します。

  1. rename.csv。 改称を反映します。次の orgchart.csv を改称後の名前で書けるように、先に処理します。
  2. orgchart.csv。 新設と、親組織の付け替えを反映します。所属先になる組織が、この時点で揃います。
  3. roster.csv。 組織ごとの再編後の名簿を反映します。

実行を終えたら、全体にアナウンスします。 内容は「アプリ設定の見直し」に書いています。

組織を廃止する場合は、同じ日に、このスクリプト、全体アナウンス、組織廃止の記事の[削除予定]の設定の順に行います。 アナウンスには削除日も書きます。

コード全体

3 つの CSV を順に処理するスクリプトです。実行する前に、次の「実行の進め方」を読んでください。

import csv
import requests
from requests.auth import HTTPBasicAuth

BASE = 'https://example.questetra.net'
AUTH = HTTPBasicAuth('admin@example.com', '{API パスワード}')
LIMIT = 10


def check(r):
    if not r.ok:
        print(r.status_code, r.text)
        r.raise_for_status()
    return r


def call(method, path, **kwargs):
    return check(requests.request(method, f'{BASE}{path}', auth=AUTH, **kwargs))


groups = {}
start = 0
while True:
    r = call('GET', '/API/User/Qgroup/list',
             params={'start': start, 'limit': LIMIT})
    qgroups = r.json()['qgroups']
    groups.update({g['name']: g for g in qgroups})
    if len(qgroups) < LIMIT:
        break
    start += LIMIT

with open('rename.csv', encoding='utf-8') as f:
    renames = list(csv.DictReader(f))
with open('orgchart.csv', encoding='utf-8') as f:
    orgchart = list(csv.DictReader(f))
with open('roster.csv', encoding='utf-8') as f:
    reader = csv.reader(f)
    next(reader)
    rows = [(row[0], [email for email in row[1:] if email])
            for row in reader if row]

errors = []

known = set(groups)
for row in renames:
    if row['old_name'] in known:
        if row['new_name'] in known:
            errors.append((row['new_name'], 'すでにある組織名です'))
        known.discard(row['old_name'])
        known.add(row['new_name'])
    elif row['new_name'] not in known:
        errors.append((row['old_name'], '組織一覧にありません'))

for row in orgchart:
    if row['parent'] not in known:
        errors.append((row['name'], '親組織が見つかりません:', row['parent']))
    known.add(row['name'])

names = [name for name, _ in rows]
for name in sorted({name for name in names if names.count(name) > 1}):
    errors.append((name, '2 行以上あります'))

roster = {}
for name, emails in rows:
    if name not in known:
        errors.append((name, '組織が見つかりません'))
    memberships = []
    for email in emails:
        found = requests.get(f'{BASE}/API/User/Quser/find', auth=AUTH,
                             params={'email': email})
        if found.status_code != 400:
            check(found)
        if not found.ok:
            errors.append((email, 'ユーザが見つかりません'))
            continue
        memberships.append({'quserId': found.json()['quser']['id']})
    roster[name] = memberships

if errors:
    for error in errors:
        print(*error)
    raise SystemExit('何も反映しませんでした')

for row in renames:
    old = groups.get(row['old_name'])
    if not old:
        print(row['new_name'], '改称済みです')
        continue
    call('POST', '/API/UGA/Qgroup/update',
         data={'id': old['id'], 'name': row['new_name']})
    groups[row['new_name']] = groups.pop(row['old_name'])
    print(row['old_name'], '→', row['new_name'])


def ensure_group(name, parent_id):
    group = groups.get(name)
    if not group:
        r = call('POST', '/API/UGA/Qgroup/add',
                 data={'name': name, 'parentQgroupId': parent_id})
        groups[name] = r.json()['qgroup']
        return '新設しました'
    if group['parentQgroupId'] != parent_id:
        call('POST', '/API/UGA/Qgroup/update',
             data={'id': group['id'], 'parentQgroupId': parent_id})
        group['parentQgroupId'] = parent_id
        return '親組織を付け替えました'
    return '変更はありません'


for row in orgchart:
    print(row['name'], ensure_group(row['name'], groups[row['parent']]['id']))

for name, memberships in roster.items():
    call('POST', '/API/UGA/Membership/set',
         json={'qgroupId': groups[name]['id'], 'memberships': memberships})
    print(name, len(memberships), '人にしました')

実行の進め方

POST の API は、ワークフロー基盤のデータをその場で書き換えます。 試しに実行する仕組みはなく、どの CSV も、流した時点で本番が変わります。動きを事前に確かめたい場合は、本番とは別のワークフロー基盤で試してください。

改称だけは、少しずつ流しながら進めることもできます。 orgchart.csv と roster.csv を見出し行だけにして、rename.csv の行を少しずつ増やしながら実行します。改称済みの行は飛ばされるので、すでに流した行はそのまま残せます。スクリプトは 3 つの CSV をすべて開くので、ファイルが無いと止まります。見出し行だけの CSV は、何も反映せずに次へ進みます。流すたびに、[システム設定]>[組織一覧]で組織名が意図どおりかを確認します。

組織図の反映と所属の入れ替えは、行を絞らず、1 回で実行します。 組織図や所属を変えるたびに、各ユーザの「新規開始」の一覧が計算し直されるためです。改称を先に流した場合は、rename.csv はそのままにして、orgchart.csv と roster.csv に本来の内容を書いて実行します。

反映のされ方は、画面によって違います。 [システム設定]>[組織一覧]には、実行した直後から反映されます。一方、各ユーザの「新規開始」の一覧は、数分遅れて変わります。実行直後は、まだ変わっていないことがあります。

途中で止まったときは、スクリプトを最初から実行し直します。 照合で止まったときは、表示された問題を CSV で直します。何も反映されていないので、そのまま実行し直せます。反映の途中で止まったときも、最初から実行し直します。改称済みの行と、変更のない組織は飛ばされ、名簿は同じ内容で置き換え直されるので、すでに反映した行をやり直しても結果は変わりません。

実行を終えたら、「アプリ設定の見直し」の内容で全体にアナウンスします。

アプリ設定の見直し

既存の組織を直した場合、アプリの設定を直す必要はありません。 組織 ID が変わらないので、アプリが指している組織は変わらないままです。新しい組織を作った場合は、旧組織を指しているアプリの設定を、組織廃止の記事の流れで各アプリのアプリ管理者に直してもらいます。

ただし、アプリの設定を直す必要がなくても、処理担当者が意図どおりになっているとはかぎりません。 次の 3 つは、どれもエラーにならず、アプリ管理者にも知らせが届きません。実行する人がアプリを開いて確かめられるともかぎらないので、作業を終えたら全体にアナウンスして、各アプリのアプリ管理者に見直しを依頼してください。組織を大きく変えると、対象のアプリ管理者に個別に伝えるのは難しいためです。

  • [配下の組織を含む]を指定している処理担当者設定。 親組織を付け替えると、含まれる組織が変わります。付け替えた組織が元いた場所と、移った先の両方で、担当者が変わります。
  • 新設した組織。 組織を作っただけでは、どのアプリからも指定されていません。新設した組織を処理担当者にするには、アプリ側での設定が要ります。
  • 所属メンバを入れ替えた組織。 その組織を処理担当者に指定しているアプリでは、入れ替えたあとに生成されるタスクから担当者が変わります。

アナウンスでは、組織の変更を終えたことと、この 3 つの見直しを知らせます。組織を廃止する場合は、削除する組織と削除日もあわせて知らせます。廃止について書く内容は、組織廃止の記事の「[削除予定]の設定」にあります。

システム管理権限があれば、どのアプリがどの組織を指しているかを、実行する人の側でも確認できます。 [システム設定]>[組織一覧]で組織を開き、[組織を参照しているアプリ一覧]で確認します。ユーザ管理権限だけでは、このボタンは表示されません。調べる対象は、アプリの最新バージョン(稼働中のバージョン)です。編集中バージョンだけが指している場合は、この一覧には出てきません。

その組織を指しているアプリを組織の詳細画面から確認できる

関連ページ

Questetra Supportをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む