Zoho CRMの商談に混ざった過去の受注明細は、商談名などの規則で機械的に切り分け、販売実績用のカスタムモジュールへ移行すると、商談を「これから受注を目指す案件」だけに戻せます。この作業を、ここでは「商談からの過去受注の切り出し」と呼びます。
この記事では、中小の金属表面処理加工業で、十数万件の過去受注を商談から販売実績モジュールへ移した手順を紹介します。後半では、移行先の項目作成が抜けたまま移行を終えてしまった失敗と、そこからどう復旧したかも書きます。同じ作業をする方が、同じところでつまずかないための記録です。
何が起きていたか
この会社では、Zoho CRMの導入時に、基幹システムから出力した過去の受注明細を商談モジュールへ取り込んでいました。1件の商談が「伝票の1行(1品目)」にあたり、件数は十数万件になっていました。
その結果、次の2つの問題が出ていました。
- ダッシュボードの受注金額が意味のない数字になる。過去数年分の明細がすべて「受注」ステージに入っているため、直近の営業成果が埋もれていました
- 日々の一覧が使いにくい。営業担当が動かしている商談と、過去の明細が同じ一覧に並んでいました
そこで、過去の明細を「販売実績」というカスタムモジュールへ移し、商談は営業担当が作った案件だけにすることにしました。データは消さずに参照できる状態で残す、というのが前提です。
手順1:移行対象を機械的に切り分ける
最初に、どのレコードが「過去の明細」で、どれが「本物の商談」かを分けます。
この会社の取り込みデータは、商談名に決まった形がありました。
取引先名_品番_伝票番号_連番
営業担当が手で作る商談には、この形はありません。そこで商談名を _(半角アンダースコア)で区切り、末尾の2つがどちらも数字のものを移行対象としました。取引先名には空白などが入るため、先頭からではなく末尾から分解するのがポイントです。
- 末尾 = 連番(伝票内の行番号)
- 末尾から2番目 = 伝票番号
- 末尾から3番目 = 品番(空のものや「送料」のような品番でない値も含む)
- 残り = 取引先名の表記
実データで確かめると、商談名の99.9%がこの形に当てはまりました。当てはまらない百数十件は、営業担当が作った商談かテスト用のデータだったため、商談に残しています。
なお、Zoho CRMのデータをSQLに似た書き方で取り出すCOQL(CRM Object Query Language)にも LIKE による部分一致はありますが、当社が確認した時点では正規表現のような細かい条件は書けませんでした。そのため件数の見積もりにだけCOQLを使い、本番の判定はバックアップのCSVをスクリプトで分解して行い、結果を目視でサンプリングして確かめています。
手順2:移行キーには何を使う?完全に一意な値だけにする
移行キーとは、移行元と移行先のレコードを1対1で結びつけるための値です。再実行しても二重登録にならないようにするために使います。
最初は「伝票番号+連番+品番」で一意になると考えていました。ところが実データを集計すると、この組み合わせでも数十件が重複していました。同じ伝票に同じ品番の行が重なっている、本物の重複明細です。
そこで移行キーは、移行元の商談のレコードID(データID)だけにしました。移行先には「元商談ID」というテキスト項目を作って保存します。これが後の復旧で効いてきます。
手順3:項目対応表を「既存・要作成・除外」の3つに分ける

次に、移行元の列ごとに、移行先のどの項目へ入れるかを決めます。このとき、列を3つに分けて表にしました(対応表の「要検討」を潰していく進め方は「Zoho CRMへのデータ移行で漏れを防ぐ」で解説しています)。
| 区分 | 意味 | 例 |
|---|---|---|
| 既存 | 移行先にすでにあり、すぐ入れられる | 名前、金額、日付、担当者 |
| 要作成 | 移行前に項目を作る必要がある | 取引先(ルックアップ)、伝票番号、品番、連番、元商談ID、サイズ、元作成日時 |
| 除外 | 移行しない | ステージ、期待値、商談期間などの商談プロセス用の指標 |
販売実績モジュールは作ってあったものの、業務用の項目は金額と日付くらいしかない、ほぼ空の状態でした。つまり「要作成」の行をすべて作り終えないと、移行しても取引先とつながらない台帳になります。
除外した列は、パイプライン(営業の進み具合)を測るための指標です。全件が「受注」で固定の実績データに持ち込んでも意味がないため、入れませんでした。
手順4:商談とのルックアップは空のまま入れる
販売実績と「受注につながった商談」を結びつけたいという要望もありました。ただ、明細と商談の対応は機械的には決まりません。そこで、商談へのルックアップは空のまま入れ、紐づけは後から人が確かめて行う方針にしました。
自動で推測した紐づけを大量に入れると、間違いを後から見つけるのが難しくなります。確信の持てない関連は空で入れる、というのは大量移行の基本です。
手順5:削除の前に全列のバックアップを取る
商談側のレコードを消す(またはステージを変える)前に、商談モジュールを全列でエクスポートしておきます。取引先のIDのような関連の値も、このファイルに残ります。
移行は一度に流さず、件数を突き合わせながら段階的に進めます。移行前と移行後で件数と金額の合計が合うことを確かめてから、商談側を整理します。
失敗:項目を作らないまま移行と削除まで進んだ
ここからが失敗の話です。
項目対応表では、取引先ルックアップや伝票番号などの項目を作る設計になっていました。ところが当社の移行作業では、本番に項目を作る手順が抜けたまま、販売実績にもともとあった3項目(名前・納品日・金額)だけをコピーする最小構成で移行し、そのまま商談側の削除まで完了していました。
しばらくして、お客様からご指摘をいただきました。商談にあったころは取引先とつながっていたので、取引先別の売上グラフや、取引先のカテゴリ別の円グラフを作れました。ところが販売実績に移った後は取引先とのつながりがなく、月ごとの金額しか分からなくなっていたのです。
原因は、意図して情報を落としたことではありません。「本番に項目を作る」という作業が、移行の手順の中で独立した一歩になっていなかったことです。設計書には書いてあっても、実行の手順書にチェック項目として載っていなければ抜けることがあります。
移行後に項目が足りないと分かったら?バックアップから全件を補完する
幸い、商談を削除する前のバックアップ(40列近い全列・全行)が残っていました。取引先のIDもすべて入っていたため、移行をやり直さずに足りない項目だけを補完できました。進め方は次のとおりです。
- 項目を追加する。 取引先(ルックアップ)、伝票番号、品番、明細行番号、元商談ID、サイズ、元作成日時の7項目を販売実績に作りました
- API名をラベルから引く。 日本語ラベルで作った項目は、当社の環境ではAPI名が
field1のような自動の名前になりました。スクリプトにAPI名を直接書かず、項目一覧APIからラベルで引くようにしました(APIで項目を作るときの注意点は「Zoho CRMのカスタム項目をAPIで作成する」) - 移行元と移行先を突き合わせる。 販売実績の名前と、バックアップの商談名を一致させます。全角・半角の空白の違いは1つの半角空白にそろえてから比べました。同じ名前が重複しているもの(数十組)は、金額・日付・IDの順に並べて上から順に対応させました
- ドライランで全件の対応を確かめる。 書き込まずに突き合わせだけを行い、両側とも対応のないレコードが0件であることを確認しました
- 本実行する。 100件ずつ更新し、ワークフローは動かさない設定にしました(1回の更新は100件まで。Zoho CRM API:Update Records)。元商談IDが入っているレコードは飛ばすようにしたため、何度実行しても同じ結果になります
- 途中で止まっても再実行する。 実際に、全体の4分の3ほど進んだところで一時的なネットワーク切断が起きました。リトライの仕組みを足して再実行し、残りを補完しました
- 事後に未設定0件を確かめる。 元商談ID・取引先・伝票番号が空のレコードが残っていないことを、COQLで確認しました
担当者の項目も、バックアップの値で戻しました。ただし、すでに無効になったユーザーは担当者に設定できないため、該当する数件は担当者以外の項目だけを補完しています。
補完後は、取引先の画面に販売実績の関連リストが表示され、取引先別の売上や、取引先のカテゴリ別の集計もレポートで作れるようになりました。
実装のポイント(コード例)
補完に使った処理の要点を、項目名を一般的な名前に置き換えて載せます。Node.js(18以降)で動く形です。トークンの取得部分は省いています。
// 商談名を末尾から分解する(取引先名_品番_伝票番号_連番)
function parseDealName(name) {
const parts = name.split('_');
const last = parts[parts.length - 1];
const second = parts[parts.length - 2];
if (parts.length < 3 || !/^\d+$/.test(last) || !/^\d+$/.test(second)) {
return null; // 本物の商談またはテストデータ
}
return {
lineNo: parseInt(last, 10),
slipNo: second,
partNo: parts[parts.length - 3],
};
}
// 名前の空白をそろえてから比べる
const normName = (s) => s.replace(/[\s ]+/g, ' ').trim();
// 移行先を全件取得する(IDの昇順で、前回の最後のIDより大きいものを取る)
async function fetchAll(apiDomain, token) {
const records = [];
let lastId = '0';
for (;;) {
const res = await fetch(`${apiDomain}/crm/v8/coql`, {
method: 'POST',
headers: { Authorization: `Zoho-oauthtoken ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
select_query: `select id, Name, Amount, Delivery_Date from Sales_History where id > ${lastId} order by id asc limit 200`,
}),
});
if (res.status === 204) break; // 該当なし
const json = await res.json();
const rows = json.data || [];
if (rows.length === 0) break;
records.push(...rows);
lastId = rows[rows.length - 1].id;
if (!json.info?.more_records) break;
}
return records;
}
// 100件ずつ更新する。trigger を空にしてワークフローを動かさない
async function updateBatch(apiDomain, token, batch) {
const res = await fetch(`${apiDomain}/crm/v8/Sales_History`, {
method: 'PUT',
headers: { Authorization: `Zoho-oauthtoken ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ data: batch, trigger: [] }),
});
return res.json();
}
全件取得では、ページ番号で順にたどる方式ではなく、IDの昇順で「前回の最後のIDより大きいもの」を取る方式にしました。当社が確認した時点では、ページ送りでたどれる件数に上限があり、10万件を超えるモジュールでは途中から取得できなくなったためです。
事後確認には、次のようなCOQLを使いました。結果が0件なら補完漏れはありません。
select id from Sales_History where Account_Lookup is null limit 10
次に同じ作業をするときのチェックリスト
- 移行対象を判定する規則を決め、実データで当てはまる割合と例外を確かめたか
- 移行キーは、組み合わせではなく完全に一意な値にしたか
- 項目対応表を「既存・要作成・除外」に分けたか
- 「要作成」の項目を本番に作り終えたことを、移行の前に確かめたか(ここを独立した手順にする)
- 確信の持てない関連(ルックアップ)は空で入れる方針にしたか
- 移行元を消す前に、関連のIDまで含む全列のバックアップを取ったか
- 更新処理は、再実行しても結果が変わらない作りになっているか
- 移行後に「空のまま残っている項目」をCOQLで数えたか
移行の作業は、データを動かすことよりも「移行先の器が整っているか」を確かめる方が抜けやすいものです。手順書の上で項目作成を一つの手順として切り出し、実行前に確認する欄を設けておくことをおすすめします。
SalesforceなどほかのCRMから移ってくる場合の確認項目は、SalesforceからZoho CRMへ移行する前に確認すべきデータ項目 にまとめています。移行全体の進め方のご相談は、SalesforceからZoho CRMへの移行・乗り換え支援でお受けしています。
よくある質問
Zoho CRMで商談と販売実績を分けるべきなのはどんなときですか?
商談(これから受注を目指す案件)と、受注済みの明細(伝票の1行ごとの実績)が同じモジュールに混ざり、パイプラインやダッシュボードの数字が実態を表さなくなったときです。実績は別のカスタムモジュールに分けると、商談の一覧と集計が日々の営業に使える状態に戻ります。
移行キーには何を使えばよいですか?
移行元のレコードIDのように、完全に一意な値を使います。伝票番号や品番の組み合わせは一意に見えても、実データでは重複することがあります。移行先にIDを保存しておけば、再実行しても二重登録にならず、後から元データと突き合わせることもできます。
移行後に項目が足りないと分かった場合、やり直しが必要ですか?
移行元の全列バックアップが残っていれば、やり直さずに補完できます。移行先に項目を追加し、移行元と移行先を名前やIDで突き合わせて、足りない値だけを更新します。バックアップを取らずに移行元を消すと、この方法が使えなくなります。
大量のレコードをAPIで更新するとき、ワークフローが動いてしまいませんか?
Zoho CRMのレコード更新APIは、リクエストに trigger を空の配列で渡すと、ワークフローなどの自動処理を動かさずに更新できます(当社が確認した時点の仕様です)。大量更新では、自動処理の想定外の発火を防ぐために指定しておくと安全です。




