給与管理システム新規作成
This commit is contained in:
43
backend/add_tax_year_field.py
Normal file
43
backend/add_tax_year_field.py
Normal 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完了!")
|
||||
@@ -81,14 +81,40 @@ app.include_router(year_locks.router)
|
||||
from app.routers import cash
|
||||
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
|
||||
import os
|
||||
|
||||
app.mount(
|
||||
"/static",
|
||||
StaticFiles(directory="app/static", html=True),
|
||||
name="static",
|
||||
)
|
||||
# 静的ファイル配信(コメントアウト - ディレクトリが存在しない)
|
||||
# app.mount(
|
||||
# "/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",
|
||||
# )
|
||||
|
||||
|
||||
|
||||
|
||||
3
backend/app/payroll/__init__.py
Normal file
3
backend/app/payroll/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
給与管理モジュール (Payroll Module)
|
||||
"""
|
||||
1
backend/app/payroll/bonus/__init__.py
Normal file
1
backend/app/payroll/bonus/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bonus Module
|
||||
139
backend/app/payroll/bonus/router.py
Normal file
139
backend/app/payroll/bonus/router.py
Normal 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()
|
||||
63
backend/app/payroll/bonus/schemas.py
Normal file
63
backend/app/payroll/bonus/schemas.py
Normal 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
|
||||
235
backend/app/payroll/bonus/service.py
Normal file
235
backend/app/payroll/bonus/service.py
Normal 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
|
||||
}
|
||||
3
backend/app/payroll/calculation/__init__.py
Normal file
3
backend/app/payroll/calculation/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
月次給与計算 (Monthly Payroll Calculation)
|
||||
"""
|
||||
284
backend/app/payroll/calculation/router.py
Normal file
284
backend/app/payroll/calculation/router.py
Normal 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()
|
||||
129
backend/app/payroll/calculation/schemas.py
Normal file
129
backend/app/payroll/calculation/schemas.py
Normal 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
|
||||
317
backend/app/payroll/calculation/service.py
Normal file
317
backend/app/payroll/calculation/service.py
Normal 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
|
||||
}
|
||||
3
backend/app/payroll/employees/__init__.py
Normal file
3
backend/app/payroll/employees/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
従業員管理 (Employee Management)
|
||||
"""
|
||||
196
backend/app/payroll/employees/router.py
Normal file
196
backend/app/payroll/employees/router.py
Normal 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()
|
||||
109
backend/app/payroll/employees/schemas.py
Normal file
109
backend/app/payroll/employees/schemas.py
Normal 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] = []
|
||||
3
backend/app/payroll/settings/__init__.py
Normal file
3
backend/app/payroll/settings/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
給与設定・税率・保険料管理 (Settings Management)
|
||||
"""
|
||||
869
backend/app/payroll/settings/router.py
Normal file
869
backend/app/payroll/settings/router.py
Normal 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()
|
||||
|
||||
220
backend/app/payroll/settings/schemas.py
Normal file
220
backend/app/payroll/settings/schemas.py
Normal 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
|
||||
434
backend/app/static/payroll-calculation.html
Normal file
434
backend/app/static/payroll-calculation.html
Normal 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>
|
||||
302
backend/app/static/payroll-employees.html
Normal file
302
backend/app/static/payroll-employees.html
Normal 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>
|
||||
291
backend/app/static/payroll-salary-settings.html
Normal file
291
backend/app/static/payroll-salary-settings.html
Normal 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>
|
||||
326
backend/app/static/payroll-settings.html
Normal file
326
backend/app/static/payroll-settings.html
Normal 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>
|
||||
99
backend/app/static/payroll.html
Normal file
99
backend/app/static/payroll.html
Normal 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
20
backend/check_excel.py
Normal 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)}")
|
||||
59
backend/check_precision.py
Normal file
59
backend/check_precision.py
Normal 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()
|
||||
72
backend/create_payroll_db.py
Normal file
72
backend/create_payroll_db.py
Normal 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
68
backend/fix_precision.py
Normal 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% と表示されます。")
|
||||
51
backend/income_tax_sample.csv
Normal file
51
backend/income_tax_sample.csv
Normal 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
|
||||
|
Binary file not shown.
56
backend/revert_precision.py
Normal file
56
backend/revert_precision.py
Normal 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桁に丸めることができます。")
|
||||
17
backend/sql/add_tax_year.sql
Normal file
17
backend/sql/add_tax_year.sql
Normal 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 '適用年度';
|
||||
116
backend/sql/payroll_enhancements.sql
Normal file
116
backend/sql/payroll_enhancements.sql
Normal 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歳以上)');
|
||||
172
backend/sql/payroll_migration_20260119.sql
Normal file
172
backend/sql/payroll_migration_20260119.sql
Normal 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 = '介護保険'; -- 介護保険料率の確認
|
||||
238
backend/sql/payroll_schema.sql
Normal file
238
backend/sql/payroll_schema.sql
Normal 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%');
|
||||
436
backend/sql/payroll_schema_complete.sql
Normal file
436
backend/sql/payroll_schema_complete.sql
Normal 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; -- 扶養判定条件確認
|
||||
79
backend/update_precision.py
Normal file
79
backend/update_precision.py
Normal 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完了!")
|
||||
Reference in New Issue
Block a user