""" 月次給与計算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, calc_income_tax=request.calc_income_tax, calc_social_insurance=request.calc_social_insurance ) 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, request: schemas.RecalculateRequest = None): """給与を再計算""" if request is None: request = schemas.RecalculateRequest() 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", calc_income_tax=request.calc_income_tax, calc_social_insurance=request.calc_social_insurance ) 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}") 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() return {"message": "給与データを削除しました", "payroll_id": payroll_id}