@@ -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"))
|
||||
|
||||
# 扶養人数による税率軽減(簡略化)
|
||||
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%
|
||||
|
||||
return (bonus_amount * rate).quantize(Decimal("1"))
|
||||
# 前月給与がない場合: 月額表方式が必要だがフォールバックとして10.21%
|
||||
return (bonus_amount * Decimal("0.1021")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
|
||||
table = _load_bonus_tax_rates()
|
||||
salary_k = float(previous_salary_avg) / 1000 # 千円単位に変換
|
||||
dep_idx = min(int(dependents_count), 7)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user