FAX1枚が「注文書」であり、その中に複数の「明細行」がある。
この2層の構造をどう持つか。スプレッドシートで管理しようとしたら、「1枚の注文書に対して行数が変わる」という問題にすぐ直面する。月次カレンダー形式のA社なら1枚に20〜30行。D社なら数行。行数が可変のデータはスプレッドシートで扱いづらい。
PostgreSQLを選んだのはそのためだ。
テーブル設計
3つのテーブルで管理する。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_code VARCHAR(64) UNIQUE NOT NULL,
supplier VARCHAR(128),
status VARCHAR(32) DEFAULT 'draft',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id),
product_name VARCHAR(256),
quantity NUMERIC(10, 2),
unit VARCHAR(32),
delivery_date DATE,
confidence VARCHAR(16) DEFAULT 'medium',
note TEXT,
confirmed BOOLEAN DEFAULT FALSE
);
CREATE TABLE order_images (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id),
image_path VARCHAR(512),
page_no INTEGER DEFAULT 1
);
order_codeは「FAX-20260606-MR01」のような形式にした。日付と取引先コードと連番を組み合わせる。同じ日に同じ取引先から複数枚届いても区別できる。
confidenceはClaude Visionが各行の読取に対して返す確信度だ。high・medium・lowの3段階。これを確認画面のUIで活用する。
APIの構成
POST /api/orders/upload # 画像アップロード+AI読取開始
GET /api/orders # 注文書一覧
GET /api/orders/{id} # 注文書詳細(明細含む)
PATCH /api/orders/{id}/items/{item_id} # 明細修正
POST /api/orders/{id}/confirm # 注文書確認完了
GET /api/orders/{id}/images/{page} # 画像取得
RESTful な構造で、フロントエンドはvanilla JSで書いた静的HTMLが叩く。
非同期処理の設計
画像をアップロードしてからClaude Visionの処理が返るまで、10〜30秒かかることがある。これを同期で処理するとAPIがタイムアウトしてしまう。
FastAPIのBackgroundTasksを使った。アップロードが完了した時点でorder_idをフロントエンドに返し、Claude Vision処理はバックグラウンドで走る。
@app.post('/api/orders/upload')
async def upload_order(file: UploadFile, background_tasks: BackgroundTasks):
order_id = save_file_and_create_record(file)
background_tasks.add_task(process_with_ai, order_id)
return {'order_id': order_id, 'status': 'processing'}
フロントエンドはポーリング(3秒ごとに状態確認)で「processingからdraftへの変化」を検知する。変化を検知したら確認画面に切り替わる。
Claude Vision呼び出し
import anthropic, base64
client = anthropic.Anthropic()
def read_fax_image(image_path: str, supplier: str) -> list[dict]:
with open(image_path, 'rb') as f:
image_data = base64.standard_b64encode(f.read()).decode('utf-8')
message = client.messages.create(
model='claude-opus-4-8',
max_tokens=4096,
messages=[{
'role': 'user',
'content': [
{
'type': 'image',
'source': {
'type': 'base64',
'media_type': 'image/jpeg',
'data': image_data,
},
},
{'type': 'text', 'text': build_prompt(supplier)}
]
}]
)
return parse_response(message.content[0].text)
プロンプトは取引先ごとに変える。カレンダー形式のA社と、丸印形式のD社では、帳票の読み方の説明が根本的に違うからだ。
設計を決めてから開発する理由
このシステムは「動く」だけでなく「担当者が毎日使える」ことが条件だ。スキーマ設計を後から変えると、既存データの移行が必要になる。order_codeのUNIQUE制約・confidenceカラム・confirmedフラグ。これらは機能要件から逆算して最初から入れた。
後から追加するより、最初から正しく設計する方が結果的に速い。
*次回は「Claude Visionで帳票を読む——4種の帳票と精度の違い」*
*シンプルシステム株式会社 代表 伊藤勝彦*