// モデルケース想定シナリオ(特定企業の実案件ではありません)
結論:スマレジ等のPOS CSVをClaude Codeで月次集計し、Slack通知まで一気通貫で自動化できる
- 月次集計レポートの作成工数を週3〜4時間から約15分に短縮(想定)
- スマレジ・Square・Airレジ等の主要POS CSV形式に対応可能
- Claude Codeが自然言語でCSV変換・集計ロジックを生成、非エンジニアでも修正可能
対象読者:飲食店の店長・バックオフィス担当・中小IT担当者 / 今日できること:本記事のサンプルコードをそのまま実行して最小構成を動かす
はじめに:月末の集計地獄をどうにかしたい
飲食店経営において、月末の売上集計は地味に重い作業だ。POSレジはデータを吐き出してくれるが、「CSVを開いてピボットテーブルを作って、前月比を計算して、Excelに貼り付けて、店長にメールする」というルーティンを毎月繰り返している現場は多い。
筆者がいくつかの飲食店運営会社のIT化支援に関わって気づいたのは、「POSのデータは取れている。集計の自動化は技術的に難しくない。でも誰もやる時間がない」という構造だ。
本記事では、スマレジ・Square・Airレジなど主要POS CSVを Claude Code で自動集計し、月次レポートをSlackに通知するまでの実装パターンを全工程公開する。Python経験が浅い担当者でも Claude Code があれば実装できるように設計している。
免責事項:本記事のシナリオは想定モデルケースです。売上数値・工数削減率はすべて例示値であり、特定企業の実績を示すものではありません。実際の効果は環境・運用体制によって異なります。
対象のモデルケース
本記事で想定するモデルケースは以下の通り。
- 業態:飲食チェーン(居酒屋・カフェ等)、店舗数5〜20店舗規模
- POSレジ:スマレジ(メイン)、または Square / Airレジ(サブ)
- 課題:各店舗から月次CSV出力 → 本部で手動集計 → 集計に毎月3〜4時間かかっている
- IT環境:Mac/Windows + Python 3.10以上、Slack ワークスペースあり
- 担当者:バックオフィス担当1名(Python初心者〜中級者)
主要POS CSVのフォーマット比較
まず各POSが吐き出すCSVの列構成を把握しておく。フォーマットが違うため、共通スキーマへの正規化が最初のボトルネックになる。
| POS名 | 日時列 | 売上列 | 商品列 | 文字コード |
|---|---|---|---|---|
| スマレジ | 取引日時 |
売上合計 |
商品名 |
UTF-8 (BOM付き) |
| Square | Date / Time |
Gross Sales |
Item |
UTF-8 |
| Airレジ | 売上日 |
売上金額 |
商品名称 |
Shift_JIS |
| ユビレジ | 日付 |
合計金額 |
商品 |
UTF-8 |
Airレジの Shift_JIS は要注意だ。pandas.read_csv() のデフォルト UTF-8 で読もうとするとエラーになる。Claude Code にCSVを貼り付けると自動でエンコーディングを推定してくれるが、正規化スクリプトに明示的に書いておくのが安全だ。
実装の全体アーキテクチャ
各店舗のPOS CSV (月1回手動エクスポート or APIエクスポート)
↓
/data/raw/{YYYY-MM}/{store_id}/smaregi_YYYYMM.csv ← 命名規則統一
↓
normalize.py ← Claude Codeで生成・修正
(POS種別判定 → 統一スキーマ変換 → data/normalized/ 出力)
↓
aggregate.py ← Claude Codeで生成・修正
(月次・店舗別集計 → reports/YYYY-MM-report.json)
↓
notify_slack.py ← Claude Codeで生成・修正
(Slack Incoming Webhook → #monthly-sales チャンネル通知)
↓
run_monthly.sh ← cronまたは手動実行のエントリポイント
Claude Code を使った実装ステップ(6〜8ステップ)
以下の手順は Claude Code(ターミナル版)を使った実装フローだ。コマンドラインで claude を起動した状態で進める。
-
プロジェクトディレクトリの初期化
mkdir -p ~/pos-aggregator/{data/raw,data/normalized,reports,scripts}でフォルダ構成を作る。Claude Code に「このプロジェクト構成で月次POS集計システムを作りたい」と最初に伝えることでコンテキストを共有する。 -
サンプルCSVを用意してClaude Codeに読み込ませる
スマレジ管理画面から先月の売上CSV(小規模なもの)をエクスポートし、data/raw/2026-05/store01/smaregi_202605.csvに配置する。Claude Code のセッションで/add data/raw/2026-05/store01/smaregi_202605.csvコマンドでファイルをコンテキストに追加する。 -
正規化スクリプトの生成を依頼する
「このCSVを読み込んで、date / store_id / item / quantity / amount の5列に正規化するPythonスクリプトを書いてください。スマレジ・Square・Airレジを自動判別できるようにしてください」と指示する。Claude Code がnormalize.pyを生成する。 -
集計スクリプトの生成を依頼する
「正規化されたCSVを店舗別・カテゴリ別に月次集計して、前月比も計算したJSONを出力するスクリプトを書いてください」と指示する。生成されたaggregate.pyを実行して出力を確認する。 -
Slack通知スクリプトの生成を依頼する
「集計JSONを受け取り、Slack Incoming Webhook でフォーマットされたメッセージを送るスクリプトを書いてください。店舗ごとの売上ランキングと前月比を含めてください」と指示する。 -
実際の全店舗CSVで動作確認する
全店舗のCSVを配置してpython normalize.py --month 2026-05→python aggregate.py --month 2026-05→python notify_slack.py --month 2026-05 --dry-runの順で実行し、出力を目視確認する。 -
エラーをClaude Codeに貼り付けてデバッグする
AirレジのShift_JISエラーや列名の不一致など、実行時エラーが出た場合はエラーメッセージをそのままClaude Codeに貼り付ける。「このエラーを直してください」だけで修正案を出してくれる。 -
月次cronまたはGitHub Actionsで自動化する
毎月1日の朝に自動実行するcronジョブまたはGitHub Actions ワークフローを Claude Code に生成させる。「毎月1日9時にrun_monthly.shを実行するcron設定と、失敗時のエラーSlack通知も追加してください」と指示する。
Pythonサンプルコード:正規化スクリプト(normalize.py)
Claude Code が生成した正規化スクリプトの実例を示す。POS種別の自動判定ロジックが核心部分だ。
"""
normalize.py — POS CSV 正規化スクリプト
対応POS: スマレジ / Square / Airレジ / ユビレジ
Claude Code で生成・修正(モデルケース用サンプル)
"""
import pandas as pd
import pathlib
import json
import sys
from datetime import datetime
# ---- POS 判定ロジック ----
def detect_pos_type(df: pd.DataFrame) -> str:
"""列名からPOS種別を判定する"""
cols = set(df.columns)
if "取引日時" in cols and "売上合計" in cols:
return "smaregi"
elif "Date" in cols and "Gross Sales" in cols:
return "square"
elif "売上日" in cols and "売上金額" in cols:
return "airregi"
elif "日付" in cols and "合計金額" in cols:
return "ubiregi"
raise ValueError(f"未知のPOS形式です。列名: {list(cols)[:10]}")
# ---- スキーマ変換 ----
def normalize_smaregi(df: pd.DataFrame, store_id: str) -> pd.DataFrame:
return pd.DataFrame({
"date": pd.to_datetime(df["取引日時"]).dt.date,
"store_id": store_id,
"item": df.get("商品名", "不明"),
"quantity": df.get("数量", 1).fillna(1).astype(int),
"amount": df["売上合計"].astype(float),
})
def normalize_square(df: pd.DataFrame, store_id: str) -> pd.DataFrame:
# Square は Date + Time が別列
date_str = df["Date"].astype(str) + " " + df["Time"].astype(str)
return pd.DataFrame({
"date": pd.to_datetime(date_str).dt.date,
"store_id": store_id,
"item": df.get("Item", "不明"),
"quantity": df.get("Qty", 1).fillna(1).astype(int),
"amount": df["Gross Sales"].str.replace(",", "").astype(float),
})
def normalize_airregi(df: pd.DataFrame, store_id: str) -> pd.DataFrame:
return pd.DataFrame({
"date": pd.to_datetime(df["売上日"]).dt.date,
"store_id": store_id,
"item": df.get("商品名称", "不明"),
"quantity": df.get("数量", 1).fillna(1).astype(int),
"amount": df["売上金額"].astype(float),
})
def normalize_ubiregi(df: pd.DataFrame, store_id: str) -> pd.DataFrame:
return pd.DataFrame({
"date": pd.to_datetime(df["日付"]).dt.date,
"store_id": store_id,
"item": df.get("商品", "不明"),
"quantity": df.get("数量", 1).fillna(1).astype(int),
"amount": df["合計金額"].astype(float),
})
NORMALIZER = {
"smaregi": normalize_smaregi,
"square": normalize_square,
"airregi": normalize_airregi,
"ubiregi": normalize_ubiregi,
}
# ---- メイン処理 ----
def normalize_csv(csv_path: pathlib.Path, store_id: str) -> pd.DataFrame:
# エンコーディングを自動判定(Airレジ対応)
for enc in ["utf-8-sig", "shift_jis", "utf-8"]:
try:
df = pd.read_csv(csv_path, encoding=enc)
break
except (UnicodeDecodeError, Exception):
continue
else:
raise ValueError(f"エンコーディング判定失敗: {csv_path}")
pos_type = detect_pos_type(df)
print(f" [{store_id}] POS判定: {pos_type} (行数: {len(df)})")
return NORMALIZER[pos_type](df, store_id)
def main(month: str, data_dir: pathlib.Path, output_dir: pathlib.Path):
"""month: YYYY-MM 形式"""
raw_dir = data_dir / "raw" / month
if not raw_dir.exists():
print(f"データディレクトリが見つかりません: {raw_dir}")
sys.exit(1)
all_frames = []
for store_dir in sorted(raw_dir.iterdir()):
if not store_dir.is_dir():
continue
store_id = store_dir.name
csv_files = list(store_dir.glob("*.csv"))
if not csv_files:
print(f" [{store_id}] CSVなし — スキップ")
continue
for csv_file in csv_files:
try:
normalized = normalize_csv(csv_file, store_id)
all_frames.append(normalized)
except Exception as e:
print(f" [{store_id}] エラー: {e}")
if not all_frames:
print("正規化できるデータがありません")
sys.exit(1)
combined = pd.concat(all_frames, ignore_index=True)
output_dir.mkdir(parents=True, exist_ok=True)
out_path = output_dir / f"{month}-normalized.csv"
combined.to_csv(out_path, index=False)
print(f"n正規化完了: {out_path} ({len(combined):,}行)")
return out_path
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--month", required=True, help="YYYY-MM")
parser.add_argument("--data-dir", default="./data", type=pathlib.Path)
parser.add_argument("--output-dir", default="./data/normalized", type=pathlib.Path)
args = parser.parse_args()
main(args.month, args.data_dir, args.output_dir)
Pythonサンプルコード:月次集計スクリプト(aggregate.py)
正規化済みCSVを受け取って店舗別・前月比込みのJSONを生成するスクリプトだ。このJSONがSlack通知の素材になる。
"""
aggregate.py — 月次集計スクリプト
入力: data/normalized/{YYYY-MM}-normalized.csv
出力: reports/{YYYY-MM}-report.json
"""
import pandas as pd
import json
import pathlib
import sys
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
def load_normalized(month: str, normalized_dir: pathlib.Path) -> pd.DataFrame:
path = normalized_dir / f"{month}-normalized.csv"
if not path.exists():
raise FileNotFoundError(f"正規化ファイルが見つかりません: {path}nnormalize.py を先に実行してください")
df = pd.read_csv(path)
df["date"] = pd.to_datetime(df["date"])
df["amount"] = df["amount"].astype(float)
return df
def calc_monthly(df: pd.DataFrame) -> dict:
"""店舗別・全体の月次集計"""
total = float(df["amount"].sum())
tx_count = len(df)
avg_ticket = total / tx_count if tx_count > 0 else 0
by_store = (
df.groupby("store_id")["amount"]
.agg(["sum", "count"])
.rename(columns={"sum": "total", "count": "transactions"})
.sort_values("total", ascending=False)
.reset_index()
)
by_store["avg_ticket"] = by_store["total"] / by_store["transactions"]
# 日次推移(上位5日)
daily = (
df.groupby(df["date"].dt.day)["amount"]
.sum()
.sort_values(ascending=False)
.head(5)
)
return {
"total_sales": round(total),
"transaction_count": tx_count,
"avg_ticket": round(avg_ticket),
"by_store": by_store.to_dict(orient="records"),
"top5_days": [{"day": int(k), "sales": round(float(v))} for k, v in daily.items()],
}
def calc_mom(current: dict, prev: dict) -> float:
"""前月比(%)"""
if prev["total_sales"] == 0:
return None
return round((current["total_sales"] / prev["total_sales"] - 1) * 100, 1)
def main(month: str, data_dir: pathlib.Path, reports_dir: pathlib.Path):
normalized_dir = data_dir / "normalized"
current = load_normalized(month, normalized_dir)
current_stats = calc_monthly(current)
# 前月データ(存在すれば)
prev_month = (datetime.strptime(month, "%Y-%m") - relativedelta(months=1)).strftime("%Y-%m")
try:
prev_df = load_normalized(prev_month, normalized_dir)
prev_stats = calc_monthly(prev_df)
mom = calc_mom(current_stats, prev_stats)
prev_total = prev_stats["total_sales"]
except FileNotFoundError:
mom = None
prev_total = None
report = {
"month": month,
"generated_at": datetime.now().isoformat(),
"current": current_stats,
"prev_month": prev_month,
"prev_total": prev_total,
"mom_pct": mom,
}
reports_dir.mkdir(parents=True, exist_ok=True)
out_path = reports_dir / f"{month}-report.json"
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
print(f"集計完了: {out_path}")
print(f" 総売上: {current_stats['total_sales']:,}円 前月比: {mom:+.1f}%" if mom else f" 総売上: {current_stats['total_sales']:,}円 前月比: データなし")
return out_path
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--month", required=True)
parser.add_argument("--data-dir", default="./data", type=pathlib.Path)
parser.add_argument("--reports-dir", default="./reports", type=pathlib.Path)
args = parser.parse_args()
main(args.month, args.data_dir, args.reports_dir)
Pythonサンプルコード:Slack通知スクリプト(notify_slack.py)
集計JSONをSlackに投げるスクリプトだ。Block Kit 形式で視認性の高いメッセージを作る。
"""
notify_slack.py — Slack Incoming Webhook 通知スクリプト
環境変数: SLACK_WEBHOOK_URL
"""
import json
import os
import pathlib
import sys
import urllib.request
from datetime import datetime
def load_report(month: str, reports_dir: pathlib.Path) -> dict:
path = reports_dir / f"{month}-report.json"
if not path.exists():
raise FileNotFoundError(f"集計レポートが見つかりません: {path}")
return json.loads(path.read_text())
def build_slack_blocks(report: dict) -> dict:
"""Slack Block Kit ペイロード構築"""
month = report["month"]
c = report["current"]
mom = report.get("mom_pct")
mom_str = f"{mom:+.1f}%" if mom is not None else "データなし"
mom_emoji = "📈" if (mom or 0) >= 0 else "📉"
# 店舗ランキング(上位5店舗)
store_lines = []
for i, s in enumerate(c["by_store"][:5], start=1):
store_lines.append(
f"{i}. *{s['store_id']}* {int(s['total']):,}円 ({int(s['transactions'])}件)"
)
store_text = "n".join(store_lines) if store_lines else "データなし"
payload = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"📊 {month} 月次売上レポート",
"emoji": True
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*総売上*n:moneybag: {c['total_sales']:,}円"
},
{
"type": "mrkdwn",
"text": f"*前月比*n{mom_emoji} {mom_str}"
},
{
"type": "mrkdwn",
"text": f"*取引件数*n{c['transaction_count']:,}件"
},
{
"type": "mrkdwn",
"text": f"*客単価*n{c['avg_ticket']:,}円"
}
]
},
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*店舗別売上ランキング(上位5店舗)*n{store_text}"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"生成: {report.get('generated_at', '不明')} | POS CSV → Claude Code 自動集計"
}
]
}
]
}
return payload
def send_to_slack(webhook_url: str, payload: dict, dry_run: bool = False) -> bool:
if dry_run:
print("[DRY RUN] 以下のペイロードを送信します:")
print(json.dumps(payload, ensure_ascii=False, indent=2))
return True
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
webhook_url,
data=data,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
body = resp.read().decode()
if body == "ok":
print("Slack通知送信完了")
return True
else:
print(f"Slack APIエラー: {body}")
return False
except Exception as e:
print(f"Slack通知失敗: {e}")
return False
def main(month: str, reports_dir: pathlib.Path, dry_run: bool = False):
webhook_url = os.environ.get("SLACK_WEBHOOK_URL", "")
if not webhook_url and not dry_run:
print("エラー: 環境変数 SLACK_WEBHOOK_URL を設定してください")
sys.exit(1)
report = load_report(month, reports_dir)
payload = build_slack_blocks(report)
success = send_to_slack(webhook_url, payload, dry_run=dry_run)
sys.exit(0 if success else 1)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--month", required=True)
parser.add_argument("--reports-dir", default="./reports", type=pathlib.Path)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
main(args.month, args.reports_dir, args.dry_run)
実行スクリプトとcron設定(run_monthly.sh)
3スクリプトをまとめて実行するシェルスクリプトと、毎月1日9時に自動実行するcron設定を示す。Claude Code に「これら3つをまとめて毎月自動実行するシェルスクリプトを作って」と依頼して生成したものベースだ。
#!/bin/bash
# run_monthly.sh — 月次POS集計 エントリポイント
# 使用例: bash run_monthly.sh 2026-05
# cron例: 0 9 1 * * /path/to/run_monthly.sh $(date -d "last month" +%Y-%m) >> /var/log/pos-aggregator.log 2>&1
set -euo pipefail
MONTH="${1:-$(date -d 'last month' +%Y-%m 2>/dev/null || date -v-1m +%Y-%m)}"
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV="$BASE_DIR/.venv"
echo "======================================="
echo "POS月次集計 開始: $MONTH"
echo "実行日時: $(date '+%Y-%m-%d %H:%M:%S')"
echo "======================================="
# venv 有効化(存在する場合)
if [ -f "$VENV/bin/activate" ]; then
source "$VENV/bin/activate"
fi
# Step 1: 正規化
echo "[Step 1] CSV正規化..."
python "$BASE_DIR/normalize.py" --month "$MONTH" --data-dir "$BASE_DIR/data"
# Step 2: 集計
echo "[Step 2] 月次集計..."
python "$BASE_DIR/aggregate.py" --month "$MONTH"
--data-dir "$BASE_DIR/data"
--reports-dir "$BASE_DIR/reports"
# Step 3: Slack通知
echo "[Step 3] Slack通知..."
python "$BASE_DIR/notify_slack.py" --month "$MONTH"
--reports-dir "$BASE_DIR/reports"
echo "======================================="
echo "完了: $MONTH 月次集計 正常終了"
echo "======================================="
cron設定(crontab -e で追記):
# 毎月1日 09:00 JST に前月分の集計を実行
0 9 1 * * SLACK_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
/path/to/pos-aggregator/run_monthly.sh
>> /var/log/pos-aggregator.log 2>&1
Claude Code の使い方のコツ:この実装で学んだ3つ
実際にこの実装を進める中で、Claude Code を効果的に使うためのパターンが見えてきた。
-
実際のCSVをそのまま貼り付ける
「スマレジのCSVを解析して」と文章で説明するより、実際のCSVの先頭10行をそのまま貼り付けた方が圧倒的に精度が高い。列名・データ型・エンコーディングを Claude Code が自分で判断してくれる。個人情報が含まれる場合は列を削除またはマスクしてから貼り付けること。 -
エラーメッセージを丸ごとコピーする
実行してエラーが出たら、Traceback全文をそのまま貼り付けて「直してください」だけで十分だ。「〇〇エラーが出ました」と要約するより、生のエラーを渡す方が確実に修正してくれる。 -
「テストデータで動かしてから本番データで」の順序を守る
1店舗・1ヶ月分の小さなCSVで動作確認してから全店舗データを流す。Claude Code が生成するコードは基本的に動くが、実データ特有の例外(空行・文字化け・列名の微妙な違い)は実際に動かさないと出てこない。
導入効果の試算(モデルケース)
| 指標 | 導入前(手動) | 導入後(自動) | 改善 |
|---|---|---|---|
| 月次集計作業時間 | 3〜4時間 | 約15分 | 約85%削減 |
| レポート作成ミス | 月1〜2件(コピペミス等) | 0件(自動化) | ヒューマンエラーゼロ |
| レポート共有までのラグ | 月末〜翌3〜5日 | 翌1日 午前中 | 意思決定スピード向上 |
| 初期実装工数(Claude Code使用) | — | 約8〜12時間 | 初期1回限り |
※ 上記は5〜10店舗規模のモデルケース想定値。実際の削減率は店舗数・CSV品質・担当者スキルによって異なります。
導入ロードマップ(3フェーズ)
Phase 1(1〜2週間): 1店舗でPoC
1店舗分のCSV 1〜3ヶ月分を使ってnormalize.py → aggregate.py → notify_slack.py の動作を確認する。Slack通知の形式を店長・バックオフィス担当者に見せてフィードバックをもらう。Claude Codeに「店長が見やすいように通知の形式を変えて」と頼むだけでレイアウトを調整できる。
Phase 2(1〜2ヶ月): 全店舗展開 + cron自動化
全店舗のCSV出力フローを統一する(ファイル命名規則の統一が鍵)。cronまたはGitHub Actionsで毎月1日に自動実行するよう設定する。失敗時のエラー通知もSlackに送るよう設定する。
Phase 3(3ヶ月以降): 分析の高度化
曜日別・時間帯別の売上分析、気候・イベントとの相関分析、異常値アラート(前週比30%超の急落を検知)などを追加する。これらも Claude Code に「〇〇の分析を追加して」と依頼するだけで実装できる。POSのAPIが使えるレジ(スマレジはAPIあり)はCSV手動エクスポートをAPIに切り替えて完全自動化する。
よくある失敗パターンと対処法
実装でよくハマるパターンを整理する。
-
❌ Airレジ CSVがUnicodeDecodeErrorで読めない
⭕encoding='shift_jis'または'cp932'を指定する。サンプルコードのfor enc in ["utf-8-sig", "shift_jis", "utf-8"]のループで自動判定できる -
❌ 金額列に「¥」や「,」が含まれてfloat変換エラー
⭕df["amount"].str.replace("[¥,]", "", regex=True).astype(float)で前処理する。Claude Code にエラーを貼り付けると自動でこの修正を提案してくれる -
❌ 店舗のCSVファイル名がバラバラでファイル検索できない
⭕ ファイル命名規則を先に決めてドキュメント化する。「2026-05_store01.csv」のように年月・店舗IDをファイル名に含める運用ルールが必須 -
❌ Slack通知が届かない(webhook URLの設定ミス)
⭕ まず--dry-runフラグで出力を確認する。curl で直接webhookを叩いてURLの有効性を確認する:curl -X POST -H 'Content-type: application/json' --data '{"text":"test"}' YOUR_WEBHOOK_URL
スマレジAPIを使った完全自動化(発展)
スマレジは公式のREST APIを提供しており、CSV手動エクスポートをスキップして売上データを直接取得できる。Claude Code に「スマレジAPIの公式ドキュメントのURLを貼るから、このAPIで売上データを取得するコードを書いて」と指示すると、OAuth認証込みのクライアントを生成してくれる。
スマレジAPIの主要エンドポイント(スマレジ開発者向け公式ページ参照):
GET /transactions— 取引一覧(売上データの本体)GET /products— 商品マスタ(商品名・カテゴリの正規化に使う)GET /stores— 店舗一覧(store_id のマッピングに使う)
APIを使う場合、毎月1日にcronで「前月分の取引データを自動取得 → 集計 → Slack通知」まで完全無人化できる。CSV出力の手間がゼロになるため、繁忙期でも集計が滞らない。
このパターンが適用できる他の業態
本記事で示した「POSデータ → Claude Code 集計 → Slack通知」のパターンは飲食店に限らず、以下の業態でも同様に適用できる。
- 美容室・ネイルサロン: スタイリスト別売上・指名率の月次レポート
- アパレル小売: SKU別・カラー別売上の週次集計
- フィットネスジム: 会員属性別の月会費収入集計
- コンビニFC加盟店: 本部提供のPOS集計データをオーナー独自指標で再集計
共通点は「CSVが吐き出される仕組みがある」こと。どんな業態でもCSVがあればこのパターンが使える。
まとめ:Claude Code は「集計の自動化」に特に向いている
月次POS集計は、データ構造が安定していてロジックが明確なため、Claude Code による自動化と相性が良い。
実装の核は3点だ。1つ目は各POS CSVを統一スキーマに正規化する部分で、Claude Code が実際のCSVを見て自動的に変換ロジックを書いてくれる。2つ目は集計ロジックで、前月比・店舗ランキング等も「計算してください」と依頼するだけで実装される。3つ目はSlack通知で、Block Kit の見やすいフォーマットも自然言語で指示できる。
Python初心者が1人で実装する場合でも、Claude Code があれば2日程度で最小構成を動かせる。初期投資は8〜12時間の工数だが、毎月3〜4時間の削減効果が継続するため、3〜4ヶ月でペイする計算だ。
FAQ
- Q: Pythonが書けなくても実装できますか?
- A: はい。Claude Code に「このエラーを直して」「この列を追加して」と自然言語で指示するだけで修正できます。基本的な実行コマンド(
python xxx.py --month 2026-05)さえ覚えれば運用できます。 - Q: スマレジ以外のPOSには対応できますか?
- A: はい。サンプルコードはSquare・Airレジ・ユビレジに対応しています。他のPOS CSVは実際のファイルをClaude Codeに貼り付けて「これも対応してください」と依頼すれば追加できます。
- Q: 売上データをClaude Codeに渡すのはセキュリティ上問題ありませんか?
- A: Claude Codeはローカル実行のため、CSVファイルの中身がAnthropicのサーバーに送られるわけではありません。ただしClaude Codeがコード生成のために会話文脈を送信する際に一部のデータが含まれる可能性があります。個人情報(顧客名・連絡先等)が含まれるCSVはマスク処理してから使用することを推奨します。
- Q: 複数のPOSが混在する環境でも使えますか?
- A: 使えます。サンプルコードのdetect_pos_type()関数が列名からPOS種別を自動判定します。店舗Aはスマレジ、店舗BはSquareのように混在していても、正規化後は同じスキーマに統一されます。
- Q: Slack以外(メール・LINE等)への通知もできますか?
- A: はい。notify_slack.pyの代わりに「メールで送るバージョンを作って」「LINE Notifyで送るバージョンを作って」とClaude Codeに依頼するだけで別の通知スクリプトを生成できます。
内部リンク
// 導入支援のご相談
Claude Code × 業務自動化の個別指導・導入支援
株式会社Uravationでは、Claude Codeを活用した業務自動化の個別指導・受託開発支援を提供しています。本記事のようなCSV集計・Slack通知の実装から、業務特有の複雑な要件まで対応します。