fixed-overtime-calculator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/cli.js +40 -0
- package/index.js +11 -0
- package/overtime.js +19 -0
- package/package.json +34 -0
- package/report.js +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# fixed-overtime-calculator
|
|
2
|
+
|
|
3
|
+
This is a CLI tool that calculates the amount of the fixed overtime allowance (fixed overtime pay) based on the total gross pay.
|
|
4
|
+
This tool is designed for use in Japan and is based on Japanese labor laws.
|
|
5
|
+
|
|
6
|
+
総支給額から、定額残業手当(固定残業代)の金額を計算するCLIツールです。
|
|
7
|
+
|
|
8
|
+
## 使い方
|
|
9
|
+
|
|
10
|
+
インストールは不要です。`npx` でそのまま実行できます。
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx fixed-overtime-calculator
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
実行すると、対話形式で3つの質問に答えるだけで結果が表示されます。
|
|
17
|
+
|
|
18
|
+
1. 総支給額
|
|
19
|
+
2. 月平均所定労働時間
|
|
20
|
+
3. みなし残業時間
|
|
21
|
+
|
|
22
|
+
## 出力例
|
|
23
|
+
|
|
24
|
+
総支給額30万円、月平均所定労働時間160時間、みなし残業30時間の場合。
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
$ npx fixed-overtime-calculator
|
|
28
|
+
✔ 総支給額を入力してください · 300000
|
|
29
|
+
✔ 所定労働時間を入力してください · 160
|
|
30
|
+
✔ みなし残業時間を入力してください · 30
|
|
31
|
+
【入力】
|
|
32
|
+
総支給額: 300,000円
|
|
33
|
+
月平均所定労働時間: 160時間
|
|
34
|
+
みなし残業時間: 30時間
|
|
35
|
+
|
|
36
|
+
【内訳】
|
|
37
|
+
基本給: 243,000円
|
|
38
|
+
定額残業代: 57,000円
|
|
39
|
+
----------------------------------
|
|
40
|
+
合計: 300,000円
|
|
41
|
+
|
|
42
|
+
みなし残業 30時間分の定額残業代は57,000円です。
|
|
43
|
+
|
|
44
|
+
【単価】
|
|
45
|
+
時間給: 1,518.75円
|
|
46
|
+
割増単価(1.25倍): 1,898.44円
|
|
47
|
+
※時間給が地域別最低賃金を下回らないようご注意ください
|
|
48
|
+
|
|
49
|
+
【注意】
|
|
50
|
+
・法定時間外労働の割増率1.25倍のみを前提としています。深夜割増や休日割増は含みません。
|
|
51
|
+
・実際の残業時間がみなし残業時間を超えた月は、超過分の割増賃金を別途支払う必要があります。
|
|
52
|
+
・定額残業代が有効とされるには、通常の労働時間の賃金部分と割増賃金部分を判別できること、およびその手当が時間外労働の対価であることが必要です。
|
|
53
|
+
・総支給額は、家族手当・通勤手当など割増賃金の基礎から除外される賃金を除いた額を入力してください。除外できるかは名称ではなく実質で判断されます。
|
|
54
|
+
・本ツールの結果は目安です。実際の賃金設計は社会保険労務士等の専門家にご確認ください。
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 計算のしくみ
|
|
58
|
+
|
|
59
|
+
総支給額を T、月平均所定労働時間を H、みなし残業時間を N、基本給を B とします。
|
|
60
|
+
|
|
61
|
+
割増賃金は `時間給 × 1.25 × 残業時間` で計算されるため、両者の関係は次の式になります。
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
T = B + (B ÷ H × 1.25 × N)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 前提条件
|
|
68
|
+
|
|
69
|
+
- **割増率は法定時間外労働の1.25倍のみ**を扱います。深夜割増(25%)、法定休日割増(35%)、月60時間超の割増(50%)は対象外です。固定残業代は法定時間外労働分のみを対象とするのが一般的なためです。
|
|
70
|
+
- **総支給額は、割増賃金の基礎から除外される賃金を除いた額**を入力してください。労働基準法37条5項および同法施行規則21条により、次の7つが限定列挙されています。
|
|
71
|
+
|
|
72
|
+
家族手当、通勤手当、別居手当、子女教育手当、住宅手当、臨時に支払われた賃金、1ヶ月を超える期間ごとに支払われる賃金
|
|
73
|
+
|
|
74
|
+
ただしこれらは**名称ではなく実質で判断**されます。家族手当でも家族数に関係なく一律に支払われるものや、通勤手当でも距離に関係なく一律に支払われるものは、算定の基礎に含めなければなりません。
|
|
75
|
+
|
|
76
|
+
- 月平均所定労働時間は入力値をそのまま使用します。年間所定休日数からの自動計算は行いません。
|
|
77
|
+
- 最低賃金との比較は行いません。
|
|
78
|
+
|
|
79
|
+
## 固定残業代を設計する際の留意点
|
|
80
|
+
|
|
81
|
+
**実際の残業時間がみなし残業時間を超えた月は、超過分の割増賃金を別途支払う必要があります**(労働基準法37条1項)。
|
|
82
|
+
また固定残業代が有効と認められるには、**通常の労働時間の賃金部分と割増賃金部分を判別できること(明確区分性)**と、**その手当が時間外労働の対価であること(対価性)**が必要とされています(最判平成29年7月7日 医療法人康心会事件、最判平成30年7月19日 日本ケミカル事件)。
|
|
83
|
+
なお1時間当たりの賃金額および割増賃金額に1円未満の端数が生じた場合、50銭未満を切り捨て、50銭以上を1円に切り上げる処理が認められています(基発第150号)。本ツールは単価を参考値として端数を丸めずに表示しています。
|
|
84
|
+
|
|
85
|
+
## 必要環境
|
|
86
|
+
|
|
87
|
+
- Node.js 18以上
|
|
88
|
+
|
|
89
|
+
## 免責
|
|
90
|
+
|
|
91
|
+
本ツールの結果はあくまで目安です。実際の賃金設計にあたっては、社会保険労務士等の専門家にご確認ください。
|
|
92
|
+
|
|
93
|
+
## 参考
|
|
94
|
+
|
|
95
|
+
- [労働基準法 第37条(時間外、休日及び深夜の割増賃金)](https://laws.e-gov.go.jp/law/322AC0000000049)
|
|
96
|
+
- [労働基準法施行規則 第21条(割増賃金の基礎から除外される賃金)](https://laws.e-gov.go.jp/law/322M40000100023)
|
|
97
|
+
- [固定残業代(定額残業代)の取扱い | 厚生労働省 スタートアップ労働条件](https://www.startup-roudou.mhlw.go.jp/qa/zigyonushi/chingin/q11.html)
|
|
98
|
+
|
|
99
|
+
## ライセンス
|
|
100
|
+
|
|
101
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import enquirer from "enquirer";
|
|
2
|
+
|
|
3
|
+
export const askTotal = async () => {
|
|
4
|
+
const prompt = new enquirer.Input({
|
|
5
|
+
message: "総支給額を入力してください",
|
|
6
|
+
validate: (value) => {
|
|
7
|
+
const result = validatePositiveNumber(value);
|
|
8
|
+
if (result !== true) return result;
|
|
9
|
+
if (!Number.isInteger(Number(value))) return "整数を入力してください。";
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
const answer = await prompt.run();
|
|
14
|
+
return Number(answer);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const askMonthlyHours = async () => {
|
|
18
|
+
const prompt = new enquirer.Input({
|
|
19
|
+
message: "所定労働時間を入力してください",
|
|
20
|
+
validate: validatePositiveNumber,
|
|
21
|
+
});
|
|
22
|
+
const answer = await prompt.run();
|
|
23
|
+
return Number(answer);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const askOvertimeHours = async () => {
|
|
27
|
+
const prompt = new enquirer.Input({
|
|
28
|
+
message: "みなし残業時間を入力してください",
|
|
29
|
+
validate: validatePositiveNumber,
|
|
30
|
+
});
|
|
31
|
+
const answer = await prompt.run();
|
|
32
|
+
return Number(answer);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const validatePositiveNumber = (value) => {
|
|
36
|
+
const number = Number(value);
|
|
37
|
+
if (Number.isNaN(number)) return "数値を入力してください。";
|
|
38
|
+
if (number <= 0) return "0より大きい金額を入力してください。";
|
|
39
|
+
return true;
|
|
40
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { calculate } from "./overtime.js";
|
|
4
|
+
import { buildReport } from "./report.js";
|
|
5
|
+
import { askTotal, askMonthlyHours, askOvertimeHours } from "./cli.js";
|
|
6
|
+
|
|
7
|
+
const total = await askTotal();
|
|
8
|
+
const monthlyHours = await askMonthlyHours();
|
|
9
|
+
const overtimeHours = await askOvertimeHours();
|
|
10
|
+
|
|
11
|
+
console.log(buildReport(calculate({ total, monthlyHours, overtimeHours })));
|
package/overtime.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const PREMIUM_RATE = 1.25;
|
|
2
|
+
|
|
3
|
+
export const calculate = ({ total, monthlyHours, overtimeHours }) => {
|
|
4
|
+
const ratio = 1 + (PREMIUM_RATE * overtimeHours) / monthlyHours;
|
|
5
|
+
// 基本給を100円単位で切り捨て、差額を定額残業代に寄せることで内訳の合計を総支給額に一致させる
|
|
6
|
+
const baseSalary = Math.floor(total / ratio / 100) * 100;
|
|
7
|
+
const fixedAmount = total - baseSalary;
|
|
8
|
+
const hourlyWage = baseSalary / monthlyHours; // 時給
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
total,
|
|
12
|
+
monthlyHours,
|
|
13
|
+
overtimeHours,
|
|
14
|
+
baseSalary,
|
|
15
|
+
fixedAmount,
|
|
16
|
+
hourlyWage,
|
|
17
|
+
premiumUnitWage: hourlyWage * PREMIUM_RATE,
|
|
18
|
+
};
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fixed-overtime-calculator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "総支給額から、定額残業手当(固定残業代)の金額を計算するCLIツールです。",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fixed-overtime-calculator": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"lint": "prettier --check . && eslint .",
|
|
12
|
+
"fix": "prettier --write . && eslint --fix ."
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^10.0.1",
|
|
20
|
+
"eslint": "^10.9.1",
|
|
21
|
+
"eslint-config-prettier": "^10.1.8",
|
|
22
|
+
"globals": "^17.12.0",
|
|
23
|
+
"prettier": "^3.9.6"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"enquirer": "^2.4.1"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"overtime.js",
|
|
31
|
+
"report.js",
|
|
32
|
+
"cli.js"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/report.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const LABEL_COLUMN = 22;
|
|
2
|
+
const AMOUNT_COLUMN = 10;
|
|
3
|
+
const pattern = /[ -~。-゚]/;
|
|
4
|
+
const NOTES = [
|
|
5
|
+
"法定時間外労働の割増率1.25倍のみを前提としています。深夜割増や休日割増は含みません。",
|
|
6
|
+
"実際の残業時間がみなし残業時間を超えた月は、超過分の割増賃金を別途支払う必要があります。",
|
|
7
|
+
"定額残業代が有効とされるには、通常の労働時間の賃金部分と割増賃金部分を判別できること、およびその手当が時間外労働の対価であることが必要です。",
|
|
8
|
+
"総支給額は、家族手当・通勤手当など割増賃金の基礎から除外される賃金を除いた額を入力してください。除外できるかは名称ではなく実質で判断されます。",
|
|
9
|
+
"本ツールの結果は目安です。実際の賃金設計は社会保険労務士等の専門家にご確認ください。",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const buildReport = (result) => {
|
|
13
|
+
return [
|
|
14
|
+
"【入力】",
|
|
15
|
+
` ${padEndByWidth("総支給額:", LABEL_COLUMN)}${padStartByWidth(formatYen(result.total), AMOUNT_COLUMN)}`,
|
|
16
|
+
` ${padEndByWidth("月平均所定労働時間:", LABEL_COLUMN)}${padStartByWidth(formatHours(result.monthlyHours), AMOUNT_COLUMN)}`,
|
|
17
|
+
` ${padEndByWidth("みなし残業時間:", LABEL_COLUMN)}${padStartByWidth(formatHours(result.overtimeHours), AMOUNT_COLUMN)}`,
|
|
18
|
+
"",
|
|
19
|
+
"【内訳】",
|
|
20
|
+
` ${padEndByWidth("基本給:", LABEL_COLUMN)}${padStartByWidth(formatYen(result.baseSalary), AMOUNT_COLUMN)}`,
|
|
21
|
+
` ${padEndByWidth("定額残業代:", LABEL_COLUMN)}${padStartByWidth(formatYen(result.fixedAmount), AMOUNT_COLUMN)}`,
|
|
22
|
+
"----------------------------------",
|
|
23
|
+
` ${padEndByWidth("合計:", LABEL_COLUMN)}${padStartByWidth(formatYen(result.total), AMOUNT_COLUMN)}`,
|
|
24
|
+
"",
|
|
25
|
+
` みなし残業 ${result.overtimeHours}時間分の定額残業代は${formatYen(result.fixedAmount)}です。`,
|
|
26
|
+
"",
|
|
27
|
+
"【単価】",
|
|
28
|
+
` ${padEndByWidth("時間給:", LABEL_COLUMN)}${padStartByWidth(formatYen(result.hourlyWage, 2), AMOUNT_COLUMN)}`,
|
|
29
|
+
` ${padEndByWidth("割増単価(1.25倍):", LABEL_COLUMN)}${padStartByWidth(formatYen(result.premiumUnitWage, 2), AMOUNT_COLUMN)}`,
|
|
30
|
+
"※時間給が地域別最低賃金を下回らないようご注意ください",
|
|
31
|
+
"",
|
|
32
|
+
"【注意】",
|
|
33
|
+
...NOTES.map((note) => `・${note}`),
|
|
34
|
+
].join("\n");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const formatYen = (amount, fractionDigits = 0) => {
|
|
38
|
+
return `${amount.toLocaleString("ja-JP", {
|
|
39
|
+
minimumFractionDigits: fractionDigits,
|
|
40
|
+
maximumFractionDigits: fractionDigits,
|
|
41
|
+
})}円`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const formatHours = (hours) => {
|
|
45
|
+
return `${hours}時間`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const displayWidth = (text) => {
|
|
49
|
+
let width = 0;
|
|
50
|
+
for (const char of text) {
|
|
51
|
+
if (pattern.test(char)) {
|
|
52
|
+
width += 1;
|
|
53
|
+
} else {
|
|
54
|
+
width += 2;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return width;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const padEndByWidth = (text, columnSize) => {
|
|
61
|
+
const space = columnSize - displayWidth(text);
|
|
62
|
+
return `${text}${" ".repeat(Math.max(space, 0))}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const padStartByWidth = (text, columnSize) => {
|
|
66
|
+
const space = columnSize - displayWidth(text);
|
|
67
|
+
return `${" ".repeat(Math.max(space, 0))}${text}`;
|
|
68
|
+
};
|