自動化演習:erpintegrations で取込ジョブを実行・監視する
このページの API の項目名と finder は、26A の erpintegrations の公式ガイドで確認したものです。モックは本物の Oracle API ではなく、状態の値・ログの形式・所要時間は実機と違います。違いは「ステップ 5」の表にまとめています。
この演習で身につくこと
Section titled “この演習で身につくこと”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で消します。
環境なしルート
Section titled “環境なしルート”このサイトの練習用モック 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 プレイグラウンド にも説明があります) |
Fusion 環境ありルート
Section titled “Fusion 環境ありルート”- テスト環境 と統合ユーザーを使います(本番では行いません)。統合ユーザーのロールは ERP Integration Service の「必要な権限」、取り込むジョブの権限は FBDI 演習 の前提を参照してください。
- FBDI 演習の実機ルートで作った zip(
ApInvoices_20261015_01.zipなど)。 - 実機に向けるのは、モックで一通り試したあとの「ステップ 5」です。
ステップ 1:スクリプトを保存する
Section titled “ステップ 1:スクリプトを保存する”投入から結果の記録までの流れを、1 つのスクリプトにします。
投入 → 要求 ID を台帳に記録 → 終了状態になるまで状態を確認 → ログと出力を取得 → 判定(終了コード)→ 結果を台帳に記録次を 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 argparseimport base64import csvimport ioimport jsonimport osimport sysimport timeimport zipfilefrom 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 があるなら、先に状態を確認する |
ステップ 2:正常系を実行する
Section titled “ステップ 2:正常系を実行する”投入から判定までを実行します。16 秒ほどかかります。
python3 ess_job.py; echo "終了コード $?"出力の例です。ReqstId と時刻は実行のたびに変わり、秒数は前後します。
1) importBulkData で投入します(FileName = ApInvoices_20261015_152135.zip) ReqstId = 24819695102) ESSJobStatusRF で終了を待ちます [ 0 秒] RequestStatus = WAIT [ 5 秒] RequestStatus = RUNNING [ 16 秒] RequestStatus = SUCCEEDED3) 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>.outcat ess_ledger.jsonlFusion ERP Lab mock API - ESS job log (fictional)Request ID : 2481969510Operation : importBulkDataSubmitted : 2026-10-15T06:21:35.000ZFinal status : SUCCEEDEDMessage : 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_REJECTED2481969510,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 の件数は固定値 |
出力・インタフェース表・画面の件数と金額を、取込元と突き合わせる |
手順です。
- 環境変数を設定します。パスワードは、シェルの履歴や画面に残らない方法で入れます(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。入力は画面に表示されません- ジョブの指定を、自環境のジョブ定義で確認して設定します。
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="<自環境の並びで作った値>"- FBDI 演習の実機ルートで作った zip を投入します。待つ上限は、リハーサルの実測で決めます(ここでは 30 分の例)。
python3 ess_job.py --zip ApInvoices_20261015_01.zip --timeout 1800- ナビゲータ > ツール > スケジュール済プロセスで、同じ要求 ID を検索します。状態がスクリプトの表示と一致することを確かめ、状態の綴りを記録して
TERMINALと照らし合わせます。 - 子ジョブを確認します。インポートの結果は、
ESSExecutionDetailsRFで見ます。モックには実装がなく、400 になります。finder の変数はrequestIdとjobOptionsです。jobOptionsは公式の GET のガイド(26A)で「将来の利用のため」とされており、通常は指定しません。
curl -s -u "$FA_USER:$FA_PASS" "$BASE/erpintegrations?finder=ESSExecutionDetailsRF;requestId=<ReqstId>&onlyData=true"- 出力(Import Payables Invoices の出力など)を見て、
read_counts()を実際の形式に合わせて作り直します。インタフェース表と画面の件数・金額も、取込元と突き合わせます(FBDI 演習 のステップ 6)。
確認ポイント
Section titled “確認ポイント”- 正常系で、状態が
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 = 24819695112) ESSJobStatusRF で終了を待ちます [ 0 秒] RequestStatus = WAIT [ 5 秒] RequestStatus = RUNNING [ 16 秒] RequestStatus = ERROR3) 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() の呼び出しに置き換えます。
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)]) 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 = SUCCEEDED3) ESSJobExecutionDetailsRF でログと出力を取得します logs/2481969510_LOG.zip に保存しました(697 バイト:2481969510.log, 2481969510.out)4) 判定:成功(処理 3 件・リジェクト 0 件) → 終了コード 0確認し直したあとは、台帳に SUCCEEDED の行が加わります(ファイル名は (不明) になります)。時間切れは、ジョブが失敗した証拠ではなく、待つのをやめただけです。Fusion 側では動き続けているので、再投入すると同じデータを二重に取り込むおそれがあります。要求 ID があれば、何度でも状態を確認し直せます。
よくあるエラーと対処
Section titled “よくあるエラーと対処”| 症状 | 原因 | 対処 |
|---|---|---|
設定の誤り:環境変数が未設定です: ...(終了コード 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) |
- 二重投入の防止:投入の前に台帳を読み、同じ
FileNameがSUBMITTEDや成功で残っていれば投入しません(--forceで上書き)。応答のなかった POST は、実機でDocumentIdsByFilePrefixRFにより投入済みかを確かめる流れを、設計書に書きます。 - 並列投入:3 つの zip を
concurrent.futures.ThreadPoolExecutorで同時に投入し、同時実行数を 2 に絞ります。実機の ESS の同時実行数と自己非互換(Blocked)を、どう考慮するかを書きます(移行設計 の「性能」)。 - 突合:取込元の件数・金額と、REST の
/invoicesで取得した件数・金額を突き合わせます(REST 演習3 のページングを使います)。モックの請求書は架空の固定データで、投入した内容は反映されません。 - コールバック:ポーリングを
CallbackURLに置き換えた設計(受け口、認証、通知が届かないときの確認)を書きます。モックはコールバックしないので、設計だけを行います(ERP Integration Service の「設計パターン」)。 - ジョブ運用表:台帳の項目を、ESS のジョブ運用表(投入ユーザー、スケジュール、前提ジョブ、通知、失敗時の対応)に対応づけます。OIC で同じことをすると、どこが設定に置き換わるかも書きます(OIC 演習1)。
公式ドキュメント
Section titled “公式ドキュメント”本サイトは学習目的で個人が作成した非公式の教材であり、Oracle Corporation およびその関連会社とは関係ありません。内容の正確性は保証しません。最新の仕様・試験範囲は必ず公式ドキュメントと Oracle University の情報で確認してください。
模擬テストの問題は公開されている出題範囲(Exam Topics)と公式ドキュメントをもとに独自に作成したもので、実際の試験問題ではありません。
Oracle、Java、MySQL は Oracle Corporation およびその子会社、関連会社の米国およびその他の国における登録商標です。その他の製品名・会社名は各社の商標です。
© 2026 Fusion ERP 実装ラボ