20200910のlaravelに関する記事は8件です。

【Laravel×Vue.js】リダイレクト時にセッションデータを持ってメッセージを表示

ログインユーザーのみアクセス可能なURL/sample/に非ログイン状態でアクセスした際、
ログインページにリダイレクトし、「この機能を使うためにはログインしてください。」とメッセージを表示させた時の実装です。
/sample/からリダイレクトされた場合のみメッセージを表示する。)

SampleController.php

リダイレクトとともにセッションデータを渡す処理を記載

SampleController.php
namespace App\Http\Controllers;

// 現在のHTTPリクエストインスタンスを取得するため、Illuminate\Http\Requestクラスを指定
use Illuminate\Http\Request;

class SampleController extends Controller
{
    public function __invoke(Request $request)

        // 未ログイン時はloginページにリダイレクト
        // withで「showMessage」というセッションデータを渡す
        if (!\Auth::check()) {
            return redirect('login')->with('showMessage', true);
        }

        // ログインしていれば/sample/を表示
        return view('sample.index');
    }
}

redirect時に、withを使ってshowMessageというBoolean型のデータを持たせている。
(trueのところに'メッセージです。'とすると、テキストメッセージを持たせることも可能。)

これで未ログイン時に/sample/ページにアクセスすると、
/login/にリダイレクトされ、 ページ上でshowMessageの値を使用できる。

login.blade.php

ログイン用のviewであるlogin.blade.phpで、セッションを表示してみる。

login.blade.php
{{ session('showMessage') }}
// 1

/login/にshowMessageの真偽値true が渡っているので、「1」と表示される。
これでセッションデータで値が渡っているのを確認できた。

この値をvue.jsのコンポーネント に渡す処理をかく。

login.blade.php
@section('content')
    <sample-component>
                 :show-message="{{ session('showMessage') ? 'true' : 'false'}} ">
    </sample-component>
@endsection

SampleComponent.vue

コンポーネントが埋め込まれているlogin.blade.php から渡されたセッションデータshow-message を、
親から子がデータを受け取る際に使うprops で受け取っておく。
(vue.jsではケバブケースではなくキャメルケースで受け取る。)

SampleComponent.vue
export default {
  props: {
    showMessage: {
      type: Boolean,
      default: false,
    },
  },
};

ここまできたら、受け取ったshowMessageを使って、
v-if などでtrueだったらメッセージを表示するなどが可能。

SampleComponent.vue
<template>
  <div class="mb-4">
    <div v-if="showMessage">
      <p>この機能を使うためにはログインしてください。</p>
    </div>
    ...省略...
  </div>
</template>

<script>
export default {
...省略...
  props: {
    showMessage: {
      type: Boolean,
      default: false,
    },
  },
};
</script>

これで未ログイン時に/sample/ページから/login/にリダイレクトしてきた時のみ、
showMessageというセッションデータを持って真偽の判定を持たせることができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel + Oracleの環境構築

はじめに

Laradockを使ってLaravel + Oracleの環境を構築する方法です。

手順

以下は~/laracle 以下に環境を構築する想定のコードです。

$ mkdir ~/laracle

OracleのDocker image作成

まず、oracleで配布されているdocker-imageからOracleのimageを作成します。

$ cd ~/laracle
$ git clone https://github.com/oracle/docker-images.git

以下のURLからOracle Databaseのバイナリをダウンロードします。
https://www.oracle.com/database/technologies/oracle-database-software-downloads.html
ダウンロードしたファイルを~/laracle/docker-images/OracleDatabase/SingleInstance/dockerfiles/以下の、対応するバージョンのディレクトリへ移動します。

ビルドします。

$ cd ~/laracle/docker-images/OracleDatabase/SingleInstance/dockerfiles/
$ ./buildDockerImage.sh -v <oracleのversion> -s -i

以下のようにoracleのイメージが表示されていればOKです。

$ docker images | grep oracle
oracle/database                      <oracle-version>                    784b3e60b30a        2 days ago          8.43GB

Laradockの設定

Laradockをクローンします。

$ cd ~/laracle
$ git clone https://github.com/laradock/laradock.git

docker-compose.yamlのservicesにoracleの設定を追加します。imageのバージョンはダウンロードしたoracleに合わせてください。

~/laracle/laradock/docker-compose.yaml
...
### Oracle ################################################
    oracle:
      image: oracle/database:<oracle-version>
      ports:
        - 1521:1521
      environment:
        ORACLE_SID: ${ORACLE_SID}
        ORACLE_PDB: ${ORACLE_PDB}
        ORACLE_PWD: ${ORACLE_PWD}
        ORACLE_CHARACTERSET: ${ORACLE_CHARACTERSET}
      volumes:
        - ${DATA_PATH_HOST}/oracle:/opt/oracle/oradata
      networks:
        - backend
...

env-exampleにも以下のコードを追加します。

~/laracle/laradock/env-example
...
### Oracle ################################################
ORACLE_SID=ORCLCDB
ORACLE_PDB=ORCLPDB1
ORACLE_PWD=secret
ORACLE_CHARACTERSET=AL32UTF8
...

env-exampleをコピーし、.envの以下の項目の値を書き換えます。

$ cp env-example .env
.env
APP_CODE_PATH_HOST=~/laracle/my_project
DATA_PATH_HOST=~/.laradock/my_project
COMPOSE_PROJECT_NAME=my_project
WORKSPACE_INSTALL_OCI8=true
PHP_FPM_INSTALL_OCI8=true
ORACLE_PWD=<Oracleの初期パスワードに設定したい値>

下記コマンドでコンテナを立ち上げます。oracle18cのコンテナのログを確認し、ORACLE_PWD で設定したパスワードが表示されていることを確認してください。
DATABASE IS READY TO USE! が表示されたら初期化は完了なので、ctrl + cで抜けてしまってOKです。

$ docker-compose up -d workspace php-fpm nginx oracle
$ docker-compose logs -f oracle

Attaching to my_project_oracle_1
oracle_1            | ORACLE PASSWORD FOR SYS, SYSTEM AND PDBADMIN: <ORACLE_PWDに設定した値>
oracle_1            |
oracle_1            | LSNRCTL for Linux: Version 18.0.0.0.0 - Production on 09-SEP-2020 07:56:56
...
...
#########################
DATABASE IS READY TO USE!
#########################
..

OracleのUser作成

Laravelで使用するためのユーザーをOracleに作成します。

$ cd ~/laracle/laradock
$ docker-compose exec oracle bash

# dockerコンテナ内
$ sqlplus system/${ORACLE_PWD}@ORCLPDB1
SQL> show con_name
CON_NAME
------------------------------
ORCLPDB1

SQL> create user laracle_user identified by laracle_pass default tablespace users temporary tablespace temp;
User created.

SQL> grant connect, resource to laracle_user;
Grant succeeded.

SQL> alter user laracle_user quota unlimited on users;
User altered.

SQL> exit
Disconnected from Oracle Database 18c Standard Edition 2 Release 18.0.0.0.0 - Production
Version 18.3.0.0.0

作成したユーザーで接続、テーブルの作成などの操作ができることを確認します。

$ sqlplus laracle_user/laracle_pass@ORCLPDB1

SQL> create table test_table ( id varchar2(8) not null primary key);
Table created.

SQL> insert into test_table (id) values ('01234');
1 row created.

SQL> select * from test_table;
ID
--------
01234

SQL> drop table test_table;
Table dropped.

SQL> exit
Disconnected from Oracle Database 18c Standard Edition 2 Release 18.0.0.0.0 - Production
Version 18.3.0.0.0

LaravelからOracleへの接続

まず、laradockのworkspaceコンテナにアタッチし、Laravelプロジェクトを作成します。

$ cd ~/laracle/laradock
# コンテナにアタッチ
$ docker-compose exec workspace bash

# プロジェクト作成
$ composer create-project laravel/laravel ./

続いて、Oracleに接続するために必要なライブラリのインストールと設定を行います。

$ composer require yajra/laravel-oci8

config/app.php に以下を追加します。

config/app.php
Yajra\Oci8\Oci8ServiceProvider::class,

下記コマンドを実行して設定ファイルを書き出します。

$ php artisan vendor:publish --tag=oracle

最後に.env の以下の項目の値を書き換えます。

.env
DB_CONNECTION=oracle
DB_HOST=oracle
DB_PORT=1521
DB_DATABASE=
DB_SERVICE_NAME=ORCLPDB1
DB_USERNAME=laracle_user
DB_PASSWORD=laracle_pass
DB_CHARSET=AL32UTF8
DB_SERVER_VERSION=18c

DBのマイグレートが実行できれば成功です。

$ artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.08 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.02 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.07 seconds)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでcreateを実行してもカラムの値がNULLになってしまうときの解決策

問題

  • Laravelcreateを実行したが、値を指定したのに挿入される値はNULLになっていた。
  • Laravelcreateを実行した際、SQLSTATE[HY000]: General error: 1364 Field (カラム名) doesn't have a default valueが発生した。

解決策

モデルを見直す

自分の場合、モデルで該当のカラムを$fillableの対象に記入していなかった。

app\Model.php
class Model extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 
    //(ここにcreateを実行してもNULLになってしまっていたカラムを指定する)
    ];

原因

Laravelでは、モデルを通してcreateなどを利用しデータベースにレコードなどを保存する際、悪意あるユーザーが勝手にカラムなどを作成し、その人が実質上の管理者とならないように管理者を保護する機能が標準で備わっている。
そのため、$fillableなどでcreateで作成、挿入する対象のカラムを指定しないと、Laravelが悪意あるユーザーかもしれないと思ってしまい、自動でNULLを入れて保護してしまうことが原因だった。

createなどでデフォルトの値を設定していないカラムは、$fillableなどで保護するのを忘れないようにしよう!!

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マネーフォワードクラウド請求書から案件データぶっこ抜いて、Exmentに突っ込み簡易SFA/CRMを行う

SaaSのサービスをAPI同士で繋いで、業務改善アプリ的なものをつくるのが近年の趣味な筆者です。こんにちは。

マネーフォワードクラウド請求書 is 何?

マネーフォワードクラウドは、freeeと人気を二分する、中小企業やフリーランス向けのSaaS会計システムです。筆者は会社勤務の傍ら副業もしてますので、マネーフォワードクラウド確定申告というのを使って、毎年確定申告をしています。

また、関連サービスとしてマネーフォワードクラウド請求書(以下MFクラウド請求書)というサービスもあり、これを使って見積書と請求書を発行しています。

本稿とは別の余談になりますが、なかなかよく出来たサービスですので、請求書発行のサービスを探しておられる方は一度トライアルされてみてはいかがでしょうか。

Exment is 何?

Exmentは、OSSのWeb DBシステムです。SaaSではありませんが、筆者はLightSail + Dockerというクラウド環境で試験運用しています。

詳しくは下記記事をご参照ください。

また、詳しくは開発者の方の記事も参考になると思います。

APIで繋ぎこむ

MFクラウド請求書もExmentもAPIが用意されています。となると繋ぎこんでみたくなるのが人情というもの。

MFクラウド請求書はあくまで請求書発行のためのサービスなので、顧客マスタの概念はあっても、そこに対してのSFAやCRM的な機能はありません。

そこで、APIを使って、汎用的なDBシステムであるExmentにデータをコピーし、Exment上で色々やってみたいと思います。

この記事で実現できること

おおまかには以下です。

  1. 顧客ごとの売上額がわかる
  2. 顧客ごとの見込み額がわかる
  3. 案件ごとの見込み確度がわかる

これらの機能はMFクラウド請求書だけでは数値化・視覚化することができません。

セールスや経営の現場では、これらの情報が日々重要であり、セールス担当者は案件ごと、顧客ごとにこれらの情報を元に効率よく行動することができるようになります。それがSFAやCRMに求められる機能です。

SalesforceやkintoneといったSaaSを契約している企業であれば、それらでやってしまえばいいのでしょうけど、それらを契約できない規模の中小企業や私のような副業ワーカーであれば、Google App Scriptや、今回のExmentでやる、というのが現実的な方向性になると思います。

MF請求書のAPI仕様

MFクラウド請求書は、アクセストークンが必要で、リフレッシュトークンとともに発行される仕組みです。アクセストークン発行の仕組みは、下記記事をご覧ください。

APIドキュメントはこちらです。

ExmentのAPI仕様

ExmentのAPIは、3つの認証方式があります。今回は、API Key方式で行きたいと思います。MFクラウド請求書APIと同じく、アクセストークンとリフレッシュトークンが発行される方式です。

詳しくは下記記事を参考にしてください。

ExmentのAPIドキュメントはこちらです。

実装してみる

さて、いよいよ実装です。まずは、Exment側でカスタムテーブルを作ります。Salesforceでいうところのオブジェクト、kintoneで言うところのアプリ、そしてRDBMSで言うところの、テーブルに相当します。

「顧客リスト」カスタムテーブルの作成

カスタムテーブル作成の実際は、下記記事が詳しいので、ぜひご参照ください。

以下は、私が作成した「顧客リスト」のカスタム列設定です。RDBMSでいうところの、スキーマに相当します。カッコ内は、列種類(Exmentの用語、RDBMSでいうところの「型」に相当)

  • 顧客名(1行テキスト)

以上です。そっけないかもしれませんが、今回は顧客データベースをつくるのではなく、顧客ごとに売上や案件を管理したいだけなので、まずはこれだけにしておきます。

なお、idやcreated_atなどの列は自動的に追加されます。

「案件一覧」カスタムテーブルの作成

続いて、案件のカスタムテーブルを作成します。作成したカスタム列は、以下の通りです。この段階で、MFクラウド請求書API側のレスポンスのどのデータをどの列に突っ込みたいかを考えておきます。今回は、営業的な面で必要なものだけに絞りました。

  • 案件名(一行テキスト)※
  • 顧客名(選択肢(他のテーブルの値一覧から選択))※
  • 金額(税込)(通貨)※
  • 金額(税抜)(通貨)※
  • 確度(選択肢(値・見出しを登録))※
  • 除外(YES/NO)
  • 引き合い日(日付)
  • 見積作成日(日付と時刻)※
  • 見積書更新日(日付と時刻)※
  • 受注日(日付)

※印をつけた項目は、MFクラウド請求書側からのデータを受け付ける列になります。金額に税込と税別があるのも、MFクラウド請求書側がそのプロパティを持っているからですね。

また、確度の項目は、以下のように設定しました。(RDBMSでいうところのenum型ですね)

  • 1,アイデアレベル
  • 2,検討中
  • 3,見積発行
  • 4,商談中
  • 5,意思決定直前
  • 6,受注
  • 7,失注
  • 8,保留

1から6までは、受注確度です。営業用語でいうところの「顧客の温度感」ってヤツです。

アクセストークンを取得しておく

先程リンクしたMFクラウド請求書APIの記事を読んで、アクセストークンを取得します。

実際にはPostmanを使用して取得しました。

本当はプログラム化しておいたほうが良いのですが…

続いて、Exmentのアクセストークン取得です。こちらはプログラム化しました。普段筆者はJavaScriptに慣れ親しんでいるので、今回はNode.jsで書いています。ローカルで運用する前提です。

auth_exment.js
const axios = require('axios')
const fs = require('fs')
const moment = require('moment')

const isExistFile = (file) => {
  try {
    fs.statSync(file);
    return true
  } catch(err) {
    if(err.code === 'ENOENT') return false
  }
}

const getTokens = async(refresh_token) => {
  let body = ''

  if(refresh_token) {
    body = {
      grant_type: 'refresh_token',
      client_id: 'df013b70-f1aa-11ea-8af3-c94ebdad7ad4',
      client_secret: 'PSXXuKY8R3MrllA8EFSqPYtX3o3Oj7RP8s1cQBy1',
      refresh_token: refresh_token
    }
  } else {
    body = {
      grant_type: 'api_key',
      client_id: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      client_secret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      api_key: 'key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      scope: 'me value_read value_write'
    }
  }


  await axios.post('https://exmaple.com/oauth/token', body, {
    headers: {
      'Content-Type': 'application/json'
    }
  })
  .then(res => {
    const at = res.data.access_token
    const rt = res.data.refresh_token
    const expires = res.data.expires_in

    const expireDay = moment().add(expires, 's').format()

    const data = {
      access_token: at,
      refresh_token: rt,
      expires_in: expireDay
    }
    fs.writeFileSync('./exment_tokens.txt', JSON.stringify(data))
    return { tokens: data }
  })
  .catch(err => {
    console.log(err)
  })
}

;(async() => {

  let tokens = {}

  if(isExistFile('tokens.txt')) {
    // 既にトークンが存在するとき
    tokens = JSON.parse(await fs.readFileSync('exment_tokens.txt'))

    // トークンが期限切れの時
    if(moment().isAfter(tokens.expireDay)) {
      tokens = {}
      getTokens(tokens.refresh_token) // リフレッシュトークンを使って、再取得
      tokens = JSON.parse(await fs.readFileSync('exment_tokens.txt'))
    }

  } else {
    // トークンが存在しない時
    getTokens()
    tokens = JSON.parse(await fs.readFileSync('exment_tokens.txt'))
  }

  console.log(tokens)
  return tokens

})()

やってることは単純で、トークンの情報が書かれたテキストファイルが存在しなければ、初回トークン作成のPOSTをaxiosで投げます。そして取得したトークンの情報をローカルにテキストファイルで保存します。

顧客リスト取得とExmentに突っ込むプログラムを書いてみる

引き続き、顧客リスト取得のプログラムです。これもローカルで1回実行することが前提です。ホスティングして定期的に回すことは今のところ前提としていません(本当はそこまでやりたいですけど)

get-clients.js
const axios = require('axios')
const fs = require('fs')

;(async() => {
  const endpoint = 'https://invoice.moneyforward.com/'
  const query = 'api/v2/partners'

  await axios.get(endpoint + query, {
    headers: {
      'Accept': "application/json",
      'Authorization': 'Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 
    }
  })
  .then(async (res) => {
    console.log(res)
    let items = []
    res.data.data.forEach( item => {
      items.push({
        parent_type: '',
        value: {
          name: item.attributes.name
        }
      })
    })

    const tokens = JSON.parse(await fs.readFileSync('./exment_tokens.txt'))
    const exmentEndpoint = 'https://exmaple.com/api/data/clients'
    await axios.post(exmentEndpoint, { data: items },   {
      headers: {
        'Authorization': 'Bearer ' + tokens.access_token
      }
    })
    .then(res => {
      console.log(res)
    })
    .catch(err => {
      console.log(err)
    })
  })
  .catch(err => {
    console.log(err.errors)
  })
})()

MFクラウド請求書APIのアクセストークンがベタ書きです。いけませんね。良い子は真似しちゃダメです。dotenvなどで適切に処理しましょう。

それ以外の処理についてですが、

  1. 空の配列itemsにMFクラウド請求書APIをaxiosのGETで叩いた結果を収める
  2. itemsをペイロードに詰め、axiosのPOSTでExmentのAPIに投げる

というのがおおまかな流れです。itemsに突っ込む時は、ExmentのAPIドキュメントにあるカスタムデータ新規作成の記事を読んで

2020-09-10_13h54_04.png

上図は、突っ込んでみた結果です。うまく行きました。

案件(見積書)データを取得とExmentに突っ込むプログラムを書く。

引き続き、Node.jsです。

set-project.js
const axios = require('axios')
const fs = require('fs')
const _ = require('lodash')
const moment = require('moment')

;(async() => {
  const endpoint = 'https://invoice.moneyforward.com/'
  const query = '/api/v2/quotes'

  await axios.get(endpoint + query, {
    headers: {
      'Authorization': 'Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 
    }
  })
  .then(async (res) => {
    // console.log(res)    
    // クライアント名とIDの紐付けデータ作成
    const tokens = JSON.parse(await fs.readFileSync('./exment_tokens.txt'))
    const exmentClietnsEndopoint = 'https://exmaple.com/api/data/clients'
    let clients = []

    await axios.get(exmentClietnsEndopoint, {
      headers: {
        'Authorization': 'Bearer ' + tokens.access_token
      }
    })
    .then(res => {
      res.data.data.forEach(item => {
        clients.push({
          id: item.id,
          name: item.value.name
        })
      })
    })

    let items = []

    const status = async (item) => {
      if(item.attributes.order_status === 'default') {
        return 3 // 見積作成フラグ
      } else if(item.attributes.order_status === 'received') {
        return 6 // 受注フラグ
      } else if(item.attributes.order_status === 'failure') {
        return 7 // 失注フラグ
      }
    }

    const getId = async (item) => {
      const client = _.find(clients, { name: item.attributes.partner_name })
      if(client) {
        return client.id
      } else {
        return 8 // 取引停止
      }
    }

    const exclude = async (cond) => {
      if(cond === 8) {
        return true
      } else {
        return false
      }
    }

    for(let i = 0; i < res.data.data.length; i++) {

      let pdf = ''
      await axios.get(res.data.data[i].attributes.pdf_url, {
        responseType: 'arraybuffer',
        headers: {
          'Authorization': 'Bearer bbbf071b178fb29f1be08d41f3aad341ba599433f58d795d937266fd8d11dfda'
        }
      })
      .then(res => {
        console.log('get pdf succeeded')
        pdf = new Buffer.from(res.data, 'binary').toString('base64')
      })
      .catch(err => console.log(err))


      const payload = {
        value: {
          name: res.data.data[i].attributes.title,
          client: await getId(res.data.data[i]),
          amount: Math.floor(Number(res.data.data[i].attributes.total_price)),
          sub_amount: Math.floor(Number(res.data.data[i].attributes.subtotal)),
          reliability: await status(res.data.data[i]),
          exclude: await exclude(await getId(res.data.data[i])),
          estimate_created_at: moment(res.data.data[i].attributes.created_at).format('YYYY-MM-DD HH:mm:ss'),
          estimate_updated_at: moment(res.data.data[i].attributes.updated_at).format('YYYY-MM-DD HH:mm:ss'),
        }
      }

      const exmentEndpoint = 'https://exmaple.com/api/data/projects'
      await axios.post(exmentEndpoint, { data: [payload] }, {
        headers: {
          'Authorization': 'Bearer ' + tokens.access_token
        }
      })
      .then(async (res) => {
        // console.log(res.data[0].id)
        await axios.post('https://exmaple.com/api/document/projects' + '/' + res.data[0].id, { name: JSON.parse(res.config.data).data[0].value.name + '_見積書.pdf', base64: pdf }, {
          headers: {
            'Authorization': 'Bearer ' + tokens.access_token
          }
        })
        .then(res => {
          console.log(res)
        })
        .catch(err => {
          console.log(err.response.data.errors)
        })
      })
      .catch(err => {
        console.log(err)
      }) 

    }
  })
  .catch(err => {
    console.log(err)
  })
})()

今回はちょっとばかり複雑なのと、axiosのコールバックがネストしまくってて、これもあまり良くありません。まあ、ワンオフ1回きりのコードなので、大目に見てください…

さて、各部の説明です。

set-project.js
  const endpoint = 'https://invoice.moneyforward.com/'
  const query = '/api/v2/quotes'

  await axios.get(endpoint + query, {
    headers: {
      'Authorization': 'Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 
    }
  })
  .then(async (res) => {
    // console.log(res)    
    // クライアント名とIDの紐付けデータ作成
    const tokens = JSON.parse(await fs.readFileSync('./exment_tokens.txt'))
    const exmentClietnsEndopoint = 'https://exmaple.com/api/data/clients'
    let clients = []

    await axios.get(exmentClietnsEndopoint, {
      headers: {
        'Authorization': 'Bearer ' + tokens.access_token
      }
    })
    .then(res => {
      res.data.data.forEach(item => {
        clients.push({
          id: item.id,
          name: item.value.name
        })
      })
    })

まず、axiosのGETでクライアント名一覧をMFクラウド請求書側から取得しています。まあ、Exment側から取っています… というか、これ、今気づいたんですが、Exmentに既に顧客テーブル作ったんだから、そっちから取得しても良かったんじゃね? って思いました… 次回からそうしよ。

次に初回axiosのコールバックで、またまたaxiosのGETでExmentのAPIを叩き、Exmentの顧客id(先程の挿入時に自動採番されている)を取得します。

最後に、両者を紐付けて、空の配列clientsにオブジェクトで突っ込みforEachで回します。

set-project.js
let items = []

    const status = async (item) => {
      if(item.attributes.order_status === 'default') {
        return 3 // 見積作成フラグ
      } else if(item.attributes.order_status === 'received') {
        return 6 // 受注フラグ
      } else if(item.attributes.order_status === 'failure') {
        return 7 // 失注フラグ
      }
    }

    const getId = async (item) => {
      const client = _.find(clients, { name: item.attributes.partner_name })
      if(client) {
        return client.id
      } else {
        return 8 // 取引停止
      }
    }

    const exclude = async (cond) => {
      if(cond === 8) {
        return true
      } else {
        return false
      }
    }

let items = []でからの配列を作成しています。

status関数は、それぞれの案件がどういう状態にあるかを定義する関数です。見積書があるということは、Exment側の「確度」項目では「3の見積作成」に相当するので、3を返すように。受注したものは6を、失注は7を返すようにしました。

getId関数は、顧客名から、Exmentの顧客idを取得できるようにするものです。lodashのfindメソッドを使って紐付けしています。

clientのid8番は、取引停止という特殊なクライアントです。MFクラウド請求書側で顧客データを削除してしまった場合は、顧客名が空欄になるので、Exment側で取引停止というクライアントを作成して対応させています。

exclude関数は、そもそも取引停止になっている案件の見積書は、除外しておきたいよね、という目的で作成しました。Exment側の「除外」列に相当します。クライアントid8番(つまり、取引停止)の場合は、除外列にtrueをセットします。

set-clients.js
for(let i = 0; i < res.data.data.length; i++) {

      let pdf = ''
      await axios.get(res.data.data[i].attributes.pdf_url, {
        responseType: 'arraybuffer',
        headers: {
          'Authorization': 'Bearer bbbf071b178fb29f1be08d41f3aad341ba599433f58d795d937266fd8d11dfda'
        }
      })
      .then(res => {
        console.log('get pdf succeeded')
        pdf = new Buffer.from(res.data, 'binary').toString('base64')
      })
      .catch(err => console.log(err))


      const payload = {
        value: {
          name: res.data.data[i].attributes.title,
          client: await getId(res.data.data[i]),
          amount: Math.floor(Number(res.data.data[i].attributes.total_price)),
          sub_amount: Math.floor(Number(res.data.data[i].attributes.subtotal)),
          reliability: await status(res.data.data[i]),
          exclude: await exclude(await getId(res.data.data[i])),
          estimate_created_at: moment(res.data.data[i].attributes.created_at).format('YYYY-MM-DD HH:mm:ss'),
          estimate_updated_at: moment(res.data.data[i].attributes.updated_at).format('YYYY-MM-DD HH:mm:ss'),
        }
      }

      const exmentEndpoint = 'https://exmaple.com/api/data/projects'
      await axios.post(exmentEndpoint, { data: [payload] }, {
        headers: {
          'Authorization': 'Bearer ' + tokens.access_token
        }
      })
      .then(async (res) => {
        // console.log(res.data[0].id)
        await axios.post('https://exmaple.com/api/document/projects' + '/' + res.data[0].id, { name: JSON.parse(res.config.data).data[0].value.name + '_見積書.pdf', base64: pdf }, {
          headers: {
            'Authorization': 'Bearer ' + tokens.access_token
          }
        })
        .then(res => {
          console.log(res)
        })
        .catch(err => {
          console.log(err.response.data.errors)
        })
      })
      .catch(err => {
        console.log(err)
      }) 
    }

いよいよ回していきます。ちょっとこの下りが複雑で冗長なのですが、大まかな流れとしては以下です。

  • 見積書PDFの取得 from MFクラウド請求書API
  • PDFデータをbase64エンコードして、空の変数に突っ込む
  • ペイロードに突っ込むデータを作る。金額は小数点でMFクラウド請求書APIから返ってくるので、Math.floorで丸めておく。
  • ExmentのAPIにaxiosのPOSTで1件のデータを突っ込む
  • そのレスポンスで返ってきた案件idを利用し、コールバックでもう一度axiosのPOSTで、今度は個別の案件に対してPDFを突っ込む

という処理をしています。

ExmentのAPI側は、複数案件をオブジェクトに収めて投げても受け付けてくれるのですが、これだとまとめて見積書PDFを収めることができません。そこで、1案件ごとに各フィールドを埋めるペイロードを用意し、POSTするようにし、axiosのコールバックとレスポンスで返ってくるid番号を利用してPDFだけあとから突っ込むということを実現できました。(いやー、コールバックとレスポンスって、こういうことのために使うんですねぇ。初めてこんなことしたので、勉強になりました)

なお、PDFを添付ファイルに添付する必要がなければ、複数案件まとめてAPIに投げちゃってもいいかな、と思います。その方が早いしリクエストも1回で済みますしね。

なお、PDF取得の処理にそれなりに時間がかかるので、Exment側(というかLaravel)のAPIコール上限には達しませんでしたが、必要があればExment側のAPIコール上限を緩和する設定などを施してください。

さて、無事データが突っ込めました。下記のようになってるでしょうか

2020-09-10_14h41_30.png

(実際にはこの画像、突っ込んだ後に編集しているので、厳密には異なる内容となるはずです)

Exment側でビューを作っていく

さて、ここまで来たらあとはNoCodeです。

Exmentにはビューという機能があります。デフォルトでは全件ビューというのになっていて、スプレッドシートのような外観をしています。

カスタムビューは、これらのデータを特定の条件に従ってフィルタリングしたり計算したりできる機能です。

フィルターだけなら全件ビューでもできるのですが、それを保存してユーザー間で共有できるところがビューの強みになります。

なお、計算ができるといっても簡易なものなので、複雑な処理をしたければ、APIからデータを取得して、処理するアプリケーションの開発をする必要があります。

さて、今回は以下のビューを作ってみます。

  1. 顧客別売上(今年)
  2. 顧客別売上(昨年)
  3. 見込み総額
  4. リードタイム

顧客別売上(今年)

案件情報画面に入り、右上にある「テーブル詳細設定」ボタンを押します。次に、出てきたモーダル内の「ビュー設定」
を押します。カスタムビュー設定という画面になるので、右上の「+新規」を押します。

またモーダルが出てくるので「集計ビュー新規作成」を押します。

「カスタムビュー設定 作成」という画面になるので、まずはビュー表示名のところを「顧客別売上(今年)」にします。

グループ列選択、集計列選択、データ表示条件は、以下のように設定しましょう。

2020-09-10_14h54_04.png

グループ列選択

集計したい軸を選択します。今回は顧客ごとに金額を集計するので、「顧客名」を選択しています。

集計列選択

集計したい値を選択します。今回は顧客ごとの金額ですので、「金額(税込)」と「金額(税抜)」を選択しました。

データ表示条件

列の値を使って、集計する対象を選定します。フィルタリングですね。

なお、今更なんですが、案件情報の列について、そのもたせた意味についてちょっと説明です。

除外

副業というか、受託をやってると「ちょっと見積書だけほしいんだけど(発注するかどうかは怪しいけど)」みたいなケースがあります。実際には、クライアントが補助金や助成金ありきのサイト制作を考えていて、その申請書類として見積書がほしいということでした。

そういう案件は受注するといいのですが、結構な金額で実際には受注しないということも多いので、営業戦略的には邪魔なデータとなってしまいます。

そんな案件は、手動で除外したいので「除外」というフラグを持たせています。また、取引停止の見積書もこの設定にしたのは先述の通りです。

確度

ここでは、確度ステータスは受注したか否かの判定に使っています。

受注日

これも営業戦略的には非常に重要で、月や四半期の売上目標の判定に必要です。今回は「今年」という期間なので、受注日が今年の範疇に含まれるものを設定しています。

出来たビューを見てみる

2020-09-10_15h08_21.png
こんな感じです。筆者は副業ということもあり、アクティブな取引先はそんなに多くないのですが、取引先の多い企業では、ここがずらずら~っと並んで、取引額の多い順番に並ぶということですね。

実際の営業現場では、取引先の金額が高い順番に、手厚い対応を取ることになると思います。

顧客別売上(昨年)

ということは、集計ビューを作った時に、データ表示条件で受注日を絞ったところを「去年」にすれば昨年の売上も出るわけですね。

一度作ったビューはコピーすることができるので、顧客別売上(今年)を複製して、最後の設定だけ変更してみましょう。

カスタムビュー設定画面の右端「操作」列の、2枚の紙が重なったアイコンをクリックすると、そのビューを複製することができます。

簡単ですね。

見込み総額

先程までは、売上、つまりこれまでの数値を可視化しました。営業的には「これから」の数値である「見込み総額」を算出したいと思います。

見込み総額ビューの設定は下記の通りです。

2020-09-10_15h22_46.png

売上の時と考え方は同じで、フィルタリングする条件で、確度を受注以前状態の値に設定しています。

リードタイム

リードタイムとは、引き合いをもらってから、受注に至るまでの期間のことです。見積り金額が増えれば増えるほどリードタイムが伸びる傾向にあります(それだけ大きいプロジェクトなので、顧客の意思決定にも複数人が関わる)。リードタイムが短い価格低めの案件を大量に獲るか、長めの案件を獲るかの基準づくりや、リードタイムかかりすぎの案件を洗い出すための判断基準として重要です。

今回の例では、引き合い日と受注日が設定されている案件が対象となります。リードタイムでは、カレンダービューという機能を使います。設定は下図の通りです。

2020-09-10_16h08_33.png

すると、こんな風にカレンダー表示してくれます。

2020-09-10_16h09_38.png

ま、正直この見せ方はベストではないと思うんですが、感覚的にリードタイムを掴むには良いのかな、と思いました。もう少し正確に把握するためには、やはりmomentとかで日付計算をして「○○万円以上の案件で△日以上リードタイムかかってたらアラートをだす」みたいなアプリを開発すべきでしょう。

これはひとつ、簡易的なやりかたということで。

まとめ

見積書・請求書に特化したサービスであるMFクラウド請求書を、汎用WebDBであるExmentと連携させて、簡易的なSFA/CRMにしてみました。SFA/CRMと名乗るにはまだまだ機能が足りないのですが、取り敢えず

  • どんぐらい稼げてるか
  • 太客はどこか
  • 案件獲得にどれくらいかかってるか

などは、これで洗い出せることになりました。

Google App Script + Googleスプレッドシートなどもいいですが、Exmentはカスタムビューの機能が強力だな、と思い今回は活用してみました。

Node.jsのくだりも、ただ今回インポートするだけの機能しかないので、今後は定期的に実行してMFクラウド請求書側のマスタとExment側のマスタが同期するような仕組みも作ってみたいと思います。

また、双方のAPIの仕組みもなんとなくわかっていただけたのではないでしょうか。双方ともに基本的なCRUDが出来るので、やりようによってはもっと高度な分析や日々の運用もこなせると思います。

これを機会に、MFクラウド請求書やExmentのユーザーが増えてくれると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】シーディングの私的まとめ

Laravel公式ドキュメント-シーディングの私的まとめ

シーディング

  • シーダークラスDBにテストデータを設定できる
  • シーダークラスは好きな名前をつけられる
  • DatabaseSeederクラスでシーディングの順番をコントロールできる

シーダクラス定義

シーダ作成
php artisan make:seeder UserSeeder

作成されたシーダはdatabase/seederに保存される

このシーダクラスのrunメソッドしかない
このメソッドに処理を書くことでデータを設定できる

シーダの呼び出し

DatabaseSeeder.php
public function run()
{
    $this->call([
        UserSeeder::class,
        PostSeeder::class,
        CommentSeeder::class,
    ]);
}

callメソッドで呼び出すことでシーダを実行できる

シーダの実行

db:seedを使う

Composerのオートローダを再生成するために以下コマンドを実行
これをやらないと作成したクラスガオーとローダに反映されない

composer dump-autoload

DBへの初期値設定のために以下コマンドを実行

php artisan db:seed

これはデフォルトでDatabaseSeederクラスを実行する
特定ファイルを実行したい場合は--classオプションを使う

php artisan db:seed --class=UserSeeder

これでUserSeederクラスが実行される

migrate:freshを使う

またmigrate:freshを使っても初期値を設定できる。

php artisan migrate:fresh --seed

migrate:freshはDBを完全に作り直したいときに使える(最初にロールバックしてマイグレーションを実行するため)

ここまでを軽くやってみる

シーダはこれ

TestTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class TestTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $param=[
            'id'=>12,
            'stringCoslumn'=>'てすと',
        ];
         DB::table('testtable')->insert($param);
    }
}

シーダ作成からデータ設定までを軽くやってみると以下のようなエラーが出た

$ php artisan db;seed --class=TestTableClass
Target class [TestTableSeeder] does not exist.

TestTableSeederを作成したのがだそれが見つからないという

原因

composer dump-autoload

を実行していなかった

ぐぐったらすぐわかりました
参考:追加したはずのSeederが Class TableSeeder does not exist とか言われる

しかしエラー

$ composer dump-autoload
$ php artisan db:seed
Target class [TestTableSeeder] does not exist.

またも見つからないと言われDBを作り直すrefreshもやってみたが駄目だった

$ php artisan migrate:fresh --seed
Target class [TestTableSeeder] does not exist.

試行錯誤

シーダファイルのインサート分を消すとシーダファイルの認識はされている

TestTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class TestTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $param=[
            'id'=>12,
            'stringCoslumn'=>'てすと',
        ];
         //DB::table('testtable')->insert($param);
    }
}

$ php artisan db:seed
Seeding: TestTableSeeder
Seeded:  TestTableSeeder (0 seconds)
Database seeding completed successfully.

もうダメだお手上げだと思ってしばらく放置

そしてなぜか時間をおいたら実行できた

もう訳がわからない

ただし

もう一度ドキュメント通りに進めてみたら今度は普通にデータがセットできた
なぜ上記の状態ではできなかったのかは全くわからないが今は良しとする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでneo4jを使ってみる②Loginをneo4jに対応してみる

※モデルファイルをapp/Models内に移動しています。
※参考【Laravel】モデルのディレクトリ構成変更についてのメモ

②ログイン機能を対応してみる

1)ログイン機能の有効化

下記コマンドをプロジェクトで実行

php artisan make:auth

2)User.phpの変更

app/Models/User.php※フォルダ構成変更していない場合はapp/User.php
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
//use Illuminate\Foundation\Auth\User as Authenticatable;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

use Vinelab\NeoEloquent\Eloquent\Model as NeoEloquent;

class User extends NeoEloquent implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use Notifiable,Authenticatable, Authorizable, CanResetPassword;

    protected $label = 'User';
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

【説明】
継承元の変更
元々UserクラスはLaravelフレームワークのAuthenticatableを継承していますが、こちらがNeoEloquentから派生していないため、
こちらで書き直しています。
モデルクラスがNeoEloquentから派生していない場合はエラーが発生します。

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

use Vinelab\NeoEloquent\Eloquent\Model as NeoEloquent;

class User extends NeoEloquent implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract

ラベルの設定
こちらを設定することで挿入されたデータのラベルがUserになります。
設定しない場合はAppModelsUserになりました。

protected $label = 'User';

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでneo4jを使ってみる①設定編

①設定

・laravel バージョン 5.6
・neoEloquent バージョン 1.4.6

1)Laravelプロジェクトをバージョン指定して作成

composer create-project "laravel/laravel=5.6.*" [プロジェクト名]

2)Vinelab/NeoEloquentの設定

composer require vinelab/neoeloquent 1.4.6

※neo4jの設定がないと怒られる場合、先にconfig/database.phpの設定をしてキャッシュクリア。
 

3)neo4jデータベースの利用設定

config/database.phpにneo4jの設定を追加

    'default' => env('DB_CONNECTION', 'neo4j'),
   'connections' => [
       ~
       'neo4j' => [
            'driver' => 'neo4j',
            'host'   => env('DB_HOST', 'ホスト名'),
            'port'   => env('DB_PORT', '7474'),
            'username' => env('DB_USERNAME', "neo4jユーザ名"),
            'password' => env('DB_PASSWORD', "neo4jパスワード")
        ],

.envの設定を変更

DB_CONNECTION=neo4j
DB_HOST=ホスト名
DB_PORT=7474
DB_USERNAME=neo4jユーザ名
DB_PASSWORD=neo4jパスワード

参考
NeoEloquent

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

�【Laravel】Base64エンコードされた画像データをファイルに変換してバリデーションする

はじめに

画像アップロードAPIを実装する際、webだと「multipart/form-data形式」なことが多いですが、アプリは「Base64形式」で処理したいといったことがあります(ありました)

Base64をバリデーションしたい

Laravelはバリデーションが豊富なので、Base64もいい感じに処理してくれるかなーと思ったのですが、そんなに甘くはありませんでした。

ググったら、Base64用のバリデーション書け的な感じで、そうなるとアップロード処理もbase64用に書く必要がありそうです。
すでに画像ファイルのアップロード処理は実装済だったので、バリデーション前にBase64を画像ファイルに変換してあげることができればよさそうです。

Base64をファイルに変換して処理する

「multipart/form-data」で送信したファイルをFormRequestでdd()してみると、UploadedFileオブジェクトであることがわかりました。
FormRequestでバリデーション前に何か処理をするにはvalidationData()に記述します。ここでBase64をUploadedFileに変換してあげます。

せっかくなのでパラメータを2つ用意し、どちらの形式でもアップロードできるようにしました。

  • imageFile:multipart/form-data形式
  • imageBase64:Base64形式
<?php

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\File\File;

class UploadImageRequest extends FormRequest
{
    public function validationData()
    {
        $all = parent::validationData();

        // imageBase64パラメータがあればUploadedFileオブジェクトに変換してimageFileパラメータに上書きする。
        if ($this->has('imageBase64')) {
            // base64をデコード。プレフィックスに「data:image/jpeg;base64,」のような文字列がついている場合は除去して処理する。
            $data = explode(',', $this->get('imageBase64'));
            if (isset($data[1])) {
                $fileData = base64_decode($data[1]);
            } else {
                $fileData = base64_decode($data[0]);
            }

            // tmp領域に画像ファイルとして保存してUploadedFileとして扱う
            $tmpFilePath = sys_get_temp_dir() . '/' . Str::uuid()->toString();
            file_put_contents($tmpFilePath, $fileData);
            $tmpFile = new File($tmpFilePath);
            $file = new UploadedFile(
                $tmpFile->getPathname(),
                $tmpFile->getFilename(),
                $tmpFile->getMimeType(),
                0,
                true // Mark it as test, since the file isn't from real HTTP POST.
            );
            $all['imageFile'] = $file;
        }

        return $all;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'imageFile' => 'nullable|image|mimes:jpeg|max:5000|dimensions:max_width=1200,max_height=1200,ratio=1/1', // ファイルのバリデーションよしなに。
            'imageBase64' => 'nullable|string', // 画像データをbase64で文字列としても受け入れる。バリデーションルールはimageFileが適用される。
        ];
    }
}

あとは$request->validated()['imageFile']を使ってアップロード処理を実装するだけです。
共通化できて最&高 DJ KOO イージードゥーダーンス!!

さいごに

テスト用にファイルをbase64に変換して送信するフォームを作ったので載せておきます。

<html>
<head>
</head>
<body>
    <form action="http://localhost/uploadImage" method="post" enctype="multipart/form-data">
        <p>アップロードするファイルを選択して下さい。</p>
        <p><input type="file" name="imageFile"></p>
        <input type="submit" value="保存">

        <p>base64で送る用<input id="file" type="file"></p>
        <div id="result"></div>
    </form>
<script>
    var file = document.getElementById('file');
    var result = document.getElementById('result');

    function loadLocalImage(e) {
        // ファイル情報を取得
        var fileData = e.target.files[0];

        // 画像ファイル以外は処理を止める
        if (!fileData.type.match('image.*')) {
            alert('画像を選択してください');
            return;
        }

        // FileReaderオブジェクトを使ってファイル読み込み
        var reader = new FileReader();
        // ファイル読み込みに成功したときの処理
        reader.onload = function () {
            // ブラウザ上に画像を表示する
            var img = document.createElement('img');
            var base64_string = reader.result;
            img.src = base64_string;
            result.appendChild(img);
            var input_hidden = document.createElement('input');
            input_hidden.type = 'hidden';
            input_hidden.name = 'imageBase64';
            input_hidden.value = base64_string;
            // input_hidden.value = base64_string.replace('data:image/jpeg;base64,', ''); // こっちでもいける。
            result.appendChild(input_hidden);
        }
        // ファイル読み込みを実行
        reader.readAsDataURL(fileData);
    }

    // ファイルが指定された時にloadLocalImage()を実行
    file.addEventListener('change', loadLocalImage, false);
</script>
</body>
</html>

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む