給与管理システム新規作成
This commit is contained in:
3
backend/app/payroll/calculation/__init__.py
Normal file
3
backend/app/payroll/calculation/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
月次給与計算 (Monthly Payroll Calculation)
|
||||
"""
|
||||
284
backend/app/payroll/calculation/router.py
Normal file
284
backend/app/payroll/calculation/router.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
月次給与計算API (Monthly Payroll Calculation API)
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from . import schemas
|
||||
from .service import PayrollCalculationService
|
||||
from ...core.database import get_connection
|
||||
|
||||
|
||||
router = APIRouter(prefix="/payroll/calculation", tags=["Payroll - Calculation"])
|
||||
|
||||
|
||||
@router.post("/calculate", response_model=schemas.MonthlyPayroll, status_code=status.HTTP_201_CREATED)
|
||||
def calculate_payroll(request: schemas.PayrollCalculationRequest):
|
||||
"""給与を計算して保存"""
|
||||
|
||||
# 既存のデータを確認
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT payroll_id FROM monthly_payroll
|
||||
WHERE employee_id = %s
|
||||
AND payroll_year = %s
|
||||
AND payroll_month = %s
|
||||
""",
|
||||
(request.employee_id, request.payroll_year, request.payroll_month)
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="この従業員の指定月の給与データは既に存在します"
|
||||
)
|
||||
|
||||
# 給与計算を実行
|
||||
try:
|
||||
calc_result = PayrollCalculationService.calculate_payroll(
|
||||
employee_id=request.employee_id,
|
||||
payroll_year=request.payroll_year,
|
||||
payroll_month=request.payroll_month,
|
||||
working_days=request.working_days,
|
||||
working_hours=request.working_hours,
|
||||
overtime_hours=request.overtime_hours,
|
||||
holiday_hours=request.holiday_hours,
|
||||
late_hours=request.late_hours,
|
||||
absent_days=request.absent_days,
|
||||
payment_date=request.payment_date,
|
||||
other_allowance=request.other_allowance,
|
||||
resident_tax=request.resident_tax,
|
||||
other_deduction=request.other_deduction
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# 結果をデータベースに保存
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO monthly_payroll (
|
||||
employee_id, payroll_year, payroll_month, payment_date, status,
|
||||
working_days, working_hours, overtime_hours, holiday_hours, late_hours, absent_days,
|
||||
base_salary, overtime_pay, holiday_pay, commute_allowance, other_allowance, total_payment,
|
||||
health_insurance, care_insurance, pension_insurance, employment_insurance,
|
||||
income_tax, resident_tax, other_deduction, total_deduction,
|
||||
net_payment, 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, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s
|
||||
)
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
calc_result["employee_id"], calc_result["payroll_year"], calc_result["payroll_month"],
|
||||
calc_result["payment_date"], calc_result["status"],
|
||||
calc_result["working_days"], calc_result["working_hours"], calc_result["overtime_hours"],
|
||||
calc_result["holiday_hours"], calc_result["late_hours"], calc_result["absent_days"],
|
||||
calc_result["base_salary"], calc_result["overtime_pay"], calc_result["holiday_pay"],
|
||||
calc_result["commute_allowance"], calc_result["other_allowance"], calc_result["total_payment"],
|
||||
calc_result["health_insurance"], calc_result["care_insurance"],
|
||||
calc_result["pension_insurance"], calc_result["employment_insurance"],
|
||||
calc_result["income_tax"], calc_result["resident_tax"], calc_result["other_deduction"],
|
||||
calc_result["total_deduction"],
|
||||
calc_result["net_payment"], datetime.now(), calc_result["calculated_by"]
|
||||
)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
conn.commit()
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.MonthlyPayroll])
|
||||
def get_payrolls(payroll_year: int = None, payroll_month: int = None, employee_id: int = None):
|
||||
"""給与計算一覧を取得"""
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if payroll_year:
|
||||
conditions.append("payroll_year = %s")
|
||||
params.append(payroll_year)
|
||||
|
||||
if payroll_month:
|
||||
conditions.append("payroll_month = %s")
|
||||
params.append(payroll_month)
|
||||
|
||||
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 monthly_payroll
|
||||
WHERE {where_clause}
|
||||
ORDER BY payroll_year DESC, payroll_month DESC, employee_id
|
||||
""",
|
||||
params
|
||||
)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
@router.get("/{payroll_id}", response_model=schemas.MonthlyPayrollWithDetails)
|
||||
def get_payroll(payroll_id: int):
|
||||
"""給与計算詳細を取得"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM monthly_payroll WHERE payroll_id = %s", (payroll_id,))
|
||||
payroll = cur.fetchone()
|
||||
|
||||
if not payroll:
|
||||
raise HTTPException(status_code=404, detail="給与データが見つかりません")
|
||||
|
||||
cur.execute(
|
||||
"SELECT * FROM payroll_details WHERE payroll_id = %s ORDER BY detail_id",
|
||||
(payroll_id,)
|
||||
)
|
||||
details = cur.fetchall()
|
||||
|
||||
return {**payroll, "details": details}
|
||||
|
||||
|
||||
@router.put("/{payroll_id}", response_model=schemas.MonthlyPayroll)
|
||||
def update_payroll(payroll_id: int, request: schemas.MonthlyPayrollUpdate):
|
||||
"""給与データを更新(再計算は行わない)"""
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
if not update_data:
|
||||
raise HTTPException(status_code=400, detail="更新するデータがありません")
|
||||
|
||||
set_clause = ", ".join([f"{key} = %s" for key in update_data.keys()])
|
||||
values = list(update_data.values()) + [payroll_id]
|
||||
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"UPDATE monthly_payroll SET {set_clause} WHERE payroll_id = %s RETURNING *",
|
||||
values,
|
||||
)
|
||||
result = cur.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="給与データが見つかりません")
|
||||
conn.commit()
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{payroll_id}/recalculate", response_model=schemas.MonthlyPayroll)
|
||||
def recalculate_payroll(payroll_id: int):
|
||||
"""給与を再計算"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 既存データを取得
|
||||
cur.execute("SELECT * FROM monthly_payroll WHERE payroll_id = %s", (payroll_id,))
|
||||
existing = cur.fetchone()
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="給与データが見つかりません")
|
||||
|
||||
if existing["status"] == "approved":
|
||||
raise HTTPException(status_code=400, detail="承認済みの給与は再計算できません")
|
||||
|
||||
# 再計算
|
||||
try:
|
||||
calc_result = PayrollCalculationService.calculate_payroll(
|
||||
employee_id=existing["employee_id"],
|
||||
payroll_year=existing["payroll_year"],
|
||||
payroll_month=existing["payroll_month"],
|
||||
working_days=existing["working_days"],
|
||||
working_hours=existing["working_hours"],
|
||||
overtime_hours=existing["overtime_hours"],
|
||||
holiday_hours=existing["holiday_hours"],
|
||||
late_hours=existing["late_hours"],
|
||||
absent_days=existing["absent_days"],
|
||||
payment_date=existing["payment_date"],
|
||||
other_allowance=existing["other_allowance"],
|
||||
resident_tax=existing["resident_tax"],
|
||||
other_deduction=existing["other_deduction"],
|
||||
calculated_by="recalculated"
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# 更新
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE monthly_payroll SET
|
||||
base_salary = %s, overtime_pay = %s, holiday_pay = %s,
|
||||
commute_allowance = %s, total_payment = %s,
|
||||
health_insurance = %s, pension_insurance = %s, employment_insurance = %s,
|
||||
income_tax = %s, total_deduction = %s, net_payment = %s,
|
||||
calculated_at = %s, calculated_by = %s
|
||||
WHERE payroll_id = %s
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
calc_result["base_salary"], calc_result["overtime_pay"], calc_result["holiday_pay"],
|
||||
calc_result["commute_allowance"], calc_result["total_payment"],
|
||||
calc_result["health_insurance"], calc_result["pension_insurance"],
|
||||
calc_result["employment_insurance"],
|
||||
calc_result["income_tax"], calc_result["total_deduction"], calc_result["net_payment"],
|
||||
datetime.now(), calc_result["calculated_by"],
|
||||
payroll_id
|
||||
)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
conn.commit()
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{payroll_id}/approve", response_model=schemas.MonthlyPayroll)
|
||||
def approve_payroll(payroll_id: int, request: schemas.PayrollApprovalRequest):
|
||||
"""給与を承認"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE monthly_payroll
|
||||
SET status = 'approved', approved_at = %s, approved_by = %s
|
||||
WHERE payroll_id = %s AND status = 'calculated'
|
||||
RETURNING *
|
||||
""",
|
||||
(datetime.now(), request.approved_by, payroll_id)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="給与データが見つからないか、既に承認されています"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{payroll_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_payroll(payroll_id: int):
|
||||
"""給与データを削除"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# ステータス確認
|
||||
cur.execute(
|
||||
"SELECT status FROM monthly_payroll WHERE payroll_id = %s",
|
||||
(payroll_id,)
|
||||
)
|
||||
payroll = cur.fetchone()
|
||||
|
||||
if not payroll:
|
||||
raise HTTPException(status_code=404, detail="給与データが見つかりません")
|
||||
|
||||
if payroll["status"] == "approved":
|
||||
raise HTTPException(status_code=400, detail="承認済みの給与は削除できません")
|
||||
|
||||
cur.execute("DELETE FROM monthly_payroll WHERE payroll_id = %s", (payroll_id,))
|
||||
conn.commit()
|
||||
129
backend/app/payroll/calculation/schemas.py
Normal file
129
backend/app/payroll/calculation/schemas.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
月次給与計算のスキーマ定義 (Payroll Calculation Schemas)
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class PayrollDetailBase(BaseModel):
|
||||
"""給与明細項目"""
|
||||
item_type: str # payment/deduction
|
||||
item_code: str
|
||||
item_name: str
|
||||
amount: Decimal
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class PayrollDetailCreate(PayrollDetailBase):
|
||||
"""給与明細項目の作成"""
|
||||
pass
|
||||
|
||||
|
||||
class PayrollDetail(PayrollDetailBase):
|
||||
"""給与明細項目の情報"""
|
||||
detail_id: int
|
||||
payroll_id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MonthlyPayrollBase(BaseModel):
|
||||
"""月次給与計算の基本情報"""
|
||||
employee_id: int
|
||||
payroll_year: int
|
||||
payroll_month: int
|
||||
payment_date: date
|
||||
|
||||
# 勤怠情報
|
||||
working_days: Decimal = Decimal("0")
|
||||
working_hours: Decimal = Decimal("0")
|
||||
overtime_hours: Decimal = Decimal("0")
|
||||
holiday_hours: Decimal = Decimal("0")
|
||||
late_hours: Decimal = Decimal("0")
|
||||
absent_days: Decimal = Decimal("0")
|
||||
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class MonthlyPayrollCreate(MonthlyPayrollBase):
|
||||
"""月次給与計算の作成"""
|
||||
pass
|
||||
|
||||
|
||||
class MonthlyPayrollUpdate(BaseModel):
|
||||
"""月次給与計算の更新"""
|
||||
payment_date: Optional[date] = None
|
||||
working_days: Optional[Decimal] = None
|
||||
working_hours: Optional[Decimal] = None
|
||||
overtime_hours: Optional[Decimal] = None
|
||||
holiday_hours: Optional[Decimal] = None
|
||||
late_hours: Optional[Decimal] = None
|
||||
absent_days: Optional[Decimal] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class MonthlyPayroll(MonthlyPayrollBase):
|
||||
"""月次給与計算の情報"""
|
||||
payroll_id: int
|
||||
status: str
|
||||
|
||||
# 支給項目
|
||||
base_salary: Decimal
|
||||
overtime_pay: Decimal
|
||||
holiday_pay: Decimal
|
||||
commute_allowance: Decimal
|
||||
other_allowance: Decimal
|
||||
total_payment: Decimal
|
||||
|
||||
# 控除項目
|
||||
health_insurance: Decimal
|
||||
pension_insurance: Decimal
|
||||
employment_insurance: Decimal
|
||||
income_tax: Decimal
|
||||
resident_tax: Decimal
|
||||
other_deduction: Decimal
|
||||
total_deduction: Decimal
|
||||
|
||||
# 差引支給額
|
||||
net_payment: Decimal
|
||||
|
||||
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
|
||||
|
||||
|
||||
class MonthlyPayrollWithDetails(MonthlyPayroll):
|
||||
"""明細項目を含む月次給与情報"""
|
||||
details: list[PayrollDetail] = []
|
||||
|
||||
|
||||
class PayrollCalculationRequest(BaseModel):
|
||||
"""給与計算リクエスト"""
|
||||
employee_id: int
|
||||
payroll_year: int
|
||||
payroll_month: int
|
||||
working_days: Decimal = Decimal("0")
|
||||
working_hours: Decimal = Decimal("0")
|
||||
overtime_hours: Decimal = Decimal("0")
|
||||
holiday_hours: Decimal = Decimal("0")
|
||||
late_hours: Decimal = Decimal("0")
|
||||
absent_days: Decimal = Decimal("0")
|
||||
payment_date: date
|
||||
other_allowance: Decimal = Decimal("0")
|
||||
resident_tax: Decimal = Decimal("0")
|
||||
other_deduction: Decimal = Decimal("0")
|
||||
|
||||
|
||||
class PayrollApprovalRequest(BaseModel):
|
||||
"""給与承認リクエスト"""
|
||||
approved_by: str
|
||||
317
backend/app/payroll/calculation/service.py
Normal file
317
backend/app/payroll/calculation/service.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
給与計算サービス (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
|
||||
}
|
||||
Reference in New Issue
Block a user