前の章では、OAuth2.0の概要と、データ連携を行う際のシステム構成パターンについて説明しました。今回は、その知識を実践に移す具体例として、会計freeeのAPIを使用して損益計算書(PL)と貸借対照表(BS)のデータを取得し、DWHに連携するプログラムをご紹介します。
ユースケース:会計freeeからDWHへのデータ連携
今回のユースケースでは、会計freeeの損益計算書(PL)と貸借対照表(BS)のデータをDWHに連携する方法を説明します。
具体的には、以下の手順でデータを取得し、CSVファイルとして保存します。これは、DWHへの取り込みの前段階として、データを扱いやすい形式に変換するステップです。
- freee APIから認証トークンを取得
- 取得したトークンを使用してPLとBSのデータを取得
- 取得したデータをCSVファイルとして保存
この方法を使えば、取得したCSVファイルを任意のDWHシステムに簡単にインポートできます。
freee会計のAPIの特徴
freee会計のAPIもOAuth2.0の認証形式を採用しており、セキュアな認証と柔軟なアクセス制御を実現しています。ただし、freee会計 APIには留意すべき特徴があります。
その特徴とはリフレッシュトークンの扱いです。新しいアクセストークンを取得すると、使用したリフレッシュトークンが無効になり、新しいリフレッシュトークンが発行されます。これはSalesforceの認証には無い仕様であり、プログラムの実装ではこの仕様に対応する必要があります。
この特徴に対応するために、以下の手順が必要になります:
- アクセストークン取得時に、新しいリフレッシュトークンも同時に取得
- 取得した新しいリフレッシュトークンを保存
- 次回のアクセス時には、更新されたリフレッシュトークンを使用
この方法により、継続的かつ安全にfreee APIにアクセスすることができます。
実装例:Node.jsを使用したfreee APIとの連携
それでは、実際にNode.jsを使用してfreee APIから損益計算書(PL)と貸借対照表(BS)のデータを取得し、CSVファイルとして保存するプログラムを見ていきましょう。
Step 1. 必要な認証情報の取得
こちらについては多くの記事で紹介されております。以下の記事等をご参照ください。
- freee APIを使ってみました(準備:登録~アクセストークンの取得と、事業所IDの取得、ユーザーIDの取得、アクセストークンのリフレッシュ)
- https://qiita.com/yuji_saito/items/6df726e7c7c844fcd13c
Step 2. 認証情報の保存
実際の運用ではよりセキュアな箇所に保存すべきですが、今回はサンプルとして、'dotenv'
を使用しますので、.envファイルに認証情報を保存してください。
.env
FREEE_TOKEN_URL=https://accounts.secure.freee.co.jp/public_api/token
FREEE_REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
FREEE_CLIENT_ID=<<ここにClient IDを保存してください>>
FREEE_CLIENT_SECRET=<<ここにClient Secretを保存してください>>
FREEE_REFRESH_TOKEN=<<ここにRefresh Tokenを保存してください>>
freee-client.js
// FreeeClient.js
const fs = require('fs');
const request = require('request-promise-native');
const _ = require('lodash');
require('dotenv').config();
/**
* Freee APIと対話するためのFreeeClientクラス
* このクラスは財務レポートの取得と認証管理のためのメソッドを提供します
*/
class FreeeClient {
constructor() {
this.access_token = null;
this.refresh_token = null;
this.base_url = 'https://api.freee.co.jp';
}
/**
* 会計年度の損益計算書を取得します
* @param {number} fiscal_year - データを取得する会計年度
* @returns {Promise<Array>} - 損益データの配列
*/
async pl_year(fiscal_year) {
return this._pl_bs_year(fiscal_year, 'pl');
}
/**
* 会計年度の詳細な損益計算書を取得します
* @param {number} fiscal_year - データを取得する会計年度
* @returns {Promise<Array>} - 詳細な損益データの配列
*/
async pl_year_detail(fiscal_year) {
return this._pl_bs_year(fiscal_year, 'pl', 'partner');
}
/**
* 会計年度の貸借対照表を取得します
* @param {number} fiscal_year - データを取得する会計年度
* @returns {Promise<Array>} - 貸借対照表データの配列
*/
async bs_year(fiscal_year) {
return this._pl_bs_year(fiscal_year, 'bs');
}
/**
* 会計年度の詳細な貸借対照表を取得します
* @param {number} fiscal_year - データを取得する会計年度
* @returns {Promise<Array>} - 詳細な貸借対照表データの配列
*/
async bs_year_detail(fiscal_year) {
return this._pl_bs_year(fiscal_year, 'bs', 'partner');
}
/**
* 会計年度の損益計算書または貸借対照表データを取得する内部メソッド
* @param {number} fiscal_year - データを取得する会計年度
* @param {string} report_type - レポートの種類('pl'または'bs')
* @param {string} [breakdown_display_type] - オプションの内訳表示タイプ
* @returns {Promise<Array>} - 財務データの配列
*/
async _pl_bs_year(fiscal_year, report_type, breakdown_display_type) {
const months = _.range(1, 24); // 1から23までの配列を生成
const all = [];
for (const month of months) {
const currentYear = month <= 12 ? fiscal_year : fiscal_year + 1;
const currentMonth = month <= 12 ? month : month - 12;
const data = await this._pl_bs_month(currentYear, currentMonth, report_type, breakdown_display_type);
all.push(...data);
}
return all;
}
/**
* 特定の月の損益計算書または貸借対照表データを取得する内部メソッド
* @param {number} fiscal_year - 会計年度
* @param {number} fiscal_month - 会計月
* @param {string} report_type - レポートの種類('pl'または'bs')
* @param {string} [breakdown_display_type] - オプションの内訳表示タイプ
* @returns {Promise<Array>} - 指定された月の財務データの配列
*/
async _pl_bs_month(fiscal_year, fiscal_month, report_type, breakdown_display_type) {
const optionString = breakdown_display_type ? `&breakdown_display_type=${breakdown_display_type}` : '';
const endpoint = report_type === 'pl' ? 'trial_pl' : 'trial_bs';
await this.retrieveAccessTokenByRefresh();
const res = await this.apiRequestGet(`/api/1/reports/${endpoint}?company_id=${process.env.FREEE_PRAZTO_CODE}&fiscal_year=${fiscal_year}&start_month=${fiscal_month}&end_month=${fiscal_month}${optionString}`);
return this.processFinancialData(res, fiscal_year, fiscal_month, report_type, breakdown_display_type);
}
/**
* 財務データを処理してフォーマットします
* @param {Object} res - APIレスポンス
* @param {number} fiscal_year - 会計年度
* @param {number} fiscal_month - 会計月
* @param {string} report_type - レポートの種類('pl'または'bs')
* @param {string} [breakdown_display_type] - オプションの内訳表示タイプ
* @returns {Array} - 処理された財務データ
*/
processFinancialData(res, fiscal_year, fiscal_month, report_type, breakdown_display_type) {
const data = res[`trial_${report_type}`];
if (!data) {
return [];
}
return _.flatMap(data.balances, e => {
const baseEntry = {
...e,
account_month: `${fiscal_year}-${_.padStart(fiscal_month.toString(), 2, '0')}-01`
};
if (breakdown_display_type) {
return this.flattenPartnerData(baseEntry);
} else {
return [baseEntry];
}
});
}
/**
* freee会計APIからの財務データを取引先ごとに平坦化します
*
* freee会計APIでは、財務データの'partners'プロパティに取引先情報がネストされて返却されます。
* このメソッドは、そのネストされたデータ構造を解除し、各取引先のデータを個別の行として
* 平坦化します。これにより、データの分析や処理が容易になります。
*
* 取引先データが存在しない場合は、元のエントリにnullの取引先情報を設定して返します。
* 取引先データが存在する場合は、各取引先に対して新しいエントリを作成し、
* 元のエントリの情報と取引先の情報を組み合わせて返します。
*
* @param {Object} entry - APIから返却された財務エントリ
* @returns {Array} - 平坦化されたデータの配列
*/
flattenPartnerData(entry) {
if (_.isEmpty(entry.partners)) {
return [{
...entry,
partner_id: null,
partner_name: null,
comment: ''
}];
} else {
return _.map(entry.partners, partner => ({
..._.omit(entry, 'partners'),
partner_id: partner.id,
partner_name: partner.name,
opening_balance: partner.opening_balance,
debit_amount: partner.debit_amount,
credit_amount: partner.credit_amount,
closing_balance: partner.closing_balance,
composition_ratio: partner.composition_ratio
}));
}
}
/**
* リフレッシュトークンを使用して新しいアクセストークンを取得します
* @returns {Promise<Object>} - access_tokenとrefresh_tokenを含むオブジェクト
*/
async retrieveAccessTokenByRefresh() {
if (this.refresh_token) {
return;
}
try {
const res = await request(this.requestOptionRefreshToken());
this.writeEnv(res.refresh_token);
this.access_token = res.access_token;
this.refresh_token = res.refresh_token;
return {access_token: res.access_token, refresh_token: res.refresh_token};
} catch (error) {
console.error('アクセストークンの取得中にエラーが発生しました:', error);
throw error;
}
}
/**
* 認可コードを使用して新しいアクセストークンを取得します
* @param {string} code - 認可コード
* @returns {Promise<Object>} - access_tokenとrefresh_tokenを含むオブジェクト
*/
async retrieveAccessTokenByCode(code) {
if (this.refresh_token) {
return;
}
try {
const res = await request(this.requestOptionAuthorizationCode(code));
this.writeEnv(res.refresh_token);
this.access_token = res.access_token;
this.refresh_token = res.refresh_token;
return {access_token: res.access_token, refresh_token: res.refresh_token};
} catch (error) {
console.error('アクセストークンの取得中にエラーが発生しました:', error);
throw error;
}
}
/**
* Freee HR APIにGETリクエストを送信します
* @param {string} url - APIエンドポイント
* @param {Object} [params] - オプションのクエリパラメータ
* @returns {Promise<Object>} - APIレスポンス
*/
async apiRequestGetHr(url, params) {
try {
const options = {
method: 'GET',
url: `${this.base_url}/hr${url}`,
qs: {...params, access_token: this.access_token},
json: true
};
return await request(options);
} catch (error) {
console.error('HR APIリクエスト中にエラーが発生しました:', error);
throw error;
}
}
/**
* Freee APIにGETリクエストを送信します
* @param {string} url - APIエンドポイント
* @param {Object} [params] - オプションのクエリパラメータ
* @returns {Promise<Object>} - APIレスポンス
*/
async apiRequestGet(url, params) {
try {
const options = {
method: 'GET',
url: `${this.base_url}${url}`,
qs: {...params, access_token: this.access_token},
json: true
};
return await request(options);
} catch (error) {
console.error('APIリクエスト中にエラーが発生しました:', error);
throw error;
}
}
/**
* 認可コード用のリクエストオプションを生成します
* @param {string} code - 認可コード
* @returns {Object} - リクエストオプション
*/
requestOptionAuthorizationCode(code) {
return {
method: 'POST',
url: process.env.FREEE_TOKEN_URL,
headers: { 'cache-control': 'no-cache', 'Content-Type': 'application/json' },
form: {
grant_type: "authorization_code",
redirect_uri: process.env.FREEE_REDIRECT_URI,
client_id: process.env.FREEE_CLIENT_ID,
client_secret: process.env.FREEE_CLIENT_SECRET,
code: code
},
json: true
};
}
/**
* リフレッシュトークン用のリクエストオプションを生成します
* @returns {Object} - リクエストオプション
*/
requestOptionRefreshToken() {
return {
method: 'POST',
url: process.env.FREEE_TOKEN_URL,
headers: { 'cache-control': 'no-cache', 'Content-Type': 'application/x-www-form-urlencoded' },
form: {
grant_type: "refresh_token",
redirect_uri: process.env.FREEE_REDIRECT_URI,
client_id: process.env.FREEE_CLIENT_ID,
client_secret: process.env.FREEE_CLIENT_SECRET,
refresh_token: process.env.FREEE_REFRESH_TOKEN
},
json: true
};
}
/**
* リフレッシュトークンを.envファイルに書き込みます
* @param {string} refreshToken - 新しいリフレッシュトークン
*/
writeEnv(refreshToken) {
const envVars = _(process.env)
.keys()
.filter(k => _.startsWith(k, 'ACCOUNT_') || _.startsWith(k, 'BOARD_') || _.startsWith(k, 'FREEE_') || _.startsWith(k, 'SF_'))
.without('FREEE_REFRESH_TOKEN')
.map(k => `${k}=${process.env[k]}`)
.join('\n');
const newEnvContent = `${envVars}\nFREEE_REFRESH_TOKEN=${refreshToken}\n`;
fs.writeFileSync('./.env', newEnvContent);
}
}
module.exports = FreeeClient;
損益計算書(PL)の取得処理(freee-data-export-pl.js)
// freee-data-export-pl.js
const fs = require('fs').promises;
const _ = require('lodash');
const { parse } = require('json2csv');
const FreeeClient = require('./lib/freee-client');
/**
* Freeeから損益計算書の詳細データを取得し、直接CSVとして保存する
* @param {number} year - 取得する会計年度
* @returns {Promise<void>}
*/
const exportFreePLDetail = async (year) => {
const freeeClient = new FreeeClient();
const outputPath = `./data/freee_pl_detail_${year}.csv`;
try {
console.log(`${year}年度のデータ取得開始`);
const plData = await freeeClient.pl_year_detail(year);
if (_.isEmpty(plData)) {
console.warn('データが空です。処理を中断します。');
return;
}
const csv = parse(plData);
await fs.writeFile(outputPath, csv);
console.log(`CSVファイル保存完了: ${outputPath}`);
} catch (error) {
console.error('エラーが発生しました:', error);
throw error;
}
};
// メイン処理の実行
(async () => {
try {
const year = 2023;
await exportFreePLDetail(year);
console.log('処理完了');
} catch (error) {
console.error('プログラム実行エラー:', error);
process.exit(1);
}
})();
貸借対照表(BS)の取得処理(freee-bs-data-export-bs.js)
// freee-bs-data-export-bs.js
const fs = require('fs').promises;
const _ = require('lodash');
const { parse } = require('json2csv');
const FreeeClient = require('./lib/freee-client');
/**
* Freeeから貸借対照表の詳細データを取得し、直接CSVとして保存する
* @param {number} year - 取得する会計年度
* @returns {Promise<void>}
*/
const exportFreeBSDetail = async (year) => {
const freeeClient = new FreeeClient();
const outputPath = `./data/freee_bs_detail_${year}.csv`;
try {
console.log(`${year}年度のBS取得開始`);
const bsData = await freeeClient.bs_year_detail(year);
if (_.isEmpty(bsData)) {
console.warn('データが空です。処理を中断します。');
return;
}
const csv = parse(bsData);
await fs.writeFile(outputPath, csv);
console.log(`CSVファイル保存完了: ${outputPath}`);
} catch (error) {
console.error('エラーが発生しました:', error);
throw error;
}
};
// メイン処理の実行
(async () => {
try {
const year = 2023;
await exportFreeBSDetail(year);
console.log('処理完了');
} catch (error) {
console.error('プログラム実行エラー:', error);
process.exit(1);
}
})();
このコードは以下の手順で動作します:
- リフレッシュトークンを使用してアクセストークンを取得
- 新しいリフレッシュトークンを.envに保存(通常の運用はこのようにはなりません)
- 取得したアクセストークンを使用してPLとBSのデータを取得
- 取得したデータをCSVファイルとして保存
注意点として、このコードを実行する前に、CLIENT_ID、CLIENT_SECRET、COMPANY_IDを適切な値に設定する必要があります。また、初回のリフレッシュトークンは別途取得し、ファイルに保存しておく必要があります。
このプログラムを定期的に実行することで、会計freeeのデータを自動的に取得し、CSVファイルとして保存できます。実際の運用では取得したデータをDWHに保存することで、他のビジネスデータと組み合わせた分析が可能になります。
まとめと次章について
本章では、freee会計APIを活用して損益計算書(PL)と貸借対照表(BS)のデータを取得し、CSVファイルとして保存するプログラムの実装例を紹介しました。この方法により、会計データを定期的にデータウェアハウス(DWH)に連携し、より深度のある経営分析が可能となります。
freee APIの特性、特にリフレッシュトークンの適切な管理に注意を払うことで、専用のETL(抽出・変換・ロード)サービスがなくても、継続的なデータ連携を実現できます。さらに、この実装例は他のOAuth2.0を採用しているAPIにも応用可能な汎用性を持っています。
次の章では、Praztoの実際のシステム連携実績を紹介します。200社以上のSalesforce導入支援を通じて、ETLツールの活用やスクラッチ開発による連携システムの構築など、多様なシステム間連携を実現してきました。システム連携は多くの企業のデジタルトランスフォーメーション(DX)推進において重要な役割を果たしていますが、その実装には専門知識とスキルが必要です。Praztoは豊富な経験と技術力を活かし、お客様のシステム連携プロジェクトの成功をサポートしています。
【次の章へはこちらから】