298 lines
12 KiB
Python
298 lines
12 KiB
Python
"""
|
||
賞与計算サービス (Bonus Calculation Service)
|
||
"""
|
||
from decimal import Decimal
|
||
from datetime import date
|
||
from typing import Dict, Any
|
||
from ...core.database import get_connection
|
||
|
||
|
||
class BonusCalculationService:
|
||
"""賞与計算サービス"""
|
||
|
||
@staticmethod
|
||
def get_insurance_rate(rate_type: str, target_date: date, calculation_target: str = '賞与') -> Dict[str, Any]:
|
||
"""保険料率を取得(賞与用、年度がない場合は前年度を参照)"""
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# rate_yearで年度を判定
|
||
target_year = target_date.year
|
||
cur.execute(
|
||
"""
|
||
SELECT * FROM insurance_rates
|
||
WHERE rate_type = %s
|
||
AND rate_year = %s
|
||
AND calculation_target = %s
|
||
ORDER BY rate_id DESC
|
||
LIMIT 1
|
||
""",
|
||
(rate_type, target_year, calculation_target)
|
||
)
|
||
result = cur.fetchone()
|
||
|
||
# 現在の年度でデータが見つからない場合は前年度を検索(最大3年遡る)
|
||
if not result:
|
||
for year_offset in range(1, 4):
|
||
previous_year = target_year - year_offset
|
||
cur.execute(
|
||
"""
|
||
SELECT * FROM insurance_rates
|
||
WHERE rate_type = %s
|
||
AND rate_year = %s
|
||
AND calculation_target = %s
|
||
ORDER BY rate_id DESC
|
||
LIMIT 1
|
||
""",
|
||
(rate_type, previous_year, calculation_target)
|
||
)
|
||
result = cur.fetchone()
|
||
if result:
|
||
break
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
def get_insurance_limit(setting_type: str, target_date: date) -> Decimal:
|
||
"""社会保険料上限を取得"""
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT limit_amount FROM insurance_limit_settings
|
||
WHERE setting_type = %s
|
||
AND valid_from <= %s
|
||
AND (valid_to IS NULL OR valid_to >= %s)
|
||
ORDER BY valid_from DESC
|
||
LIMIT 1
|
||
""",
|
||
(setting_type, target_date, target_date)
|
||
)
|
||
result = cur.fetchone()
|
||
return Decimal(str(result["limit_amount"])) if result else Decimal("0")
|
||
|
||
@staticmethod
|
||
def get_previous_salary_avg(employee_id: int, payment_date: date) -> Decimal:
|
||
"""前月までの3ヶ月平均給与を取得(賞与所得税計算用)"""
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# 前月までの3ヶ月の総支給額を取得
|
||
cur.execute(
|
||
"""
|
||
SELECT AVG(total_payment - commute_allowance) as avg_salary
|
||
FROM (
|
||
SELECT total_payment, commute_allowance
|
||
FROM monthly_payroll
|
||
WHERE employee_id = %s
|
||
AND payment_date < %s
|
||
AND status IN ('approved', 'paid')
|
||
ORDER BY payment_date DESC
|
||
LIMIT 3
|
||
) as recent_payroll
|
||
""",
|
||
(employee_id, payment_date)
|
||
)
|
||
result = cur.fetchone()
|
||
return Decimal(str(result["avg_salary"])) if result and result["avg_salary"] else Decimal("0")
|
||
|
||
@staticmethod
|
||
def calculate_bonus_income_tax(
|
||
bonus_amount: Decimal,
|
||
previous_salary_avg: Decimal,
|
||
dependents_count: int
|
||
) -> Decimal:
|
||
"""
|
||
賞与所得税を計算
|
||
賞与税率 = (前月給与額 - 社会保険料) × 扶養人数に応じた税率
|
||
"""
|
||
# 簡易計算: 賞与額から社会保険料概算を引いた額の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"))
|
||
|
||
@staticmethod
|
||
def calculate_bonus(
|
||
employee_id: int,
|
||
bonus_year: int,
|
||
bonus_month: int,
|
||
bonus_type: str,
|
||
payment_date: date,
|
||
base_bonus: Decimal,
|
||
performance_bonus: Decimal = Decimal("0"),
|
||
other_bonus: Decimal = Decimal("0"),
|
||
other_deduction: Decimal = Decimal("0"),
|
||
calculated_by: str = "system"
|
||
) -> Dict[str, Any]:
|
||
"""賞与を計算"""
|
||
|
||
# 総支給額
|
||
total_bonus = base_bonus + performance_bonus + other_bonus
|
||
|
||
# 社会保険料上限を取得(賞与専用の標準賞与額上限を使用)
|
||
health_insurance_limit = BonusCalculationService.get_insurance_limit(
|
||
"健康保険標準賞与額上限", payment_date
|
||
)
|
||
pension_insurance_limit = BonusCalculationService.get_insurance_limit(
|
||
"厚生年金標準賞与額上限", payment_date
|
||
)
|
||
|
||
# 賞与の標準賞与額(上限適用)
|
||
health_standard_bonus = min(total_bonus, health_insurance_limit) if health_insurance_limit > 0 else total_bonus
|
||
pension_standard_bonus = min(total_bonus, pension_insurance_limit) if pension_insurance_limit > 0 else total_bonus
|
||
|
||
# 従業員情報を取得(年齢計算のため)
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"SELECT birth_date FROM employees WHERE employee_id = %s",
|
||
(employee_id,)
|
||
)
|
||
employee_info = cur.fetchone()
|
||
|
||
# 年齢を計算(介護保険は40歳以上65歳未満が対象)
|
||
age = 0
|
||
if employee_info and employee_info["birth_date"]:
|
||
birth_date = employee_info["birth_date"]
|
||
# より正確な年齢計算
|
||
age = payment_date.year - birth_date.year
|
||
# 誕生日がまだ来ていない場合は-1
|
||
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||
age -= 1
|
||
|
||
# 健康保険(賞与額の1/1000で計算)
|
||
health_insurance_rate = BonusCalculationService.get_insurance_rate("健康保険", payment_date)
|
||
health_insurance = Decimal("0")
|
||
if health_insurance_rate:
|
||
health_insurance = (
|
||
health_standard_bonus * Decimal(str(health_insurance_rate["employee_rate"]))
|
||
).quantize(Decimal("1"))
|
||
|
||
# 介護保険(40歳以上65歳未満が対象)
|
||
care_insurance = Decimal("0")
|
||
if 40 <= age < 65:
|
||
care_insurance_rate = BonusCalculationService.get_insurance_rate("介護保険", payment_date)
|
||
if care_insurance_rate:
|
||
care_insurance = (
|
||
health_standard_bonus * Decimal(str(care_insurance_rate["employee_rate"]))
|
||
).quantize(Decimal("1"))
|
||
|
||
# 厚生年金
|
||
pension_insurance_rate = BonusCalculationService.get_insurance_rate("厚生年金", payment_date)
|
||
pension_insurance = Decimal("0")
|
||
if pension_insurance_rate:
|
||
pension_insurance = (
|
||
pension_standard_bonus * Decimal(str(pension_insurance_rate["employee_rate"]))
|
||
).quantize(Decimal("1"))
|
||
|
||
# 雇用保険
|
||
employment_insurance_rate = BonusCalculationService.get_insurance_rate("雇用保険", payment_date)
|
||
employment_insurance = Decimal("0")
|
||
if employment_insurance_rate:
|
||
employment_insurance = (
|
||
total_bonus * Decimal(str(employment_insurance_rate["employee_rate"]))
|
||
).quantize(Decimal("1"))
|
||
|
||
# 子ども・子育て支援金(令和8年4月分以降のみ適用)
|
||
# 賞与の場合: 標準報酬月額表の淨化率を使い、total_bonus × 子育て率 / 2
|
||
child_support = Decimal("0")
|
||
is_child_support_applicable = (
|
||
(bonus_year == 2026 and bonus_month >= 4) 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}")
|
||
|
||
# 所得税の計算(賞与特有の計算)
|
||
from ..calculation.service import PayrollCalculationService
|
||
dependents_count = PayrollCalculationService.get_active_dependents_count(employee_id, payment_date)
|
||
previous_salary_avg = BonusCalculationService.get_previous_salary_avg(employee_id, payment_date)
|
||
|
||
# 課税対象額
|
||
taxable_bonus = (
|
||
total_bonus -
|
||
health_insurance -
|
||
care_insurance -
|
||
pension_insurance -
|
||
employment_insurance -
|
||
child_support
|
||
)
|
||
|
||
income_tax = BonusCalculationService.calculate_bonus_income_tax(
|
||
taxable_bonus,
|
||
previous_salary_avg,
|
||
dependents_count
|
||
)
|
||
|
||
# 総控除額
|
||
total_deduction = (
|
||
health_insurance +
|
||
care_insurance +
|
||
pension_insurance +
|
||
employment_insurance +
|
||
child_support +
|
||
income_tax +
|
||
other_deduction
|
||
)
|
||
|
||
# 差引支給額
|
||
net_bonus = total_bonus - total_deduction
|
||
|
||
return {
|
||
"employee_id": employee_id,
|
||
"bonus_year": bonus_year,
|
||
"bonus_month": bonus_month,
|
||
"bonus_type": bonus_type,
|
||
"payment_date": payment_date,
|
||
"status": "calculated",
|
||
|
||
# 支給項目
|
||
"base_bonus": base_bonus,
|
||
"performance_bonus": performance_bonus,
|
||
"other_bonus": other_bonus,
|
||
"total_bonus": total_bonus,
|
||
|
||
# 控除項目
|
||
"health_insurance": health_insurance,
|
||
"care_insurance": care_insurance,
|
||
"pension_insurance": pension_insurance,
|
||
"employment_insurance": employment_insurance,
|
||
"child_support": child_support,
|
||
"income_tax": income_tax,
|
||
"other_deduction": other_deduction,
|
||
"total_deduction": total_deduction,
|
||
|
||
# 差引支給額
|
||
"net_bonus": net_bonus,
|
||
|
||
"calculated_by": calculated_by
|
||
}
|