529 lines
24 KiB
Python
529 lines
24 KiB
Python
"""
|
||
給与計算サービス (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:
|
||
"""有効な扶養家族数を取得(源泉税控除対象:16歳以上)"""
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# その年の12月31日時点で16歳以上の扶養家族をカウント
|
||
# 源泉所得税の扶養控除対象は16歳以上が対象
|
||
print(f"[DEBUG] 扶養人数取得: 従業員ID={employee_id}, 対象日={target_date}")
|
||
|
||
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)
|
||
AND birth_date IS NOT NULL
|
||
AND EXTRACT(YEAR FROM AGE(DATE(EXTRACT(YEAR FROM %s) || '-12-31'), birth_date)) >= 16
|
||
""",
|
||
(employee_id, target_date, target_date, target_date)
|
||
)
|
||
result = cur.fetchone()
|
||
count = result["count"] if result else 0
|
||
print(f"[DEBUG] 取得した扶養人数: {count}人")
|
||
return count
|
||
|
||
@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, 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 calculate_income_tax(
|
||
monthly_income: Decimal,
|
||
dependents_count: int,
|
||
target_date: date
|
||
) -> Decimal:
|
||
"""所得税を計算(税年度がない場合は前年度の表を参照)"""
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# 現在の年度で検索
|
||
# 月収がmonthly_income_from <= monthly_income < monthly_income_toの範囲に入るか、
|
||
# または月収がmonthly_income_from <= monthly_income <= monthly_income_toの範囲(Toが同値の場合も含む)
|
||
print(f"[DEBUG] 所得税検索: 月収={monthly_income}, 扶養人数={dependents_count}, 日付={target_date}")
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT tax_id, monthly_income_from, monthly_income_to, tax_amount, valid_from
|
||
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()
|
||
|
||
if result:
|
||
print(f"[DEBUG] 見つかった(範囲検索): From={result['monthly_income_from']}, To={result['monthly_income_to']}, 税額={result['tax_amount']}")
|
||
|
||
# 見つからない場合は、Toが同値のケースも検索(月収がちょうどToの値に達する場合)
|
||
if not result:
|
||
cur.execute(
|
||
"""
|
||
SELECT tax_id, monthly_income_from, monthly_income_to, tax_amount, valid_from
|
||
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()
|
||
if result:
|
||
print(f"[DEBUG] 見つかった(To同値検索): From={result['monthly_income_from']}, To={result['monthly_income_to']}, 税額={result['tax_amount']}")
|
||
|
||
# データが見つからない場合は、指定された日付に関係なく前年度以前の表を検索
|
||
if not result:
|
||
for year_offset in range(1, 4):
|
||
previous_date = date(target_date.year - year_offset, target_date.month, target_date.day)
|
||
print(f"[DEBUG] 前年度検索: {previous_date}")
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT tax_id, monthly_income_from, monthly_income_to, tax_amount, valid_from
|
||
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
|
||
""",
|
||
(previous_date, previous_date, dependents_count, monthly_income, monthly_income)
|
||
)
|
||
result = cur.fetchone()
|
||
if result:
|
||
print(f"[DEBUG] 前年度で見つかった: {previous_date}, 税額={result['tax_amount']}")
|
||
break
|
||
|
||
# Toが同値のケースも検索
|
||
cur.execute(
|
||
"""
|
||
SELECT tax_id, monthly_income_from, monthly_income_to, tax_amount, valid_from
|
||
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
|
||
""",
|
||
(previous_date, previous_date, dependents_count, monthly_income, monthly_income)
|
||
)
|
||
result = cur.fetchone()
|
||
if result:
|
||
print(f"[DEBUG] 前年度で見つかった(To同値): {previous_date}, 税額={result['tax_amount']}")
|
||
break
|
||
|
||
if not result:
|
||
print(f"[DEBUG] 該当する所得税データなし、デフォルト値 0 を返す")
|
||
|
||
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",
|
||
calc_income_tax: bool = True,
|
||
calc_social_insurance: bool = True
|
||
) -> Dict[str, Any]:
|
||
"""給与を計算"""
|
||
|
||
# バリデーションエラーを集約
|
||
errors = []
|
||
|
||
# 給与設定を取得
|
||
salary_setting = PayrollCalculationService.get_salary_setting(employee_id, payment_date)
|
||
if not salary_setting:
|
||
errors.append("給与設定が見つかりません")
|
||
|
||
# 基本給の計算
|
||
base_salary = Decimal(str(salary_setting["base_salary"])) if salary_setting else Decimal("0")
|
||
|
||
# 時給の場合は勤務時間から計算
|
||
if salary_setting and 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 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 and salary_setting["hourly_rate"]:
|
||
hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
|
||
else:
|
||
# 月給を時給に換算(月平均労働時間を160時間と仮定)
|
||
hourly_rate = (Decimal(str(salary_setting["base_salary"])) if salary_setting else Decimal("0")) / 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
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# 給与年月の標準報酬月額表から、総支給額が属する等級を検索
|
||
target_year = payment_date.year
|
||
result = None
|
||
|
||
# まず指定年度の標準報酬月額表を検索
|
||
cur.execute(
|
||
"""
|
||
SELECT monthly_amount, child_support
|
||
FROM standard_remuneration
|
||
WHERE rate_year = %s
|
||
AND salary_from <= %s
|
||
AND (salary_to IS NULL OR salary_to > %s)
|
||
ORDER BY rate_year DESC, salary_from DESC
|
||
LIMIT 1
|
||
""",
|
||
(target_year, total_payment, total_payment)
|
||
)
|
||
result = cur.fetchone()
|
||
|
||
# 見つからない場合は前年度以前を検索(最大3年遡る)
|
||
if not result:
|
||
for year_offset in range(1, 4):
|
||
previous_year = target_year - year_offset
|
||
cur.execute(
|
||
"""
|
||
SELECT monthly_amount, child_support
|
||
FROM standard_remuneration
|
||
WHERE rate_year = %s
|
||
AND salary_from <= %s
|
||
AND (salary_to IS NULL OR salary_to > %s)
|
||
ORDER BY rate_year DESC, salary_from DESC
|
||
LIMIT 1
|
||
""",
|
||
(previous_year, total_payment, total_payment)
|
||
)
|
||
result = cur.fetchone()
|
||
if result:
|
||
break
|
||
|
||
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}")
|
||
else:
|
||
print(f"[DEBUG] 標準報酬月額表検索: 該当等級なし(総支給額={total_payment})")
|
||
child_support_full = Decimal("0")
|
||
|
||
|
||
# 社会保険料上限を取得
|
||
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歳以上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
|
||
|
||
# 健康保険(標準報酬月額表から検索した月額で計算)
|
||
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("0.01"))
|
||
elif calc_social_insurance:
|
||
errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year})")
|
||
|
||
# 介護保険(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("0.01"))
|
||
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
|
||
|
||
# 厚生年金(上限適用後の標準報酬月額で計算)
|
||
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("0.01"))
|
||
print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}")
|
||
elif calc_social_insurance:
|
||
errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year})")
|
||
|
||
# 雇用保険
|
||
employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date)
|
||
employment_insurance = Decimal("0")
|
||
|
||
# 子ども・子育て支援金(令和8年4月分以降のみ適用)
|
||
child_support = Decimal("0")
|
||
is_child_support_applicable = (
|
||
(payroll_year == 2026 and payroll_month >= 4) 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}")
|
||
|
||
# 雇用保険加入対象かどうか確認
|
||
is_employment_insurance_eligible = salary_setting.get("employment_insurance_eligible", True) if salary_setting else True
|
||
|
||
if is_employment_insurance_eligible and employment_insurance_rate:
|
||
employment_insurance = (
|
||
total_payment * Decimal(str(employment_insurance_rate["employee_rate"]))
|
||
).quantize(Decimal("0.01"))
|
||
elif not is_employment_insurance_eligible:
|
||
print(f"[DEBUG] この従業員は雇用保険非対象です")
|
||
elif not employment_insurance_rate and calc_social_insurance:
|
||
errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year})")
|
||
|
||
# 社会保険計算スキップ時は全て0にする
|
||
if not calc_social_insurance:
|
||
health_insurance = Decimal("0")
|
||
care_insurance = Decimal("0")
|
||
pension_insurance = Decimal("0")
|
||
employment_insurance = Decimal("0")
|
||
child_support = Decimal("0")
|
||
|
||
# エラーがある場合は、エラーメッセージを返す
|
||
if errors:
|
||
raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors))
|
||
|
||
# 所得税の計算
|
||
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 -
|
||
child_support
|
||
)
|
||
|
||
if calc_income_tax:
|
||
income_tax = PayrollCalculationService.calculate_income_tax(
|
||
taxable_income,
|
||
dependents_count,
|
||
payment_date
|
||
)
|
||
else:
|
||
income_tax = Decimal("0")
|
||
|
||
# 総控除額
|
||
total_deduction = (
|
||
health_insurance +
|
||
care_insurance +
|
||
pension_insurance +
|
||
employment_insurance +
|
||
child_support +
|
||
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,
|
||
"child_support": child_support,
|
||
"income_tax": income_tax,
|
||
"resident_tax": resident_tax,
|
||
"other_deduction": other_deduction,
|
||
"total_deduction": total_deduction,
|
||
|
||
# 差引支給額
|
||
"net_payment": net_payment,
|
||
|
||
"calculated_by": calculated_by
|
||
}
|