Files
njts-accounting-core/backend/app/payroll/calculation/service.py
2026-01-20 11:15:49 +09:00

318 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
給与計算サービス (Payroll Calculation Service)
"""
from decimal import Decimal
from datetime import date
from typing import Dict, Any
from ...core.database import get_connection
class PayrollCalculationService:
"""給与計算サービス"""
@staticmethod
def get_active_dependents_count(employee_id: int, target_date: date) -> int:
"""有効な扶養家族数を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COUNT(*) as count FROM dependents
WHERE employee_id = %s
AND valid_from <= %s
AND (valid_to IS NULL OR valid_to >= %s)
""",
(employee_id, target_date, target_date)
)
result = cur.fetchone()
return result["count"] if result else 0
@staticmethod
def get_salary_setting(employee_id: int, target_date: date) -> Dict[str, Any]:
"""給与設定を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT * FROM salary_settings
WHERE employee_id = %s
AND valid_from <= %s
AND (valid_to IS NULL OR valid_to >= %s)
ORDER BY valid_from DESC
LIMIT 1
""",
(employee_id, target_date, target_date)
)
return cur.fetchone()
@staticmethod
def get_insurance_rate(rate_type: str, target_date: date) -> Dict[str, Any]:
"""保険料率を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT * FROM insurance_rates
WHERE rate_type = %s
AND valid_from <= %s
AND (valid_to IS NULL OR valid_to >= %s)
ORDER BY valid_from DESC
LIMIT 1
""",
(rate_type, target_date, target_date)
)
return cur.fetchone()
@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 calculate_income_tax(
monthly_income: Decimal,
dependents_count: int,
target_date: date
) -> Decimal:
"""所得税を計算"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT tax_amount FROM income_tax_table
WHERE valid_from <= %s
AND (valid_to IS NULL OR valid_to >= %s)
AND dependents_count = %s
AND monthly_income_from <= %s
AND monthly_income_to >= %s
ORDER BY valid_from DESC
LIMIT 1
""",
(target_date, target_date, dependents_count, monthly_income, monthly_income)
)
result = cur.fetchone()
return Decimal(str(result["tax_amount"])) if result else Decimal("0")
@staticmethod
def calculate_payroll(
employee_id: int,
payroll_year: int,
payroll_month: int,
working_days: Decimal,
working_hours: Decimal,
overtime_hours: Decimal,
holiday_hours: Decimal,
late_hours: Decimal,
absent_days: Decimal,
payment_date: date,
other_allowance: Decimal = Decimal("0"),
resident_tax: Decimal = Decimal("0"),
other_deduction: Decimal = Decimal("0"),
calculated_by: str = "system"
) -> Dict[str, Any]:
"""給与を計算"""
# 給与設定を取得
salary_setting = PayrollCalculationService.get_salary_setting(employee_id, payment_date)
if not salary_setting:
raise ValueError("給与設定が見つかりません")
# 基本給の計算
base_salary = Decimal(str(salary_setting["base_salary"]))
# 時給の場合は勤務時間から計算
if salary_setting["payment_type"] == "時給" and salary_setting["hourly_rate"]:
hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
base_salary = hourly_rate * working_hours
# 欠勤控除(日給制の場合)
absence_deduction = Decimal("0")
if absent_days > 0 and salary_setting["payment_type"] == "月給":
# 月給を営業日数で割って欠勤日数を掛ける(簡易計算)
daily_rate = base_salary / Decimal("22") # 月平均営業日数を22日と仮定
absence_deduction = daily_rate * absent_days
base_salary = base_salary - absence_deduction
# 残業手当の計算(時給 × 1.25
overtime_pay = Decimal("0")
if overtime_hours > 0:
if salary_setting["hourly_rate"]:
hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
else:
# 月給を時給に換算月平均労働時間を160時間と仮定
hourly_rate = Decimal(str(salary_setting["base_salary"])) / Decimal("160")
overtime_pay = hourly_rate * overtime_hours * Decimal("1.25")
# 休日手当の計算(時給 × 1.35
holiday_pay = Decimal("0")
if holiday_hours > 0:
if salary_setting["hourly_rate"]:
hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
else:
hourly_rate = Decimal(str(salary_setting["base_salary"])) / Decimal("160")
holiday_pay = hourly_rate * holiday_hours * Decimal("1.35")
# 通勤手当
commute_allowance = Decimal(str(salary_setting["commute_allowance"]))
# 総支給額
total_payment = (
base_salary + overtime_pay + holiday_pay +
commute_allowance + other_allowance
)
# 社会保険料の計算(標準報酬月額を総支給額と仮定)
standard_monthly_salary = total_payment
# 社会保険料上限を取得
health_insurance_limit = PayrollCalculationService.get_insurance_limit(
"健康保険標準報酬月額上限", payment_date
)
pension_insurance_limit = PayrollCalculationService.get_insurance_limit(
"厚生年金標準報酬月額上限", payment_date
)
commute_tax_free_limit = PayrollCalculationService.get_insurance_limit(
"通勤手当非課税限度額", payment_date
)
# 標準報酬月額に上限を適用
health_standard_salary = min(standard_monthly_salary, health_insurance_limit) if health_insurance_limit > 0 else standard_monthly_salary
pension_standard_salary = min(standard_monthly_salary, pension_insurance_limit) if pension_insurance_limit > 0 else standard_monthly_salary
# 従業員情報を取得(年齢計算のため)
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歳以上が対象
age = 0
if employee_info and employee_info["birth_date"]:
age = (payment_date - employee_info["birth_date"]).days // 365
# 健康保険(上限適用後の標準報酬月額で計算)
health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date)
health_insurance = Decimal("0")
if health_insurance_rate:
health_insurance = (
health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"]))
).quantize(Decimal("1"))
# 介護保険40歳以上65歳未満が対象、健康保険と同じ標準報酬月額
care_insurance = Decimal("0")
if 40 <= age < 65:
care_insurance_rate = PayrollCalculationService.get_insurance_rate("介護保険", payment_date)
if care_insurance_rate:
care_insurance = (
health_standard_salary * Decimal(str(care_insurance_rate["employee_rate"]))
).quantize(Decimal("1"))
# 厚生年金(上限適用後の標準報酬月額で計算)
pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date)
pension_insurance = Decimal("0")
if pension_insurance_rate:
pension_insurance = (
pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"]))
).quantize(Decimal("1"))
# 雇用保険
employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date)
employment_insurance = Decimal("0")
if employment_insurance_rate:
employment_insurance = (
total_payment * Decimal(str(employment_insurance_rate["employee_rate"]))
).quantize(Decimal("1"))
# 所得税の計算
dependents_count = PayrollCalculationService.get_active_dependents_count(employee_id, payment_date)
# 通勤手当非課税限度額を取得設定がなければ150000円
if commute_tax_free_limit == Decimal("0"):
commute_tax_free_limit = Decimal("150000")
# 課税対象額 = 総支給額 - 通勤手当(非課税限度額まで) - 社会保険料
taxable_income = (
total_payment -
min(commute_allowance, commute_tax_free_limit) -
health_insurance -
care_insurance -
pension_insurance -
employment_insurance
)
income_tax = PayrollCalculationService.calculate_income_tax(
taxable_income,
dependents_count,
payment_date
)
# 総控除額
total_deduction = (
health_insurance +
care_insurance +
pension_insurance +
employment_insurance +
income_tax +
resident_tax +
other_deduction
)
# 差引支給額
net_payment = total_payment - total_deduction
return {
"employee_id": employee_id,
"payroll_year": payroll_year,
"payroll_month": payroll_month,
"payment_date": payment_date,
"status": "calculated",
# 勤怠情報
"working_days": working_days,
"working_hours": working_hours,
"overtime_hours": overtime_hours,
"holiday_hours": holiday_hours,
"late_hours": late_hours,
"absent_days": absent_days,
# 支給項目
"base_salary": base_salary,
"overtime_pay": overtime_pay,
"holiday_pay": holiday_pay,
"commute_allowance": commute_allowance,
"other_allowance": other_allowance,
"total_payment": total_payment,
# 控除項目
"health_insurance": health_insurance,
"care_insurance": care_insurance,
"pension_insurance": pension_insurance,
"employment_insurance": employment_insurance,
"income_tax": income_tax,
"resident_tax": resident_tax,
"other_deduction": other_deduction,
"total_deduction": total_deduction,
# 差引支給額
"net_payment": net_payment,
"calculated_by": calculated_by
}