見出し画像

【ChatGPT×GAS】Salesforceデータを活用!請求書自動作成アプリを作ろう!

皆さんこんにちは!
今回は、ChatGPT×GASでの自動化第2弾!!
多くの企業で使用されている二つの強力なツール、Google Apps Script(GAS)とSalesforceの連携です!
前回の記事では、ChatGPTとGASを活用することで、今まで行っていた定型作業をどのように効率化できるかご紹介しました!

今回はより高度な業務効率化事例として、
ChatGPTの力も借りながら、Salesforceデータの取得とスプレッドシートへの入力+PDF変換 を自動化していきます!
この記事では請求書を例にしていますが、さまざまな業務に応用可能です!
ではさっそく見ていきましょう!



はじめに

今回はChatGPTを用いて、GASからSalesforce APIに接続し請求書を自動作成する機能を開発していきます!

⭐️ChatGPTを使用したコード開発のポイント⭐️
最終的には長いスクリプトを作成するのですが、各機能ずつプロンプトを投げて、コードを出力させるのがおすすめです!
その方が、機能の動作確認や、エラーが出た場合の原因究明がしやすくなります!
一度に全体のプロンプトを渡し、コードを出力してもらう方法も試しましたが、そうするとエラーが発生した時にエラー箇所の特定がしにくく、、💦
機能ごとに応用もしやすいので、今回の記事で行っている各機能ずつプロンプトを投げる方法をぜひ試してみてください!
エラーが発生した場合の確認ポイントなども記載していますので、ご参考になれば幸いです。

実際に作成してみよう

実現させたいこと
今回実現させたい処理の流れは下記の通りです!

1️⃣ スプレッドシート内「請求書メニュー」ボタンをクリックし、
 請求書番号を入力

2️⃣ 請求書番号が一致するデータをSalesforceから取得し、請求書シートを  
 自動作成

3️⃣ PDFに変換し、任意のGoogleドライブフォルダに自動で格納

今回の前提
まずは前提をお伝えします。
① Salesforce
今回は、Trailheadで作成したPlayground(Developer Edition環境)を使用しています!

Trailhead Playground は、ハンズオン Challenge に挑戦したり、新しい機能やカスタマイズを試したりできる組織です。

Salesforce Trailheadより

この環境に「請求」オブジェクトと「請求商品」オブジェクトを作成しています。「請求」オブジェクトが親、「請求商品」オブジェクトが子という関係です。
項目は画面の通りです!

「請求」オブジェクト

「請求商品」オブジェクト

② スプレッドシート
前回と同じ請求書フォーマットを使用します。
請求先や明細など、Salesforceから取得する情報は空欄にし、
単価・数量・税率が入ると金額項目は自動計算されるようにしています。
このフォーマットの該当箇所にデータを自動入力していきます!

ではさっそく、実際に機能を開発してみようと思います!
最初にSalesforce側の設定を行います!

1. Salesforce : 接続アプリケーションを作成

Salesforceの機能である「接続アプリケーション」を作成していきます!

接続アプリケーションは、API や標準プロトコル (SAML、OAuth、OpenID Connect など) を使用して、外部アプリケーションを Salesforce に統合できるようにするフレームワークです。

Salesforce Help

「設定」>「アプリケーションマネージャー」>「新規接続アプリケーション」ボタンをクリック。

下記の画面が表示されたら、どちらでも作成可能ですが、今回は「接続アプリケーションを作成」を選択しています。


必須項目のアプリケーション名・取引先責任者 メールを入力します。
取引先責任者と記載がありますが、自身のメールアドレスで大丈夫です!


次に、API(OAuth設定の有効化)を設定します。
OAuth設定の有効化にチェックを入れると、設定項目が表示されます!


🔧 設定内容

1️⃣ コールバックURL:必須となっていますが、今回は使用しないため任意の値を入力します。今回は「https://example.com」と入力しました。

2️⃣ 選択したOAuth範囲:下記の3つを選択します。
① APIを使用してユーザーデータを管理(api)
②いつでも要求を実行(refresh_token, offine_access)
③フルアクセス(full)

3️⃣ 「クライアントログイン情報フローを有効化」にチェックを入れる(Salesforce APIへ接続するにあたり、今回はユーザー名やパスワードをスクリプトに記述する必要のない「クライアントログイン情報フロー」(Client Credentialsフロー)を使用します)

保存ボタンで閉じます。
作成した接続アプリケーションは、「アプリケーションマネージャー」で確認できます!「設定」>「アプリケーションマネージャー」で作成した接続アプリケーションを開き、続きの設定を行っていきます!
画面上部の「Manage」をクリック>「ポリシーを編集」をクリックします。


🔧 設定内容

1️⃣ OAuthポリシー:許可されているユーザーで「すべてのユーザーは自己承認可能」を選択、IP制限の緩和で「IP制限の緩和」を選択

2️⃣ クライアント情報フロー:「別のユーザーとして実行」にて任意のユーザーを選択(「APIの有効化」権限があるユーザーを選択します)

⚠️ 本番環境で実装する際は、IP制限やAPI用のユーザーを使用するなどセキュリティ面に注意してください

設定が完了しました!
保存したら、接続アプリケーションの画面に戻り、「コンシューマーキーと秘密」を確認します!
「コンシューマーの詳細を管理」をクリックします。


確認した「コンシューマー鍵」と「コンシューマーの秘密」は、後ほどGASで使用するのでメモしておきましょう!


⏸️ 一般的には「コンシューマー鍵」は「クライアントID」、「コンシューマーの秘密」は「クライアントシークレット」と呼ばれます。
以下では「クライアントID」と「クライアントシークレット」と呼びます。


最後に、これも後ほどGASで必要となる「私のドメイン」を確認しておきます。「設定」>「私のドメイン」を開き、「現在の私のドメインのURL」をメモしておきましょう!

これでSalesforceでの準備は完了です!(お疲れ様です!ぜひ一息ついてくださいね☕️)


ここからは、GASでスクリプトを記述していきます!
機能を細かく分解し、実装と動作確認を繰り返しながら行うことで、エラーが出た場合の原因対処を行っていきます!

2. GAS : Salesforce APIに接続する関数を作成

まず、GASでSalesforce APIに接続する認証部分を作成していきます!
認証にはOAuth2を使用しますが、
今回はユーザ名やパスワードを使用する必要のないクライアント情報フロー(Client Credentialsフロー)を使用します!
ここでChatGPTの出番です!
下記の様に聞いてみます。

💬 プロンプト

GASからSalesforce APIに接続します。
OAuth2のClient Credentialsフローを使用したコードを作成してください。

出力されたコードがこちらです!

// GASスクリプトでアクセストークンを取得する
function getSalesforceAccessToken() {
  var clientId = 'YOUR_CLIENT_ID'; // SalesforceのConnected AppのClient ID
  var clientSecret = 'YOUR_CLIENT_SECRET'; // SalesforceのConnected AppのClient Secret
  var tokenUrl = 'https://your-instance.salesforce.com/services/oauth2/token'; // エンドポイント
  
  // リクエスト用のペイロード
  var payload = {
    'grant_type': 'client_credentials',
    'client_id': clientId,
    'client_secret': clientSecret
  };
  
  // POSTリクエストのオプション
  var options = {
    'method': 'post',
    'payload': payload
  };
  
  // Salesforceにアクセストークンをリクエスト
  var response = UrlFetchApp.fetch(tokenUrl, options);
  
  // レスポンスをパースしてアクセストークンを取得
  var result = JSON.parse(response.getContentText());
  
  // エラーチェック
  if (result.error) {
    Logger.log('Error: ' + result.error_description);
    return null;
  }
  
  Logger.log('Access Token: ' + result.access_token);
  return result.access_token;
}

// Salesforce APIを呼び出す
function callSalesforceApi() {
  var accessToken = getSalesforceAccessToken();
  if (!accessToken) {
    Logger.log('アクセストークンの取得に失敗しました。');
    return;
  }
  
  // APIエンドポイント (例: アカウント情報取得)
  var apiUrl = 'https://your-instance.salesforce.com/services/data/v57.0/sobjects/Account';
  
  // APIリクエストのオプション
  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + accessToken
    }
  };
  
  try {
    var response = UrlFetchApp.fetch(apiUrl, options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result);
  } catch (error) {
    Logger.log('API呼び出し中にエラーが発生しました: ' + error);
  }
}

このコードを元に、スクリプトを作成してみましょう!
スプレッドシートを開き、「拡張機能」から「Apps Script」を選択します。
上記のコードを貼り付け、下記の書き換えを行います!

✍️ 書き換え

1️⃣ YOUR_CLIENT_IDとYOUR_CLIENT_SECRETを先ほど取得したクライアントID・クライアントシークレットに書き換え

2️⃣ tokenUrlの「https://your-instance.salesforce.com」と、apiUrlの「https://your-instance.salesforce.com」を私のドメインURLに書き換え

※これ以降のコードでも、都度修正することをお忘れなく!

保存したら、実行してみましょう!
このコードは、まずはアクセストークンを取得するgetSalesforceAccessToken()関数を実行します!

成功し、アクセストークンを取得できたことが確認できました!

次に、Salesforce APIを呼び出すcallSalesforceApi()関数を実行してみます!
このコードでは、例として取引先の情報を取得するよう記述されています。

こちらも取引先データを取得できたことが確認できました!

3. GAS : Salesforceからデータを取得する関数を作成

次に、この取引先データを取得する部分を、今回必要な「請求」と「請求商品」オブジェクトを取得するように変更していきます!

💬 プロンプト

下記の要件に合うように、先ほどのコードを変更してください。

#要件
invoiceNumberという変数を設定します。
請求オブジェクト(Invoice__c )から、請求書番号(InvoiceNumber__c)がinvoiceNumber変数の値と一致するレコードを取得してださい。

取得する項目:
Id, 請求書番号(InvoiceNumber__c), 取引先の名前(Account__r.Name), 請求先郵便番号(BillingPostalCode__c), 請求先住所(BillingAddress__c), 支払期日(PaymentDueDate__c), 備考(Notes__c)

次に、請求商品オブジェクト(InvoiceLineItem__c)のレコードを取得してください。

条件:
請求項目(Invoice__c)が上で取得した請求レコードのIDと一致するもの

取得する項目:
Id, 日付(Date__c), 請求商品名(Name), 数量(Quantity__c), 単価(UnitPrice__c), 税率(TaxRate__c)

#変更前コード
(先ほどのコードを記述)

invoiceNumberという変数を設定したのは、指定した値と一致するデータを正しく取得する機能を実装するためです!

出力されたコードはこちらです!

// GASスクリプトでアクセストークンを取得する
function getSalesforceAccessToken() {
  var clientId = 'YOUR_CLIENT_ID'; // SalesforceのConnected AppのClient ID
  var clientSecret = 'YOUR_CLIENT_SECRET'; // SalesforceのConnected AppのClient Secret
  var tokenUrl = 'https://your-instance.salesforce.com/services/oauth2/token'; // エンドポイント
  
  // リクエスト用のペイロード
  var payload = {
    'grant_type': 'client_credentials',
    'client_id': clientId,
    'client_secret': clientSecret
  };
  
  // POSTリクエストのオプション
  var options = {
    'method': 'post',
    'payload': payload
  };
  
  // Salesforceにアクセストークンをリクエスト
  var response = UrlFetchApp.fetch(tokenUrl, options);
  
  // レスポンスをパースしてアクセストークンを取得
  var result = JSON.parse(response.getContentText());
  
  // エラーチェック
  if (result.error) {
    Logger.log('Error: ' + result.error_description);
    return null;
  }
  
  Logger.log('Access Token: ' + result.access_token);
  return result.access_token;
}

// 請求レコードと請求商品のレコードを取得する
function getInvoiceAndLineItems(invoiceNumber) {
  var accessToken = getSalesforceAccessToken();
  if (!accessToken) {
    Logger.log('アクセストークンの取得に失敗しました。');
    return;
  }

  // 1. Invoice__cから請求書番号に基づくレコードを取得
  var invoiceQuery = `
    SELECT Id, InvoiceNumber__c, Account__r.Name, BillingPostalCode__c, BillingAddress__c, PaymentDueDate__c, Notes__c
    FROM Invoice__c
    WHERE InvoiceNumber__c = '${invoiceNumber}'
    LIMIT 1
  `;
  
  var invoiceApiUrl = `https://your-instance.salesforce.com/services/data/v57.0/query/?q=${encodeURIComponent(invoiceQuery)}`;

  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + accessToken
    }
  };

  try {
    // 1. 請求レコードを取得
    var invoiceResponse = UrlFetchApp.fetch(invoiceApiUrl, options);
    var invoiceResult = JSON.parse(invoiceResponse.getContentText());

    if (invoiceResult.totalSize === 0) {
      Logger.log('一致する請求書が見つかりませんでした。');
      return;
    }

    var invoice = invoiceResult.records[0]; // 請求レコード
    Logger.log('請求レコード: ' + JSON.stringify(invoice));

    // 請求レコードのIDを取得
    var invoiceId = invoice.Id;

    // 2. InvoiceLineItem__cから関連する請求商品を取得
    var lineItemQuery = `
      SELECT Id, Date__c, Name, Quantity__c, UnitPrice__c, TaxRate__c
      FROM InvoiceLineItem__c
      WHERE Invoice__c = '${invoiceId}'
    `;

    var lineItemApiUrl = `https://your-instance.salesforce.com/services/data/v57.0/query/?q=${encodeURIComponent(lineItemQuery)}`;

    // 2. 請求商品のレコードを取得
    var lineItemResponse = UrlFetchApp.fetch(lineItemApiUrl, options);
    var lineItemResult = JSON.parse(lineItemResponse.getContentText());

    if (lineItemResult.totalSize === 0) {
      Logger.log('関連する請求商品が見つかりませんでした。');
    } else {
      Logger.log('請求商品レコード: ' + JSON.stringify(lineItemResult.records));
    }
  } catch (error) {
    Logger.log('API呼び出し中にエラーが発生しました: ' + error);
  }
}

// 請求書番号を設定して取得を実行
function executeGetInvoiceAndLineItems() {
  var invoiceNumber = 'YOUR_INVOICE_NUMBER'; // 検索したい請求書番号
  getInvoiceAndLineItems(invoiceNumber);
}

コードを見てみると、最後にある「// 請求書番号を設定して取得を実行」というコメントの付いたexecuteGetInvoiceAndLineItems()関数で、検索したい請求書番号を設定しています。(指示したinvoiceNumber変数が反映されていますね!)
YOUR_INVOICE_NUMBERを実際の請求レコードの請求書番号に書き換えます!再度書き換える箇所をまとめておきます。

✍️ 書き換え

1️⃣ YOUR_CLIENT_IDとYOUR_CLIENT_SECRETを取得したクライアントID・クライアントシークレットに書き換え

2️⃣ tokenUrl、invoiceApiUrl、lineItemApiUrlの「https://your-instance.salesforce.com」を私のドメインURLに書き換え

3️⃣ YOUR_INVOICE_NUMBERを実際の請求書番号に書き換え

ではexecuteGetInvoiceAndLineItems()関数を実行してみます!

実行完了し、請求書番号に基づいたレコードを取得したことが確認できました!

🚨エラー発生時の確認ポイント
エラーが発生したら、下記が正しく設定されているか確認してください。
・クエリの間違い(項目のAPI参照名が正しいか等)
・URLの変更漏れ
・請求書番号を実際にある番号に設定できているか
また、それでも解決しない場合は、エラー文と「エラーの原因と対処法を教えて」とChatGPTに聞くことでヒントをもらえます!

4. GAS : 請求書スクリプトを変更し、全体を調整

前回作成した請求書コードを渡して、今回実装した機能に前回の機能を追加する指示を出します!(前回の記事はこちら
具体的には、下記の変更を指示します。
・ユーザーが「請求書メニュー」で入力した請求書番号を元に請求レコードと請求商品レコードを取得するように変更
・請求書シートを複製し、複製したシートに取得したデータを入力させる
・複製したシートをPDFに変換し、指定したフォルダに格納させる
などです!

🧑‍💻 前回のおさらい
「請求書メニュー」カスタムボタンをクリックし、顧客IDを入力すると、その顧客IDと一致するデータを請求管理シートから取得し請求書を自動作成してくれるというスクリプトを作成しました!

💻 前回実装したスクリプトはこちら

// スプレッドシートを開いたときに「請求書メニュー」を追加する
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('請求書メニュー') // メニュー名
    .addItem('顧客IDを入力して請求書を作成', 'userPromptForCustomerId') // ボタン名と呼び出す関数
    .addToUi();
}

// 顧客IDを入力して請求書を作成するプロンプト表示
function userPromptForCustomerId() {
  var ui = SpreadsheetApp.getUi(); // UIの取得
  var response = ui.prompt('請求書作成', '顧客IDを入力してください。', ui.ButtonSet.OK_CANCEL);

  // OKボタンが押された場合のみ処理を続行
  if (response.getSelectedButton() == ui.Button.OK) {
    var customerId = response.getResponseText(); // 入力された顧客IDを取得
    
    if (customerId === '') {
      ui.alert('顧客IDが入力されていません。');
      return;
    }

    // 顧客IDを基に請求書を作成
    var result = createInvoiceFromCustomerID(customerId);
    
    // 結果をユーザに通知
    ui.alert(result.getContent());
  } else {
    ui.alert('操作がキャンセルされました。');
  }
}

// 顧客IDを基に請求書を作成し、そのシートをPDFに変換する関数
function createInvoiceFromCustomerID(customerId) {
  var ss = SpreadsheetApp.openById('XXXXXX'); // あなたのスプレッドシートID
  var invoiceManagementSheet = ss.getSheetByName('請求管理'); // 請求管理シート
  var templateSheet = ss.getSheetByName('請求書'); // テンプレートシート
  
  // 「請求管理」シートから、入力された顧客IDと合致する請求情報を取得
  var data = invoiceManagementSheet.getDataRange().getValues();
  var filteredData = [];
  
  for (var i = 1; i < data.length; i++) { // ヘッダーをスキップ
    if (data[i][0] == customerId) { // A列の顧客IDで一致するものを抽出
      filteredData.push(data[i]);
    }
  }
  
  if (filteredData.length === 0) {
    return ContentService.createTextOutput('該当する顧客IDが見つかりませんでした。');
  }
  
  // 複数の請求明細が存在する可能性を考慮して、シートを複製し、請求番号をシート名にする
  var invoiceNumber = filteredData[0][1]; // B列(請求番号)
  // var newSheet = templateSheet.copyTo(ss).setName(invoiceNumber + '_' + new Date().getTime()); // タイムスタンプでユニークにする
  var newSheet = templateSheet.copyTo(ss).setName(invoiceNumber);

  // シート更新を強制
  SpreadsheetApp.flush();

  // 複製されたシートのIDを取得
  var targetSheet = ss.getSheetByName(newSheet.getName());
  var targetSheetId = targetSheet.getSheetId();
  
  // ログで確認
  Logger.log('Created sheet: ' + newSheet.getName());
  Logger.log('Sheet ID: ' + newSheet.getSheetId());
  Logger.log('Exporting sheet with ID: ' + targetSheetId);

  // シート更新後の必要なデータの配置
  newSheet.getRange('O4').setValue(invoiceNumber);
  newSheet.getRange('B6').setValue(filteredData[0][2]);
  newSheet.getRange('B7').setValue(filteredData[0][3]);
  newSheet.getRange('B5').setValue(filteredData[0][4] + ' 御中');
  newSheet.getRange('C38').setValue(filteredData[0][10]);

  // 複数レコード対応: F列〜J列の値を17行目以降に順番に格納
  var startRow = 16;
  for (var i = 0; i < filteredData.length; i++) {
    newSheet.getRange('B' + (startRow + i)).setValue(filteredData[i][5]); // F列(日付)
    newSheet.getRange('D' + (startRow + i)).setValue(filteredData[i][6]); // G列(請求内容)
    newSheet.getRange('M' + (startRow + i)).setValue(filteredData[i][7]); // H列(単価)
    newSheet.getRange('K' + (startRow + i)).setValue(filteredData[i][8]); // I列(数量)
    newSheet.getRange('O' + (startRow + i)).setValue(filteredData[i][9]); // J列(税率)
  }

  // 複製したシートをアクティブ化してPDFエクスポートの対象に設定
  newSheet.activate();  // 確実に複製したシートをアクティブにする
  SpreadsheetApp.flush();  // 変更を反映

  // 作成したシートをPDFに変換して保存する処理
  var folderId = 'XXXXXX'; // 保存先フォルダIDに置き換えてください
  var folder = DriveApp.getFolderById(folderId);

  // ファイル名を生成
  var fileName = 'Invoice_' + invoiceNumber + '.pdf';

  // スプレッドシートのIDを取得
  var ssId = ss.getId();

  // PDFエクスポート用のパラメータで新しく作成したシートのIDを使用
  var url = 'https://docs.google.com/spreadsheets/d/' + ssId + '/export?' +
            'format=pdf' +
            '&size=A4' +          // 用紙サイズ
            '&portrait=true' +     // 縦向き
            '&fitw=true' +         // 幅に合わせて調整
            '&sheetnames=false' +  // シート名を表示しない
            '&printtitle=false' +  // スプレッドシートのタイトルを表示しない
            '&pagenumbers=false' + // ページ番号を表示しない
            '&gridlines=false' +   // グリッド線を表示しない
            '&fzr=false' +         // 凍結行を印刷しない
            '&gid=' + targetSheetId; // 複製されたシートのIDを使用

  Logger.log('PDF Export URL: ' + url); // URLをログに記録

  var token = ScriptApp.getOAuthToken();
  var response = UrlFetchApp.fetch(url, {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  });

  var blob = response.getBlob().setName(fileName);
  var file = folder.createFile(blob);

  return ContentService.createTextOutput('請求書の作成とPDFの保存が完了しました: ' + file.getUrl());
}


💬 プロンプト
前回は「顧客ID」を使用しましたが、今回は請求書IDと変更しているので合わせて修正を指示しています。

前回までのコードに、今回のこれまでの実装内容を追加し、要件に合うように変更してください。

#要件
・ユーザーが「請求書メニュー」で入力した請求書番号を元に請求レコードと請求商品レコードを取得
・請求書シートを複製し、複製したシートに取得したデータを入力する
・取得した値を指定したセルに入力
・複製したシートをPDFに変換し、指定したフォルダに格納
・「顧客ID」という文言を「請求書ID」に変更
・「CustomerId」は「InvoiceId」に変更
・実行後のメッセージやエラーメッセージをUI上に表示

#データの入力先セル
<請求オブジェクト(Invoice__c)>  
-取引先(Account__c)のName項目:B5   ※取引先(Account__c)のName項目の値に’ ’(半角スペース)+’御中’を付与してセットする  
-請求書番号(InvoiceNumber__c):O4  
-請求先郵便番号(BillingPostalCode__c):B6  
-請求先住所(BillingAddress__c):B7  
-支払期日(PaymentDueDate__c):C33  
-備考(Notes__c):B36
<請求商品オブジェクト(InvoiceLineItem__c)>
レコードが複数ある場合、16行目以降の各セルにレコードの数だけ入力を繰り返してください。  
-日付(Date__c):B16  
-請求商品名(Name):D16  
-数量(Quantity__c):K16  
-単価(UnitPrice__c):M16  
-税率(TaxRate__c):O16

#前回までのコード
(コードを記述)
#今回のこれまでの実装内容
(コードを記述)

コードはこちらです!
PDF化する際に、値が格納される前のテンプレートのシートをPDF化してしまう現象があったので、
「// 5. シートをPDFに変換して保存」内の、下記はこちらで追加しました。

newSheet.activate(); // 確実に複製したシートをアクティブにするSpreadsheetApp.flush(); // 変更を反映

// スプレッドシートを開いたときに「請求書メニュー」を追加する
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('請求書メニュー') // メニュー名
    .addItem('請求書番号を入力して請求書を作成', 'userPromptForInvoiceNumber') // ボタン名と呼び出す関数
    .addToUi();
}

// ユーザーに請求書番号を入力させるプロンプト
function userPromptForInvoiceNumber() {
  var ui = SpreadsheetApp.getUi();
  var response = ui.prompt('請求書作成', '請求書番号を入力してください。', ui.ButtonSet.OK_CANCEL);

  if (response.getSelectedButton() == ui.Button.OK) {
    var invoiceNumber = response.getResponseText(); // 入力された請求書番号
    if (invoiceNumber === '') {
      ui.alert('請求書番号が入力されていません。');
      return;
    }
    // 入力された請求書番号を基に請求書を作成
    createInvoiceFromSalesforce(invoiceNumber);
  } else {
    ui.alert('操作がキャンセルされました。');
  }
}

// Salesforceから請求レコードと請求商品を取得し、複製したシートにデータを入力し、PDFに変換
function createInvoiceFromSalesforce(invoiceNumber) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var ui = SpreadsheetApp.getUi(); 
  var templateSheet = ss.getSheetByName('請求書'); // テンプレートシート
  
  // 1. Salesforceから請求レコードと請求商品を取得
  var accessToken = getSalesforceAccessToken();
  if (!accessToken) {
    ui.alert('アクセストークンの取得に失敗しました。');
    return;
  }

  var invoiceQuery = `
    SELECT Id, InvoiceNumber__c, Account__r.Name, BillingPostalCode__c, BillingAddress__c, PaymentDueDate__c, Notes__c
    FROM Invoice__c
    WHERE InvoiceNumber__c = '${invoiceNumber}'
    LIMIT 1
  `;
  
  var invoiceApiUrl = `https://your-instance.salesforce.com/services/data/v57.0/query/?q=${encodeURIComponent(invoiceQuery)}`;
  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + accessToken
    }
  };

  try {
    var invoiceResponse = UrlFetchApp.fetch(invoiceApiUrl, options);
    var invoiceResult = JSON.parse(invoiceResponse.getContentText());

    if (invoiceResult.totalSize === 0) {
      ui.alert('一致する請求書が見つかりませんでした。');
      return;
    }

    var invoice = invoiceResult.records[0]; // 請求レコード
    var invoiceId = invoice.Id;

    // 2. Salesforceから請求商品のデータを取得
    var lineItemQuery = `
      SELECT Id, Date__c, Name, Quantity__c, UnitPrice__c, TaxRate__c
      FROM InvoiceLineItem__c
      WHERE Invoice__c = '${invoiceId}'
    `;
    var lineItemApiUrl = `https://your-instance.salesforce.com/services/data/v57.0/query/?q=${encodeURIComponent(lineItemQuery)}`;
    var lineItemResponse = UrlFetchApp.fetch(lineItemApiUrl, options);
    var lineItemResult = JSON.parse(lineItemResponse.getContentText());

    // 3. テンプレートシートを複製し、データを入力
    var newSheet = templateSheet.copyTo(ss).setName(invoice.InvoiceNumber__c);
    SpreadsheetApp.flush(); // シートの更新を強制
    newSheet.getRange('B5').setValue(invoice.Account__r.Name + ' 御中'); // 取引先の名前 + ' 御中'
    newSheet.getRange('O4').setValue(invoice.InvoiceNumber__c); // 請求書番号
    newSheet.getRange('B6').setValue(invoice.BillingPostalCode__c); // 請求先郵便番号
    newSheet.getRange('B7').setValue(invoice.BillingAddress__c); // 請求先住所
    newSheet.getRange('C33').setValue(invoice.PaymentDueDate__c); // 支払期日
    newSheet.getRange('B36').setValue(invoice.Notes__c); // 備考

    // 4. 請求商品のデータを複製したシートに入力
    if (lineItemResult.totalSize > 0) {
      var startRow = 16;
      for (var i = 0; i < lineItemResult.records.length; i++) {
        var item = lineItemResult.records[i];
        newSheet.getRange('B' + (startRow + i)).setValue(item.Date__c); // 日付
        newSheet.getRange('D' + (startRow + i)).setValue(item.Name); // 請求商品名
        newSheet.getRange('K' + (startRow + i)).setValue(item.Quantity__c); // 数量
        newSheet.getRange('M' + (startRow + i)).setValue(item.UnitPrice__c); // 単価
        newSheet.getRange('O' + (startRow + i)).setValue(item.TaxRate__c); // 税率
      }
    }

    // 5. シートをPDFに変換して保存
    // 複製したシートをアクティブ化してPDFエクスポートの対象に設定
    newSheet.activate();  // 確実に複製したシートをアクティブにする
    SpreadsheetApp.flush();  // 変更を反映
    var folderId = 'YOUR_FOLDER_ID'; // 保存先フォルダIDを指定
    var folder = DriveApp.getFolderById(folderId);
    var fileName = 'Invoice_' + invoice.InvoiceNumber__c + '.pdf';
    var ssId = ss.getId();
    var targetSheetId = newSheet.getSheetId();
    
    var pdfUrl = 'https://docs.google.com/spreadsheets/d/' + ssId + '/export?' +
                 'format=pdf' +
                 '&size=A4' +
                 '&portrait=true' +
                 '&fitw=true' +
                 '&sheetnames=false' +
                 '&printtitle=false' +
                 '&pagenumbers=false' +
                 '&gridlines=false' +
                 '&fzr=false' +
                 '&gid=' + targetSheetId;
    
    Logger.log('PDF Export URL: ' + pdfUrl); // URLをログに記録

    var token = ScriptApp.getOAuthToken();
    var pdfResponse = UrlFetchApp.fetch(pdfUrl, {
      headers: {
        'Authorization': 'Bearer ' + token
      }
    });

    var blob = pdfResponse.getBlob().setName(fileName);
    var file = folder.createFile(blob);

    ui.alert('PDF保存完了: ' + file.getUrl());
    return ContentService.createTextOutput('請求書が正常に作成され、PDFが保存されました: ' + file.getUrl());
  } catch (error) {
    ui.alert('エラーが発生しました: ' + error);
  }
}

// Salesforceのアクセストークンを取得する関数
function getSalesforceAccessToken() {
  var clientId = 'YOUR_CLIENT_ID'; // SalesforceのConnected AppのClient ID
  var clientSecret = 'YOUR_CLIENT_SECRET'; // SalesforceのConnected AppのClient Secret
  var tokenUrl = 'https://your-instance.salesforce.com/services/oauth2/token'; // Salesforceのエンドポイント

  var payload = {
    'grant_type': 'client_credentials',
    'client_id': clientId,
    'client_secret': clientSecret
  };

  var options = {
    'method': 'post',
    'payload': payload
  };

  var response = UrlFetchApp.fetch(tokenUrl, options);
  var result = JSON.parse(response.getContentText());

  if (result.error) {
    Logger.log('Error: ' + result.error_description);
    return null;
  }
  
  return result.access_token;
}

下記の値を書き換えます!
✍️ 書き換え

1️⃣ YOUR_CLIENT_ID、YOUR_CLIENT_SECRETをクライアントID・クライアントシークレットに書き換え

2️⃣ tokenUrl、invoiceApiUrl、lineItemApiUrlの「https://your-instance.salesforce.com」を私のドメインURLに書き換え

3️⃣ YOUR_FOLDER_IDを任意のドライブIDに書き換え

5. 実行してみる

では実行してみます!!

請求書番号のシートが作成され、データが出力されたことが確認できました!
PDFも確認します!

指定したフォルダにPDFが作成され、請求先や明細も反映されていました!
実装完了です!!

まとめ

今回は、ChatGPT、Google Apps Script、Salesforceを組み合わせて、請求書作成業務を自動化する方法を紹介しました。
この自動化により、以下のようなメリットが得られます!

  1. 手作業による入力ミスの削減

  2. 請求書作成にかかる時間の大幅な短縮

  3. Salesforceとスプレッドシート間のデータ連携の効率化

  4. PDF化と保存の自動化によるファイル管理の簡素化

この方法は請求書作成だけでなく、他の業務プロセスにも応用可能です!
例えば、見積書の作成、在庫管理レポートの自動生成、顧客情報の同期など。

ChatGPTを活用することで、このように段階的にコードを作成し、複雑な自動化を実現できることがお分かりいただけたかと思います!

ただ、こうした自動化のプロセスは一度セットアップすれば万能というわけではなく、時には微調整が必要になることもあるため、
GASに少し慣れておくことがおすすめです!

SAMURAI では、GASやChatGPTについて学べるレッスンを提供しています。
初心者でもわかりやすく、自動化に必要な知識を段階的に習得できるので、是非チェックしてみてください!

業務効率の向上と、より創造的な仕事への時間の確保につなげましょう!
今回の記事を参考に、皆さんも自社の業務に合わせた自動化にチャレンジしてみてくださいね!