DX

Zoho CRMの受注書から勘定奉行クラウドへ売上仕訳を自動連携

受注が決まるたびの「会計システムへの転記」を、Zoho CRM 標準機能だけで無くします。二重計上を止める設計と実測値つきの構築ガイドです。

Zoho CRMの受注書から勘定奉行クラウドへ売上仕訳を自動連携

受注が決まるたびの「会計システムへの転記」を、Zoho CRM 標準機能だけで無くします。二重計上を止める設計と実測値つきの構築ガイドです。

※ Zoho および Zoho CRM は Zoho Corporation の商標または登録商標です。「奉行」「勘定奉行クラウド」は株式会社オービックビジネスコンサルタントの登録商標です。その他記載の会社名・製品名は各社の商標または登録商標です。

こんな悩み、ありませんか

  • 受注が決まるたびに、顧客管理ツール(CRM)の画面を見ながら会計システムに売上仕訳(取引を借方・貸方に振り分けた記録)を手で入力している
  • 「あの案件、もう仕訳切った?」という確認が、締めのたびに社内チャットを流れていく
  • 入力したつもりが抜けていて、月次を締める直前に慌てて追加している
  • 逆に二重に入れてしまい、売上が膨らんでいることに気づくのが翌月になる
  • CSV で受け渡そうとしたが、結局毎回誰かが手で加工することになり、手間が減らなかった

受注の情報は CRM にあり、仕訳は会計システムに要る。この間にある「転記」という作業は、誰がやっても新しい価値を生まないのに、間違えると決算に響きます。しかも転記は「やり忘れ」と「やりすぎ(二重計上)」の両方が起きるので、どちらも潰せる仕組みでないと安心できません。

本記事のコンセプト

Zoho CRM の受注書に「奉行に仕訳連携」ボタンを1つ足し、押すと勘定奉行クラウドに売上仕訳が立ち、採番された伝票Noが受注書に書き戻るところまでを作ります。連携用のサーバーも外部の連携サービスも使わず、Zoho CRM に最初から入っている機能だけ(外部サービスの認証を預かる Connection、Zoho のスクリプト言語 Deluge、画面に足せるカスタムボタン)で完結させます。

  • Zoho CRM の Connection・Deluge 関数・カスタムボタンだけで、受注書から勘定奉行クラウドへ売上仕訳を立てる方法
  • 勘定科目・取引先・消費税区分を「コードに書かず」奉行のマスタから引く構成の作り方
  • 二重計上を止める2層の設計と、重複確認そのものが失敗したときの振る舞い(=計上しない)
  • 課税10%・軽減8%・非課税・不課税を取り違えずに渡す方法と、やってはいけない金額の渡し方
  • ボタン実行の10秒上限に収めるための、参照の往復の削り方と逃げ道(ワークフロー/スケジュール)

設定の手順は記事の後半にまとめています。仕組みと設計判断だけ知りたい方は「二重計上をどう止めるか」まで読めば足ります。

作ったもの、そして何が変わるか

ボタン1つで何が起きるか

受注書の画面上部に「奉行に仕訳連携」ボタンを1つ置きます。押すと数秒で勘定奉行クラウドに売上仕訳が立ち、奉行が採番した伝票Noがその場で返ります。

Zoho CRM の受注書でボタンを押した直後の結果ダイアログ。伝票No・計上日・税抜金額・消費税額・税込金額が返っている
ボタンを押した直後。計上日・税抜金額(net)・消費税額(tax)・税込金額(total)と、奉行が採番した伝票No が返る
これまでこの仕組みを入れた後
仕訳の起票CRM を見ながら会計システムに手で入力受注書のボタンを1回押す(実測 約6秒)
立て忘れの確認会計システムと CRM を突き合わせる受注書の「奉行連携ステータス」を見る。未連携だけを集めたビューを作れば締め前に一覧できる
二重計上気づくのが翌月になることがある同じ受注を順に押す経路・書き戻しに失敗した後にやり直す経路は2つの関所で止まる(同時クリックは別途ロックが必要)
失敗したとき誰も気づかない/口頭で伝わる受注書に理由が日本語で残る
計上した金額の記録会計システム側にしかない受注書にも控えが残る

金額の渡し方だけは間違えられない

効果は手数だけではありません。転記を人がやっている限り、金額の読み違いは必ず混ざります。仕組みに落とすときも、金額の扱いを間違えれば同じことが起きます。

「税込金額から割り戻す」モードで渡してはいけない

奉行に金額を渡すとき、「税込金額から割り戻す」モードを使うと、消費税のない受注にも税が作られます。770,000 円の受注なら、売上 700,000 円+消費税 70,000 円として記録されます。売上が 70,000 円過少、仮受消費税が 70,000 円過大です。画面上は正常に動いて見えるので、奉行側を照会 API で読み直すまで気づけません。税抜金額と消費税額を明示して渡すのが正解です。

税抜金額と消費税額を明示して立てた仕訳は次のとおりです。すべて勘定奉行クラウドの照会 API で読み直して確認しています。

受注書税区分奉行に立った仕訳消費税区分伝票No所要
海外法人向け役務提供 180,000 円不課税売上売掛金 180,000 / 売上高 180,000(消費税 0・税計算しない)0005 不課税売上000040—
コーポレートサイト制作一式 528,000 円課税売上 10%売掛金 528,000 / 売上高 480,000 + 消費税 48,000(税率種別 標準)0060 課売上0000416.6 秒
定期刊行物の制作・納品 237,600 円課税売上 軽減8%売掛金 237,600 / 売上高 220,000 + 消費税 17,600(税率種別 軽減)0060 課売上0000426.2 秒
土地の賃貸借に係る手数料 90,000 円非課税売上売掛金 90,000 / 売上高 90,000(消費税 0・税計算しない)0080 非課税売上0000437.2 秒

前提と対象読者

項目内容
対象読者Zoho CRM を使っていて、会計は勘定奉行クラウドという会社の情報システム担当・経理
前提知識Zoho CRM の管理者設定が触れること。コードは JavaScript が読める程度で十分
構築方法Zoho CRM の Connection + Deluge 関数 + カスタムボタン(連携用サーバーや外部の連携サービスは使わない)
必要なエディションZoho CRM Enterprise 以上(カスタム関数がエディション要件のため)
奉行側の前提勘定奉行クラウドで API 連携の利用申込が必要。条件・費用はオービックビジネスコンサルタントに確認してください
対応する税区分課税(標準10%/軽減8%)・非課税・不課税の4区分を実機で確認。ただし1枚の受注に複数税率が混ざる場合は対象外
所要時間半日程度(本記事の手順どおりに構築する場合の目安)
検証環境Zoho CRM 日本データセンター/勘定奉行クラウド 自社サンドボックス

まず、CRM をどう構成するか

勘定科目コードをプログラムに書かない

まず、勘定科目コードをプログラムに書かない理由です。ここを外すと、あとで必ず作り直しになります。

売掛金 = "01350"、売上高 = "05000" と定数で持てば動きます。でも、科目が増えたとき、商品ごとに売上科目を分けたくなったとき、部門別に変えたくなったとき——そのたびにエンジニアがコードを直して再デプロイすることになります。経理の都合で開発者が呼ばれ続ける構造です。

会計システム連携では、奉行のマスタを CRM 側に同期して、レコードから参照するのが定石です。勘定科目・補助科目・取引先・部門・担当者・摘要といったマスタを同期し、受注レコードがそれぞれに参照関係を持つ形にします。

勘定奉行クラウドのマスタを Zoho CRM に同期し、商品・取引先・受注書から参照する構成を示したデータ構成図
勘定奉行クラウドのマスタが正。CRM はそれを参照するだけで、科目コードはプログラムに1つも書かない

奉行のマスタを CRM に同期する

Zoho CRM 側に用意するものは5つです。

置き場所作るもの何が決まるか
カスタムモジュール「奉行 勘定科目」科目コード(一意)/科目名/科目名カナ科目の一覧そのもの。奉行から同期する
商品(標準モジュール)奉行 売上科目(参照)貸方の売上科目。商品ごとに変えられる
取引先(標準モジュール)奉行 債権科目(参照)/得意先コード/補助科目コード借方の債権科目と、伝票に載せる取引先
受注書(標準モジュール)奉行 税区分(選択リスト)課税 10%/課税 軽減8%/非課税/不課税
カスタムモジュール「奉行 消費税区分」区分コード(一意)/区分名奉行に送る消費税区分。商品に既定を持たせ、受注書で上書きする

マスタの同期は、奉行の照会 API を1回叩いて CRM に流し込むだけです。同じ手順で消費税区分マスタも同期します(検証環境では勘定科目304件、消費税区分43件。0060 課税売上/0080 非課税売上/0005 不課税売上/0000 対象外 など)。

bashsync-account-master.sh
# 1. 勘定奉行クラウドから勘定科目マスタを取得(itemList は必須)
curl -X POST "$OBC_BASE/gl/ExportAccountItem" \
  -H "Authorization: Bearer $OBC_TOKEN" \
  -H "X-OBC-TenantID: $OBC_TENANT" \
  -H "X-OBC-SubscriptionKey: $OBC_SUBKEY" \
  -H "Content-Type: application/json" \
  -d '{"itemList":["<科目コードの項目記号>","<科目名の項目記号>","<科目名カナの項目記号>"]}' > accounts.json
# 項目記号は勘定奉行クラウドの API 仕様書に記載がある。itemList は省略できない
# => [{"<科目コード>":"05000","<科目名>":"技術料売上高","<カナ>":"ギジュツリョウウリ"}, ...] 304件

# 2. Zoho CRM のカスタムモジュールへ登録する(data 配列に最大100件ずつ詰めて分割 POST)
curl -X POST "https://www.zohoapis.jp/crm/v8/Bugyo_Account_Items" \
  -H "Authorization: Zoho-oauthtoken $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"data":[
        {"Name":"05000 技術料売上高","Account_Code":"05000",
         "Account_Name":"技術料売上高","Account_Kana":"ギジュツリョウウリ"}
      ]}'
勘定科目マスタの同期。定期実行にすれば科目の増減も追従できる

同期元は奉行の勘定科目マスタです。この検証環境には304件の科目が登録されています。

勘定奉行クラウドの勘定科目マスタ画面。登録件数304件で、01350 売掛金が選択されている
奉行側の勘定科目マスタ。この304件をそのまま CRM に取り込む
Zoho CRM のカスタムモジュール「奉行 勘定科目」の一覧画面。勘定奉行クラウドから同期した科目コードと科目名が並んでいる
同期した勘定科目。コードと名前がそのまま CRM のレコードになる

商品と取引先に科目を紐づける

あとは、商品と取引先に「どの科目か」を紐づけます。ここは経理が自分でできる作業です。 選択して保存するだけで、エンジニアは要りません。

Zoho CRM の商品詳細画面。奉行 売上科目に 05000 技術料売上高 が設定されている
商品に売上科目を紐づける。商品ごとに違う科目を当てられる
Zoho CRM の取引先詳細画面。奉行 債権科目に 01350 売掛金、奉行 得意先コードに T00040 が設定されている
取引先に債権科目と得意先コードを紐づける。補助科目コードもここに置く

科目が決まらないときは計上しない

Deluge 側はこうなります。科目コードの定数は1つもありません。

javascriptpostSalesOrderToBugyo.dg(抜粋1)
// 借方の債権科目は「受注書 → 取引先 → 奉行 債権科目」とたどる
acReceivable = "";
custCode     = "";
subAcctCode  = "";
acctRef = so.get("Account_Name");
if (acctRef != null && acctRef.get("id") != null)
{
    acct = zoho.crm.getRecordById("Accounts", acctRef.get("id").toLong());
    if (acct != null)
    {
        custCode    = ifnull(acct.get("Bugyo_Customer_Code"),   "").toString().trim();
        subAcctCode = ifnull(acct.get("Bugyo_Sub_Account_Code"),"").toString().trim();
        // マスタの Name は「<コード> <名称>」で同期する。
        // 表示名の先頭を取れば、参照先レコードを取りに行かずに済む
        raRef = acct.get("Bugyo_Receivable_Account");
        if (raRef != null)
        {
            acReceivable = ifnull(raRef.get("name"), "").toString().trim().getPrefix(" ").trim();
        }
    }
}
// マスタに無ければ組織変数の既定値を使う
if (acReceivable == "")
{
    acReceivable = ifnull(zoho.crm.getOrgVariable("Bugyo_Default_Receivable_Account"), "").toString().trim();
}

// 貸方の売上科目は「受注明細の商品 → 奉行 売上科目」とたどる(同じ形なので省略)

// 既定値も空なら計上しない。科目が分からないまま伝票を立てない
if (acReceivable == "" || acSales == "")
{
    emap = Map();
    emap.put("Bugyo_Sync_Status", "エラー");
    emap.put("Bugyo_Sync_Error",  "勘定科目が決まりません。取引先の債権科目/商品の売上科目/組織変数の既定値のいずれかを設定してください");
    zoho.crm.updateRecord("Sales_Orders", soId.toLong(), emap);
    return "{\"error\":\"ACCOUNT_NOT_RESOLVED\"}";
}
科目はマスタから引く。引けなければ計上しない

既定値は組織変数に逃がす

「全商品に科目を設定し終わるまで動かない」では運用が始まりません。組織変数に既定の債権科目・売上科目を置き、マスタに無ければそこから取ります。それも空なら計上しません。 既定値で通った件数を定期に見て、紐づけを埋めていく運用にします。

取引先コードは奉行のマスタと一致させる

マスタを揃えるのは CRM 側だけでは足りません。

取引先コードを伝票に載せるなら、奉行の取引先マスタと合わせること

伝票に取引先コードを載せる場合、そのコードが奉行の取引先マスタに存在しないと、受入は 借方取引先が存在しません。 で却下されます。CRM 側だけでコードを決めても意味がなく、奉行の取引先マスタと同じコードでなければ伝票が立ちません。取引先マスタも照会 API で同期し、CRM の取引先と対応づけておく必要があります。奉行側の取引先マスタが空の場合は、先に奉行へ取引先を登録してください(照会・登録とも API があります)。

この構成にすると、科目が増えても、商品ごとに売上科目を分けたくなっても、直すのはマスタと紐づけであってプログラムではありません。 部門や担当者を伝票に載せたくなったときも、同じやり方でマスタを1つ増やすだけです。

二重計上をどう止めるか

仕訳は「立て忘れ」より「二重に立つ」ほうが見つけにくく、厄介です。 立て忘れは後から気づいて足せますが、二重計上は気づくのが遅れますし、取り消すには赤伝が要ります。

2つの関所の役割分担

二重計上を止める2層の判定を示したフローチャート。第1の関所は Zoho CRM 側、第2の関所は勘定奉行クラウド側
二重計上を止める2つの関所。速い関所で日常を捌き、遅い関所で事実を確かめる

関所を2つ置いています。どちらか片方だけでは足りません。

第1の関所(Zoho CRM 側)第2の関所(勘定奉行クラウド側)
何を見るか受注書の「奉行連携ステータス」奉行の伝票の摘要にトレースキーがあるか
速さ1.4 秒。奉行の API は1回も呼ばない数秒。照会 API を1回呼ぶ
止められるもの同じ画面での連打書き戻しに失敗した後の再実行、別経路からの実行
止められないもの計上後に書き戻しが失敗したケース(ステータスが未連携のまま残る)照会の窓(計上日の前後31日)の外にある伝票
弱点人が手でステータスを書き換えられる毎回 API を1回払う。2人が同時に押すと両方とも抜けうる

第2の関所の鍵になるのが トレースキー です。ZOHO-SO-<受注書のレコードID> という文字列を作り、奉行に渡す明細行の摘要の先頭に必ず入れます。次に同じ受注でボタンが押されたとき、計上日の前後31日ぶんの伝票を照会して、この文字列を含む伝票があるかどうかを見ます。

鍵はレコードIDで作る

レコードID を使うのが肝です。 受注番号や件名を鍵にすると、人が後から書き換えられてしまいます。件名を鍵にした場合、件名が同じ別の受注を「計上済み」と誤判定し、未計上の売上が「連携済」として消えます。二重計上より見つけにくい事故です。

plain
traceKey = "ZOHO-SO-" + soId;      // soId は受注書のレコードID(人が書き換えられない)

奉行の明細行の摘要:
  [ZOHO-SO-47497000000618004] コーポレートサイト制作一式
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ここを次回の照会で探す
トレースキーは摘要の先頭に置く。摘要は200字で切られるため、後ろが切れても鍵が残るようにする

重複確認が失敗したら計上しない

重複確認そのものが失敗したときは、計上しません。

javascriptpostSalesOrderToBugyo.dg(抜粋2)
// 計上日の前後31日を照会する。計上日が後から変更されても拾えるようにするため
qBody = Map();
qBody.put("startDate", slipDate.toDate().subDay(31).toString("yyyy/MM/dd"));
qBody.put("endDate",   slipDate.toDate().addDay(31).toString("yyyy/MM/dd"));
qBody.put("itemList",  qItems);   // 照会したい項目のリスト。奉行の照会APIでは必須

exResp = invokeurl
[
    url        : baseUrl + "/gl/ExportJournalSlip"
    type       : POST
    headers    : hdrBase
    parameters : qBody.toString()   // ← Map を直渡しすると form-encoded になる。必ず文字列化する
    connection : "bugyo_conn"       // ← 鍵はここから自動で付く
    detailed   : true               // ← これが無いと responseCode も responseHeader も取れない
];

exCode = ifnull(exResp.get("responseCode"), 0);

// 照会そのものが失敗したら計上しない(fail-closed)
// 「二重計上していないか分からない」状態で起票するのが一番まずい
if (exCode != 200)
{
    emap = Map();
    emap.put("Bugyo_Sync_Status", "エラー");
    emap.put("Bugyo_Sync_Error",  "重複確認の照会が HTTP " + exCode + " のため計上を中止しました");
    uR = zoho.crm.updateRecord("Sales_Orders", soId.toLong(), emap);
    wb = "";
    if (uR.toString().contains("error")) { wb = ",\"writebackFailed\":true"; }
    return "{\"error\":\"DEDUPE_CHECK_FAILED\",\"httpCode\":" + exCode + wb + "}";
}
重複確認。失敗したら計上しない(fail-closed)

分からないときは起票しない。 この判断は、立て忘れと二重計上のコストが非対称だからこそ成り立ちます。ただし代償があります。奉行のアクセストークンは自動更新のない静的なトークンなので、失効した瞬間に全受注がこの経路で止まります。止まること自体は正しい挙動ですが、気づけるのは「ボタンを押した人が受注書のエラー欄を見たとき」だけです。エラー状態の受注を集めたビューを作り、締め前に確認する運用はセットで必要です。

updateRecord は、黙って失敗する

zoho.crm.updateRecord は 1項目でも不正だと更新全体が失敗しますが、例外を投げません。戻り値を文字列化して error を含むか見るまで気づけません(返り値の例: {'code':'INVALID_DATA','details':{'expected_data_type':'datetime',…}})。この取りこぼしが起きると、奉行には伝票があるのに受注書は未連携のままという最悪の不整合になり、第1の関所が効かなくなります。書き戻しは14箇所すべてで戻り値を検査し、失敗したら返り値に writebackFailed を立てます。

全体の流れ

Zoho CRM・Deluge 関数・勘定奉行クラウド API の3者間のやり取りを示したシーケンス図
ボタンを押してから受注書に書き戻るまでの流れ。呼ぶ API は3種類

ボタン1つに見えて、裏では勘定奉行クラウドの API を 3種類 呼んでいます。

  1. 照会 … 同じ受注の仕訳がもう立っていないかを確認する
  2. 受入 … 仕訳データを渡す。ここで返ってくるのは「受け付けた」という返事だけで、仕訳はまだ出来ていない
  3. 受入結果の確認 … 受け付けた処理が終わったかを確かめる(結果照会の注意点は後述)

最後にもう一度「照会」を呼び、採番された伝票Noを引き当てて受注書に書き戻します。

種類は3つですが、実際のリクエスト数は最大15回です(照会1 + 受入1 + 受入結果の確認が最大12 + 伝票No引き当て1)。これに加えて、受注書・取引先・商品を読むための CRM 内のレコード取得が3回入ります。この数が、後で出てくる実行時間の上限に効いてきます。

実装手順:Connection からカスタムボタンまで

ここからは実際に作る手順です。経理・決裁の観点で読まれている方は、「実装でつまずくところ」まで飛ばしていただいて構いません。

Step 1:認証を Connection に預け、残りを組織変数に置く

勘定奉行クラウドの API は、リクエストに3つのヘッダーを要求します。

ヘッダー役割置き場所理由
Authorizationアクセストークン(=事実上の鍵)Zoho CRM の Connectionコードにも変数にも残さないため
X-OBC-TenantIDどのテナント(会社)かZoho CRM の組織変数Connection に入れられるのは1つだけ
X-OBC-SubscriptionKeyどの契約かZoho CRM の組織変数同上

鍵にあたる Authorization は、Connection(Zoho CRM が外部サービスの認証情報を預かる仕組み)に入れます。関数のコードにも組織変数にも書かないためです。コードに書けば関数を開ける人全員に見えますし、Git に写せば履歴に残ります。Connection に入れておけば、コード側は「この接続を使う」と指定するだけで済みます。

設定は「設定 > 開発者向け情報 > 外部連携 > Custom Services」から行います。

Zoho CRM の Custom Service 作成画面。Authentication Type に API Key、Parameter Type に Header、Parameter Key に Authorization を設定している
Custom Service の作成画面。認証方式は API Key、渡し方は Header、キー名は Authorization
設定項目値
Service NameBugyo Cloud
Service Link Namebugyocloud
Authentication TypeAPI Key
Parameter TypeHeader
Parameter KeyAuthorization
Parameter Display NameAccess Token

Service と Connection は別物

ここで作るのは「サービスの定義」であって、トークンそのものはまだどこにも入っていません。トークンを入力するのは、この Service から Connection を作るときです。だから Service の定義を他人に見せても鍵は漏れません。なお勘定奉行クラウドのトークンは自動更新のない静的なトークンで、発行後の再表示もできません。失効時の再発行手順と保管ルールを先に決めておいてください。

Connection を作ると一覧に並び、接続済みのものは緑の印が付きます。ここで接続名(例では bugyo_conn)を控えておきます。コードから指定するのはこの名前です。

Zoho CRM の My Connections 画面。Bugyo Cloud の接続 bugyo_conn が接続済みとして緑の印付きで表示されている
Connection が接続済みになった状態。コードからはこの接続名を指定する

残りの2つと API のベースURLは、「設定 > 開発者向け情報 > 変数」で組織変数として作ります。コードからはこう読みます。

javascriptpostSalesOrderToBugyo.dg(抜粋3)
baseUrl  = ifnull(zoho.crm.getOrgVariable("Bugyo_Base_URL"),          "").toString().trim();
subKey   = ifnull(zoho.crm.getOrgVariable("Bugyo_Subscription_Key"), "").toString().trim();
tenantId = ifnull(zoho.crm.getOrgVariable("Bugyo_Tenant_ID"),        "").toString().trim();
if (baseUrl == "" || subKey == "" || tenantId == "")
{
    return "{\"error\":\"CONFIG_MISSING\"}";
}

// 全リクエスト共通のヘッダー。Authorization は Connection が付けるのでここには書かない
hdrBase = Map();
hdrBase.put("X-OBC-TenantID",        tenantId);
hdrBase.put("X-OBC-SubscriptionKey", subKey);
hdrBase.put("Content-Type",          "application/json");
設定値の読み込みと共通ヘッダー

確認方法:Connection の一覧で緑の印が付き、組織変数3本が入っていれば準備完了です。実際の疎通は Step 3 で確かめます。

Step 2:受注書に項目を足す

「まず、CRM をどう構成するか」で作ったマスタに加えて、受注書にも項目を足します。連携したという事実を、受注書そのものに残すためです。別のログに書くのではなく受注書に書くのは、営業も経理も普段見る画面がそこだからです。「この受注、仕訳は切れてる?」に画面を開くだけで答えられる状態を作ります。

項目名API 名型役割
奉行 税区分Bugyo_Tax_Type選択リスト(課税売上 10%/課税売上 軽減8%/非課税売上/不課税売上)消費税の扱いを決める
奉行連携ステータスBugyo_Sync_Status選択リスト(未連携/連携済/エラー/要確認)連打を止める第1の関所
奉行 仕訳伝票NoBugyo_Slip_No1行テキスト奉行が採番した番号
奉行連携 トレースキーBugyo_Trace_Key1行テキスト奉行側の摘要と突き合わせる鍵
奉行連携 エラー内容Bugyo_Sync_Error1行テキスト失敗したときの理由をそのまま残す
奉行 計上金額(控え)Bugyo_Posted_Amount通貨実際に計上した税込金額の控え
奉行 消費税区分Bugyo_Tax_Category参照(奉行 消費税区分)商品の既定を受注書で上書きする(任意)

選択リストの値は、コードの文字列と一字一句そろえる

第1の関所は Bugyo_Sync_Status の値を "連携済" という文字列と比較して判定します。選択肢を「連携済み」や「Posted」で作ると、関所が常に素通りして二重計上の第1防壁が消えます。同じことが標準項目の受注ステータスにも言えます。getRecordById が返すのは表示値なので、日本語UIの組織では Approved ではなく「承認済み」等になることがあります。実装前に1件 getRecordById して、自分の組織が返す値を必ず目で確認してください。

項目は画面からも作れますが、API で作っておくと検証環境と本番に同じものを確実に用意できます。つまずきやすい選択リストを例に挙げます。

bashadd-picklist-field.sh
# 第1の関所が見る選択リスト項目。選択肢の文字列がコード側と一致していること
curl -X POST "https://www.zohoapis.jp/crm/v8/settings/fields?module=Sales_Orders" \
  -H "Authorization: Zoho-oauthtoken $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [{
      "field_label": "奉行連携ステータス",
      "api_name":    "Bugyo_Sync_Status",
      "data_type":   "picklist",
      "pick_list_values": [
        {"display_value": "未連携", "actual_value": "未連携"},
        {"display_value": "連携済", "actual_value": "連携済"},
        {"display_value": "エラー", "actual_value": "エラー"},
        {"display_value": "要確認", "actual_value": "要確認"}
      ]
    }]
  }'
選択リスト項目の追加。api_name は必ず明示する

日本語ラベルだけで項目を作ると、API 名が勝手に決まる

api_name を省いて日本語ラベルだけで項目を作ると、API 名は field のような自動名で払い出されます。項目更新のリクエスト(PUT /crm/v8/settings/fields/{id} など)は INVALID_REQUEST_METHOD が返り、API 名は変更できません。作り直そうと削除して同じラベルで再作成すると、削除済み項目がごみ箱に残るため DUPLICATE_DATA になります。最初から api_name を明示してください。

もう1点、これらの項目には運用上の注意があります。

この項目は人が書き換えられる

Bugyo_Sync_Status を誰かが手で「連携済」にすれば計上漏れ、「未連携」に戻せば(照会の窓の外なら)二重計上になります。第1の関所を人の手から守るため、項目レベル権限で管理者以外は読み取り専用にしておくことをおすすめします。

Step 3:Deluge 関数を書く

Deluge(Zoho 独自のスクリプト言語)で単独関数を1本書きます。やることは「受注書を読む → 計上してよいか判断する → 仕訳を組む → 奉行に渡す → 結果を書き戻す」の5つです。

計上してよいかを判断する

まず、計上してよいかの判断から書きます。ここを通り過ぎてから奉行を呼ぶ順番が大事です。

javascriptpostSalesOrderToBugyo.dg(抜粋4)
so = zoho.crm.getRecordById("Sales_Orders", soId.toLong());
if (so == null || so.get("id") == null) { return "{\"error\":\"SO_NOT_FOUND\"}"; }

traceKey = "ZOHO-SO-" + soId;

// 関所1: すでに連携済みなら、奉行の API を1回も呼ばずに終わる
curStatus = ifnull(so.get("Bugyo_Sync_Status"), "").toString();
curSlipNo = ifnull(so.get("Bugyo_Slip_No"),     "").toString();
if (curStatus == "連携済")      // ← 選択肢の表示値と完全一致させること
{
    return "{\"skipped\":\"ALREADY_POSTED_LOCAL\",\"slipNo\":\"" + curSlipNo + "\"}";
}

// 関所2: 受注ステータスは白リスト。下書きやキャンセルは計上しない
// ※ getRecordById が返すのは「表示値」。自組織の値を確認してから書くこと
soStatus = ifnull(so.get("Status"), "").toString();
// ↓ 自組織の getRecordById が返す値に置き換えること(日本語組織では「承認済み」等)
if (soStatus != "Approved" && soStatus != "Delivered")
{
    wmap = Map();
    wmap.put("Bugyo_Sync_Status", "エラー");
    wmap.put("Bugyo_Sync_Error", "計上できるのは承認済または納品済の受注だけです。現在: " + soStatus);
    uR = zoho.crm.updateRecord("Sales_Orders", soId.toLong(), wmap);
    wb = "";
    if (uR.toString().contains("error")) { wb = ",\"writebackFailed\":true"; }   // ← 黙って落ちる
    return "{\"error\":\"SO_STATUS_NOT_ALLOWED\",\"status\":\"" + soStatus + "\"" + wb + "}";
}

// 関所3: 計上日は受注の「期限」を正とする。実行日から推測しない
dueDate  = so.get("Due_Date");
slipDate = "";
if (dueDate != null && dueDate.toString().length() >= 10)
{
    slipDate = dueDate.toString().subString(0, 10).replaceAll("-", "/");
}
if (slipDate == "")
{
    emap = Map();
    emap.put("Bugyo_Sync_Status", "エラー");
    emap.put("Bugyo_Sync_Error", "計上日に使う「期限」が未設定です。実行日から推測すると会計期間がずれるため計上しません");
    uR = zoho.crm.updateRecord("Sales_Orders", soId.toLong(), emap);
    wb = "";
    if (uR.toString().contains("error")) { wb = ",\"writebackFailed\":true"; }
    return "{\"error\":\"NO_SLIP_DATE\"" + wb + "}";
}
計上してよいかを判断する3つの関所

関所3を「空なら中止」にしているのには理由があります。期限が無いときに実行日で代用すると、zoho.currentdate が返すのは組織/ユーザーのタイムゾーン設定に従った日付だという問題に当たります。既定が米国のままの組織では、日本時間 20:19 が 04:19 として扱われます。この状態では 日本時間の 0:00 から 17:00 ごろまでに押した伝票が前日付で立ちます。月初の午前に押せば、前月=締め済み・申告済みの期間に入ってしまいます。

タイムゾーンの設定を直せば解消しますが、会計日付を「いつボタンを押したか」に依存させないほうが安全です。

「期限」を計上基準日として使っている点は、運用前に要確認

受注書の「期限(Due Date)」を計上日として使います。これはこの運用でそう決めたということであり、会計上の正解ではありません。売上の計上基準は原則として引渡基準(出荷日・検収日)です。受注書の「期限」は将来日付であることが多く、そのまま計上日にすると未来日付の伝票になります。期限が翌期にまたがる受注では期間帰属がずれます。本番では引渡基準日を持つ項目を別に用意し、それを計上日にするのが本筋です。

金額と消費税額を確定する

次に金額です。ここは会計的な影響がもっとも大きい箇所です。

javascriptpostSalesOrderToBugyo.dg(抜粋5)
// 受注書の Tax 項目は getRecordById から 0 にしか見えないため、総計から逆算する
subTotal   = ifnull(so.get("Sub_Total"),   0).toDecimal();
discount   = ifnull(so.get("Discount"),    0).toDecimal();
adjust     = ifnull(so.get("Adjustment"),  0).toDecimal();
grandTotal = ifnull(so.get("Grand_Total"), 0).toDecimal();

netRaw = subTotal - discount + adjust;   // 税抜の本体金額
taxRaw = grandTotal - netRaw;            // 消費税額(無ければ 0 になる)

// 丸めは先に総計と税で行い、本体は差で出す。
// こうすると net + tax = total が定義として必ず成立する
totalR = grandTotal.round(0);
taxR   = taxRaw.round(0);
netR   = totalR - taxR;
金額の確定。消費税は「総計 − 本体」で求める

丸めの順番をこうしているのは、奉行が貸借を「本体金額 + 消費税額」で判定するからです。本体を先に丸めてから税を差で出すと、1円ずれたときに貸借が合わなくなります。税抜モードで消費税額を空欄のまま送ると税0として扱われ、「貸借差額が発生しています。」で却下されます(次の表の3行目)。

税率マスターを設定していない組織では、API で渡した消費税額は残らない

税率マスターを設定していない組織では、API で Tax を渡して作った「小計 500,000/総計 550,000」の状態は CRM 側に保持されません。レコードが1回更新されると総計が再計算され、消費税額は失われます(550,000 → 500,000)。また Deluge の getRecordById からは最初から 0 にしか見えません。そのため消費税額は Grand_Total −(Sub_Total − Discount + Adjustment) で逆算しています。税率マスターを設定した組織では挙動が変わり得るため、導入時に自組織で必ず再確認してください。

奉行の消費税自動計算は、指定モードと渡す金額の組み合わせで次のように振る舞います。

消費税自動計算の指定渡す本体金額渡す消費税額結果
税込金額から計算する税込 110,000空欄税 10,000 / 税抜 100,000 に自動分解される
税抜金額からの計算税抜 100,00010,000 を明示そのまま登録される
税抜金額からの計算税抜 100,000空欄税0扱いになり「貸借差額が発生しています。」で却下される

つまり、空欄にしてシステムに計算させられるのは「税込金額から計算する」のときだけです。ここでは「税抜金額からの計算」+税額の明示を選びます。奉行に割り戻させない、という判断です。

摘要を組み立てる

次に摘要を組み立てます。

javascriptpostSalesOrderToBugyo.dg(抜粋6)
// 件名は人が自由に書くテキスト。JSON を壊す文字を潰してから使う
rawSubject = ifnull(so.get("Subject"), "Zoho CRM juchu").toString();
rawSubject = rawSubject.replaceAll("\\", "/");
rawSubject = rawSubject.replaceAll("\"", "'");
rawSubject = rawSubject.replaceAll("\n", " ");
rawSubject = rawSubject.replaceAll("\r", " ");

// トレースキーは必ず先頭に置く。摘要は200字で切られるので、
// 後ろが切れても重複検知の鍵だけは生き残るようにする
lineMemo = "[" + traceKey + "] " + rawSubject;
if (lineMemo.length() > 200)
{
    lineMemo = lineMemo.subString(0, 200);
}
摘要の組み立て。サニタイズと200字制限

この金額と摘要をもとに仕訳を組みます。消費税がある受注と無い受注で、組み方を変えます。

税区分から消費税区分を決める

税額の有無だけで判断すると、「本当に不課税の取引」と「税率の設定漏れ」が区別できません。そこで受注書の税区分を見ます。

javascriptpostSalesOrderToBugyo.dg(抜粋7)
// 税区分。税額0を勝手に「不課税」と決めつけない
taxType   = ifnull(so.get("Bugyo_Tax_Type"), "").toString().trim();
isTaxable = false;
rateKind  = RATEKIND_STD;                       // 0: 標準
if (taxType == "課税売上 10%")    { isTaxable = true; rateKind = RATEKIND_STD; }
if (taxType == "課税売上 軽減8%") { isTaxable = true; rateKind = RATEKIND_RED; }   // 1: 軽減

taxErr = "";
if (taxType == "")
{
    taxErr = "税区分が未設定です。課税/軽減/非課税/不課税のどれかを選んでください";
}
else if (isTaxable && taxR <= 0)
{
    taxErr = "税区分が課税売上なのに消費税額が0です。税率マスタの設定か金額を確認してください";
}
else if (!isTaxable && taxR > 0)
{
    taxErr = "税区分が " + taxType + " なのに消費税額が " + taxR + " あります";
}
if (taxErr != "")
{
    emap = Map();
    emap.put("Bugyo_Sync_Status", "エラー");
    emap.put("Bugyo_Sync_Error",  taxErr);
    uR = zoho.crm.updateRecord("Sales_Orders", soId.toLong(), emap);
    wb = "";
    if (uR.toString().contains("error")) { wb = ",\"writebackFailed\":true"; }
    return "{\"error\":\"TAX_TYPE_INVALID\",\"taxType\":\"" + taxType + "\"" + wb + "}";
}
税区分の検証。ここも「分からないときは止める」で揃える

この金額・科目・税区分をもとに仕訳を組みます。

javascriptpostSalesOrderToBugyo.dg(抜粋8)
// 項目記号(伝票日付・摘要・借方科目…を指す識別子)は
// 勘定奉行クラウドの API 仕様書に記載がある。ここでは意味が読めるよう定数名に写してある。
line = Map();
line.put(ITEM_ROW_MARK,  "*");          // 新規行を表す区分値
line.put(ITEM_SLIP_DATE, slipDate);     // 伝票日付
line.put(ITEM_LINE_MEMO, lineMemo);     // 明細行の摘要(ここにトレースキーが入る)

// 借方: 債権科目(取引先マスタ由来)
line.put(ITEM_DR_ACCOUNT, acReceivable);
line.put(ITEM_DR_AMOUNT,  totalR.toString());    // 借方は税込の総額
line.put(ITEM_DR_TAX,     "0");
line.put(ITEM_DR_TAXCALC, TAXCALC_NONE);         // 借方では税計算させない

// 貸方: 売上科目(商品マスタ由来)と消費税区分(マスタ由来)
line.put(ITEM_CR_ACCOUNT,     acSales);
line.put(ITEM_CR_TAXCATEGORY, taxCatCode);   // 課税・非課税にかかわらず必ず送る

// 補助科目・取引先コードは設定されているときだけ送る(空で送ると弾かれる)
if (subAcctCode != "")
{
    line.put(ITEM_DR_SUBACCOUNT, subAcctCode);
    line.put(ITEM_CR_SUBACCOUNT, subAcctCode);
}
if (custCode != "")
{
    line.put(ITEM_DR_CUSTOMER, custCode);
    line.put(ITEM_CR_CUSTOMER, custCode);
}

if (isTaxable)
{
    // 税抜金額と消費税額を明示する。奉行に税を計算させない
    // 消費税率種別で標準(10%)と軽減(8%)を分ける
    line.put(ITEM_CR_AMOUNT,   netR.toString());
    line.put(ITEM_CR_TAX,      taxR.toString());
    line.put(ITEM_CR_TAXCALC,  TAXCALC_EXCL);
    line.put(ITEM_CR_RATEKIND, rateKind);
}
else
{
    // 非課税・不課税。税額は0で立てる
    // 消費税区分は上で送っているので、科目マスタの既定(課税売上)は使われない
    line.put(ITEM_CR_AMOUNT,  totalR.toString());
    line.put(ITEM_CR_TAX,     "0");
    line.put(ITEM_CR_TAXCALC, TAXCALC_NONE);
}
仕訳の組み立て。科目も取引先も税率種別も、すべてマスタと税区分から決まる

税額を0にするだけでは、不課税にならない

非課税・不課税のときに消費税区分コードを送らないと、科目マスタ側の既定(課税売上)が適用され、不課税のつもりの伝票が「課売上 10%」で記録されます。税額が0なので貸借は合いますし、画面もエラーになりません。しかし消費税の集計では課税売上として拾われます。金額が合っていても区分が違えば誤りです。

対処は勘定科目と同じです。消費税区分も奉行のマスタを正とし、CRM から参照します。 コードに「不課税ならこの区分」と書けば、同じ誤りを別の場所で作ることになります。

javascriptpostSalesOrderToBugyo.dg(抜粋9)
// 奉行に送る消費税区分コードをマスタから引く
// 受注書の上書き > 商品マスタの既定 の順に見る
taxCatCode = "";
taxCatName = "";
if (taxCatRef != null)
{
    tcLabel    = ifnull(taxCatRef.get("name"), "").toString().trim();
    taxCatCode = tcLabel.getPrefix(" ").trim();   // 「0005 不課税売上」の先頭
    taxCatName = tcLabel.getSuffix(" ").trim();
}
if (taxCatCode == "")
{
    // 区分が決まらないなら計上しない。ここも他のガードと同じ fail-closed に揃える
    emap = Map();
    emap.put("Bugyo_Sync_Status", "エラー");
    emap.put("Bugyo_Sync_Error",  "消費税区分が決まりません。受注書か商品に「奉行 消費税区分」を設定してください");
    uR = zoho.crm.updateRecord("Sales_Orders", soId.toLong(), emap);
    wb = "";
    if (uR.toString().contains("error")) { wb = ",\"writebackFailed\":true"; }
    return "{\"error\":\"TAX_CATEGORY_NOT_RESOLVED\"" + wb + "}";
}

// 税区分(課税か否か)とマスタの区分名が食い違っていたら止める
catIsTaxable = taxCatName.startsWith("課税売上");
if (catIsTaxable != isTaxable)
{
    return "{\"error\":\"TAX_CATEGORY_MISMATCH\"}";
}
消費税区分もマスタ由来にする

軽減税率は「消費税率種別」で表せる

仕訳伝票には消費税率種別という項目があり、0: 標準 と 1: 軽減 を指定できます。受注書の税区分がそのままここに対応するので、軽減8%の受注も同じ1本の仕組みで通ります。伝票には「消費税率種別 1 軽減」が記録されます。ただし1枚の受注に10%と8%が混在する場合は、明細の税率ごとに仕訳行を分ける必要があります。1受注=1仕訳行なので、そこは対象外です。

Deluge の文法上の制約

ここまでで仕訳の中身は組み上がります。最後に、Deluge の文法上の制約を1つ。

関数の先頭にコメントを置くと保存できない

Deluge は関数シグネチャの前にコメントがあると Improper code format で保存を拒否します。説明コメントは { の内側に移してください。

Zoho CRM の Deluge エディタ。関数の先頭にコメントブロックを置いた状態で Improper code format というエラーが表示されている
関数の先頭にコメントを置いた状態。画面下部に Improper code format が出て保存できない

Step 4:カスタムボタンから呼ぶ

ここで1つ制約があります。カスタムボタンから選べるのは「ボタン」カテゴリーの関数だけで、ここまで書いてきた単独カテゴリーの関数は一覧に出てきません。 そこで、ボタンから呼ばれる薄いラッパーを別に1本用意します。

javascriptbugyoPostFromButton.dg
string button.bugyoPostFromButton(String soId)
{
    // カスタムボタンから選べるのは「ボタン」カテゴリーの関数だけなので、
    // 本体を単独関数に置き、ここは薄いラッパーにしている
    return standalone.postSalesOrderToBugyo(soId);
}
カスタムボタンから呼ばれる関数。ボタン関数から単独関数は呼べる

副作用として、本体の単独関数はワークフローやスケジュール実行からもそのまま呼べます。

「設定 > タブと項目 > 受注書 > ボタン」からカスタムボタンを作り、動作に「関数を実行する」を選ぶと、登録済みのボタン関数が一覧で選べます。

Zoho CRM のカスタムボタン作成画面で、実行する関数の一覧から対象の関数を選んでいるところ
ボタンから呼ぶ関数を選ぶ。ここに出てくるのは「ボタン」カテゴリーの関数だけ

引数には受注書のレコードID を割り当てます。引数欄で # キーを押すとマージ項目のピッカーが開くので、「受注書 - 受注書ID」を選びます(${!Sales_Orders.Id} が入ります)。ここで件名や受注番号を渡したくなりますが、どちらも後から人が書き換えられる値なので使いません。

Zoho CRM の受注書詳細画面の上部に「奉行に仕訳連携」ボタンが表示されている
受注書の詳細画面にボタンが出た状態

確認方法:承認済み・期限ありの受注書を1件作ってボタンを押します。数秒待って伝票Noが返り、受注書の「奉行連携ステータス」が「連携済」になっていれば成功です。

実装でつまずくところ

受入結果の照会はトークンを引き継ぐ

受入結果の照会は、前回の応答ヘッダーを引き継がないと永遠に終わらない

仕訳の受入は非同期です。受入 API は処理IDを返すだけで、完了したかどうかは結果照会 API で確かめます。この結果照会は、処理IDだけでは 200 秒・66 回ポーリングしても Processing のままです。受入から2分経過し処理が終わっている後でも結果は同じで、しかも毎回あたらしいトークンが発行されます(=照会セッションが毎回リセットされます)。

正解は、応答ヘッダー X-OBC-ContinuationToken を次のリクエストのヘッダーに載せることです。引き継がないと照会セッションが毎回リセットされ、永遠に完了しません。

plaincontinuation-token.log
# 受入から2分後。処理は確実に終わっている。

## (A) ContinuationToken を付けずに照会 → 何度やっても Processing
{"operationID":"83173c93-...","status":"Processing","startDateTime":"2026-10-19T01:55:05Z","unAcceptedReasonList":null}
{"operationID":"83173c93-...","status":"Processing","startDateTime":"2026-10-19T01:55:05Z","unAcceptedReasonList":null}
{"operationID":"83173c93-...","status":"Processing","startDateTime":"2026-10-19T01:55:05Z","unAcceptedReasonList":null}

## (B) 直前の応答ヘッダーのトークンを引き継いで照会
[1] token=(なし)            -> Processing   ← ここで発行されたトークンを次に使う
[2] token=MjAyNjEwMTkwMTU3… -> Processing
[3] token=MjAyNjEwMTkwMTU3… -> Finished     ← 到達する
同じ処理IDに対する照会の比較。トークンを引き継ぐかどうかだけが違う

Deluge 側では、ポーリングのたびにヘッダーの Map を作り直し、前回のトークンがあれば載せます。使い回すと前回のトークンが別の要求に混ざります。

javascriptpostSalesOrderToBugyo.dg(抜粋10)
// Deluge の単独関数では while が使えないため、固定リストを回す
for each idx in {1,2,3,4,5,6,7,8,9,10,11,12}
{
    if (!isFinished)
    {
        pollHdr = Map();
        pollHdr.put("X-OBC-TenantID",        tenantId);
        pollHdr.put("X-OBC-SubscriptionKey", subKey);
        pollHdr.put("Content-Type",          "application/json");
        if (contToken != "")
        {
            pollHdr.put("X-OBC-ContinuationToken", contToken);
        }

        pResp = invokeurl
        [
            url        : baseUrl + "/gl/v2/ImportJournalSlipResult"
            type       : POST
            headers    : pollHdr
            parameters : pBody.toString()      // {"operationID": opId}
            connection : "bugyo_conn"
            detailed   : true                  // responseHeader を取るために必須
        ];

        // 次のリクエストのためにトークンを取り出しておく
        respHdrs  = ifnull(pResp.get("responseHeader"), Map());
        contToken = ifnull(respHdrs.get("X-OBC-ContinuationToken"),
                    ifnull(respHdrs.get("x-obc-continuationtoken"), "")).toString();

        pText = ifnull(pResp.get("responseText"), "").toString().trim();
        if (pText.startsWith("{"))
        {
            pMap = pText.toMap();
            if (ifnull(pMap.get("status"), "").toString() == "Finished") { isFinished = true; }
        }
    }
}
ContinuationToken を引き継ぐポーリング

curl で間隔を空けた場合は2〜3回で終わりますが、Deluge 側では待機を挟めないため、上限回数まで回ることを前提に組みます。そして ループを抜けても Finished にならなかったときの着地を必ず用意してください。ここを「エラー」と書いてしまうと、人が再実行します。そのとき伝票はまだ確定しておらず照会にも出てこないので、重複判定をすり抜けて2枚目が立ちます。「要確認」というステータスにして処理IDを残し、再実行を促さない形にします。

responseText は、応答が JSON 配列だとリストで返る

invokeurl の responseText は、応答が JSON 配列のとき文字列ではなくリストで返ります。toString().startsWith("[") で分岐すると通らない経路ができるので、リストと文字列の両方に対応させてください。ここを取りこぼすと「奉行から伝票Noを取れない」ように見えますが、原因はコード側です。なお奉行の照会は該当0件のとき、空ボディのことも [] のこともあります。どちらも「該当なし」として扱う必要があります。

奉行 API の作法

奉行の API 側にも細かい作法があります。全エンドポイントが POST で、ボディが空の POST は CDN/WAF 層で HTTP 411(Length Required)になります(空でも -d '' が要ります)。また Deluge の invokeurl で parameters に Map を直渡しすると form-encoded になり、Content-Type: application/json を付けていても JSON として届きません。必ず toString() で文字列にしてから渡してください。

10秒の実行上限と、参照の往復

カスタムボタンから呼ぶ関数には10秒の実行上限がある

マスタを参照する作りにすると、実測は 9.05 秒まで伸びます。上限まで1秒を切ります。参照先のレコードを1つずつ取りに行くためで、この状態では本番で落ちます。

原因は参照のたどり方です。受注書、取引先、その先の債権科目、商品、その先の売上科目、消費税区分——素直に書くと1回の実行で6回レコードを取りに行くことになります。

削り方は単純です。マスタの名前を 01350 売掛金 のように「コード+名称」で同期しておけば、参照の表示名の先頭を取るだけでコードが分かります。参照先そのものを取得する必要がありません。

javascriptpostSalesOrderToBugyo.dg(抜粋11)
// 参照先を取りに行かず、表示名からコードを取る(1往復ぶん節約)
raRef = acct.get("Bugyo_Receivable_Account");
if (raRef != null)
{
    acReceivable = ifnull(raRef.get("name"), "").toString().trim().getPrefix(" ").trim();
}
マスタの Name を「コード+名称」で同期しておけば、参照先の取得が要らない
設計レコード取得実測10秒までの余裕
参照をたどるたびにレコードを取得する6回9.05 秒0.9 秒
マスタ名「コード+名称」を表示名から読む3回5.6〜6.6 秒3.4 秒

上限がある場所では、余裕がどれだけ残っているかまで測っておく必要があります。

上限に当たりそうなときの逃げ道

関数の呼び出し元を変える方法もあります。Zoho CRM の関数の実行上限は、どこから呼ぶかで変わります。

呼び出し元実行時間の上限
カスタムボタン/関連リスト/入力規則/REST API10 秒
ワークフロールール/ブループリント/承認プロセス30 秒
スケジュール実行15 分

ロジックを単独関数に一本化してあるので、同じ関数をワークフローやスケジュールから呼べます。件数が増えたら、ボタンは「受付」までにして、確定はスケジュール実行に逃がす設計に変えられます。

受注書の総計は後から変わる

受注書の総計は、あとから変わる

受注書の総計は、レコードが更新されると再計算されます(税率マスター未設定の組織では 550,000 円 → 500,000 円のように消費税が失われます)。計上そのものは実行時点の総計で行われますが、あとから受注書を見ても「いくらで計上したのか」が分からなくなります。そのため「奉行 計上金額(控え)」項目に、計上した税込金額をそのまま書き戻します。

受注書の状態ごとの挙動

税区分がどう記録されるか

奉行側に実際に立った仕訳です。税区分の違いがそのまま記録に出ています。

勘定奉行クラウドの仕訳帳画面。不課税・課税10%・軽減8%の3伝票が並び、それぞれ消費税の扱いが異なっている
上から 不課税(税区分の表示なし)/課税10%/軽減8%。摘要の先頭にトレースキーが入り、取引先コードも載る

いちばん上の不課税の行だけ、貸方に税区分の表示がありません。消費税区分コードを送らない場合、ここは「課売上 10%」になります。

CRM 側では、成功したときに連携ステータス・伝票No・トレースキー・計上金額の控えが入ります。

Zoho CRM の受注書詳細画面。奉行連携ステータスが連携済、仕訳伝票No が 000032、トレースキーと計上金額の控え 330,000 円が入っている
計上後の受注書。伝票No・トレースキー・計上した税込金額が残り、エラー欄は空のまま(画面は別回の計上時のもの)

連打と失敗時に何が残るか

同じボタンをもう一度押しても、仕訳は増えません。第1の関所で止まるので、奉行の API は1回も呼ばれず、1.4 秒で返ってきます。

ボタンを再度押したときの結果ダイアログ。ALREADY_POSTED_LOCAL と既存の伝票No が返っている
2回目のクリック。計上せず、既にある伝票Noを返すだけで終わる(返り値 ALREADY_POSTED_LOCAL =すでに計上済み)

計上できない受注は、理由を受注書に残して止まります。画面にエラーが出て消えるだけだと、あとから何が起きたのか分かりません。

期限が未設定の受注書でボタンを押したときの結果ダイアログ。NO_SLIP_DATE が返っている
期限が未設定の受注。計上日を推測せずに中止する(返り値 NO_SLIP_DATE =計上日が決まらない)
Zoho CRM の受注書詳細画面。奉行連携ステータスがエラーになり、エラー内容に理由の文章が記録されている
失敗の理由は受注書に日本語で残る。開けば何が足りないか分かる

受注書の状態によって、仕組みは次のように振る舞います。

受注書の状態結果受注書に残るもの
承認済・期限あり・課税売上 10%計上成功連携済/伝票No/トレースキー/計上金額の控え
承認済・期限あり・課税売上 軽減8%計上成功(税率種別 軽減)同上
承認済・期限あり・不課税売上計上成功(消費税 0・区分は不課税売上)同上
承認済・期限あり・非課税売上計上成功(消費税 0・区分は非課税売上)同上
下書きのまま計上せず中止エラー/「承認済または納品済の受注だけです」
承認済だが期限が空計上せず中止エラー/「期限が未設定です」
税区分が未設定計上せず中止エラー/「税区分が未設定です」
税区分は課税なのに消費税額が0計上せず中止エラー/「課税売上なのに消費税額が0です」
科目がどこにも設定されていない計上せず中止エラー/「勘定科目が決まりません」
総計が0以下/内訳が総計を超える計上せず中止エラー/金額の整合を確認する
すでに連携済みの受注を再度クリック計上せず、既存の伝票Noを返す(1.4 秒)変化なし

返り値と、人がやること

ボタンが返す値と、そのとき人がやるべきことを整理しておきます。「要確認」だけは、押し直してはいけない状態です。

返り値受注書のステータス人がやること
計上成功連携済なし
計上成功・伝票Noは反映待ち連携済後で伝票Noだけ確認する
受入結果が確定せず要確認押し直さない。 処理IDをもとに奉行側を確認する
奉行が受入を却下エラー却下理由を読んで受注書やマスタを直す
勘定科目が決まらないエラー取引先の債権科目/商品の売上科目/既定値のどれかを設定する
税区分が不正エラー税区分を選ぶ、または金額と突き合わせる
総計が0以下エラー受注明細と金額を確認する
金額の内訳が総計を超えるエラー小計・値引・調整と総計の整合を確認する
重複確認の照会が失敗エラートークンの失効や通信を疑う。計上はされていない
すでに計上済み(Zoho 側で判定/奉行側で判定)連携済なし
計上はできたが書き戻しに失敗未連携のまま奉行には伝票がある。 手で受注書を直す

最後の行が、この仕組みでいちばん危ない状態です。だから書き戻しは全箇所で戻り値を検査しています。

残っている課題

この構成は実用に足りますが、そのまま大規模に広げられる設計ではありません。適用前に次の点を確認してください。

  • 同時クリックは止められない。 1人が連打する分は第1の関所で止まりますが、2人がほぼ同時に押した場合は両方が関所を抜けうる。奉行側の照会でも、相手の伝票がまだ現れていなければ検知できません。「計上中」という中間ステータスを先に書き込んでから進む作り(楽観ロック)が要ります。
  • 照会の窓(計上日の前後31日)の外は検知できない。 計上後に連携ステータスが失われ、かつ期限が31日以上動かされると、第2の関所も素通りします。
  • 1受注=1仕訳行なので、1枚に10%と8%が混ざる受注は分解できない。 税率種別と消費税区分そのものは扱えますが、混在する場合は明細の税率ごとに仕訳行を分ける設計が要ります。
  • 計上後の取消・金額変更に対応していない。 受注書の金額は後から変わりうるため、逆仕訳や差額仕訳を起こす経路が要りますが、本構成にはありません。控え金額と現在の総計の差分を定期に突き合わせるレポートが要ります。
  • 部門・担当者・摘要マスタは未対応。 勘定科目・取引先・消費税区分は同期しましたが、部門別に仕訳を分けたい場合は同じやり方でマスタを増やす必要があります(仕組みは同じなので、作業量の問題です)。
  • マスタ同期は初期投入のみを想定している。 科目や取引先が増えたときに追従させるには、スケジュール実行で定期的に同期する仕掛けが要ります。
  • トークンが失効すると全件止まる。 正しい挙動ですが、能動的な通知がありません。エラー状態の受注を集めたビューを作り、締め前に確認する運用が要ります。
  • 実行時間の余裕が3秒前後しかない。 マスタ参照で約1秒増えました。件数が増えるなら、ボタンは受付までにして確定はスケジュール実行へ、という設計変更が前提です。
  • 本番テナントでは会計期間・締め処理・権限によって弾かれる経路がある。 本記事の構成はサンドボックスを前提にしています。

それでも、「転記という仕事そのものを無くす」という目的に対しては十分に効きます。

運用に乗せる前に決めること

技術的に動くことと、経理の業務に乗ることは別です。導入前に次の4点を決めてください。

  1. 計上基準日に使う項目を決める。 受注書の「期限」は支払・納品の期限であって、売上の計上基準日とは別の概念です。引渡基準日を持つ項目を別に用意するのが本筋です
  2. 連携ステータスを人が書き換えられないようにする。 Bugyo_Sync_Status は第1の関所そのものです。項目レベル権限で管理者以外は読み取り専用にします
  3. 「エラー」「要確認」の受注を集めたビューを作る。 失敗は受注書に残りますが、誰かが見に行かないと気づけません。締め前に確認する運用とセットにします
  4. アクセストークン失効時の手順を決める。 奉行のトークンは自動更新がなく再表示もできません。失効すると全件が計上中止になります。再発行の手順と保管場所を先に決めておきます

まとめ

  • Zoho CRM の Connection + Deluge 関数 + カスタムボタンだけで、受注書から勘定奉行クラウドへの売上仕訳を自動化できる。連携用のサーバーも外部の連携サービスも要らない
  • 勘定科目をプログラムに書かない。 奉行のマスタを CRM に同期し、商品・取引先から参照する。科目が増えても直すのはマスタであってコードではない
  • 鍵は Connection に預け、コードにも組織変数にも書かない
  • 計上日は受注書の項目から取る。実行日から推測する経路は作らない(タイムゾーン設定しだいで前日付になる)
  • 消費税は 総計 − 本体 で求め、税区分を受注書に持たせて「税0」と「税率の設定漏れ」を区別する。軽減8%は消費税率種別で表せる
  • 税額を0にするだけでは不課税にならない。 消費税区分コードを送らないと科目マスタの既定(課税売上)が使われる。区分もマスタから引く
  • 上限がある場所では余裕を測る。 参照のたびにレコードを取ると6往復で9秒まで伸びる。表示名からコードを取る形にすれば3往復・5.6〜6.6秒に収まる
  • 二重計上は Zoho 側の速い関所と奉行側の遅い関所の2層で止める。重複確認が失敗したときは計上しない
  • 受入結果の照会は 応答ヘッダーのトークンを引き継がないと永遠に終わらない
  • updateRecord は黙って失敗する。全箇所で戻り値を検査する
  • ボタンから呼ぶ関数の実行上限は10秒。実測は約6秒で、余裕は多くない。増えたらワークフロー(30秒)やスケジュール(15分)へ逃がす

会計システムが freee の場合も、考え方は同じです。受注・請求の流れは Zoho CRM と freee をつなぐと、営業と経理の毎日は何が変わるか、在庫の動きを会計に送る部分は Zoho Inventory の出荷と棚卸差異を freee会計の伝票にする関数を検証 で扱っています。Zoho の導入から会計連携までの全体像は Zoho 導入・会計連携支援 にまとめています。

同じ考え方は、請求書の発行や入金消込(請求と入金を突き合わせて完了処理すること)にもそのまま使えます。CRM と会計の間で人が転記している作業にも、同じ方法が使えます。

本記事の検証環境について

本記事のスクリーンショットおよび動作検証は、勘定奉行クラウドの自社サンドボックス環境と、Zoho CRM の日本データセンター上の検証組織で実施しています。検証組織には税率マスターが無いため、消費税のある受注は API で税額を与えて再現しました。本番環境では、会計期間・締め処理・権限設定・税率マスターの有無によって挙動が異なる場合があります。また、仕訳の勘定科目や税区分、計上基準日の設計は会計上の判断を伴いますので、顧問税理士・公認会計士の確認を受けたうえで運用してください。勘定奉行クラウドの API 利用条件については、株式会社オービックビジネスコンサルタントの提供条件をご確認ください。

Zoho CRM と会計システムの連携、自社で組むのが難しい場合は

はてなベース株式会社では、Zoho CRM の設計・構築から、勘定奉行クラウドや freee 等の会計システムとの API 連携までまとめて支援しています。「転記作業を無くしたいが、社内に Deluge を書ける人がいない」「二重計上が起きない設計かどうか不安」という方はお気軽にご相談ください。

無料相談はこちら