diff --git a/backend/app/payroll/calculation/router.py b/backend/app/payroll/calculation/router.py index 17acbdd..700aa62 100644 --- a/backend/app/payroll/calculation/router.py +++ b/backend/app/payroll/calculation/router.py @@ -51,7 +51,9 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest): payment_date=request.payment_date, other_allowance=request.other_allowance, resident_tax=request.resident_tax, - other_deduction=request.other_deduction + 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)) @@ -174,8 +176,10 @@ def update_payroll(payroll_id: int, request: schemas.MonthlyPayrollUpdate): @router.post("/{payroll_id}/recalculate", response_model=schemas.MonthlyPayroll) -def recalculate_payroll(payroll_id: int): +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: # 既存データを取得 @@ -204,7 +208,9 @@ def recalculate_payroll(payroll_id: int): other_allowance=existing["other_allowance"], resident_tax=existing["resident_tax"], other_deduction=existing["other_deduction"], - calculated_by="recalculated" + 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)) diff --git a/backend/app/payroll/calculation/schemas.py b/backend/app/payroll/calculation/schemas.py index 163c29f..eb60730 100644 --- a/backend/app/payroll/calculation/schemas.py +++ b/backend/app/payroll/calculation/schemas.py @@ -123,6 +123,14 @@ class PayrollCalculationRequest(BaseModel): other_allowance: Decimal = Decimal("0") resident_tax: Decimal = Decimal("0") other_deduction: Decimal = Decimal("0") + calc_income_tax: bool = True + calc_social_insurance: bool = True + + +class RecalculateRequest(BaseModel): + """再計算リクエスト""" + calc_income_tax: bool = True + calc_social_insurance: bool = True class PayrollApprovalRequest(BaseModel): diff --git a/backend/app/payroll/calculation/service.py b/backend/app/payroll/calculation/service.py index 64d9301..e93ea4f 100644 --- a/backend/app/payroll/calculation/service.py +++ b/backend/app/payroll/calculation/service.py @@ -231,7 +231,9 @@ class PayrollCalculationService: other_allowance: Decimal = Decimal("0"), resident_tax: Decimal = Decimal("0"), other_deduction: Decimal = Decimal("0"), - calculated_by: str = "system" + calculated_by: str = "system", + calc_income_tax: bool = True, + calc_social_insurance: bool = True ) -> Dict[str, Any]: """給与を計算""" @@ -378,7 +380,7 @@ class PayrollCalculationService: health_insurance = ( health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"])) ).quantize(Decimal("0.01")) - else: + elif calc_social_insurance: errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year})") # 介護保険(40歳以上65歳未満が対象、標準報酬月額表から検索した月額で計算) @@ -391,7 +393,7 @@ class PayrollCalculationService: ).quantize(Decimal("0.01")) print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}") else: - errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year})") + errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year})") if calc_social_insurance else None # 厚生年金(上限適用後の標準報酬月額で計算) pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date) @@ -401,7 +403,7 @@ class PayrollCalculationService: 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}") - else: + elif calc_social_insurance: errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year})") # 雇用保険 @@ -417,9 +419,16 @@ class PayrollCalculationService: ).quantize(Decimal("0.01")) elif not is_employment_insurance_eligible: print(f"[DEBUG] この従業員は雇用保険非対象です") - elif not employment_insurance_rate: + 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") + # エラーがある場合は、エラーメッセージを返す if errors: raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors)) @@ -441,11 +450,14 @@ class PayrollCalculationService: employment_insurance ) - income_tax = PayrollCalculationService.calculate_income_tax( - taxable_income, - dependents_count, - payment_date - ) + if calc_income_tax: + income_tax = PayrollCalculationService.calculate_income_tax( + taxable_income, + dependents_count, + payment_date + ) + else: + income_tax = Decimal("0") # 総控除額 total_deduction = ( diff --git a/backend/app/payroll/settings/router.py b/backend/app/payroll/settings/router.py index 51819a7..43fc4f0 100644 --- a/backend/app/payroll/settings/router.py +++ b/backend/app/payroll/settings/router.py @@ -12,6 +12,7 @@ import openpyxl import xlrd from . import schemas from ...core.database import get_connection +from psycopg.errors import UniqueViolation router = APIRouter(prefix="/payroll/settings", tags=["Payroll - Settings"]) @@ -67,45 +68,51 @@ def create_salary_setting(setting: schemas.SalarySettingCreate): ) has_employment_insurance_column = cur.fetchone() - if has_employment_insurance_column: - # カラムが存在する場合 - cur.execute( - """ - INSERT INTO salary_settings ( - employee_id, base_salary, hourly_rate, employment_type, - payment_type, commute_allowance, other_allowance, - employment_insurance_eligible, - valid_from, valid_to - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - RETURNING * - """, - ( - setting.employee_id, setting.base_salary, setting.hourly_rate, - setting.employment_type, setting.payment_type, - setting.commute_allowance, setting.other_allowance, - setting.employment_insurance_eligible, - setting.valid_from, setting.valid_to - ), + try: + if has_employment_insurance_column: + # カラムが存在する場合 + cur.execute( + """ + INSERT INTO salary_settings ( + employee_id, base_salary, hourly_rate, employment_type, + payment_type, commute_allowance, other_allowance, + employment_insurance_eligible, + valid_from, valid_to + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING * + """, + ( + setting.employee_id, setting.base_salary, setting.hourly_rate, + setting.employment_type, setting.payment_type, + setting.commute_allowance, setting.other_allowance, + setting.employment_insurance_eligible, + setting.valid_from, setting.valid_to + ), + ) + else: + # カラムが存在しない場合(互換性維持) + cur.execute( + """ + INSERT INTO salary_settings ( + employee_id, base_salary, hourly_rate, employment_type, + payment_type, commute_allowance, other_allowance, + valid_from, valid_to + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING * + """, + ( + setting.employee_id, setting.base_salary, setting.hourly_rate, + setting.employment_type, setting.payment_type, + setting.commute_allowance, setting.other_allowance, + setting.valid_from, setting.valid_to + ), + ) + print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました") + except UniqueViolation: + raise HTTPException( + status_code=409, + detail="この適用開始日はすでに登録されています。別の日付を指定してください。" ) - else: - # カラムが存在しない場合(互換性維持) - cur.execute( - """ - INSERT INTO salary_settings ( - employee_id, base_salary, hourly_rate, employment_type, - payment_type, commute_allowance, other_allowance, - valid_from, valid_to - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) - RETURNING * - """, - ( - setting.employee_id, setting.base_salary, setting.hourly_rate, - setting.employment_type, setting.payment_type, - setting.commute_allowance, setting.other_allowance, - setting.valid_from, setting.valid_to - ), - ) - print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました") result = cur.fetchone() conn.commit() @@ -184,10 +191,16 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate) with get_connection() as conn: with conn.cursor() as cur: - cur.execute( - f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *", - values, - ) + try: + cur.execute( + f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *", + values, + ) + except UniqueViolation: + raise HTTPException( + status_code=409, + detail="この適用開始日はすでに登録されています。別の日付を指定してください。" + ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="給与設定が見つかりません") @@ -195,6 +208,21 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate) return result +@router.delete("/salary/{setting_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_salary_setting(setting_id: int): + """給与設定を削除""" + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM salary_settings WHERE setting_id = %s RETURNING setting_id", + (setting_id,), + ) + result = cur.fetchone() + if not result: + raise HTTPException(status_code=404, detail="給与設定が見つかりません") + conn.commit() + + # ======================================== # 社会保険料率管理 # ======================================== diff --git a/backend/app/payroll/vouchers/schemas.py b/backend/app/payroll/vouchers/schemas.py index 7813ca0..1567af4 100644 --- a/backend/app/payroll/vouchers/schemas.py +++ b/backend/app/payroll/vouchers/schemas.py @@ -32,6 +32,7 @@ class VoucherDataSummary(BaseModel): employee_id: int employee_code: str employee_name: str + employment_type: Optional[str] = None has_salary_data: bool has_bonus_data: bool salary_count: int = 0 diff --git a/backend/app/payroll/vouchers/service.py b/backend/app/payroll/vouchers/service.py index 00225f5..cb04d24 100644 --- a/backend/app/payroll/vouchers/service.py +++ b/backend/app/payroll/vouchers/service.py @@ -310,6 +310,17 @@ def build_voucher_summary( emp_name = emp_row.get("name") else: emp_code, emp_name = emp_row + + # 雇用形態を最新の給与設定から取得 + cur.execute( + "SELECT employment_type FROM salary_settings WHERE employee_id = %s ORDER BY valid_from DESC LIMIT 1", + (emp_id,) + ) + ss_row = cur.fetchone() + if ss_row: + employment_type = ss_row.get("employment_type") if isinstance(ss_row, dict) else ss_row[0] + else: + employment_type = None except Exception as e: logger.error(f"従業員情報取得エラー: {str(e)}") continue @@ -333,6 +344,7 @@ def build_voucher_summary( employee_id=emp_id, employee_code=emp_code, employee_name=emp_name, + employment_type=employment_type, has_salary_data=len(salary_list) > 0, has_bonus_data=len(bonus_list) > 0, salary_count=len(salary_list), diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index be367ec..eeeeee8 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -129,6 +129,7 @@