TL;DR
納品書や出荷表を 写真/PDF で投げると、vision(gpt-4o)が明細表と伝票ヘッダー(取引先・日付・伝票番号)を抽出する。
抽出後は在庫CSVと同じ会話UIを mode で使い回し、できた表は aiTableToParsedRows で既存の入荷/出荷パイプラインに無改修注入される。
伝票ヘッダーは「列」でなく「1伝票=全行共通の値」として扱い、再利用は取引先(clientId)キーで誤マッチを防ぐ。
会話UIや列カードの仕組みは在庫と完全に共有です。入荷/出荷で増えるのは次の3点だけ。
| 観点 | 在庫CSV | 入荷/出荷 |
|---|---|---|
| 入口 | CSV/Excel | CSV/Excel + 写真/PDF |
| 伝票ヘッダー(documentFields) | 常に空(無視) | 取引先・伝票番号・日付を1伝票=全行共通で使う |
| 再利用キー | 列名だけ | 列名 + 取引先ID(誤マッチ防止) |
「同じものを作り直さない」が徹底されていて、入荷/出荷は在庫の会話エンジンに橋渡しの薄い層を足しただけです。
imageFileToDataUrl(file)(src/util/imageToDataUrl.ts)。対応は jpg/png/webp のみ、HEIC非対応(呼び出し側で事前に弾く)。data:image/...)でインライン送信するのは、任意URLをOpenAIのフェッチャに渡すSSRF踏み台化を防ぐため(サーバー側でも data:image/ 以外を拒否)。複数ページ/複数枚は images: [{url}, ...] で一度に渡せる。サーバーの extractDocumentTable が会社確認→権限→日次クォータ(200)→vision LLM→矯正、の順で処理します。
{ headers, rows, documentFields }。「見えない値は創作しない」「合計・小計行は明細に含めない」「品目と無関係な個人情報は出さない」をプロンプトで指示。aiClient.structured(..., ExtractedDocumentSchema, {tier:'strong', schemaName:'document_extract'})。sanitizeExtractedDocument(上限: 50列 / 2000行 / セル2000字)。AI由来のデータを、既存の受払処理(プレビュー→実行)に手を入れずに流し込むための変換層。3つの純関数が橋渡しします。
| 関数 | 役割 |
|---|---|
aiTableToParsedRows(cleanedTable, documentFields) | 正規化済みの表を、既存パーサが食べる ParsedRow[] に変換。取引先/伝票番号/日付は「列」でなく documentFields から全行に当てる。アイテム名空・数量不正はエラー行に。 |
aiTableToHeaderMapping(headers) | 入荷/出荷が使う HeaderMapping を生成。単価・税区分・取引先・日付・伝票番号の列インデックスは全て -1(=documentFieldsと後段導出で扱う)。 |
detectAiTableColumns(headers) | アイテム名列(無ければエラー)・在庫数列(=数量)・nancoID列を特定し、残りをフォルダ列/属性列に分類。 |
useImportAttributePricing が「価格ロール属性」から導出するため。価格の出どころを1箇所に統一して二重計算を防ぐ。useAiTradeImportFlow在庫の CSVImportModal が持つAIライフサイクルを、入荷/出荷向けに切り出した共有フック。責務はドロップの振り分けと、抽出・同意・取込成功・ロールバックの管理です。
startFromFile)resolveClientIdByName で clientId 解決(既存取引先のみ)→ seedClientId。detectTradeTemplateMatch。テンプレ適合・空データ・サイズ超過は従来パースへ。不適合だけ空列を落としてスレッドへ。detectTradeTemplateMatch(在庫の detectStockTemplateMatch の入荷出荷版)。会話・抽出・注入は対称(ほぼ同じコード)。本質的に違うのは価格ロールの向きだけです。
skipMatched: true = 新規行のみ仕入値を導出ReceivingCsvImportMultipleModalskipMatched: false = 既存ヒット行も売価で上書きShippingCsvImportMultipleModalどちらのモーダルも useAiTradeImportFlow を mode 違いで使い、aiPhase(thread / extracting / consent)で表示を切り替えます。AI注入口 loadFromAiTable は aiTableToHeaderMapping + aiTableToParsedRows をプレビューに流すだけ。
AiImportThread は mode プロップ(stock / receiving / shipping)を取りますが、UIの構造は変わりません。mode は主にデータ経路の引数として効きます。
mode を渡す。seedClientId 指定時(入荷/出荷)は lookup を取引先キーで照合(誤マッチ防止)。onComplete で返す documentFields は入荷/出荷で「1文書=1タスク」に使い、在庫では親が捨てる。mergeDocumentFields は画像seedとCSV解析結果をフィールド単位で ?? 合成する。文書全体に seed ?? result すると、空(全null but 非nullオブジェクト)で解析結果が丸ごと落ち、取引先・日付が消えるため。