給与管理システム新規作成

This commit is contained in:
admin
2026-01-20 11:15:49 +09:00
parent 9dc7cbcb07
commit 8a00de8f03
45 changed files with 8925 additions and 5 deletions

View File

@@ -0,0 +1 @@
# Bonus Module

View File

@@ -0,0 +1,139 @@
"""
賞与計算API (Bonus Calculation API)
"""
from fastapi import APIRouter, HTTPException, status
from typing import List
from datetime import datetime
from . import schemas
from .service import BonusCalculationService
from ...core.database import get_connection
router = APIRouter(prefix="/payroll/bonus", tags=["Payroll - Bonus"])
@router.post("/calculate", response_model=schemas.BonusPayment, status_code=status.HTTP_201_CREATED)
def calculate_bonus(request: schemas.BonusCalculationRequest):
"""賞与を計算"""
# 賞与を計算
calc_result = BonusCalculationService.calculate_bonus(
employee_id=request.employee_id,
bonus_year=request.bonus_year,
bonus_month=request.bonus_month,
bonus_type=request.bonus_type,
payment_date=request.payment_date,
base_bonus=request.base_bonus,
performance_bonus=request.performance_bonus,
other_bonus=request.other_bonus,
other_deduction=request.other_deduction,
calculated_by=request.calculated_by
)
# データベースに保存
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO bonus_payments (
employee_id, bonus_year, bonus_month, bonus_type, payment_date, status,
base_bonus, performance_bonus, other_bonus, total_bonus,
health_insurance, care_insurance, pension_insurance, employment_insurance,
income_tax, other_deduction, total_deduction,
net_bonus, calculated_at, calculated_by
) VALUES (
%s, %s, %s, %s, %s, %s,
%s, %s, %s, %s,
%s, %s, %s, %s,
%s, %s, %s,
%s, %s, %s
)
RETURNING *
""",
(
calc_result["employee_id"], calc_result["bonus_year"], calc_result["bonus_month"],
calc_result["bonus_type"], calc_result["payment_date"], calc_result["status"],
calc_result["base_bonus"], calc_result["performance_bonus"],
calc_result["other_bonus"], calc_result["total_bonus"],
calc_result["health_insurance"], calc_result["care_insurance"],
calc_result["pension_insurance"], calc_result["employment_insurance"],
calc_result["income_tax"], calc_result["other_deduction"],
calc_result["total_deduction"],
calc_result["net_bonus"], datetime.now(), calc_result["calculated_by"]
)
)
result = cur.fetchone()
conn.commit()
return result
@router.get("/", response_model=List[schemas.BonusPayment])
def get_bonuses(bonus_year: int = None, employee_id: int = None):
"""賞与一覧を取得"""
conditions = []
params = []
if bonus_year:
conditions.append("bonus_year = %s")
params.append(bonus_year)
if employee_id:
conditions.append("employee_id = %s")
params.append(employee_id)
where_clause = " AND ".join(conditions) if conditions else "1=1"
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
f"""
SELECT * FROM bonus_payments
WHERE {where_clause}
ORDER BY bonus_year DESC, bonus_month DESC
""",
params
)
return cur.fetchall()
@router.get("/{bonus_id}", response_model=schemas.BonusPayment)
def get_bonus(bonus_id: int):
"""賞与詳細を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM bonus_payments WHERE bonus_id = %s", (bonus_id,))
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="賞与が見つかりません")
return result
@router.post("/{bonus_id}/approve", response_model=schemas.BonusPayment)
def approve_bonus(bonus_id: int, approved_by: str):
"""賞与を承認"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE bonus_payments
SET status = 'approved', approved_at = %s, approved_by = %s
WHERE bonus_id = %s AND status = 'calculated'
RETURNING *
""",
(datetime.now(), approved_by, bonus_id)
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="賞与が見つかりません、または既に承認済みです")
conn.commit()
return result
@router.delete("/{bonus_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bonus(bonus_id: int):
"""賞与を削除"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM bonus_payments WHERE bonus_id = %s AND status = 'draft'", (bonus_id,))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="賞与が見つかりません、または削除できません")
conn.commit()

View File

@@ -0,0 +1,63 @@
"""
賞与計算のスキーマ定義 (Bonus Schemas)
"""
from pydantic import BaseModel
from datetime import date, datetime
from typing import Optional
from decimal import Decimal
class BonusPaymentBase(BaseModel):
"""賞与計算基本情報"""
employee_id: int
bonus_year: int
bonus_month: int
bonus_type: str # 夏季賞与/冬季賞与/決算賞与/その他
payment_date: date
base_bonus: Decimal = Decimal("0")
performance_bonus: Decimal = Decimal("0")
other_bonus: Decimal = Decimal("0")
class BonusPaymentCreate(BonusPaymentBase):
"""賞与計算の作成"""
pass
class BonusCalculationRequest(BaseModel):
"""賞与計算リクエスト"""
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"
class BonusPayment(BonusPaymentBase):
"""賞与計算結果"""
bonus_id: int
status: str
total_bonus: Decimal
health_insurance: Decimal
care_insurance: Decimal
pension_insurance: Decimal
employment_insurance: Decimal
income_tax: Decimal
other_deduction: Decimal
total_deduction: Decimal
net_bonus: Decimal
notes: Optional[str] = None
calculated_at: Optional[datetime] = None
calculated_by: Optional[str] = None
approved_at: Optional[datetime] = None
approved_by: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

View File

@@ -0,0 +1,235 @@
"""
賞与計算サービス (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) -> 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 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 monthly_payroll
WHERE employee_id = %s
AND payment_date < %s
AND status IN ('approved', 'paid')
ORDER BY payment_date DESC
LIMIT 3
""",
(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()
# 年齢を計算
age = 0
if employee_info and employee_info["birth_date"]:
age = (payment_date - employee_info["birth_date"]).days // 365
# 健康保険賞与額の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"))
# 所得税の計算(賞与特有の計算)
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
)
income_tax = BonusCalculationService.calculate_bonus_income_tax(
taxable_bonus,
previous_salary_avg,
dependents_count
)
# 総控除額
total_deduction = (
health_insurance +
care_insurance +
pension_insurance +
employment_insurance +
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,
"income_tax": income_tax,
"other_deduction": other_deduction,
"total_deduction": total_deduction,
# 差引支給額
"net_bonus": net_bonus,
"calculated_by": calculated_by
}