GAミント至上主義

Web Monomaniacal Developer.

Vue.jsとPHPで給与計算処理を書いた

シニアジョブでは、派遣従業員の勤怠や給与計算まで自社のシステムでまかなっていますが、いろいろと足りない機能や問題があり、これまで多くが手作業で行われてました。
その作業をできるだけ無くすために、既存の機能と併存する形で新しく開発しました。

機能は大きく分けて、2つあります。
1、Vue.jsで総支給額や勤務時間から、残業代、深夜手当などの金額を算出する部分
2、PHP(Laravel)で、1で算出した金額と勤怠情報を組み合わせて月の給料を計算する部分

ただでさえ計算に必要な時間、金額など項目数が多い上に、月給、日給、時給でそれぞれ算出が微妙に異なったり、さらに「休日手当と残業代は重複しない」、「小数点以下の端数処理は労働者の不利にならないようにする」などのビジネスロジックドメイン知識が満載なので、開発者の設計力が試されるいいお題だと思います。

変数名などはfreee人事労務APIを参考にさせていただきました。
developer.freee.co.jp


今回は時間も限られ、根本的な設計し直しは今後控えている全面リニューアル時にするとして、既存の実装の上に追加という形で納得行っていない部分もありますが、覚えておきたいポイントをメモ。

1、契約金額の算出(Vue.js)

f:id:uyamazak:20200312102116p:plain

こんな感じの画面で、総支給額や労働時間等を入れるとそれぞれの値をいわば逆算し、DBに保存することで後述のLaravel側と連携します。
開発中のもので数字は適当です。
派遣会社特有の機能として、従業員に支払う給料と、派遣先への請求金額がそれぞれ分かれています。

単一のコンポーネントで十分そうだったのでVue CLIなどは使用せず、scriptタグでVue本体を読み込んで使ってます。
部分的な改修なのでLaravel Mixも使ってません。

公式ドキュメントでいうとCDNのやつ。
https://jp.vuejs.org/v2/guide/installation.html

なにげに業務でこの使い方するの初めてだったけどすぐ作れて便利だった。

Laravel→Vueへの連携 @jsonPHPの値をJSON

すでにDBに保存されているデータはLaravel側でView (Bladeを使用)に渡して@jsonでJSのオブジェクトにして使います。

<script>
const metaObject = @json($meta);
// いろいろ
</script>

ユーザー入力値以外はひたすらcomputedを使う

総支給額と、労働時間、固定残業時間などを入れたらあとは全部計算で決まるのでcomputedの出番です。
実際のコードはこんな感じ。

  computed: {
    // 基本給:給与総額 - 固定残業代 円/月
    // 固定残業代と加算した場合、総支給額の端数が出ないように調整する
    basicPaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.totalPaymentAmount[type] - Math.ceil(this.fixedOvertimePaymentAmount[type]),
            'daily': type => this.totalPaymentAmount[type] - Math.ceil(this.fixedOvertimePaymentAmount[type]),
            'hourly': null
        });
    },
    // 固定残業代:固定残業時間 × 法定内残業代 × 1.25 円/月
    fixedOvertimePaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.fixedOvertimeHours * this.excessStatutoryWorkPaymentAmount[type] * 1.25,
            'daily': type => this.fixedOvertimeHours * this.excessStatutoryWorkPaymentAmount[type] * 1.25,
            'hourly': null
        });
    },
    // 法定内残業代:給与総額 ÷ (所定労働時間 + 固定残業時間 × 1.25) 円/時
    excessStatutoryWorkPaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.totalPaymentAmount[type] / (this.monthlyNormalWorkHours + this.fixedOvertimeHours * 1.25),
            'daily': type => this.totalPaymentAmount[type] / (this.dailyNormalWorkHours + this.fixedOvertimeHours * 1.25),
            'hourly': null
        });
    },
    // 法定外残業代:法定内残業代 × 1.25 円/時
    overtimeExceptNormalWorkPaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.excessStatutoryWorkPaymentAmount[type] * 1.25,
            'daily': type => this.excessStatutoryWorkPaymentAmount[type] * 1.25,
            'hourly': type => this.totalPaymentAmount[type] * 1.25
        });
    },
    // 休日手当:法定内残業代 × 1.35 円/時
    holidayWorkAllowancePaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.excessStatutoryWorkPaymentAmount[type] * 1.35,
            'daily': type => this.excessStatutoryWorkPaymentAmount[type] * 1.35,
            'hourly': type => this.totalPaymentAmount[type] * 1.35
        });
    },
    // 深夜手当:法定内残業代 × 1.5 円/時
    lateNightWorkAllowancePaymentAmount: function () {
        return this.calcAmountsBypayCalcType({
            'monthly': type => this.excessStatutoryWorkPaymentAmount[type] * 1.5,
            'daily': type => this.excessStatutoryWorkPaymentAmount[type] * 1.5,
            'hourly': type => this.totalPaymentAmount[type] * 1.5
        });
    },

1.25とかの倍率は普通は定数化するところかも知れないけど、ぱっと見で倍率が分かるこの方がいいかなとあえてこのままにしてる。

可能な限りDRYを目指す。

月給(monthly)、日給(daily)、時給(hourly)で算出方法が異なったり、不要な場合があるのでここはそれぞれ書くしか無い。
でも給料(pay)と、請求金額 (invoice)は元になる金額は違うものの、計算式は同じ。

普通に書くとそれぞれ3給与制 × 2(給与・請求)* 9項目、計54の式 を書いて保守していくのはつらい。

ということで専用のメソッドをつくり、計算式をコールバックで渡すことで、共通化しました。
ここはreduceを使ったりして可読性が落ちてるけど、computedの方はシンプルになるのでいいかなと。

  methods: {
    /**
     * 請求、給料ごとに給与タイプ別のcallbackを計算してObjectでまとめて返す
     * @return Object {'invoice': number , 'pay': number}
     */
    calcAmountsBypayCalcType: function (callbacks) {
        if (callbacks[this.payCalcType] === null) {
            return {};
        }
        return this.calcAmounts(callbacks[this.payCalcType]);
    },
    /**
     * 請求、給料ごとに計算してObjectでまとめて返す
     * @return Object {'invoice': number , 'pay': number}
     */
    calcAmounts: function (callback) {
        return amountTypes.reduce((result, type) => {
            result[type] = callback(type);
            return result;
        }, {});
    },

2、月ごとの給与額の計算

勤怠情報と上記で設定した金額を元に月ごとの金額を算出します。
勤怠情報には日別で、出勤時間、退勤時間、休憩時間、出勤タイプ(休日出勤とか、欠勤とか)が入ってる感じ。
ここは今回は手をつけず既存のものをそのまま利用。

処理の流れとしてはこんな感じ。

  1. 日ごとに、いろんな時間を算出
  2. 月間合計の時間を算出
  3. 各設定金額と掛け算して残業代とかを算出

ちゃんと時間集計があっているか、デバッグ用、営業さんが確認できる用にこんな表を見れるようにしました。
f:id:uyamazak:20200312114653p:plain

min(), max()を活用

時間系の計算では、今までたまにしか使わなかったmin(), max()を大量に使うことになりました。

簡単な例で、早退&遅刻時間です。

例1として所定労働時間が8時間なのに、
総労働時間が7時間だったら
1時間遅刻か早退したなってことですね。

この例だったらこの式で問題ありません。

所定労働時間 - 総労働時間 = 早退&遅刻時間
8 -7 = 1時間

でも例2として、残業したとき
所定労働時間が8時間なのに、
総労働時間が9時間だったら

8 -9 = -1時間
  • 1が返ってしまいます。早退・遅刻はしてないんだから0が返ってほしいところ。

これをif分で書くといくつかパターンがありますが、一つはこんな感じになると思います。

早退&遅刻時間 = 所定労働時間 - 総労働時間 
if ( 早退&遅刻時間 < 0) {
  早退&遅刻時間 = 0;
}

でもここでmax関数を使うと

早退&遅刻時間 = max( 所定労働時間 - 総労働時間, 0); 

この1行で済みます。

例2のときは−1となり、max関数は大きい方を返すので0を返してくれます。

max( -1, 0); 
=> 0

簡単な例だとたいして恩恵はないんですが、深夜勤務時間の算出では下記ページで見つけた式に大変お世話になりました。

Excel for iPad:深夜勤務時間を求めるには

= "5:00" - MIN("5:00", 開始時刻) + MIN("29:00", 終了時刻) - MIN(MAX("22:00", 開始時刻), 終了時刻)

勤務時間のうち0時 - 5時か、22-29時(翌日の朝5時)の時間をこれだけの式で出してくれます。
これをif分で書こうとすると、出勤時間が5時より前か後か、退勤時間が22時以降か、29時前か・・・と考え始めて大変なことになりました。

今回の開発で改めて自分の数学・・・というか算数レベルの能力の低さを実感できました。

これからはじめるVue.js実践入門

これからはじめるVue.js実践入門