【ServiceNow】カタログ変数をUIアクションでCSV・PDFにエクスポートする方法【UIアクション】

:::note info
この記事はServiceNowアドベントカレンダー2024の12月22日分の記事として執筆しています。
:::
https://qiita.com/advent-calendar/2024/servicenow

# はじめに
今回は、本記事タイトルのカスタマイズ機能の実装について、備忘録 & 情報共有のために書いていこうと思います。

:::note alert
ServiceNowは基本的にOOTBで運用することが推奨されています。
カスタマイズの実装は最終手段としてお考えください。
:::


# 何を目的に、何をしたのか

### 背景と目的

今回は例として、カタログアイテム経由で送られた要求から、担当者が必要な情報を名刺業者へ共有する作業を想定します。
今まで手作業で行われていた必要情報の書き出しを自動化することで効率化を目指します。

### 課題

要求アイテムのフォームからカタログ変数のみをエクスポートする方法は標準機能ではありません。
[Exporting Catalog Variables](https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0862598)

カタログ変数を含めた要求アイテム全体をPDFにエクスポートすることはできますが、不要な情報が含まれるため、名刺業者へ共有することには適しません。
[Is it possible to export all fields including 'variables' on Request Item form?](https://www.servicenow.com/community/platform-analytics-forum/is-it-possible-to-export-all-fields-including-variables-on/m-p/1277165)

### 解決策

カタログ変数のみをエクスポートする機能を実装する。

# 実装した機能のデモ

![従業員センターのフォーム画面 2024-12-03 130913.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/7165c105-2cd6-7ced-986b-90f5844b10c9.png)
従業員センターから、上記内容で申請が送られてくることを想定します。

　
![RITMフォーム画面でコンテキストメニューを開いている状態.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/b65b0f61-b219-9578-24ea-986d221a78fb.png)
要求アイテムのフォーム画面にUIアクションを追加しています。フォームコンテキストメニューから実行します。

　
![カタログ変数ファイルが添付された画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/e237d840-a19a-b126-0114-48116619f092.png)
UIアクションを実行することで、カタログ変数をCSV、PDF形式にエクスポートして要求アイテムに添付ファイルとして設定することができます。

　
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/be04bc3d-1899-1b8e-2438-6180a395e54f.jpeg" alt="空欄なし_catalog_variables_RITM0010009_page-0001" width="340px">　 <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/89c264cf-dfa8-3fab-09d8-e6c4ce5f400b.jpeg" alt="役職空欄_catalog_variables_RITM0010009_page-0001" width="340px">
実際にエクスポートされたPDFです。右側はカタログ変数の役職の値を空欄にした場合です。（モバイル表示だと下側）
値が空欄の場合、その項目はエクスポートされないよう実装しています。

　
![CSV空欄有り無し縦並び_catalog_variables_RITM0010009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/324a998f-6aa1-e770-581e-53b11933930e.png)
CSVも同様に、値が空欄の場合その項目はエクスポートされません。

# 実装

:::note warn
インスタンスのバージョンはXanaduです。
:::

### 機能要件

- 要求アイテムのフォームから、オンデマンドでカタログ変数の情報のみエクスポートができること
- エクスポート形式はCSV、PDFから選択できること
- 不要な情報をエクスポートしないこと

### 実装内容

![UIアクション設定画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220967/b62604cb-d26f-8cac-3ba5-26e30faaeb34.png)
UIアクションの各種設定は上記画像の通りです。CSV、PDFともに設定内容は同じです。

##### スクリプト：PDFへのエクスポート

PDFの見出しは仮で要求アイテム番号を設定しています。
PDFはHTMLで構成を変えられるので、要望にあわせた柔軟なエクスポートが可能です。

```javascript:カタログ変数をPDFファイルとして添付.js
(function executeAction(current, gs, action) {
    try {
        // RITMのsys_id取得
        var ritmSysId = current.sys_id;

        // カタログ変数の取得
        var variables = getCatalogVariables(ritmSysId);
        if (!variables || variables.size() === 0) {
            handleNoVariablesFound(current, action);
            return;
        }

        // HTML構造生成
        var htmlContent = generateHtmlContent(variables);

        // HTMLをPDFに変換して現在のレコードに添付
        generateAndAttachPdf(htmlContent);

    } catch (e) {
        handleError(e);
    } finally {
        redirectToCurrentRecord();
    }

    /**
     * カタログ変数を取得
     * @param {string} requestId - リクエストアイテムのSys ID
     * @returns {GlideRecord} カタログ変数のリスト
     */
    function getCatalogVariables(requestId) {
        var set = new GlideappVariablePoolQuestionSet();
        set.setRequestID(requestId);
        set.load();
        return set.getFlatQuestions();
    }

    /**
     * カタログ変数がない場合の処理
     */
    function handleNoVariablesFound() {
        gs.addErrorMessage("表示されているカタログ変数が見つかりませんでした。");
        var currentUrl = current.getTableName() + ".do?sys_id=" + current.sys_id;
        action.setRedirectURL(currentUrl);
    }

    /**
     * HTMLを生成
     * @param {GlideRecord} variables - カタログ変数のリスト
     * @returns {string} 生成されたHTMLコンテンツ
     */
    function generateHtmlContent(variables) {
        var htmlContent = `
        <html>
        <head>
        <style>
            @page {
            body { line-height: 1.6; margin: 0; padding: 20px; }
            h1 { color: #333; margin: 0 0 20px; }
            .variable-editor { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
            .variable-editor th, .variable-editor td { border: 1px solid #ddd; padding: 8px; }
            .variable-editor th { background-color: #eaeaea; text-align: left; } /* グレーの背景色を追加 */
            }
        </style>
        </head>
        <body>
        <h1>Requested Item: ${current.number}</h1>
        <table class="variable-editor">
        <thead>
        <tr>
            <th>項目</th>
            <th>値</th>
        </tr>
        </thead>
        <tbody>`;

        for (var i = 0; i < variables.size(); i++) {
            var variable = variables.get(i);

            // 空の変数はスキップ
            if (!variable.getDisplayValue()) {
                continue;
            }

            var label = variable.getLabel();
            var value = variable.getDisplayValue();
            htmlContent += `<tr><td>${label}</td><td>${value}</td></tr>`;
        }

        htmlContent += `
        </tbody>
        </table>
        </body>
        </html>`;
        return htmlContent;
    }

    /**
     * HTMLをPDFに変換して現在のレコードに添付
     * @param {string} htmlContent - PDFに変換するHTMLコンテンツ
     */
    function generateAndAttachPdf(htmlContent) {
        var fileName = "catalog_variables_" + current.number;
        var pdfDoc = generatePdf(htmlContent, fileName);

        if (pdfDoc) {
            gs.addInfoMessage("PDFファイルが添付されました: " + fileName + ".pdf");
        } else {
            gs.addErrorMessage("PDFファイルの生成に失敗しました。");
        }
    }

    /**
     * HTMLからPDFを生成
     * @param {string} htmlContent - PDFに変換するHTMLコンテンツ
     * @param {string} fileName - 生成するPDFのファイル名
     * @returns {Object} 生成されたPDFドキュメント
     */
    function generatePdf(htmlContent, fileName) {
        var pdfUtil = new sn_pdfgeneratorutils.PDFGenerationAPI();
        var targetTable = current.getTableName();
        var targetTableSysId = current.sys_id;
        return pdfUtil.convertToPDF(htmlContent, targetTable, targetTableSysId, fileName);
    }

    /**
     * エラー処理
     * @param {Error} error - 発生したエラー
     */
    function handleError(error) {
        gs.error("Error in UI Action: " + error.message);
        gs.addErrorMessage("エラーが発生しました: " + error.message);
    }

    /**
     * UIアクションクリック時に表示していたレコードにリダイレクト
     */
    function redirectToCurrentRecord() {
        var redirectUrl = current.getTableName() + ".do?sys_id=" + current.sys_id;
        action.setRedirectURL(redirectUrl);
    }
})(current, gs, action);

```
　
##### スクリプト：CSVへのエクスポート

```javascript:カタログ変数をCSVファイルとして添付.js
(function executeAction(current, gs, action) {
    try {
        // RITMのsys_id取得
        var ritmSysId = current.sys_id;

        // カタログ変数の取得
        var variables = getCatalogVariables(ritmSysId);
        if (!variables || variables.size() === 0) {
            handleNoVariablesFound();
            return;
        }

        // CSVの生成
        var csvData = generateCSVData(variables);

        // CSVを現在のレコードに添付
        attachCSVToRecord(csvData);

    } catch (e) {
        handleError(e);
    } finally {
        redirectToCurrentRecord();
    }


    /**
     * カタログ変数を取得
     * @param {string} requestId - リクエストアイテムのSys ID
     * @returns {GlideRecord} カタログ変数のリスト
     */
    function getCatalogVariables(requestId) {
        var set = new GlideappVariablePoolQuestionSet();
        set.setRequestID(requestId);
        set.load();
        return set.getFlatQuestions();
    }
    /**
     * カタログ変数がない場合の処理
     */
    function handleNoVariablesFound() {
        gs.addErrorMessage("表示されているカタログ変数が見つかりませんでした。");
        var currentUrl = current.getTableName() + ".do?sys_id=" + current.sys_id;
        action.setRedirectURL(currentUrl);
    }

    /**
     * CSVを生成
     * @param {GlideRecord} variables - カタログ変数のリスト
     * @returns {string} 生成されたCSV
     */
    function generateCSVData(variables) {
        var headers = [];
        var values = [];

        for (var i = 0; i < variables.size(); i++) {
            var variable = variables.get(i);

            // 空の変数はスキップ
            if (!variable.getDisplayValue()) {
                continue;
            }

            headers.push(variable.getLabel());
            values.push(variable.getDisplayValue());
        }

        if (headers.length === 0) {
            return null;
        }

        var csvHeaderRow = headers.join(",");
        var valueRow = values.join(",");
        return "\uFEFF" + csvHeaderRow + "\n" + valueRow; // UTF-8 BOM付き
    }

    /**
     * CSVを現在のレコードに添付
     * @param {string} csvData - CSVデータ
     */
    function attachCSVToRecord(csvData) {
        var attachment = new GlideSysAttachment();
        var fileName = "catalog_variables_" + current.number + ".csv";

        var attachmentSysId = attachment.write(current, fileName, "text/csv", csvData);

        if (attachmentSysId) {
            gs.addInfoMessage("CSVファイルが添付されました: " + fileName);
        } else {
            gs.addErrorMessage("CSVファイルの添付に失敗しました。");
        }
    }

    /**
     * エラー処理
     * @param {Error} error - 発生したエラー
     */
    function handleError(error) {
        gs.error("Error in UI Action: " + error.message);
        gs.addErrorMessage("エラーが発生しました: " + error.message);
    }

    /**
     * UIアクションクリック時に表示していたレコードにリダイレクト
     */
    function redirectToCurrentRecord() {
        var redirectUrl = current.getTableName() + ".do?sys_id=" + current.sys_id;
        action.setRedirectURL(redirectUrl);
    }
})(current, gs, action);

```

# おわりに

この記事では、要求アイテムに紐づけられたカタログ変数を、UIアクションを使ってCSV、PDF形式でエクスポートする機能を実装した例をご紹介しました。
カタログ変数のエクスポートは、需要はあるものの情報が少なく、実装に苦労しました。
最後に、この記事が読者の皆さんの実装のヒントや課題解決の手助けとなれば幸いです。もしご質問やフィードバックがございましたら、ぜひコメント欄でお知らせください。
OOTBで同等の機能を実装する方法も頭の中ではイメージ出来てるので、いつか続編を書きます！
# 参考文献

[Generate csv file with the catalog variables and attaching to RITM record](https://www.servicenow.com/community/developer-blog/generate-csv-file-with-the-catalog-variables-and-attaching-to/ba-p/2279044)
[Generating Custom PDFs - using the new PDFGenerationAPI](https://www.servicenow.com/community/developer-articles/generating-custom-pdfs-using-the-new-pdfgenerationapi/ta-p/2318979)