20200910のNode.jsに関する記事は6件です。

Sequelize で名前を指定してmigrationを実行する

sequelize db:migrate:status // でmigration nameの一覧表示
sequelize db:migrate --name <migration name> // migration 実行
sequelize db:migrate:undo --name <migration name> // rollback
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sequelize で名前を指定してmigrationをrollbackする

sequelize db:migrate:status // でmigration nameの一覧表示
sequelize db:migrate:undo --name <migration name> // rollback
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

同僚の作成したdockerコンテナをbuildする際にコケたポイントメモ

初めに

同僚が作成してくれたnodeのdockerコンテナをbuildする際に何点かコケたポイントがありましたのでメモしてみます。
同様のケースで嵌っている方がいたら参考になれたらと思います!

前提

コンテナを作成してくれた人のPC
Mac Pro
こちらの環境では一発docker-compose up --buildを流せばbuildとupが通る
私のPC
Windows10 Pro
npm --version
6.14.5
数回コケる
コンテナの構成
Node→image: node:latest
MySQL→image: mysql:5.7
Nginx→image: nginx:latest

Cannot create container for service mysql: Conflict

image.png

既にmysqlという名のコンテナが存在するからコンフリクトするぞというエラーですね。
他のnodeやnginxも同様に発生しました。
良くデフォルトの名前でコンテナ名をつける事が多いので、プロジェクト毎にコンテナ名をAservice_mysqlとかユニークな名前を指定するとスムーズかも知れませんね。
既に存在したmysqlコンテナはもう殆ど動かないプロジェクトのものでしたので、

docker rm コンテナID

削除しました。

npm ERR! enoent ENOENT: no such file or directory, rename '/src/node_modules/constantinople' -> '/src/node_modules/.constantinople.DELETE'

constantinopleが見つからないと怒られました。
constantinopleは定数の評価で使用するモジュールのようですね。
https://www.npmjs.com/package/constantinople
使用しているnpmのバージョンを最新にアップデートし解決しました。
6.14.5→6.14.8

npm install -g npm

仮にこの方法で直らなかった場合、単にnpm install時にmoduleの中身が破損しているパターンや、package-lock.jsonが各モジュールを良くない組み合わせのバージョンで固定しているパターンが考えられるので、
node_modulesの中身をすべて削除する
package-lock.jsonを削除して再度npm installする
を試してみてください。

npm ERR! Maximum call stack size exceeded

Nodeの最大コールスタックサイズを超えると発生するようです。
何でMacで起きないのにWindowsでは起きるんだ・・・。
npmのキャッシュを削除して再度buildしたら問題なく起動することができました。

npm cache clean --force

参考:https://www.it-swarm.dev/ja/javascript/npm%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A7%E6%9C%80%E5%A4%A7%E3%82%B3%E3%83%BC%E3%83%AB%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%82%B5%E3%82%A4%E3%82%BA%E3%82%92%E8%B6%85%E3%81%88%E3%81%BE%E3%81%97%E3%81%9F/829336672/
ちなみにnodeのstackサイズを変更すれば暫定的にこのコールスタックサイズ問題は解決しますが、
Nodeのパフォーマンスが悪くなる、最悪動かなくなる恐れがあるので止めた方が無難みたいです。。

最後に

同じ開発環境を瞬時に構築できるdockerは開発シーンに必須といえるツールになっていますね!
異なるOSでも動くのは素敵です。

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

え!? わずか3分でローカルにTypeScriptの実行環境を!?

できらぁ!(様式美)

ということでローカルにTypeScriptの実行環境を作ります。すぐできます。

TypeScriptを使うだけなら、TypeScript playground等を使えばいいと思うのですが、「○○のパッケージを試したい。ついでだからTypeScriptも使いたい」という欲張りさんはローカルに環境構築したくなることもあるでしょう。え? codesandbox? 知らんなぁ。

とにかくローカルにTypeScriptの実行環境を作っていきます。ゴールは「コンパイルして出来たjsファイルをnodeコマンドで実行するところ」までです。

事前準備

以下は事前に準備できてるとします。出来てない方は適当にググってください。

  • node, npm(yarn)

ローカルにTypeScriptの実行環境を作成

ここから本題です。
あと私はyarn派なのでyarnを使います。

プロジェクトの作成

プロジェクトディレクトリを作成して、package.jsonを作りましょう。

$ mkdir typescript_try
$ cd typescript_try/
$ yarn init

yarn init後に色々聞かれますが、とりあえず全部Enterで良いです。(ちゃんと設定したい人はしてください)

TypeScriptのインストール

$ yarn add -D typescript @types/node

typescript等はもちろん開発でしか使わないので、-Dはつけましょう(すぐ忘れる)

tsconfig.json

これが無いとコンパイル出来ないので作ります。ルートディレクトリに置いて下さい。

$ touch tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "module": "CommonJS",
    "outDir": "dist", // コンパイル後に生成されるJSファイルの置き場所をTSCに指示
    "sourceMap": true,
    "strict": true,
    "target": "ES2015"
  },
  "include": ["src"] // TSCがTypeScriptファイルを見つけるためにどのディレクトリを探せば良いか?の指定
}

なおこの内容は、オライリーのTypeScript本の2.3.1 tsconfig.json に記載された内容をベースに作成しました。(yarn tsc --initでも内容は異なりますが最低限のものを作れます。こっちの方が普通かも?)

詳細は割愛しますが、以降の説明に関連する2つのパラメータに関してはjsonのコメントに説明を記載しました。なおTSCはTypeScriptのコンパイラのことです。

他の項目に関してはググってください。もしくはオライリー本を買って下さい! 超良書です。

またこの時点では、"src"/"dist" フォルダが無いため、エディタによってはエラーが表示されるかもしれませんが、そこは一旦スルーして下さい。

TSファイルの作成

実行したいTypeScriptファイルを作成します。
tsconfig.jsonのincludeで指定した通り、srcディレクトリを作成して、その下に作って下さい。

$ mkdir src
$ touch src/index.ts
src/index.ts
const hello: string = 'Hello TypeScript!'
console.log(hello)

コンパイル後に生成されるJSファイルの置き場所を作成

ディレクトリを作るだけでOKです。

$ mkdir dist

もし先ほどtsconfig.jsonでエラーが出ていた場合は、この後エラーが消えていることを確認して下さい。

TSファイルのコンパイル

作成したTypeScriptファイルをコンパイルします。

$ yarn tsc
yarn run v1.22.4
$ node_modules/.bin/tsc
✨  Done in 1.39s.

生成されたJSファイルはdist下に保存されているはずです。確認してみましょう。

dist/index.js
"use strict";
const hello = 'Hello TypeScript!';
console.log(hello);
//# sourceMappingURL=index.js.map

それっぽく出来てますね!

生成されたJSファイルの実行

nodeコマンドで生成されたJSファイルを実行します。

$ node dist/index.js
Hello TypeScript!

これでローカル環境でTypeScriptのコードを実行できました。簡単でしたね!

さいごに

「手元でJavaScript周りのパッケージの動作確認等をする時、サラッとTypeScript使えてたらカッコよくない?」という不純な気持ちで書きました。

ただパッケージによっては、別途TypeScript用の設定が必要だったりするのでそこはご注意くださいm(_ _)m

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

無料でSSR・ホスティング・API鯖を立てれるVercel。GitHub Actionsで自動デプロイ。

あらかじめローカルでプロジェクトを作っておく

環境変数

VECEL_TOKEN → コンパネのSettings→Tokens
VECEL_ORG_ID → .vercel/project.jsonに書いてる
VECEL_PROJECT_ID → .vercel/project.jsonに書いてる

ソースコード

デプロイするファイル

public/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>hello</h1>
  </body>
</html>
.github/workflows/main.yml
# 何でもいいから名前をつける。今回はCI
name: CI

# リポジトリにpushされた時に動く
on: [push]

# 必ず書く
jobs:

  # jobの名前を書く
  aaa:

    # osはubuntuを使う
    runs-on: ubuntu-latest

    # 実行手順
    steps:
      # masterブランチにチェックアウト
      - uses: actions/checkout@master

      # 何でもいいから名前をつける
      - name: a

        # 実行するコマンドを書く
        run: |
          mkdir .vercel
          echo {\"orgId\": \"${{ secrets.VECEL_ORG_ID }}\", \"projectId\": \"${{ secrets.VECEL_PROJECT_ID }}\"} > .vercel/project.json
          npx vercel --token ${{secrets.VECEL_TOKEN}} --prod

ワイの成果物

https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378

①コード共有サイト(SPA) React
https://code.itsumen.com

②画像共有サイト(SPA) React
https://gazou.itsumen.com

③チャット(SSR) Nuxt.js
https://nuxtchat.itsumen.com

④チャット(SPA) React
https://chat4.itsumen.com

⑤掲示板(SSR) Next.js
https://board.itsumen.com

⑥掲示板(SPA) Vue
https://board.itsumen.com

⑦レジの店員を呼ぶスマホアプリ(Android)
https://play.google.com/store/apps/details?id=com.itsumen.regi&hl=ja

⑧ブログ(静的サイトジェネレータ) Hugo
https://yuzuru.itsumen.com

ワイのLINE: https://line.me/ti/p/-GXpQkyXAm

お仕事待ってます^^

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