Claude CodeでMRPの所要量展開を組む — BOM・在庫CSVから検算まで
BOM・在庫・発注残・需要の5つのCSVから、多階層の総所要量、正味所要量、発注時期をPythonで展開する手順。Claude Codeに書かせ、検算スクリプトで照合します。
MRP(資材所要量計画)の中心は、完成品の需要から部品の必要数と手配の時期を逆算する所要量展開です。品目が数百に増え、毎週のように在庫や発注残が変わるなら、表計算の式を書き換え続けるより、CSVを読む小さなプログラムに固定するほうが続けやすくなります。
この記事では、BOM・品目マスタ・在庫・発注残・需要の5つのCSVを入力に、総所要量、正味所要量、発注量、発注日までをPythonで展開します。コードはClaude Codeに書かせ、結果は別のスクリプトで検算します。Excelの式で組む方法はClaude for ExcelでMRPの所要量計算を組む手順にあります。ここで扱うのは、品目数が多い場合や、同じ計算を毎週再実行する場合です。
多階層の所要量展開は処理の順番で決まる
所要量展開は、品目ごとに次の3つを繰り返す計算です。
1品目あたりの計算
- 1
総所要量を集める
上位品目の発注量にBOMの員数を掛けた数と、その品目自身への需要を、必要日ごとに足し合わせます。
- 2
正味所要量を出す
総所要量から、使える在庫(手持在庫と、必要日までに着く発注残)を引きます。マイナスは0にします。
- 3
発注量と発注日を決める
正味所要量をロットの倍数に切り上げ、必要日からリードタイムをさかのぼった日を発注日にします。この発注日が、下位品目の必要日になります。
実装でつまずくのは、計算そのものより処理の順番です。部品が2つの経路から使われると、親ごとに別々の階層に現れます。例題では、ネジ(SCR-M4)は製品(PUMP)の直下にも、モーター(MOTOR)の下にもあります。PUMPからの所要量だけでネジの正味所要量を出してしまうと、MOTORから来る分が抜けます。
回避の定番は、各品目に低位コードを振る方法です。その品目が現れる最も深い階層の番号を持たせ、浅い順に処理します。ネジはPUMPの直下なら階層1、MOTORの下なら階層2なので、低位コードは2です。親の発注量がすべて決まってから、ネジの総所要量を集計できます。
例題と入力CSV
製品PUMPは、モーター・ケース・ネジ6本でできています。モーターは、ステーターとネジ4本でできています。
| 品目 | リードタイム(日) | ロット | 手持在庫 | 発注残 |
|---|---|---|---|---|
| PUMP | リードタイム(日)3 | ロット1 | 手持在庫5 | 発注残なし |
| MOTOR | リードタイム(日)7 | ロット10 | 手持在庫12 | 発注残10(11月9日着) |
| CASE | リードタイム(日)10 | ロット1 | 手持在庫20 | 発注残なし |
| STATOR | リードタイム(日)14 | ロット20 | 手持在庫15 | 発注残20(11月2日着) |
| SCR-M4 | リードタイム(日)5 | ロット500 | 手持在庫300 | 発注残なし |
PUMPの需要は、11月16日に30台、11月30日に40台です。入力は次の5ファイルに分けます。
items.csv item,lead_days,lot
bom.csv parent,child,qty
stock.csv item,on_hand
po.csv item,due,qty
demand.csv item,due,qtyBOMをExcelから書き出したCSVは、先頭にバイトオーダーマーク(BOM)が付くことがあります。部品表のBOMと同じ略称ですが別物で、Pythonでそのまま読むと最初の列名が parent と一致しなくなります。後述のスクリプトでは encoding="utf-8-sig" で読んでいます。症状の詳細はps1とBOMの記事にまとめています。
リードタイムはカレンダー日で持たせました。営業日で数える場合は、休業日のリストを足す必要があります。
Claude Codeに頼む前に、検算を決めておく
Claude Codeは、コードベースを読み、ファイルを編集し、コマンドを実行するエージェント型のツールです。CSVを読んでPythonを書き、動かして直すところまで任せられます。
任せる前に決めておくのは、「何をもって正しいとするか」です。公式のベストプラクティスは、Claudeが完了と判断する材料として、テストやスクリプトのように実行して合否が返るチェックを渡すことを最初に挙げています。チェックがなければ、「できたように見える」ことが唯一の合図になり、間違いに気づく役目が人に回るためです。
所要量展開には、人が手で追わなくても確かめられる性質が2つあります。
- 子品目の総所要量の合計は、親の発注量にBOMの員数を掛けた合計に一致する
- 品目ごとに、在庫と発注残と発注量の合計が、総所要量の合計以上になる
この2つを check.py として先に頼み、本体の mrp.py はその検算に通るまで直させる、という順序にします。実装の方針を先に見たいときは、Plan Modeを使います。claude --permission-mode plan で起動すると、Claudeはファイルを読んで質問に答えるだけで、変更はしません。低位コードの扱いと、需要日が複数あるときの在庫の消し込み方を、計画の段階で読んでおけます。
プロジェクトのCLAUDE.mdには、次のような約束を置いておくと、再実行のたびに同じ作法で動きます。
# MRP計算の約束
- 計算は mrp.py に固定し、数量や日付を会話の中で暗算しない
- 変更したら python3 mrp.py > out.csv && python3 check.py out.csv を実行し、
OK が出るまで直す
- BOMに循環があれば mrp.py は止まる。止まったら経路を報告し、直さない
- 日付はカレンダー日。休業日の扱いが必要なら先に質問するCLAUDE.mdは短く保つほうが効くので、数行にとどめています。
mrp.pyの全体 — 標準ライブラリだけで書く
次が、例題を処理するスクリプトです。標準ライブラリのみで動き、読み込んだCSVの内容を変えなければ、毎回同じ結果を返します。
import csv, math, sys
from collections import defaultdict
from datetime import date, timedelta
def rows(p):
return list(csv.DictReader(open(p, encoding="utf-8-sig")))
items = {r["item"]: (int(r["lead_days"]), int(r["lot"]))
for r in rows("items.csv")}
bom = defaultdict(list)
for r in rows("bom.csv"):
bom[r["parent"]].append((r["child"], int(r["qty"])))
stock = {r["item"]: int(r["on_hand"]) for r in rows("stock.csv")}
po = defaultdict(list)
for r in rows("po.csv"):
po[r["item"]].append((date.fromisoformat(r["due"]), int(r["qty"])))
gross = defaultdict(list) # 品目 -> [(必要日, 数量)]
for r in rows("demand.csv"):
gross[r["item"]].append((date.fromisoformat(r["due"]), int(r["qty"])))
level = {} # 低位コード(最も深い階層)
def walk(item, depth=0, path=()):
if item in path:
sys.exit("BOM循環: " + " > ".join(path + (item,)))
level[item] = max(level.get(item, 0), depth)
for child, _ in bom.get(item, []):
walk(child, depth + 1, path + (item,))
for top in list(gross):
walk(top)
print("item,need_date,gross,available,net,order_qty,release_date")
for item in sorted(level, key=level.get):
lead, lot = items[item]
avail = stock.get(item, 0)
receipts = sorted(po[item])
for need, qty in sorted(gross[item]):
avail += sum(q for d, q in receipts if d <= need)
receipts = [(d, q) for d, q in receipts if d > need]
net = max(0, qty - avail)
order = math.ceil(net / lot) * lot
release = need - timedelta(days=lead)
print(f"{item},{need},{qty},{avail},{net},{order},"
f"{release if order else ''}")
avail += order - qty
if order:
for child, per in bom.get(item, []):
gross[child].append((release, order * per))読みどころは3か所です。
walkが低位コードを振ります。BOMが輪になっている場合(AがBを、BがAを使う)は、無限に潜る前に経路を表示して止めます- 発注残は、必要日までに着く分だけを在庫に足します。必要日より後に着く分は、次の需要日まで持ち越します
- 発注量が需要を上回った余りは、
avail += order - qtyで次の需要日に繰り越されます。ロット単位で切り上げた分が、後続の需要を吸収する動きです
実行結果 — 手元のPython 3.12で動かした出力
5つのCSVを同じフォルダに置いて実行します。
python3 mrp.py > out.csv && cat out.csvitem,need_date,gross,available,net,order_qty,release_date
PUMP,2026-11-16,30,5,25,25,2026-11-13
PUMP,2026-11-30,40,0,40,40,2026-11-27
MOTOR,2026-11-13,25,22,3,10,2026-11-06
MOTOR,2026-11-27,40,7,33,40,2026-11-20
CASE,2026-11-13,25,20,5,5,2026-11-03
CASE,2026-11-27,40,0,40,40,2026-11-17
STATOR,2026-11-06,10,35,0,0,
STATOR,2026-11-20,40,25,15,20,2026-11-06
SCR-M4,2026-11-06,40,300,0,0,
SCR-M4,2026-11-13,150,260,0,0,
SCR-M4,2026-11-20,160,110,50,500,2026-11-15
SCR-M4,2026-11-27,240,450,0,0,available は、その需要日に消し込む直前の使える数量です。階層の浅い順に並び、PUMPの発注日が、そのままMOTORとCASEの必要日になっているのが分かります。
手計算で2品目を追う
出力を信じる前に、数行だけ手で追います。全行を追う必要はありません。階層をまたぐ品目と、ロットが効く品目を選びます。
MOTORの1回目は、必要日が11月13日です。PUMPの発注25台が11月13日に出るので、モーターの総所要量は25です。手持12に、11月9日に着く発注残10を足して22。25から22を引いた正味所要量は3で、ロット10に切り上げた10台を、7日前の11月6日に発注します。余りは22+10-25で7です。2回目の11月27日は、総所要量40から余り7を引いた33を、40に切り上げます。
SCR-M4は、4つの総所要量が別々の親から来ます。
| 必要日 | 総所要量 | 出どころ |
|---|---|---|
| 11月6日 | 総所要量40 | 出どころMOTORの発注10台 × 4本 |
| 11月13日 | 総所要量150 | 出どころPUMPの発注25台 × 6本 |
| 11月20日 | 総所要量160 | 出どころMOTORの発注40台 × 4本 |
| 11月27日 | 総所要量240 | 出どころPUMPの発注40台 × 6本 |
手持300から順に引くと、11月20日の時点で使える数量が110になり、160に足りません。不足の50を、ロット500に切り上げて発注します。発注日は5日前の11月15日です。
check.pyで機械的に照合する
手計算で追えるのは数行です。品目が数百になれば、同じ性質を機械に確かめさせます。
import csv, sys
from collections import defaultdict
out = list(csv.DictReader(open(sys.argv[1])))
bom = [(r["parent"], r["child"], int(r["qty"]))
for r in csv.DictReader(open("bom.csv"))]
stock = {r["item"]: int(r["on_hand"])
for r in csv.DictReader(open("stock.csv"))}
po = defaultdict(int)
for r in csv.DictReader(open("po.csv")):
po[r["item"]] += int(r["qty"])
gross, order = defaultdict(int), defaultdict(int)
for r in out:
gross[r["item"]] += int(r["gross"])
order[r["item"]] += int(r["order_qty"])
ng = 0
for c in sorted({c for _, c, _ in bom}):
exp = sum(order[p] * q for p, cc, q in bom if cc == c)
if gross[c] != exp:
ng += 1
print(f"NG gross {c}: {gross[c]} != {exp}")
for i in sorted(gross):
left = stock.get(i, 0) + po[i] + order[i] - gross[i]
if left < 0:
ng += 1
print(f"NG short {i}: {left}")
print("OK" if ng == 0 else f"{ng} NG")python3 check.py out.csvOKこの検算は、mrp.py の中身を使わず、出力CSVと入力CSVだけから組んでいます。計算した側と確かめる側が同じ実装を共有すると、同じ誤りを両方が持つためです。
階層の順番を間違えると何が起きるか
低位コードを使わずに、品目名の辞書順で処理するように変えたスクリプトを走らせてみます。sorted(level, key=level.get) を sorted(level) に置き換えただけの版です。
item,need_date,gross,available,net,order_qty,release_date
PUMP,2026-11-16,30,5,25,25,2026-11-13
PUMP,2026-11-30,40,0,40,40,2026-11-27
SCR-M4,2026-11-13,150,300,0,0,
SCR-M4,2026-11-27,240,150,90,500,2026-11-22エラーは出ず、CSVも最後まで出ます。しかしMOTORとCASEとSTATORの行が丸ごと消え、SCR-M4の発注日は正しい11月15日より1週間遅い11月22日になります。辞書順ではCASEとMOTORがPUMPより先に処理され、まだ総所要量が集まっていないためです。
check.py に通すと、こう返ります。
NG gross CASE: 0 != 65
NG gross MOTOR: 0 != 65
2 NG見た目がもっともらしい出力が、黙って間違うことがあります。公式のベストプラクティスも、Claudeが妥当に見える実装を作っても端のケースを扱えていない状態を、よくある失敗の型として挙げています。検算を渡しておく意味は、この種の失敗を、人が見る前に止めることにあります。
定期実行は、計算と検算を先に済ませる
毎週の実行を自動化するなら、計算と検算は通常のシェルで走らせ、Claudeには結果を読む部分だけを任せる構成が扱いやすくなります。claude -p は非対話で動き、標準入力を受け取れます。
python3 mrp.py > out.csv && python3 check.py out.csv \
&& cat out.csv | claude -p "order_qty が0でない行だけ、\
発注日の早い順に品目・数量・発注日の表にして" \
--permission-mode dontAskcheck.py が OK 以外を返したときは、&& でつないであるので、Claudeの呼び出しまで進みません。dontAsk は、確認が必要になる呼び出しを自動で拒否するモードで、固定した環境で無人実行するときに使います。このコマンドはファイルの読み書きもコマンド実行もさせず、標準入力のCSVを要約するだけなので、権限を追加で許可する必要はありません。
Claudeにスクリプトの修正や再実行までさせるなら、--allowedTools で許可するツールを絞ります。たとえば Bash(python3 mrp.py *) のように、実行してよいコマンドを1つずつ書きます。裸の Bash を許可すると、任意のシェルコマンドが確認なしで通るため、無人の環境では避けます。
この実装が前提にしていること
例題のコードは、次の点を単純にしています。そのまま実運用に使う前に、自社の運用と照らして足す部分です。
- 日付の単位は日で、週次や月次のバケットにまとめていません
- リードタイムはカレンダー日です。休業日を飛ばす計算は含みません
- 安全在庫、歩留まり、不良見込みは入っていません
- 発注量はロットの倍数への切り上げだけです。最小発注量や、複数日の需要をまとめて発注する方式は扱いません
- 必要日より後に着く発注残は、その需要には使いません。納期の遅れを検知する仕組みは別に必要です
- 品目マスタにない品目がBOMに出ると、
KeyErrorで止まります。止まるのは、黙って0として流すよりも安全です
どれを先に足すかは、現場で一番ずれが出ている点から決めます。休業日が多い工場なら発注日の計算、歩留まりが悪い工程なら所要量への上乗せ、というように、入力CSVの列を増やす形で拡張できます。列を足したら check.py の性質も更新し、検算を先に直してからClaudeに実装を頼むと、同じ順序を保てます。
BOMの改訂が入った週は、改訂前後の差分を先に見ておくと原因を絞れます。改訂による影響部品の洗い方はECN前後のBOM差分の記事にまとめています。
まとめ
多階層の所要量展開は、低位コード順に処理するという1点を外すと、エラーなしで数字が欠けます。正しさは、出力から独立した検算で確かめるのが確実です。Claude Codeには、検算スクリプトを先に作らせ、本体はその検算に通るまで直させる、という順序で任せると、品目が増えても結果の信頼度を保ちやすくなります。