Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

会計FreeeAPIとGASを活用した請求管理システムの構築

学習塾運営会社のコーポレートエンジニアを担当している、massです。

弊社では、スリムなバックオフィスを実現することを一つの目標として掲げています。その中でも、会計・経理業務の自動化が一つのテーマとなっています。

この記事では、社内システムと会計Freeeを連携した業務効率化・自動化の事例について紹介させていただきます。

社内業務のちょっとした「面倒くさい」問題

弊社では、顧客の請求業務に某社の自動口座振替システムを利用しています。

これまでの運用ルールでは、各店舗の責任者が顧客ごとの請求金額をExcelで集計し、本社の経理担当者が口座振替システムに請求金額を入力する、と決められていたのですが、いくつか改善すべき点がありました。

店舗や品目ごとに売上データを集計したい

弊社では、会計Freeeの部門別会計の機能を用いて、店舗や品目ごとの売上データを管理することを目指していました。そのためには、口座振替システムから一括入金されるひと月分の売上金を、部門、品目ごとに分けて会計Freeeに仕訳入力する必要がありました。

ところが、実際の運用では、詳細な売上金額を入力する余裕がありませんでした。そのため、店舗や品目ごとの集計データが必要になった際には、過去の口座振替履歴データや請求書発行履歴データをかき集め、手動で集計する必要がありました。

こうした状況を改善するためにも、店舗や品目ごとの売上データを細かい粒度で会計Freeeに反映し、常に最新の経営状況を把握できることが求められていたのです。

口座振替と銀行振込が混在する煩雑な業務を改善したい

毎月の口座振替で問題になりがちなのが、顧客入会後の1〜2ヶ月間、口座振替手続きが完了するまでの請求管理です。この期間は、口座振替システムによる売上金の回収を行うことができないため、別な手段で売上金を回収する必要があります。

従来は、請求書を送付して銀行振込を依頼し、売上金を回収していました。この業務では、責任者による請求内容の申請、経理部門による請求内容の承認、Excelでの請求書作成、請求書の郵送手配、会計ソフトへの売掛入力...など、多くの事務作業が発生します。

また、重複請求や請求漏れを防ぐための管理方法が属人化しており、銀行振込から口座振替に移行するタイミングでの請求ミスが多発していました。

これらの事務作業負担の軽減と、請求ミスを防ぐ仕組みが必要とされていたのです。

楽しく便利に、業務ハック

これらをはじめとした問題を解決するため、今回、GAS+スプレッドシートのシステムにFreeeAPIを連携した社内システムを開発しました。ここでは、FreeeAPIとの連携部分に注目して紹介します。

請求管理機能にFreeeAPI連携する

前述の通り、弊社の売上回収方法には口座振替と請求書送付+銀行振込の2種類の請求方法があります。それぞれについて、FreeeAPIとの連携方法を説明していきます。

1.口座振替システム及びFreee取引APIを利用した売上金の回収と仕訳

以下の図は、請求データの流れを表しています。

まず、各担当者は、スプレッドシートに請求データを入力します。その後、口座振替システム連携用のCSVファイルを出力するのと同時に、会計Freeeの取引APIを利用して部門・品目別に売上仕訳を立てます。入金後、自動で経理の画面から売掛金の消し込みを実行します。

スクリーンショット 2020-12-06 20.34.00.png

2.Freee請求書APIを利用した売上金の回収と仕訳

一方で、口座振替手続きが完了する前の顧客への請求には口座振替システムを利用できないため、会計Freeeの請求書APIを利用して、請求書の郵送手配までを行います。入金後、自動で経理の画面から売掛金の消し込みを実行します。

また、顧客口座の残高不足などにより口座振替が実施できなかった際にも、Freeeの請求書APIを利用して請求書の郵送手配までを行います。

スクリーンショット 2020-12-06 20.34.06.png

社内ワークフローの変更点と改善効果

では、今回導入した社内システムがもたらした業務改善効果はどのようなものだったのでしょうか。ここでは、FreeeAPI連携による改善効果に着目して紹介します。

1.口座振替のワークフロー

以下は、口座振替のワークフローです。システム導入前後のフローと作業工数を比較しています。

スクリーンショット 2020-12-06 21.27.53.png

振替不能時の請求書発行にFreee請求書APIを用いることで、請求書1件あたり25分ほどの工数削減になっています。(Freee請求書APIの利用による削減効果の詳細については、後述します。)

また、売上仕訳や売掛金の消し込み作業にFreee取引APIを用いることで10分ほどの工数削減になっています。さらに、部門や品目ごとの売上集計が可能になったため、工数削減以外の面での改善効果もありました。

スクリーンショット 2020-12-06 8.26.33.png

上図は、口座振替明細の入力画面になります。スプレッドシート上のインターフェイスから全ての操作ができるようになっています。また、slackとも連携しており、担当者に通知が送られるようになっています。

なお、重複請求を防止するため、口座振替手続きが完了するまでの2ヶ月間は対象者の名前が記載されないようになっています。

2.請求書発行のワークフロー

以下は、請求書発行のワークフローです。先ほどと同様に、システム導入前後のフローと作業工数を比較しています。

スクリーンショット 2020-12-06 21.27.58.png

請求書の発行について、請求書1件あたり25分ほどの工数削減につながりました。なお、会社全体で毎月20件ほどの請求書を発行しているので、1月あたり8時間程度の工数軽減につながっています。また、請求情報が、システムのデータフローのみで完結するようになったため、請求ミスの防止効果もあると言えるでしょう。

スクリーンショット 2020-12-06 16.23.34.png

上図は、請求書の発行画面になります。モーダル画面に、申請者が入力した請求内容が表示されています。請求書発行ボタンを押すと、Freee請求書APIが実行され、請求書の下書きが作成されます。なお、郵送手配の確定はAPIから実行することができないため、Freeeの管理画面から確定する必要があります。

また、こちらもslackとも連携しており、担当者へ通知が送られるようになっています。

実装上のポイント

TypeScript+webpackでのGAS開発

GASエディタでの開発は不便な点が多いため、VSCodeを用いてローカル環境で開発をおこないました。また、npmパッケージを導入するため、webpackでビルドします。ソースコードのデプロイには、google/claspを使用しました。

また、スプレッドシート上に表示されるモーダル画面の実装にはVue.jsとbootstrapを使用しています。

詳細については、以下にボイラープレートを公開していますので、ご参照ください。
https://github.com/mass584/gas_boilerplate

GASプロジェクトの階層化

弊社では、今回構築した請求管理システムの他にも、FreeeAPIを利用する別な社内システムがあります。このような場合、FreeeAPI呼び出しを行うスクリプトを、独立したGASプロジェクトとしてモジュール化しておくと便利です。

また、このモジュールは、APIトークンや部門ID・品目IDの管理機能も担っています。これらをスプレッドシートにリスト化し、GASプロジェクトから読み込めるようにしておきます。

スクリーンショット_2020-12-06_6_33_46.jpg

なお、実際の開発では、テスト環境と本番環境を用意しておくと便利です。Freeeの開発用テスト事業所の作成方法は、こちらに記載されています。

FreeeAPIの型定義ファイル

開発を快適に行うために、APIの型定義ファイルがあると便利です。

GitHubに公開されているFreeeAPIのjsonスキーマファイルを利用し、swagger-to-ts でTypeScriptの型定義に変換します。

FreeeAPIの呼び出し

以下は、取引APIを使用して売上仕訳を立てるコードの抜粋です。こちらの関数を別のGASプロジェクトから利用できるようにエクスポートしておきます。

なお、シートからAPIトークンや部門ID・品目IDを取得する関数の中身については、記述を省略しています。

// 取引仕訳を作成する関数
export function createCustomerDeal(
  body: {
    issueDate: string;
    dueDate: string;
    sectionName: SectionName;
    details: {
      fixedTutorial: {
        amount: number;
        tax: '課対売上10%' | '課対売上8%(軽)' | '対象外';
      };
      // 他の売上品目も列挙します
    };
  },
): GlobalFunctions.FreeeResponse {
  // 設定スプレッドシートのIDを指定します
  const fileId = process.env.SPREADSHEET_ID;

  // 事業所IDを指定します(テスト環境と本番環境で事業所IDを分けます)
  const companyId = Number(process.env.FREEE_COMPANY_ID);

  // 設定シートを取得します
  const spreadsheet = SpreadsheetApp.openById(fileId);
  const settingSheet = spreadsheet.getSheetByName('設定シート');
  if (!settingSheet) {
    return { success: false, errors: ['シートが見つかりません。'] };
  }

  // アクセストークンを取得します
  const accessToken = getAccessToken(settingSheet);
  if (!accessToken) {
    return { success: false, errors: ['トークンが取得できませんでした。'] };
  }

  // リクエストボディを設定します
  const reqBody: definitions['dealCreateParams'] = {
    company_id: companyId,
    issue_date: body.issueDate,
    due_date: body.dueDate,
    partner_id: getPartnerId(settingSheet, '日本システム収納'),
    type: 'income',
    details: [
      {
        amount: body.details.fixedTutorial.amount,
        tax_code: getTaxCode(settingSheet, body.details.fixedTutorial.tax),
        account_item_id: getAccountItemId(settingSheet, '売上高'),
        item_id: getItemId(settingSheet, '授業料'),
        section_id: getSectionId(settingSheet, body.sectionName),
      },
      // 他の売上品目も列挙します
    ],
  };

  return createDeal(accessToken, reqBody);
}

// アクセストークン取得部分
function getAccessToken(sheet: GoogleAppsScript.Spreadsheet.Sheet): string {
  const refreshToken = getRefreshToken(sheet); // シート上に保存したリフレッシュトークンを取得します
  const loginResponse = refresh(refreshToken);
  updateRefreshToken(sheet, loginResponse.refreshToken); // シート上のリフレッシュトークンを更新します

  return loginResponse.accessToken;
}

function refresh(refresh_token: string): {
  accessToken: string,
  refreshToken: string,
} {
  // リダイレクトURL, クライアントID, クライアントシークレットを取得します
  const redirect_uri = process.env.FREEE_API_REDIRECT_URL;
  const client_id = process.env.FREEE_API_CLIENT_ID;
  const client_secret = process.env.FREEE_API_CLIENT_SECRET;

  const httpResponse = UrlFetchApp.fetch(token_url, {
    method: 'post',
    contentType: 'application/x-www-form-urlencoded',
    payload: {
      grant_type: 'refresh_token',
      redirect_uri,
      client_id,
      client_secret,
      refresh_token,
    },
    muteHttpExceptions: true,
  });
  const response = JSON.parse(httpResponse.getContentText()) as FreeeResponse.RefreshResponse;

  return {
    accessToken: response.access_token,
    refreshToken: response.refresh_token,
  };
}

// API呼び出し部分
function createDeal(
  token: string,
  reqBody: definitions['dealCreateParams'],
): ApiResponse {
  const url = 'https://api.freee.co.jp/api/1/deals';

  // API呼び出しを実行します
  const httpResponse = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + token,
      'X-Api-Version': '2020-06-15',
    },
    payload: JSON.stringify(reqBody),
    muteHttpExceptions: true,
  });

  // 結果ステータスを取得します
  const status = httpResponse.getResponseCode();
  switch (status) {
    case 201: {
      const response = JSON.parse(httpResponse.getContentText()) as FreeeResponse.DealResponse;
      return { success: true, id: response.deal.id };
    }
    // 4XX、5XXの処理が入ります
    default: {
      return { success: false, errors: ['API呼び出しエラーです'] };
    }
  }
}

まとめ

今回、社内システムと会計Freeeを連携した、業務効率化・自動化の事例について紹介しました。主に請求管理システムとの連携に関する記事となりましたが、弊社では他にも、給与管理システムや見込み顧客管理システムとのAPI連携も実施しています。

FreeeAPIを用いた社内業務の効率化を検討している方々のご参考になれば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away