""" 給与設定・税率・保険料管理API (Settings Management API) """ from fastapi import APIRouter, HTTPException, status, UploadFile, File from typing import List from datetime import date import csv import io from decimal import Decimal from pathlib import Path import openpyxl import xlrd from . import schemas from ...core.database import get_connection router = APIRouter(prefix="/payroll/settings", tags=["Payroll - Settings"]) # ======================================== # 給与基本設定 # ======================================== @router.post("/salary", response_model=schemas.SalarySetting, status_code=status.HTTP_201_CREATED) def create_salary_setting(setting: schemas.SalarySettingCreate): """給与設定を作成""" with get_connection() as conn: with conn.cursor() as cur: # 従業員の存在確認 cur.execute("SELECT employee_id FROM employees WHERE employee_id = %s", (setting.employee_id,)) if not cur.fetchone(): raise HTTPException(status_code=404, detail="従業員が見つかりません") 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 ), ) result = cur.fetchone() conn.commit() return result @router.get("/salary/{employee_id}", response_model=List[schemas.SalarySetting]) def get_salary_settings(employee_id: int): """従業員の給与設定履歴を取得""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ SELECT * FROM salary_settings WHERE employee_id = %s ORDER BY valid_from DESC """, (employee_id,) ) return cur.fetchall() @router.get("/salary/{employee_id}/current", response_model=schemas.SalarySetting) def get_current_salary_setting(employee_id: int, target_date: date = None): """従業員の現在の給与設定を取得""" if target_date is None: target_date = date.today() 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) ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="給与設定が見つかりません") return result @router.put("/salary/{setting_id}", response_model=schemas.SalarySetting) def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate): """給与設定を更新""" update_data = setting.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()) + [setting_id] 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, ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="給与設定が見つかりません") conn.commit() return result # ======================================== # 社会保険料率管理 # ======================================== @router.post("/insurance-rates", response_model=schemas.InsuranceRate, status_code=status.HTTP_201_CREATED) def create_insurance_rate(rate: schemas.InsuranceRateCreate): """保険料率を作成""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO insurance_rates ( rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """, ( rate.rate_year, rate.rate_type, rate.calculation_target, rate.prefecture, rate.employee_rate, rate.employer_rate, rate.care_insurance_age_threshold, rate.notes ), ) result = cur.fetchone() conn.commit() return result @router.get("/insurance-rates", response_model=List[schemas.InsuranceRate]) def get_insurance_rates( rate_year: int = None, calculation_target: str = None, prefecture: str = None ): """保険料率一覧を取得(年度・計算対象・都道府県でフィルター可能)""" with get_connection() as conn: with conn.cursor() as cur: conditions = [] params = [] if rate_year: conditions.append("rate_year = %s") params.append(rate_year) if calculation_target: conditions.append("calculation_target = %s") params.append(calculation_target) if prefecture: conditions.append("prefecture = %s") params.append(prefecture) where_clause = " AND ".join(conditions) if conditions else "1=1" cur.execute( f""" SELECT * FROM insurance_rates WHERE {where_clause} ORDER BY rate_year DESC, calculation_target, rate_type """, params ) return cur.fetchall() @router.delete("/insurance-rates/{rate_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_insurance_rate(rate_id: int): """保険料率を削除""" with get_connection() as conn: with conn.cursor() as cur: cur.execute("DELETE FROM insurance_rates WHERE rate_id = %s RETURNING rate_id", (rate_id,)) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="保険料率が見つかりません") conn.commit() @router.put("/insurance-rates/{rate_id}", response_model=schemas.InsuranceRate) def update_insurance_rate(rate_id: int, rate: schemas.InsuranceRateUpdate): """保険料率を更新""" print(f"\n=== 保険料率更新リクエスト ===") print(f"Rate ID: {rate_id}") print(f"受信データ: {rate}") update_data = rate.model_dump(exclude_unset=True) print(f"更新データ: {update_data}") 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()) + [rate_id] print(f"SQL SET句: {set_clause}") print(f"SQL値: {values}") with get_connection() as conn: with conn.cursor() as cur: sql = f"UPDATE insurance_rates SET {set_clause} WHERE rate_id = %s RETURNING *" print(f"実行SQL: {sql}") cur.execute(sql, values) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="保険料率が見つかりません") print(f"更新結果: {result}") conn.commit() print("コミット完了") return result # ======================================== # 所得税率表管理 # ======================================== @router.post("/income-tax", response_model=schemas.IncomeTaxTable, status_code=status.HTTP_201_CREATED) def create_income_tax_table(tax: schemas.IncomeTaxTableCreate): """所得税率表を作成""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO income_tax_table ( monthly_income_from, monthly_income_to, dependents_count, tax_amount, valid_from, valid_to ) VALUES (%s, %s, %s, %s, %s, %s) RETURNING * """, ( tax.monthly_income_from, tax.monthly_income_to, tax.dependents_count, tax.tax_amount, tax.valid_from, tax.valid_to ), ) result = cur.fetchone() conn.commit() return result @router.post("/income-tax/import") def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None): """ 所得税率表をCSV/Excelからインポート 税年度が指定されない場合、Excelファイルのタイトルから自動抽出(例:令和5年分 → 2023) 同じ年度の既存データは自動的に削除されます """ imported_count = 0 filename = file.filename.lower() extracted_year = tax_year with get_connection() as conn: with conn.cursor() as cur: # Excel file処理 if filename.endswith(('.xlsx', '.xls')): content = file.file.read() # .xls (旧形式) と .xlsx (新形式) で処理を分岐 if filename.endswith('.xls') and not filename.endswith('.xlsx'): # .xls形式 - xlrdを使用 workbook = xlrd.open_workbook(file_contents=content) sheet = workbook.sheet_by_index(0) # 年度を自動抽出(まずB1セルを優先的にチェック) if not extracted_year: import re # B1セル(0行1列)を最優先でチェック if sheet.nrows > 0 and sheet.ncols > 1: b1_value = sheet.cell_value(0, 1) if b1_value and isinstance(b1_value, str): print(f"B1セルの値: {b1_value}") if '令和' in b1_value and '年' in b1_value: match = re.search(r'令和(\d+)年', b1_value) if match: reiwa_year = int(match.group(1)) extracted_year = 2018 + reiwa_year print(f"B1セルから年度を抽出: 令和{reiwa_year}年 = {extracted_year}") elif '平成' in b1_value and '年' in b1_value: match = re.search(r'平成(\d+)年', b1_value) if match: heisei_year = int(match.group(1)) extracted_year = 1988 + heisei_year print(f"B1セルから年度を抽出: 平成{heisei_year}年 = {extracted_year}") # B1から取得できない場合は他のセルも検索 if not extracted_year: for row_idx in range(min(5, sheet.nrows)): for col_idx in range(sheet.ncols): cell_value = sheet.cell_value(row_idx, col_idx) if cell_value and isinstance(cell_value, str): if '令和' in cell_value and '年' in cell_value: match = re.search(r'令和(\d+)年', cell_value) if match: reiwa_year = int(match.group(1)) extracted_year = 2018 + reiwa_year print(f"Excelから年度を抽出: 令和{reiwa_year}年 = {extracted_year}") break elif '平成' in cell_value and '年' in cell_value: match = re.search(r'平成(\d+)年', cell_value) if match: heisei_year = int(match.group(1)) extracted_year = 1988 + heisei_year print(f"Excelから年度を抽出: 平成{heisei_year}年 = {extracted_year}") break if extracted_year: break # 年度が取得できない場合は現在年度を使用 if not extracted_year: extracted_year = date.today().year print(f"年度を自動設定: {extracted_year}") # 既存の同年度データを削除 cur.execute("DELETE FROM income_tax_table WHERE tax_year = %s", (extracted_year,)) deleted_count = cur.rowcount print(f"{extracted_year}年度の既存データを削除: {deleted_count}件") valid_from = date(extracted_year, 1, 1) valid_to = date(extracted_year, 12, 31) # ヘッダー行を探す(複数パターンに対応) header_row = None for row_idx in range(min(10, sheet.nrows)): for col_idx in range(sheet.ncols): cell_value = sheet.cell_value(row_idx, col_idx) if cell_value and ( '社会保険料控除後' in str(cell_value) or '社会保' in str(cell_value) or '給与等の金額' in str(cell_value) ): header_row = row_idx print(f"ヘッダー行を検出: 行{row_idx}, セル値: {cell_value}") break if header_row is not None: break if header_row is None: # デバッグ用:最初の10行を出力 print("デバッグ: 最初の10行を確認") for row_idx in range(min(10, sheet.nrows)): row_values = [str(sheet.cell_value(row_idx, c)) for c in range(min(5, sheet.ncols))] print(f" 行{row_idx}: {row_values}") raise HTTPException(status_code=400, detail="Excelのヘッダー行が見つかりません") # 扶養人数の列を特定 dependents_cols = [] header_row_idx = header_row + 2 if header_row_idx < sheet.nrows: for col_idx in range(sheet.ncols): cell_value = sheet.cell_value(header_row_idx, col_idx) if cell_value and '人' in str(cell_value): try: dep_count = int(str(cell_value).replace('人', '').strip()) dependents_cols.append((col_idx, dep_count)) print(f"扶養人数列を検出: 列{col_idx} = {dep_count}人") except Exception as e: print(f"扶養人数解析エラー: {cell_value}, エラー: {e}") except: pass # データ行を読み込む data_start_row = header_row + 3 for row_idx in range(data_start_row, sheet.nrows): first_cell = sheet.cell_value(row_idx, 0) if not first_cell: continue try: income_from = None income_to = None for col_idx in range(min(3, sheet.ncols)): cell_value = sheet.cell_value(row_idx, col_idx) if cell_value: val = str(cell_value).replace(',', '').replace('円', '').strip() try: if income_from is None: income_from = Decimal(val) elif income_to is None: income_to = Decimal(val) break except: pass if income_from is None: continue # 扶養人数ごとの税額を取得 for col_idx, dep_count in dependents_cols: if col_idx < sheet.ncols: tax_value = sheet.cell_value(row_idx, col_idx) if tax_value not in (None, ''): try: tax_amount = Decimal(str(tax_value).replace(',', '').replace('円', '').strip()) cur.execute( """ INSERT INTO income_tax_table ( monthly_income_from, monthly_income_to, dependents_count, tax_amount, tax_year, valid_from, valid_to ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, (income_from, income_to, dep_count, tax_amount, extracted_year, valid_from, valid_to) ) imported_count += 1 except Exception as e: print(f"税額変換エラー: {tax_value}, エラー: {e}") except Exception as e: print(f"行処理エラー: {e}") continue # .xlsファイルの処理完了 conn.commit() print(f"インポート完了: {imported_count}件のデータをインポートしました") return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year} else: # .xlsx形式 - openpyxlを使用 wb = openpyxl.load_workbook(io.BytesIO(content)) ws = wb.active # 年度を自動抽出(まずB1セルを優先的にチェック) if not extracted_year: import re # B1セルを最優先でチェック b1_cell = ws['B1'] if b1_cell.value: b1_value = str(b1_cell.value) print(f"B1セルの値: {b1_value}") if '令和' in b1_value and '年' in b1_value: match = re.search(r'令和(\d+)年', b1_value) if match: reiwa_year = int(match.group(1)) extracted_year = 2018 + reiwa_year print(f"B1セルから年度を抽出: 令和{reiwa_year}年 = {extracted_year}") elif '平成' in b1_value and '年' in b1_value: match = re.search(r'平成(\d+)年', b1_value) if match: heisei_year = int(match.group(1)) extracted_year = 1988 + heisei_year print(f"B1セルから年度を抽出: 平成{heisei_year}年 = {extracted_year}") # B1から取得できない場合は他のセルも検索 if not extracted_year: for row in ws.iter_rows(min_row=1, max_row=5): for cell in row: if cell.value: cell_text = str(cell.value) if '令和' in cell_text and '年' in cell_text: match = re.search(r'令和(\d+)年', cell_text) if match: reiwa_year = int(match.group(1)) extracted_year = 2018 + reiwa_year print(f"Excelから年度を抽出: 令和{reiwa_year}年 = {extracted_year}") break elif '平成' in cell_text and '年' in cell_text: match = re.search(r'平成(\d+)年', cell_text) if match: heisei_year = int(match.group(1)) extracted_year = 1988 + heisei_year print(f"Excelから年度を抽出: 平成{heisei_year}年 = {extracted_year}") break if extracted_year: break # 年度が取得できない場合は現在年度を使用 if not extracted_year: extracted_year = date.today().year print(f"年度を自動設定: {extracted_year}") # 既存の同年度データを削除 cur.execute("DELETE FROM income_tax_table WHERE tax_year = %s", (extracted_year,)) deleted_count = cur.rowcount print(f"{extracted_year}年度の既存データを削除: {deleted_count}件") # valid_from と valid_to を設定 valid_from = date(extracted_year, 1, 1) valid_to = date(extracted_year, 12, 31) # ヘッダー行を探す header_row = None for row_idx, row in enumerate(ws.iter_rows(min_row=1, max_row=10), start=1): for cell in row: if cell.value and '社会保険料控除後' in str(cell.value): header_row = row_idx break if header_row: break if not header_row: raise HTTPException(status_code=400, detail="Excelのヘッダー行が見つかりません") # 扶養人数の列を特定 dependents_cols = [] header_row_data = list(ws.iter_rows(min_row=header_row+2, max_row=header_row+2))[0] for col_idx, cell in enumerate(header_row_data, start=1): if cell.value and '人' in str(cell.value): try: dep_count = int(str(cell.value).replace('人', '').strip()) dependents_cols.append((col_idx, dep_count)) except: pass # データ行を読み込む data_start_row = header_row + 3 for row in ws.iter_rows(min_row=data_start_row): if not row[0].value: continue try: income_from = None income_to = None for i in range(min(3, len(row))): if row[i].value: val = str(row[i].value).replace(',', '').replace('円', '').strip() try: if income_from is None: income_from = Decimal(val) elif income_to is None: income_to = Decimal(val) break except: pass if income_from is None: continue # 扶養人数ごとの税額を取得 for col_idx, dep_count in dependents_cols: tax_value = row[col_idx - 1].value if col_idx - 1 < len(row) else None if tax_value is not None: try: tax_amount = Decimal(str(tax_value).replace(',', '').replace('円', '').strip()) cur.execute( """ INSERT INTO income_tax_table ( monthly_income_from, monthly_income_to, dependents_count, tax_amount, tax_year, valid_from, valid_to ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, (income_from, income_to, dep_count, tax_amount, extracted_year, valid_from, valid_to) ) imported_count += 1 except Exception as e: print(f"税額変換エラー: {tax_value}, エラー: {e}") pass except Exception as e: print(f"行処理エラー: {e}") continue # .xlsxファイルの処理完了 conn.commit() print(f"インポート完了: {imported_count}件のデータをインポートしました") return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year} # CSV file処理 else: if not extracted_year: extracted_year = date.today().year # 既存の同年度データを削除 cur.execute("DELETE FROM income_tax_table WHERE tax_year = %s", (extracted_year,)) deleted_count = cur.rowcount print(f"{extracted_year}年度の既存データを削除: {deleted_count}件") valid_from = date(extracted_year, 1, 1) valid_to = date(extracted_year, 12, 31) content = file.file.read().decode("utf-8-sig") csv_reader = csv.DictReader(io.StringIO(content)) for row in csv_reader: cur.execute( """ INSERT INTO income_tax_table ( monthly_income_from, monthly_income_to, dependents_count, tax_amount, tax_year, valid_from, valid_to ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( Decimal(row["月収From"]), Decimal(row["月収To"]), int(row["扶養人数"]), Decimal(row["税額"]), extracted_year, valid_from, valid_to ), ) imported_count += 1 conn.commit() return {"message": f"{extracted_year}年度のデータを{imported_count}件インポートしました(既存データを置き換え)", "year": extracted_year} @router.get("/income-tax/years") def get_income_tax_years(): """所得税率表の年度一覧を取得""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ SELECT DISTINCT tax_year FROM income_tax_table WHERE tax_year IS NOT NULL ORDER BY tax_year DESC """ ) years = [row['tax_year'] for row in cur.fetchall()] return {"years": years} @router.get("/income-tax", response_model=List[schemas.IncomeTaxTable]) def get_income_tax_table(target_date: date = None, dependents_count: int = None, tax_year: int = None): """所得税率表を取得""" with get_connection() as conn: with conn.cursor() as cur: # tax_yearが指定されている場合はそれを優先 if tax_year is not None: if dependents_count is not None: cur.execute( """ SELECT * FROM income_tax_table WHERE tax_year = %s AND dependents_count = %s ORDER BY monthly_income_from """, (tax_year, dependents_count) ) else: cur.execute( """ SELECT * FROM income_tax_table WHERE tax_year = %s ORDER BY dependents_count, monthly_income_from """, (tax_year,) ) else: # target_dateで検索 if target_date is None: target_date = date.today() if dependents_count is not None: cur.execute( """ SELECT * FROM income_tax_table WHERE valid_from <= %s AND (valid_to IS NULL OR valid_to >= %s) AND dependents_count = %s ORDER BY monthly_income_from """, (target_date, target_date, dependents_count) ) else: cur.execute( """ SELECT * FROM income_tax_table WHERE valid_from <= %s AND (valid_to IS NULL OR valid_to >= %s) ORDER BY dependents_count, monthly_income_from """, (target_date, target_date) ) return cur.fetchall() @router.get("/income-tax/calculate") def calculate_income_tax(monthly_income: Decimal, dependents_count: int = 0, target_date: date = None): """月収と扶養人数から所得税額を計算""" if target_date is None: target_date = date.today() 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 LIMIT 1 """, (target_date, target_date, dependents_count, monthly_income, monthly_income) ) result = cur.fetchone() if not result: return {"monthly_income": monthly_income, "dependents_count": dependents_count, "tax_amount": 0} return { "monthly_income": monthly_income, "dependents_count": dependents_count, "tax_amount": result["tax_amount"] } # ======================================== # 社会保険料上限設定管理 # ======================================== @router.post("/insurance-limits", response_model=schemas.InsuranceLimitSetting, status_code=status.HTTP_201_CREATED) def create_insurance_limit(limit: schemas.InsuranceLimitSettingCreate): """社会保険料上限設定を作成""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO insurance_limit_settings ( setting_type, limit_amount, valid_from, valid_to, notes ) VALUES (%s, %s, %s, %s, %s) RETURNING * """, (limit.setting_type, limit.limit_amount, limit.valid_from, limit.valid_to, limit.notes) ) result = cur.fetchone() conn.commit() return result @router.get("/insurance-limits", response_model=List[schemas.InsuranceLimitSetting]) def get_insurance_limits(target_date: date = None): """社会保険料上限設定一覧を取得""" if target_date is None: target_date = date.today() with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ SELECT * FROM insurance_limit_settings WHERE valid_from <= %s AND (valid_to IS NULL OR valid_to >= %s) ORDER BY setting_type, valid_from DESC """, (target_date, target_date) ) return cur.fetchall() @router.delete("/insurance-limits/{limit_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_insurance_limit(limit_id: int): """社会保険料上限設定を削除""" with get_connection() as conn: with conn.cursor() as cur: cur.execute("DELETE FROM insurance_limit_settings WHERE limit_id = %s RETURNING limit_id", (limit_id,)) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="上限設定が見つかりません") conn.commit() @router.put("/insurance-limits/{limit_id}", response_model=schemas.InsuranceLimitSetting) def update_insurance_limit(limit_id: int, limit: schemas.InsuranceLimitSettingUpdate): """社会保険料上限設定を更新""" update_data = limit.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()) + [limit_id] with get_connection() as conn: with conn.cursor() as cur: cur.execute( f"UPDATE insurance_limit_settings SET {set_clause} WHERE limit_id = %s RETURNING *", values ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="上限設定が見つかりません") conn.commit() return result # ======================================== # 子ども・子育て拠出金率管理 # ======================================== @router.post("/child-support-rates", response_model=schemas.ChildSupportContributionRate, status_code=status.HTTP_201_CREATED) def create_child_support_rate(rate: schemas.ChildSupportContributionRateCreate): """子ども・子育て拠出金率を作成""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO child_support_contribution_rates ( rate_year, income_threshold, contribution_rate, notes ) VALUES (%s, %s, %s, %s) RETURNING * """, (rate.rate_year, rate.income_threshold, rate.contribution_rate, rate.notes) ) result = cur.fetchone() conn.commit() return result @router.get("/child-support-rates", response_model=List[schemas.ChildSupportContributionRate]) def get_child_support_rates(): """子ども・子育て拠出金率一覧を取得""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ SELECT * FROM child_support_contribution_rates ORDER BY rate_year DESC """ ) return cur.fetchall() @router.delete("/child-support-rates/{contribution_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_child_support_rate(contribution_id: int): """子ども・子育て拠出金率を削除""" with get_connection() as conn: with conn.cursor() as cur: cur.execute("DELETE FROM child_support_contribution_rates WHERE contribution_id = %s RETURNING contribution_id", (contribution_id,)) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="拠出金率が見つかりません") conn.commit() @router.put("/child-support-rates/{contribution_id}", response_model=schemas.ChildSupportContributionRate) def update_child_support_rate(contribution_id: int, rate: schemas.ChildSupportContributionRateUpdate): """子ども・子育て拠出金率を更新""" update_data = rate.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()) + [contribution_id] with get_connection() as conn: with conn.cursor() as cur: cur.execute( f"UPDATE child_support_contribution_rates SET {set_clause} WHERE contribution_id = %s RETURNING *", values ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="拠出金率が見つかりません") conn.commit() return result # ======================================== # 扶養家族判定条件管理 # ======================================== @router.post("/dependent-rules", response_model=schemas.DependentRule, status_code=status.HTTP_201_CREATED) def create_dependent_rule(rule: schemas.DependentRuleCreate): """扶養家族判定条件を作成""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO dependent_rules ( rule_type, rule_value, valid_from, valid_to, description ) VALUES (%s, %s, %s, %s, %s) RETURNING * """, (rule.rule_type, rule.rule_value, rule.valid_from, rule.valid_to, rule.description) ) result = cur.fetchone() conn.commit() return result @router.get("/dependent-rules", response_model=List[schemas.DependentRule]) def get_dependent_rules(target_date: date = None): """扶養家族判定条件一覧を取得""" if target_date is None: target_date = date.today() with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ SELECT * FROM dependent_rules WHERE valid_from <= %s AND (valid_to IS NULL OR valid_to >= %s) ORDER BY rule_type, valid_from DESC """, (target_date, target_date) ) return cur.fetchall()