2021/05/06

MANUAL

FAQ:稟議申請などのワークフロー(承認プロセス)を実装する

## 説明 稟議申請などで用いられる承認ワークフロー(プロセス管理)機能を、サーバスクリプトを用いて実装します。 当ページでの説明は、申請者が所属している組織(部署)の、決裁者グループに所属しているユーザが承認処理を行うものとし、幹部社員はグループID:3、上級幹部社員はグループID:4となります。1,000,000円を超える稟議申請の場合、決裁者は役員となり、役員は組織ID:2に所属しているものとします。 ※ここで説明するワークフロー機能を実装したテーブルは、サイトパッケージを公開しています。 以下のページで、データのダウンロード、および使用手順を説明しております。 [FAQ:稟議申請ワークフローの例のサイトパッケージをインポートする](/manual/faq-server-script-workflow-site-package) ## 概要 申請者の所属部署、申請金額に応じて承認ルートを自動で設定し、承認プロセスの管理を行います。 決裁者は、申請金額が200,000円未満の場合は申請者が所属する部署の幹部社員のみ。200,000円以上、1,000,000円未満の場合は幹部社員に加えて上級幹部、1,000,000円以上の場合は更に役員の承認が必要となる承認ルートが設定されます。 稟議申請が登録された時点で、該当する決裁者にはメールにて通知が送られます。 処理のフローとしては以下となります。 1. 申請者の所属部署・申請金額から決裁ルートを自動設定 1. 決裁者へのメール通知 1. 承認時のプロセス更新(次の決裁者宛に更新) 1. 承認された申請を読み取り専用として更新 ![image](/binaries/cd598f23a9c44774910395ba57bc82cb) ## スクリプト構成 ワークフロー機能の実装は以下6つのサーバスクリプトで構成されます。 |タイトル|条件| |:--|:--| |決裁ルートの自動選択|レコード読み込み時| |承認時のプロセス更新|更新前| |自動メール通知|更新後| |決裁ルートに伴う画面表示|画面表示の前| |承認・否決ボタンの設置|画面表示の前| |完了したものは読み取り専用|画面表示の前| ![image](/binaries/7704b746233f47a79057045431c54279) ## 決裁ルートの自動選択 条件:レコード読み込み時 ### 処理の流れ • 入力された金額によって承認ルートを設定します。  →modelオブジェクト • 幹部社員の所属するグループから、申請者が所属する組織の幹部ユーザを取得します。  →groupsオブジェクト、GetMembersメソッド • 役員の承認が必要なルートの場合、役員の組織から役員ユーザを取得します。  →deptsオブジェクト、GetMembersメソッド • 取得したユーザのIDを各承認者の分類項目にセットします。  →modelオブジェクト • 最終承認時のステータスコードをメモリ上に保持します。  →context.UserData.FinCodeプロパティ ``` try { context.Log('決裁ルートの自動選択'); if (model.NumA < 200000) { context.UserData.FinCode = 300; model.ClassB = manager(context.DeptId, 3).UserId; } else if (model.NumA < 1000000) { context.UserData.FinCode = 400; model.ClassB = manager(context.DeptId, 3).UserId; model.ClassC = manager(context.DeptId, 4).UserId; } else { context.UserData.FinCode = 900; model.ClassB = manager(context.DeptId, 3).UserId; model.ClassC = manager(context.DeptId, 4).UserId; model.ClassD = officer().UserId; } function manager(deptId, groupId) { let members = Array.from(groups.Get(groupId).GetMembers()); for (let i = 0; i < members.length; i++) { if (members[i].UserId > 0) { let user = users.Get(members[i].UserId); if (user.DeptId === context.DeptId) { return user; } } } } function officer () { let users = Array.from(depts.Get(2).GetMembers()); for (let i = 0; i < users.length; i++) { return users[i]; } } } catch (e){ context.Log(e.stack); } ``` ## 承認時のプロセス更新 条件:更新前 ### 処理の流れ • ユーザが操作したボタンのID属性を取得して分岐します。  →context.ControlIdプロパティ • 次のプロセスのステータスコードをセットします。  →model.Statusプロパティ • 次のプロセスの決裁者をセットします。  →model.Managerプロパティ • 決裁した日をセットします。  →model.DateXプロパティ • 最終承認が行われた場合には現在位置を申請者に戻します。  →最終承認はcontext.UserData.FinCodeで確認 ``` try { context.Log('承認時のプロセス更新'); switch (context.ControlId) { case 'Approval': model.Status = 200; model.Manager = manager(model.ClassB); break; case 'ApprovalM1': model.Status = status(300); model.Manager = manager(model.ClassC); model.DateB = new Date(); break; case 'ApprovalM2': model.Status = status(400); model.Manager = manager(model.ClassD); model.DateC = new Date(); break; case 'ApprovalM3': model.Status = status(900); model.DateD = new Date(); break; } if (model.Status === 900) { model.Manager = model.Owner; } function status(code) { if (context.UserData.FinCode === code) { return 900; } else { return code; } } function manager(userId) { var user = users.Get(userId); if (user) { return user.UserId; } else { return model.Manager; } } } catch (e) { context.Log(e.stack); } ``` ## 自動メール通知 条件:更新後 ### 処理の流れ • 通知用のオブジェクトを作成します。  →notifications.New()メソッドで作成 • 現在のプロセスのユーザIDを指定しメールアドレスを取得します。  →notification.Address = ‘[User’ + user.UserId + ‘]’ • 承認依頼、可決、否決のプロセスで分岐しメッセージを設定します。  →model.Statusプロパティ • タイトル、本文を指定して通知メールを送信します。  →notification.Title、notification.Body、 notification.Send() ``` try { context.Log('自動メール通知'); let user = users.Get(model.Manager); let notification = notifications.New(); if (user.UserId > 0) { notification.Address = '[User' + user.UserId + ']'; switch (model.Status) { case 200: case 300: case 400: notification.Title = '承認依頼:' + model.Title; notification.Body = user.Name + '殿:掲題の承認依頼が届いています。'; notification.Send(); break; case 900: notification.Title = '可決:' + model.Title; notification.Body = user.Name + '殿:掲題の申請が可決されました。'; notification.Send(); break; case 920: notification.Title = '否決:' + model.Title; notification.Body = user.Name + '殿:掲題の申請が否決されました。'; notification.Send(); break; } } } catch (e){ context.Log(e.stack); } ``` ## 決裁ルートに伴う画面表示 条件:画面表示の前 ### 処理の流れ • 最終承認時のステータスコードで分岐します。  →context.UserData.FinCodeプロパティ • 承認に不要な決裁レベルの入力欄「[セクション](/manual/table-management-tab-and-section)」を非表示にします。  →siteSettings.Sections[n].Hide = true ``` try { context.Log('決裁ルートに伴う画面表示'); switch (context.UserData.FinCode) { case 300: siteSettings.Sections[3].Hide = true; siteSettings.Sections[4].Hide = true; break; case 400: siteSettings.Sections[4].Hide = true; break; case 900: break; default: siteSettings.Sections[2].Hide = true; siteSettings.Sections[3].Hide = true; siteSettings.Sections[4].Hide = true; } } catch (e){ context.Log(e.stack); } ``` ## 承認・否決ボタンの設置 条件:画面表示の前 ### 処理の流れ • 現在のプロセスで分岐します。  →model.Statusプロパティ • 現在の承認レベルの欄にボタンを表示します。  →columns.ColumnName.ExtendedHtmlAfterFieldプロパ ティ • HtmlにボタンのID、Confirm、表示名、アイコンを入力します。 ``` try { context.Log('承認・否決ボタンの設置'); if (model.Manager === context.UserId) { switch (model.Status) { case 100: if (context.Action !== 'new') { columns.AttachmentsA.ExtendedHtmlAfterField = buttons('', '申請'); } break; case 200: columns.DescriptionB.ExtendedHtmlAfterField = buttons('M1', '幹部承認'); break; case 300: columns.DescriptionC.ExtendedHtmlAfterField = buttons('M2', '上級幹部承認'); break; case 400: columns.DescriptionD.ExtendedHtmlAfterField = buttons('M3', '役員承認'); break; } } function buttons(suffix, text) { let html = '<div class="approval-control"><button id="Approval' + suffix + '" class="button button-icon validate" type="button" onclick="if (!confirm(\'' + text + 'してよろしいですか?\')) return false;$p.send($(this));" data-icon="ui-icon-circle-triangle-e" data-action="Update" data-method="put">' + text + '</button></div>'; if (suffix !== '') { html += '<div class="approval-control"><button id="Veto' + suffix + '" class="button button-icon validate" type="button" onclick="if (!confirm(\'否決してしてよろしいですか?\')) return false;$p.send($(this));" data-icon="ui-icon-circle-close" data-action="Update" data-method="put">否決</button></div>'; } return html; } } catch (e){ context.Log(e.stack); } ``` ## 完了したものは読み取り専用 条件:画面表示の前 ### 処理の流れ • 現在のプロセスで完了をチェックします。  →model.Statusプロパティ • 完了の場合、読み取り専用をオンに設定します。  →model.ReadOnly = true ``` try { context.Log('完了したものは読み取り専用'); if (model.Status === 900) { model.ReadOnly = true; } } catch (e) { context.Log(e.stack); } ``` ## 注意事項 こちらは[サーバスクリプト](/manual/table-management-server-script)機能で使用するメソッドです。[スクリプト](/manual/table-management-script)機能では使用できません。 ## 関連情報 ・[FAQ:稟議申請ワークフローの例のサイトパッケージをインポートする](/manual/faq-server-script-workflow-site-package) ・[オブジェクトごとの実行タイミング](/manual/server-script-conditions) ・[siteSettingsオブジェクト](/manual/server-script-site-settings) ・[contextオブジェクト](/manual/server-script-context) ・[itemsオブジェクト](/manual/server-script-items) ・[modeオブジェクトl](/manual/server-script-model) ・[api_modelオブジェクト](/manual/server-script-api-model) ・[notificationsオブジェクト](/manual/server-script-notifications) ・[FAQ:サーバスクリプトのログを出力する](/manual/faq-server-script-log)
このページをシェアする