Laravel + Misoca API でワンクリックで請求書をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、現状フリーランスとしてある程度長く活動させていただいているのですが、その中でいろいろな人たちに助けていただいて今があります。(心より感謝です✨)

また、「助けていただいてる」のは人だけでなく、いろんな「ウェブサービス」もお世話になってます。例えば、チャットシステムは特に恩恵を受けているのですが、そんな中もう一つ便利なサービスが思い浮かびました。

それが・・・・・・・

Misoca(請求書や見積書をインターネット上で作成できるサービス)

です。

このサービスは使う人のことを考えた「ユーザビリティ」が素晴らしいのでずっと利用させていただいているのですが、つい最近APIがあることを知りました。

そこで❗

今回は、Laravel + Misoca APIを使って「ワンクリックだけで請求書(PDF)を作成する」機能を作ってみたいと思います。

いわゆる業務の効率化というやつですね。
ぜひ何かの参考になりましたら嬉しいです 😊✨

「Misocaは、弥生に買収される
前からお世話になってます」

開発環境: Laravel 8.x、Misoca API v3

準備する

Misoca API

まずMisoca APIが使えるように準備していきます。
Misocaにログインして以下のURLへ移動してください。

OAuth プロバイダー

すると、アプリケーションを登録するページが表示されるので、「新しいアプリケーション」ボタンをクリックします。

続いて、必要な情報を入力するフォームが表示されるので、それぞれ以下のように入力して「登録」ボタンをクリックします。

すると、「アプリケーションID」と「シークレット」「コールバックURL」が取得できるので、これを控えておいてください。(後で使います👍)

各種フォルダを用意する

後でつくるコード内で、以下2つのファイル & フォルダが必要になってきます。

  • 請求書(PDF)を保存するフォルダ
  • リフレッシュ・トークンを保存するファイル

ということで、それぞれstorage内にフォルダをつくっておきます。

storage/app/pdf/invoices

storage/app/oauth2/misoca_refresh_token

⚠ご注意: なお、この2つのフォルダ&ファイルには書き込み権限をつけておいてください。

パッケージをインストールする

コードを書いていく際に「OAuth2 認証」を使うのでパッケージをインストールしておきます。

以下のコマンドを実行してください。

composer require league/oauth2-client

テストデータをつくる

モデル&マイグレーションをつくる

続いて今回の機能に必要な以下2つのテーブルをつくり、そこにテストデータを追加していきます。

  • 取引してる会社: companies
  • 注文内容: orders

※ なお、今回の本題から外れるので商品テーブルや税額、注文日時などは省略しています。

では、以下のコマンドを実行してください。

php artisan make:model Company -m
php artisan make:model Order -m

すると、ファイルが作成されるので中身を以下のように変更します。

database/migrations/****_**_**_******_create_companies_table.php

// 省略

public function up()
{
    Schema::create('companies', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('会社名');
        $table->timestamps();
    });
}

database/migrations/****_**_**_******_create_orders_table.php

// 省略

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('company_id')->comment('会社ID');
        $table->unsignedInteger('amount')->comment('注文金額');
        $table->timestamps();

        $table->foreign('company_id')->references('id')->on('companies');
    });
}

Seederをつくる

続いて、先ほどつくったテーブル内にテストデータを追加していきます。
以下のコマンドを実行してください。

php artisan make:seed CompanySeeder
php artisan make:seed OrderSeeder

そして中身を以下のように変更します。

database/seeders/CompanySeeder.php

<?php

namespace Database\Seeders;

use App\Models\Company;
use Illuminate\Database\Seeder;

class CompanySeeder extends Seeder
{
    public function run()
    {
        for($i = 1; $i <= 10 ; $i++) {

            $company = new Company();
            $company->name = 'テスト会社名'. $i;
            $company->save();

        }
    }
}

database/seeders/OrderSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Company;
use App\Models\Order;
use Illuminate\Database\Seeder;

class OrderSeeder extends Seeder
{
    public function run()
    {
        $company_ids = Company::pluck('id');

        for($i = 0 ; $i < 25 ; $i++){

            $order = new Order();
            $order->company_id = $company_ids->random();
            $order->amount = rand(1, 5) * rand(1, 3) * 100000;
            $order->save();

        }
    }
}

そして、Seederはそのままでは実行されませんので、Laravelへ登録します。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    // 省略

    public function run()
    {
        $this->call(CompanySeeder::class);
        $this->call(OrderSeeder::class);
    }
}

Factoryが好きな人はそちらでも問題ありません👍

では、この状態でデータベースを再構築します。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

実際のテーブルはこうなりました。

コントローラーをつくる

では、コントローラーをつくって請求書を発行する部分を作っていきます。

以下のコマンドを実行してください。

php artisan make:controller InvoiceController

するとファイルが作成されるので、中身を以下のように変更します。

app/Http/Controllers/InvoiceController.php

<?php

namespace App\Http\Controllers;

use App\Models\Company;
use App\Models\Order;
use Illuminate\Http\Request;
use League\OAuth2\Client\Provider\GenericProvider;

class InvoiceController extends Controller
{
    private $provider;

    public function __construct()
    {
        $this->provider = new GenericProvider([
            'clientId' => '(ここにアプリケーションID)',
            'clientSecret' => '(ここにシークレット)',
            'redirectUri' => '(ここにコールバックURL)',
            'urlAuthorize' => 'https://app.misoca.jp/oauth2/authorize',
            'urlAccessToken' => 'https://app.misoca.jp/oauth2/token',
            'urlResourceOwnerDetails' => '',
            'scopes' => 'write'
        ]);

    }

    public function index() {

        $companies = Company::pluck('name', 'id');
        $orders = Order::get();
        $grouped_orders = $orders->groupBy('company_id')->sortKeys();

        return view('invoice.index')->with([
            'companies' => $companies,
            'grouped_orders' => $grouped_orders
        ]);

    }

    public function generate(Request $request) {

        $refresh_token = \Storage::get('oauth2/misoca_refresh_token');

        if($request->filled('code') || !empty($refresh_token)) {

            $company_id = ($request->filled('company_id'))
                ? $request->company_id
                : $request->session()->get('invoice_company_id');

            $grant = '';
            $params = [];

            if(!empty($refresh_token)) {

                $grant = 'refresh_token';
                $params = ['refresh_token' => $refresh_token];

            } else {

                $grant = 'authorization_code';
                $params = ['code' => $request->code];

            }


            $company = Company::find($company_id);
            $orders = Order::where('company_id', $company_id)->get();

            if($orders->isNotEmpty()) {

                try {

                    $token = $this->provider->getAccessToken($grant, $params);
                    $access_token = $token->getToken();
                    $refresh_token = $token->getRefreshToken();
                    \Storage::put('oauth2/misoca_refresh_token', $refresh_token);

                    $this->generateInvoice($access_token, $company, $orders);

                } catch (\Exception $e) {

                    dd($e->getMessage());

                }

            }

        } else {

            $request->session()->put('invoice_company_id', $request->company_id);
            $authorization_url = $this->provider->getAuthorizationUrl();
            return redirect($authorization_url);

        }

    }

    private function generateInvoice($access_token, $company, $orders) {

        // 取引先一覧
        $request = $this->provider->getAuthenticatedRequest(
            'GET',
            'https://app.misoca.jp/api/v3/contacts',
            $access_token,
            [
                'headers' => [
                    'Content-Type' => 'application/json;charset=UTF-8'
                ]
            ]
        );
        $response = $this->provider->getResponse($request);
        $contacts = json_decode($response->getBody()->getContents(), true);
        $contact_id = $contacts[0]['id']; // テストのため1番目で固定

        // 請求書作成
        $params = [
            'subject' => '●年●月のご請求',
            'issue_date' => today()->format('Y-m-d'),
            'contact_id' => $contact_id,
            'items' => []
        ];

        foreach ($orders as $order) {

            $params['items'][] = [
                'name' => '品目(ID: '. $order->id .')',
                'quantity' => 1,
                'unit_price' => $order->amount
            ];

        }

        $request = $this->provider->getAuthenticatedRequest(
            'POST',
            'https://app.misoca.jp/api/v3/invoice',
            $access_token,
            [
                'body' => json_encode($params),
                'headers' => [
                    'Content-Type' => 'application/json;charset=UTF-8',
                ]
            ]
        );
        $response = $this->provider->getResponse($request);
        $invoice_data = json_decode($response->getBody()->getContents(), true);

        $invoice_id = $invoice_data['id'];
        $url = 'https://app.misoca.jp/api/v3/invoice/'. $invoice_id .'/pdf';
        $request = $this->provider->getAuthenticatedRequest(
            'GET',
            $url,
            $access_token,
            [
                'headers' => [
                    'Content-Type' => 'application/json;charset=UTF-8'
                ]
            ]
        );

        $response = $this->provider->getResponse($request);
        $pdf_content = $response->getBody()->getContents();

        $pdf_path = storage_path('app/pdf/invoices/'. time() .'.pdf');
        file_put_contents($pdf_path, $pdf_content);

    }

}

※ 「アプリケーションID」と「シークレット」」「コールバックURL」はあなたのものに置き換えてください。

なお、この中で少し説明をしないといけないのが、取引先一覧の部分です。

実はMisoca APIで請求書や見積書をつくる場合にはcontact_id、つまり取引先のIDが必須なのですが、これは「すでに Misoca で登録された取引先のID」ということになります。

しかし、この部分まで開発してしまうとただでさえ複雑な内容がさらにグッチャグチャになってしまう…💦 ので、今回は「APIで取得した取引先一覧の中から、一番最初のID」を固定で使うようにしています。

※ つまり、少なくとも1件は取引先が登録されていないといけないということになります。

ビューをつくる

続いて、注文があった会社一覧を表示するためのビューをつくります。

resources/views/invoice/index.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>会社名</th>
                <th>請求合計金額</th>
                <th>請求書(PDF)</th>
            </tr>
        </thead>
        @foreach($grouped_orders as $company_id => $orders)

            <tr>
                <td>
                    {{ $companies[$company_id] }}
                </td>
                <td class="text-right">
                    {{ number_format($orders->sum('amount')) }}({{ $orders->count() }}件)
                </td>
                <td>
                    <a class="btn btn-primary" href="{{ route('invoice.generate', ['company_id' => $company_id]) }}" target="_blank">作成する</a>
                </td>
            </tr>

        @endforeach
    </table>
</div>
</body>
</html>

これで開発は完了です😊

テストしてみる

では、実際に請求書をつくってみましょう❗

まず、「http://******/invoice/」にブラウザでアクセスして適当なボタンをクリックしてみます。

すると・・・・・・

以下のようにMisocaの承認ページが表示されますので、「承認」ボタンをクリックします。

(つまり、1回目だけはツークリックです。ちょっと釣りみたいなタイトルでゴメンナサイ😘)

ボタンを押すとリダイレクトされることになります。
では、請求書PDFが作成されてるかチェックしてみましょう。

きちんと保存されています👍

では、Misocaの本家の方でも確認してみましょう。

はい、うまくデータ追加されていますね😊👍
中身もチェックしてみましょう。

はい❗
品目もうまく表示されています。

成功です😊✨

ちなみに、/storage/app/oauth2/misoca_refresh_tokenにリフレッシュ・トークンも書き込まれていました。

次回からはこのリフレッシュ・トークンを使えばウェブ認証なしでも請求書をつくることができます。

全て成功です✨😊👍

企業様へのご提案

今回の機能を使えば以下のようなことが実現できます!

  • 毎月好きな日にちにタイマー実行し、請求書作成を完全自動化できます。
  • 保存したPDFをメールでお取引先へ送信できます。(これも自動化OKです)
  • 自社製品リストにチェックをいれるだけで、見積書を作成することができます。(いちいち品目を入力する必要はありません)
  • APIを使うので、すでに可動しているシステムとの統合もしやすいかと思います。

もしご用意したい機能がございましたら、ぜひお問い合わせからお気軽にご連絡ください。どうぞよろしくお願いいたします。m(_ _)m

おわりに

正直なことを言うと、OAuth2は以前にも使ったことがあったので「どうせ楽にできるだろう(ニッコリ)」と高をくくっていたのですが、甘かったです。。

はじめはSocialiteを使って実装しようとしたのですが、独自拡張するプロバイダーがどうにもうまくいかなかったため、方針転換して今回のパッケージを使うことにしました。

また、Misoca APIの情報があまりにも少なく苦戦したたのですが、そんな中、1件だけとても有益なページに助けられることになりました。

せめてもの感謝の気持ちということで、最後にご紹介させてください。

📝 参考ページ: OAuth2の認証を使う(Misoca API)

迷子になっていたところ、親切に道を教えてくれたような安心感がありました。どうもありがとうございました。m(_ _)m

と、ちょっと難しいかもしれませんがぜひ皆さんもチャレンジしてみてくださいね。

ではでは〜❗


「案件として、
こういう記事が書けたらいいな❗
(お待ちしてます😘)」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly