tyoshikawa1106のブログ

- Force.com Developer Blog -

SFDC:売上予測の目標の登録を試してみました

Sales Cloudユーザ向けの便利機能、売上予測の目標データ登録を試してみました。

f:id:tyoshikawa1106:20181107191630p:plain


この機能を利用するには設定で有効化する必要があります。
f:id:tyoshikawa1106:20181107191704p:plain


目標の登録方法の詳細はヘルプにまとめられています。

コラボレーション売上予測の目標データの読み込み

売上予測の目標の流れ

売上予測には実は下記のようにいくつかの種類があります。基本的には『OpportunityRevenue: 商談 - 収益』がよく利用されていると思います。
f:id:tyoshikawa1106:20181107191938p:plain


売上予測の目標の登録はデータローダかSalesforce APIによる対応が必要になります。登録には「売上予測種別ID」が必要になります。開発者コンソールで下記クエリを実行して取得するのが簡単だと思います。

SELECT Id,DeveloperName FROM ForecastingType


取得結果はこんな感じ。0Dbから始まるIDが「売上予測種別ID」です。
f:id:tyoshikawa1106:20181107192228p:plain


売上予測種別IDが確認できたらデータローダでインポートするためのCSVファイルを用意します。必要な情報は下記のとおりです。ユーザ名の列は取り込み時には不要ですが、あるとデータを準備する作業がやりやすいと思います。(...ヘルプでそう紹介されていました。)
f:id:tyoshikawa1106:20181107192352p:plain

f:id:tyoshikawa1106:20181107192509p:plain:w300


目標を取り込む際に指定するオブジェクトは「売上予測目標 (ForecastingQuota)」オブジェクトです。
f:id:tyoshikawa1106:20181107192606p:plain


項目マッピングはこんな感じです。
f:id:tyoshikawa1106:20181107192632p:plain


これで取り込み処理を実行すると売上予測の目標の列に値がセットされます。これでその月の商談の受注額が目標に到達しているかひと目で確認できるようになります。
f:id:tyoshikawa1106:20181107192808p:plain

売上予測の使い方

売上予測で確認できることはレポートと似たような感じですが、レポートよりも営業目線で必要な情報がわかりやすく表示されます。例えば対象年月のマスをクリックするとその部分に該当する商談が一覧表示されます。
f:id:tyoshikawa1106:20181107193121p:plain


また放置状態となってしまっている商談も視覚化されます。例えば現在11月なので下記の赤枠の部分は本来完了予定にもかかわらず未完了のままの商談になります。
f:id:tyoshikawa1106:20181107193427p:plain


こうした未完了商談は期限が過ぎたタイミングで再度完了予定日を調整したり、失注ステータスに更新してクローズしたりと対応することで対応漏れを防ぎ、あいまいな状態のまま放置されることを回避することができます。

売上予測とレポート

売上予測の目標を登録後、レポートを活用することでグラフ表示による比較が可能になります。目標をつかったレポート作成にはカスタムレポートタイプの作成が必要になります。下記の構成で作成してみてください。(子オブジェクト側は関連レコードの有無は問いませんの方を選択します。)
f:id:tyoshikawa1106:20181107193945p:plain


細かい条件は利用用途によって変更することになりますが例としてざっくりこんな感じ。
f:id:tyoshikawa1106:20181107194303p:plain


これで目標と商談の受注した金額を比較するためのグラフが作成できました。
f:id:tyoshikawa1106:20181107194407p:plain


レポート作成の際に商談の金額とは別に売上予測 金額という類似の項目もあるので注意してください。今回の設定手順の場合は「売上予測 金額」項目は欲しい情報とは異なる項目になります。
f:id:tyoshikawa1106:20181107194609p:plain


毎月の目標と金額の比較は上記のような感じで可能になりますが、Salesforceでは累積レポートの作成も可能です。累積レポートの設定方法は下記サイトに詳しくまとめられていました。

http://deferloader.blog.uhuru.co.jp/?p=5156


レポート設定のこの部分から集計項目を作成します。
f:id:tyoshikawa1106:20181107195001p:plain:w300


形式タブで開始日を選びます。
f:id:tyoshikawa1106:20181107195019p:plain:w300


続いて数式を記載しますがフルエディタに切り替えるとやりやすいと思います。
f:id:tyoshikawa1106:20181107195130p:plain


式は下記のような感じ。上のリンクの参考サイトの数式をそのまま使いました。

目標の累積
ForecastingQuota.QuotaAmount:SUM +
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 2)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 2))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 3)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 3))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 4)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 4))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 5)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 5))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 6)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 6))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 7)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 7))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 8)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 8))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 9)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 9))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 10)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 10))+
IF (ISNULL (PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 11)), 0, PREVGROUPVAL(ForecastingQuota.QuotaAmount:SUM, ForecastingQuota.StartDate, 11))
金額の累積
ForecastingFact_Opp.Amount:SUM +
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 2)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 2))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 3)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 3))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 4)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 4))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 5)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 5))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 6)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 6))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 7)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 7))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 8)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 8))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 9)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 9))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 10)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 10))+
IF (ISNULL (PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 11)), 0, PREVGROUPVAL(ForecastingFact_Opp.Amount:SUM, ForecastingQuota.StartDate, 11))

一見複雑ですが、よく見るとわかりやすい数式です。
f:id:tyoshikawa1106:20181107195515p:plain


項目のAPI名ですが、「ForecastingFact_Opp.Amount」というように数式用の記載のされ方になっている部分があります。API名がわからないときは挿入ボタンで挿入すると確認できます。
f:id:tyoshikawa1106:20181107195434p:plain


累積するには「PREVGROUPVAL」関数を利用しますが、下記のように空欄になって表示されることがあります。詳細行を非表示にしたりグルーピング項目を一つにすることで正しい値が表示されるようになります。
f:id:tyoshikawa1106:20181107200111p:plain


グラフの設定では通常の縦棒グラフを選択、X軸に開始日を指定してY軸に先程作成した集計項目を指定します。「+基準」で複数項目を指定できます。
f:id:tyoshikawa1106:20181107200353p:plain:w300

f:id:tyoshikawa1106:20181107200342p:plain:w300


これで累積棒グラフの表示が可能になります。(※試してみた感じではこうした累積の比較グラフを作成する際に積み上げ棒グラフとの組み合わせはできないみたいです。)
f:id:tyoshikawa1106:20181107200512p:plain


こうしてレポートを用意したあとはダッシュボードでグラフをまとめることでより便利になります。
f:id:tyoshikawa1106:20181107201100p:plain


チームの目標はもちろん、営業メンバーごとに目標と金額の比較グラフを用意するとうまくいっている人や苦戦している人を確認しやすくなり、苦戦している人がいる場合はフォローにはいるなどの判断がしやすくなると思います。


売上予測の目標を登録することでこうした便利な使い方も可能になります。

売上予測目標とSOQL

売上予測の目標はデータローダでの登録が必要になりますが、一度登録した目標は開発者コンソールから編集が可能になります。クエリはこんな感じ。

Select Id, PeriodId, StartDate, ProductFamily, QuotaAmount, QuotaQuantity, QuotaOwnerId, IsQuantity, IsAmount, CreatedDate, CreatedById, LastModifiedDate, LastModifiedById, SystemModstamp, ForecastingTypeId FROM ForecastingQuota

f:id:tyoshikawa1106:20181107201813p:plain


Salesforce APIでも取り込めるとのことなので、データ管理ようのVisualforceあたりを開発しておくと良いのかもしれません。(実装可能かは未確認ですが)

SFDC:Force.comサイトとページビュー制限について

Force.comサイトとページビュー制限についてです。Force.comサイトはSalesforceにログインせずに外部からアクセスできるWebサイトを構築できる仕組みです。

f:id:tyoshikawa1106:20181104230325p:plain

JP:Sites FAQ - developer.force.com


基本的にあまり気にする必要はありませんが、月に表示可能な数に上限があります。

Force.com Sitesは月のページビューをベースに価格付けを行います:

  • Enterprise Editionには月間500,000ページビューまで含まれます
  • Unlimited Editionには月間1,000,000ページビューまで含まれます

追加の月刊ページビューをオーダーすることもできます:

  • $1,000/月で1,000,000の追加ページビュー(全Edition共通)


アクセス数が多いサイトをForce.comサイトで構築する場合は追加ページビューの購入を検討する必要があります。


ページビューの上限を超えたときの挙動ですが下記で紹介されていました。
f:id:tyoshikawa1106:20181104230626p:plain

https://resources.docs.salesforce.com/208/latest/ja-jp/sfdc/pdf/limits_limitations.pdf


少し超えた場合は即座に利用不可になるのではなく、管理者などに通知メールが届きます。制限を超えたあと、そのまま放置するか、大きく超えた場合(300%)はサイトが表示されなくなるようです。

  • 組織が月間ページビュー制限の 110% に達した場合、サイト管理者と請求管理者にメール通知が送信されます。
  • 組織で 4 か月連続してページビュー制限の 110% を超えた場合、次のカレンダー月が始まるまで、またはページビューを追加購入するまで、サイトが無効になります。メール通知が、サイト管理者と請求管理者、および関連するアカウントエグゼクティブに送信されます。
  • 指定されたカレンダー月で、組織がページビュー制限の 300% に達した場合、次のカレンダー月が始まるまで、またはページビューを追加購入するまで、サイトが無効になります。メール通知が、サイト管理者と請求管理者、および関連するアカウントエグゼクティブに送信されます。


ページビュー数はサイトの設定画面で確認できます。
f:id:tyoshikawa1106:20181104230901p:plain


またレポートやダッシュボードで確認できるようにすることも可能なようです。
f:id:tyoshikawa1106:20181104230957p:plain

Help | Training | Salesforce

SFDC:WebサイトへのForce.comサイトページの埋め込みを試してみました

SalesforceのVisualforceページを外部に公開したい場合はForce.comサイトを仕組みを使用します。ページ全体をForce.comサイトで開発できるときは話が簡単ですが、既存のWebサイト内にある入力フォームだけをForce.comサイトで構築したい場合は少し検討が必要な部分があるので今回その部分について確認してみました。

JP:Sites - developer.force.com

はじめに

Salesforceと関係無いWebサイトにForce.comサイトページを表示するにはiframeで表示させます。ですが、ただiframeで表示するだけだとモバイル端末でアクセスした際にレイアウトが崩れてしまいます。


社外の様々な人たちがアクセスする外部サイトの場合は画面サイズに合わせてレイアウトが調整されるレスポンシブ対応を考慮する必要があります。


※iframeのレスポンシブ対応についてはこちらのサイトがとても参考になりました。

iframeのレスポンシブ対応はもの凄く簡単 | ウェブデザインスクールをお探しならWEB塾 超現場主義|東京(上野)・長野・浜松・札幌

iframeのレスポンシブ対応についての動作検証

まずは通常のWebサイトだけでiframeのレスポンシブ対応がうまくいくかを確認してみます。Webサイトの立ち上げはHerokuとLightning Design SystemのGetting Startedの手順で用意しました。

f:id:tyoshikawa1106:20181027180537p:plain

Heroku - Lightning Design System

Step 1: Initialize your project

ディレクトリ作成

$ mkdir demo_slds_heroku
$ cd demo_slds_heroku


初期設定

$ npm init


npm initで入力する内容

name:
version:
description:
entry point: (index.js) [server.js]
test command:
git repository:
keywords:
author:
license: (ISC)


package.jsonの内容

{
  "name": "demo_slds_heroku",
  "version": "0.0.0",
  "description": "demo_slds_heroku",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
Step 2: Install node dependencies - Express
$ npm install express --save
Step 3: Create public/index.html
$ mkdir public
$ touch public/index.html

index.htmlの内容

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Playground</title>
  </head>
  <body>
    Kaixo!
  </body>
</html>
Step 4: Create server.js
$ touch server.js


server.jsの内容

var express = require('express');
var app = express();
var port = process.env.PORT || 8080;

// Serve static files
app.use(express.static(__dirname + '/public'));

// Serve your app
console.log('Served: http://localhost:' + port);
app.listen(port);


localhostの起動

$ node server.js

f:id:tyoshikawa1106:20181027181632p:plain


ここでGoogleマップを埋め込んでみます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Playground</title>
  </head>
  <body>
    <div>
      <iframe src="略" width="600" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
    </div>
  </body>
</html>


この時点ではiframeの幅や高さが固定のためブラウザの幅小さくなると表示されない領域が発生することを確認できます。
f:id:tyoshikawa1106:20181027181918p:plain

f:id:tyoshikawa1106:20181027181931p:plain:w250


続いて最初に記載したリンク先の手順どおりにcssを適用させます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Playground</title>
    <style>
      .google_map{
        position:relative;
        width:100%;
        height:0;
        padding-top:75%;
      }
      .google_map iframe{
        position:absolute;
        top:0;
        left:0;
        width:100%;
        height:100%;
      }
    </style>
  </head>
  <body>
    <div class="google_map">
      <iframe src="略" width="600" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
    </div>
  </body>
</html>


無事にiframeの内容がページ幅に合わせて動的に切り替わることを確認できました。
f:id:tyoshikawa1106:20181027182404p:plain

f:id:tyoshikawa1106:20181027182418p:plain:w250


目的の動作検証を行うことができましたが、Lightning Design Systemを適用してHerokuへデプロイまでやっておきます。

Step 5: Download the Salesforce Lightning Design System

f:id:tyoshikawa1106:20181027182705p:plain

Step 6: Add components

styleタグの中身を別のcssファイルに移動したり、ヘッダーをつけてこんな感じ。
f:id:tyoshikawa1106:20181027183934p:plain

index.htmlの中身

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" type="text/css" href="/assets/styles/salesforce-lightning-design-system.css" />
    <link rel="stylesheet" type="text/css" href="/css/style.css" />
    <title>Playground</title>
  </head>
  <body>
    <div class="slds-grid slds-wrap">
      <!-- Header -->
      <nav class="slds-col slds-size_1-of-1">
        <div class="slds-page-header">
          <div class="slds-page-header__row">
            <div class="slds-page-header__col-title">
              <div class="slds-media">
                <div class="slds-media__figure">
                  <span class="slds-icon_container slds-icon-standard-opportunity" title="opportunity">
                    <svg class="slds-icon slds-page-header__icon" aria-hidden="true">
                      <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons/standard-sprite/svg/symbols.svg#opportunity" />
                    </svg>
                    <span class="slds-assistive-text">Heroku Demo</span>
                  </span>
                </div>
                <div class="slds-media__body">
                  <div class="slds-page-header__name">
                    <div class="slds-page-header__name-title">
                      <h1>
                        <span class="slds-page-header__title slds-truncate" title="Rohde Corp - 80,000 Widgets">Heroku Demo</span>
                      </h1>
                    </div>
                  </div>
                  <p class="slds-page-header__name-meta">T.Yoshikawa Labs</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </nav>
      <!-- Main -->
      <main class="slds-col slds-size_1-of-1 slds-p-around_small">
        <div class="slds-section">
          <h3 class="slds-section__title slds-theme_shade slds-m-bottom_small">
            <span class="slds-truncate slds-p-horizontal_small" title="Section Title">Google Map</span>
          </h3>
          <!-- Google Map -->
          <div class="google_map">
            <iframe src="略" width="600" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
          </div>
        </div>
      </main>
    </div>
  </body>
</html>


こうなります。
f:id:tyoshikawa1106:20181027183907p:plain

Step 7: Deploy to Heroku
$ touch Procfile

Procfileの内容

web: node server.js


Herokuにログイン

$ heroku login
Email: [your@email.com]
Password: [typing will be hidden]

$ git init
$ heroku create [name-of-your-project]

$ git add .
$ git commit -m "Initial commit"
$ git push heroku master

$ heroku open


こんな感じでHerokuにデプロイしてページを表示できるようにしました。
f:id:tyoshikawa1106:20181027185708p:plain

WebサイトへのSalesforceサイトの組み込み

それではSalesforceサイトの組み込みを試してみます。はじめに外部に公開するためサイト設定でドメインを指定します。
f:id:tyoshikawa1106:20181027190026p:plain


埋め込むためのVisualforceページを用意します。
f:id:tyoshikawa1106:20181027190446p:plain


サイト設定でVisualforceページを表示できるように設定します。これでSalesforceにログインしなくてもアクセスできるVisualforceページを用意できました。
f:id:tyoshikawa1106:20181027190658p:plain


※iframeに別ドメインのサイトを埋め込むことになるのでクリックジャック保護レベルの設定で許可が必要になると思います。
f:id:tyoshikawa1106:20181027203559p:plain


あとは動作確認できるように入力フォームを実装します。
f:id:tyoshikawa1106:20181027191833p:plain


簡易的につくっていますがコードはこんな感じ。

<apex:page showHeader="false" sidebar="false">
    <head>
        <apex:slds />
    </head>
    <body>
        <div xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="vf-page">
            <div class="slds-p-around_small">
                <!-- Field 01 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-01">Form Label 01</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-01" class="slds-input" />
                    </div>
                </div>
                <!-- Field 02 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-02">Form Label 02</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-02" class="slds-input" />
                    </div>
                </div>
                <!-- Field 03 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-03">Form Label 03</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-03" class="slds-input" />
                    </div>
                </div>
                <!-- Field 04 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-04">Form Label 04</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-04" class="slds-input" />
                    </div>
                </div>
                <!-- Field 05 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-05">Form Label 05</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-05" class="slds-input" />
                    </div>
                </div>
                <!-- Field 06 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-06">Form Label 06</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-06" class="slds-input" />
                    </div>
                </div>
                <!-- Field 07 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-07">Form Label 07</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-07" class="slds-input" />
                    </div>
                </div>
                <!-- Field 08 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-08">Form Label 08</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-08" class="slds-input" />
                    </div>
                </div>
                <!-- Field 09 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-09">Form Label 09</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-09" class="slds-input" />
                    </div>
                </div>
                <!-- Field 10 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-10">Form Label 10</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-10" class="slds-input" />
                    </div>
                </div>
                <!-- button -->
                <div class="slds-m-top_small">
                    <button class="slds-button slds-button_brand">問い合わせ</button>
                </div>
            </div>
        </div>
    </body>
</apex:page>


Salesforceサイト側のページが用意できたのでHerokuで用意したWebサイトへiframeで組み込んでみます。まずはレスポンシブ対応無しバージョン。画面サイズに関わらず幅や高さが固定されてしまい正しく表示されません。
f:id:tyoshikawa1106:20181027192757p:plain


続いてレスポンシブ対応ありバージョンです。画面の幅に合わせてフォームが自然に表示されました。
f:id:tyoshikawa1106:20181027194442p:plain


幅を小さくしたときはこのように表示されます。
f:id:tyoshikawa1106:20181027194518p:plain:w200


これでSalesforceサイトのページをWebサイトに埋め込むことができました。iframeによる表示となりますが、Salesforceでつくった入力フォームを自然な見た目で表示することができます。


実際のモバイル端末でアクセスして確認してみました。※iPhone
f:id:tyoshikawa1106:20181027195001p:plain:w200

f:id:tyoshikawa1106:20181027195023p:plain:w200


問題なさそうです。実際のプロジェクトではAndroid端末やIEなどのブラウザで問題ないかも確認してみ他方が良いと思います。(大丈夫だと思いますが)

iframeの高さ指定

サンプルでは%になっていますが、入力フォームなど横幅は変わっても高さを変えたくない場合は、px指定にすることで対応できます。

f:id:tyoshikawa1106:20181027195223p:plain

iframeとページ遷移

当然ですがiframe内でページ遷移するとiframeの中だけでページ遷移が行われます。
f:id:tyoshikawa1106:20181027200908p:plain


JS処理でのページ遷移の場合は下記のような感じにすると親フレームでページ遷移できるので忘れないようにします。

parent.location.href=<url>;


WebサイトへのForce.comサイトページの埋め込みはこんな感じで実現できました。

サンプルコード

最後に今回検証したときのサンプルコードです。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" type="text/css" href="/assets/styles/salesforce-lightning-design-system.css" />
    <link rel="stylesheet" type="text/css" href="/css/style.css" />
    <title>Playground</title>
  </head>
  <body>
    <div class="slds-grid slds-wrap">
      <!-- Header -->
      <nav class="slds-col slds-size_1-of-1">
        <div class="slds-page-header">
          <div class="slds-page-header__row">
            <div class="slds-page-header__col-title">
              <div class="slds-media">
                <div class="slds-media__figure">
                  <span class="slds-icon_container slds-icon-standard-opportunity" title="opportunity">
                    <svg class="slds-icon slds-page-header__icon" aria-hidden="true">
                      <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons/standard-sprite/svg/symbols.svg#opportunity" />
                    </svg>
                    <span class="slds-assistive-text">Heroku Demo</span>
                  </span>
                </div>
                <div class="slds-media__body">
                  <div class="slds-page-header__name">
                    <div class="slds-page-header__name-title">
                      <h1>
                        <span class="slds-page-header__title slds-truncate" title="T.Yoshikawa Labs">Heroku Demo</span>
                      </h1>
                    </div>
                  </div>
                  <p class="slds-page-header__name-meta">T.Yoshikawa Labs</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </nav>
      <!-- Main -->
      <main class="slds-col slds-size_1-of-1 slds-p-around_small">
        <!-- Salesforce Form -->
        <div class="slds-section">
          <!-- Salesforce Form Header -->
          <h3 class="slds-section__title slds-theme_shade slds-m-bottom_small">
            <span class="slds-truncate slds-p-horizontal_small" title="Section Title">Salesforce Form</span>
          </h3>
          <!-- Salesforce Form Main -->
          <div class="iframe-section">
            <iframe src="<your salesforce page url>" width="600" height="600" frameborder="0" style="border:0" allowfullscreen></iframe>
          </div>
        </div>
        <!-- Google Map -->
        <div class="slds-section">
          <!-- Google Map Header -->
          <h3 class="slds-section__title slds-theme_shade slds-m-bottom_small">
            <span class="slds-truncate slds-p-horizontal_small" title="Section Title">Google Map</span>
          </h3>
          <!-- Google Map Main -->
          <div class="iframe-section">
            <iframe src="<your google map url>" width="600" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
          </div>
        </div>
      </main>
    </div>
  </body>
</html>
style.css
.iframe-section {
  position:relative;
  width:100%;
  height:0;
  padding-top:650px;
}
.iframe-section iframe {
  position:absolute;
  top:0;
  left:0;
  width:100%;
  height:100%;
  min-height: 600px;
}
Salesforce Form Page
<apex:page showHeader="false" sidebar="false">
    <head>
        <apex:slds />
        <script type="text/javascript">
          function move() {
                parent.location.href="<your salesforce thanks page url>";
            }
        </script>
    </head>
    <body>
        <div xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="vf-page">
            <div class="slds-p-around_small">
                <!-- Field 01 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-01">Form Label 01</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-01" class="slds-input" />
                    </div>
                </div>
                <!-- Field 02 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-02">Form Label 02</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-02" class="slds-input" />
                    </div>
                </div>
                <!-- Field 03 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-03">Form Label 03</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-03" class="slds-input" />
                    </div>
                </div>
                <!-- Field 04 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-04">Form Label 04</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-04" class="slds-input" />
                    </div>
                </div>
                <!-- Field 05 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-05">Form Label 05</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-05" class="slds-input" />
                    </div>
                </div>
                <!-- Field 06 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-06">Form Label 06</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-06" class="slds-input" />
                    </div>
                </div>
                <!-- Field 07 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-07">Form Label 07</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-07" class="slds-input" />
                    </div>
                </div>
                <!-- Field 08 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-08">Form Label 08</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-08" class="slds-input" />
                    </div>
                </div>
                <!-- Field 09 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-09">Form Label 09</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-09" class="slds-input" />
                    </div>
                </div>
                <!-- Field 10 -->
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="form-element-10">Form Label 10</label>
                    <div class="slds-form-element__control">
                        <input type="text" id="form-element-10" class="slds-input" />
                    </div>
                </div>
                <!-- button -->
                <div class="slds-m-top_small">
                    <button class="slds-button slds-button_brand" onclick="move()">問い合わせ</button>
                </div>
            </div>
        </div>
    </body>
</apex:page>
Salesforce Form Thanks Page
<apex:page showHeader="false" sidebar="false">
    <head>
        <apex:slds />
        <style>
        	* {
            	background-color: #255E9E;
                font-size: 20px;
                color: #fff;
            }
        </style>
    </head>
    <body>
        <div xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="vf-page">
            <div class="slds-p-around_small">
                Thanks!
            </div>
        </div>
    </body>
</apex:page>

SFDC:System.assertEqualsのエラーメッセージ指定を試してみました

Apexのテストクラス作成時にはSystem.assertEqualsを使って想定された結果となっているかをチェックできます。下記の場合はエラーメッセージを格納する変数の値が空白値となっているかをチェックできます。

System.assertEquals(String.isEmpty(result.errorMessage), true);


空白値が想定されている部分でエラーが発生した場合、次のようなエラーメッセージが表示されます。
f:id:tyoshikawa1106:20181023010042p:plain


trueを想定しているところにfalseが返って来ているとメッセージが表示されています。テストとしては正しく判定が行われていますが、想定外のエラーの調査には少し分かりづらいメッセージとなります。そんなときは3つ目の引数としてエラーメッセージを指定することが可能です。

System.assertEquals(String.isEmpty(result.errorMessage), true, '任意のエラーメッセージ');


例えば次のように値の必須チェックを行っているApexクラスがあります。
f:id:tyoshikawa1106:20181023010325p:plain


テストクラス作成時にテストデータの用意で本来は値をセットする部分にnullをセットしました。これで必須チェックエラーでテストが失敗する状態です。
f:id:tyoshikawa1106:20181023010621p:plain


System.assertEqualsを次のように変更します。result.errorMessage変数の値が空値でなかった際にテスト結果にresult.errorMessage変数の値を表示する書き方となります。
f:id:tyoshikawa1106:20181023011749p:plain

System.assertEquals(String.isEmpty(result.errorMessage), true, result.errorMessage);


これでテスト実行すると次のようにresult.errorMessage変数の値がテスト結果に表示されます。
f:id:tyoshikawa1106:20181023011958p:plain


エラーメッセージは任意の値をセットできるので状況に応じて指定してあげると予期せぬエラーが発生したときに調査がしやすくなります。

SFDC:商品を商談に追加するようユーザに促す機能を試してみました

商品を商談に追加するようユーザに促す機能を試してみました。この機能を有効化すると商談作成後に商談商品の登録画面が表示されるようになります。商談の設定画面で有効化できます。

f:id:tyoshikawa1106:20181019203340p:plain

商談を作成する

f:id:tyoshikawa1106:20181019203615p:plain

価格表の選択画面が表示される

f:id:tyoshikawa1106:20181019203633p:plain

商品の選択画面が表示される

f:id:tyoshikawa1106:20181019203712p:plain

単価や数量の入力画面が表示される

f:id:tyoshikawa1106:20181019203844p:plain

商品が登録され商談作成完了

f:id:tyoshikawa1106:20181019203932p:plain


金額項目に手入力させないときに有効化する機能みたいです。

参考

SFDC:Dreamforce2018 - サンフランシスコ移動ログ

Googleマップのタイムライン機能で移動ログを取得できました。ところどころGPSの記録がおかしくなっている部分がありましたが、だいたいこんな感じでした。サンフランシスコに到着後、空港と市内への移動 (BART) と会場からヒルトンホテルへの移動 (シャトルバス)は以外は徒歩で移動できました。

9月24日 - Day 0

サンフランシスコに到着した日。
f:id:tyoshikawa1106:20181014184140p:plain


空港から市内へ。
f:id:tyoshikawa1106:20181014184154p:plain


Dreamforceの参加者バッチをもらってホテルチェックインと市内観光。
f:id:tyoshikawa1106:20181014184241p:plain

9月25日 - Day 1

ホテルから会場へ。夜はDreamforceのパーティーに。
f:id:tyoshikawa1106:20181014184332p:plain

9月26日 - Day 2

ホテルから会場へ。夜はDreamfestでMETALLICAとジャネット・ジャクソンのライブに行きました。
f:id:tyoshikawa1106:20181014184504p:plain

9月27日 - Day 3

一番GPSの記録がうまく残っていませんでしたが、ホテルと会場の往復と市内の散歩に行った日です。
f:id:tyoshikawa1106:20181014184656p:plain

9月28日 - Day 4

朝はユニオン・スクエア近くで朝食。昼は会場へ。夜はグレース大聖堂近くのステーキハウスに食事に行きました。
f:id:tyoshikawa1106:20181014184757p:plain

9月29日 - Day 5

朝、ホテルをチェックアウトして空港へ。9時過ぎだと混雑してました。もっと早く行ったほうが良さそうです。SFO→NRTの部分は記録残っていませんでした。。
f:id:tyoshikawa1106:20181014184909p:plain


Googleマップのタイムライン機能は結構おもしろいと思います。

関連記事

SFDC:Dreamforce2018 - 番外編「Clipper Card」をつかってみました

サンフランシスコ国際空港到着後に市内へ移動するにはタクシー、UBER、BARTといくつか選択肢がありますが、今回はBARTを利用しました。
f:id:tyoshikawa1106:20180924115344j:plain:w300


BARTの切符購入をしようと思ってポチポチ操作していたところ、以前見た紙の切符ではなくClipper Cardがでてきました。(3ドル追加料金がかかりましたが..)
f:id:tyoshikawa1106:20180924120249j:plain:w300


PASMOやSUICAみたいなタッチで支払いができるICカードです。操作ミスに近い形での購入でしたが結果的には紙の切符より便利でした。


ちなみに紙の切符はこんな感じ。紙の切符もチャージして繰り返し使える仕組みでした。
f:id:tyoshikawa1106:20181014172114p:plain:w300


耐久度的にはICカードの方がしっかりしているのでClipperを一枚持っておいた方が良さそうな感じでした。

クリッパーカード - Wikipedia


ClipperですがBART専用のICカードだと思っていましたが、その他のサービスでも利用できる仕組みみたいです。
f:id:tyoshikawa1106:20181014175443p:plain:w300

Clipper Home


またICカードへのチャージ (Add Value)はWebサイトから行えるようになっています。(おそらくカードの購入も)
f:id:tyoshikawa1106:20181014175822p:plain:w300

https://www.clippercard.com/ClipperCard/register.jsf


Clipperカードには裏面に専用の番号が記載されています。こちらの番号をつかってアカウントを作成できるみたいです。・・ですが試してみようと思ったところユーザの住所登録が必要になり米国以外の住所は想定されていなさそうでした。(よく考えてみれば日本に住んでる人がアカウントを作成する必要なんて無いので当たり前でした...)
f:id:tyoshikawa1106:20181014180352p:plain:w300


Webサイトをポチポチ見ていたところアカウントがなくてもチャージができることはできるみたいです。(紛失時の再発行等の対応は不可になりそうです。)
※画面はGoogle翻訳で変換後のもの。
f:id:tyoshikawa1106:20181014180657p:plain:w300


Google翻訳で変換しながら覗いてみたのですがチャージまで進めるには少し大変そうでした。
f:id:tyoshikawa1106:20181014181123p:plain:w300


Webサイトで事前にチャージしておければ便利そうでしたが、切符購入機でチャージしてみたとき、何も問題なくスムーズに進められたので現地についてから対応してもそれほど問題はないと思います。


その他Twitter経由でClipperの使い方について教えてもらう機会がありました。

  • CalTrainでやMuni、ケーブルカー、バス、フェリーなどで利用できる
  • Walgreensならクレカで入金が行える


Walgreenとは薬局チェーンの会社とのことだと思います。

ウォルグリーン - Wikipedia


検索したところこちらのサイトが参考になりました。店舗にいってチャージをしてもらえそうです。(他にもいくつかサイトが見つかりました。)


今回はBART以外での利用は必要ありませんでしたが、バスやケーブルカーなどの利用が必要な場合は調べておくと便利な使い方が見つかるかもしれません。


余談ですがClipperサービスのTwitterアカウントもありました。

関連記事

2017年にサンフランシスコに行ったときの記事です。BARTで切符を書いたときの記録が残っています。