yonet77的な雑記帳

日々思いついたネタなどを書き留めておきます

SalesforceでExtJS4を使ってみようか (1)

今日もForce.comネタです。すみません。

Salesforceの画面で、中を斜め読みでもしていると分かったりしますが、どうやらExtJSを色々利用しているみたいです。
このJavascriptライブラリ、最新版のExtJS4 ではMVCアーキテクチャを採用していて、なかなか使い勝手は良いと思います。他にも backbone.js など、様々なフレームワークがありますが、ExtJSを昔から使っています。なんとなくw
※ExtJS4の概要や基本的な使い方については、以下を参照下さい。。
Getting Started
MVC Architecture

そんなExtJS4を、Salesforce上で自分でも自由に使えないかなー・・と思って色々試してみました。
ExtJSSalesforce上で利用する上で、データをどうやってやり取りするか!?というところで悶え苦しんだあたりを今回は書き留めておこうと思います。

SalesforceのRemoteActionを利用するためのproxyを作成する

サーバ側(ココではSalesforce)からデータを参照したり、データを作成/更新/削除するのに、REST APIを利用するのは最も簡単で早く実装できる・・と思います。
Salesforceでは、REST APIを利用できるようになってはいますが、REST APIでアクセスする回数に制限(!!)があります。
「組織プロファイル」のAPI要求数で確認できます。
f:id:yonet77:20120219153629p:plain

※ちなみに、Sandbox環境では、5,000,000 に対して、運用環境では、5,000 もしくはユーザ数×1,000で大きい方が採用されます。
この数字、逆にして欲しいと願ってやまない今日この頃です。というか、そもそももっと増やして欲しい。

このAPI要求数、金出せば上限を増やすことはできるので、潤沢なAPI数が用意されているのであればREST APIで接続すれば良いと思います。
ただ、(資金が十分になくて)APIを好きなだけ使えない場合、どうすれば良いか・・・??
ということで、今回は「Remote Action」を使ってみました。

大雑把にいうと、

  1. Salesforce側でRemoteActionを用意しておく
  2. Javascript側から、SalesforceのRemoteAcitonを呼び出して、データの参照/作成/更新/削除を実行する

という感じです。

ただ、Javascriptに明るくないので、SalesforceのRemoteActionをコールするExtJS用のproxyを用意するのはどうすればッ・・・!?
※結論から言うと、見よう見真似で作ってみたので、一応動いてるけど、ちゃんと理解しないとどっかで痛い目みそうですねー・・というワケで、先に言っておこうと思います。
ぎゃふん (/++)/

ExtJSで近いProxyクラスは "Ext.data.proxy.Ajax" かなー・・というあたりで、これをコピーして適当(?)にちょちょいと書き換えました。。
(ホントは「ちょちょいと」なんて感じではなかったんですが、その四苦八苦の様子はそのうち整理して書けたら。。)
こんな感じです。↓

"remoteActionName" で、Salesforce側で用意してるRemoteActionを指定します。
"extraParams" で、RemoteActionに渡すパラメータをハッシュで指定します。

そいでもって、Salesforce側でRemoteActionを例えばこんな感じに定義します。

global with sharing class Con_RemoteAction {

    @RemoteAction
    global static List<sObject> doLoadData(Map<String, String> params){
        String query = params.containsKey('query') ? params.get('query') : '';
        return Database.query(query);
    }
}

元々、RemoteActionでは引数の数は自由に指定できます。

global with sharing class Sample {
    @RemoteAction
    global static void doTest(String str1, String str2, String str3){
        ....
    }
}

という感じに、例えば引数を3つ指定することもできます。
この場合、Javascriptからは

Sample.doTest('aaa','bbb','ccc', function(res,event){
    ....
}, {escape:true});

という感じに呼び出すことができます。
が、RemoteAction毎に引数の数がバラバラだと、Javascriptから呼び出すとき、どうやって1つのproxyクラスで対応するのか解決策が見つかりませんでした。。。
(RemoteAction毎にproxyクラスを作成する・・なんて面倒なことはやりたくないので。。)
そんなわけで、引数をハッシュとして持たせて、引数を常に1つとなるようにしてみました。

Javascript側で、こんな感じでproxyを作成します。

...
proxy:{
  type: 'remoteaction',
  remoteActionName: Con_RemoteAction.doLoadData,
  extraParams: {'query' : 'Select Id From Account'},
  reader: {
    type: 'json',
    model: '(ExtJS側で自前で作成したモデル名を指定)'
  }
},
...

そうすると、

  1. ExtJSのproxyが、Con_RemoteAction.doLoadData({'query' : 'Select Id From Account'}) をコールする
  2. Salesforce側で、変数queryに 'Select Id From Account' を格納する
  3. Salesforce側で、Database.query(query) を実行して結果を返す
  4. ExtJSのproxyで、受け取ったレコードを処理する(me.processResponse(...) とあるところ)

という流れになります。
Javascript側の処理の流れ(me.processResponse...)については....ちょっと追い切れてない、というか理解が追いついてないので省略(!?)しちゃってますが、そのうち整理できるといーなーと考えてます。。

GridPanelのDataStoreや、TreePanelのDataStoreに、上記のproxyを設定すればGrid / TreePanelにデータが表示されるかと。
Salesforceの画面に飽きてきたら、ExtJSのようなJavascriptフレームワークを使って全く違う画面を作ってみるのも一興かもしれません、というお話でした。

Glovia Order Management (on Salesforce) を使ってみようか (3)

前回の続きで、もう少しシステムポリシーをイジってみます。

ドキュメントページ設定 について

システムポリシー詳細画面に「ドキュメントページ設定」というセクションがありますが、何を設定して良いのやらよく分かりませんでした。。

f:id:yonet77:20120218224853p:plain

一応ヘルプを見るとこんな感じに書かれています。

販売見積書、請求書等の帳票としてカスタム帳票(Visualforce Page)を作成した場合、対応する各ページにカスタム帳票を指定することで、システムからの出力がカスタム帳票に切り替わる。標準の帳票を使用する場合は、各ページの指定を空白とする。

なるほど。自前で帳票用のページを作成すれば、それを指定することもできるということか。
・・というわけで、自前でVisualforce Pageを作成して、ページ名を埋め込んでみたけれど...エラー。
f:id:yonet77:20120219232216j:plain

おいおい、どういうことだッ・・・!?と色々と問い合わせたところ、

ページ名を埋め込む際、先頭に "c__" を付与する必要がある

ということが分かりました。(せめてヘルプに書いておいて欲しかったな。。)
というわけで、気を取り直してこんな感じに直したら、確かに差し替えられました!
f:id:yonet77:20120219232916j:plain


ちなみに、PDF用のVisualforce Pageを自作する際には、StandardController(標準コントローラ)に販売オーダーや購買オーダー等、帳票を差し替えたいgOMのオブジェクトを指定して、必要に応じて拡張コントローラを用意しておけば大丈夫です。
gOMでは、販売オーダー、購買オーダーの画面で、PDFを添付する/送付する機能が標準で実装されていて、なかなか良さげです。
それを利用するにはPDFの差し替えは必須となりますが、こんな注意が必要ですよー、というお話でした。

Glovia Order Management (on Salesforce) を使ってみようか (2)

前回の続き・・ということで。
システムポリシーで設定しておくと良いかなーと思ったことで、「消費税」について書き留めておきます。

消費税の設定

gOMでは商品の単価は、基本的に「税抜」として扱ってます。そのため金額計算時には消費税計算を考慮する必要があります。
(課税対象であれば、ですが。まぁ、大抵課税対象ですよね。。)

gOMでは税額計算機能も、もちろん備えてます。

でも、システムポリシーとかみても、「消費税」というキーワードは全くないので、どこで設定すれば良いのか・・?と分からなくなりますよね。というか、自分は分かりませんでした。。。てへぺろ

で、結局どこで設定するのか?というと、「VAT(Value-Added Tax、付加価値税)」を使います。
ココでも紹介されているように、消費税は欧米でいうところのVATに相当するみたいです。

VATレコードの作成

gOMの「コードメンテナンスタブ」→「VAT」タブから作ります。
f:id:yonet77:20120217183106p:plain

VATレコードを作った後、そのままVAT税率も作ります。VAT税率に実際の税率(今だと5%)を登録します。
これは税率が変わっても対応できるように、「VAT」という枠と「VAT税率」という税率を格納するオブジェクトを親子関係で結んでいるのだと思います。
出来上がると、こんなイメージかと・・・(作成後の画面です。。)
f:id:yonet77:20120217183438p:plain

実際試してないですが、gOMでは有効日が最新のVAT税率を使ってくれる・・と思います。

消費税用のVATをデフォルト設定にする

上記で作成したVATを自動で使ってもらうよう、システムポリシーに設定します。
システムポリシーの画面で、以下2つの項目を設定します。

  • 「VATを有効化する」にチェックを入れる
  • 「デフォルトVAT」に作成したVATを設定する

f:id:yonet77:20120217185554p:plain



設定は以上になります。
こんな感じで設定しておくと、販売オーダーの明細(所謂、受注明細)を作ったときに、「課税情報」のVAT項目に上で作成した消費税用のVATが自動セットされ、VAT税額として消費税額が計算されるようになります。
(こんな感じです)
f:id:yonet77:20120217185953p:plain

予め設定しておいた方が良いかなーと思う「消費税」の設定についてでした。


ちょっと面倒な問題点など

ただ、gOMの仕組み上、面倒な問題点があります。(以下、実際に遭遇した問題点です。。)

1. 小数が発生したときに、切り上げ/切り捨て/四捨五入してくれない
元々、米国発祥であるあたりから、金額項目は小数第二位まで指定できるようになっています。
※商品原価はもっと細かく指定できますが。

そのため、「2.50」といった小数付きの消費税額が計算されてしまいます。
おそらく、取引先マスタの方に端数計算処理のパターン(切り上げ/切り捨て/四捨五入)を登録しておいて、税額計算する際に端数処理するようトリガを仕込む・・といった感じになると思います。


2. 消費税を計算する単位が「明細」単位に固定されている
企業によっては、税額計算する単位が

  1. 明細単位
  2. 注文単位
  3. 締単位

と分かれるケースがあると思いますが、それに対応できません。。。
特にgOMでは「締」という概念がないため、「締単位」で税額を計算する、といった話は拡張する感じですかねー。
(そのあたりの話も今後できたらいーなーと考えてますが。。)


「1.」のあたりとかは、日本向けにローカライズするときには標準機能として備わっていて欲しいところですね。
今後のgOMに期待!
(次回は未定です)

Glovia Order Management (on Salesforce) を使ってみようか (1)

Hatena Blog に移行してみました。古い記事は(ほとんどないけど)こちらになります。

最近、お仕事でGlovia Order Management (on Salesforce)をカスタマイズすることが多いので、備忘録として自分がハマったポイントなど書き溜めておこうと思います。

 

  • 前提

CRM機能を利用する、という前提にしてます。CRMを使わずにGloviaのみ使う・・という場合は「商談」が使えないくらいかと思います。

 

  • インストールしてから利用できるようになるまで

インストールしてからいくつか設定しないと、そのままでは使えないのです。

以下、必要最低限(と思う)作業を紹介します~。

 

  • 日本語の有効化

設定画面上で、「管理者設定」→「トランスレーションワークベンチ」→「翻訳設定」に移動します。

翻訳設定を「有効化」します。

※元々、Glovia Order Management(以下、gOM)は米国発祥のパッケージなのでオブジェクトの名称や項目名は全て英語表気になっています。

もちろん、日本語化することで日本語として利用できますので、そのための設定を行う感じです。 

 

  • システムポリシーの作成

gOMの設定を司るオブジェクトのレコードを作成します。

このデータがgOMのシステム設定という感じです。

 

作り方は

「コードメンテナンス」タブ→「システムポリシー」タブ→「システムポリシーを生成」をクリック

という順番です。

これでデフォルト設定のシステムポリシーが出来上がります。

 

  • 取引先関連情報の作成

「取引先関連情報」はgOM側で管理している取引先マスタになります。CRM側の取引先と1:1で対応していて、CRM側の取引先を拡張したオブジェクト・・というイメージです。

なお、CRM側の取引先と1:1であることが前提でプログラムが作られているので、こうなっていない場合、何か処理したときにエラーが発生したりします。

あと、Salesforce標準のの「取引先のマージ」を利用すると、マージしたCRM側の取引先に「取引先関連情報」が複数紐付くようになるので、後で削除する等の処置が必要です。(要注意です)

 

作り方は

「コードメンテナンス」タブ→「システムポリシー」タブ→「取引先関連情報を生成」をクリック

という順番です。

これで、既に作成しているCRM側の取引先に対して、1件の取引先関連情報が作成されます。

※新たに取引先を作成した場合は、取引先関連情報は自動生成されます。

  

  • 商品関連情報の作成

取引先関連情報と同様に、gOM側で管理している商品マスタに該当します。CRM側の商品と1:1で対応していて、CRM側の商品を拡張したオブジェクト・・というイメージです。

これも1:1であることが前提となっています。

 

作り方は

「コードメンテナンス」タブ→「システムポリシー」タブ→「商品関連情報を生成」をクリック

という順番です。

これで、既に作成しているCRM側の商品に対して、1件の商品関連情報が作成されます。

※新たに商品を作成した場合は、商品関連情報は自動生成されます。

 

ちなみに、上記3つの設定は以下の画面で行います。(システムポリシーは既に作成済の画面です)

 ↓

f:id:yonet77:20120216232819p:plain

 

  • gOMで追加しているボタンの追加など

CRM側の商談から販売オーダーを作成するための変換機能が元々備わっています。

「カスタムボタン」という形で提供されているので、商談オブジェクトのページレイアウトを編集して、gOMが追加しているボタンをレイアウトに追加します。

※この他、関連リストも適宜追加すると良いでしょう。

 

 

最低限必要な設定はこんなところでしょうか。。。

とりあえずこれで販売オーダーを作る、等の基本機能が利用できるかと思います。

※ちなみに、gOMインストール後にメールで自動送信される設定ガイドに詳しく書かれてます。

 

 

実際に利用する上で、こういう設定しておいた方が良いかなー・・というモノは次回に。。

(例えば、消費税の設定とか)

無題

明日から2月になるという。早いものだ。
ふと帰り途中に思い出したけど、2/1は中学受験する小学生にとっては重要な日だったなぁ。

ちなみに、自分も2/1に中学受験したクチだ。
ただ、がっつり落ちた。。(苦笑)
そして父親が家に結果を電話で話している間に思わず泣いてしまった。
祖母がガッカリするだろうなー・・と思って思わず涙を流した。
受験勉強するにあたって、もちろん親もそうだけど、祖母にも色々サポートしてもらったことをやたらと気にしてたような気がする。
そんなわけで、祖母をガッカリさせるようなことをしてしまった自分が情けなくて思わず涙を流した・・という流れだった。
(と思う)

今から20数年前になるという事実に衝撃を受けたけど、意外に覚えてるもんだなー・・。

そんな2012年1月最終日。
今年も残すところ、11ヶ月。まだまだ続きます。

(追記)
祖母が他界して約1年。時はあまりにも早く流れることを実感した1年だったなー。
20数年前は、祖母がいなくなるなんて想像すらできなかったけど、こうして現実になると・・なんとも言葉で表現できん。
天国にて、じい様と仲良く楽しく過ごして欲しいとお祈りします。

2012年を迎えて

新年迎えたことですし、何か決意的なことでも書き残そうかと思います。

2011年ダイジェスト

新たな転機を迎えた年でしたし、色んなことがあり過ぎてお腹一杯になりそうな年でした。

1-3月:

  • 身内を亡くして久々に号泣した。やっぱり家族は大事なのです。
  • 人生初の大地震。品川から自宅まで徒歩で帰るのがしんどかった。。家族が無事で何よりだった。
  • 前の年に人生初のインフルエンザ予防接種を受けたのに、人生初のインフルエンザにかかった。インフルエンザの恐ろしさを体験した。

4-6月:

  • 職は変えないけど、会社は変わった。最初はどうなることかと思ったが、今も思ってる。
  • 開発マシンがMBPになった。Macは慣れてなかったけど、頑張って使い始めた。

7-9月:

  • Force.com上でのカスタマイズや、Ruby on Rails を使ったシステムの開発を淡々(?)と続けた。
  • 最後のRubyKaigiが近所だったのもあり、参加して楽しかった。そして、非常に勉強になった。
  • Scala会議にも参加できて楽しかった。

10-12月:

  • Cloudforce2011に出展した。パンフも用意しないというやる気レスな雰囲気があったが、思ったよりは人が来たと思う。
  • 相変わらず、Force.comとRuby on Railsの開発を続けていた。
  • Scala会議 #2 も楽しかった。非常に勉強になった。
  • Force.com Advent Calendar 2011 : ATNDにも参加した。初めてAdvent Calendarに参加する機会を得た。


2011年は、一介の開発者として再スタートを切った年だったように思う。
RubyKaigiやScala会議、その他Rails勉強会やPlay! framework勉強会等に参加できて、非常に影響を受けた。
また、開発者としてのマインド(心構え)や、開発スタイル、言語...等、学ぶことは多すぎるし、前途多難な感じでクラクラするけど、開発者という観点での世界が広がり、ワクワク感は高まったと思う。

2012年に向けて

今さらだけど...これから先、やっぱり一介の開発者として精進していくことになると思う。
開発者としてのマインドや技術の向上は引き続き継続していくとして、何かサービスを生み出すことに少しこだわっていきたいと思う。
それは小金を稼ぐ...というよりも、何かしらサービスを生み出すときのプロセスを(たとえゆっくりだとしても)こなしていくことで、"歌って踊れる開発者" に近づけるんじゃないかなーと思う。

とはいっても、やっぱり好きなことしながらお金をもらうのが一番だよなぁ...
そんな人生を目指したい、と思った年明けでしたw

Force.comでのTrigger Utility 的なモノとか

またもや Force.com 的なネタで。
Force.com をさわったことのある人なら分かると思いますが、主従関係を結んだオブジェクトの親側では、子オブジェクトの数値項目を「積上げ集計」できる機能が標準装備されています。
子オブジェクト側の数値項目を自動で集計してくれるあたりは便利なのですが、ちょっとした制約もあります。

積上げ集計項目に関する制約

  1. 1つのオブジェクト内で積上げ集計項目は10個までしか定義できない
  2. 他のオブジェクトを参照していたり、TODAY()等の関数を使用している数式項目は積上げ集計できない
  3. 主従関係にあるオブジェクトの親側でのみ定義可能(参照関係にあるオブジェクトは不可)
  4. (他にもあったらツッコミを。。)

こういったことにハマると、泣く泣く項目定義を変更したり...となかなか面倒な対応が必要となってきます。
また、参照関係にあるオブジェクトでも積上げ集計とかできたら嬉しいのに...と思うことも結構あります。
今までは個別にApex Triggerを作って対応してたのですが、「こんなのあってもいいかも?」的な感じで、積上げ集計用のApex Classを試しに作ってみました。

特徴としては、こんな感じです。

  • 主従関係または参照関係にあるオブジェクト間で利用できる
  • 親オブジェクト側で、子オブジェクトの数値項目の名称の末尾に"Sum"がついた数値項目がある場合、子オブジェクト側の数値項目を積上げ集計する
    • 例えば、子オブジェクト側に "Amount__c" という数値項目があり、親オブジェクト側に "AmountSum__c" という数値項目がある場合、Amount__cの合計をAmountSum__cに反映します
  • ただし、積上げ集計時の条件は指定できない

やってることは、こんな感じです。

  1. あるオブジェクトに対する親オブジェクト毎に、親オブジェクト名と親オブジェクトを参照している項目名を取得しておく
  2. 親オブジェクト毎に、親オブジェクトのIdをSetに格納しておく
  3. 親オブジェクト毎に、子オブジェクト側の集計対象項目と、親オブジェクト側の集計先項目をMapに格納しておく
  4. 親オブジェクトに対して、集計した値を反映する

Apex Class

public without sharing class TrgHandler_SumUp {
	private Boolean IsExecuting = false;
	private Integer BatchSize = 0;
	private Schema.SObjectType objType;
	private Map<String, Schema.SObjectField> objSchemaMap;
	
	public TrgHandler_SumUp(String cObjName, boolean param_IsExecuting, Integer param_Size){
		this.IsExecuting = param_IsExecuting;
		this.BatchSize = param_Size;
		this.objType = Schema.getGlobalDescribe().get(cObjName);
		this.objSchemaMap = objType.getDescribe().Fields.getMap();
	}
	
	/**
	* トリガで更新(or削除)のあった子オブジェクトレコードを受け取って、集計対象項目を取得&集計し、親オジェクト側に集計結果を反映する
	* @param paramLines : 子オブジェクトレコードのリスト
	**/
	public void OnAfterTrigger_Sumup(SObject[] cLines){
		Savepoint sp;
		try{
			sp = Database.setSavepoint();
			
			List<SObject> updPList = new List<SObject>();
			for(Schema.SObjectField wkFld : objSchemaMap.values()){
				Schema.DescribeFieldResult descFld = wkFld.getDescribe();
				
				for(Schema.sObjectType wkParentObjType : descFld.getReferenceTo()){
					ParentObj pObj = new ParentObj(objType, objSchemaMap, descFld.getName(), wkParentObjType.getDescribe().getName(), cLines);
					updPList.addAll(pObj.getUpdateObjs());
				}
			}
			update updPlist;
			
		}catch(Exception ex){
			Database.rollback(sp);
			System.debug('*** Error OnAfterTrigger_Sumup : ' + ex.getMessage());
		}
	}
	
	/**
	* Inner Class
	*  親オブジェクトごとに、親オブジェクト名、親オブジェクトを参照する項目名、親オブジェクトId等を格納する
	*  また、親オブジェクトの集計先項目のUpdate用Listを作成する
	**/
	private class ParentObj{
		public final String POSTFIX_SUM = 'Sum';
		
		private String ParentObjName;							// 親オブジェクト名
		private String ParentObjRefName;						// 親オブジェクトを参照する子オブジェクト側の項目名
		private Set<String> ParentIds;							// 親オブジェクトIdのSet
		private Map<String, String> ChildToParentItemMap;		// 集計対象と集計先項目のマップ(key: 子オブジェクトの集計項目 value: 親オブジェクトの集計先項目)
		private Schema.SObjectType objType;
		private Map<String, Schema.SObjectField> objSchemaMap;
		
		public ParentObj(Schema.SObjectType cObjType, Map<String, Schema.SObjectField> cObjSchemaMap, String pObjRefName, String pObjName, List<SObject> cLines){
			objType = cObjType;
			objSchemaMap = cObjSchemaMap;
			ParentObjName = pObjName;
			ParentObjRefName = pObjRefName;
			ParentIds = getParentObjIds(cLines);
			ChildToParentItemMap = getChildToParentItemMap();
		}
		
		/**
		* 親オブジェクトのId用Setを作成する
		**/
		private Set<String> getParentObjIds(List<SObject> paramLines){
			Set<String> rtn = new Set<String>();
			for(SObject line : paramLines){
				if(line.get(ParentObjRefName) != null){
					rtn.add(String.valueOf(line.get(ParentObjRefName)));
				}
			}
			return rtn;
		}
		
		/**
		* 親オブジェクト名から、集計項目と集計先項目のマップを作成する
		*  key: 子オブジェクト側の集計対象項目 value: 親オブジェクト側の集計先項目
		**/
		private Map<String, String> getChildToParentItemMap(){
			Map<String, String> rtn = new Map<String, String>();
			Map<String, Schema.SObjectField> pObjSchemaMap = Schema.getGlobalDescribe().get(ParentObjName).getDescribe().Fields.getMap();
			
			for(Schema.SObjectField wkFld : objSchemaMap.values()){
				Schema.DescribeFieldResult descFld = wkFld.getDescribe();
				if(descFld.isCustom() && (descFld.getType() == Schema.DisplayType.Currency || descFld.getType() == Schema.DisplayType.Double || descFld.getType() == Schema.DisplayType.Integer || descFld.getType() == Schema.DisplayType.Percent)){
					// 親側の項目名を生成する
					String wkSumItemName = String.valueOf(wkFld).substring(0, String.valueOf(wkFld).length()-3) + POSTFIX_SUM + '__c';
					if(pObjSchemaMap.containsKey(wkSumItemName)){
						// 親側に項目が存在して、数値属性の項目だった場合、Mapに追加する
						Schema.DescribeFieldResult pFld = pObjSchemaMap.get(wkSumItemName).getDescribe();
						if(pFld.getType() == Schema.DisplayType.Currency || pFld.getType() == Schema.DisplayType.Double || pFld.getType() == Schema.DisplayType.Integer || pFld.getType() == Schema.DisplayType.Percent){
							rtn.put(String.valueOf(wkFld), wkSumItemName);
						}
					}
				}
			}
			return rtn;
		}
		
		/**
		* 子オブジェクト側の集計対象項目を集計して、親オブジェクト側の集計先項目に反映したリストを作成する
		**/
		public List<SObject> getUpdateObjs(){
			List<SObject> rtn = new List<SObject>();
			String query = '';
			if(ChildToParentItemMap.size() > 0 && ParentIds.size() > 0){
				// 子オブジェクト側の集計用のデータを取得する
				query = 'Select ' + ParentObjRefName + ' ';
				for(String wkCol : ChildToParentItemMap.keySet()){ query += ',SUM(' + wkCol + ') ' + wkCol + ' '; }
				query += 'From ' + objType.getDescribe().getName() + ' Where ' + ParentObjRefName + ' IN (' + joinArray(ParentIds, true) + ') ';
				query += 'Group By ' + ParentObjRefName;
				Map<Id, AggregateResult> cDataMap = new Map<Id, AggregateResult>();
				for(AggregateResult ar : (List<AggregateResult>)Database.query(query)){
					cDataMap.put(String.valueOf(ar.get(ParentObjRefName)), ar);
				}
				
				// 親オブジェクトのレコードを取得する
				query = 'Select Id,' + joinArray(ChildToParentItemMap.values(), false) + ' ';
				query += 'From ' + ParentObjName + ' Where Id IN (' + joinArray(ParentIds, true) + ') ';
				Map<Id, SObject> pDataMap = new Map<Id, SObject>();
				for(SObject obj : (List<SObject>)Database.query(query)){
					pDataMap.put(String.valueOf(obj.get('Id')), obj);
				}
				
				// 集計対象項目の集計値を、親オブジェクトの集計先項目にセットする
				for(Id wkPId : pDataMap.keySet()){
					AggregateResult cObj = cDataMap.get(wkPId);
					SObject pObj = pDataMap.get(wkPId);
					
					for(String cCol : ChildToParentItemMap.keySet()){
						pObj.put(ChildToParentItemMap.get(cCol), (cObj != null ? Decimal.valueOf(String.valueOf(cObj.get(cCol))) : 0));
					}
					rtn.add(pObj);
				}
			}
			return rtn;
		}
	}
	
	/**
	* Utility:Set or List からカンマ区切りの文字列を生成する
	**/
	static String joinArray(Set<String> param, Boolean withQuote){
		String rtn = '';
		if(param == null){ return rtn; }
		for(String wk : param){ rtn += (withQuote ? ',\'' + wk + '\'' : ',' + wk); }
		return rtn.length() > 0 ? rtn.substring(1) : rtn;
	}
	static String joinArray(List<String> param, Boolean withQuote){
		String rtn = '';
		if(param == null){ return rtn; }
		for(String wk : param){ rtn += (withQuote ? ',\'' + wk + '\'' : ',' + wk); }
		return rtn.length() > 0 ? rtn.substring(1) : rtn;
	}
}

Triggerからの呼び出しは、こんな感じで...
※ "JournalLine__c" は適当なカスタムオブジェクトです。

Apex Trigger

trigger JournalLineTrigger on JournalLine__c (after delete, after insert, after update) {
	TrgHandler_SumUp sumupHandler = new TrgHandler_SumUp('JournalLine__c', Trigger.isExecuting, Trigger.size);
	
	if(Trigger.isAfter){
		List<SObject> paramLines = (Trigger.isInsert || Trigger.isUpdate) ? Trigger.new : Trigger.old;
		sumupHandler.OnAfterTrigger_Sumup(paramLines);
	}
	
}

Triggerの書き方については、こちら を参考にしました。

思いつきで書いた感じですが、一応 Github に置いておきました。
適当にリファクタしていきたいなー..と漠然と考えています。(苦笑)

今後も、思いついたモノはGithub(もしくはGist)に公開していきたいなー...と思います。