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

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,3 @@
"""
給与管理モジュール (Payroll Module)
"""

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
}

View File

@@ -0,0 +1,3 @@
"""
月次給与計算 (Monthly Payroll Calculation)
"""

View 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()

View 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

View 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
}

View File

@@ -0,0 +1,3 @@
"""
従業員管理 (Employee Management)
"""

View File

@@ -0,0 +1,196 @@
"""
従業員管理API (Employee Management API)
"""
from fastapi import APIRouter, HTTPException, status
from typing import List
from . import schemas
from ...core.database import get_connection
router = APIRouter(prefix="/payroll/employees", tags=["Payroll - Employees"])
@router.post("/", response_model=schemas.Employee, status_code=status.HTTP_201_CREATED)
def create_employee(employee: schemas.EmployeeCreate):
"""従業員を作成"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO employees (
employee_code, name, name_kana, birth_date, gender,
email, phone, postal_code, address, hire_date, termination_date, is_active,
bank_name, bank_branch, bank_account_number, bank_account_type, notes
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
employee.employee_code, employee.name, employee.name_kana,
employee.birth_date, employee.gender,
employee.email, employee.phone, employee.postal_code, employee.address,
employee.hire_date, employee.termination_date, employee.is_active,
employee.bank_name, employee.bank_branch,
employee.bank_account_number, employee.bank_account_type, employee.notes
),
)
result = cur.fetchone()
conn.commit()
return result
@router.get("/", response_model=List[schemas.Employee])
def get_employees(is_active: bool = None):
"""従業員一覧を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
if is_active is not None:
cur.execute(
"SELECT * FROM employees WHERE is_active = %s ORDER BY employee_code",
(is_active,)
)
else:
cur.execute("SELECT * FROM employees ORDER BY employee_code")
return cur.fetchall()
@router.get("/{employee_id}", response_model=schemas.EmployeeWithDependents)
def get_employee(employee_id: int):
"""従業員の詳細を取得(扶養家族を含む)"""
with get_connection() as conn:
with conn.cursor() as cur:
# 従業員情報取得
cur.execute("SELECT * FROM employees WHERE employee_id = %s", (employee_id,))
employee = cur.fetchone()
if not employee:
raise HTTPException(status_code=404, detail="従業員が見つかりません")
# 扶養家族情報取得
cur.execute(
"""
SELECT * FROM dependents
WHERE employee_id = %s
AND (valid_to IS NULL OR valid_to >= CURRENT_DATE)
ORDER BY valid_from
""",
(employee_id,)
)
dependents = cur.fetchall()
return {**employee, "dependents": dependents}
@router.put("/{employee_id}", response_model=schemas.Employee)
def update_employee(employee_id: int, employee: schemas.EmployeeUpdate):
"""従業員情報を更新"""
update_data = employee.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()) + [employee_id]
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
f"UPDATE employees SET {set_clause} WHERE employee_id = %s RETURNING *",
values,
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="従業員が見つかりません")
conn.commit()
return result
@router.delete("/{employee_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_employee(employee_id: int):
"""従業員を削除"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM employees WHERE employee_id = %s", (employee_id,))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="従業員が見つかりません")
conn.commit()
# ========================================
# 扶養家族管理
# ========================================
@router.post("/{employee_id}/dependents", response_model=schemas.Dependent, status_code=status.HTTP_201_CREATED)
def create_dependent(employee_id: int, dependent: schemas.DependentCreate):
"""扶養家族を追加"""
with get_connection() as conn:
with conn.cursor() as cur:
# 従業員の存在確認
cur.execute("SELECT employee_id FROM employees WHERE employee_id = %s", (employee_id,))
if not cur.fetchone():
raise HTTPException(status_code=404, detail="従業員が見つかりません")
cur.execute(
"""
INSERT INTO dependents (
employee_id, name, relationship, birth_date,
is_spouse, is_disabled, income_amount, valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
employee_id, dependent.name, dependent.relationship, dependent.birth_date,
dependent.is_spouse, dependent.is_disabled, dependent.income_amount,
dependent.valid_from, dependent.valid_to
),
)
result = cur.fetchone()
conn.commit()
return result
@router.get("/{employee_id}/dependents", response_model=List[schemas.Dependent])
def get_dependents(employee_id: int):
"""従業員の扶養家族一覧を取得"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT * FROM dependents
WHERE employee_id = %s
ORDER BY valid_from DESC
""",
(employee_id,)
)
return cur.fetchall()
@router.put("/dependents/{dependent_id}", response_model=schemas.Dependent)
def update_dependent(dependent_id: int, dependent: schemas.DependentUpdate):
"""扶養家族情報を更新"""
update_data = dependent.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()) + [dependent_id]
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
f"UPDATE dependents SET {set_clause} WHERE dependent_id = %s RETURNING *",
values,
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="扶養家族が見つかりません")
conn.commit()
return result
@router.delete("/dependents/{dependent_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_dependent(dependent_id: int):
"""扶養家族を削除"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM dependents WHERE dependent_id = %s", (dependent_id,))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="扶養家族が見つかりません")
conn.commit()

View File

@@ -0,0 +1,109 @@
"""
従業員管理用のスキーマ定義 (Employee Schemas)
"""
from pydantic import BaseModel, EmailStr
from datetime import date, datetime
from typing import Optional
from decimal import Decimal
class DependentBase(BaseModel):
"""扶養家族の基本情報"""
name: str
relationship: str # 配偶者/子/親/その他
birth_date: Optional[date] = None
is_spouse: bool = False
is_disabled: bool = False
income_amount: Decimal = Decimal("0")
valid_from: date
valid_to: Optional[date] = None
class DependentCreate(DependentBase):
"""扶養家族の作成"""
pass
class DependentUpdate(BaseModel):
"""扶養家族の更新"""
name: Optional[str] = None
relationship: Optional[str] = None
birth_date: Optional[date] = None
is_spouse: Optional[bool] = None
is_disabled: Optional[bool] = None
income_amount: Optional[Decimal] = None
valid_from: Optional[date] = None
valid_to: Optional[date] = None
class Dependent(DependentBase):
"""扶養家族の情報"""
dependent_id: int
employee_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class EmployeeBase(BaseModel):
"""従業員の基本情報"""
employee_code: str
name: str
name_kana: Optional[str] = None
birth_date: Optional[date] = None
gender: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
postal_code: Optional[str] = None
address: Optional[str] = None
hire_date: date
termination_date: Optional[date] = None
is_active: bool = True
bank_name: Optional[str] = None
bank_branch: Optional[str] = None
bank_account_number: Optional[str] = None
bank_account_type: Optional[str] = None # 普通/当座
notes: Optional[str] = None
class EmployeeCreate(EmployeeBase):
"""従業員の作成"""
pass
class EmployeeUpdate(BaseModel):
"""従業員の更新"""
employee_code: Optional[str] = None
name: Optional[str] = None
name_kana: Optional[str] = None
birth_date: Optional[date] = None
gender: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
postal_code: Optional[str] = None
address: Optional[str] = None
hire_date: Optional[date] = None
termination_date: Optional[date] = None
is_active: Optional[bool] = None
bank_name: Optional[str] = None
bank_branch: Optional[str] = None
bank_account_number: Optional[str] = None
bank_account_type: Optional[str] = None
notes: Optional[str] = None
class Employee(EmployeeBase):
"""従業員の情報"""
employee_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class EmployeeWithDependents(Employee):
"""扶養家族を含む従業員情報"""
dependents: list[Dependent] = []

View File

@@ -0,0 +1,3 @@
"""
給与設定・税率・保険料管理 (Settings Management)
"""

View File

@@ -0,0 +1,869 @@
"""
給与設定・税率・保険料管理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}件インポートしました"}
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}件インポートしました"}
# 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", response_model=List[schemas.IncomeTaxTable])
def get_income_tax_table(target_date: date = None, dependents_count: int = None):
"""所得税率表を取得"""
if target_date is None:
target_date = date.today()
with get_connection() as conn:
with conn.cursor() as cur:
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()

View File

@@ -0,0 +1,220 @@
"""
給与設定・税率・保険料のスキーマ定義 (Settings Schemas)
"""
from pydantic import BaseModel
from datetime import date, datetime
from typing import Optional
from decimal import Decimal
class SalarySettingBase(BaseModel):
"""給与基本設定"""
employee_id: int
base_salary: Decimal = Decimal("0")
hourly_rate: Optional[Decimal] = None
employment_type: str # 正社員/契約社員/パート/アルバイト
payment_type: str # 月給/時給/日給
commute_allowance: Decimal = Decimal("0")
other_allowance: Decimal = Decimal("0")
valid_from: date
valid_to: Optional[date] = None
class SalarySettingCreate(SalarySettingBase):
"""給与設定の作成"""
pass
class SalarySettingUpdate(BaseModel):
"""給与設定の更新"""
base_salary: Optional[Decimal] = None
hourly_rate: Optional[Decimal] = None
employment_type: Optional[str] = None
payment_type: Optional[str] = None
commute_allowance: Optional[Decimal] = None
other_allowance: Optional[Decimal] = None
valid_from: Optional[date] = None
valid_to: Optional[date] = None
class SalarySetting(SalarySettingBase):
"""給与設定の情報"""
setting_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class InsuranceRateBase(BaseModel):
"""社会保険料率"""
rate_year: int # 適用年度(例: 2026
rate_type: str # 健康保険/介護保険/厚生年金/雇用保険/労災保険
calculation_target: str # 給与/賞与
prefecture: str = "東京都" # 事業所所在地
employee_rate: Decimal
employer_rate: Decimal
care_insurance_age_threshold: Optional[int] = None # 介護保険適用年齢下限
notes: Optional[str] = None
class InsuranceRateCreate(InsuranceRateBase):
"""保険料率の作成"""
pass
class InsuranceRateUpdate(BaseModel):
"""保険料率の更新"""
rate_year: Optional[int] = None
rate_type: Optional[str] = None
calculation_target: Optional[str] = None
prefecture: Optional[str] = None
employee_rate: Optional[Decimal] = None
employer_rate: Optional[Decimal] = None
care_insurance_age_threshold: Optional[int] = None
notes: Optional[str] = None
class InsuranceRate(InsuranceRateBase):
"""保険料率の情報"""
rate_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class IncomeTaxTableBase(BaseModel):
"""所得税率表"""
monthly_income_from: Decimal
monthly_income_to: Decimal
dependents_count: int = 0
tax_amount: Decimal
valid_from: date
valid_to: Optional[date] = None
class IncomeTaxTableCreate(IncomeTaxTableBase):
"""所得税率表の作成"""
pass
class IncomeTaxTable(IncomeTaxTableBase):
"""所得税率表の情報"""
tax_id: int
created_at: datetime
class Config:
from_attributes = True
# ========================================
# 子ども・子育て拠出金率
# ========================================
class ChildSupportContributionRateBase(BaseModel):
"""子ども・子育て拠出金率"""
rate_year: int # 適用年度
income_threshold: Decimal # 所得基準額(標準報酬月額)
contribution_rate: Decimal # 拠出金率(事業主負担のみ)
notes: Optional[str] = None
class ChildSupportContributionRateCreate(ChildSupportContributionRateBase):
"""子ども・子育て拠出金率の作成"""
pass
class ChildSupportContributionRateUpdate(BaseModel):
"""子ども・子育て拠出金率の更新"""
income_threshold: Optional[Decimal] = None
contribution_rate: Optional[Decimal] = None
notes: Optional[str] = None
class ChildSupportContributionRate(ChildSupportContributionRateBase):
"""子ども・子育て拠出金率の情報"""
contribution_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ========================================
# 社会保険料上限設定
# ========================================
class InsuranceLimitSettingBase(BaseModel):
"""社会保険料上限設定"""
setting_type: str # 健康保険標準報酬月額上限/厚生年金標準報酬月額上限/通勤手当非課税限度額
limit_amount: Decimal
valid_from: date
valid_to: Optional[date] = None
notes: Optional[str] = None
class InsuranceLimitSettingCreate(InsuranceLimitSettingBase):
"""上限設定の作成"""
pass
class InsuranceLimitSettingUpdate(BaseModel):
"""上限設定の更新"""
limit_amount: Optional[Decimal] = None
valid_from: Optional[date] = None
valid_to: Optional[date] = None
notes: Optional[str] = None
class InsuranceLimitSetting(InsuranceLimitSettingBase):
"""上限設定の情報"""
limit_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ========================================
# 扶養家族判定条件
# ========================================
class DependentRuleBase(BaseModel):
"""扶養家族判定条件"""
rule_type: str # 所得限度額/年齢条件/同一生計
rule_value: str
valid_from: date
valid_to: Optional[date] = None
description: Optional[str] = None
class DependentRuleCreate(DependentRuleBase):
"""判定条件の作成"""
pass
class DependentRule(DependentRuleBase):
"""判定条件の情報"""
rule_id: int
created_at: datetime
class Config:
from_attributes = True
"""所得税率表の情報"""
tax_id: int
created_at: datetime
class Config:
from_attributes = True
class IncomeTaxImport(BaseModel):
"""所得税率表のインポート用"""
data: list[IncomeTaxTableCreate]
valid_from: date