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

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,43 @@
import os
import psycopg2
from dotenv import load_dotenv
load_dotenv()
conn = psycopg2.connect(
host=os.getenv('DB_HOST'),
port=os.getenv('DB_PORT'),
dbname=os.getenv('DB_NAME'),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD')
)
cur = conn.cursor()
# 年度フィールドを追加
try:
cur.execute("ALTER TABLE income_tax_table ADD COLUMN tax_year INTEGER")
print("✓ tax_year カラムを追加")
except Exception as e:
print(f"カラム追加スキップ(既に存在する可能性): {e}")
conn.rollback()
# 既存データに年度を設定
try:
cur.execute("UPDATE income_tax_table SET tax_year = EXTRACT(YEAR FROM valid_from) WHERE tax_year IS NULL")
print("✓ 既存データに年度を設定")
except Exception as e:
print(f"エラー: {e}")
# インデックスを追加
try:
cur.execute("CREATE INDEX IF NOT EXISTS idx_income_tax_year ON income_tax_table(tax_year)")
print("✓ インデックスを追加")
except Exception as e:
print(f"インデックス追加エラー: {e}")
conn.commit()
cur.close()
conn.close()
print("\n完了!")

View File

@@ -81,14 +81,40 @@ app.include_router(year_locks.router)
from app.routers import cash from app.routers import cash
app.include_router(cash.router) app.include_router(cash.router)
# ─────────────────────────
# 給与モジュール (Payroll Module)
# ─────────────────────────
from app.payroll.employees.router import router as payroll_employees_router
app.include_router(payroll_employees_router)
from app.payroll.settings.router import router as payroll_settings_router
app.include_router(payroll_settings_router)
from app.payroll.calculation.router import router as payroll_calculation_router
app.include_router(payroll_calculation_router)
from app.payroll.bonus.router import router as payroll_bonus_router
app.include_router(payroll_bonus_router)
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import os
app.mount( # 静的ファイル配信(コメントアウト - ディレクトリが存在しない)
"/static", # app.mount(
StaticFiles(directory="app/static", html=True), # "/static",
name="static", # StaticFiles(directory="app/static", html=True),
) # name="static",
# )
# フロントエンドファイル配信(コメントアウト - file://プロトコルで直接アクセスするため不要)
# frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
# if os.path.exists(frontend_path):
# app.mount(
# "/",
# StaticFiles(directory=frontend_path, html=True),
# name="frontend",
# )

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

View File

@@ -0,0 +1,434 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.filters {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.filters label {
margin-right: 15px;
}
.payroll-list {
margin-top: 20px;
}
.payroll-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.payroll-item:hover {
background: #f9f9f9;
}
.status-badge {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.status-draft {
background: #ffc107;
color: #000;
}
.status-calculated {
background: #17a2b8;
color: #fff;
}
.status-approved {
background: #28a745;
color: #fff;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.payroll-detail {
margin-top: 20px;
}
.detail-section {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #ddd;
}
.detail-row:last-child {
border-bottom: none;
}
.total-row {
font-weight: bold;
font-size: 1.2em;
color: #007bff;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 月次給与計算</h1>
<div class="filters">
<label>
対象年月:
<input type="number" id="filterYear" style="width: 100px;" placeholder="年">
</label>
<label>
<input type="number" id="filterMonth" style="width: 80px;" min="1" max="12" placeholder="月">
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">新規計算</button>
</div>
<div id="payrollList" class="payroll-list"></div>
<!-- 給与計算フォーム(モーダル風) -->
<div id="calculateModal" style="display:none; position:fixed; top:50px; left:50%; transform:translateX(-50%); width:800px; max-height:90vh; overflow-y:auto; background:white; padding:30px; border:2px solid #007bff; border-radius:10px; box-shadow:0 4px 8px rgba(0,0,0,0.2); z-index:1000;">
<h2>給与計算</h2>
<form id="calculateForm" onsubmit="calculatePayroll(event)">
<div class="form-group">
<label>従業員 *</label>
<select name="employee_id" id="employeeSelect" required>
<option value="">選択してください</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>対象年 *</label>
<input type="number" name="payroll_year" required>
</div>
<div class="form-group">
<label>対象月 *</label>
<input type="number" name="payroll_month" min="1" max="12" required>
</div>
<div class="form-group">
<label>支給日 *</label>
<input type="date" name="payment_date" required>
</div>
</div>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="form-row">
<div class="form-group">
<label>出勤日数</label>
<input type="number" name="working_days" step="0.5" value="0">
</div>
<div class="form-group">
<label>勤務時間</label>
<input type="number" name="working_hours" step="0.5" value="0">
</div>
<div class="form-group">
<label>残業時間</label>
<input type="number" name="overtime_hours" step="0.5" value="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>休日出勤時間</label>
<input type="number" name="holiday_hours" step="0.5" value="0">
</div>
<div class="form-group">
<label>遅刻時間</label>
<input type="number" name="late_hours" step="0.5" value="0">
</div>
<div class="form-group">
<label>欠勤日数</label>
<input type="number" name="absent_days" step="0.5" value="0">
</div>
</div>
</div>
<div class="detail-section">
<h3>その他</h3>
<div class="form-row">
<div class="form-group">
<label>その他手当</label>
<input type="number" name="other_allowance" step="0.01" value="0">
</div>
<div class="form-group">
<label>住民税</label>
<input type="number" name="resident_tax" step="0.01" value="0">
</div>
<div class="form-group">
<label>その他控除</label>
<input type="number" name="other_deduction" step="0.01" value="0">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">計算実行</button>
<button type="button" class="btn btn-secondary" onclick="closeCalculateForm()">キャンセル</button>
</form>
</div>
<!-- 給与明細表示 -->
<div id="payrollDetail" class="payroll-detail" style="display:none;"></div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
async function loadEmployees() {
const response = await fetch(`${API_BASE}/payroll/employees/?is_active=true`);
const employees = await response.json();
const select = document.getElementById('employeeSelect');
select.innerHTML = '<option value="">選択してください</option>' +
employees.map(emp => `<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`).join('');
}
async function loadPayrolls() {
const year = document.getElementById('filterYear').value;
const month = document.getElementById('filterMonth').value;
const params = new URLSearchParams();
if (year) params.append('payroll_year', year);
if (month) params.append('payroll_month', month);
try {
const response = await fetch(`${API_BASE}/payroll/calculation/?${params}`);
const payrolls = await response.json();
const html = payrolls.map(p => `
<div class="payroll-item" onclick="viewPayroll(${p.payroll_id})">
<div>
<strong>${p.payroll_year}${p.payroll_month}月</strong>
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
</div>
<div>
従業員ID: ${p.employee_id} | 支給日: ${p.payment_date}
</div>
<div>
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})
</div>
</div>
`).join('');
document.getElementById('payrollList').innerHTML = html || '<p>給与データがありません</p>';
} catch (error) {
alert('給与一覧の取得に失敗しました');
console.error(error);
}
}
function getStatusLabel(status) {
const labels = {
'draft': '下書き',
'calculated': '計算済み',
'approved': '承認済み',
'paid': '支払済み'
};
return labels[status] || status;
}
function showCalculateForm() {
document.getElementById('calculateModal').style.display = 'block';
loadEmployees();
// デフォルト値設定
const now = new Date();
document.querySelector('[name="payroll_year"]').value = now.getFullYear();
document.querySelector('[name="payroll_month"]').value = now.getMonth() + 1;
}
function closeCalculateForm() {
document.getElementById('calculateModal').style.display = 'none';
document.getElementById('calculateForm').reset();
}
async function calculatePayroll(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
data[key] = isNaN(value) || value === '' ? value : Number(value);
});
try {
const response = await fetch(`${API_BASE}/payroll/calculation/calculate`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
alert('給与計算が完了しました');
closeCalculateForm();
viewPayroll(result.payroll_id);
} else {
const error = await response.json();
alert('計算に失敗しました: ' + error.detail);
}
} catch (error) {
alert('計算に失敗しました');
console.error(error);
}
}
async function viewPayroll(payrollId) {
try {
const response = await fetch(`${API_BASE}/payroll/calculation/${payrollId}`);
const payroll = await response.json();
const html = `
<h2>${payroll.payroll_year}${payroll.payroll_month}月 給与明細</h2>
<p>従業員ID: ${payroll.employee_id} | 支給日: ${payroll.payment_date}</p>
<p>ステータス: <span class="status-badge status-${payroll.status}">${getStatusLabel(payroll.status)}</span></p>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="detail-row"><span>出勤日数:</span><span>${payroll.working_days}日</span></div>
<div class="detail-row"><span>勤務時間:</span><span>${payroll.working_hours}時間</span></div>
<div class="detail-row"><span>残業時間:</span><span>${payroll.overtime_hours}時間</span></div>
<div class="detail-row"><span>休日出勤:</span><span>${payroll.holiday_hours}時間</span></div>
<div class="detail-row"><span>欠勤日数:</span><span>${payroll.absent_days}日</span></div>
</div>
<div class="detail-section">
<h3>支給項目</h3>
<div class="detail-row"><span>基本給:</span><span>¥${Number(payroll.base_salary).toLocaleString()}</span></div>
<div class="detail-row"><span>残業手当:</span><span>¥${Number(payroll.overtime_pay).toLocaleString()}</span></div>
<div class="detail-row"><span>休日手当:</span><span>¥${Number(payroll.holiday_pay).toLocaleString()}</span></div>
<div class="detail-row"><span>通勤手当:</span><span>¥${Number(payroll.commute_allowance).toLocaleString()}</span></div>
<div class="detail-row"><span>その他手当:</span><span>¥${Number(payroll.other_allowance).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(payroll.total_payment).toLocaleString()}</span></div>
</div>
<div class="detail-section">
<h3>控除項目</h3>
<div class="detail-row"><span>健康保険:</span><span>¥${Number(payroll.health_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(payroll.pension_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(payroll.employment_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(payroll.income_tax).toLocaleString()}</span></div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(payroll.resident_tax).toLocaleString()}</span></div>
<div class="detail-row"><span>その他控除:</span><span>¥${Number(payroll.other_deduction).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(payroll.total_deduction).toLocaleString()}</span></div>
</div>
<div class="detail-section" style="background:#e7f3ff;">
<div class="detail-row total-row" style="font-size:1.5em;">
<span>差引支給額:</span>
<span>¥${Number(payroll.net_payment).toLocaleString()}</span>
</div>
</div>
${payroll.status === 'calculated' ? `
<button class="btn btn-success" onclick="approvePayroll(${payroll.payroll_id})">承認</button>
<button class="btn btn-primary" onclick="recalculatePayroll(${payroll.payroll_id})">再計算</button>
` : ''}
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
`;
document.getElementById('payrollDetail').innerHTML = html;
document.getElementById('payrollDetail').style.display = 'block';
document.getElementById('payrollDetail').scrollIntoView({ behavior: 'smooth' });
} catch (error) {
alert('給与明細の取得に失敗しました');
console.error(error);
}
}
async function approvePayroll(payrollId) {
if (!confirm('この給与を承認しますか?')) return;
try {
const response = await fetch(`${API_BASE}/payroll/calculation/${payrollId}/approve`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ approved_by: 'admin' })
});
if (response.ok) {
alert('承認しました');
viewPayroll(payrollId);
loadPayrolls();
} else {
const error = await response.json();
alert('承認に失敗しました: ' + error.detail);
}
} catch (error) {
alert('承認に失敗しました');
console.error(error);
}
}
async function recalculatePayroll(payrollId) {
if (!confirm('給与を再計算しますか?')) return;
try {
const response = await fetch(`${API_BASE}/payroll/calculation/${payrollId}/recalculate`, {
method: 'POST'
});
if (response.ok) {
alert('再計算が完了しました');
viewPayroll(payrollId);
} else {
const error = await response.json();
alert('再計算に失敗しました: ' + error.detail);
}
} catch (error) {
alert('再計算に失敗しました');
console.error(error);
}
}
// 初期化
const now = new Date();
document.getElementById('filterYear').value = now.getFullYear();
document.getElementById('filterMonth').value = now.getMonth() + 1;
loadPayrolls();
</script>
</body>
</html>

View File

@@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理 - 従業員管理</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.nav-tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: #f0f0f0;
border-radius: 5px 5px 0 0;
}
.nav-tab.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.employee-list {
margin-top: 20px;
}
.employee-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.employee-item:hover {
background: #f9f9f9;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.dependent-list {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.dependent-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
background: white;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 従業員管理</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('list')">従業員一覧</button>
<button class="nav-tab" onclick="showTab('create')">新規登録</button>
<button class="nav-tab" onclick="showTab('detail')" id="detailTab" style="display:none;">従業員詳細</button>
</div>
<!-- 従業員一覧タブ -->
<div id="tab-list" class="tab-content active">
<div class="filters">
<label>
<input type="checkbox" id="filterActive" checked onchange="loadEmployees()">
在職中のみ表示
</label>
</div>
<div id="employeeList" class="employee-list"></div>
</div>
<!-- 新規登録タブ -->
<div id="tab-create" class="tab-content">
<h2>従業員の新規登録</h2>
<form id="createForm" onsubmit="createEmployee(event)">
<div class="form-group">
<label>従業員コード *</label>
<input type="text" name="employee_code" required>
</div>
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>フリガナ</label>
<input type="text" name="name_kana">
</div>
<div class="form-group">
<label>メールアドレス</label>
<input type="email" name="email">
</div>
<div class="form-group">
<label>電話番号</label>
<input type="tel" name="phone">
</div>
<div class="form-group">
<label>入社日 *</label>
<input type="date" name="hire_date" required>
</div>
<div class="form-group">
<label>銀行名</label>
<input type="text" name="bank_name">
</div>
<div class="form-group">
<label>支店名</label>
<input type="text" name="bank_branch">
</div>
<div class="form-group">
<label>口座番号</label>
<input type="text" name="bank_account_number">
</div>
<div class="form-group">
<label>口座種別</label>
<select name="bank_account_type">
<option value="">選択してください</option>
<option value="普通">普通</option>
<option value="当座">当座</option>
</select>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button type="button" class="btn btn-secondary" onclick="showTab('list')">キャンセル</button>
</form>
</div>
<!-- 従業員詳細タブ -->
<div id="tab-detail" class="tab-content">
<div id="employeeDetail"></div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
let currentEmployee = null;
function showTab(tabName) {
document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
if (tabName === 'list') {
document.querySelector('.nav-tab').classList.add('active');
document.getElementById('tab-list').classList.add('active');
loadEmployees();
} else if (tabName === 'create') {
document.querySelectorAll('.nav-tab')[1].classList.add('active');
document.getElementById('tab-create').classList.add('active');
} else if (tabName === 'detail') {
document.getElementById('detailTab').classList.add('active');
document.getElementById('tab-detail').classList.add('active');
}
}
async function loadEmployees() {
const isActive = document.getElementById('filterActive').checked;
const url = `${API_BASE}/payroll/employees/${isActive ? '?is_active=true' : ''}`;
try {
const response = await fetch(url);
const employees = await response.json();
const html = employees.map(emp => `
<div class="employee-item" onclick="viewEmployee(${emp.employee_id})">
<strong>${emp.employee_code}</strong> - ${emp.name}
${emp.name_kana ? `(${emp.name_kana})` : ''}
<br>
<small>入社日: ${emp.hire_date} ${emp.termination_date ? ` | 退職日: ${emp.termination_date}` : ''}</small>
</div>
`).join('');
document.getElementById('employeeList').innerHTML = html || '<p>従業員が登録されていません</p>';
} catch (error) {
alert('従業員一覧の取得に失敗しました');
console.error(error);
}
}
async function createEmployee(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 空の値を除外
Object.keys(data).forEach(key => {
if (data[key] === '') {
delete data[key];
}
});
try {
const response = await fetch(`${API_BASE}/payroll/employees/`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('従業員を登録しました');
form.reset();
showTab('list');
} else {
const error = await response.json();
alert('登録に失敗しました: ' + error.detail);
}
} catch (error) {
alert('登録に失敗しました');
console.error(error);
}
}
async function viewEmployee(employeeId) {
try {
const response = await fetch(`${API_BASE}/payroll/employees/${employeeId}`);
const employee = await response.json();
currentEmployee = employee;
const html = `
<h2>${employee.name} (${employee.employee_code})</h2>
<p><strong>フリガナ:</strong> ${employee.name_kana || '-'}</p>
<p><strong>メール:</strong> ${employee.email || '-'}</p>
<p><strong>電話:</strong> ${employee.phone || '-'}</p>
<p><strong>入社日:</strong> ${employee.hire_date}</p>
<p><strong>退職日:</strong> ${employee.termination_date || '-'}</p>
<p><strong>銀行:</strong> ${employee.bank_name || '-'} ${employee.bank_branch || ''}</p>
<p><strong>口座:</strong> ${employee.bank_account_type || ''} ${employee.bank_account_number || '-'}</p>
<p><strong>備考:</strong> ${employee.notes || '-'}</p>
<div class="dependent-list">
<h3>扶養家族</h3>
${employee.dependents.map(dep => `
<div class="dependent-item">
<strong>${dep.name}</strong> (${dep.relationship})
${dep.birth_date ? ` - 生年月日: ${dep.birth_date}` : ''}
${dep.is_spouse ? ' <span style="color:blue;">[配偶者]</span>' : ''}
${dep.is_disabled ? ' <span style="color:red;">[障害者]</span>' : ''}
<br><small>有効期間: ${dep.valid_from} ${dep.valid_to || '現在'}</small>
</div>
`).join('') || '<p>扶養家族が登録されていません</p>'}
</div>
<button class="btn btn-secondary" onclick="showTab('list')">戻る</button>
`;
document.getElementById('employeeDetail').innerHTML = html;
document.getElementById('detailTab').style.display = 'block';
showTab('detail');
} catch (error) {
alert('従業員情報の取得に失敗しました');
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理 - 給与設定</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.employee-select {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table th, table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
table th {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 給与設定</h1>
<div class="employee-select">
<label><strong>従業員を選択:</strong></label>
<select id="employeeSelect" onchange="loadSalarySettings()">
<option value="">従業員を選択してください</option>
</select>
</div>
<div id="settingsContent" style="display:none;">
<h2>給与設定履歴</h2>
<button class="btn btn-primary" onclick="showAddForm()">新規設定追加</button>
<div id="settingsList"></div>
<div id="addForm" style="display:none; margin-top:20px; padding:20px; border:2px solid #007bff; border-radius:5px;">
<h3>給与設定の追加</h3>
<form onsubmit="saveSalarySetting(event)">
<div class="form-group">
<label>基本給 (円) *</label>
<input type="number" name="base_salary" step="0.01" required>
</div>
<div class="form-group">
<label>時給 (円)</label>
<input type="number" name="hourly_rate" step="0.01">
<small>時給制の場合に入力してください</small>
</div>
<div class="form-group">
<label>雇用形態 *</label>
<select name="employment_type" required>
<option value="">選択してください</option>
<option value="正社員">正社員</option>
<option value="契約社員">契約社員</option>
<option value="パート">パート</option>
<option value="アルバイト">アルバイト</option>
</select>
</div>
<div class="form-group">
<label>支給形態 *</label>
<select name="payment_type" required>
<option value="">選択してください</option>
<option value="月給">月給</option>
<option value="時給">時給</option>
<option value="日給">日給</option>
</select>
</div>
<div class="form-group">
<label>通勤手当 (円)</label>
<input type="number" name="commute_allowance" step="0.01" value="0">
</div>
<div class="form-group">
<label>その他手当 (円)</label>
<input type="number" name="other_allowance" step="0.01" value="0">
</div>
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" required>
</div>
<div class="form-group">
<label>適用終了日</label>
<input type="date" name="valid_to">
<small>通常は空欄のままにしてください</small>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-secondary" onclick="hideAddForm()">キャンセル</button>
</form>
</div>
<div id="currentSetting" style="margin-top:30px; padding:20px; background:#e7f3ff; border-radius:5px;">
<h3>現在の給与設定</h3>
<div id="currentSettingContent"></div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
let currentEmployeeId = null;
async function loadEmployees() {
try {
const response = await fetch(`${API_BASE}/payroll/employees/?is_active=true`);
const employees = await response.json();
const select = document.getElementById('employeeSelect');
select.innerHTML = '<option value="">従業員を選択してください</option>' +
employees.map(emp => `<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`).join('');
} catch (error) {
alert('従業員一覧の取得に失敗しました');
console.error(error);
}
}
async function loadSalarySettings() {
const employeeId = document.getElementById('employeeSelect').value;
if (!employeeId) {
document.getElementById('settingsContent').style.display = 'none';
return;
}
currentEmployeeId = employeeId;
document.getElementById('settingsContent').style.display = 'block';
try {
// 給与設定履歴を取得
const response = await fetch(`${API_BASE}/payroll/settings/salary/${employeeId}`);
const settings = await response.json();
const html = `
<table>
<thead>
<tr>
<th>適用期間</th>
<th>基本給</th>
<th>時給</th>
<th>雇用形態</th>
<th>支給形態</th>
<th>通勤手当</th>
<th>その他手当</th>
</tr>
</thead>
<tbody>
${settings.map(s => `
<tr>
<td>${s.valid_from} ${s.valid_to || '現在'}</td>
<td>¥${Number(s.base_salary).toLocaleString()}</td>
<td>${s.hourly_rate ? '¥' + Number(s.hourly_rate).toLocaleString() : '-'}</td>
<td>${s.employment_type}</td>
<td>${s.payment_type}</td>
<td>¥${Number(s.commute_allowance).toLocaleString()}</td>
<td>¥${Number(s.other_allowance).toLocaleString()}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('settingsList').innerHTML = html;
// 現在の設定を取得
loadCurrentSetting(employeeId);
} catch (error) {
alert('給与設定の取得に失敗しました');
console.error(error);
}
}
async function loadCurrentSetting(employeeId) {
try {
const response = await fetch(`${API_BASE}/payroll/settings/salary/${employeeId}/current`);
if (response.ok) {
const setting = await response.json();
const html = `
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:15px;">
<div><strong>基本給:</strong> ¥${Number(setting.base_salary).toLocaleString()}</div>
<div><strong>時給:</strong> ${setting.hourly_rate ? '¥' + Number(setting.hourly_rate).toLocaleString() : '-'}</div>
<div><strong>雇用形態:</strong> ${setting.employment_type}</div>
<div><strong>支給形態:</strong> ${setting.payment_type}</div>
<div><strong>通勤手当:</strong> ¥${Number(setting.commute_allowance).toLocaleString()}</div>
<div><strong>その他手当:</strong> ¥${Number(setting.other_allowance).toLocaleString()}</div>
<div><strong>適用期間:</strong> ${setting.valid_from} ${setting.valid_to || '現在'}</div>
</div>
`;
document.getElementById('currentSettingContent').innerHTML = html;
} else {
document.getElementById('currentSettingContent').innerHTML = '<p>給与設定が登録されていません</p>';
}
} catch (error) {
document.getElementById('currentSettingContent').innerHTML = '<p>給与設定の取得に失敗しました</p>';
}
}
function showAddForm() {
document.getElementById('addForm').style.display = 'block';
// デフォルト値設定
document.querySelector('[name="valid_from"]').value = new Date().toISOString().split('T')[0];
}
function hideAddForm() {
document.getElementById('addForm').style.display = 'none';
document.querySelector('#addForm form').reset();
}
async function saveSalarySetting(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
data.employee_id = parseInt(currentEmployeeId);
data.base_salary = parseFloat(data.base_salary) || 0;
data.commute_allowance = parseFloat(data.commute_allowance) || 0;
data.other_allowance = parseFloat(data.other_allowance) || 0;
if (data.hourly_rate) {
data.hourly_rate = parseFloat(data.hourly_rate);
} else {
delete data.hourly_rate;
}
if (!data.valid_to) {
delete data.valid_to;
}
try {
const response = await fetch(`${API_BASE}/payroll/settings/salary`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('給与設定を保存しました');
hideAddForm();
loadSalarySettings();
} else {
const error = await response.json();
alert('保存に失敗しました: ' + error.detail);
}
} catch (error) {
alert('保存に失敗しました');
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

View File

@@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理 - 設定</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.nav-tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: #f0f0f0;
border-radius: 5px 5px 0 0;
}
.nav-tab.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table th, table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
table th {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 設定</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('insurance')">保険料率</button>
<button class="nav-tab" onclick="showTab('tax')">所得税率表</button>
</div>
<!-- 保険料率タブ -->
<div id="tab-insurance" class="tab-content active">
<h2>社会保険料率設定</h2>
<button class="btn btn-primary" onclick="showInsuranceForm()">新規追加</button>
<div id="insuranceList"></div>
<div id="insuranceForm" style="display:none; margin-top:20px; padding:20px; border:2px solid #007bff; border-radius:5px;">
<h3>保険料率の登録</h3>
<form onsubmit="saveInsuranceRate(event)">
<div class="form-group">
<label>保険種類 *</label>
<select name="rate_type" required>
<option value="">選択してください</option>
<option value="健康保険">健康保険</option>
<option value="厚生年金">厚生年金</option>
<option value="雇用保険">雇用保険</option>
<option value="労災保険">労災保険</option>
</select>
</div>
<div class="form-group">
<label>従業員負担率 (%) *</label>
<input type="number" name="employee_rate" step="0.0001" required placeholder="例: 5.0 (5%)">
</div>
<div class="form-group">
<label>会社負担率 (%) *</label>
<input type="number" name="employer_rate" step="0.0001" required placeholder="例: 5.0 (5%)">
</div>
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" required>
</div>
<div class="form-group">
<label>適用終了日</label>
<input type="date" name="valid_to">
</div>
<div class="form-group">
<label>備考</label>
<input type="text" name="notes">
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button type="button" class="btn btn-secondary" onclick="hideInsuranceForm()">キャンセル</button>
</form>
</div>
</div>
<!-- 所得税率表タブ -->
<div id="tab-tax" class="tab-content">
<h2>所得税率表</h2>
<div class="form-group" style="max-width:300px;">
<label>扶養人数でフィルター</label>
<select id="filterDependents" onchange="loadIncomeTax()">
<option value="">全て</option>
<option value="0">0人</option>
<option value="1">1人</option>
<option value="2">2人</option>
<option value="3">3人</option>
<option value="4">4人</option>
<option value="5">5人以上</option>
</select>
</div>
<button class="btn btn-primary" onclick="document.getElementById('csvFile').click()">CSVインポート</button>
<input type="file" id="csvFile" accept=".csv" style="display:none;" onchange="importTaxTable(event)">
<div id="taxList"></div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
function showTab(tabName) {
document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
if (tabName === 'insurance') {
document.querySelectorAll('.nav-tab')[0].classList.add('active');
document.getElementById('tab-insurance').classList.add('active');
loadInsuranceRates();
} else if (tabName === 'tax') {
document.querySelectorAll('.nav-tab')[1].classList.add('active');
document.getElementById('tab-tax').classList.add('active');
loadIncomeTax();
}
}
function showInsuranceForm() {
document.getElementById('insuranceForm').style.display = 'block';
}
function hideInsuranceForm() {
document.getElementById('insuranceForm').style.display = 'none';
document.querySelector('#insuranceForm form').reset();
}
async function loadInsuranceRates() {
try {
const response = await fetch(`${API_BASE}/payroll/settings/insurance-rates`);
const rates = await response.json();
const html = `
<table>
<thead>
<tr>
<th>保険種類</th>
<th>従業員負担率</th>
<th>会社負担率</th>
<th>適用期間</th>
<th>備考</th>
</tr>
</thead>
<tbody>
${rates.map(rate => `
<tr>
<td>${rate.rate_type}</td>
<td>${(parseFloat(rate.employee_rate) * 100).toFixed(2)}%</td>
<td>${(parseFloat(rate.employer_rate) * 100).toFixed(2)}%</td>
<td>${rate.valid_from} ${rate.valid_to || '現在'}</td>
<td>${rate.notes || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('insuranceList').innerHTML = html;
} catch (error) {
alert('保険料率の取得に失敗しました');
console.error(error);
}
}
async function saveInsuranceRate(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// パーセントを小数に変換
data.employee_rate = parseFloat(data.employee_rate) / 100;
data.employer_rate = parseFloat(data.employer_rate) / 100;
// 空の値を除外
if (!data.valid_to) delete data.valid_to;
if (!data.notes) delete data.notes;
try {
const response = await fetch(`${API_BASE}/payroll/settings/insurance-rates`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('保険料率を登録しました');
hideInsuranceForm();
loadInsuranceRates();
} else {
const error = await response.json();
alert('登録に失敗しました: ' + error.detail);
}
} catch (error) {
alert('登録に失敗しました');
console.error(error);
}
}
async function loadIncomeTax() {
const dependentsFilter = document.getElementById('filterDependents').value;
const params = dependentsFilter ? `?dependents_count=${dependentsFilter}` : '';
try {
const response = await fetch(`${API_BASE}/payroll/settings/income-tax${params}`);
const taxes = await response.json();
const html = `
<table>
<thead>
<tr>
<th>扶養人数</th>
<th>月収From</th>
<th>月収To</th>
<th>税額</th>
<th>適用期間</th>
</tr>
</thead>
<tbody>
${taxes.map(tax => `
<tr>
<td>${tax.dependents_count}人</td>
<td>¥${Number(tax.monthly_income_from).toLocaleString()}</td>
<td>¥${Number(tax.monthly_income_to).toLocaleString()}</td>
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
<td>${tax.valid_from} ${tax.valid_to || '現在'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('taxList').innerHTML = html;
} catch (error) {
alert('所得税率表の取得に失敗しました');
console.error(error);
}
}
async function importTaxTable(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const validFrom = prompt('適用開始日を入力してください (YYYY-MM-DD):', new Date().toISOString().split('T')[0]);
if (!validFrom) return;
try {
const response = await fetch(`${API_BASE}/payroll/settings/income-tax/import?valid_from=${validFrom}`, {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
alert(result.message);
loadIncomeTax();
} else {
const error = await response.json();
alert('インポートに失敗しました: ' + error.detail);
}
} catch (error) {
alert('インポートに失敗しました');
console.error(error);
}
event.target.value = '';
}
// 初期化
loadInsuranceRates();
</script>
</body>
</html>

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理システム</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.menu-card {
padding: 30px;
border: 2px solid #007bff;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.menu-card:hover {
background: #007bff;
color: white;
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,123,255,0.3);
}
.menu-card h2 {
margin: 0 0 15px 0;
font-size: 1.5em;
}
.menu-card p {
margin: 0;
opacity: 0.8;
}
.menu-card:hover p {
opacity: 1;
}
.icon {
font-size: 3em;
margin-bottom: 15px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<a href="../index.html" class="back-link">← メインメニューに戻る</a>
<h1>給与管理システム</h1>
<p>従業員の給与計算、扶養管理、保険料・税金の管理を行います</p>
<div class="menu-grid">
<div class="menu-card" onclick="location.href='payroll-employees.html'">
<div class="icon">👥</div>
<h2>従業員管理</h2>
<p>従業員の基本情報、扶養家族情報を管理します</p>
</div>
<div class="menu-card" onclick="location.href='payroll-salary-settings.html'">
<div class="icon">💰</div>
<h2>給与設定</h2>
<p>各従業員の給与設定、基本給、手当などを管理します</p>
</div>
<div class="menu-card" onclick="location.href='payroll-calculation.html'">
<div class="icon">🧮</div>
<h2>月次給与計算</h2>
<p>毎月の給与計算、給与明細の作成・承認を行います</p>
</div>
<div class="menu-card" onclick="location.href='payroll-settings.html'">
<div class="icon">⚙️</div>
<h2>税率・保険料設定</h2>
<p>社会保険料率、所得税率表の管理を行います</p>
</div>
</div>
<div style="margin-top: 50px; padding: 20px; background: #f0f8ff; border-radius: 5px;">
<h3>📋 給与システムの使い方</h3>
<ol>
<li><strong>従業員管理</strong>で従業員を登録し、扶養家族情報を入力します</li>
<li><strong>給与設定</strong>で各従業員の基本給や手当を設定します</li>
<li><strong>税率・保険料設定</strong>で社会保険料率や所得税率表を確認・更新します</li>
<li><strong>月次給与計算</strong>で勤怠情報を入力し、給与を計算・承認します</li>
</ol>
</div>
</div>
</body>
</html>

20
backend/check_excel.py Normal file
View File

@@ -0,0 +1,20 @@
import xlrd
file_path = r"C:\workspace\njts-accounting-core\reports\tmp\01-07 (3).xls"
workbook = xlrd.open_workbook(file_path)
sheet = workbook.sheet_by_index(0)
print(f"シート名: {sheet.name}")
print(f"行数: {sheet.nrows}, 列数: {sheet.ncols}")
print("\n最初の15行:")
print("=" * 100)
for row_idx in range(min(15, sheet.nrows)):
row_values = []
for col_idx in range(min(15, sheet.ncols)):
cell_value = sheet.cell_value(row_idx, col_idx)
if cell_value == '':
row_values.append('')
else:
row_values.append(str(cell_value))
print(f"{row_idx}: {' | '.join(row_values)}")

View File

@@ -0,0 +1,59 @@
import os
import psycopg2
from dotenv import load_dotenv
# Load environment variables from root .env
root_env = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
load_dotenv(root_env)
# Connect to database
conn = psycopg2.connect(
host=os.getenv('DB_HOST', '192.168.0.61'),
port=os.getenv('DB_PORT', '55432'),
dbname=os.getenv('DB_NAME', 'njts_acct'),
user=os.getenv('DB_USER', 'njts_app'),
password=os.getenv('DB_PASSWORD')
)
cur = conn.cursor()
# Check current column precision
cur.execute("""
SELECT column_name, data_type, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='insurance_rates'
AND column_name IN ('employee_rate', 'employer_rate')
ORDER BY column_name
""")
print("insurance_rates カラムの現在の精度:")
for row in cur.fetchall():
print(f" {row[0]}: {row[1]}({row[2]},{row[3]})")
# Check child_support_contribution_rates
cur.execute("""
SELECT column_name, data_type, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='child_support_contribution_rates'
AND column_name = 'contribution_rate'
""")
print("\nchild_support_contribution_rates カラムの現在の精度:")
for row in cur.fetchall():
print(f" {row[0]}: {row[1]}({row[2]},{row[3]})")
# Check actual data
cur.execute("""
SELECT rate_year, rate_type, calculation_target, prefecture,
employee_rate, employer_rate
FROM insurance_rates
WHERE rate_year = 2025 AND rate_type = '健康保険'
LIMIT 3
""")
print("\n実際のデータ (2025年健康保険):")
for row in cur.fetchall():
print(f" {row[0]} {row[1]} {row[2]} {row[3]}: 従業員={row[4]} 会社={row[5]}")
cur.close()
conn.close()

View File

@@ -0,0 +1,72 @@
"""
給与システムデータベース初期化スクリプト
Payroll Database Initialization Script
"""
import psycopg
import os
from dotenv import load_dotenv
# 環境変数を読み込む
load_dotenv()
# データベース接続文字列を構築
DB_HOST = os.getenv("DB_HOST", "192.168.0.61")
DB_PORT = os.getenv("DB_PORT", "55432")
DB_NAME = os.getenv("DB_NAME", "njts_acct")
DB_USER = os.getenv("DB_USER", "njts_app")
DB_PASSWORD = os.getenv("DB_PASSWORD", "njts_app2025")
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
def create_payroll_database():
"""SQLファイルを実行してデータベースを初期化"""
# SQLファイルのパスを取得
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
sql_file = os.path.join(script_dir, 'sql', 'payroll_schema_complete.sql')
# SQLファイルを読み込む
with open(sql_file, 'r', encoding='utf-8') as f:
sql_script = f.read()
try:
# データベースに接続
print(f"データベースに接続中: {DATABASE_URL}")
conn = psycopg.connect(DATABASE_URL)
# トランザクションを開始
with conn:
with conn.cursor() as cur:
# SQLスクリプトを実行
print("SQLスクリプトを実行中...")
cur.execute(sql_script)
print("✅ データベースの初期化が完了しました!")
# テーブル一覧を確認
cur.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
""")
tables = cur.fetchall()
print("\n作成されたテーブル:")
for table in tables:
print(f" - {table[0]}")
# 保険料率データを確認
cur.execute("SELECT COUNT(*) FROM insurance_rates")
count = cur.fetchone()[0]
print(f"\n保険料率データ: {count}")
conn.close()
print("\n✅ データベース初期化が正常に完了しました!")
except Exception as e:
print(f"\n❌ エラーが発生しました: {e}")
print(f"\nエラーの詳細: {type(e).__name__}")
raise
if __name__ == "__main__":
create_payroll_database()

68
backend/fix_precision.py Normal file
View File

@@ -0,0 +1,68 @@
import os
import psycopg2
from dotenv import load_dotenv
# Load environment variables
root_env = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
load_dotenv(root_env)
# Connect to database
conn = psycopg2.connect(
host=os.getenv('DB_HOST', '192.168.0.61'),
port=os.getenv('DB_PORT', '55432'),
dbname=os.getenv('DB_NAME', 'njts_acct'),
user=os.getenv('DB_USER', 'njts_app'),
password=os.getenv('DB_PASSWORD')
)
cur = conn.cursor()
print("保険率カラムを DECIMAL(7,5) に変更中...")
print("これにより、4.955% (0.04955) のような値を正確に保存できます。")
# Alter insurance_rates table to DECIMAL(7,5)
cur.execute("""
ALTER TABLE insurance_rates
ALTER COLUMN employee_rate TYPE DECIMAL(7, 5),
ALTER COLUMN employer_rate TYPE DECIMAL(7, 5)
""")
# Alter child_support_contribution_rates table
cur.execute("""
ALTER TABLE child_support_contribution_rates
ALTER COLUMN contribution_rate TYPE DECIMAL(7, 5)
""")
conn.commit()
print("OK カラム変更完了")
# Verify
cur.execute("""
SELECT column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='insurance_rates'
AND column_name IN ('employee_rate', 'employer_rate')
ORDER BY column_name
""")
print("\n変更後の insurance_rates カラム:")
for row in cur.fetchall():
print(f" {row[0]}: numeric({row[1]},{row[2]})")
cur.execute("""
SELECT column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='child_support_contribution_rates'
AND column_name = 'contribution_rate'
""")
print("\n変更後の child_support_contribution_rates カラム:")
for row in cur.fetchall():
print(f" {row[0]}: numeric({row[1]},{row[2]})")
cur.close()
conn.close()
print("\n完了!")
print("これで 4.955% を入力すると、0.04955 として正確に保存され、4.955% と表示されます。")

View File

@@ -0,0 +1,51 @@
月収From,月収To,扶養人数,税額
0,88000,0,0
88000,89000,0,130
89000,90000,0,180
90000,93000,0,230
93000,95000,0,280
95000,97000,0,380
97000,99000,0,480
99000,101000,0,580
101000,103000,0,680
103000,105000,0,780
105000,107000,0,880
107000,109000,0,1020
109000,118000,0,1160
118000,120000,0,1300
120000,122000,0,1440
122000,124000,0,1580
124000,126000,0,1720
126000,128000,0,1860
128000,130000,0,2000
130000,132000,0,2140
132000,134000,0,2280
134000,136000,0,2420
136000,138000,0,2560
138000,140000,0,2700
140000,142000,0,2840
142000,144000,0,2980
144000,146000,0,3120
146000,148000,0,3260
148000,150000,0,3400
0,96000,1,0
96000,98000,1,130
98000,100000,1,180
100000,102000,1,230
102000,104000,1,280
104000,106000,1,380
106000,108000,1,480
108000,110000,1,580
110000,112000,1,680
112000,114000,1,780
114000,116000,1,880
116000,118000,1,1020
118000,127000,1,1160
127000,129000,1,1300
129000,131000,1,1440
131000,133000,1,1580
133000,135000,1,1720
135000,137000,1,1860
137000,139000,1,2000
139000,141000,1,2140
141000,143000,1,2280
1 月収From 月収To 扶養人数 税額
2 0 88000 0 0
3 88000 89000 0 130
4 89000 90000 0 180
5 90000 93000 0 230
6 93000 95000 0 280
7 95000 97000 0 380
8 97000 99000 0 480
9 99000 101000 0 580
10 101000 103000 0 680
11 103000 105000 0 780
12 105000 107000 0 880
13 107000 109000 0 1020
14 109000 118000 0 1160
15 118000 120000 0 1300
16 120000 122000 0 1440
17 122000 124000 0 1580
18 124000 126000 0 1720
19 126000 128000 0 1860
20 128000 130000 0 2000
21 130000 132000 0 2140
22 132000 134000 0 2280
23 134000 136000 0 2420
24 136000 138000 0 2560
25 138000 140000 0 2700
26 140000 142000 0 2840
27 142000 144000 0 2980
28 144000 146000 0 3120
29 146000 148000 0 3260
30 148000 150000 0 3400
31 0 96000 1 0
32 96000 98000 1 130
33 98000 100000 1 180
34 100000 102000 1 230
35 102000 104000 1 280
36 104000 106000 1 380
37 106000 108000 1 480
38 108000 110000 1 580
39 110000 112000 1 680
40 112000 114000 1 780
41 114000 116000 1 880
42 116000 118000 1 1020
43 118000 127000 1 1160
44 127000 129000 1 1300
45 129000 131000 1 1440
46 131000 133000 1 1580
47 133000 135000 1 1720
48 135000 137000 1 1860
49 137000 139000 1 2000
50 139000 141000 1 2140
51 141000 143000 1 2280

Binary file not shown.

View File

@@ -0,0 +1,56 @@
import os
import psycopg2
from dotenv import load_dotenv
# Load environment variables
root_env = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
load_dotenv(root_env)
# Connect to database
conn = psycopg2.connect(
host=os.getenv('DB_HOST', '192.168.0.61'),
port=os.getenv('DB_PORT', '55432'),
dbname=os.getenv('DB_NAME', 'njts_acct'),
user=os.getenv('DB_USER', 'njts_app'),
password=os.getenv('DB_PASSWORD')
)
cur = conn.cursor()
print("保険率カラムを DECIMAL(6,4) に戻しています...")
# Alter insurance_rates table back to DECIMAL(6,4)
cur.execute("""
ALTER TABLE insurance_rates
ALTER COLUMN employee_rate TYPE DECIMAL(6, 4),
ALTER COLUMN employer_rate TYPE DECIMAL(6, 4)
""")
# Alter child_support_contribution_rates table
cur.execute("""
ALTER TABLE child_support_contribution_rates
ALTER COLUMN contribution_rate TYPE DECIMAL(6, 4)
""")
conn.commit()
print("OK データベース変更完了")
# Verify
cur.execute("""
SELECT column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='insurance_rates'
AND column_name IN ('employee_rate', 'employer_rate')
ORDER BY column_name
""")
print("\n変更後:")
for row in cur.fetchall():
print(f" {row[0]}: numeric({row[1]},{row[2]})")
cur.close()
conn.close()
print("\nデータベースは DECIMAL(6,4) で、フロントエンドの表示を小数点以下3桁にします。")
print("これにより、4.955% のような値を正確に保存しつつ、表示は3桁に丸めることができます。")

View File

@@ -0,0 +1,17 @@
-- 所得税率表に年度フィールドを追加
ALTER TABLE income_tax_table
ADD COLUMN tax_year INTEGER;
-- 既存データに年度を設定valid_fromから抽出
UPDATE income_tax_table
SET tax_year = EXTRACT(YEAR FROM valid_from);
-- 年度をNOT NULLに変更
ALTER TABLE income_tax_table
ALTER COLUMN tax_year SET NOT NULL;
-- インデックスを追加
CREATE INDEX idx_income_tax_year ON income_tax_table(tax_year);
-- コメント
COMMENT ON COLUMN income_tax_table.tax_year IS '適用年度';

View File

@@ -0,0 +1,116 @@
-- 給与システム拡張機能
-- Payroll System Enhancements
-- ========================================
-- 1. 社会保険料上限設定テーブル
-- ========================================
CREATE TABLE IF NOT EXISTS insurance_limit_settings (
limit_id SERIAL PRIMARY KEY,
setting_type VARCHAR(50) NOT NULL, -- 健康保険上限/厚生年金上限/通勤手当非課税限度額
limit_amount DECIMAL(12, 2) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE insurance_limit_settings IS '社会保険料上限設定';
COMMENT ON COLUMN insurance_limit_settings.setting_type IS '設定種別: 健康保険上限/厚生年金上限/通勤手当非課税限度額';
COMMENT ON COLUMN insurance_limit_settings.limit_amount IS '上限額';
-- ========================================
-- 2. 賞与計算テーブル
-- ========================================
CREATE TABLE IF NOT EXISTS bonus_payments (
bonus_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id),
bonus_year INTEGER NOT NULL,
bonus_month INTEGER NOT NULL,
bonus_type VARCHAR(50) NOT NULL, -- 夏季賞与/冬季賞与/決算賞与/その他
payment_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'draft', -- draft/calculated/approved/paid
-- 支給項目
base_bonus DECIMAL(12, 2) DEFAULT 0, -- 基本賞与額
performance_bonus DECIMAL(12, 2) DEFAULT 0, -- 業績賞与
other_bonus DECIMAL(12, 2) DEFAULT 0, -- その他賞与
total_bonus DECIMAL(12, 2) DEFAULT 0, -- 総支給額
-- 控除項目
health_insurance DECIMAL(12, 2) DEFAULT 0,
care_insurance DECIMAL(12, 2) DEFAULT 0, -- 介護保険40歳以上
pension_insurance DECIMAL(12, 2) DEFAULT 0,
employment_insurance DECIMAL(12, 2) DEFAULT 0,
income_tax DECIMAL(12, 2) DEFAULT 0, -- 賞与所得税
other_deduction DECIMAL(12, 2) DEFAULT 0,
total_deduction DECIMAL(12, 2) DEFAULT 0,
-- 差引支給額
net_bonus DECIMAL(12, 2) DEFAULT 0,
notes TEXT,
calculated_at TIMESTAMP,
calculated_by VARCHAR(100),
approved_at TIMESTAMP,
approved_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_bonus_employee_payment UNIQUE (employee_id, bonus_year, bonus_month, bonus_type)
);
COMMENT ON TABLE bonus_payments IS '賞与計算';
COMMENT ON COLUMN bonus_payments.bonus_type IS '賞与種別: 夏季賞与/冬季賞与/決算賞与/その他';
COMMENT ON COLUMN bonus_payments.status IS 'ステータス: draft/calculated/approved/paid';
-- ========================================
-- 3. 扶養家族判定条件テーブル(所得税計算用)
-- ========================================
CREATE TABLE IF NOT EXISTS dependent_rules (
rule_id SERIAL PRIMARY KEY,
rule_type VARCHAR(50) NOT NULL, -- 所得限度額/年齢条件/同一生計
rule_value VARCHAR(100) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dependent_rules IS '扶養家族判定条件';
COMMENT ON COLUMN dependent_rules.rule_type IS 'ルール種別: 所得限度額/年齢条件/同一生計';
-- ========================================
-- インデックス作成
-- ========================================
CREATE INDEX idx_insurance_limits_valid ON insurance_limit_settings(valid_from, valid_to);
CREATE INDEX idx_bonus_employee_year ON bonus_payments(employee_id, bonus_year);
CREATE INDEX idx_bonus_status ON bonus_payments(status);
CREATE INDEX idx_dependent_rules_valid ON dependent_rules(valid_from, valid_to);
-- ========================================
-- トリガー: 更新日時の自動更新
-- ========================================
CREATE TRIGGER trg_insurance_limits_updated_at BEFORE UPDATE ON insurance_limit_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_bonus_payments_updated_at BEFORE UPDATE ON bonus_payments
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ========================================
-- 初期データ: 社会保険料上限設定 (2026年度)
-- ========================================
INSERT INTO insurance_limit_settings (setting_type, limit_amount, valid_from, notes) VALUES
('健康保険標準報酬月額上限', 1390000, '2026-01-01', '健康保険の標準報酬月額上限第50級'),
('厚生年金標準報酬月額上限', 650000, '2026-01-01', '厚生年金保険の標準報酬月額上限第32級'),
('通勤手当非課税限度額', 150000, '2026-01-01', '通勤手当の非課税限度額(月額)');
-- ========================================
-- 初期データ: 扶養家族判定条件 (2026年度)
-- ========================================
INSERT INTO dependent_rules (rule_type, rule_value, valid_from, description) VALUES
('所得限度額_一般', '480000', '2026-01-01', '一般扶養親族の所得限度額年間48万円'),
('所得限度額_配偶者', '480000', '2026-01-01', '配偶者の所得限度額年間48万円'),
('所得限度額_配偶者特別', '1330000', '2026-01-01', '配偶者特別控除の所得限度額年間133万円'),
('年齢_特定扶養親族', '19-23', '2026-01-01', '特定扶養親族の年齢範囲19歳以上23歳未満'),
('年齢_老人扶養親族', '70-', '2026-01-01', '老人扶養親族の年齢70歳以上');

View File

@@ -0,0 +1,172 @@
-- ========================================
-- 給与システム マイグレーション 2026-01-19
-- Payroll System Migration Script
-- ========================================
-- このスクリプトは既存のpayroll_schema.sqlを適用済みのDBに対して実行してください
-- This script should be run on databases that already have payroll_schema.sql applied
-- ========================================
-- 1. 既存テーブルへの列追加
-- ========================================
-- 従業員テーブルに新しい列を追加(介護保険判定用)
-- Add new columns to employees table (for care insurance determination)
ALTER TABLE employees
ADD COLUMN IF NOT EXISTS birth_date DATE,
ADD COLUMN IF NOT EXISTS gender VARCHAR(10),
ADD COLUMN IF NOT EXISTS postal_code VARCHAR(10),
ADD COLUMN IF NOT EXISTS address VARCHAR(200);
COMMENT ON COLUMN employees.birth_date IS '生年月日(介護保険料計算に使用)';
COMMENT ON COLUMN employees.gender IS '性別';
COMMENT ON COLUMN employees.postal_code IS '郵便番号';
COMMENT ON COLUMN employees.address IS '住所';
-- 月次給与計算テーブルに介護保険列を追加
-- Add care insurance column to monthly payroll table
ALTER TABLE monthly_payroll
ADD COLUMN IF NOT EXISTS care_insurance DECIMAL(12, 2) DEFAULT 0;
COMMENT ON COLUMN monthly_payroll.care_insurance IS '介護保険料40歳以上65歳未満対象';
-- ========================================
-- 2. 介護保険料率の初期データを追加(まだ存在しない場合)
-- ========================================
INSERT INTO insurance_rates (rate_type, employee_rate, employer_rate, valid_from, notes)
SELECT '介護保険', 0.0091, 0.0091, '2026-01-01', '介護保険料率 1.82% (労使折半、40歳以上対象)'
WHERE NOT EXISTS (
SELECT 1 FROM insurance_rates
WHERE rate_type = '介護保険' AND valid_from = '2026-01-01'
);
-- ========================================
-- 3. 新テーブル作成: 社会保険料上限設定
-- ========================================
CREATE TABLE IF NOT EXISTS insurance_limit_settings (
limit_id SERIAL PRIMARY KEY,
setting_type VARCHAR(50) NOT NULL, -- 健康保険上限/厚生年金上限/通勤手当非課税限度額
limit_amount DECIMAL(12, 2) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE insurance_limit_settings IS '社会保険料上限設定';
COMMENT ON COLUMN insurance_limit_settings.setting_type IS '設定種別: 健康保険標準報酬月額上限/厚生年金標準報酬月額上限/通勤手当非課税限度額';
COMMENT ON COLUMN insurance_limit_settings.limit_amount IS '上限額';
-- ========================================
-- 4. 新テーブル作成: 賞与計算
-- ========================================
CREATE TABLE IF NOT EXISTS bonus_payments (
bonus_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id),
bonus_year INTEGER NOT NULL,
bonus_month INTEGER NOT NULL,
bonus_type VARCHAR(50) NOT NULL, -- 夏季賞与/冬季賞与/決算賞与/その他
payment_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'draft', -- draft/calculated/approved/paid
-- 支給項目
base_bonus DECIMAL(12, 2) DEFAULT 0, -- 基本賞与額
performance_bonus DECIMAL(12, 2) DEFAULT 0, -- 業績賞与
other_bonus DECIMAL(12, 2) DEFAULT 0, -- その他賞与
total_bonus DECIMAL(12, 2) DEFAULT 0, -- 総支給額
-- 控除項目
health_insurance DECIMAL(12, 2) DEFAULT 0,
care_insurance DECIMAL(12, 2) DEFAULT 0, -- 介護保険40歳以上
pension_insurance DECIMAL(12, 2) DEFAULT 0,
employment_insurance DECIMAL(12, 2) DEFAULT 0,
income_tax DECIMAL(12, 2) DEFAULT 0, -- 賞与所得税
other_deduction DECIMAL(12, 2) DEFAULT 0,
total_deduction DECIMAL(12, 2) DEFAULT 0,
-- 差引支給額
net_bonus DECIMAL(12, 2) DEFAULT 0,
notes TEXT,
calculated_at TIMESTAMP,
calculated_by VARCHAR(100),
approved_at TIMESTAMP,
approved_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_bonus_employee_payment UNIQUE (employee_id, bonus_year, bonus_month, bonus_type)
);
COMMENT ON TABLE bonus_payments IS '賞与計算';
COMMENT ON COLUMN bonus_payments.bonus_type IS '賞与種別: 夏季賞与/冬季賞与/決算賞与/その他';
COMMENT ON COLUMN bonus_payments.status IS 'ステータス: draft/calculated/approved/paid';
-- ========================================
-- 5. 新テーブル作成: 扶養家族判定条件
-- ========================================
CREATE TABLE IF NOT EXISTS dependent_rules (
rule_id SERIAL PRIMARY KEY,
rule_type VARCHAR(50) NOT NULL, -- 所得限度額/年齢条件/同一生計
rule_value VARCHAR(100) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dependent_rules IS '扶養家族判定条件';
COMMENT ON COLUMN dependent_rules.rule_type IS 'ルール種別: 所得限度額/年齢条件/同一生計';
-- ========================================
-- 6. インデックス作成
-- ========================================
CREATE INDEX IF NOT EXISTS idx_insurance_limits_valid ON insurance_limit_settings(valid_from, valid_to);
CREATE INDEX IF NOT EXISTS idx_bonus_employee_year ON bonus_payments(employee_id, bonus_year);
CREATE INDEX IF NOT EXISTS idx_bonus_status ON bonus_payments(status);
CREATE INDEX IF NOT EXISTS idx_dependent_rules_valid ON dependent_rules(valid_from, valid_to);
-- ========================================
-- 7. トリガー: 更新日時の自動更新
-- ========================================
CREATE TRIGGER trg_insurance_limits_updated_at BEFORE UPDATE ON insurance_limit_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_bonus_payments_updated_at BEFORE UPDATE ON bonus_payments
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ========================================
-- 8. 初期データ: 社会保険料上限設定 (2026年度)
-- ========================================
INSERT INTO insurance_limit_settings (setting_type, limit_amount, valid_from, notes) VALUES
('健康保険標準報酬月額上限', 1390000, '2026-01-01', '健康保険の標準報酬月額上限第50級'),
('厚生年金標準報酬月額上限', 650000, '2026-01-01', '厚生年金保険の標準報酬月額上限第32級'),
('通勤手当非課税限度額', 150000, '2026-01-01', '通勤手当の非課税限度額(月額)')
ON CONFLICT DO NOTHING;
-- ========================================
-- 9. 初期データ: 扶養家族判定条件 (2026年度)
-- ========================================
INSERT INTO dependent_rules (rule_type, rule_value, valid_from, description) VALUES
('所得限度額_一般', '480000', '2026-01-01', '一般扶養親族の所得限度額年間48万円'),
('所得限度額_配偶者', '480000', '2026-01-01', '配偶者の所得限度額年間48万円'),
('所得限度額_配偶者特別', '1330000', '2026-01-01', '配偶者特別控除の所得限度額年間133万円'),
('年齢_特定扶養親族', '19-23', '2026-01-01', '特定扶養親族の年齢範囲19歳以上23歳未満'),
('年齢_老人扶養親族', '70-', '2026-01-01', '老人扶養親族の年齢70歳以上')
ON CONFLICT DO NOTHING;
-- ========================================
-- マイグレーション完了
-- ========================================
-- 以下のコマンドで適用状態を確認できます:
-- Verify migration with the following commands:
--
-- \d employees -- 従業員テーブルの構造確認
-- \d monthly_payroll -- 月次給与テーブルの構造確認
-- \dt *bonus* -- 賞与テーブルの確認
-- \dt *insurance_limit* -- 保険上限テーブルの確認
-- \dt *dependent_rule* -- 扶養判定テーブルの確認
--
-- SELECT * FROM insurance_limit_settings; -- 保険上限設定の確認
-- SELECT * FROM dependent_rules; -- 扶養判定条件の確認
-- SELECT * FROM insurance_rates WHERE rate_type = '介護保険'; -- 介護保険料率の確認

View File

@@ -0,0 +1,238 @@
-- 給与システム用スキーマ
-- Payroll System Schema
-- ========================================
-- 1. 従業員マスタ (Employees)
-- ========================================
CREATE TABLE IF NOT EXISTS employees (
employee_id SERIAL PRIMARY KEY,
employee_code VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
name_kana VARCHAR(100),
birth_date DATE, -- 生年月日(介護保険判定に使用)
gender VARCHAR(10), -- 性別
email VARCHAR(100),
phone VARCHAR(20),
postal_code VARCHAR(10), -- 郵便番号
address VARCHAR(200), -- 住所
hire_date DATE NOT NULL,
termination_date DATE,
is_active BOOLEAN DEFAULT true,
bank_name VARCHAR(100),
bank_branch VARCHAR(100),
bank_account_number VARCHAR(20),
bank_account_type VARCHAR(20), -- 普通/当座
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE employees IS '従業員マスタ';
COMMENT ON COLUMN employees.employee_code IS '従業員コード';
COMMENT ON COLUMN employees.birth_date IS '生年月日(介護保険料計算に使用)';
COMMENT ON COLUMN employees.gender IS '性別';
COMMENT ON COLUMN employees.bank_account_type IS '口座種別: 普通/当座';
-- ========================================
-- 2. 扶養家族情報 (Dependents)
-- ========================================
CREATE TABLE IF NOT EXISTS dependents (
dependent_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
relationship VARCHAR(20) NOT NULL, -- 配偶者/子/親/その他
birth_date DATE,
is_spouse BOOLEAN DEFAULT false,
is_disabled BOOLEAN DEFAULT false, -- 障害者控除対象
income_amount DECIMAL(12, 2) DEFAULT 0, -- 年間所得
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dependents IS '扶養家族情報';
COMMENT ON COLUMN dependents.relationship IS '続柄: 配偶者/子/親/その他';
COMMENT ON COLUMN dependents.is_disabled IS '障害者控除対象フラグ';
COMMENT ON COLUMN dependents.income_amount IS '年間所得額';
-- ========================================
-- 3. 給与基本設定 (Salary Settings)
-- ========================================
CREATE TABLE IF NOT EXISTS salary_settings (
setting_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id) ON DELETE CASCADE,
base_salary DECIMAL(12, 2) NOT NULL DEFAULT 0,
hourly_rate DECIMAL(10, 2),
employment_type VARCHAR(20) NOT NULL, -- 正社員/契約社員/パート/アルバイト
payment_type VARCHAR(20) NOT NULL, -- 月給/時給/日給
commute_allowance DECIMAL(12, 2) DEFAULT 0,
other_allowance DECIMAL(12, 2) DEFAULT 0,
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_employee_valid_from UNIQUE (employee_id, valid_from)
);
COMMENT ON TABLE salary_settings IS '給与基本設定';
COMMENT ON COLUMN salary_settings.employment_type IS '雇用形態: 正社員/契約社員/パート/アルバイト';
COMMENT ON COLUMN salary_settings.payment_type IS '支給形態: 月給/時給/日給';
-- ========================================
-- 4. 社会保険料率マスタ (Insurance Rates)
-- ========================================
CREATE TABLE IF NOT EXISTS insurance_rates (
rate_id SERIAL PRIMARY KEY,
rate_type VARCHAR(50) NOT NULL, -- 健康保険/厚生年金/雇用保険/労災保険
employee_rate DECIMAL(6, 4) NOT NULL, -- 従業員負担率
employer_rate DECIMAL(6, 4) NOT NULL, -- 会社負担率
valid_from DATE NOT NULL,
valid_to DATE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE insurance_rates IS '社会保険料率マスタ';
COMMENT ON COLUMN insurance_rates.rate_type IS '保険種類: 健康保険/厚生年金/雇用保険/労災保険';
COMMENT ON COLUMN insurance_rates.employee_rate IS '従業員負担率';
COMMENT ON COLUMN insurance_rates.employer_rate IS '会社負担率';
-- ========================================
-- 5. 所得税率表 (Income Tax Table)
-- ========================================
CREATE TABLE IF NOT EXISTS income_tax_table (
tax_id SERIAL PRIMARY KEY,
monthly_income_from DECIMAL(12, 2) NOT NULL,
monthly_income_to DECIMAL(12, 2) NOT NULL,
dependents_count INTEGER NOT NULL DEFAULT 0, -- 扶養家族人数
tax_amount DECIMAL(10, 2) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE income_tax_table IS '源泉所得税額表';
COMMENT ON COLUMN income_tax_table.dependents_count IS '扶養家族人数';
-- ========================================
-- 6. 月次給与計算ヘッダー (Monthly Payroll Header)
-- ========================================
CREATE TABLE IF NOT EXISTS monthly_payroll (
payroll_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id),
payroll_year INTEGER NOT NULL,
payroll_month INTEGER NOT NULL,
payment_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'draft', -- draft/calculated/approved/paid
-- 勤怠情報
working_days DECIMAL(5, 2) DEFAULT 0,
working_hours DECIMAL(6, 2) DEFAULT 0,
overtime_hours DECIMAL(6, 2) DEFAULT 0,
holiday_hours DECIMAL(6, 2) DEFAULT 0,
late_hours DECIMAL(6, 2) DEFAULT 0,
absent_days DECIMAL(5, 2) DEFAULT 0,
-- 支給項目
base_salary DECIMAL(12, 2) DEFAULT 0,
overtime_pay DECIMAL(12, 2) DEFAULT 0,
holiday_pay DECIMAL(12, 2) DEFAULT 0,
commute_allowance DECIMAL(12, 2) DEFAULT 0,
other_allowance DECIMAL(12, 2) DEFAULT 0,
total_payment DECIMAL(12, 2) DEFAULT 0,
-- 控除項目
health_insurance DECIMAL(12, 2) DEFAULT 0,
care_insurance DECIMAL(12, 2) DEFAULT 0, -- 介護保険40歳以上
pension_insurance DECIMAL(12, 2) DEFAULT 0,
employment_insurance DECIMAL(12, 2) DEFAULT 0,
income_tax DECIMAL(12, 2) DEFAULT 0,
resident_tax DECIMAL(12, 2) DEFAULT 0,
other_deduction DECIMAL(12, 2) DEFAULT 0,
total_deduction DECIMAL(12, 2) DEFAULT 0,
-- 差引支給額
net_payment DECIMAL(12, 2) DEFAULT 0,
notes TEXT,
calculated_at TIMESTAMP,
calculated_by VARCHAR(100),
approved_at TIMESTAMP,
approved_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_payroll_employee_month UNIQUE (employee_id, payroll_year, payroll_month)
);
COMMENT ON TABLE monthly_payroll IS '月次給与計算';
COMMENT ON COLUMN monthly_payroll.status IS 'ステータス: draft/calculated/approved/paid';
-- ========================================
-- 7. 給与明細項目詳細 (Payroll Detail Items)
-- ========================================
CREATE TABLE IF NOT EXISTS payroll_details (
detail_id SERIAL PRIMARY KEY,
payroll_id INTEGER NOT NULL REFERENCES monthly_payroll(payroll_id) ON DELETE CASCADE,
item_type VARCHAR(20) NOT NULL, -- payment/deduction
item_code VARCHAR(50) NOT NULL,
item_name VARCHAR(100) NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE payroll_details IS '給与明細項目詳細';
COMMENT ON COLUMN payroll_details.item_type IS '項目区分: payment/deduction';
-- ========================================
-- インデックス作成
-- ========================================
CREATE INDEX idx_employees_code ON employees(employee_code);
CREATE INDEX idx_employees_active ON employees(is_active);
CREATE INDEX idx_dependents_employee ON dependents(employee_id);
CREATE INDEX idx_salary_settings_employee ON salary_settings(employee_id);
CREATE INDEX idx_salary_settings_valid ON salary_settings(valid_from, valid_to);
CREATE INDEX idx_insurance_rates_valid ON insurance_rates(valid_from, valid_to);
CREATE INDEX idx_income_tax_valid ON income_tax_table(valid_from, valid_to);
CREATE INDEX idx_payroll_employee_year_month ON monthly_payroll(employee_id, payroll_year, payroll_month);
CREATE INDEX idx_payroll_status ON monthly_payroll(status);
CREATE INDEX idx_payroll_details_payroll ON payroll_details(payroll_id);
-- ========================================
-- トリガー: 更新日時の自動更新
-- ========================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_employees_updated_at BEFORE UPDATE ON employees
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_dependents_updated_at BEFORE UPDATE ON dependents
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_salary_settings_updated_at BEFORE UPDATE ON salary_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_insurance_rates_updated_at BEFORE UPDATE ON insurance_rates
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_monthly_payroll_updated_at BEFORE UPDATE ON monthly_payroll
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ========================================
-- 初期データ: 保険料率 (2026年度)
-- ========================================
INSERT INTO insurance_rates (rate_type, employee_rate, employer_rate, valid_from, notes) VALUES
('健康保険', 0.0500, 0.0500, '2026-01-01', '健康保険料率 10.00% (労使折半)'),
('介護保険', 0.0091, 0.0091, '2026-01-01', '介護保険料率 1.82% (労使折半、40歳以上対象)'),
('厚生年金', 0.0915, 0.0915, '2026-01-01', '厚生年金保険料率 18.30% (労使折半)'),
('雇用保険', 0.0060, 0.0095, '2026-01-01', '雇用保険料率 従業員0.6% 事業主0.95%'),
('労災保険', 0.0000, 0.0030, '2026-01-01', '労災保険料率 事業主のみ0.3%');

View File

@@ -0,0 +1,436 @@
-- ========================================
-- 給与システム 完全版スキーマ
-- Payroll System Complete Schema
-- ========================================
-- このスクリプトは既存のテーブルを削除して再作成します
-- This script will DROP existing tables and recreate them
--
-- 警告: すべてのデータが失われます!
-- WARNING: ALL DATA WILL BE LOST!
-- ========================================
-- ========================================
-- テーブル削除(依存関係の逆順)
-- ========================================
DROP TABLE IF EXISTS payroll_details CASCADE;
DROP TABLE IF EXISTS monthly_payroll CASCADE;
DROP TABLE IF EXISTS bonus_payments CASCADE;
DROP TABLE IF EXISTS income_tax_table CASCADE;
DROP TABLE IF EXISTS child_support_contribution_rates CASCADE;
DROP TABLE IF EXISTS insurance_rates CASCADE;
DROP TABLE IF EXISTS insurance_limit_settings CASCADE;
DROP TABLE IF EXISTS dependent_rules CASCADE;
DROP TABLE IF EXISTS salary_settings CASCADE;
DROP TABLE IF EXISTS dependents CASCADE;
DROP TABLE IF EXISTS employees CASCADE;
-- ========================================
-- 関数削除
-- ========================================
DROP FUNCTION IF EXISTS update_updated_at() CASCADE;
-- ========================================
-- 1. 従業員マスタ (Employees)
-- ========================================
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
employee_code VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
name_kana VARCHAR(100),
birth_date DATE, -- 生年月日(介護保険判定に使用)
gender VARCHAR(10), -- 性別
email VARCHAR(100),
phone VARCHAR(20),
postal_code VARCHAR(10), -- 郵便番号
address VARCHAR(200), -- 住所
hire_date DATE NOT NULL,
termination_date DATE,
is_active BOOLEAN DEFAULT true,
bank_name VARCHAR(100),
bank_branch VARCHAR(100),
bank_account_number VARCHAR(20),
bank_account_type VARCHAR(20), -- 普通/当座
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE employees IS '従業員マスタ';
COMMENT ON COLUMN employees.employee_code IS '従業員コード';
COMMENT ON COLUMN employees.birth_date IS '生年月日(介護保険料計算に使用)';
COMMENT ON COLUMN employees.gender IS '性別';
COMMENT ON COLUMN employees.postal_code IS '郵便番号';
COMMENT ON COLUMN employees.address IS '住所';
COMMENT ON COLUMN employees.bank_account_type IS '口座種別: 普通/当座';
-- ========================================
-- 2. 扶養家族情報 (Dependents)
-- ========================================
CREATE TABLE dependents (
dependent_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
relationship VARCHAR(20) NOT NULL, -- 配偶者/子/親/その他
birth_date DATE,
is_spouse BOOLEAN DEFAULT false,
is_disabled BOOLEAN DEFAULT false, -- 障害者控除対象
income_amount DECIMAL(12, 2) DEFAULT 0, -- 年間所得
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dependents IS '扶養家族情報';
COMMENT ON COLUMN dependents.relationship IS '続柄: 配偶者/子/親/その他';
COMMENT ON COLUMN dependents.is_disabled IS '障害者控除対象フラグ';
COMMENT ON COLUMN dependents.income_amount IS '年間所得額';
-- ========================================
-- 3. 給与基本設定 (Salary Settings)
-- ========================================
CREATE TABLE salary_settings (
setting_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id) ON DELETE CASCADE,
base_salary DECIMAL(12, 2) NOT NULL DEFAULT 0,
hourly_rate DECIMAL(10, 2),
employment_type VARCHAR(20) NOT NULL, -- 正社員/契約社員/パート/アルバイト
payment_type VARCHAR(20) NOT NULL, -- 月給/時給/日給
commute_allowance DECIMAL(12, 2) DEFAULT 0,
other_allowance DECIMAL(12, 2) DEFAULT 0,
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_employee_valid_from UNIQUE (employee_id, valid_from)
);
COMMENT ON TABLE salary_settings IS '給与基本設定';
COMMENT ON COLUMN salary_settings.employment_type IS '雇用形態: 正社員/契約社員/パート/アルバイト';
COMMENT ON COLUMN salary_settings.payment_type IS '支給形態: 月給/時給/日給';
-- ========================================
-- 4. 社会保険料率マスタ (Insurance Rates)
-- ========================================
CREATE TABLE insurance_rates (
rate_id SERIAL PRIMARY KEY,
rate_year INTEGER NOT NULL, -- 適用年度2026
rate_type VARCHAR(50) NOT NULL, -- 健康保険/介護保険/厚生年金/雇用保険/労災保険
calculation_target VARCHAR(20) NOT NULL, -- 給与/賞与
prefecture VARCHAR(50) DEFAULT '東京都', -- 事業所所在地(東京都/神奈川県/千葉県/埼玉県)
employee_rate DECIMAL(6, 4) NOT NULL, -- 従業員負担率
employer_rate DECIMAL(6, 4) NOT NULL, -- 会社負担率
care_insurance_age_threshold INTEGER DEFAULT 40, -- 介護保険適用年齢下限
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_rate_year_type_target_prefecture UNIQUE (rate_year, rate_type, calculation_target, prefecture)
);
COMMENT ON TABLE insurance_rates IS '社会保険料率マスタ';
COMMENT ON COLUMN insurance_rates.rate_year IS '適用年度';
COMMENT ON COLUMN insurance_rates.rate_type IS '保険種類: 健康保険/介護保険/厚生年金/雇用保険/労災保険';
COMMENT ON COLUMN insurance_rates.calculation_target IS '計算対象: 給与/賞与';
COMMENT ON COLUMN insurance_rates.prefecture IS '事業所所在地';
COMMENT ON COLUMN insurance_rates.employee_rate IS '従業員負担率';
COMMENT ON COLUMN insurance_rates.employer_rate IS '会社負担率';
COMMENT ON COLUMN insurance_rates.care_insurance_age_threshold IS '介護保険適用年齢下限(この年齢以上が対象)';
-- ========================================
-- 4-2. 子ども・子育て拠出金率 (Child Support Contribution Rates)
-- ========================================
CREATE TABLE child_support_contribution_rates (
contribution_id SERIAL PRIMARY KEY,
rate_year INTEGER NOT NULL, -- 適用年度
income_threshold DECIMAL(12, 2) NOT NULL, -- 所得基準額(標準報酬月額)
contribution_rate DECIMAL(6, 4) NOT NULL, -- 拠出金率(事業主負担のみ)
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_contribution_year UNIQUE (rate_year)
);
COMMENT ON TABLE child_support_contribution_rates IS '子ども・子育て拠出金率';
COMMENT ON COLUMN child_support_contribution_rates.rate_year IS '適用年度';
COMMENT ON COLUMN child_support_contribution_rates.income_threshold IS '所得基準額(標準報酬月額の上限)';
COMMENT ON COLUMN child_support_contribution_rates.contribution_rate IS '拠出金率(事業主のみ負担)';
-- ========================================
-- 5. 社会保険料上限設定 (Insurance Limit Settings)
-- ========================================
CREATE TABLE insurance_limit_settings (
limit_id SERIAL PRIMARY KEY,
setting_type VARCHAR(50) NOT NULL, -- 健康保険標準報酬月額上限/厚生年金標準報酬月額上限/通勤手当非課税限度額
limit_amount DECIMAL(12, 2) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE insurance_limit_settings IS '社会保険料上限設定';
COMMENT ON COLUMN insurance_limit_settings.setting_type IS '設定種別: 健康保険標準報酬月額上限/厚生年金標準報酬月額上限/通勤手当非課税限度額';
COMMENT ON COLUMN insurance_limit_settings.limit_amount IS '上限額';
-- ========================================
-- 6. 扶養家族判定条件 (Dependent Rules)
-- ========================================
CREATE TABLE dependent_rules (
rule_id SERIAL PRIMARY KEY,
rule_type VARCHAR(50) NOT NULL, -- 所得限度額/年齢条件/同一生計
rule_value VARCHAR(100) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dependent_rules IS '扶養家族判定条件';
COMMENT ON COLUMN dependent_rules.rule_type IS 'ルール種別: 所得限度額/年齢条件/同一生計';
-- ========================================
-- 7. 所得税率表 (Income Tax Table)
-- ========================================
CREATE TABLE income_tax_table (
tax_id SERIAL PRIMARY KEY,
monthly_income_from DECIMAL(12, 2) NOT NULL,
monthly_income_to DECIMAL(12, 2) NOT NULL,
dependents_count INTEGER NOT NULL DEFAULT 0, -- 扶養家族人数
tax_amount DECIMAL(10, 2) NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE income_tax_table IS '源泉所得税額表';
COMMENT ON COLUMN income_tax_table.dependents_count IS '扶養家族人数';
-- ========================================
-- 8. 月次給与計算 (Monthly Payroll)
-- ========================================
CREATE TABLE monthly_payroll (
payroll_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id),
payroll_year INTEGER NOT NULL,
payroll_month INTEGER NOT NULL,
payment_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'draft', -- draft/calculated/approved/paid
-- 勤怠情報
working_days DECIMAL(5, 2) DEFAULT 0,
working_hours DECIMAL(6, 2) DEFAULT 0,
overtime_hours DECIMAL(6, 2) DEFAULT 0,
holiday_hours DECIMAL(6, 2) DEFAULT 0,
late_hours DECIMAL(6, 2) DEFAULT 0,
absent_days DECIMAL(5, 2) DEFAULT 0,
-- 支給項目
base_salary DECIMAL(12, 2) DEFAULT 0,
overtime_pay DECIMAL(12, 2) DEFAULT 0,
holiday_pay DECIMAL(12, 2) DEFAULT 0,
commute_allowance DECIMAL(12, 2) DEFAULT 0,
other_allowance DECIMAL(12, 2) DEFAULT 0,
total_payment DECIMAL(12, 2) DEFAULT 0,
-- 控除項目
health_insurance DECIMAL(12, 2) DEFAULT 0,
care_insurance DECIMAL(12, 2) DEFAULT 0, -- 介護保険40歳以上65歳未満
pension_insurance DECIMAL(12, 2) DEFAULT 0,
employment_insurance DECIMAL(12, 2) DEFAULT 0,
income_tax DECIMAL(12, 2) DEFAULT 0,
resident_tax DECIMAL(12, 2) DEFAULT 0,
other_deduction DECIMAL(12, 2) DEFAULT 0,
total_deduction DECIMAL(12, 2) DEFAULT 0,
-- 差引支給額
net_payment DECIMAL(12, 2) DEFAULT 0,
notes TEXT,
calculated_at TIMESTAMP,
calculated_by VARCHAR(100),
approved_at TIMESTAMP,
approved_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_payroll_employee_month UNIQUE (employee_id, payroll_year, payroll_month)
);
COMMENT ON TABLE monthly_payroll IS '月次給与計算';
COMMENT ON COLUMN monthly_payroll.status IS 'ステータス: draft/calculated/approved/paid';
COMMENT ON COLUMN monthly_payroll.care_insurance IS '介護保険料40歳以上65歳未満対象';
-- ========================================
-- 9. 賞与計算 (Bonus Payments)
-- ========================================
CREATE TABLE bonus_payments (
bonus_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL REFERENCES employees(employee_id),
bonus_year INTEGER NOT NULL,
bonus_month INTEGER NOT NULL,
bonus_type VARCHAR(50) NOT NULL, -- 夏季賞与/冬季賞与/決算賞与/その他
payment_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'draft', -- draft/calculated/approved/paid
-- 支給項目
base_bonus DECIMAL(12, 2) DEFAULT 0, -- 基本賞与額
performance_bonus DECIMAL(12, 2) DEFAULT 0, -- 業績賞与
other_bonus DECIMAL(12, 2) DEFAULT 0, -- その他賞与
total_bonus DECIMAL(12, 2) DEFAULT 0, -- 総支給額
-- 控除項目
health_insurance DECIMAL(12, 2) DEFAULT 0,
care_insurance DECIMAL(12, 2) DEFAULT 0, -- 介護保険40歳以上65歳未満
pension_insurance DECIMAL(12, 2) DEFAULT 0,
employment_insurance DECIMAL(12, 2) DEFAULT 0,
income_tax DECIMAL(12, 2) DEFAULT 0, -- 賞与所得税
other_deduction DECIMAL(12, 2) DEFAULT 0,
total_deduction DECIMAL(12, 2) DEFAULT 0,
-- 差引支給額
net_bonus DECIMAL(12, 2) DEFAULT 0,
notes TEXT,
calculated_at TIMESTAMP,
calculated_by VARCHAR(100),
approved_at TIMESTAMP,
approved_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_bonus_employee_payment UNIQUE (employee_id, bonus_year, bonus_month, bonus_type)
);
COMMENT ON TABLE bonus_payments IS '賞与計算';
COMMENT ON COLUMN bonus_payments.bonus_type IS '賞与種別: 夏季賞与/冬季賞与/決算賞与/その他';
COMMENT ON COLUMN bonus_payments.status IS 'ステータス: draft/calculated/approved/paid';
-- ========================================
-- 10. 給与明細項目詳細 (Payroll Detail Items)
-- ========================================
CREATE TABLE payroll_details (
detail_id SERIAL PRIMARY KEY,
payroll_id INTEGER NOT NULL REFERENCES monthly_payroll(payroll_id) ON DELETE CASCADE,
item_type VARCHAR(20) NOT NULL, -- payment/deduction
item_code VARCHAR(50) NOT NULL,
item_name VARCHAR(100) NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE payroll_details IS '給与明細項目詳細';
COMMENT ON COLUMN payroll_details.item_type IS '項目区分: payment/deduction';
-- ========================================
-- インデックス作成
-- ========================================
CREATE INDEX idx_employees_code ON employees(employee_code);
CREATE INDEX idx_employees_active ON employees(is_active);
CREATE INDEX idx_dependents_employee ON dependents(employee_id);
CREATE INDEX idx_salary_settings_employee ON salary_settings(employee_id);
CREATE INDEX idx_salary_settings_valid ON salary_settings(valid_from, valid_to);
CREATE INDEX idx_insurance_rates_year ON insurance_rates(rate_year, rate_type, calculation_target);
CREATE INDEX idx_child_support_year ON child_support_contribution_rates(rate_year);
CREATE INDEX idx_insurance_limits_valid ON insurance_limit_settings(valid_from, valid_to);
CREATE INDEX idx_dependent_rules_valid ON dependent_rules(valid_from, valid_to);
CREATE INDEX idx_income_tax_valid ON income_tax_table(valid_from, valid_to);
CREATE INDEX idx_payroll_employee_year_month ON monthly_payroll(employee_id, payroll_year, payroll_month);
CREATE INDEX idx_payroll_status ON monthly_payroll(status);
CREATE INDEX idx_bonus_employee_year ON bonus_payments(employee_id, bonus_year);
CREATE INDEX idx_bonus_status ON bonus_payments(status);
CREATE INDEX idx_payroll_details_payroll ON payroll_details(payroll_id);
-- ========================================
-- トリガー関数: 更新日時の自動更新
-- ========================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ========================================
-- トリガー設定
-- ========================================
CREATE TRIGGER trg_employees_updated_at BEFORE UPDATE ON employees
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_dependents_updated_at BEFORE UPDATE ON dependents
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_salary_settings_updated_at BEFORE UPDATE ON salary_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_insurance_rates_updated_at BEFORE UPDATE ON insurance_rates
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_child_support_updated_at BEFORE UPDATE ON child_support_contribution_rates
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_insurance_limits_updated_at BEFORE UPDATE ON insurance_limit_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_monthly_payroll_updated_at BEFORE UPDATE ON monthly_payroll
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_bonus_payments_updated_at BEFORE UPDATE ON bonus_payments
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ========================================
-- 初期データ: 社会保険料率 (2026年度) - 給与計算用
-- ========================================
-- 東京都の健康保険料率(給与)
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
(2026, '健康保険', '給与', '東京都', 0.0500, 0.0500, 40, '健康保険料率 10.00% (労使折半) - 給与'),
(2026, '介護保険', '給与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 給与'),
(2026, '厚生年金', '給与', '東京都', 0.0915, 0.0915, NULL, '厚生年金保険料率 18.30% (労使折半) - 給与'),
(2026, '雇用保険', '給与', '東京都', 0.0060, 0.0095, NULL, '雇用保険料率 従業員0.6% 事業主0.95% - 給与'),
(2026, '労災保険', '給与', '東京都', 0.0000, 0.0030, NULL, '労災保険料率 事業主のみ0.3% - 給与');
-- 東京都の健康保険料率(賞与)
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
(2026, '健康保険', '賞与', '東京都', 0.0500, 0.0500, 40, '健康保険料率 10.00% (労使折半) - 賞与'),
(2026, '介護保険', '賞与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 賞与'),
(2026, '厚生年金', '賞与', '東京都', 0.0915, 0.0915, NULL, '厚生年金保険料率 18.30% (労使折半) - 賞与'),
(2026, '雇用保険', '賞与', '東京都', 0.0060, 0.0095, NULL, '雇用保険料率 従業員0.6% 事業主0.95% - 賞与');
-- ========================================
-- 初期データ: 子ども・子育て拠出金率 (2026年度)
-- ========================================
INSERT INTO child_support_contribution_rates (rate_year, income_threshold, contribution_rate, notes) VALUES
(2026, 1500000, 0.0036, '子ども・子育て拠出金率 0.36% (事業主のみ負担、標準報酬月額150万円まで)');
-- ========================================
-- 初期データ: 社会保険料上限設定 (2026年度)
-- ========================================
INSERT INTO insurance_limit_settings (setting_type, limit_amount, valid_from, notes) VALUES
('健康保険標準報酬月額上限', 1390000, '2026-01-01', '健康保険の標準報酬月額上限第50級'),
('厚生年金標準報酬月額上限', 650000, '2026-01-01', '厚生年金保険の標準報酬月額上限第32級'),
('通勤手当非課税限度額', 150000, '2026-01-01', '通勤手当の非課税限度額(月額)');
-- ========================================
-- 初期データ: 扶養家族判定条件 (2026年度)
-- ========================================
INSERT INTO dependent_rules (rule_type, rule_value, valid_from, description) VALUES
('所得限度額_一般', '480000', '2026-01-01', '一般扶養親族の所得限度額年間48万円'),
('所得限度額_配偶者', '480000', '2026-01-01', '配偶者の所得限度額年間48万円'),
('所得限度額_配偶者特別', '1330000', '2026-01-01', '配偶者特別控除の所得限度額年間133万円'),
('年齢_特定扶養親族', '19-23', '2026-01-01', '特定扶養親族の年齢範囲19歳以上23歳未満'),
('年齢_老人扶養親族', '70-', '2026-01-01', '老人扶養親族の年齢70歳以上');
-- ========================================
-- スキーマ作成完了
-- ========================================
-- 確認コマンド:
-- \dt -- テーブル一覧
-- \d employees -- 従業員テーブル構造
-- \d monthly_payroll -- 月次給与テーブル構造
-- \d bonus_payments -- 賞与テーブル構造
--
-- SELECT * FROM insurance_rates; -- 保険料率確認
-- SELECT * FROM insurance_limit_settings; -- 保険上限確認
-- SELECT * FROM dependent_rules; -- 扶養判定条件確認

View File

@@ -0,0 +1,79 @@
import os
import psycopg2
from dotenv import load_dotenv
# Load environment variables
root_env = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
load_dotenv(root_env)
# Connect to database
conn = psycopg2.connect(
host=os.getenv('DB_HOST', '192.168.0.61'),
port=os.getenv('DB_PORT', '55432'),
dbname=os.getenv('DB_NAME', 'njts_acct'),
user=os.getenv('DB_USER', 'njts_app'),
password=os.getenv('DB_PASSWORD')
)
cur = conn.cursor()
print("保険率カラムを DECIMAL(5,3) に変更中...")
# Alter insurance_rates table
cur.execute("""
ALTER TABLE insurance_rates
ALTER COLUMN employee_rate TYPE DECIMAL(5, 3),
ALTER COLUMN employer_rate TYPE DECIMAL(5, 3)
""")
# Alter child_support_contribution_rates table
cur.execute("""
ALTER TABLE child_support_contribution_rates
ALTER COLUMN contribution_rate TYPE DECIMAL(5, 3)
""")
conn.commit()
print("OK カラム変更完了")
# Verify changes
cur.execute("""
SELECT column_name, data_type, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='insurance_rates'
AND column_name IN ('employee_rate', 'employer_rate')
ORDER BY column_name
""")
print("\n変更後の insurance_rates カラム:")
for row in cur.fetchall():
print(f" {row[0]}: {row[1]}({row[2]},{row[3]})")
cur.execute("""
SELECT column_name, data_type, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name='child_support_contribution_rates'
AND column_name = 'contribution_rate'
""")
print("\n変更後の child_support_contribution_rates カラム:")
for row in cur.fetchall():
print(f" {row[0]}: {row[1]}({row[2]},{row[3]})")
# Check data
cur.execute("""
SELECT rate_year, rate_type, calculation_target, prefecture,
employee_rate, employer_rate
FROM insurance_rates
WHERE rate_year = 2025 AND rate_type = '健康保険'
LIMIT 3
""")
print("\n実際のデータ (2025年健康保険):")
for row in cur.fetchall():
print(f" {row[0]} {row[1]} {row[2]} {row[3]}: 従業員={float(row[4]):.3f} 会社={float(row[5]):.3f}")
cur.close()
conn.close()
print("\n完了!")

View File

@@ -0,0 +1,274 @@
# 工资系统增强功能说明
## 已实现的三大改进
### 1. 扶养人数输入和判定 ✓
#### 功能说明
- **扶养家族管理**: 已有的 `dependents` 表允许录入扶养家族信息
- **自动计算扶养人数**: 系统自动统计有效期内的扶养人数
- **扶养判定规则表**: 新增 `dependent_rules` 表,可配置扶养条件
#### API 端点
- `POST /payroll/employees/{employee_id}/dependents` - 添加扶养家族
- `GET /payroll/employees/{employee_id}/dependents` - 查看扶养家族列表
- `GET /payroll/settings/dependent-rules` - 查看扶养判定规则
- `POST /payroll/settings/dependent-rules` - 设置扶养判定规则
#### 扶养判定条件(可配置)
```
- 所得限度額_一般: 480,000円/年
- 所得限度額_配偶者: 480,000円/年
- 所得限度額_配偶者特別: 1,330,000円/年
- 年齢_特定扶養親族: 19-23歳
- 年齢_老人扶養親族: 70歳以上
```
---
### 2. 奖金计算 + 社保上限设置 ✓
#### 社会保险料上限表 (`insurance_limit_settings`)
可通过画面手动修改的上限值:
| 设定类型 | 2026 年初始值 | 说明 |
| ------------------------ | ------------- | -------------------------------- |
| 健康保険標準報酬月額上限 | 1,390,000 円 | 健康保险计算基数上限(第 50 级) |
| 厚生年金標準報酬月額上限 | 650,000 円 | 厚生年金计算基数上限(第 32 级) |
| 通勤手当非課税限度額 | 150,000 円 | 通勤补贴免税上限 |
#### API 端点
**社保上限管理**:
- `GET /payroll/settings/insurance-limits` - 获取当前社保上限
- `POST /payroll/settings/insurance-limits` - 创建新的上限设置
- `PUT /payroll/settings/insurance-limits/{limit_id}` - 修改上限值
**奖金计算**:
- `POST /payroll/bonus/calculate` - 计算奖金
- `GET /payroll/bonus/` - 获取奖金列表
- `GET /payroll/bonus/{bonus_id}` - 获取奖金详情
- `POST /payroll/bonus/{bonus_id}/approve` - 审批奖金
#### 奖金计算逻辑
```python
总支给额 = 基本奖金 + 业绩奖金 + 其他奖金
# 应用社保上限
健康保险 = min(总支给额, 健康保险上限) × 健康保险率
厚生年金 = min(总支给额, 厚生年金上限) × 厚生年金率
介护保险 = min(总支给额, 健康保险上限) × 介护保险率 (40-64)
雇用保险 = 总支给额 × 雇用保险率
# 奖金所得税(特殊算法)
所得税 = (总支给额 - 社保) × 根据前3月平均工资和扶养人数确定的税率
差引支给额 = 总支给额 - 总控除额
```
#### 奖金类型
- 夏季賞与(夏季奖金)
- 冬季賞与(冬季奖金)
- 決算賞与(结算奖金)
- その他(其他)
---
### 3. 源泉徴収税額表上传功能 ✓
#### 功能说明
- **CSV 上传**: 可通过 API 上传源泉徴收税额表的 CSV 文件
- **年份区分**: 每条税率记录有 `valid_from``valid_to` 字段区分年份
- **自动应用**: 计算工资时自动使用对应年份的税额表
#### API 端点
- `POST /payroll/settings/income-tax/import-csv` - 上传税额表 CSV
- `GET /payroll/settings/income-tax` - 查看当前税额表
- `GET /payroll/settings/income-tax/calculate` - 测试税额计算
#### CSV 格式示例
```csv
monthly_income_from,monthly_income_to,dependents_count,tax_amount,valid_from
0,88000,0,0,2026-01-01
88000,89000,0,130,2026-01-01
89000,90000,0,200,2026-01-01
```
**字段说明**:
- `monthly_income_from`: 月收入下限
- `monthly_income_to`: 月收入上限
- `dependents_count`: 扶养人数 (0, 1, 2, 3...)
- `tax_amount`: 源泉徴収税额
- `valid_from`: 适用开始日期
#### 使用流程
1. 准备 CSV 文件(按国税厅公布的源泉徴収税額表格式)
2. 调用上传 API: `POST /payroll/settings/income-tax/import-csv`
3. 系统自动应用新税额表进行计算
---
## 数据库变更
### 新增表
1. **insurance_limit_settings** - 社保上限设置表
```sql
- limit_id ()
- setting_type ()
- limit_amount ()
- valid_from/valid_to ()
```
2. **bonus_payments** - 奖金计算表
```sql
- bonus_id ()
- employee_id (ID)
- bonus_year/bonus_month ()
- bonus_type ()
- base_bonus/performance_bonus/other_bonus ()
- health_insurance/care_insurance/pension_insurance... ()
- total_bonus/total_deduction/net_bonus ()
```
3. **dependent_rules** - 扶养判定条件表
```sql
- rule_id ()
- rule_type ()
- rule_value ()
- valid_from/valid_to ()
- description ()
```
### 应用数据库变更
```bash
# 1. 应用增强功能schema
psql -U your_user -d your_database -f backend/sql/payroll_enhancements.sql
# 2. 验证表是否创建成功
psql -U your_user -d your_database -c "\dt *bonus* *insurance_limit* *dependent_rule*"
```
---
## 计算逻辑改进总结
### 月度工资计算
1. ✅ 应用健康保险上限 (1,390,000 円)
2. ✅ 应用厚生年金上限 (650,000 円)
3. ✅ 应用通勤补贴免税限度 (可配置)
4. ✅ 40-64 岁自动计算介护保险
5. ✅ 自动统计扶养人数用于所得税计算
### 奖金计算
1. ✅ 应用社保上限(与工资计算一致)
2. ✅ 特殊的奖金所得税算法
3. ✅ 支持多种奖金类型
4. ✅ 基于前 3 月平均工资计算税率
---
## 前端 TODO建议
需要创建以下管理画面:
1. **社保上限设置画面**
- 显示当前上限值
- 允许修改上限金额
- 查看历史变更记录
2. **扶养判定规则画面**
- 显示当前判定条件
- 允许修改所得限度额
- 配置年龄条件
3. **税额表上传画面**
- CSV 文件上传组件
- 预览上传内容
- 查看当前税额表
4. **奖金计算画面**
- 选择员工
- 输入奖金金额
- 显示计算结果
- 审批流程
---
## API 测试示例
### 1. 设置社保上限
```bash
curl -X POST http://localhost:18080/payroll/settings/insurance-limits \
-H "Content-Type: application/json" \
-d '{
"setting_type": "健康保険標準報酬月額上限",
"limit_amount": 1390000,
"valid_from": "2026-01-01",
"notes": "2026年度上限"
}'
```
### 2. 上传税额表 CSV
```bash
curl -X POST http://localhost:18080/payroll/settings/income-tax/import-csv \
-F "file=@tax_table_2026.csv" \
-F "valid_from=2026-01-01"
```
### 3. 计算奖金
```bash
curl -X POST http://localhost:18080/payroll/bonus/calculate \
-H "Content-Type: application/json" \
-d '{
"employee_id": 1,
"bonus_year": 2026,
"bonus_month": 6,
"bonus_type": "夏季賞与",
"payment_date": "2026-06-30",
"base_bonus": 500000,
"performance_bonus": 100000,
"calculated_by": "admin"
}'
```
### 4. 查询扶养判定规则
```bash
curl http://localhost:18080/payroll/settings/dependent-rules
```
---
## 总结
**问题 1**: 扶养人数输入和判定 - 已实现,可配置判定条件
**问题 2**: 奖金计算 + 社保上限 - 已实现,上限可手动修改
**问题 3**: 税额表上传 - 已实现,支持 CSV 上传和年份区分
所有功能都已实现 API 层,可以通过 RESTful 接口调用。下一步建议创建前端管理画面以便更方便地操作这些功能。

171
docs/payroll_quickstart.md Normal file
View File

@@ -0,0 +1,171 @@
# 給与システム - クイックスタートガイド
## セットアップ手順
### 1. データベーススキーマの適用
```powershell
# PostgreSQLに接続環境に応じて認証情報を変更
$env:PGPASSWORD = "your_password"
psql -U your_user -d your_database -f backend/sql/payroll_schema.sql
```
または、既存のデータベース接続を使用している場合:
```powershell
# Pythonスクリプトで実行する場合
cd backend
python -c "
from app.core.database import get_connection
with open('sql/payroll_schema.sql', 'r', encoding='utf-8') as f:
sql = f.read()
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(sql)
conn.commit()
print('給与システムのスキーマを適用しました')
"
```
### 2. サーバーの起動
```powershell
cd backend
uvicorn app.main:app --reload
```
### 3. ブラウザでアクセス
http://localhost:8000/static/payroll.html
## 初期データのセットアップ
### 1. 保険料率の確認
システムには以下の初期保険料率が登録されています2026 年度):
- 健康保険: 10.00% (労使折半 各 5.00%)
- 厚生年金: 18.30% (労使折半 各 9.15%)
- 雇用保険: 1.55% (従業員 0.6% + 事業主 0.95%)
- 労災保険: 0.30% (事業主のみ)
必要に応じて「税率・保険料設定」画面で更新してください。
### 2. 所得税率表のインポート
サンプル CSV ファイルが `backend/income_tax_sample.csv` に用意されています。
1. 「税率・保険料設定」画面を開く
2. 「所得税率表」タブを選択
3. 「CSV インポート」ボタンをクリック
4. `income_tax_sample.csv` を選択
5. 適用開始日を入力(例: 2026-01-01
### 3. 従業員の登録
1. 「従業員管理」画面を開く
2. 「新規登録」タブを選択
3. 必要な情報を入力:
- 従業員コード(必須)
- 氏名(必須)
- 入社日(必須)
- その他の情報(任意)
4. 「登録」ボタンをクリック
### 4. 扶養家族の登録(必要な場合)
1. 「従業員管理」画面で従業員を選択
2. 従業員詳細画面の「扶養家族」セクションで追加
### 5. 給与設定の登録
1. 「給与設定」画面を開く
2. 従業員を選択
3. 「新規設定追加」ボタンをクリック
4. 給与情報を入力:
- 基本給(必須)
- 雇用形態(必須)
- 支給形態(必須)
- 通勤手当(任意)
- 適用開始日(必須)
5. 「保存」ボタンをクリック
## 給与計算の実行
### 月次給与計算の手順
1. 「月次給与計算」画面を開く
2. 「新規計算」ボタンをクリック
3. 給与計算情報を入力:
- 従業員を選択
- 対象年月
- 支給日
- 勤怠情報(出勤日数、勤務時間、残業時間など)
- その他(住民税、その他控除など)
4. 「計算実行」ボタンをクリック
5. 給与明細を確認
6. 問題なければ「承認」ボタンをクリック
### 給与明細の見方
**支給項目**
- 基本給: 設定された基本給(欠勤がある場合は控除後)
- 残業手当: 残業時間 × 時給 × 1.25
- 休日手当: 休日出勤時間 × 時給 × 1.35
- 通勤手当: 設定された通勤手当
- その他手当: 入力されたその他手当
- **総支給額**: 上記の合計
**控除項目**
- 健康保険: 総支給額 × 5.00%
- 厚生年金: 総支給額 × 9.15%
- 雇用保険: 総支給額 × 0.60%
- 所得税: 所得税率表に基づいて計算
- 住民税: 入力された金額
- その他控除: 入力された金額
- **総控除額**: 上記の合計
**差引支給額** = 総支給額 - 総控除額
## よくある質問
### Q1: 給与を再計算するには?
A: 給与明細画面で「再計算」ボタンをクリックしてください。ただし、承認済みの給与は再計算できません。
### Q2: 承認済みの給与を修正するには?
A: 承認済みの給与は変更できません。給与を削除して新規に計算し直す必要があります。
### Q3: 時給制の従業員の給与計算は?
A: 給与設定で「支給形態: 時給」を選択し、時給を入力してください。基本給は「時給 × 勤務時間」で自動計算されます。
### Q4: 所得税が 0 円になる
A: 所得税率表が登録されていない可能性があります。「税率・保険料設定」画面で所得税率表を確認してください。
### Q5: 扶養家族の人数が所得税に反映されない
A: 扶養家族の「有効期間」を確認してください。計算対象月に有効な扶養家族のみがカウントされます。
## トラブルシューティング
### エラー: 「給与設定が見つかりません」
→ 「給与設定」画面で該当従業員の給与設定を登録してください
### エラー: 「保険料率が見つかりません」
→ 「税率・保険料設定」画面で保険料率を確認・登録してください
### 所得税が正しく計算されない
→ 所得税率表が正しくインポートされているか確認してください
→ 扶養家族の有効期間が正しく設定されているか確認してください
## サポート
詳細なドキュメントは `docs/payroll_system.md` を参照してください。

221
docs/payroll_system.md Normal file
View File

@@ -0,0 +1,221 @@
# 給与管理システム (Payroll Management System)
会計システムに統合された給与管理モジュールです。
## 機能概要
### 1. 従業員管理
- 従業員の基本情報管理
- 扶養家族情報の登録・管理
- 銀行口座情報の管理
### 2. 給与設定
- 従業員ごとの給与基本設定
- 基本給、時給、各種手当の設定
- 設定履歴の管理
### 3. 月次給与計算
- 勤怠情報の入力
- 給与の自動計算
- 基本給
- 残業手当(時給 × 1.25
- 休日手当(時給 × 1.35
- 社会保険料(健康保険、厚生年金、雇用保険)
- 所得税(扶養人数を考慮)
- 住民税
- 給与明細の表示
- 給与の承認機能
### 4. 税率・保険料管理
- 社会保険料率の管理
- 健康保険
- 厚生年金
- 雇用保険
- 労災保険
- 所得税率表の管理
- CSV インポート機能
## セットアップ
### 1. データベーススキーマの適用
```bash
# PostgreSQLに接続
psql -U your_user -d your_database
# スキーマを適用
\i backend/sql/payroll_schema.sql
```
### 2. サーバーの起動
```bash
cd backend
uvicorn app.main:app --reload
```
### 3. フロントエンドへのアクセス
ブラウザで以下の URL にアクセス:
- メインメニュー: http://localhost:8000/static/payroll.html
- 従業員管理: http://localhost:8000/static/payroll-employees.html
- 給与設定: http://localhost:8000/static/payroll-salary-settings.html
- 月次給与計算: http://localhost:8000/static/payroll-calculation.html
- 税率・保険料設定: http://localhost:8000/static/payroll-settings.html
## API エンドポイント
### 従業員管理
- `POST /payroll/employees/` - 従業員の作成
- `GET /payroll/employees/` - 従業員一覧の取得
- `GET /payroll/employees/{employee_id}` - 従業員詳細の取得
- `PUT /payroll/employees/{employee_id}` - 従業員情報の更新
- `DELETE /payroll/employees/{employee_id}` - 従業員の削除
### 扶養家族管理
- `POST /payroll/employees/{employee_id}/dependents` - 扶養家族の追加
- `GET /payroll/employees/{employee_id}/dependents` - 扶養家族一覧の取得
- `PUT /payroll/dependents/{dependent_id}` - 扶養家族情報の更新
- `DELETE /payroll/dependents/{dependent_id}` - 扶養家族の削除
### 給与設定
- `POST /payroll/settings/salary` - 給与設定の作成
- `GET /payroll/settings/salary/{employee_id}` - 給与設定履歴の取得
- `GET /payroll/settings/salary/{employee_id}/current` - 現在の給与設定の取得
- `PUT /payroll/settings/salary/{setting_id}` - 給与設定の更新
### 保険料率管理
- `POST /payroll/settings/insurance-rates` - 保険料率の作成
- `GET /payroll/settings/insurance-rates` - 保険料率一覧の取得
- `GET /payroll/settings/insurance-rates/{rate_type}` - 特定保険料率の取得
- `PUT /payroll/settings/insurance-rates/{rate_id}` - 保険料率の更新
### 所得税率表管理
- `POST /payroll/settings/income-tax` - 所得税率表の作成
- `POST /payroll/settings/income-tax/import` - CSV インポート
- `GET /payroll/settings/income-tax` - 所得税率表の取得
- `GET /payroll/settings/income-tax/calculate` - 所得税の計算
### 月次給与計算
- `POST /payroll/calculation/calculate` - 給与の計算
- `GET /payroll/calculation/` - 給与計算一覧の取得
- `GET /payroll/calculation/{payroll_id}` - 給与明細の取得
- `PUT /payroll/calculation/{payroll_id}` - 給与データの更新
- `POST /payroll/calculation/{payroll_id}/recalculate` - 給与の再計算
- `POST /payroll/calculation/{payroll_id}/approve` - 給与の承認
- `DELETE /payroll/calculation/{payroll_id}` - 給与データの削除
## 使い方
### 初回セットアップフロー
1. **保険料率の確認**
- 税率・保険料設定画面で、初期登録された保険料率を確認
- 必要に応じて更新
2. **所得税率表のインポート**
- CSV ファイルを準備(フォーマット: 月収 From,月収 To,扶養人数,税額)
- 税率・保険料設定画面で CSV インポート
3. **従業員の登録**
- 従業員管理画面で従業員を登録
- 必要に応じて扶養家族情報を追加
4. **給与設定**
- 給与設定画面で各従業員の給与設定を登録
- 基本給、手当などを設定
### 月次給与計算フロー
1. **勤怠情報の入力**
- 月次給与計算画面で「新規計算」をクリック
- 従業員を選択
- 出勤日数、勤務時間、残業時間などを入力
2. **給与の計算**
- 「計算実行」ボタンをクリック
- システムが自動的に給与を計算
3. **給与明細の確認**
- 計算結果を確認
- 必要に応じて「再計算」
4. **承認**
- 内容を確認後、「承認」ボタンをクリック
- 承認後は変更不可
## データベーススキーマ
### 主要テーブル
1. **employees** - 従業員マスタ
2. **dependents** - 扶養家族情報
3. **salary_settings** - 給与基本設定
4. **insurance_rates** - 社会保険料率マスタ
5. **income_tax_table** - 所得税率表
6. **monthly_payroll** - 月次給与計算ヘッダー
7. **payroll_details** - 給与明細項目詳細
## 計算ロジック
### 基本給の計算
- 月給制: 設定された基本給から欠勤日数を控除
- 時給制: 時給 × 勤務時間
### 残業手当
- 時給 × 残業時間 × 1.25
### 休日手当
- 時給 × 休日出勤時間 × 1.35
### 社会保険料
- 健康保険: 総支給額 × 従業員負担率
- 厚生年金: 総支給額 × 従業員負担率
- 雇用保険: 総支給額 × 従業員負担率
### 所得税
1. 課税対象額 = 総支給額 - 通勤手当(非課税限度額まで) - 社会保険料
2. 扶養家族数を取得
3. 所得税率表から該当する税額を取得
### 差引支給額
- 総支給額 - 総控除額
## 注意事項
- 所得税率表は国税庁の「給与所得の源泉徴収税額表」に基づいて作成してください
- 社会保険料率は年度ごとに変更される可能性があるため、定期的な更新が必要です
- 承認済みの給与は変更・削除できません
- 扶養家族の情報は所得税計算に影響するため、正確に入力してください
## 今後の拡張機能
- 賞与計算機能
- 年末調整機能
- 給与明細の PDF 出力
- 銀行振込データの出力
- 給与台帳の出力
- 源泉徴収票の作成

View File

@@ -0,0 +1,605 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.filters {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.filters label {
margin-right: 15px;
}
.payroll-list {
margin-top: 20px;
}
.payroll-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.payroll-item:hover {
background: #f9f9f9;
}
.status-badge {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.status-draft {
background: #ffc107;
color: #000;
}
.status-calculated {
background: #17a2b8;
color: #fff;
}
.status-approved {
background: #28a745;
color: #fff;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.payroll-detail {
margin-top: 20px;
}
.detail-section {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #ddd;
}
.detail-row:last-child {
border-bottom: none;
}
.total-row {
font-weight: bold;
font-size: 1.2em;
color: #007bff;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 月次給与計算</h1>
<div class="filters">
<label>
対象年月:
<input
type="number"
id="filterYear"
style="width: 100px"
placeholder="年"
/>
</label>
<label>
<input
type="number"
id="filterMonth"
style="width: 80px"
min="1"
max="12"
placeholder="月"
/>
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">
新規計算
</button>
</div>
<div id="payrollList" class="payroll-list"></div>
<!-- 給与計算フォーム(モーダル風) -->
<div
id="calculateModal"
style="
display: none;
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 800px;
max-height: 90vh;
overflow-y: auto;
background: white;
padding: 30px;
border: 2px solid #007bff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
"
>
<h2>給与計算</h2>
<form id="calculateForm" onsubmit="calculatePayroll(event)">
<div class="form-group">
<label>従業員 *</label>
<select name="employee_id" id="employeeSelect" required>
<option value="">選択してください</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>対象年 *</label>
<input type="number" name="payroll_year" required />
</div>
<div class="form-group">
<label>対象月 *</label>
<input
type="number"
name="payroll_month"
min="1"
max="12"
required
/>
</div>
<div class="form-group">
<label>支給日 *</label>
<input type="date" name="payment_date" required />
</div>
</div>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="form-row">
<div class="form-group">
<label>出勤日数</label>
<input type="number" name="working_days" step="0.5" value="0" />
</div>
<div class="form-group">
<label>勤務時間</label>
<input
type="number"
name="working_hours"
step="0.5"
value="0"
/>
</div>
<div class="form-group">
<label>残業時間</label>
<input
type="number"
name="overtime_hours"
step="0.5"
value="0"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>休日出勤時間</label>
<input
type="number"
name="holiday_hours"
step="0.5"
value="0"
/>
</div>
<div class="form-group">
<label>遅刻時間</label>
<input type="number" name="late_hours" step="0.5" value="0" />
</div>
<div class="form-group">
<label>欠勤日数</label>
<input type="number" name="absent_days" step="0.5" value="0" />
</div>
</div>
</div>
<div class="detail-section">
<h3>その他</h3>
<div class="form-row">
<div class="form-group">
<label>その他手当</label>
<input
type="number"
name="other_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>住民税</label>
<input
type="number"
name="resident_tax"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>その他控除</label>
<input
type="number"
name="other_deduction"
step="0.01"
value="0"
/>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">計算実行</button>
<button
type="button"
class="btn btn-secondary"
onclick="closeCalculateForm()"
>
キャンセル
</button>
</form>
</div>
<!-- 給与明細表示 -->
<div
id="payrollDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<script>
const API_BASE = "http://localhost:8000";
async function loadEmployees() {
const response = await fetch(
`${API_BASE}/payroll/employees/?is_active=true`
);
const employees = await response.json();
const select = document.getElementById("employeeSelect");
select.innerHTML =
'<option value="">選択してください</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
)
.join("");
}
async function loadPayrolls() {
const year = document.getElementById("filterYear").value;
const month = document.getElementById("filterMonth").value;
const params = new URLSearchParams();
if (year) params.append("payroll_year", year);
if (month) params.append("payroll_month", month);
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/?${params}`
);
const payrolls = await response.json();
const html = payrolls
.map(
(p) => `
<div class="payroll-item" onclick="viewPayroll(${
p.payroll_id
})">
<div>
<strong>${p.payroll_year}${
p.payroll_month
}月</strong>
<span class="status-badge status-${
p.status
}">${getStatusLabel(p.status)}</span>
</div>
<div>
従業員ID: ${p.employee_id} | 支給日: ${
p.payment_date
}
</div>
<div>
<strong>差引支給額: ¥${Number(
p.net_payment
).toLocaleString()}</strong>
(総支給: ¥${Number(
p.total_payment
).toLocaleString()} - 控除: ¥${Number(
p.total_deduction
).toLocaleString()})
</div>
</div>
`
)
.join("");
document.getElementById("payrollList").innerHTML =
html || "<p>給与データがありません</p>";
} catch (error) {
alert("給与一覧の取得に失敗しました");
console.error(error);
}
}
function getStatusLabel(status) {
const labels = {
draft: "下書き",
calculated: "計算済み",
approved: "承認済み",
paid: "支払済み",
};
return labels[status] || status;
}
function showCalculateForm() {
document.getElementById("calculateModal").style.display = "block";
loadEmployees();
// デフォルト値設定
const now = new Date();
document.querySelector('[name="payroll_year"]').value =
now.getFullYear();
document.querySelector('[name="payroll_month"]').value =
now.getMonth() + 1;
}
function closeCalculateForm() {
document.getElementById("calculateModal").style.display = "none";
document.getElementById("calculateForm").reset();
}
async function calculatePayroll(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
data[key] = isNaN(value) || value === "" ? value : Number(value);
});
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/calculate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (response.ok) {
const result = await response.json();
alert("給与計算が完了しました");
closeCalculateForm();
viewPayroll(result.payroll_id);
} else {
const error = await response.json();
alert("計算に失敗しました: " + error.detail);
}
} catch (error) {
alert("計算に失敗しました");
console.error(error);
}
}
async function viewPayroll(payrollId) {
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}`
);
const payroll = await response.json();
const html = `
<h2>${payroll.payroll_year}${
payroll.payroll_month
}月 給与明細</h2>
<p>従業員ID: ${payroll.employee_id} | 支給日: ${
payroll.payment_date
}</p>
<p>ステータス: <span class="status-badge status-${
payroll.status
}">${getStatusLabel(payroll.status)}</span></p>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="detail-row"><span>出勤日数:</span><span>${
payroll.working_days
}日</span></div>
<div class="detail-row"><span>勤務時間:</span><span>${
payroll.working_hours
}時間</span></div>
<div class="detail-row"><span>残業時間:</span><span>${
payroll.overtime_hours
}時間</span></div>
<div class="detail-row"><span>休日出勤:</span><span>${
payroll.holiday_hours
}時間</span></div>
<div class="detail-row"><span>欠勤日数:</span><span>${
payroll.absent_days
}日</span></div>
</div>
<div class="detail-section">
<h3>支給項目</h3>
<div class="detail-row"><span>基本給:</span><span>¥${Number(
payroll.base_salary
).toLocaleString()}</span></div>
<div class="detail-row"><span>残業手当:</span><span>¥${Number(
payroll.overtime_pay
).toLocaleString()}</span></div>
<div class="detail-row"><span>休日手当:</span><span>¥${Number(
payroll.holiday_pay
).toLocaleString()}</span></div>
<div class="detail-row"><span>通勤手当:</span><span>¥${Number(
payroll.commute_allowance
).toLocaleString()}</span></div>
<div class="detail-row"><span>その他手当:</span><span>¥${Number(
payroll.other_allowance
).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
payroll.total_payment
).toLocaleString()}</span></div>
</div>
<div class="detail-section">
<h3>控除項目</h3>
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
payroll.health_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
payroll.pension_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
payroll.employment_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(
payroll.income_tax
).toLocaleString()}</span></div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
payroll.resident_tax
).toLocaleString()}</span></div>
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
payroll.other_deduction
).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
payroll.total_deduction
).toLocaleString()}</span></div>
</div>
<div class="detail-section" style="background:#e7f3ff;">
<div class="detail-row total-row" style="font-size:1.5em;">
<span>差引支給額:</span>
<span>¥${Number(
payroll.net_payment
).toLocaleString()}</span>
</div>
</div>
${
payroll.status === "calculated"
? `
<button class="btn btn-success" onclick="approvePayroll(${payroll.payroll_id})">承認</button>
<button class="btn btn-primary" onclick="recalculatePayroll(${payroll.payroll_id})">再計算</button>
`
: ""
}
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
`;
document.getElementById("payrollDetail").innerHTML = html;
document.getElementById("payrollDetail").style.display = "block";
document
.getElementById("payrollDetail")
.scrollIntoView({ behavior: "smooth" });
} catch (error) {
alert("給与明細の取得に失敗しました");
console.error(error);
}
}
async function approvePayroll(payrollId) {
if (!confirm("この給与を承認しますか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/approve`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approved_by: "admin" }),
}
);
if (response.ok) {
alert("承認しました");
viewPayroll(payrollId);
loadPayrolls();
} else {
const error = await response.json();
alert("承認に失敗しました: " + error.detail);
}
} catch (error) {
alert("承認に失敗しました");
console.error(error);
}
}
async function recalculatePayroll(payrollId) {
if (!confirm("給与を再計算しますか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/recalculate`,
{
method: "POST",
}
);
if (response.ok) {
alert("再計算が完了しました");
viewPayroll(payrollId);
} else {
const error = await response.json();
alert("再計算に失敗しました: " + error.detail);
}
} catch (error) {
alert("再計算に失敗しました");
console.error(error);
}
}
// 初期化
const now = new Date();
document.getElementById("filterYear").value = now.getFullYear();
document.getElementById("filterMonth").value = now.getMonth() + 1;
loadPayrolls();
</script>
</body>
</html>

View File

@@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 従業員管理</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.nav-tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: #f0f0f0;
border-radius: 5px 5px 0 0;
}
.nav-tab.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.employee-list {
margin-top: 20px;
}
.employee-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.employee-item:hover {
background: #f9f9f9;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.dependent-list {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.dependent-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
background: white;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 従業員管理</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('list')">
従業員一覧
</button>
<button class="nav-tab" onclick="showTab('create')">新規登録</button>
<button
class="nav-tab"
onclick="showTab('detail')"
id="detailTab"
style="display: none"
>
従業員詳細
</button>
</div>
<!-- 従業員一覧タブ -->
<div id="tab-list" class="tab-content active">
<div class="filters">
<label>
<input
type="checkbox"
id="filterActive"
checked
onchange="loadEmployees()"
/>
在職中のみ表示
</label>
</div>
<div id="employeeList" class="employee-list"></div>
</div>
<!-- 新規登録タブ -->
<div id="tab-create" class="tab-content">
<h2>従業員の新規登録</h2>
<form id="createForm" onsubmit="createEmployee(event)">
<div class="form-group">
<label>従業員コード *</label>
<input type="text" name="employee_code" required />
</div>
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" required />
</div>
<div class="form-group">
<label>フリガナ</label>
<input type="text" name="name_kana" />
</div>
<div class="form-group">
<label>メールアドレス</label>
<input type="email" name="email" />
</div>
<div class="form-group">
<label>電話番号</label>
<input type="tel" name="phone" />
</div>
<div class="form-group">
<label>入社日 *</label>
<input type="date" name="hire_date" required />
</div>
<div class="form-group">
<label>銀行名</label>
<input type="text" name="bank_name" />
</div>
<div class="form-group">
<label>支店名</label>
<input type="text" name="bank_branch" />
</div>
<div class="form-group">
<label>口座番号</label>
<input type="text" name="bank_account_number" />
</div>
<div class="form-group">
<label>口座種別</label>
<select name="bank_account_type">
<option value="">選択してください</option>
<option value="普通">普通</option>
<option value="当座">当座</option>
</select>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="showTab('list')"
>
キャンセル
</button>
</form>
</div>
<!-- 従業員詳細タブ -->
<div id="tab-detail" class="tab-content">
<div id="employeeDetail"></div>
</div>
</div>
<script>
const API_BASE = "http://localhost:8000";
let currentEmployee = null;
function showTab(tabName) {
document
.querySelectorAll(".nav-tab")
.forEach((tab) => tab.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
if (tabName === "list") {
document.querySelector(".nav-tab").classList.add("active");
document.getElementById("tab-list").classList.add("active");
loadEmployees();
} else if (tabName === "create") {
document.querySelectorAll(".nav-tab")[1].classList.add("active");
document.getElementById("tab-create").classList.add("active");
} else if (tabName === "detail") {
document.getElementById("detailTab").classList.add("active");
document.getElementById("tab-detail").classList.add("active");
}
}
async function loadEmployees() {
const isActive = document.getElementById("filterActive").checked;
const url = `${API_BASE}/payroll/employees/${
isActive ? "?is_active=true" : ""
}`;
try {
const response = await fetch(url);
const employees = await response.json();
const html = employees
.map(
(emp) => `
<div class="employee-item" onclick="viewEmployee(${
emp.employee_id
})">
<strong>${emp.employee_code}</strong> - ${emp.name}
${emp.name_kana ? `(${emp.name_kana})` : ""}
<br>
<small>入社日: ${emp.hire_date} ${
emp.termination_date ? ` | 退職日: ${emp.termination_date}` : ""
}</small>
</div>
`
)
.join("");
document.getElementById("employeeList").innerHTML =
html || "<p>従業員が登録されていません</p>";
} catch (error) {
alert("従業員一覧の取得に失敗しました");
console.error(error);
}
}
async function createEmployee(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 空の値を除外
Object.keys(data).forEach((key) => {
if (data[key] === "") {
delete data[key];
}
});
try {
const response = await fetch(`${API_BASE}/payroll/employees/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("従業員を登録しました");
form.reset();
showTab("list");
} else {
const error = await response.json();
alert("登録に失敗しました: " + error.detail);
}
} catch (error) {
alert("登録に失敗しました");
console.error(error);
}
}
async function viewEmployee(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}`
);
const employee = await response.json();
currentEmployee = employee;
const html = `
<h2>${employee.name} (${employee.employee_code})</h2>
<p><strong>フリガナ:</strong> ${
employee.name_kana || "-"
}</p>
<p><strong>メール:</strong> ${employee.email || "-"}</p>
<p><strong>電話:</strong> ${employee.phone || "-"}</p>
<p><strong>入社日:</strong> ${employee.hire_date}</p>
<p><strong>退職日:</strong> ${
employee.termination_date || "-"
}</p>
<p><strong>銀行:</strong> ${employee.bank_name || "-"} ${
employee.bank_branch || ""
}</p>
<p><strong>口座:</strong> ${
employee.bank_account_type || ""
} ${employee.bank_account_number || "-"}</p>
<p><strong>備考:</strong> ${employee.notes || "-"}</p>
<div class="dependent-list">
<h3>扶養家族</h3>
${
employee.dependents
.map(
(dep) => `
<div class="dependent-item">
<strong>${dep.name}</strong> (${
dep.relationship
})
${
dep.birth_date
? ` - 生年月日: ${dep.birth_date}`
: ""
}
${
dep.is_spouse
? ' <span style="color:blue;">[配偶者]</span>'
: ""
}
${
dep.is_disabled
? ' <span style="color:red;">[障害者]</span>'
: ""
}
<br><small>有効期間: ${dep.valid_from} ${
dep.valid_to || "現在"
}</small>
</div>
`
)
.join("") || "<p>扶養家族が登録されていません</p>"
}
</div>
<button class="btn btn-secondary" onclick="showTab('list')">戻る</button>
`;
document.getElementById("employeeDetail").innerHTML = html;
document.getElementById("detailTab").style.display = "block";
showTab("detail");
} catch (error) {
alert("従業員情報の取得に失敗しました");
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 給与設定</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.employee-select {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table th,
table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
table th {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 給与設定</h1>
<div class="employee-select">
<label><strong>従業員を選択:</strong></label>
<select id="employeeSelect" onchange="loadSalarySettings()">
<option value="">従業員を選択してください</option>
</select>
</div>
<div id="settingsContent" style="display: none">
<h2>給与設定履歴</h2>
<button class="btn btn-primary" onclick="showAddForm()">
新規設定追加
</button>
<div id="settingsList"></div>
<div
id="addForm"
style="
display: none;
margin-top: 20px;
padding: 20px;
border: 2px solid #007bff;
border-radius: 5px;
"
>
<h3>給与設定の追加</h3>
<form onsubmit="saveSalarySetting(event)">
<div class="form-group">
<label>基本給 (円) *</label>
<input type="number" name="base_salary" step="0.01" required />
</div>
<div class="form-group">
<label>時給 (円)</label>
<input type="number" name="hourly_rate" step="0.01" />
<small>時給制の場合に入力してください</small>
</div>
<div class="form-group">
<label>雇用形態 *</label>
<select name="employment_type" required>
<option value="">選択してください</option>
<option value="正社員">正社員</option>
<option value="契約社員">契約社員</option>
<option value="パート">パート</option>
<option value="アルバイト">アルバイト</option>
</select>
</div>
<div class="form-group">
<label>支給形態 *</label>
<select name="payment_type" required>
<option value="">選択してください</option>
<option value="月給">月給</option>
<option value="時給">時給</option>
<option value="日給">日給</option>
</select>
</div>
<div class="form-group">
<label>通勤手当 (円)</label>
<input
type="number"
name="commute_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>その他手当 (円)</label>
<input
type="number"
name="other_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" required />
</div>
<div class="form-group">
<label>適用終了日</label>
<input type="date" name="valid_to" />
<small>通常は空欄のままにしてください</small>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button
type="button"
class="btn btn-secondary"
onclick="hideAddForm()"
>
キャンセル
</button>
</form>
</div>
<div
id="currentSetting"
style="
margin-top: 30px;
padding: 20px;
background: #e7f3ff;
border-radius: 5px;
"
>
<h3>現在の給与設定</h3>
<div id="currentSettingContent"></div>
</div>
</div>
</div>
<script>
const API_BASE = "http://localhost:8000";
let currentEmployeeId = null;
async function loadEmployees() {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/?is_active=true`
);
const employees = await response.json();
const select = document.getElementById("employeeSelect");
select.innerHTML =
'<option value="">従業員を選択してください</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
)
.join("");
} catch (error) {
alert("従業員一覧の取得に失敗しました");
console.error(error);
}
}
async function loadSalarySettings() {
const employeeId = document.getElementById("employeeSelect").value;
if (!employeeId) {
document.getElementById("settingsContent").style.display = "none";
return;
}
currentEmployeeId = employeeId;
document.getElementById("settingsContent").style.display = "block";
try {
// 給与設定履歴を取得
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${employeeId}`
);
const settings = await response.json();
const html = `
<table>
<thead>
<tr>
<th>適用期間</th>
<th>基本給</th>
<th>時給</th>
<th>雇用形態</th>
<th>支給形態</th>
<th>通勤手当</th>
<th>その他手当</th>
</tr>
</thead>
<tbody>
${settings
.map(
(s) => `
<tr>
<td>${s.valid_from} ${
s.valid_to || "現在"
}</td>
<td>¥${Number(
s.base_salary
).toLocaleString()}</td>
<td>${
s.hourly_rate
? "¥" +
Number(s.hourly_rate).toLocaleString()
: "-"
}</td>
<td>${s.employment_type}</td>
<td>${s.payment_type}</td>
<td>¥${Number(
s.commute_allowance
).toLocaleString()}</td>
<td>¥${Number(
s.other_allowance
).toLocaleString()}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("settingsList").innerHTML = html;
// 現在の設定を取得
loadCurrentSetting(employeeId);
} catch (error) {
alert("給与設定の取得に失敗しました");
console.error(error);
}
}
async function loadCurrentSetting(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${employeeId}/current`
);
if (response.ok) {
const setting = await response.json();
const html = `
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:15px;">
<div><strong>基本給:</strong> ¥${Number(
setting.base_salary
).toLocaleString()}</div>
<div><strong>時給:</strong> ${
setting.hourly_rate
? "¥" +
Number(setting.hourly_rate).toLocaleString()
: "-"
}</div>
<div><strong>雇用形態:</strong> ${
setting.employment_type
}</div>
<div><strong>支給形態:</strong> ${
setting.payment_type
}</div>
<div><strong>通勤手当:</strong> ¥${Number(
setting.commute_allowance
).toLocaleString()}</div>
<div><strong>その他手当:</strong> ¥${Number(
setting.other_allowance
).toLocaleString()}</div>
<div><strong>適用期間:</strong> ${
setting.valid_from
} ${setting.valid_to || "現在"}</div>
</div>
`;
document.getElementById("currentSettingContent").innerHTML = html;
} else {
document.getElementById("currentSettingContent").innerHTML =
"<p>給与設定が登録されていません</p>";
}
} catch (error) {
document.getElementById("currentSettingContent").innerHTML =
"<p>給与設定の取得に失敗しました</p>";
}
}
function showAddForm() {
document.getElementById("addForm").style.display = "block";
// デフォルト値設定
document.querySelector('[name="valid_from"]').value = new Date()
.toISOString()
.split("T")[0];
}
function hideAddForm() {
document.getElementById("addForm").style.display = "none";
document.querySelector("#addForm form").reset();
}
async function saveSalarySetting(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
data.employee_id = parseInt(currentEmployeeId);
data.base_salary = parseFloat(data.base_salary) || 0;
data.commute_allowance = parseFloat(data.commute_allowance) || 0;
data.other_allowance = parseFloat(data.other_allowance) || 0;
if (data.hourly_rate) {
data.hourly_rate = parseFloat(data.hourly_rate);
} else {
delete data.hourly_rate;
}
if (!data.valid_to) {
delete data.valid_to;
}
try {
const response = await fetch(`${API_BASE}/payroll/settings/salary`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("給与設定を保存しました");
hideAddForm();
loadSalarySettings();
} else {
const error = await response.json();
alert("保存に失敗しました: " + error.detail);
}
} catch (error) {
alert("保存に失敗しました");
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

121
frontend/payroll.html Normal file
View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理システム</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.menu-card {
padding: 30px;
border: 2px solid #007bff;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.menu-card:hover {
background: #007bff;
color: white;
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3);
}
.menu-card h2 {
margin: 0 0 15px 0;
font-size: 1.5em;
}
.menu-card p {
margin: 0;
opacity: 0.8;
}
.menu-card:hover p {
opacity: 1;
}
.icon {
font-size: 3em;
margin-bottom: 15px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<a href="../index.html" class="back-link">← メインメニューに戻る</a>
<h1>給与管理システム</h1>
<p>従業員の給与計算、扶養管理、保険料・税金の管理を行います</p>
<div class="menu-grid">
<div class="menu-card" onclick="location.href='payroll-employees.html'">
<div class="icon">👥</div>
<h2>従業員管理</h2>
<p>従業員の基本情報、扶養家族情報を管理します</p>
</div>
<div
class="menu-card"
onclick="location.href='payroll-salary-settings.html'"
>
<div class="icon">💰</div>
<h2>給与設定</h2>
<p>各従業員の給与設定、基本給、手当などを管理します</p>
</div>
<div
class="menu-card"
onclick="location.href='payroll-calculation.html'"
>
<div class="icon">🧮</div>
<h2>月次給与計算</h2>
<p>毎月の給与計算、給与明細の作成・承認を行います</p>
</div>
<div class="menu-card" onclick="location.href='payroll-settings.html'">
<div class="icon">⚙️</div>
<h2>税率・保険料設定</h2>
<p>社会保険料率、所得税率表の管理を行います</p>
</div>
</div>
<div
style="
margin-top: 50px;
padding: 20px;
background: #f0f8ff;
border-radius: 5px;
"
>
<h3>📋 給与システムの使い方</h3>
<ol>
<li>
<strong>従業員管理</strong
>で従業員を登録し、扶養家族情報を入力します
</li>
<li><strong>給与設定</strong>で各従業員の基本給や手当を設定します</li>
<li>
<strong>税率・保険料設定</strong
>で社会保険料率や所得税率表を確認・更新します
</li>
<li>
<strong>月次給与計算</strong
>で勤怠情報を入力し、給与を計算・承認します
</li>
</ol>
</div>
</div>
</body>
</html>

BIN
reports/tmp/01-07 (3).xls Normal file

Binary file not shown.

View File

@@ -78,6 +78,8 @@ command = C:\Users\zxh_y\AppData\Local\Programs\Python\Python312\python.exe -m v
cd C:\workspace\njts-accounting-core\backend cd C:\workspace\njts-accounting-core\backend
.venv\Scripts\activate .venv\Scripts\activate
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080 uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
& .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
http://localhost:18080/docs http://localhost:18080/docs