@@ -1,11 +1,26 @@
|
||||
"""
|
||||
賞与計算サービス (Bonus Calculation Service)
|
||||
"""
|
||||
from decimal import Decimal
|
||||
import json
|
||||
import os
|
||||
import functools
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import date
|
||||
from typing import Dict, Any
|
||||
from ...core.database import get_connection
|
||||
|
||||
_BONUS_TAX_RATES_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'config', 'bonus_tax_rates.json'
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _load_bonus_tax_rates():
|
||||
"""賞与算出率表をJSONファイルから読み込む(キャッシュ付き)"""
|
||||
with open(_BONUS_TAX_RATES_PATH, encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
class BonusCalculationService:
|
||||
"""賞与計算サービス"""
|
||||
@@ -15,8 +30,10 @@ class BonusCalculationService:
|
||||
"""保険料率を取得(賞与用、年度がない場合は前年度を参照)"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# rate_yearで年度を判定
|
||||
target_year = target_date.year
|
||||
# 翌月払いのため、新年度料率は5月支払分(4月分)から適用
|
||||
# rate_year=XXXX は XXXX年5月支払〜(XXXX+1)年4月支払に適用
|
||||
# 1〜4月支払は前年度(year-1)の料率を使用
|
||||
target_year = target_date.year if target_date.month >= 5 else target_date.year - 1
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT * FROM insurance_rates
|
||||
@@ -75,12 +92,12 @@ class BonusCalculationService:
|
||||
"""前月までの3ヶ月平均給与を取得(賞与所得税計算用)"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 前月までの3ヶ月の総支給額を取得
|
||||
# 前月までの3ヶ月の社会保険料等控除後の給与を取得
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT AVG(total_payment - commute_allowance) as avg_salary
|
||||
SELECT AVG(total_payment - commute_allowance - health_insurance - care_insurance - pension_insurance - employment_insurance - COALESCE(child_support, 0)) as avg_salary
|
||||
FROM (
|
||||
SELECT total_payment, commute_allowance
|
||||
SELECT total_payment, commute_allowance, health_insurance, care_insurance, pension_insurance, employment_insurance, child_support
|
||||
FROM monthly_payroll
|
||||
WHERE employee_id = %s
|
||||
AND payment_date < %s
|
||||
@@ -102,24 +119,34 @@ class BonusCalculationService:
|
||||
) -> Decimal:
|
||||
"""
|
||||
賞与所得税を計算
|
||||
賞与税率 = (前月給与額 - 社会保険料) × 扶養人数に応じた税率
|
||||
国税庁「賞与に対する源泉徴収税額の算出率の表」甲欄を使用
|
||||
bonus_amount: 社会保険料等控除後の賞与額
|
||||
previous_salary_avg: 前月の社会保険料等控除後の給与額
|
||||
"""
|
||||
# 簡易計算: 賞与額から社会保険料概算を引いた額の10.21%を所得税とする
|
||||
# 実際は前月給与から税率を求める複雑な計算が必要
|
||||
if previous_salary_avg == Decimal("0"):
|
||||
# 前月給与がない場合は賞与額の10.21%
|
||||
return (bonus_amount * Decimal("0.1021")).quantize(Decimal("1"))
|
||||
# 前月給与がない場合: 月額表方式が必要だがフォールバックとして10.21%
|
||||
return (bonus_amount * Decimal("0.1021")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 扶養人数による税率軽減(簡略化)
|
||||
rate = Decimal("0.1021") # 基本税率 10.21%
|
||||
if dependents_count == 1:
|
||||
rate = Decimal("0.0820") # 8.20%
|
||||
elif dependents_count == 2:
|
||||
rate = Decimal("0.0619") # 6.19%
|
||||
elif dependents_count >= 3:
|
||||
rate = Decimal("0.0418") # 4.18%
|
||||
table = _load_bonus_tax_rates()
|
||||
salary_k = float(previous_salary_avg) / 1000 # 千円単位に変換
|
||||
dep_idx = min(int(dependents_count), 7)
|
||||
|
||||
return (bonus_amount * rate).quantize(Decimal("1"))
|
||||
rate_percent = Decimal("10.21") # フォールバック
|
||||
for entry in table['entries']:
|
||||
rng = entry['ranges'][dep_idx]
|
||||
from_k = rng[0]
|
||||
to_k = rng[1]
|
||||
if to_k is None:
|
||||
# 上限なし(最高税率帯)
|
||||
if salary_k >= from_k:
|
||||
rate_percent = Decimal(str(entry['rate']))
|
||||
break
|
||||
elif from_k <= salary_k < to_k:
|
||||
rate_percent = Decimal(str(entry['rate']))
|
||||
break
|
||||
|
||||
rate = rate_percent / Decimal("100")
|
||||
return (bonus_amount * rate).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
@staticmethod
|
||||
def calculate_bonus(
|
||||
@@ -176,7 +203,7 @@ class BonusCalculationService:
|
||||
if health_insurance_rate:
|
||||
health_insurance = (
|
||||
health_standard_bonus * Decimal(str(health_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 介護保険(40歳以上65歳未満が対象)
|
||||
care_insurance = Decimal("0")
|
||||
@@ -185,7 +212,7 @@ class BonusCalculationService:
|
||||
if care_insurance_rate:
|
||||
care_insurance = (
|
||||
health_standard_bonus * Decimal(str(care_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 厚生年金
|
||||
pension_insurance_rate = BonusCalculationService.get_insurance_rate("厚生年金", payment_date)
|
||||
@@ -193,7 +220,7 @@ class BonusCalculationService:
|
||||
if pension_insurance_rate:
|
||||
pension_insurance = (
|
||||
pension_standard_bonus * Decimal(str(pension_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 雇用保険
|
||||
employment_insurance_rate = BonusCalculationService.get_insurance_rate("雇用保険", payment_date)
|
||||
@@ -201,35 +228,21 @@ class BonusCalculationService:
|
||||
if employment_insurance_rate:
|
||||
employment_insurance = (
|
||||
total_bonus * Decimal(str(employment_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 子ども・子育て支援金(令和8年4月分以降のみ適用)
|
||||
# 賞与の場合: 標準報酬月額表の淨化率を使い、total_bonus × 子育て率 / 2
|
||||
# 賞与の場合: 社会保険料率設定(insurance_rates)の料率 × 賞与総額で計算
|
||||
child_support = Decimal("0")
|
||||
is_child_support_applicable = (
|
||||
(bonus_year == 2026 and bonus_month >= 4) or bonus_year >= 2027
|
||||
(bonus_year == 2026 and bonus_month >= 5) or bonus_year >= 2027
|
||||
)
|
||||
if is_child_support_applicable:
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 標準報酬月額表から子育て率を取得(最上位等級rowの child_support/monthly_amountで率を計算)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT child_support, monthly_amount
|
||||
FROM standard_remuneration
|
||||
WHERE rate_year = %s
|
||||
AND monthly_amount > 0 AND child_support > 0
|
||||
ORDER BY monthly_amount DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(payment_date.year,)
|
||||
)
|
||||
sr = cur.fetchone()
|
||||
if sr and sr["monthly_amount"] and sr["monthly_amount"] > 0:
|
||||
cs_rate = Decimal(str(sr["child_support"])) / Decimal(str(sr["monthly_amount"]))
|
||||
# 標準賞与額に封上限なし(法定上賞与には標準賞与額上限なし)
|
||||
child_support = (total_bonus * cs_rate / Decimal("2")).quantize(Decimal("1"))
|
||||
print(f"[DEBUG] 賞与子育て支援金: 率={cs_rate}, 賞与総額={total_bonus} → 従業員負担={child_support}")
|
||||
child_support_rate = BonusCalculationService.get_insurance_rate("子ども・子育て支援金", payment_date)
|
||||
if child_support_rate:
|
||||
child_support = (
|
||||
total_bonus * Decimal(str(child_support_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
print(f"[DEBUG] 賞与子育て支援金: 賞与総額={total_bonus} × 料率={child_support_rate['employee_rate']} = {child_support}")
|
||||
|
||||
# 所得税の計算(賞与特有の計算)
|
||||
from ..calculation.service import PayrollCalculationService
|
||||
|
||||
@@ -224,7 +224,8 @@ def recalculate_payroll(payroll_id: int, request: schemas.RecalculateRequest = N
|
||||
UPDATE monthly_payroll SET
|
||||
base_salary = %s, overtime_pay = %s, holiday_pay = %s,
|
||||
commute_allowance = %s, total_payment = %s,
|
||||
health_insurance = %s, pension_insurance = %s, employment_insurance = %s,
|
||||
health_insurance = %s, care_insurance = %s, pension_insurance = %s,
|
||||
employment_insurance = %s, child_support = %s,
|
||||
income_tax = %s, total_deduction = %s, net_payment = %s,
|
||||
calculated_at = %s, calculated_by = %s
|
||||
WHERE payroll_id = %s
|
||||
@@ -233,8 +234,9 @@ def recalculate_payroll(payroll_id: int, request: schemas.RecalculateRequest = N
|
||||
(
|
||||
calc_result["base_salary"], calc_result["overtime_pay"], calc_result["holiday_pay"],
|
||||
calc_result["commute_allowance"], calc_result["total_payment"],
|
||||
calc_result["health_insurance"], calc_result["pension_insurance"],
|
||||
calc_result["employment_insurance"],
|
||||
calc_result["health_insurance"], calc_result["care_insurance"],
|
||||
calc_result["pension_insurance"], calc_result["employment_insurance"],
|
||||
calc_result["child_support"],
|
||||
calc_result["income_tax"], calc_result["total_deduction"], calc_result["net_payment"],
|
||||
datetime.now(), calc_result["calculated_by"],
|
||||
payroll_id
|
||||
|
||||
@@ -84,6 +84,7 @@ class MonthlyPayroll(MonthlyPayrollBase):
|
||||
care_insurance: Decimal
|
||||
pension_insurance: Decimal
|
||||
employment_insurance: Decimal
|
||||
child_support: Optional[Decimal] = None
|
||||
income_tax: Decimal
|
||||
resident_tax: Decimal
|
||||
other_deduction: Decimal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
給与計算サービス (Payroll Calculation Service)
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import date
|
||||
from typing import Dict, Any
|
||||
from ...core.database import get_connection
|
||||
@@ -58,8 +58,10 @@ class PayrollCalculationService:
|
||||
"""保険料率を取得(年度がない場合は前年度を参照)"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# rate_yearで年度を判定
|
||||
target_year = target_date.year
|
||||
# 翌月払いのため、新年度料率は5月支払分(4月分)から適用
|
||||
# rate_year=XXXX は XXXX年5月支払〜(XXXX+1)年4月支払に適用
|
||||
# 1〜4月支払は前年度(year-1)の料率を使用
|
||||
target_year = target_date.year if target_date.month >= 5 else target_date.year - 1
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT * FROM insurance_rates
|
||||
@@ -300,7 +302,7 @@ class PayrollCalculationService:
|
||||
# まず指定年度の標準報酬月額表を検索
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT monthly_amount, child_support
|
||||
SELECT monthly_amount
|
||||
FROM standard_remuneration
|
||||
WHERE rate_year = %s
|
||||
AND salary_from <= %s
|
||||
@@ -318,7 +320,7 @@ class PayrollCalculationService:
|
||||
previous_year = target_year - year_offset
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT monthly_amount, child_support
|
||||
SELECT monthly_amount
|
||||
FROM standard_remuneration
|
||||
WHERE rate_year = %s
|
||||
AND salary_from <= %s
|
||||
@@ -334,11 +336,9 @@ class PayrollCalculationService:
|
||||
|
||||
if result:
|
||||
standard_monthly_salary = Decimal(str(result["monthly_amount"]))
|
||||
child_support_full = Decimal(str(result["child_support"])) if result["child_support"] is not None else Decimal("0")
|
||||
print(f"[DEBUG] 標準報酬月額表検索: 総支給額={total_payment} → 標準報酬月額={standard_monthly_salary}, 子育て支援金全額={child_support_full}")
|
||||
print(f"[DEBUG] 標準報酬月額表検索: 総支給額={total_payment} → 標準報酬月額={standard_monthly_salary}")
|
||||
else:
|
||||
print(f"[DEBUG] 標準報酬月額表検索: 該当等級なし(総支給額={total_payment})")
|
||||
child_support_full = Decimal("0")
|
||||
|
||||
|
||||
# 社会保険料上限を取得
|
||||
@@ -381,7 +381,7 @@ class PayrollCalculationService:
|
||||
if health_insurance_rate:
|
||||
health_insurance = (
|
||||
health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("0.01"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
elif calc_social_insurance:
|
||||
errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year})")
|
||||
|
||||
@@ -392,7 +392,7 @@ class PayrollCalculationService:
|
||||
if care_insurance_rate:
|
||||
care_insurance = (
|
||||
health_standard_salary * Decimal(str(care_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("0.01"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}")
|
||||
else:
|
||||
errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year})") if calc_social_insurance else None
|
||||
@@ -403,7 +403,7 @@ class PayrollCalculationService:
|
||||
if pension_insurance_rate:
|
||||
pension_insurance = (
|
||||
pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("0.01"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}")
|
||||
elif calc_social_insurance:
|
||||
errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year})")
|
||||
@@ -412,14 +412,19 @@ class PayrollCalculationService:
|
||||
employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date)
|
||||
employment_insurance = Decimal("0")
|
||||
|
||||
# 子ども・子育て支援金(令和8年4月分以降のみ適用)
|
||||
# 子ども・子育て支援金(令和8年4月分=5月納付分以降のみ適用)
|
||||
# 翌月払いのため、payroll_month=5(5月支払)が4月分保険料の初回控除月
|
||||
child_support = Decimal("0")
|
||||
is_child_support_applicable = (
|
||||
(payroll_year == 2026 and payroll_month >= 4) or payroll_year >= 2027
|
||||
(payroll_year == 2026 and payroll_month >= 5) or payroll_year >= 2027
|
||||
)
|
||||
if is_child_support_applicable and calc_social_insurance:
|
||||
child_support = (child_support_full / Decimal("2")).quantize(Decimal("0.01"))
|
||||
print(f"[DEBUG] 子育て支援金: 全額={child_support_full} → 折半額(従業員負担)={child_support}")
|
||||
child_support_rate = PayrollCalculationService.get_insurance_rate("子ども・子育て支援金", payment_date)
|
||||
if child_support_rate:
|
||||
child_support = (
|
||||
standard_monthly_salary * Decimal(str(child_support_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
print(f"[DEBUG] 子育て支援金: 標準報酬月額={standard_monthly_salary} × 料率={child_support_rate['employee_rate']} = {child_support}")
|
||||
|
||||
# 雇用保険加入対象かどうか確認
|
||||
is_employment_insurance_eligible = salary_setting.get("employment_insurance_eligible", True) if salary_setting else True
|
||||
@@ -427,7 +432,7 @@ class PayrollCalculationService:
|
||||
if is_employment_insurance_eligible and employment_insurance_rate:
|
||||
employment_insurance = (
|
||||
total_payment * Decimal(str(employment_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("0.01"))
|
||||
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
elif not is_employment_insurance_eligible:
|
||||
print(f"[DEBUG] この従業員は雇用保険非対象です")
|
||||
elif not employment_insurance_rate and calc_social_insurance:
|
||||
|
||||
280
backend/app/payroll/config/bonus_tax_rates.json
Normal file
280
backend/app/payroll/config/bonus_tax_rates.json
Normal file
@@ -0,0 +1,280 @@
|
||||
{
|
||||
"version": "令和2年分(令和2年4月1日以降適用)",
|
||||
"source": "国税庁 賞与に対する源泉徴収税額の算出率の表(甲欄)平成24年3月31日財務省告示第115号別表第三(平成31年3月29日財務省告示第97号改正)",
|
||||
"note": "単位は千円。rate は%値。ranges[i] は扶養i人の[以上(千円), 未満(千円) または null=上限なし]。賞与の金額は社会保険料等控除後の金額に適用する。",
|
||||
"entries": [
|
||||
{
|
||||
"rate": 0.0,
|
||||
"ranges": [
|
||||
[0, 68],
|
||||
[0, 94],
|
||||
[0, 133],
|
||||
[0, 171],
|
||||
[0, 210],
|
||||
[0, 243],
|
||||
[0, 275],
|
||||
[0, 308]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 2.042,
|
||||
"ranges": [
|
||||
[68, 79],
|
||||
[94, 243],
|
||||
[133, 269],
|
||||
[171, 295],
|
||||
[210, 300],
|
||||
[243, 300],
|
||||
[275, 333],
|
||||
[308, 372]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 4.084,
|
||||
"ranges": [
|
||||
[79, 252],
|
||||
[243, 282],
|
||||
[269, 312],
|
||||
[295, 345],
|
||||
[300, 378],
|
||||
[300, 406],
|
||||
[333, 431],
|
||||
[372, 456]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 6.126,
|
||||
"ranges": [
|
||||
[252, 300],
|
||||
[282, 338],
|
||||
[312, 369],
|
||||
[345, 398],
|
||||
[378, 424],
|
||||
[406, 450],
|
||||
[431, 476],
|
||||
[456, 502]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 8.168,
|
||||
"ranges": [
|
||||
[300, 334],
|
||||
[338, 365],
|
||||
[369, 393],
|
||||
[398, 417],
|
||||
[424, 444],
|
||||
[450, 472],
|
||||
[476, 499],
|
||||
[502, 523]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 10.21,
|
||||
"ranges": [
|
||||
[334, 363],
|
||||
[365, 394],
|
||||
[393, 420],
|
||||
[417, 445],
|
||||
[444, 470],
|
||||
[472, 496],
|
||||
[499, 521],
|
||||
[523, 545]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 12.252,
|
||||
"ranges": [
|
||||
[363, 395],
|
||||
[394, 422],
|
||||
[420, 450],
|
||||
[445, 477],
|
||||
[470, 503],
|
||||
[496, 525],
|
||||
[521, 547],
|
||||
[545, 571]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 14.294,
|
||||
"ranges": [
|
||||
[395, 426],
|
||||
[422, 455],
|
||||
[450, 484],
|
||||
[477, 510],
|
||||
[503, 534],
|
||||
[525, 557],
|
||||
[547, 582],
|
||||
[571, 607]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 16.336,
|
||||
"ranges": [
|
||||
[426, 520],
|
||||
[455, 520],
|
||||
[484, 520],
|
||||
[510, 544],
|
||||
[534, 570],
|
||||
[557, 597],
|
||||
[582, 623],
|
||||
[607, 650]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 18.378,
|
||||
"ranges": [
|
||||
[520, 601],
|
||||
[520, 617],
|
||||
[520, 632],
|
||||
[544, 647],
|
||||
[570, 662],
|
||||
[597, 677],
|
||||
[623, 693],
|
||||
[650, 708]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 20.42,
|
||||
"ranges": [
|
||||
[601, 678],
|
||||
[617, 699],
|
||||
[632, 721],
|
||||
[647, 745],
|
||||
[662, 768],
|
||||
[677, 792],
|
||||
[693, 815],
|
||||
[708, 838]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 22.462,
|
||||
"ranges": [
|
||||
[678, 708],
|
||||
[699, 733],
|
||||
[721, 757],
|
||||
[745, 782],
|
||||
[768, 806],
|
||||
[792, 831],
|
||||
[815, 856],
|
||||
[838, 880]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 24.504,
|
||||
"ranges": [
|
||||
[708, 745],
|
||||
[733, 771],
|
||||
[757, 797],
|
||||
[782, 823],
|
||||
[806, 849],
|
||||
[831, 875],
|
||||
[856, 900],
|
||||
[880, 926]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 26.546,
|
||||
"ranges": [
|
||||
[745, 788],
|
||||
[771, 814],
|
||||
[797, 841],
|
||||
[823, 868],
|
||||
[849, 896],
|
||||
[875, 923],
|
||||
[900, 950],
|
||||
[926, 978]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 28.588,
|
||||
"ranges": [
|
||||
[788, 846],
|
||||
[814, 874],
|
||||
[841, 902],
|
||||
[868, 931],
|
||||
[896, 959],
|
||||
[923, 987],
|
||||
[950, 1015],
|
||||
[978, 1043]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 30.63,
|
||||
"ranges": [
|
||||
[846, 914],
|
||||
[874, 944],
|
||||
[902, 975],
|
||||
[931, 1005],
|
||||
[959, 1036],
|
||||
[987, 1066],
|
||||
[1015, 1096],
|
||||
[1043, 1127]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 32.672,
|
||||
"ranges": [
|
||||
[914, 1312],
|
||||
[944, 1336],
|
||||
[975, 1360],
|
||||
[1005, 1385],
|
||||
[1036, 1409],
|
||||
[1066, 1434],
|
||||
[1096, 1458],
|
||||
[1127, 1482]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 35.735,
|
||||
"ranges": [
|
||||
[1312, 1521],
|
||||
[1336, 1526],
|
||||
[1360, 1526],
|
||||
[1385, 1538],
|
||||
[1409, 1555],
|
||||
[1434, 1555],
|
||||
[1458, 1555],
|
||||
[1482, 1583]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 38.798,
|
||||
"ranges": [
|
||||
[1521, 2621],
|
||||
[1526, 2645],
|
||||
[1526, 2669],
|
||||
[1538, 2693],
|
||||
[1555, 2716],
|
||||
[1555, 2740],
|
||||
[1555, 2764],
|
||||
[1583, 2788]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 41.861,
|
||||
"ranges": [
|
||||
[2621, 3495],
|
||||
[2645, 3527],
|
||||
[2669, 3559],
|
||||
[2693, 3590],
|
||||
[2716, 3622],
|
||||
[2740, 3654],
|
||||
[2764, 3685],
|
||||
[2788, 3717]
|
||||
]
|
||||
},
|
||||
{
|
||||
"rate": 45.945,
|
||||
"ranges": [
|
||||
[3495, null],
|
||||
[3527, null],
|
||||
[3559, null],
|
||||
[3590, null],
|
||||
[3622, null],
|
||||
[3654, null],
|
||||
[3685, null],
|
||||
[3717, null]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
3446
backend/app/payroll/config/monthly_tax_table.json
Normal file
3446
backend/app/payroll/config/monthly_tax_table.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/bonus_rate_nta.xlsx
Normal file
BIN
docs/bonus_rate_nta.xlsx
Normal file
Binary file not shown.
BIN
docs/賞与に対する源泉徴収税額の算出率の表.xls
Normal file
BIN
docs/賞与に対する源泉徴収税額の算出率の表.xls
Normal file
Binary file not shown.
@@ -2,13 +2,16 @@
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<script src="js/auth.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>給与管理 - 月次給与計算</title>
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<style>
|
||||
.container {
|
||||
max-width: 900px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
@@ -122,6 +125,15 @@
|
||||
font-size: 1.2em;
|
||||
color: #007bff;
|
||||
}
|
||||
.calc-note {
|
||||
padding: 3px 10px 6px 14px;
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
background: #f0f4f8;
|
||||
border-left: 3px solid #b0c4de;
|
||||
margin: -1px 0 4px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
@@ -156,6 +168,40 @@
|
||||
.tab-content-calc.active {
|
||||
display: block;
|
||||
}
|
||||
/* 左右分割レイアウト */
|
||||
.split-layout {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.split-left {
|
||||
flex: 0 0 420px;
|
||||
min-width: 320px;
|
||||
max-width: 420px;
|
||||
max-height: calc(100vh - 160px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
.split-right {
|
||||
display: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-left: 2px solid #e0e8f0;
|
||||
padding-left: 24px;
|
||||
max-height: calc(100vh - 160px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
.split-right .payroll-detail {
|
||||
max-width: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -193,9 +239,11 @@
|
||||
|
||||
<!-- 給与計算タブ -->
|
||||
<div id="tab-salary" class="tab-content-calc active">
|
||||
<div class="filters">
|
||||
<label>
|
||||
対象年:
|
||||
<div class="split-layout">
|
||||
<div class="split-left">
|
||||
<div class="filters" style="display:flex; flex-direction:column; gap:8px;">
|
||||
<label style="display:flex; align-items:center; gap:8px;">
|
||||
<span style="white-space:nowrap; min-width:60px;">対象年:</span>
|
||||
<input
|
||||
type="number"
|
||||
id="filterYear"
|
||||
@@ -205,19 +253,24 @@
|
||||
max="2099"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
従業員:
|
||||
<select id="filterEmployee" style="min-width: 200px">
|
||||
<label style="display:flex; align-items:center; gap:8px;">
|
||||
<span style="white-space:nowrap; min-width:60px;">従業員:</span>
|
||||
<select id="filterEmployee" style="flex:1;">
|
||||
<option value="">全員</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
|
||||
<button class="btn btn-success" onclick="showCalculateForm()">
|
||||
新規計算
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="showCalculateForm()">新規計算</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="payrollList" class="payroll-list"></div>
|
||||
</div><!-- /split-left -->
|
||||
<div class="split-right" id="splitRightSalary">
|
||||
<div id="payrollDetail" class="payroll-detail" style="display:none"></div>
|
||||
</div><!-- /split-right -->
|
||||
</div><!-- /split-layout -->
|
||||
|
||||
<!-- 給与計算フォーム(モーダル風) -->
|
||||
<div
|
||||
@@ -420,16 +473,12 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 給与明細表示 -->
|
||||
<div
|
||||
id="payrollDetail"
|
||||
class="payroll-detail"
|
||||
style="display: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 賞与計算タブ -->
|
||||
<div id="tab-bonus" class="tab-content-calc">
|
||||
<div class="split-layout">
|
||||
<div class="split-left">
|
||||
<div class="filters">
|
||||
<label>
|
||||
対象年:
|
||||
@@ -449,6 +498,11 @@
|
||||
</div>
|
||||
|
||||
<div id="bonusList" class="payroll-list"></div>
|
||||
</div><!-- /split-left -->
|
||||
<div class="split-right" id="splitRightBonus">
|
||||
<div id="bonusDetail" class="payroll-detail" style="display:none"></div>
|
||||
</div><!-- /split-right -->
|
||||
</div><!-- /split-layout -->
|
||||
|
||||
<!-- 賞与計算フォーム -->
|
||||
<div
|
||||
@@ -584,12 +638,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 賞与明細表示 -->
|
||||
<div
|
||||
id="bonusDetail"
|
||||
class="payroll-detail"
|
||||
style="display: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 账票出力タブ -->
|
||||
@@ -966,18 +1014,16 @@
|
||||
: `従業員ID: ${p.employee_id}`;
|
||||
return `
|
||||
<div class="payroll-item" style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||
<div style="flex:1; cursor:pointer;" onclick="viewPayroll(${p.payroll_id})">
|
||||
<div style="flex:1; min-width:0; cursor:pointer;" onclick="viewPayroll(${p.payroll_id})">
|
||||
<div>
|
||||
<strong>${empLabel}</strong>
|
||||
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
|
||||
<span style="margin-left:8px; font-size:12px; color:#666;">支給日: ${p.payment_date}</span>
|
||||
</div>
|
||||
<div style="margin-top:4px;">
|
||||
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
|
||||
<span style="color:#666; font-size:13px;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})</span>
|
||||
<div style="font-size:12px; color:#666; margin-top:2px; white-space:nowrap;">支給日: ${p.payment_date}</div>
|
||||
<div style="margin-top:4px; white-space:nowrap;"><strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong></div>
|
||||
<div style="color:#666; font-size:13px; white-space:nowrap;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger btn-small" onclick="deletePayroll(${p.payroll_id}); event.stopPropagation();" style="margin-left:10px; white-space:nowrap; background-color:#dc3545; color:#fff; border:none;">
|
||||
<button class="btn btn-danger btn-small" onclick="deletePayroll(${p.payroll_id}); event.stopPropagation();" style="margin-left:10px; white-space:nowrap; background-color:#dc3545; color:#fff; border:none; flex-shrink:0;">
|
||||
削除
|
||||
</button>
|
||||
</div>
|
||||
@@ -1126,6 +1172,44 @@
|
||||
}).length
|
||||
: 0;
|
||||
|
||||
// 計算明細表示用の変数
|
||||
const _health = Number(payroll.health_insurance || 0);
|
||||
const _care = Number(payroll.care_insurance || 0);
|
||||
const _pension = Number(payroll.pension_insurance || 0);
|
||||
const _emp = Number(payroll.employment_insurance || 0);
|
||||
const _child = Number(payroll.child_support || 0);
|
||||
const _totalPay = Number(payroll.total_payment || 0);
|
||||
// 標準報酬月額を厚生年金保険料(個人負担率 9.150%)から逆算
|
||||
const _stdRemun = _pension > 0 ? Math.round(_pension / 0.0915) : 0;
|
||||
const _stdFmt =
|
||||
_stdRemun > 0 ? "¥" + _stdRemun.toLocaleString() : "-";
|
||||
const _hRateFull =
|
||||
_stdRemun > 0
|
||||
? (((_health * 2) / _stdRemun) * 100).toFixed(2)
|
||||
: "-";
|
||||
const _hRateHalf =
|
||||
_stdRemun > 0 ? ((_health / _stdRemun) * 100).toFixed(2) : "-";
|
||||
const _cRateFull =
|
||||
_stdRemun > 0 && _care > 0
|
||||
? (((_care * 2) / _stdRemun) * 100).toFixed(2)
|
||||
: null;
|
||||
const _cRateHalf =
|
||||
_stdRemun > 0 && _care > 0
|
||||
? ((_care / _stdRemun) * 100).toFixed(2)
|
||||
: null;
|
||||
const _empRate =
|
||||
_totalPay > 0 ? ((_emp / _totalPay) * 100).toFixed(3) : "-";
|
||||
const _csRateFull =
|
||||
_stdRemun > 0 && _child > 0
|
||||
? (((_child * 2) / _stdRemun) * 100).toFixed(3)
|
||||
: null;
|
||||
const _csRateHalf =
|
||||
_stdRemun > 0 && _child > 0
|
||||
? ((_child / _stdRemun) * 100).toFixed(3)
|
||||
: null;
|
||||
const _socialSub = _health + _care + _pension + _emp + _child;
|
||||
const _taxable = _totalPay - _socialSub;
|
||||
|
||||
const html = `
|
||||
<h2>${payroll.payroll_year}年${
|
||||
payroll.payroll_month
|
||||
@@ -1184,35 +1268,45 @@
|
||||
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
|
||||
payroll.total_payment,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||||
payroll.health_insurance,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>介護保険:</span><span>¥${Number(
|
||||
payroll.care_insurance || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
||||
payroll.pension_insurance,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
||||
payroll.employment_insurance,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${Number(
|
||||
Number(payroll.health_insurance || 0) +
|
||||
Number(payroll.care_insurance || 0) +
|
||||
Number(payroll.pension_insurance || 0) +
|
||||
Number(payroll.employment_insurance || 0),
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>健康保険:</span><span>¥${_health.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${_stdFmt}(標準報酬月額)× ${_hRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||||
個人負担率:${_hRateHalf}% ※協会けんぽ東京
|
||||
</div>
|
||||
<div class="detail-row"><span>介護保険:</span><span>¥${_care.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${
|
||||
_cRateFull
|
||||
? `${_stdFmt}(標準報酬月額)× ${_cRateFull}%(保険料率・全体)÷ 2(折半)<br>個人負担率:${_cRateHalf}% ※40歳以上65歳未満が対象`
|
||||
: "介護保険非該当(40歳未満または65歳以上)"
|
||||
}
|
||||
</div>
|
||||
<div class="detail-row"><span>厚生年金:</span><span>¥${_pension.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${_stdFmt}(標準報酬月額)× 18.300%(保険料率・全体)÷ 2(折半)<br>
|
||||
個人負担率:9.150%(固定)
|
||||
</div>
|
||||
<div class="detail-row"><span>雇用保険:</span><span>¥${_emp.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
総支給額 ¥${_totalPay.toLocaleString()} × ${_empRate}%(雇用保険料率・従業員負担分)<br>
|
||||
※標準報酬でなく実際の総支給額に料率を乗じる
|
||||
</div>
|
||||
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_child.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${
|
||||
_csRateFull
|
||||
? `${_stdFmt}(標準報酬月額)× ${_csRateFull}%(拠出金率・全体)÷ 2(折半)<br>個人負担率:${_csRateHalf}% ※令和8年5月支払分(4月分)より控除開始`
|
||||
: "令和8年5月支払分(4月分保険料)より控除開始 → 当月は対象外"
|
||||
}
|
||||
</div>
|
||||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div>
|
||||
<div class="detail-row">
|
||||
<span>所得税 <small style="color:#666;">(甲欄${dependentCount}人)</small>:</span>
|
||||
<span>¥${Number(
|
||||
payroll.income_tax,
|
||||
).toLocaleString()}</span>
|
||||
<span>¥${Number(payroll.income_tax).toLocaleString()}</span>
|
||||
</div>
|
||||
<div style="padding: 5px 10px; background: #f0f8ff; border-radius: 3px; margin: 5px 0; font-size: 12px; color: #666;">
|
||||
<small>
|
||||
※課税対象額から源泉徴収税額表(甲欄)を参照して計算<br>
|
||||
扶養親族${dependentCount}人(16歳以上が対象)
|
||||
</small>
|
||||
<div class="calc-note">
|
||||
課税対象額 = 総支給額 ¥${_totalPay.toLocaleString()} − 社会保険料計 ¥${_socialSub.toLocaleString()} = ¥${_taxable.toLocaleString()}<br>
|
||||
→ 源泉徴収税額表(甲欄・扶養${dependentCount}人)を参照して計算
|
||||
</div>
|
||||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||||
payroll.resident_tax,
|
||||
@@ -1242,14 +1336,13 @@
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'; var sr=document.getElementById('splitRightSalary'); if(sr) sr.style.display='none';">閉じる</button>
|
||||
`;
|
||||
|
||||
document.getElementById("payrollDetail").innerHTML = html;
|
||||
const splitRightSalary = document.getElementById("splitRightSalary");
|
||||
if (splitRightSalary) splitRightSalary.style.display = "block";
|
||||
document.getElementById("payrollDetail").style.display = "block";
|
||||
document
|
||||
.getElementById("payrollDetail")
|
||||
.scrollIntoView({ behavior: "smooth" });
|
||||
} catch (error) {
|
||||
alert("給与明細の取得に失敗しました");
|
||||
console.error(error);
|
||||
@@ -1435,6 +1528,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
alert("再計算が完了しました");
|
||||
await loadPayrolls();
|
||||
viewPayroll(payrollId);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
@@ -1552,34 +1646,18 @@
|
||||
|
||||
return `
|
||||
<div class="payroll-item" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="flex: 1; cursor: pointer;" onclick="viewBonus(${
|
||||
b.bonus_id
|
||||
})">
|
||||
<div style="flex: 1; min-width: 0; cursor: pointer;" onclick="viewBonus(${b.bonus_id})">
|
||||
<div>
|
||||
<strong>${b.bonus_year}年${
|
||||
b.bonus_month
|
||||
}月</strong>
|
||||
<span class="status-badge status-calculated">${
|
||||
b.bonus_type
|
||||
}</span>
|
||||
<strong>${b.bonus_year}年${b.bonus_month}月</strong>
|
||||
<span class="status-badge status-calculated">${b.bonus_type}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 2px; white-space: nowrap;">
|
||||
${empLabel} | 支給日: ${b.payment_date}
|
||||
</div>
|
||||
<div>
|
||||
<strong>差引支給額: ¥${Number(
|
||||
b.net_bonus,
|
||||
).toLocaleString()}</strong>
|
||||
(総支給: ¥${Number(
|
||||
b.total_bonus,
|
||||
).toLocaleString()} - 控除: ¥${Number(
|
||||
b.total_deduction || 0,
|
||||
).toLocaleString()})
|
||||
<div style="white-space: nowrap;"><strong>差引支給額: ¥${Number(b.net_bonus).toLocaleString()}</strong></div>
|
||||
<div style="color: #666; font-size: 13px; white-space: nowrap;">(総支給: ¥${Number(b.total_bonus).toLocaleString()} - 控除: ¥${Number(b.total_deduction || 0).toLocaleString()})</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger btn-small" onclick="deleteBonus(${
|
||||
b.bonus_id
|
||||
}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; background-color: #dc3545; color: #fff; border: none;">
|
||||
<button class="btn btn-danger btn-small" onclick="deleteBonus(${b.bonus_id}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; flex-shrink: 0; background-color: #dc3545; color: #fff; border: none;">
|
||||
削除
|
||||
</button>
|
||||
</div>
|
||||
@@ -1643,6 +1721,7 @@
|
||||
const result = await response.json();
|
||||
alert("賞与計算が完了しました");
|
||||
closeBonusForm();
|
||||
await loadBonus();
|
||||
viewBonus(result.bonus_id);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
@@ -1669,6 +1748,25 @@
|
||||
);
|
||||
const employee = await empResponse.json();
|
||||
|
||||
// 計算明細表示用の変数(賞与は総支給額に料率を乗じる)
|
||||
const _bHealth = Number(bonus.health_insurance || 0);
|
||||
const _bCare = Number(bonus.care_insurance || 0);
|
||||
const _bPension = Number(bonus.pension_insurance || 0);
|
||||
const _bEmp = Number(bonus.employment_insurance || 0);
|
||||
const _bChild = Number(bonus.child_support || 0);
|
||||
const _bTotal = Number(bonus.total_bonus || 0);
|
||||
const _bHRateFull = _bTotal > 0 && _bHealth > 0 ? (((_bHealth * 2) / _bTotal) * 100).toFixed(3) : "-";
|
||||
const _bHRateHalf = _bTotal > 0 && _bHealth > 0 ? ((_bHealth / _bTotal) * 100).toFixed(3) : "-";
|
||||
const _bCRateFull = _bTotal > 0 && _bCare > 0 ? (((_bCare * 2) / _bTotal) * 100).toFixed(3) : null;
|
||||
const _bCRateHalf = _bTotal > 0 && _bCare > 0 ? ((_bCare / _bTotal) * 100).toFixed(3) : null;
|
||||
const _bPRateFull = _bTotal > 0 && _bPension > 0 ? (((_bPension * 2) / _bTotal) * 100).toFixed(3) : "-";
|
||||
const _bPRateHalf = _bTotal > 0 && _bPension > 0 ? ((_bPension / _bTotal) * 100).toFixed(3) : "-";
|
||||
const _bEmpRate = _bTotal > 0 && _bEmp > 0 ? ((_bEmp / _bTotal) * 100).toFixed(3) : "-";
|
||||
const _bCSRateFull = _bTotal > 0 && _bChild > 0 ? (((_bChild * 2) / _bTotal) * 100).toFixed(4) : null;
|
||||
const _bCSRateHalf = _bTotal > 0 && _bChild > 0 ? ((_bChild / _bTotal) * 100).toFixed(4) : null;
|
||||
const _bSocialSub = _bHealth + _bCare + _bPension + _bEmp + _bChild;
|
||||
const _bTaxable = _bTotal - _bSocialSub;
|
||||
|
||||
const html = `
|
||||
<h2>${bonus.bonus_year}年${
|
||||
bonus.bonus_month
|
||||
@@ -1697,27 +1795,47 @@
|
||||
|
||||
<div class="detail-section">
|
||||
<h3>控除項目</h3>
|
||||
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||||
bonus.health_insurance || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>介護保険:</span><span>¥${Number(
|
||||
bonus.care_insurance || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
||||
bonus.pension_insurance || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
||||
bonus.employment_insurance || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${Number(
|
||||
Number(bonus.health_insurance || 0) +
|
||||
Number(bonus.care_insurance || 0) +
|
||||
Number(bonus.pension_insurance || 0) +
|
||||
Number(bonus.employment_insurance || 0),
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>健康保険:</span><span>¥${_bHealth.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bHRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||||
個人負担率:${_bHRateHalf}% ※協会けんぽ東京
|
||||
</div>
|
||||
<div class="detail-row"><span>介護保険:</span><span>¥${_bCare.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${
|
||||
_bCRateFull
|
||||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCRateFull}%(保険料率・全体)÷ 2(折半)<br>個人負担率:${_bCRateHalf}% ※40歳以上65歳未満が対象`
|
||||
: "介護保険非該当(40歳未満または65歳以上)"
|
||||
}
|
||||
</div>
|
||||
<div class="detail-row"><span>厚生年金:</span><span>¥${_bPension.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bPRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||||
個人負担率:${_bPRateHalf}%(固定) ※上限:標準賞与額150万円/月
|
||||
</div>
|
||||
<div class="detail-row"><span>雇用保険:</span><span>¥${_bEmp.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${_bEmp > 0
|
||||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
|
||||
: "賞与に対する雇用保険は別途計算方式による(または0円)"
|
||||
}
|
||||
</div>
|
||||
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_bChild.toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
${
|
||||
_bCSRateFull
|
||||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCSRateFull}%(拠出金率・全体)÷ 2(折半)<br>個人負担率:${_bCSRateHalf}% ※令和8年5月支払分(4月分)より控除開始`
|
||||
: "令和8年5月支払分(4月分保険料)より控除開始 → 当賞与は対象外"
|
||||
}
|
||||
</div>
|
||||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
|
||||
<div class="detail-row"><span>所得税:</span><span>¥${Number(
|
||||
bonus.income_tax || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
<div class="calc-note">
|
||||
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} − 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}<br>
|
||||
→ 賞与に対する源泉徴収税額の算出率の表を参照して計算
|
||||
</div>
|
||||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||||
bonus.resident_tax || 0,
|
||||
).toLocaleString()}</span></div>
|
||||
@@ -1741,14 +1859,13 @@
|
||||
<button class="btn btn-primary" onclick="recalculateBonus(${
|
||||
bonus.bonus_id
|
||||
})">再計算</button>
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'">閉じる</button>
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'; var sr=document.getElementById('splitRightBonus'); if(sr) sr.style.display='none';">閉じる</button>
|
||||
`;
|
||||
|
||||
document.getElementById("bonusDetail").innerHTML = html;
|
||||
const splitRightBonus = document.getElementById("splitRightBonus");
|
||||
if (splitRightBonus) splitRightBonus.style.display = "block";
|
||||
document.getElementById("bonusDetail").style.display = "block";
|
||||
document
|
||||
.getElementById("bonusDetail")
|
||||
.scrollIntoView({ behavior: "smooth" });
|
||||
} catch (error) {
|
||||
alert("賞与明細の取得に失敗しました");
|
||||
console.error(error);
|
||||
@@ -1795,6 +1912,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
alert("再計算が完了しました");
|
||||
await loadBonus();
|
||||
viewBonus(bonusId);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
<li>給与計算と賞与計算で異なる料率を設定できます</li>
|
||||
<li>事業所所在地(都道府県)ごとに設定できます</li>
|
||||
<li>介護保険の適用年齢は設定可能です(デフォルト: 40歳以上)</li>
|
||||
<li><strong>年度適用期間:</strong> 「年度」に設定した値は <strong>その年の4月〜翌年3月</strong> に適用されます。<br>
|
||||
例)2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。<br>
|
||||
2026年1月〜3月分は 2025年度(rate_year=2025)の料率が参照されます。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -802,6 +805,26 @@
|
||||
pensionRate: metadata.pension_rate,
|
||||
};
|
||||
|
||||
// メタデータに料率がない場合、データから逆算する
|
||||
if (rates.healthNoCareRate == null) {
|
||||
const refRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.health_insurance_no_care) > 0);
|
||||
if (refRow) {
|
||||
const ma = Number(refRow.monthly_amount);
|
||||
rates.healthNoCareRate = Number(refRow.health_insurance_no_care) / ma * 100;
|
||||
rates.healthWithCareRate = Number(refRow.health_insurance_with_care) / ma * 100;
|
||||
if (Number(refRow.child_support ?? 0) > 0) {
|
||||
rates.childSupportRate = Number(refRow.child_support) / ma * 100;
|
||||
}
|
||||
}
|
||||
// 厚生年金は低等級(1-3)で0になるため、pension_insurance > 0 の行を別途検索
|
||||
if (rates.pensionRate == null) {
|
||||
const pensionRefRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.pension_insurance ?? 0) > 0);
|
||||
if (pensionRefRow) {
|
||||
rates.pensionRate = Number(pensionRefRow.pension_insurance) / Number(pensionRefRow.monthly_amount) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// メタデータはヘッダーに組み込むため、ここでは空にする
|
||||
let metadataHtml = "";
|
||||
|
||||
|
||||
123
verify_fix.py
123
verify_fix.py
@@ -1,123 +0,0 @@
|
||||
import psycopg
|
||||
import json
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
# 連接到数据库
|
||||
conn = psycopg.connect(
|
||||
host="127.0.0.1",
|
||||
port=5432,
|
||||
dbname="njts_accounting",
|
||||
user="postgres",
|
||||
password="postgres"
|
||||
)
|
||||
|
||||
with conn:
|
||||
with conn.cursor(row_factory=dict_row) as cur:
|
||||
print("=" * 80)
|
||||
print("【検証1】is_latestフィールドが存在するかどうか")
|
||||
print("=" * 80)
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as cnt
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'journal_entries' AND column_name = 'is_latest'
|
||||
""")
|
||||
result = cur.fetchone()
|
||||
has_is_latest = result['cnt'] > 0
|
||||
print(f"is_latest フィールドが存在: {has_is_latest}")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("【検証2】test元(ID=431, 432)のバージョン情報")
|
||||
print("=" * 80)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
journal_entry_id,
|
||||
description,
|
||||
is_latest,
|
||||
is_deleted,
|
||||
parent_entry_id,
|
||||
original_entry_id,
|
||||
revision_count,
|
||||
entry_date,
|
||||
created_at
|
||||
FROM journal_entries
|
||||
WHERE description LIKE '%test%'
|
||||
ORDER BY journal_entry_id
|
||||
""")
|
||||
test_entries = cur.fetchall()
|
||||
for entry in test_entries:
|
||||
print(f"ID={entry['journal_entry_id']}: {entry['description']}")
|
||||
print(f" is_latest={entry['is_latest']}, is_deleted={entry['is_deleted']}")
|
||||
print(f" parent_entry_id={entry['parent_entry_id']}, original_entry_id={entry['original_entry_id']}")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("【検証3】修正前のロジック(親IDで排除)の結果")
|
||||
print("=" * 80)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
a.account_code,
|
||||
a.account_name,
|
||||
SUM(l.debit) as debit,
|
||||
SUM(l.credit) as credit
|
||||
FROM journal_lines l
|
||||
JOIN journal_entries e ON e.journal_entry_id = l.journal_entry_id
|
||||
JOIN accounts a ON a.account_id = l.account_id
|
||||
WHERE e.is_deleted = false
|
||||
AND e.journal_entry_id NOT IN (
|
||||
SELECT parent_entry_id
|
||||
FROM journal_entries
|
||||
WHERE parent_entry_id IS NOT NULL
|
||||
)
|
||||
AND a.account_code = '701'
|
||||
GROUP BY a.account_code, a.account_name
|
||||
""")
|
||||
old_logic = cur.fetchone()
|
||||
if old_logic:
|
||||
print(f"701売上高(旧ロジック): 借={old_logic['debit']}, 貸={old_logic['credit']}")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("【検証4】修正後のロジック(is_latest=trueで過濾)の結果")
|
||||
print("=" * 80)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
a.account_code,
|
||||
a.account_name,
|
||||
SUM(l.debit) as debit,
|
||||
SUM(l.credit) as credit
|
||||
FROM journal_lines l
|
||||
JOIN journal_entries e ON e.journal_entry_id = l.journal_entry_id
|
||||
JOIN accounts a ON a.account_id = l.account_id
|
||||
WHERE e.is_deleted = false
|
||||
AND e.is_latest = true
|
||||
AND a.account_code = '701'
|
||||
GROUP BY a.account_code, a.account_name
|
||||
""")
|
||||
new_logic = cur.fetchone()
|
||||
if new_logic:
|
||||
print(f"701売上高(新ロジック): 借={new_logic['debit']}, 貸={new_logic['credit']}")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("【検証5】各test要素の詳細")
|
||||
print("=" * 80)
|
||||
for entry in test_entries:
|
||||
eid = entry['journal_entry_id']
|
||||
print(f"\n ID={eid} ({entry['description']}):")
|
||||
cur.execute("""
|
||||
SELECT
|
||||
a.account_code,
|
||||
a.account_name,
|
||||
l.debit,
|
||||
l.credit
|
||||
FROM journal_lines l
|
||||
JOIN accounts a ON a.account_id = l.account_id
|
||||
WHERE l.journal_entry_id = %s
|
||||
ORDER BY a.account_code
|
||||
""", (eid,))
|
||||
lines = cur.fetchall()
|
||||
for line in lines:
|
||||
print(f" {line['account_code']} {line['account_name']}: 借={line['debit']}, 貸={line['credit']}")
|
||||
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user