Zoho CRMで商談の利益率を自動算出する仕組みを作る(1商談に複数の仕入がある場合)

2026年08月03日

やりたかったこと

商談モジュールに対して、別モジュールで管理している仕入データが複数紐づいている構成です。商談の金額から仕入の合計を差し引いて粗利を出し、さらに利益率まで商談レコード上に表示させたい、という要件でした。

営業担当が商談を開いたときに「この案件はいくら残るのか」が一目で分かる状態を作るのが目的です。

なぜ数式フィールドでは実現できないのか

Zoho CRMには数式フィールドがあり、同一レコード内の項目同士であれば四則演算が可能です。商談レコードの中に仕入金額のフィールドを持たせているだけの構成であれば、数式フィールドひとつで完結します。関数もワークフローも不要です。

しかし今回は1つの商談に対して仕入レコードが複数ぶら下がる構成でした。数式フィールドは他モジュールのフィールドを参照できないため、この時点で数式だけでの実現は不可能になります。

ロールアップサマリーフィールドで合計を集計するという選択肢もありますが、数式フィールドがロールアップサマリーフィールドを参照できるかどうかは環境やバージョンによって挙動が異なります。確実性を優先するのであれば、合計から利益率の算出まで含めてDeluge関数で完結させるのが安全です。

全体の構成

構成としてはシンプルです。

商談モジュールに、仕入金額合計・粗利・利益率の3つのフィールドを追加します。仕入金額合計と粗利は通貨型、利益率はパーセント型もしくは小数点付きの数値型にしておきます。

そのうえで、仕入側と商談側それぞれにワークフローを設定し、Deluge関数を呼び出して3つのフィールドを書き換えます。

なお、この3つのフィールドは読み取り専用にしておくことを推奨します。関数が自動で書き込む項目なので、手入力できる状態のままだと誤って上書きされる事故が起こります。

事前準備としてAPI名を控える

コードを書き始める前に、使用するフィールドのAPI名を全て控えておきます。

設定からモジュールとフィールドを開き、対象モジュールのフィールド一覧から確認できます。もしくは設定内の開発者向け情報にあるAPIとSDKの画面からモジュール単位で一覧表示できるので、こちらの方が効率的です。

注意点として、カスタムモジュールのAPI名はURLに表示されている文字列と一致するとは限りません。必ず設定画面側で確認してください。ここを推測で書くと、エラーにならないまま何も取得できないという厄介な状態になります。

今回控えたのは以下です。実際のAPI名は環境によって異なるため、ここでは役割のみ記載します。

仕入モジュール側
・仕入金額のAPI名
・商談ルックアップのAPI名

商談モジュール側
・金額のAPI名(標準項目)
・仕入金額合計のAPI名
・粗利のAPI名
・利益率のAPI名

関数を2本作る

仕入側から呼ぶものと、商談側から呼ぶものの2本を用意します。

処理内容はほぼ同じですが、入口が違います。仕入側のワークフローから渡されるのは仕入レコードのIDなので、そこから商談IDを引き直す一手間が必要になります。商談側のワークフローからは商談IDが直接渡ってくるため、その工程が不要です。

Delugeは別のカスタム関数を名前で呼び出すことができない仕様のため、共通処理を切り出して使い回すことができません。多少冗長になりますが、同じロジックを2本に書く形になります。

以下は仕入側から呼ぶ関数です。カテゴリーは自動化、引数は仕入レコードIDを受け取る文字列型ひとつです。

void automation.関数名(String txnId)
{
txn = zoho.crm.getRecordById("仕入モジュールAPI名",txnId.toLong());
lk = txn.get("商談ルックアップAPI名");
if(lk != null)
{
dealId = lk.get("id").toString();
total = 0.0;
for each pageNum in {1,2,3,4,5}
{
resp = zoho.crm.searchRecords("仕入モジュールAPI名","(商談ルックアップAPI名:equals:" + dealId + ")",pageNum,200);
if(resp == null || resp.size() == 0)
{
break;
}
for each rec in resp
{
amt = rec.get("仕入金額API名");
if(amt != null)
{
total = total + amt.toDecimal();
}
}
if(resp.size() < 200)
{
break;
}
}
deal = zoho.crm.getRecordById("Deals",dealId.toLong());
salesAmt = 0.0;
if(deal.get("Amount") != null)
{
salesAmt = deal.get("Amount").toDecimal();
}
gross = salesAmt - total;
rate = 0.0;
if(salesAmt != 0)
{
rate = (gross / salesAmt) * 100;
rate = rate.round(1);
}
upd = Map();
upd.put("仕入金額合計API名",total);
upd.put("粗利API名",gross);
upd.put("利益率API名",rate);
res = zoho.crm.updateRecord("Deals",dealId.toLong(),upd);
info res;
}
}

商談側から呼ぶ関数は、冒頭の商談ID取得部分を丸ごと削り、引数で受け取った商談IDをそのまま使う形にします。それ以外は同一です。

ルックアップフィールドを条件にした検索は、searchRecordsに対して丸括弧でくくった条件式を渡すことで引けます。ルックアップの値はマップとして返ってくるので、そこからidを取り出す点に注意してください。

ゼロ除算の回避は必須です。金額が未入力の商談でこの関数が動くと、そのまま割り算に入って落ちます。

ワークフローの設定と無限ループの回避

仕入側のワークフローは、作成時と編集時の両方をトリガーにし、繰り返し実行を有効にします。ここのチェックを入れ忘れると、2回目以降の編集で関数が動かなくなります。条件は商談ルックアップが空でないこととし、引数にはレコードIDを渡します。

問題は商談側です。

商談側のワークフローを「編集時・全項目・繰り返し実行あり」で作ってしまうと、関数が商談レコードを更新した時点でそのワークフローが再度発火し、無限ループに陥る恐れがあります。

これを避けるため、商談側のトリガーは「特定の項目が変更されたとき」に限定し、対象を金額フィールドのみに絞ります。関数が書き込むのは合計・粗利・利益率の3項目だけなので、金額を監視対象にしておけば自己再発火は起きません。

項目更新をトリガーにする場合、適用条件を追加で設定する必要はありません。条件の指定画面では「すべての商談」を選んで問題ありません。

既存データを一括で反映させる

ここまでの設定だけでは、これから編集される商談にしか値が入りません。既存の全レコードに反映させるには、別途一括再計算の関数が必要です。

この関数はカテゴリーをスケジュールで作成し、引数はなしとします。処理の流れは3段階です。

まず仕入モジュールを全件読み込み、商談IDをキーにした集計マップを作ります。次に商談モジュールを全件読み込み、IDと金額だけをリストに退避します。最後にそのリストを回して計算し、まとめて更新をかけます。

ここで重要なのが、商談の読み込みと更新を明確に分ける点です。

getRecordsは既定で更新日時順にレコードを返します。読みながら順次更新していくと、更新したレコードが並び順の先頭に移動してしまい、ページ送りの過程で未処理のレコードを飛ばしたり、同じレコードを二度処理したりします。全件を先に読み切ってから更新フェーズに入る構成にすることで、この問題を回避できます。

更新にはbulkUpdateを使います。1回の呼び出しで最大100件まで処理でき、消費されるAPIコールも1回で済みます。1件ずつupdateRecordを回すのに比べて、API消費量が大幅に抑えられます。

以下は更新部分の骨格です。

updList = List();
for each  dd in dealData
{
    u = Map();
    u.put("id",dd.get("id"));
    u.put("仕入金額合計API名",total);
    u.put("粗利API名",gross);
    u.put("利益率API名",rate);
    updList.add(u);
    if(updList.size() == 100)
    {
        res = zoho.crm.bulkUpdate("Deals",updList);
        info res;
        updList = List();
    }
}
if(updList.size() > 0)
{
    res = zoho.crm.bulkUpdate("Deals",updList);
    info res;
}

ループの外に最後の処理を置いている点に注意してください。これがないと、100件で割り切れなかった端数が更新されずに残ります。

なお、この一括再計算はレコード削除への対策も兼ねています。Zoho CRMのワークフローはレコード削除時にトリガーできないため、仕入レコードを消しても商談側の合計は古いままになります。この関数を夜間スケジュールで回しておけば、翌日には整合が取れる状態になります。

ページングの上限に注意

Delugeはwhile文による無限ループに対応していないため、ページ送りはリストリテラルを使ったforループで代替します。

for each pageNum in {1,2,3,4,5}

この書き方の場合、上記であれば5ページ分、つまり最大1000件までしか処理できません。レコード件数がそれを超える場合は、リスト内の数字を実際の件数に合わせて増やす必要があります。

件数が数万規模になると関数の実行時間制限に抵触する可能性が出てきます。その規模になった時点で、Bulk Read APIを使った別方式への切り替えを検討することになります。

ハマったところ

今回一番時間を取られたのが、関数の保存時に出る2つのエラーです。

ひとつ目は、VOID型の関数は値を返せないというエラーです。自動化カテゴリーの関数は戻り値がVOIDになるため、コード内にreturn文を書くと保存できません。処理を途中で抜けたい場合は、早期リターンを使わず条件分岐のブロックで全体を囲む書き方に変更する必要があります。

ふたつ目が、コードのフォーマットが不正であるというエラーです。エラーメッセージには正しい形式として、戻り値の型・カテゴリー・関数名・引数リスト・波括弧で囲んだコード、という構造が提示されます。

これは関数エディタが1行目に関数のシグネチャを要求している状態です。処理本体だけを貼り付けると保存できません。1行目に宣言を書き、コード全体を波括弧で囲む必要があります。

void automation.関数名(String 引数名)
{
処理
}

エディタによっては新規作成時にこの宣言行が自動で挿入されますが、既存の関数を編集する際にコード全体を貼り替えると宣言ごと消えてしまい、このエラーに遭遇します。

切り分けの方法としては、コードを1行だけの最小構成にして保存を試すのが確実です。それが通れば関数の定義側は正常であり、原因はコード本体にあると判断できます。逆に1行でも保存できない場合は、関数名や引数の設定を疑うことになります。

なお関数名にはスペースや特殊文字を含められません。表示名は日本語で構いませんが、関数名そのものは半角英数とアンダースコアで構成してください。宣言行に関数名がそのまま埋め込まれる仕様のため、日本語名だとフォーマット検証で弾かれる可能性があります。

まとめ

1商談に複数の関連レコードがぶら下がる構成で集計値を出したい場合、Deluge関数とワークフローの組み合わせが現実的な解になります。

設計上のポイントは3つです。

自己更新による無限ループを避けるため、商談側のトリガーは特定項目の変更に限定すること。全件処理では読み込みと更新のフェーズを分離すること。そしてワークフローが削除に追従できない以上、スケジュールによる定期再計算を必ず併用すること。

この3点さえ押さえておけば、あとは素直に組める内容です。

Zoho CRMで商談の利益率を自動算出する仕組みを作る(1商談に複数の仕入がある場合) | Zoho CRMを活用した中小企業へのDX支援は【And So株式会社】。全ての企業にDXを!