Delugeの contains() とは、文字列の中に指定した文字列が含まれているかどうかを true/false で返す関数です。手軽に使えますが、税率のように「数字の並び」を判定するときに使うと、思わぬ一致を拾います。
当社は、Webメディアを運営する中小企業のZoho CRMで、商談の明細(サブフォーム)から見積書を自動作成するボタン関数を改修しました。その改修と受け入れテストの中で、2つの不具合が見つかりました。
- 消費税10%の商品が、非課税として扱われる(改修作業中にコードを読んで発見)
- 商談に3行の明細を入れても、見積書には1行しか載らない(受け入れテストで発見)
2つは時期も見つかり方も別の、独立した不具合です。どちらもDelugeの書き方に関わるもので、仕様を知っていれば防げるものでした。この記事では、原因と直し方を、一般化したコードでまとめます。見積書を自動作成する関数そのものの作り方は「知識ゼロからDeluge関数で見積書データの自動生成を設定してみた」、Delugeの基本は「はじめてのDeluge」で紹介しています。
前提:見積書を自動作成する関数の流れ
改修前の関数は、おおよそ次の流れでした。
- 商談を取得し、明細のサブフォームを読む
- 明細の1行ごとに、商品を取得して税の設定を読む
- 税の設定(文字列)から税率を抜き出し、課税か非課税かを決める
- 見積書の明細を組み立てる
- 全行がそろったら見積書を作成する
当社が扱った環境のコードでは、商品の税の設定を「税名 - 10.0 %」の形の文字列として読んでいました。1つ目の問題は手順3にありました。
落とし穴1:なぜcontains()の税率判定で10%が非課税になるのか
改修前のコードは、次のような判定をしていました(項目名は一般化しています)。
taxValue = productTax.get("value");
if(taxValue != null && taxValue.contains("0.0 %"))
{
isTaxExempt = true;
}
「0%の税なら非課税」というつもりの判定です。ところが contains() は部分一致なので、次のようになります。
| 文字列 | contains(“0.0 %”) | 意図 |
|---|---|---|
| 非課税 - 0.0 % | true | 非課税(正しい) |
| 消費税 - 10.0 % | true | 課税(誤って非課税になる) |
| 軽減税率 - 8.0 % | false | 課税(正しい) |
「10.0 %」の後ろ5文字が「0.0 %」なので、一致してしまいます。8%は一致しないため、テストで8%の商品だけを試していると気づけません。
endsWith()に変えれば直る?
最初の対策として、contains() を endsWith()(末尾が一致するかを返す関数)に変える案が出ました。しかし、"10.0 %".endsWith("0.0 %") も true です。末尾の5文字が同じだからです。
前にスペースを付けて endsWith(" 0.0 %") とすれば、この例では区別できます。ただし、税名の付け方や表記(「10%」「10.0%」など)が変わった時点でまた壊れます。税率を文字列の見た目で判定すること自体をやめるのが根本的な対策です。
落とし穴2:for eachの中のreturnは何を終わらせるのか

2つ目の不具合は、税率を抜き出せなかったときの処理でした。
for each line in dealLines
{
// …税率の抜き出し…
if(taxRate == null)
{
info "税率を取得できませんでした";
return "商品の税設定を確認してください。";
}
// …見積書の明細を組み立てる…
}
Delugeの return は、ループではなく関数そのものを終わらせます。ある行で税率が取れなかった時点で、残りの行は処理されません。
当社の事例では、3行入れた商談から見積書を作ると1行しか載らない症状が出ました。原因の箇所として、税率が取れない行で return してしまう処理を特定し、continue に直したあと3行とも載ることを確かめました。なお、return の時点で関数が終わるなら見積書自体が作られないはずで、1行だけ載った正確な経路までは当社でも確定できていません。
Delugeには、その行だけを飛ばして次の行へ進む continue と、ループだけを抜ける break があります(Zoho公式のDelugeヘルプ「continue」に記載)。行ごとの問題には continue を使います。
| 書き方 | 終わる範囲 | 向いている場面 |
|---|---|---|
| return | 関数全体 | 商談が見つからないなど、処理全体の前提が崩れたとき |
| break | ループだけ(後続の処理は続く) | 必要な行が見つかったので、残りを見なくてよいとき |
| continue | その行だけ(次の行へ進む) | 1行だけ問題があり、ほかの行は処理したいとき |
直し方
1. 税率は文字列から抜き出さず、フラグで分岐する
今回の改修では、商談の明細に「課税対象」のチェックボックス(true/false)を持たせ、それで課税・非課税を分ける形にしました。税率は文字列から抜き出さず、会社として使う税率(10%)を関数の先頭で1か所に定義します。
税率が複数ある場合(8%と10%など)は、文字列の見た目で比べるのではなく、数値に変換してから比較します。次のコードは「数値に変換して比較する」考え方を示す例で、記事用に書いたものです。当社では実行確認をしていないため、使う前にご利用の環境で、税の設定の値がどの形で読めるか、区切り文字がない値でどう動くかを確かめてください。
rateText = taxValue.getSuffix(" - ").replaceAll("[^0-9.]","");
if(rateText == "")
{
taxRate = null;
}
else
{
taxRate = rateText.toDecimal();
}
if(taxRate != null && taxRate == 0)
{
isTaxExempt = true;
}
数値にしてしまえば、10と0、8と0を取り違えることはありません。Delugeの文字列関数で思わぬ結果になる別の例は「Zoho Delugeで改行分割できない時の対処法」でも扱っています。
2. 行ごとの問題は continue で飛ばし、件数を返す
商品が選ばれていない、税の設定がないといった行ごとの問題は、continue で飛ばし、飛ばした件数を数えます。業務と相談したうえで、「税設定のない行は飛ばし、ほかの行で見積書を作る」「飛ばした件数は画面とログに出す」を仕様として決めました。
3. 重複している処理は1つの関数にまとめる
同じ関数群を見直す中で、請求番号の重複チェックが2か所に約120行ずつ書かれていることも分かりました。片方だけ直して、もう片方が古いまま残る、という不具合の温床です。専用の関数にまとめ、呼び出す形に変えました。税の判定のように複数の関数で使う処理も、同じ考え方で1か所にまとめておくと、直し漏れを防げます。
改修後のコード例
商談の明細から見積書を作る関数を、一般化して載せます。明細のサブフォームは Line_Items、課税フラグは Is_Taxable としています。記事用に書き直した例のため、項目名をご利用の環境に合わせ、テスト環境で動作を確かめてから使ってください。
string createQuoteFromDeal(String dealId)
{
TAX_NAME = "消費税";
TAX_RATE = 10; // 会社ごとの税率に置き換える
deal = zoho.crm.getRecordById("Deals",dealId.toLong());
if(deal.get("id") == null)
{
return "商談が見つかりません。";
}
dealLines = deal.get("Line_Items");
if(dealLines == null || dealLines.size() == 0)
{
return "商談に明細がありません。";
}
quoteItems = List();
skippedCount = 0;
lineNo = 0;
for each line in dealLines
{
lineNo = lineNo + 1;
product = line.get("Product");
if(product == null)
{
info "明細" + lineNo + "行目: 商品が未選択のため飛ばします";
skippedCount = skippedCount + 1;
continue;
}
quantity = ifnull(line.get("Quantity"),0);
unitPrice = ifnull(line.get("Unit_Price"),0);
item = Map();
item.put("Product_Name",{"id":product.get("id")});
item.put("Quantity",quantity);
item.put("List_Price",unitPrice);
if(line.get("Is_Taxable") == true)
{
taxList = List();
taxEntry = Map();
taxEntry.put("name",TAX_NAME);
taxEntry.put("percentage",TAX_RATE);
taxList.add(taxEntry);
item.put("Line_Tax",taxList);
}
quoteItems.add(item);
}
if(quoteItems.size() == 0)
{
return "見積書に載せられる明細がありませんでした(飛ばした行: " + skippedCount + ")。";
}
quote = Map();
quote.put("Subject",deal.get("Deal_Name"));
quote.put("Deal_Name",{"id":dealId});
quote.put("Quoted_Items",quoteItems);
created = zoho.crm.createRecord("Quotes",quote);
if(created.get("id") == null)
{
info created;
return "見積書の作成に失敗しました。";
}
message = "見積書を作成しました(明細" + quoteItems.size() + "行)。";
if(skippedCount > 0)
{
message = message + "商品が未選択の行を" + skippedCount + "行飛ばしました。";
}
return message;
}
ポイントは3つです。
- return は「商談がない」「明細がない」「作成に失敗した」という、処理全体の前提が崩れたときだけに使う
- 行ごとの問題は continue で飛ばし、行番号つきでログに残す
- 最後に「何行載せて、何行飛ばしたか」を利用者に返す。飛ばした行があることに、ボタンを押した人がその場で気づける
なお、明細に設定する税名は、組織の税設定と合わせてください。当社の事例では、商品に設定されていない税名を指定すると「Given tax is not present in the corresponding product」というエラーになり、見積書を作れませんでした。
テストで押さえる4パターン
今回の2つの不具合は、どちらも「1行だけ」「8%だけ」のテストでは見つかりません。次の4パターンをテストケースに入れておくと、同じ種類の不具合を早く見つけられます。
- 明細1行(課税)
- 明細3行(すべて課税)→ 3行とも見積書に載るか
- 課税と非課税の混在 → 10%の行が非課税にならないか
- 商品未選択・税設定なしの行を含む → その行だけ飛ばされ、残りで見積書ができ、件数が表示されるか
当社の事例でも、改修後に「3行入れて3行載る」ことを確かめてから、次の機能追加に進みました。異常系のテスト項目の洗い出し方は「Zoho CRMのウィジェットで作る受注書入力画面のテスト項目」も参考になります。
まとめ:Delugeで明細を処理するときの確認リスト
- 数値の判定に contains()・endsWith() などの文字列比較を使っていないか
- 税率・金額は数値に変換してから比較しているか、またはフラグで分岐しているか
- ループの中に return がないか。あるなら、本当に関数全体を止めるべき場面か
- 飛ばした行の件数と行番号を、ログと画面の両方に出しているか
- 同じ処理が複数の関数にコピーされていないか
- 1行・複数行・混在・異常行の4パターンでテストしたか
Delugeは短いコードで多くのことができる分、文字列と数値の扱いや、ループの抜け方の違いが不具合に直結します。「動いた1件」ではなく「壊れ方」を先に想定してテストを組むのが、遠回りに見えて近道です。
Delugeの関数の不具合調査・改修は、Zoho CRMカスタマイズ・設定代行サービスで承っています。
よくある質問
Delugeの contains() と endsWith() の違いは何ですか?
contains() は文字列のどこかに指定した文字列があれば true、endsWith() は末尾が一致すれば true を返します。どちらも大文字と小文字を区別する文字列の比較です。"10.0 %" は末尾が "0.0 %" でもあるため、どちらを使っても10%と0%を区別できません。
Delugeのfor eachループを途中で止めたいときは、return・break・continueのどれを使えばよいですか?
関数全体をやめるなら return、ループだけを抜けて後続の処理(見積書の作成など)に進むなら break、その行だけ飛ばして次の行へ進むなら continue です。明細の1行に問題があるだけなら、多くの場合は continue が適切です。
税率が設定されていない商品は、エラーにして止めるべきですか?
業務の決め方次第です。今回の事例では、関係者と相談して「その行は飛ばし、ほかの行で見積書を作り、飛ばした件数をログと画面に出す」ことを仕様にしました。全体を止めたい場合も、ループの中で return せず、全行を確かめてから最後に判断すると、どの行が問題かをまとめて伝えられます。
見積書の明細に税を設定するとき、税名は自由に付けられますか?
当社の事例では、商品に設定されていない税名を明細に指定したところ「Given tax is not present in the corresponding product」というエラーで作成できませんでした。税名は組織の税設定・商品の設定と合わせてください。




