コンテンツにスキップ

自動化演習:erpintegrations で取込ジョブを実行・監視する

このページの API の項目名と finder は、26A の erpintegrations の公式ガイドで確認したものです。モックは本物の Oracle API ではなく、状態の値・ログの形式・所要時間は実機と違います。違いは「ステップ 5」の表にまとめています。

  • importBulkData を Python(requests)で呼び、返った要求 ID(ReqstId)を台帳に残せる
  • ESSJobStatusRF のポーリングを、終了状態の集合・間隔・上限時間で設計できる
  • ESSJobExecutionDetailsRF でログと出力の zip を取り、中身から結果を判定できる
  • 失敗の種類(呼び出しの誤り、一時的な障害、ジョブの ERROR、時間切れ、リジェクト)ごとに、再試行するかしないかを判断できる
  • 実機に向けて変える点を挙げられる

仕組みは ERP Integration Service と ESS、取り込む zip の作り方は FBDI 演習 を参照してください。

前提・準備(Fusion 環境あり/なし)

Section titled “前提・準備(Fusion 環境あり/なし)”
  • Python 3.9 以上と requests(pip install requests)。
  • 作業用のフォルダ(例:ess-lab)。ログと台帳は、スクリプトを実行したフォルダに作られます。
  • コマンドは bash の書き方です。Windows では WSL を使うと、そのまま進められます。PowerShell を使う場合は、環境変数を $env:BASE = "..." の形で設定し、python3 を python に、終了コードの $? を $LASTEXITCODE に読み替えます。課題の MOCK_FAULT=503 python3 ... のように、コマンドの前に環境変数を付ける書き方は PowerShell では使えないので、$env:MOCK_FAULT = "503" で設定してから実行し、終わったら Remove-Item Env:MOCK_FAULT で消します。

このサイトの練習用モック API を使います。ベース URL と認証は ハンズオン演習の進め方 を参照してください。

ターミナルウィンドウ
export BASE="https://<このサイトのURL>/api/mock/fscmRestApi/resources/11.13.18.05"
export FA_USER=learner FA_PASS=welcome1

この演習で使うモックの挙動です。

呼び出し モックの挙動
importBulkData zip の中身は読まず、ReqstId を返します。FileName に error を含むと ERROR、warning を含むと WARNING で終わります。DocumentContent が zip でないときも ERROR です
ESSJobStatusRF 投入から 5 秒未満は WAIT、15 秒未満は RUNNING、その後は最終の状態を返します
ESSJobExecutionDetailsRF 終了後に、<ReqstId>.log と <ReqstId>.out を入れた zip(Base64)を返します。.out の件数は状態ごとの固定値で、投入した zip の中身とは無関係です
障害の再現 リクエストヘッダー X-Mock-Fault(429、500、503、random)で、擬似的な障害を返します。429 と 503 は Retry-After: 1 付きです。スクリプトは環境変数 MOCK_FAULT で付けます(練習用 API プレイグラウンド にも説明があります)
  • テスト環境 と統合ユーザーを使います(本番では行いません)。統合ユーザーのロールは ERP Integration Service の「必要な権限」、取り込むジョブの権限は FBDI 演習 の前提を参照してください。
  • FBDI 演習の実機ルートで作った zip(ApInvoices_20261015_01.zip など)。
  • 実機に向けるのは、モックで一通り試したあとの「ステップ 5」です。

ステップ 1:スクリプトを保存する

Section titled “ステップ 1:スクリプトを保存する”

投入から結果の記録までの流れを、1 つのスクリプトにします。

投入 → 要求 ID を台帳に記録 → 終了状態になるまで状態を確認 → ログと出力を取得 → 判定(終了コード)→ 結果を台帳に記録

次を ess_job.py として保存します。

ess_job.py
#!/usr/bin/env python3
"""importBulkData で投入 → ESSJobStatusRF で終了を待つ → ESSJobExecutionDetailsRF でログを取る。
練習用モック API(本物の Oracle API ではありません)でそのまま動きます。
export BASE="https://<このサイトのURL>/api/mock/fscmRestApi/resources/11.13.18.05"
export FA_USER=learner FA_PASS=welcome1
python3 ess_job.py # 内蔵の簡略 CSV を zip にして投入
python3 ess_job.py --zip ApInvoices_20261015_01.zip # 手持ちの zip を投入
python3 ess_job.py --file-name ApInvoices_error_01.zip # FileName に error を含めると、モックは ERROR を返す
python3 ess_job.py --request-id 2481914010 # 投入せず、既存の要求 ID の終了を待ってログを取る
終了コード:0 成功/1 失敗(ERROR など)/2 要確認(WARNING・リジェクトあり)/3 時間切れ(状態不明)/4 呼び出しの失敗・設定の誤り
"""
import argparse
import base64
import csv
import io
import json
import os
import sys
import time
import zipfile
from pathlib import Path
import requests
# ---- 実機では、ここを自環境のジョブ定義に合わせる(環境変数でも上書きできる) --------------
JOB_NAME = os.environ.get("ESS_JOB_NAME", "oracle/apps/ess/financials/payables/invoices/transactions,APXIIMPT")
DOCUMENT_ACCOUNT = os.environ.get("ESS_DOCUMENT_ACCOUNT", "fin$/payables$/import$")
# 公式の AP の例と同じ 13 個の並び(2 番目が BU 名、8 番目がソース)。並びは自環境のジョブ定義で確認する
PARAMETER_LIST = os.environ.get(
"ESS_PARAMETER_LIST", "#NULL,JP01 Business Unit,#NULL,#NULL,#NULL,#NULL,#NULL,External,#NULL,#NULL,#NULL,1,#NULL"
)
# 終了とみなす状態。Completed・Blocked などは含めない。綴りは実機で確認する
TERMINAL = {"SUCCEEDED", "WARNING", "ERROR", "CANCELED", "CANCELLED", "EXPIRED", "VALIDATION_FAILED", "FINISHED"}
# -----------------------------------------------------------------------------------------
LEDGER = "ess_ledger.jsonl" # 投入の台帳(1 行 1 件の JSON)
LOG_DIR = "logs"
RETRY_GET = {429, 500, 502, 503, 504} # 読み取りは、一時的な障害なら再試行してよい
RETRY_POST = {429, 503} # 投入は、受け付ける前に断られたと考えてよいものだけ再試行する
class ApiError(Exception):
"""呼び出しの失敗。再試行しても直らない、または再試行の上限に達した"""
class WaitTimeout(Exception):
"""上限時間までに終了状態にならなかった(状態不明)"""
class Client:
def __init__(self):
missing = [n for n in ("BASE", "FA_USER", "FA_PASS") if not os.environ.get(n)]
if missing:
raise ApiError("環境変数が未設定です: " + ", ".join(missing))
self.url = os.environ["BASE"].rstrip("/") + "/erpintegrations"
self.session = requests.Session()
self.session.auth = (os.environ["FA_USER"], os.environ["FA_PASS"])
if os.environ.get("MOCK_FAULT"): # モック専用:X-Mock-Fault ヘッダーで 429・503 などを起こす
self.session.headers["X-Mock-Fault"] = os.environ["MOCK_FAULT"]
def call(self, method, retry_codes, **kwargs):
"""1 回の呼び出し。一時的な障害だけ最大 4 回まで試し、再試行しても直らないものはすぐ失敗にする"""
for attempt in range(1, 5):
try:
r = self.session.request(method, self.url, timeout=(5, 60), **kwargs)
except (requests.ConnectionError, requests.Timeout) as e:
if method != "GET": # 応答がない POST は、受け付けられたか分からない。再送しない
raise ApiError(f"{method} の応答を受け取れませんでした({type(e).__name__})。接続できなかっただけなら"
"未投入ですが、送信後に切れたなら投入済みかもしれません。再送の前に確認してください") from e
reason, pause = type(e).__name__, 2 ** attempt
else:
if r.ok:
return r
if r.status_code not in retry_codes: # 400・401・403・404 などは、再試行しても直らない
raise ApiError(f"{r.status_code} {r.reason}: {r.text[:300]}")
reason, retry_after = str(r.status_code), r.headers.get("Retry-After", "")
pause = min(int(retry_after), 60) if retry_after.isdigit() else 2 ** attempt
if attempt == 4:
raise ApiError(f"一時的な障害({reason})が続いたため中止しました")
print(f" 一時的な障害({reason})。{pause} 秒後に再試行します({attempt}/3)")
time.sleep(pause)
def submit(self, zip_bytes, file_name):
"""importBulkData を POST して、要求 ID(ReqstId)を返す"""
body = {
"OperationName": "importBulkData",
"DocumentContent": base64.b64encode(zip_bytes).decode("ascii"), # 改行を含めない 1 行
"ContentType": "zip",
"FileName": file_name,
"DocumentAccount": DOCUMENT_ACCOUNT,
"JobName": JOB_NAME,
"ParameterList": PARAMETER_LIST,
"CallbackURL": "#NULL",
"NotificationCode": "#NULL",
}
return str(self.call("POST", RETRY_POST, json=body).json()["ReqstId"])
def finder(self, text):
"""GET ?finder=... を呼び、items を返す"""
r = self.call("GET", RETRY_GET, params={"finder": text, "onlyData": "true"})
return r.json().get("items", [])
def wait(self, req_id, timeout):
"""ESSJobStatusRF で、終了状態になるまで待つ。間隔は 2 秒から 1.5 倍ずつ伸ばし、10 秒で頭打ち"""
start, interval, last = time.monotonic(), 2.0, None
while True:
items = self.finder(f"ESSJobStatusRF;requestId={req_id}")
if not items:
raise ApiError(f"要求 ID {req_id} が見つかりません")
status = (items[0].get("RequestStatus") or "").upper().replace(" ", "_")
if status != last:
print(f" [{time.monotonic() - start:4.0f} 秒] RequestStatus = {status}")
last = status
if status in TERMINAL:
return status
remaining = start + timeout - time.monotonic()
if remaining <= 0:
raise WaitTimeout(f"{timeout} 秒以内に終了しませんでした(最後の状態:{status})")
time.sleep(min(interval, remaining))
interval = min(interval * 1.5, 10)
def logs(self, req_id):
"""ESSJobExecutionDetailsRF でログと出力の zip を取り、保存して {ファイル名: 中身} を返す"""
items = self.finder(f"ESSJobExecutionDetailsRF;requestId={req_id},fileType=LOG")
if not items or not items[0].get("DocumentContent"):
raise ApiError(f"要求 ID {req_id} のログを取得できませんでした")
data = base64.b64decode(items[0]["DocumentContent"])
Path(LOG_DIR).mkdir(exist_ok=True)
path = Path(LOG_DIR) / f"{req_id}_LOG.zip"
path.write_bytes(data)
try:
with zipfile.ZipFile(io.BytesIO(data)) as z: # 展開はせず、名前と中身を読むだけ
files = {n: z.read(n).decode("utf-8", errors="replace") for n in z.namelist()}
except zipfile.BadZipFile as e:
raise ApiError("ログが zip として読めません") from e
print(f" {path} に保存しました({len(data)} バイト:{', '.join(files)})")
return files
def sample_zip():
"""練習用の簡略 CSV(請求書 1 件・明細 1 行)を zip にする。実際の FBDI は、テンプレートが作る zip を使う"""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("ApInvoicesInterface.csv", "INVOICE_ID,INVOICE_NUM,INVOICE_AMOUNT\n1001,INV-2026-0001,110000\n")
z.writestr("ApInvoiceLinesInterface.csv", "INVOICE_ID,LINE_NUMBER,AMOUNT\n1001,1,110000\n")
return buf.getvalue()
def record(file_name, req_id, status, note=""):
"""台帳に 1 行追記する。ファイル名・要求 ID・時刻・結果を残す"""
row = {"time": time.strftime("%Y-%m-%d %H:%M:%S"), "file_name": file_name, "req_id": req_id,
"status": status, "note": note}
with open(LEDGER, "a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
def read_counts(files):
"""出力(.out)から (処理件数, リジェクト件数) を読む。モックの .out は CSV。
実機の出力は形式が違うので、読めなければ None を返し、人が確認する"""
for name, text in files.items():
if name.endswith(".out"):
rows = list(csv.DictReader(io.StringIO(text)))
if rows and all((rows[0].get(k) or "").isdigit() for k in ("RECORDS_PROCESSED", "RECORDS_REJECTED")):
return int(rows[0]["RECORDS_PROCESSED"]), int(rows[0]["RECORDS_REJECTED"])
return None
def decide(status, counts):
"""終了状態と件数から、終了コードと説明を決める"""
c = f"処理 {counts[0]} 件・リジェクト {counts[1]} 件" if counts else "件数は読めませんでした"
if status == "SUCCEEDED" and counts and counts[1] == 0:
return 0, f"成功({c})"
if status == "SUCCEEDED":
return 2, f"SUCCEEDED ですが、リジェクトがあるか、件数を確認できません({c})"
if status == "WARNING":
return 2, f"WARNING です。取り込まれた範囲を確認してください({c})"
return 1, f"{status} です。ログで原因を確認してください。自動では再投入しません({c})"
def problem_lines(files):
"""ログから問題の手がかりになりそうな行を集める(目印にすぎない。原因は全文を読んで判断する)"""
return [f"{name}: {line.strip()}" for name, text in files.items() for line in text.splitlines()
if any(k in line.lower() for k in ("error", "warning", "fail", "ora-"))]
def main():
ap = argparse.ArgumentParser(description="importBulkData → ESSJobStatusRF → ESSJobExecutionDetailsRF")
ap.add_argument("--zip", help="投入する zip。省略すると、内蔵の簡略 CSV を zip にする")
ap.add_argument("--file-name", help="importBulkData の FileName。省略すると、zip のファイル名か日時入りの名前")
ap.add_argument("--request-id", help="投入せず、この要求 ID の終了を待ってログを取る")
ap.add_argument("--timeout", type=int, default=120, help="終了を待つ上限(秒)。既定は 120")
args = ap.parse_args()
try:
client = Client()
except ApiError as e:
print(f"設定の誤り:{e}", file=sys.stderr)
return 4
req_id = args.request_id
file_name = args.file_name or (Path(args.zip).name if args.zip else time.strftime("ApInvoices_%Y%m%d_%H%M%S.zip"))
try:
if req_id is None:
data = Path(args.zip).read_bytes() if args.zip else sample_zip()
print(f"1) importBulkData で投入します(FileName = {file_name})")
req_id = client.submit(data, file_name)
record(file_name, req_id, "SUBMITTED") # 応答を受け取ったら、すぐ要求 ID を残す
print(f" ReqstId = {req_id}")
else:
file_name = "(不明)"
print("2) ESSJobStatusRF で終了を待ちます")
status = client.wait(req_id, args.timeout)
print("3) ESSJobExecutionDetailsRF でログと出力を取得します")
files = client.logs(req_id)
except WaitTimeout as e:
print(f"時間切れ:{e}", file=sys.stderr)
print(f" ジョブは動き続けているかもしれません。再投入せず、--request-id {req_id} で確認し直してください", file=sys.stderr)
record(file_name, req_id, "UNKNOWN", str(e))
return 3
except (ApiError, OSError) as e:
print(f"失敗:{e}", file=sys.stderr)
if req_id:
print(f" 要求 ID {req_id} の状態は不明です。再投入せず、--request-id {req_id} で確認し直してください", file=sys.stderr)
record(file_name, req_id, "UNKNOWN" if req_id else "SUBMIT_FAILED", str(e))
return 4
code, message = decide(status, read_counts(files))
print(f"4) 判定:{message} → 終了コード {code}")
if code != 0:
for line in problem_lines(files):
print(f" {line}")
record(file_name, req_id, status, message)
return code
if __name__ == "__main__":
sys.exit(main())

スクリプトの構成です。

  • 設定(冒頭のブロック):JobName・DocumentAccount・ParameterList は、FBDI の取込ジョブの指定です。環境変数(ESS_JOB_NAME など)でも上書きできます。TERMINAL は終了とみなす状態の集合で、Completed と Blocked は含めません(ESS の「状態遷移」)。
  • Client.call:すべての呼び出しの入口です。やり直してよい失敗(読み取りは 429・5xx と通信エラー、投入は 429・503)だけを、最大 3 回やり直します。400・401・403・404 は、やり直しても直らないのですぐ失敗にします。応答を受け取れなかった POST は、受け付けられたか分からないので再送しません。
  • submit:importBulkData の本文を組み立てて POST し、ReqstId を返します。DocumentContent は、zip を Base64 にした、改行のない 1 行です。
  • wait:ESSJobStatusRF を、間隔を伸ばしながら呼び、TERMINAL に入るまで待ちます。上限時間を超えたら WaitTimeout(状態不明)にします。
  • logs:ESSJobExecutionDetailsRF(fileType=LOG)でログと出力の zip を取り、logs/ に保存して、中身を読みます。zip は展開せず、名前と中身を読むだけにしています。
  • record:台帳 ess_ledger.jsonl に、投入の直後と終了後に 1 行ずつ追記します。応答を受け取ったらすぐ要求 ID を残すのが要点です。
  • decide:終了状態と、出力の件数から終了コードを決めます。SUCCEEDED でもリジェクトがあれば 2 です。read_counts は、モックの .out(CSV)専用の読み方です。

終了コードは、ジョブ管理ツールやシェルスクリプトから結果を判定するための約束です。

終了コード 意味 運用での扱い(例)
0 成功(リジェクトなし) 次のジョブへ進む
1 失敗(ERROR など) 自動で再投入しない。通知して、人が原因を調べる
2 要確認(WARNING、リジェクトあり) リジェクト分を確認する。次へ進めるかは人が判断する
3 時間切れ(状態不明) 再投入しない。要求 ID で確認し直す
4 呼び出しの失敗、設定の誤り 原因を直して再実行する。要求 ID があるなら、先に状態を確認する

投入から判定までを実行します。16 秒ほどかかります。

ターミナルウィンドウ
python3 ess_job.py; echo "終了コード $?"

出力の例です。ReqstId と時刻は実行のたびに変わり、秒数は前後します。

1) importBulkData で投入します(FileName = ApInvoices_20261015_152135.zip)
ReqstId = 2481969510
2) ESSJobStatusRF で終了を待ちます
[ 0 秒] RequestStatus = WAIT
[ 5 秒] RequestStatus = RUNNING
[ 16 秒] RequestStatus = SUCCEEDED
3) ESSJobExecutionDetailsRF でログと出力を取得します
logs/2481969510_LOG.zip に保存しました(697 バイト:2481969510.log, 2481969510.out)
4) 判定:成功(処理 3 件・リジェクト 0 件) → 終了コード 0
終了コード 0

保存した zip を展開して、ログと出力を読みます。<ReqstId> は、自分の出力の値に置き換えます。

ターミナルウィンドウ
python3 -m zipfile -e logs/<ReqstId>_LOG.zip logs/<ReqstId>
cat logs/<ReqstId>/<ReqstId>.log logs/<ReqstId>/<ReqstId>.out
cat ess_ledger.jsonl
Fusion ERP Lab mock API - ESS job log (fictional)
Request ID : 2481969510
Operation : importBulkData
Submitted : 2026-10-15T06:21:35.000Z
Final status : SUCCEEDED
Message : The job completed successfully.
This log was generated by the practice mock API. It is not an Oracle Fusion log.
このログは練習用モック API が生成した架空のものです。
REQUEST_ID,STATUS,RECORDS_PROCESSED,RECORDS_REJECTED
2481969510,SUCCEEDED,3,0
{"time": "2026-10-15 15:21:35", "file_name": "ApInvoices_20261015_152135.zip", "req_id": "2481969510", "status": "SUBMITTED", "note": ""}
{"time": "2026-10-15 15:21:51", "file_name": "ApInvoices_20261015_152135.zip", "req_id": "2481969510", "status": "SUCCEEDED", "note": "成功(処理 3 件・リジェクト 0 件)"}

確認する点です。

  • 状態は WAIT → RUNNING → SUCCEEDED と進みます。スクリプトは、状態が変わったときだけ表示します。
  • .out の「処理 3 件」は、モックの固定値です。投入した zip の件数(請求書 1 件)とは関係がありません。実機では、出力・インタフェース表・画面の件数を、取込元と突き合わせます。
  • 台帳には、投入の直後の行(SUBMITTED)と、終了後の行の 2 行が残ります。投入の応答を受け取った時点で、要求 ID を残しています。

ステップ 3:課題 1 ERROR を起こして、処理を確かめる

Section titled “ステップ 3:課題 1 ERROR を起こして、処理を確かめる”

1-1 ERROR を起こす

FileName に error を含めて投入します。

ターミナルウィンドウ
python3 ess_job.py --file-name ApInvoices_error_01.zip; echo "終了コード $?"

問い:(a) 状態は何になり、どの呼び出しの結果で分かりましたか。(b) 原因の手がかりは、ログのどの行ですか。(c) スクリプトは自動で再投入しましたか。自動で再投入すると、どんなリスクがありますか。(d) 台帳には、どの行が残りましたか。

1-2 WARNING を起こす

FileName を ApInvoices_warning_01.zip にして、終了コードと件数を 1-1 と比べます。

ターミナルウィンドウ
python3 ess_job.py --file-name ApInvoices_warning_01.zip; echo "終了コード $?"

問い:WARNING を、運用でどう扱いますか。方針を 1 行で書いてください。

1-3 zip でないファイルを送る

スクリプト自身(zip ではないファイル)を、zip として送ります。

ターミナルウィンドウ
python3 ess_job.py --zip ess_job.py; echo "終了コード $?"

問い:FileName(ess_job.py)に error を含まないのに、なぜ ERROR になりましたか。

1-4 改造:通知文を組み立てる

ERROR や WARNING のとき、運用担当に送る通知文を組み立てる関数 build_notice() を作ります。要求 ID、ファイル名、状態、判定、ログの保存先、ログの手がかりの行を入れます。パスワードと DocumentContent は入れません。送信は作らず、画面に出すだけでかまいません。

ステップ 4:課題 2 一時的な障害と時間切れを起こす

Section titled “ステップ 4:課題 2 一時的な障害と時間切れを起こす”

2-1 503 を返し続ける

ターミナルウィンドウ
MOCK_FAULT=503 python3 ess_job.py; echo "終了コード $?"

問い:何回、何秒おきに再試行し、最後にどうなりますか。台帳には何が残りますか。

2-2 ランダムに 429・503 を返す

ターミナルウィンドウ
MOCK_FAULT=random python3 ess_job.py; echo "終了コード $?"

問い:障害が起きても、最後まで進みますか。再試行の間隔は、何で決まっていますか。

2-3 500 を返し続ける(読み取りと投入)

読み取り(GET)は、ステップ 2 で控えた ReqstId を <ReqstId> に入れて試します。投入(POST)は、新しく投入します。

ターミナルウィンドウ
MOCK_FAULT=500 python3 ess_job.py --request-id <ReqstId>
MOCK_FAULT=500 python3 ess_job.py

問い:読み取りと投入で、500 の扱いはどう違いますか。なぜ違えているのですか。

2-4 ジョブ名の形式を壊す

ターミナルウィンドウ
ESS_JOB_NAME=APXIIMPT python3 ess_job.py; echo "終了コード $?"

問い:400 が返ったとき、再試行しないのはなぜですか。

2-5 時間切れを起こして、確認し直す

待つ上限を 5 秒にします。ジョブは 15 秒ほどかかるので、時間切れになります。投入から 15 秒以上たってから、表示された ReqstId で確認し直します。

ターミナルウィンドウ
python3 ess_job.py --timeout 5; echo "終了コード $?"
python3 ess_job.py --request-id <表示された ReqstId>

問い:時間切れのとき、再投入しないのはなぜですか。台帳には何が残りましたか。

ステップ 5:実機に向ける(Fusion 環境あり)

Section titled “ステップ 5:実機に向ける(Fusion 環境あり)”

モックで一通り試したあと、テスト環境に向けます。次の表が、変える点です。

観点 モック 実機で変える・確認すること
接続先 BASE はモックの URL https://<your-pod>.fa.<region>.oraclecloud.com/fscmRestApi/resources/11.13.18.05。バージョンは、対象リリースの公式ガイドで確認する
認証 Basic。ユーザー名とパスワードは任意 統合専用ユーザー。方式(Basic、OAuth、JWT)は環境の方針に従い、資格情報は環境変数かシークレット管理から読む(認証・認可)
ジョブの指定 JobName が「パッケージ名,定義名」の形ならよい 実在するジョブ定義の JobName、ParameterList の並び、DocumentAccount。FBDI ガイドと、スケジュール済プロセスのパラメータ画面で確認する
zip 中身を読まない テンプレートの Generate CSV File で作った zip。中身が不正なら、ロードの失敗やインポートのリジェクトになる
状態の値 WAIT → RUNNING → SUCCEEDED・ERROR・WARNING REST ガイド(26A)にも SOAP ガイド(26D)にも一覧がない。ESS の状態名に対応する値が返ると考え、綴りを 1 回確認して TERMINAL を直す。Completed と Blocked は終了ではない
所要時間 約 15 秒 データ量と混み具合で、数分以上かかる。間隔の上限、待つ上限(--timeout)、応答待ちのタイムアウト(timeout=(5, 60))は、リハーサルの実測で決める
要求 ID 1 つの ID importBulkData が返すのは、最初のジョブ(ロード)の ID。インポートの結果は、子ジョブを ESSExecutionDetailsRF で確認する
ログと出力 .log と .out(件数の CSV) ジョブごとに、名前も形式も違う。拡張子で扱い、判定(read_counts)は実際の出力に合わせて作り直す
障害 X-Mock-Fault で起こす 実際の 429・5xx・タイムアウト・メンテナンス。MOCK_FAULT は実機では使わない
結果の確認 .out の件数は固定値 出力・インタフェース表・画面の件数と金額を、取込元と突き合わせる

手順です。

  1. 環境変数を設定します。パスワードは、シェルの履歴や画面に残らない方法で入れます(PowerShell では、履歴に残らない入力方法で $env:FA_PASS に設定します)。
ターミナルウィンドウ
export BASE="https://<your-pod>.fa.<region>.oraclecloud.com/fscmRestApi/resources/11.13.18.05"
export FA_USER="<統合ユーザー名>"
read -rs FA_PASS && export FA_PASS # bash・zsh。入力は画面に表示されません
  1. ジョブの指定を、自環境のジョブ定義で確認して設定します。ParameterList は位置で意味が決まるので、スケジュール済プロセスのパラメータ画面の順序と照らし合わせます(ERP Integration Service の「ParameterList と #NULL」)。
ターミナルウィンドウ
export ESS_JOB_NAME="oracle/apps/ess/financials/payables/invoices/transactions,APXIIMPT"
export ESS_DOCUMENT_ACCOUNT='fin$/payables$/import$'
export ESS_PARAMETER_LIST="<自環境の並びで作った値>"
  1. FBDI 演習の実機ルートで作った zip を投入します。待つ上限は、リハーサルの実測で決めます(ここでは 30 分の例)。
ターミナルウィンドウ
python3 ess_job.py --zip ApInvoices_20261015_01.zip --timeout 1800
  1. ナビゲータ > ツール > スケジュール済プロセスで、同じ要求 ID を検索します。状態がスクリプトの表示と一致することを確かめ、状態の綴りを記録して TERMINAL と照らし合わせます。
  2. 子ジョブを確認します。インポートの結果は、ESSExecutionDetailsRF で見ます。モックには実装がなく、400 になります。finder の変数は requestId と jobOptions です。jobOptions は公式の GET のガイド(26A)で「将来の利用のため」とされており、通常は指定しません。
ターミナルウィンドウ
curl -s -u "$FA_USER:$FA_PASS" "$BASE/erpintegrations?finder=ESSExecutionDetailsRF;requestId=<ReqstId>&onlyData=true"
  1. 出力(Import Payables Invoices の出力など)を見て、read_counts() を実際の形式に合わせて作り直します。インタフェース表と画面の件数・金額も、取込元と突き合わせます(FBDI 演習 のステップ 6)。
  • 正常系で、状態が WAIT → RUNNING → SUCCEEDED と進み、終了コード 0 になった
  • logs/ の zip を展開し、.log と .out の役割を説明できる
  • 台帳に、投入の直後(SUBMITTED)と終了後の 2 行が残り、応答を受け取ってすぐ記録する理由を説明できる
  • FileName に error を含めて ERROR を再現し、ログの該当行を見つけた
  • 429・503 は再試行し、400 は再試行しない理由を説明できる
  • 応答を受け取れなかった POST を再送しない理由と、代わりに確認する方法を説明できる
  • 時間切れのとき、再投入せず --request-id で確認し直す理由を説明できる
  • SUCCEEDED でも成功とは限らない理由(リジェクト、子ジョブ)を説明できる
  • 実機に向けて変える点を、5 つ挙げられる
課題 1(ERROR・WARNING・zip でないファイル)の解答

1-1 の出力です。

1) importBulkData で投入します(FileName = ApInvoices_error_01.zip)
ReqstId = 2481969511
2) ESSJobStatusRF で終了を待ちます
[ 0 秒] RequestStatus = WAIT
[ 5 秒] RequestStatus = RUNNING
[ 16 秒] RequestStatus = ERROR
3) ESSJobExecutionDetailsRF でログと出力を取得します
logs/2481969511_LOG.zip に保存しました(740 バイト:2481969511.log, 2481969511.out)
4) 判定:ERROR です。ログで原因を確認してください。自動では再投入しません(処理 0 件・リジェクト 3 件) → 終了コード 1
2481969511.log: Final status : ERROR
2481969511.log: Message : The file name (or job name) contains "error", so this practice job ended in ERROR.
2481969511.out: 2481969511,ERROR,0,3
  • (a) ERROR です。ESSJobStatusRF の RequestStatus で分かります。ログは ESSJobExecutionDetailsRF で取ります。終了コードは 1 です。
  • (b) .log の Message の行です。実機のログは形式が違うので、全文を保存して人が読みます。problem_lines() は、手がかりになりそうな行を拾う目印にすぎません。
  • (c) 再投入していません。原因が直っていないまま再投入すると、同じ失敗を繰り返します。ロードの途中まで進んでいた場合は、同じデータを二重に取り込むおそれもあります。原因を直し、人が判断してから、ファイル名の連番を進めて投入します。
  • (d) SUBMITTED と ERROR の 2 行です。

1-2 は、終了コード 2、件数は「処理 2 件・リジェクト 1 件」です。

4) 判定:WARNING です。取り込まれた範囲を確認してください(処理 2 件・リジェクト 1 件) → 終了コード 2
2481969512.log: Final status : WARNING
2481969512.log: Message : The file name (or job name) contains "warning", so this practice job ended with WARNING.
2481969512.out: 2481969512,WARNING,2,1

方針の例です。WARNING は成功扱いにせず、リジェクト分を担当者が確認します。取り込まれた分はそのままにして、リジェクト分だけを直して再取込します(AP なら Correct Import Errors)。許容する警告は、理由を台帳に残します。

1-3 は、DocumentContent が zip ではないので ERROR です(モックの仕様)。ログの Message は DocumentContent was not a zip file, so the load job ended in ERROR. になります。FileName に error を含まなくても ERROR になる、つまり原因はファイル名ではなく中身です。実機でも、FBDI の取込は zip が前提です。CSV をそのまま Base64 にして ContentType を zip で送るのは、よくあるミスです(ERP Integration Service の「落とし穴とベストプラクティス」)。

1-4 の例です。problem_lines() を使い回して、通知文を組み立てます。main() の if code != 0: の中を、build_notice() の呼び出しに置き換えます。

ess_job.py に追加する build_notice()(main() の前に置く)
def build_notice(file_name, req_id, status, message, files):
"""運用担当への通知文を組み立てる(送信はしない。パスワードや DocumentContent は載せない)"""
lines = [
f"取込ジョブが {status} で終了しました",
f"ファイル:{file_name}",
f"要求 ID:{req_id}",
f"判定:{message}",
f"ログ:{LOG_DIR}/{req_id}_LOG.zip",
"手がかり:",
]
return "\n".join(lines + [f" {line}" for line in problem_lines(files)])
main() の末尾の変更
if code != 0:
print(build_notice(file_name, req_id, status, message, files)) # 実務では、メールやチャットで送る

ERROR のときの出力(末尾)です。

取込ジョブが ERROR で終了しました
ファイル:ApInvoices_error_01.zip
要求 ID:2481972511
判定:ERROR です。ログで原因を確認してください。自動では再投入しません(処理 0 件・リジェクト 3 件)
ログ:logs/2481972511_LOG.zip
手がかり:
2481972511.log: Final status : ERROR
2481972511.log: Message : The file name (or job name) contains "error", so this practice job ended in ERROR.
2481972511.out: 2481972511,ERROR,0,3
課題 2(一時的な障害と時間切れ)の解答

2-1 の出力です。終了コードは 4 です。

1) importBulkData で投入します(FileName = ApInvoices_20261015_152135.zip)
一時的な障害(503)。1 秒後に再試行します(1/3)
一時的な障害(503)。1 秒後に再試行します(2/3)
一時的な障害(503)。1 秒後に再試行します(3/3)
失敗:一時的な障害(503)が続いたため中止しました

モックが返す Retry-After: 1 に従い、1 秒おきに 3 回再試行して、4 回目の失敗で中止します。台帳には、要求 ID のない SUBMIT_FAILED の行が 1 行残ります。投入が受け付けられていないので、障害が直ってから、同じ内容で投入し直せます。

2-2 は、再試行しながら最後まで進みます。出力の例です(障害の出方は実行ごとに変わります)。

2) ESSJobStatusRF で終了を待ちます
[ 0 秒] RequestStatus = WAIT
[ 5 秒] RequestStatus = RUNNING
一時的な障害(503)。1 秒後に再試行します(1/3)
一時的な障害(429)。1 秒後に再試行します(2/3)
一時的な障害(503)。1 秒後に再試行します(1/3)
[ 19 秒] RequestStatus = SUCCEEDED

再試行の間隔は、Retry-After ヘッダーがあればその秒数(上限 60 秒)、なければ 2 秒、4 秒、8 秒と倍にします。同じ呼び出しで 4 回続けて障害になると、まれに失敗します。

2-3 の出力です。読み取り(GET)は 500 でも再試行し、投入(POST)は再試行しません。

2) ESSJobStatusRF で終了を待ちます
一時的な障害(500)。2 秒後に再試行します(1/3)
一時的な障害(500)。4 秒後に再試行します(2/3)
一時的な障害(500)。8 秒後に再試行します(3/3)
失敗:一時的な障害(500)が続いたため中止しました
要求 ID 2481972511 の状態は不明です。再投入せず、--request-id 2481972511 で確認し直してください
1) importBulkData で投入します(FileName = ApInvoices_20261015_152135.zip)
失敗:500 Internal Server Error: Internal server error (simulated by the X-Mock-Fault header).

読み取りは、何度呼んでも状態が変わらないので、再試行しても安全です。投入は、500 が「処理の途中で失敗した」ことを意味する場合があり、受け付けられていた場合は再送が二重投入になります。429 と 503 は、受け付ける前に断られたと考えてよいので、再試行します。それでも不安なときは、再送の前に台帳とスケジュール済プロセスで、投入済みかを確認します。POST を再送すると重複が起きる点は、REST 演習2 の請求書の作成でも扱っています。

2-4 は、400 が返り、再試行せずに終了コード 4 で終わります。400 は入力の誤りで、同じ内容を送り直しても同じ結果になるからです。メッセージ(JobName must be "<job package>,<job definition name>")を読んで、カンマで区切った形に直します。

2-5 の出力です。時間切れは終了コード 3、台帳には SUBMITTED と UNKNOWN の行が残ります。

2) ESSJobStatusRF で終了を待ちます
[ 0 秒] RequestStatus = WAIT
[ 5 秒] RequestStatus = RUNNING
時間切れ:5 秒以内に終了しませんでした(最後の状態:RUNNING)
ジョブは動き続けているかもしれません。再投入せず、--request-id 2481969510 で確認し直してください
2) ESSJobStatusRF で終了を待ちます
[ 0 秒] RequestStatus = SUCCEEDED
3) ESSJobExecutionDetailsRF でログと出力を取得します
logs/2481969510_LOG.zip に保存しました(697 バイト:2481969510.log, 2481969510.out)
4) 判定:成功(処理 3 件・リジェクト 0 件) → 終了コード 0

確認し直したあとは、台帳に SUCCEEDED の行が加わります(ファイル名は (不明) になります)。時間切れは、ジョブが失敗した証拠ではなく、待つのをやめただけです。Fusion 側では動き続けているので、再投入すると同じデータを二重に取り込むおそれがあります。要求 ID があれば、何度でも状態を確認し直せます。

症状 原因 対処
設定の誤り:環境変数が未設定です: ...(終了コード 4) BASE、FA_USER、FA_PASS のどれかが空 同じターミナルで設定し直す
404 Not Found、または No connection adapters were found BASE の末尾(/api/mock/fscmRestApi/resources/11.13.18.05)が欠けている。先頭の https:// がない BASE を見直す。末尾の / はスクリプトが取り除く
POST の応答を受け取れませんでした(ConnectionError) 接続先の誤り、ネットワーク、プロキシ、証明書 ホスト名、プロキシ(HTTPS_PROXY)、社内 CA の証明書(REQUESTS_CA_BUNDLE)を確認する。送信後に切れた場合は、台帳と画面で投入済みか確認してから再送する
400 Bad Request: JobName must be ... JobName が「ジョブ・パッケージ名,ジョブ定義名」の形でない カンマで区切る。ESS_JOB_NAME を見直す。400 は再試行しても直らない
一時的な障害(503)が続いたため中止しました 障害が続いている。または MOCK_FAULT が残っている unset MOCK_FAULT。実機は、再実行の前に台帳で投入済みかを確認する
終了コード 3(時間切れ) ジョブが上限時間内に終わらない 再投入せず、--request-id で確認する。Wait や Blocked のままなら、スケジュール済プロセスで理由を調べる
要求 ID ... が見つかりません 要求 ID の誤り(別環境の ID、桁の欠け) 台帳の req_id を確認する
ERROR(FileName に error がない) zip ではないファイルを送った python3 -m zipfile -l <ファイル> で確かめる(zip でなければ BadZipFile になる)
SUCCEEDED なのに、データが入っていない(実機) ロードは成功でも、インポートのリジェクトや、子ジョブの失敗がある ESSExecutionDetailsRF と出力、Correct Import Errors で確認する(FBDI)
  1. 二重投入の防止:投入の前に台帳を読み、同じ FileName が SUBMITTED や成功で残っていれば投入しません(--force で上書き)。応答のなかった POST は、実機で DocumentIdsByFilePrefixRF により投入済みかを確かめる流れを、設計書に書きます。
  2. 並列投入:3 つの zip を concurrent.futures.ThreadPoolExecutor で同時に投入し、同時実行数を 2 に絞ります。実機の ESS の同時実行数と自己非互換(Blocked)を、どう考慮するかを書きます(移行設計 の「性能」)。
  3. 突合:取込元の件数・金額と、REST の /invoices で取得した件数・金額を突き合わせます(REST 演習3 のページングを使います)。モックの請求書は架空の固定データで、投入した内容は反映されません。
  4. コールバック:ポーリングを CallbackURL に置き換えた設計(受け口、認証、通知が届かないときの確認)を書きます。モックはコールバックしないので、設計だけを行います(ERP Integration Service の「設計パターン」)。
  5. ジョブ運用表:台帳の項目を、ESS のジョブ運用表(投入ユーザー、スケジュール、前提ジョブ、通知、失敗時の対応)に対応づけます。OIC で同じことをすると、どこが設定に置き換わるかも書きます(OIC 演習1)。

本サイトは学習目的で個人が作成した非公式の教材であり、Oracle Corporation およびその関連会社とは関係ありません。内容の正確性は保証しません。最新の仕様・試験範囲は必ず公式ドキュメントと Oracle University の情報で確認してください。

模擬テストの問題は公開されている出題範囲(Exam Topics)と公式ドキュメントをもとに独自に作成したもので、実際の試験問題ではありません。

Oracle、Java、MySQL は Oracle Corporation およびその子会社、関連会社の米国およびその他の国における登録商標です。その他の製品名・会社名は各社の商標です。

© 2026 Fusion ERP 実装ラボ