freee APIでSalesforceと連携
freee APIを利用して、Salesforceで帳票の項目を入力し、ApexでPOST メソッドをCalloutしてfreee内で請求書を作成します。
手順
- freee開発者環境を作成 (freee)
- freee API Referenceで試す (freee)
- Salesforce用のアプリを作成 (freee)
- Auth. Providerを設定 (Salesforce)
- External Credentialsを設定 (Salesforce)
- Named Credetialsを設定 (Salesforce)
- ApexでCalloutし、POSTするメソッドを作成 (Salesforce)
- LWCでSalesforceからfreeeの取引先(Partner)を作成(Salesforce)
freee開発者環境を作成
freee開発者環境は無料で作成できます。開発者環境で遠慮なく請求書を作成してみます。以下のサイトに丁寧なチュートリアルがあるのでおすすめです。
https://developer.freee.co.jp/startguide
また開発者アプリ一覧から、API Referenceからテストするためのアプリと、Salesforceから接続するアプリを別途管理していると便利です。私は、アプリ自体は Salesforce Connector と名づけ、API Reference ver.とSalesforce ver.に分けました。基本は同じでCallback URLが違います。
https://app.secure.freee.co.jp/developers/applications
Salesforce Connector (API Reference) ver.
Callback URLが、urn:ietf:wg:oauth:2.0:oob
になります。アプリ名と概要は任意です。Client ID
とClient Secret
は後ほど、Customer Keyを取得する際に利用しますので、メモっておくかこちらから常に取得できるようにしておいてください。
freee API Referenceで試す (freee)
それでは、まずAPI Referenceでテストしてみましょう。
https://developer.freee.co.jp/reference/accounting/reference
API Callはプランによって1日の制限数がありますが、大抵の普通の利用では、使い切ることはないでしょう。
AuthorizeにてTokenを入力する
スクロールしていくとAuthorize
するボタンが出てくるので、クリックを押します。
するとTokenを入れろと出てくるので、Tokenが必要になります。Tokenの取得は、こちらのURLから。
https://accounts.secure.freee.co.jp/public_api/authorize?client_id={Client_ID}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=token
{Client_ID}は先ほどのアプリに表示されているClient IDを入力します。入力すると、freeeへログインを促されたのち、以下のようにアプリに接続許可を与えます。
与えると、次は無事アクセス用のTokenが発行されます。こちらを、先ほどのAuthorize
の際に求められる箇所へ入力することで、API Referenceページからテストランができるようになります。
このように入力します。Authorizeボタンを押して後、CloseでOKです。
まず試しに、接続している組織のデータを取得してみましょう。
Companies 事業所のセクションへ移動して、Try it out
ボタンを押してみます。
company_idを取得する
すると、接続している事業所の情報が返ってきます。便利ですね。ここで、取得するidが後ほど利用するcompany_id
になるのでメモしておきます。
以上で、まず開発環境でアプリを作成して、API Referenceのページからアクセストークンを入力して、組織に入れることを確認しました。
Salesforce用のアプリを作成 (freee)
では同様にSalesforceからアクセスするためのアプリを作成します。ここで取得するClient IdとClient Secretを次のAuth. Providerを設定で利用します。 アプリは「プライベート」です。
Client IdとClient Secretを取得する
すると、また別のClient IdとClient Secretが表示されるのでメモして、次の「Auth. Providerを設定 (Salesforce)」で利用します。
Auth. Providerを設定 (Salesforce)
次は、Salesforceから同様にアクセするための設定を行います。まず、Auth. Providerの登録です。
Provider Type | Open ID Connect |
---|---|
Name | freeeDev |
Consumer Key | {Client_ID} |
Consumer Secret | {Client_Secret} |
Authorize Endpoint URL | https://accounts.secure.freee.co.jp/public_api/authorize |
Token Endpoint URL | https://accounts.secure.freee.co.jp/public_api/token |
名前は任意でつけてください。私はfreeeDevつけました。Client ID
とClient Secret
はSalesforce用のアプリを作成 (freee)の中で表示されるアプリの設定に自動作成されているものを使います。
Callback URLを取得する
Callback URLをコピペして、Salesforce Connector アプリのCallback URLへ入力します。こうすることによって、Salesforceからアクセスできようになります。
Callback URLを変更する
先ほど作成したSalesforce ConnectorアプリのCallback URLを、Auth. Providerを作成した際に表示されてCallback URLに置き換えます。この作業が大事です。
External Credentialsを設定 (Salesforce)
次は、freeeは外部のサービスなので、External Credentialsを作成します。Named Credentailsのセクションで、External Credentialsのタブにいき、Newを押します。
Auth. Providerを選択
次に作成する際は、以下の設定でExternal Credentialsを作成します。Identity Providerは先ほど作成したAuth. Providerを選択します。
Named Credentialsを作成
次に、Named Credentialsを作成します。Named CrdentialsのセクションでNewを選択し、URLにapiのendpointを入力します。会計freeeの場合はhttps://api.freee.co.jp
ですが、請求書freeeの場合は、https://api.freee.co.jp/iv
になります。
PrincipalをNamed CredentialsにしてConfiguredする
結果、一つのExternal Credentialが、Auth. Providerを利用して、2つのNamed Credentialsを管理するこのような形になります。これで、会計freeeにも請求書freeeにも一つのExternal Credentialsでアクセスできることになります。管理が楽ですね。
ApexでCalloutし、POSTするメソッドを作成 (Salesforce)
ApexでCalloutして取引先を作成しますが、いつものAnonyous Windowでテストできます。
@TestVisible
private static String endpoint = 'callout:freeeDev' + '/api/1/partners';
Map<String,Object> jsonMap = new Map<String,Object>{
'company_id' => {company_id},
'name' => '新しい取引先 1',
'contact_name' => '営業担当',
'email' => 'contact1@example1.com'
};
// Convert the map to a JSON string
String jsonString = JSON.serialize(jsonMap);
String requestBody = jsonString;
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
req.setBody(requestBody);
Http http = new Http();
HttpResponse res = http.send(req);
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
system.debug('response:' + response);
companyIdは、組織のIdになりますが、今回はCustom Metadataに保存していて、Queryで取得して充てています。Sandbox ver.と分けて管理してもいいです。
POST メソッドでrequestBodyをjsonに変換して送信することで、無事取引先が作成されました。responseは逆にdeserializeすることで、いい感じにsystem.debug()で閲覧できるようになります。
LWCでSalesforceからfreeeの取引先(Partner)を作成(Salesforce)
というわけで、Open Execute Anonymous Windowで成功すれば、あとはApexとLWCでそれを実装するのみです。
Files
apex
public with sharing class FreeeApiController {
@AuraEnabled
public static Map<String, Object> createPartner(String requestBody) {
HttpRequest req = new HttpRequest();
String endpoint = 'callout:freeeDev' + '/api/1/partners';
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
req.setBody(requestBody);
Http http = new Http();
HttpResponse res = http.send(req);
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
system.debug('response: ' + response);
return response;
}
@AuraEnabled
public static String getCompanyId() {
List<freee__mdt> metaData = [SELECT Id__c FROM freee__mdt WHERE DeveloperName = 'companyId' LIMIT 1];
return metaData[0].Id__c;
}
}
html
<template>
<lightning-card hide-header="true">
<template lwc:if={isFreeePartner}>
<div class="slds-grid slds-grid_vertical-align-center slds-p-around_x-small">
<lightning-formatted-text class="slds-card__header-title slds-m-right_x-small"
value={account_partner_name}></lightning-formatted-text>
<a href={externalUrl} target="_blank"><lightning-icon icon-name="utility:new_window" size="x-small"
alternative-text="open freee window"></lightning-icon></a>
</div>
</template>
<template lwc:else>
<div class="slds-grid slds-grid_vertical slds-p-around_small" style="position:relative;">
<lightning-input label="Name" name="name" value={name} onchange={handleChange}></lightning-input>
<lightning-input label="Email" name="email" value={email} onchange={handleChange}></lightning-input>
<div class="slds-grid slds-gutters">
<lightning-combobox placeholder="都道府県" class="slds-col slds-size_1-of-2" label="Prefecture"
name="prefecture" value={prefecture} options={prefectureOptions}
onchange={handleChange}></lightning-combobox>
<lightning-input placeholder="郵便番号" class="slds-col slds-size_1-of-2" label="Postal Code"
name="postalCode" value={postalCode} onchange={handleChange}></lightning-input>
</div>
<lightning-input label="Address" placeholder="市区町村・番地" name="address1" value={address1}
onchange={handleChange}></lightning-input>
<lightning-input label="Building" placeholder="建物名・部屋番号など" name="address2" value={address2}
onchange={handleChange}></lightning-input>
<lightning-button class="slds-m-vertical_x-small" label="Create Partner"
onclick={handleCreatePartner}></lightning-button>
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading" size="x-small"></lightning-spinner>
</template>
</div>
</template>
</lightning-card>
</template>
js
import { LightningElement, wire, api } from 'lwc';
import createPartner from '@salesforce/apex/FreeeApiController.createPartner';
import getCompanyId from '@salesforce/apex/FreeeApiController.getCompanyId';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class FreeeConnectorDemo extends LightningElement {
@api recordId;
companyId;
accountData;
phone;
website;
email;
name;
postalCode;
prefecture;
address1;
address2;
isFreeePartner = false;
isLoading = false;
externalUrl;
account_partner_name;
async connectedCallback() {
this.companyId = await getCompanyId();
}
get address_attributes() {
return {
zipcode: this.postalCode,
prefecture_code: this.prefecture,
street_name1: this.address1,
street_name2: this.address2
};
};
get requestBody() {
return JSON.stringify({
name: this.name,
email: this.email,
company_id: Number(this.companyId),
contact_name: this.name,
address_attributes: this.address_attributes
});
}
fields = [
'Account.Name',
'Account.Phone',
'Account.Website',
'Account.BillingStreet',
'Account.BillingCity',
'Account.BillingState',
'Account.BillingPostalCode',
'Account.Partner_Name__c',
'Account.Partner_Id__c'
];
@wire(getRecord, { recordId: '$recordId', fields: '$fields' })
account({ error, data }) {
if (data) {
this.accountData = Object.keys(data.fields).reduce((acc, fieldName) => {
acc[fieldName] = data.fields[fieldName].value;
return acc;
}, {});
if (this.accountData['Partner_Id__c']) {
this.isFreeePartner = true;
this.externalUrl = 'https://settings.secure.freee.co.jp/partners/' + this.accountData['Partner_Id__c'];
this.account_partner_name = this.accountData['Partner_Name__c'];
return;
}
this.phone = this.accountData['Phone'];
this.website = this.accountData['Website'];
this.name = this.accountData['Name'];
this.postalCode = this.accountData['BillingPostalCode'];
this.prefecture = this.prefectureOptions.find(option => option.label === this.accountData['BillingState']).value;
this.address1 = this.accountData['BillingCity'] + this.accountData['BillingStreet'];
} else if (error) {
console.error('Error fetching account:', error);
}
}
handleChange(event) {
this[event.target.name] = event.target.value;
}
handleCreatePartner() {
this.isLoading = true;
createPartner({ requestBody: this.requestBody })
.then(response => {
console.log('created:', JSON.stringify(response));
this.isFreeePartner = true;
this.isLoading = false;
this.externalUrl = 'https://settings.secure.freee.co.jp/partners/' + response.partner.id;
this.account_partner_name = response.partner.name;
const fields = {};
fields['Id'] = this.recordId;
fields['Partner_Id__c'] = response.partner.id;
fields['Partner_Name__c'] = response.partner.name;
const recordInput = { fields };
console.log('recordInput:', JSON.stringify(recordInput));
updateRecord(recordInput)
.then(() => {
console.log('Account updated');
})
.catch(error => {
console.error('Error updating account:', error);
});
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Partner created successfully',
variant: 'success'
})
);
})
.catch(error => {
console.error('Error creating partner:', error);
this.isLoading = false;
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: 'Error creating partner',
variant: 'error'
})
);
});
}
prefectureOptions = [
{ label: '北海道', value: 0 },
{ label: '青森県', value: 1 },
{ label: '岩手県', value: 2 },
{ label: '宮城県', value: 3 },
{ label: '秋田県', value: 4 },
{ label: '山形県', value: 5 },
{ label: '福島県', value: 6 },
{ label: '茨城県', value: 7 },
{ label: '栃木県', value: 8 },
{ label: '群馬県', value: 9 },
{ label: '埼玉県', value: 10 },
{ label: '千葉県', value: 11 },
{ label: '東京都', value: 12 },
{ label: '神奈川県', value: 13 },
{ label: '新潟県', value: 14 },
{ label: '富山県', value: 15 },
{ label: '石川県', value: 16 },
{ label: '福井県', value: 17 },
{ label: '山梨県', value: 18 },
{ label: '長野県', value: 19 },
{ label: '岐阜県', value: 20 },
{ label: '静岡県', value: 21 },
{ label: '愛知県', value: 22 },
{ label: '三重県', value: 23 },
{ label: '滋賀県', value: 24 },
{ label: '京都府', value: 25 },
{ label: '大阪府', value: 26 },
{ label: '兵庫県', value: 27 },
{ label: '奈良県', value: 28 },
{ label: '和歌山県', value: 29 },
{ label: '鳥取県', value: 30 },
{ label: '島根県', value: 31 },
{ label: '岡山県', value: 32 },
{ label: '広島県', value: 33 },
{ label: '山口県', value: 34 },
{ label: '徳島県', value: 35 },
{ label: '香川県', value: 36 },
{ label: '愛媛県', value: 37 },
{ label: '高知県', value: 38 },
{ label: '福岡県', value: 39 },
{ label: '佐賀県', value: 40 },
{ label: '長崎県', value: 41 },
{ label: '熊本県', value: 42 },
{ label: '大分県', value: 43 },
{ label: '宮崎県', value: 44 },
{ label: '鹿児島県', value: 45 },
{ label: '沖縄県', value: 46 }
];
}
解説
ファイル内での要点をいくつかまとめて解説します。
Apex
getCompanyIdメソッド
@AuraEnabled
public static String getCompanyId() {
List<freee__mdt> metaData = [SELECT Id__c FROM freee__mdt WHERE DeveloperName = 'companyId' LIMIT 1];
return metaData[0].Id__c;
}
getCompanyId()
というMethodを作成しています。こちらはcompany_idをベタ打ちしないためのもので、簡易に実装する場合は下手打ちで試されても構いません。
createPartnerメソッド
@AuraEnabled
public static Map<String, Object> createPartner(String requestBody) {
HttpRequest req = new HttpRequest();
String endpoint = 'callout:freeeDev' + '/api/1/partners';
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
req.setBody(requestBody);
Http http = new Http();
HttpResponse res = http.send(req);
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
system.debug('response: ' + response);
return response;
}
こちらはrequestBody
を直接パラメーターとして渡して、http requestに設定しています。戻り値はMapで返してデバッグや戻り値を利用する際に使いやすい形で返しています。
js
getterでrequestBodyを動的に設定
get requesetBody
get address_attributes() {
return {
zipcode: this.postalCode,
prefecture_code: this.prefecture,
street_name1: this.address1,
street_name2: this.address2
};
};
get requestBody() {
return JSON.stringify({
name: this.name,
email: this.email,
company_id: Number(this.companyId),
contact_name: this.name,
address_attributes: this.address_attributes
});
}
prefectureOptionsの設置
こちらは、都道府県を番号へ置き換えるためのデータセットです。別途jsファイルでexportしてもいいですが、簡易的に同コンポーネント内に記述しています。再利用する場合は別途、jsで各所importしてもいいでしょう。
prefectureOptions
prefectureOptions = [
{ label: '北海道', value: 0 },
{ label: '青森県', value: 1 },
{ label: '岩手県', value: 2 },
{ label: '宮城県', value: 3 },
{ label: '秋田県', value: 4 },
{ label: '山形県', value: 5 },
{ label: '福島県', value: 6 },
{ label: '茨城県', value: 7 },
{ label: '栃木県', value: 8 },
{ label: '群馬県', value: 9 },
{ label: '埼玉県', value: 10 },
{ label: '千葉県', value: 11 },
{ label: '東京都', value: 12 },
{ label: '神奈川県', value: 13 },
{ label: '新潟県', value: 14 },
{ label: '富山県', value: 15 },
{ label: '石川県', value: 16 },
{ label: '福井県', value: 17 },
{ label: '山梨県', value: 18 },
{ label: '長野県', value: 19 },
{ label: '岐阜県', value: 20 },
{ label: '静岡県', value: 21 },
{ label: '愛知県', value: 22 },
{ label: '三重県', value: 23 },
{ label: '滋賀県', value: 24 },
{ label: '京都府', value: 25 },
{ label: '大阪府', value: 26 },
{ label: '兵庫県', value: 27 },
{ label: '奈良県', value: 28 },
{ label: '和歌山県', value: 29 },
{ label: '鳥取県', value: 30 },
{ label: '島根県', value: 31 },
{ label: '岡山県', value: 32 },
{ label: '広島県', value: 33 },
{ label: '山口県', value: 34 },
{ label: '徳島県', value: 35 },
{ label: '香川県', value: 36 },
{ label: '愛媛県', value: 37 },
{ label: '高知県', value: 38 },
{ label: '福岡県', value: 39 },
{ label: '佐賀県', value: 40 },
{ label: '長崎県', value: 41 },
{ label: '熊本県', value: 42 },
{ label: '大分県', value: 43 },
{ label: '宮崎県', value: 44 },
{ label: '鹿児島県', value: 45 },
{ label: '沖縄県', value: 46 }
];
まとめ
以上で、簡易的にfreee とSalesforceをAPIで連携してみました。会計freeeと請求書freeeはurlが違うので、そこを注意していれば意外とあっさり繋がります。Client_Id, Client_SecretとCallbackですが、API Referenceから呼ぶときとSalesforceから呼ぶときでは違うのでそこの設定だけ注意が必要です。
請求書APIについては詳細に書きませんが、要領は会計freeeと同じです。使用はこちらのAPI referenceを参照にしてください。
https://developer.freee.co.jp/reference/iv/reference