給与管理システム新規作成
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
|
from app.routers import cash
|
||||||
app.include_router(cash.router)
|
app.include_router(cash.router)
|
||||||
|
|
||||||
|
# ─────────────────────────
|
||||||
|
# 給与モジュール (Payroll Module)
|
||||||
|
# ─────────────────────────
|
||||||
|
from app.payroll.employees.router import router as payroll_employees_router
|
||||||
|
app.include_router(payroll_employees_router)
|
||||||
|
|
||||||
|
from app.payroll.settings.router import router as payroll_settings_router
|
||||||
|
app.include_router(payroll_settings_router)
|
||||||
|
|
||||||
|
from app.payroll.calculation.router import router as payroll_calculation_router
|
||||||
|
app.include_router(payroll_calculation_router)
|
||||||
|
|
||||||
|
from app.payroll.bonus.router import router as payroll_bonus_router
|
||||||
|
app.include_router(payroll_bonus_router)
|
||||||
|
|
||||||
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
import os
|
||||||
|
|
||||||
app.mount(
|
# 静的ファイル配信(コメントアウト - ディレクトリが存在しない)
|
||||||
"/static",
|
# app.mount(
|
||||||
StaticFiles(directory="app/static", html=True),
|
# "/static",
|
||||||
name="static",
|
# StaticFiles(directory="app/static", html=True),
|
||||||
)
|
# name="static",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# フロントエンドファイル配信(コメントアウト - file://プロトコルで直接アクセスするため不要)
|
||||||
|
# frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
|
||||||
|
# if os.path.exists(frontend_path):
|
||||||
|
# app.mount(
|
||||||
|
# "/",
|
||||||
|
# StaticFiles(directory=frontend_path, html=True),
|
||||||
|
# name="frontend",
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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完了!")
|
||||||
274
docs/payroll_enhancements.md
Normal file
274
docs/payroll_enhancements.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# 工资系统增强功能说明
|
||||||
|
|
||||||
|
## 已实现的三大改进
|
||||||
|
|
||||||
|
### 1. 扶养人数输入和判定 ✓
|
||||||
|
|
||||||
|
#### 功能说明
|
||||||
|
|
||||||
|
- **扶养家族管理**: 已有的 `dependents` 表允许录入扶养家族信息
|
||||||
|
- **自动计算扶养人数**: 系统自动统计有效期内的扶养人数
|
||||||
|
- **扶养判定规则表**: 新增 `dependent_rules` 表,可配置扶养条件
|
||||||
|
|
||||||
|
#### API 端点
|
||||||
|
|
||||||
|
- `POST /payroll/employees/{employee_id}/dependents` - 添加扶养家族
|
||||||
|
- `GET /payroll/employees/{employee_id}/dependents` - 查看扶养家族列表
|
||||||
|
- `GET /payroll/settings/dependent-rules` - 查看扶养判定规则
|
||||||
|
- `POST /payroll/settings/dependent-rules` - 设置扶养判定规则
|
||||||
|
|
||||||
|
#### 扶养判定条件(可配置)
|
||||||
|
|
||||||
|
```
|
||||||
|
- 所得限度額_一般: 480,000円/年
|
||||||
|
- 所得限度額_配偶者: 480,000円/年
|
||||||
|
- 所得限度額_配偶者特別: 1,330,000円/年
|
||||||
|
- 年齢_特定扶養親族: 19-23歳
|
||||||
|
- 年齢_老人扶養親族: 70歳以上
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 奖金计算 + 社保上限设置 ✓
|
||||||
|
|
||||||
|
#### 社会保险料上限表 (`insurance_limit_settings`)
|
||||||
|
|
||||||
|
可通过画面手动修改的上限值:
|
||||||
|
|
||||||
|
| 设定类型 | 2026 年初始值 | 说明 |
|
||||||
|
| ------------------------ | ------------- | -------------------------------- |
|
||||||
|
| 健康保険標準報酬月額上限 | 1,390,000 円 | 健康保险计算基数上限(第 50 级) |
|
||||||
|
| 厚生年金標準報酬月額上限 | 650,000 円 | 厚生年金计算基数上限(第 32 级) |
|
||||||
|
| 通勤手当非課税限度額 | 150,000 円 | 通勤补贴免税上限 |
|
||||||
|
|
||||||
|
#### API 端点
|
||||||
|
|
||||||
|
**社保上限管理**:
|
||||||
|
|
||||||
|
- `GET /payroll/settings/insurance-limits` - 获取当前社保上限
|
||||||
|
- `POST /payroll/settings/insurance-limits` - 创建新的上限设置
|
||||||
|
- `PUT /payroll/settings/insurance-limits/{limit_id}` - 修改上限值
|
||||||
|
|
||||||
|
**奖金计算**:
|
||||||
|
|
||||||
|
- `POST /payroll/bonus/calculate` - 计算奖金
|
||||||
|
- `GET /payroll/bonus/` - 获取奖金列表
|
||||||
|
- `GET /payroll/bonus/{bonus_id}` - 获取奖金详情
|
||||||
|
- `POST /payroll/bonus/{bonus_id}/approve` - 审批奖金
|
||||||
|
|
||||||
|
#### 奖金计算逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
总支给额 = 基本奖金 + 业绩奖金 + 其他奖金
|
||||||
|
|
||||||
|
# 应用社保上限
|
||||||
|
健康保险 = min(总支给额, 健康保险上限) × 健康保险率
|
||||||
|
厚生年金 = min(总支给额, 厚生年金上限) × 厚生年金率
|
||||||
|
介护保险 = min(总支给额, 健康保险上限) × 介护保险率 (40-64岁)
|
||||||
|
雇用保险 = 总支给额 × 雇用保险率
|
||||||
|
|
||||||
|
# 奖金所得税(特殊算法)
|
||||||
|
所得税 = (总支给额 - 社保) × 根据前3月平均工资和扶养人数确定的税率
|
||||||
|
|
||||||
|
差引支给额 = 总支给额 - 总控除额
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 奖金类型
|
||||||
|
|
||||||
|
- 夏季賞与(夏季奖金)
|
||||||
|
- 冬季賞与(冬季奖金)
|
||||||
|
- 決算賞与(结算奖金)
|
||||||
|
- その他(其他)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 源泉徴収税額表上传功能 ✓
|
||||||
|
|
||||||
|
#### 功能说明
|
||||||
|
|
||||||
|
- **CSV 上传**: 可通过 API 上传源泉徴收税额表的 CSV 文件
|
||||||
|
- **年份区分**: 每条税率记录有 `valid_from` 和 `valid_to` 字段区分年份
|
||||||
|
- **自动应用**: 计算工资时自动使用对应年份的税额表
|
||||||
|
|
||||||
|
#### API 端点
|
||||||
|
|
||||||
|
- `POST /payroll/settings/income-tax/import-csv` - 上传税额表 CSV
|
||||||
|
- `GET /payroll/settings/income-tax` - 查看当前税额表
|
||||||
|
- `GET /payroll/settings/income-tax/calculate` - 测试税额计算
|
||||||
|
|
||||||
|
#### CSV 格式示例
|
||||||
|
|
||||||
|
```csv
|
||||||
|
monthly_income_from,monthly_income_to,dependents_count,tax_amount,valid_from
|
||||||
|
0,88000,0,0,2026-01-01
|
||||||
|
88000,89000,0,130,2026-01-01
|
||||||
|
89000,90000,0,200,2026-01-01
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
- `monthly_income_from`: 月收入下限
|
||||||
|
- `monthly_income_to`: 月收入上限
|
||||||
|
- `dependents_count`: 扶养人数 (0, 1, 2, 3...)
|
||||||
|
- `tax_amount`: 源泉徴収税额
|
||||||
|
- `valid_from`: 适用开始日期
|
||||||
|
|
||||||
|
#### 使用流程
|
||||||
|
|
||||||
|
1. 准备 CSV 文件(按国税厅公布的源泉徴収税額表格式)
|
||||||
|
2. 调用上传 API: `POST /payroll/settings/income-tax/import-csv`
|
||||||
|
3. 系统自动应用新税额表进行计算
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库变更
|
||||||
|
|
||||||
|
### 新增表
|
||||||
|
|
||||||
|
1. **insurance_limit_settings** - 社保上限设置表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- limit_id (主键)
|
||||||
|
- setting_type (设定类型)
|
||||||
|
- limit_amount (上限金额)
|
||||||
|
- valid_from/valid_to (有效期)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **bonus_payments** - 奖金计算表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- bonus_id (主键)
|
||||||
|
- employee_id (员工ID)
|
||||||
|
- bonus_year/bonus_month (年月)
|
||||||
|
- bonus_type (奖金类型)
|
||||||
|
- base_bonus/performance_bonus/other_bonus (支给项目)
|
||||||
|
- health_insurance/care_insurance/pension_insurance... (控除项目)
|
||||||
|
- total_bonus/total_deduction/net_bonus (合计金额)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **dependent_rules** - 扶养判定条件表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- rule_id (主键)
|
||||||
|
- rule_type (规则类型)
|
||||||
|
- rule_value (规则值)
|
||||||
|
- valid_from/valid_to (有效期)
|
||||||
|
- description (说明)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 应用数据库变更
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 应用增强功能schema
|
||||||
|
psql -U your_user -d your_database -f backend/sql/payroll_enhancements.sql
|
||||||
|
|
||||||
|
# 2. 验证表是否创建成功
|
||||||
|
psql -U your_user -d your_database -c "\dt *bonus* *insurance_limit* *dependent_rule*"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 计算逻辑改进总结
|
||||||
|
|
||||||
|
### 月度工资计算
|
||||||
|
|
||||||
|
1. ✅ 应用健康保险上限 (1,390,000 円)
|
||||||
|
2. ✅ 应用厚生年金上限 (650,000 円)
|
||||||
|
3. ✅ 应用通勤补贴免税限度 (可配置)
|
||||||
|
4. ✅ 40-64 岁自动计算介护保险
|
||||||
|
5. ✅ 自动统计扶养人数用于所得税计算
|
||||||
|
|
||||||
|
### 奖金计算
|
||||||
|
|
||||||
|
1. ✅ 应用社保上限(与工资计算一致)
|
||||||
|
2. ✅ 特殊的奖金所得税算法
|
||||||
|
3. ✅ 支持多种奖金类型
|
||||||
|
4. ✅ 基于前 3 月平均工资计算税率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端 TODO(建议)
|
||||||
|
|
||||||
|
需要创建以下管理画面:
|
||||||
|
|
||||||
|
1. **社保上限设置画面**
|
||||||
|
|
||||||
|
- 显示当前上限值
|
||||||
|
- 允许修改上限金额
|
||||||
|
- 查看历史变更记录
|
||||||
|
|
||||||
|
2. **扶养判定规则画面**
|
||||||
|
|
||||||
|
- 显示当前判定条件
|
||||||
|
- 允许修改所得限度额
|
||||||
|
- 配置年龄条件
|
||||||
|
|
||||||
|
3. **税额表上传画面**
|
||||||
|
|
||||||
|
- CSV 文件上传组件
|
||||||
|
- 预览上传内容
|
||||||
|
- 查看当前税额表
|
||||||
|
|
||||||
|
4. **奖金计算画面**
|
||||||
|
- 选择员工
|
||||||
|
- 输入奖金金额
|
||||||
|
- 显示计算结果
|
||||||
|
- 审批流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 测试示例
|
||||||
|
|
||||||
|
### 1. 设置社保上限
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:18080/payroll/settings/insurance-limits \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"setting_type": "健康保険標準報酬月額上限",
|
||||||
|
"limit_amount": 1390000,
|
||||||
|
"valid_from": "2026-01-01",
|
||||||
|
"notes": "2026年度上限"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 上传税额表 CSV
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:18080/payroll/settings/income-tax/import-csv \
|
||||||
|
-F "file=@tax_table_2026.csv" \
|
||||||
|
-F "valid_from=2026-01-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 计算奖金
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:18080/payroll/bonus/calculate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"employee_id": 1,
|
||||||
|
"bonus_year": 2026,
|
||||||
|
"bonus_month": 6,
|
||||||
|
"bonus_type": "夏季賞与",
|
||||||
|
"payment_date": "2026-06-30",
|
||||||
|
"base_bonus": 500000,
|
||||||
|
"performance_bonus": 100000,
|
||||||
|
"calculated_by": "admin"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 查询扶养判定规则
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:18080/payroll/settings/dependent-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
✅ **问题 1**: 扶养人数输入和判定 - 已实现,可配置判定条件
|
||||||
|
✅ **问题 2**: 奖金计算 + 社保上限 - 已实现,上限可手动修改
|
||||||
|
✅ **问题 3**: 税额表上传 - 已实现,支持 CSV 上传和年份区分
|
||||||
|
|
||||||
|
所有功能都已实现 API 层,可以通过 RESTful 接口调用。下一步建议创建前端管理画面以便更方便地操作这些功能。
|
||||||
171
docs/payroll_quickstart.md
Normal file
171
docs/payroll_quickstart.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# 給与システム - クイックスタートガイド
|
||||||
|
|
||||||
|
## セットアップ手順
|
||||||
|
|
||||||
|
### 1. データベーススキーマの適用
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# PostgreSQLに接続(環境に応じて認証情報を変更)
|
||||||
|
$env:PGPASSWORD = "your_password"
|
||||||
|
psql -U your_user -d your_database -f backend/sql/payroll_schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
または、既存のデータベース接続を使用している場合:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Pythonスクリプトで実行する場合
|
||||||
|
cd backend
|
||||||
|
python -c "
|
||||||
|
from app.core.database import get_connection
|
||||||
|
with open('sql/payroll_schema.sql', 'r', encoding='utf-8') as f:
|
||||||
|
sql = f.read()
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql)
|
||||||
|
conn.commit()
|
||||||
|
print('給与システムのスキーマを適用しました')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. サーバーの起動
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd backend
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ブラウザでアクセス
|
||||||
|
|
||||||
|
http://localhost:8000/static/payroll.html
|
||||||
|
|
||||||
|
## 初期データのセットアップ
|
||||||
|
|
||||||
|
### 1. 保険料率の確認
|
||||||
|
|
||||||
|
システムには以下の初期保険料率が登録されています(2026 年度):
|
||||||
|
|
||||||
|
- 健康保険: 10.00% (労使折半 各 5.00%)
|
||||||
|
- 厚生年金: 18.30% (労使折半 各 9.15%)
|
||||||
|
- 雇用保険: 1.55% (従業員 0.6% + 事業主 0.95%)
|
||||||
|
- 労災保険: 0.30% (事業主のみ)
|
||||||
|
|
||||||
|
必要に応じて「税率・保険料設定」画面で更新してください。
|
||||||
|
|
||||||
|
### 2. 所得税率表のインポート
|
||||||
|
|
||||||
|
サンプル CSV ファイルが `backend/income_tax_sample.csv` に用意されています。
|
||||||
|
|
||||||
|
1. 「税率・保険料設定」画面を開く
|
||||||
|
2. 「所得税率表」タブを選択
|
||||||
|
3. 「CSV インポート」ボタンをクリック
|
||||||
|
4. `income_tax_sample.csv` を選択
|
||||||
|
5. 適用開始日を入力(例: 2026-01-01)
|
||||||
|
|
||||||
|
### 3. 従業員の登録
|
||||||
|
|
||||||
|
1. 「従業員管理」画面を開く
|
||||||
|
2. 「新規登録」タブを選択
|
||||||
|
3. 必要な情報を入力:
|
||||||
|
- 従業員コード(必須)
|
||||||
|
- 氏名(必須)
|
||||||
|
- 入社日(必須)
|
||||||
|
- その他の情報(任意)
|
||||||
|
4. 「登録」ボタンをクリック
|
||||||
|
|
||||||
|
### 4. 扶養家族の登録(必要な場合)
|
||||||
|
|
||||||
|
1. 「従業員管理」画面で従業員を選択
|
||||||
|
2. 従業員詳細画面の「扶養家族」セクションで追加
|
||||||
|
|
||||||
|
### 5. 給与設定の登録
|
||||||
|
|
||||||
|
1. 「給与設定」画面を開く
|
||||||
|
2. 従業員を選択
|
||||||
|
3. 「新規設定追加」ボタンをクリック
|
||||||
|
4. 給与情報を入力:
|
||||||
|
- 基本給(必須)
|
||||||
|
- 雇用形態(必須)
|
||||||
|
- 支給形態(必須)
|
||||||
|
- 通勤手当(任意)
|
||||||
|
- 適用開始日(必須)
|
||||||
|
5. 「保存」ボタンをクリック
|
||||||
|
|
||||||
|
## 給与計算の実行
|
||||||
|
|
||||||
|
### 月次給与計算の手順
|
||||||
|
|
||||||
|
1. 「月次給与計算」画面を開く
|
||||||
|
2. 「新規計算」ボタンをクリック
|
||||||
|
3. 給与計算情報を入力:
|
||||||
|
- 従業員を選択
|
||||||
|
- 対象年月
|
||||||
|
- 支給日
|
||||||
|
- 勤怠情報(出勤日数、勤務時間、残業時間など)
|
||||||
|
- その他(住民税、その他控除など)
|
||||||
|
4. 「計算実行」ボタンをクリック
|
||||||
|
5. 給与明細を確認
|
||||||
|
6. 問題なければ「承認」ボタンをクリック
|
||||||
|
|
||||||
|
### 給与明細の見方
|
||||||
|
|
||||||
|
**支給項目**
|
||||||
|
|
||||||
|
- 基本給: 設定された基本給(欠勤がある場合は控除後)
|
||||||
|
- 残業手当: 残業時間 × 時給 × 1.25
|
||||||
|
- 休日手当: 休日出勤時間 × 時給 × 1.35
|
||||||
|
- 通勤手当: 設定された通勤手当
|
||||||
|
- その他手当: 入力されたその他手当
|
||||||
|
- **総支給額**: 上記の合計
|
||||||
|
|
||||||
|
**控除項目**
|
||||||
|
|
||||||
|
- 健康保険: 総支給額 × 5.00%
|
||||||
|
- 厚生年金: 総支給額 × 9.15%
|
||||||
|
- 雇用保険: 総支給額 × 0.60%
|
||||||
|
- 所得税: 所得税率表に基づいて計算
|
||||||
|
- 住民税: 入力された金額
|
||||||
|
- その他控除: 入力された金額
|
||||||
|
- **総控除額**: 上記の合計
|
||||||
|
|
||||||
|
**差引支給額** = 総支給額 - 総控除額
|
||||||
|
|
||||||
|
## よくある質問
|
||||||
|
|
||||||
|
### Q1: 給与を再計算するには?
|
||||||
|
|
||||||
|
A: 給与明細画面で「再計算」ボタンをクリックしてください。ただし、承認済みの給与は再計算できません。
|
||||||
|
|
||||||
|
### Q2: 承認済みの給与を修正するには?
|
||||||
|
|
||||||
|
A: 承認済みの給与は変更できません。給与を削除して新規に計算し直す必要があります。
|
||||||
|
|
||||||
|
### Q3: 時給制の従業員の給与計算は?
|
||||||
|
|
||||||
|
A: 給与設定で「支給形態: 時給」を選択し、時給を入力してください。基本給は「時給 × 勤務時間」で自動計算されます。
|
||||||
|
|
||||||
|
### Q4: 所得税が 0 円になる
|
||||||
|
|
||||||
|
A: 所得税率表が登録されていない可能性があります。「税率・保険料設定」画面で所得税率表を確認してください。
|
||||||
|
|
||||||
|
### Q5: 扶養家族の人数が所得税に反映されない
|
||||||
|
|
||||||
|
A: 扶養家族の「有効期間」を確認してください。計算対象月に有効な扶養家族のみがカウントされます。
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### エラー: 「給与設定が見つかりません」
|
||||||
|
|
||||||
|
→ 「給与設定」画面で該当従業員の給与設定を登録してください
|
||||||
|
|
||||||
|
### エラー: 「保険料率が見つかりません」
|
||||||
|
|
||||||
|
→ 「税率・保険料設定」画面で保険料率を確認・登録してください
|
||||||
|
|
||||||
|
### 所得税が正しく計算されない
|
||||||
|
|
||||||
|
→ 所得税率表が正しくインポートされているか確認してください
|
||||||
|
→ 扶養家族の有効期間が正しく設定されているか確認してください
|
||||||
|
|
||||||
|
## サポート
|
||||||
|
|
||||||
|
詳細なドキュメントは `docs/payroll_system.md` を参照してください。
|
||||||
221
docs/payroll_system.md
Normal file
221
docs/payroll_system.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# 給与管理システム (Payroll Management System)
|
||||||
|
|
||||||
|
会計システムに統合された給与管理モジュールです。
|
||||||
|
|
||||||
|
## 機能概要
|
||||||
|
|
||||||
|
### 1. 従業員管理
|
||||||
|
|
||||||
|
- 従業員の基本情報管理
|
||||||
|
- 扶養家族情報の登録・管理
|
||||||
|
- 銀行口座情報の管理
|
||||||
|
|
||||||
|
### 2. 給与設定
|
||||||
|
|
||||||
|
- 従業員ごとの給与基本設定
|
||||||
|
- 基本給、時給、各種手当の設定
|
||||||
|
- 設定履歴の管理
|
||||||
|
|
||||||
|
### 3. 月次給与計算
|
||||||
|
|
||||||
|
- 勤怠情報の入力
|
||||||
|
- 給与の自動計算
|
||||||
|
- 基本給
|
||||||
|
- 残業手当(時給 × 1.25)
|
||||||
|
- 休日手当(時給 × 1.35)
|
||||||
|
- 社会保険料(健康保険、厚生年金、雇用保険)
|
||||||
|
- 所得税(扶養人数を考慮)
|
||||||
|
- 住民税
|
||||||
|
- 給与明細の表示
|
||||||
|
- 給与の承認機能
|
||||||
|
|
||||||
|
### 4. 税率・保険料管理
|
||||||
|
|
||||||
|
- 社会保険料率の管理
|
||||||
|
- 健康保険
|
||||||
|
- 厚生年金
|
||||||
|
- 雇用保険
|
||||||
|
- 労災保険
|
||||||
|
- 所得税率表の管理
|
||||||
|
- CSV インポート機能
|
||||||
|
|
||||||
|
## セットアップ
|
||||||
|
|
||||||
|
### 1. データベーススキーマの適用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQLに接続
|
||||||
|
psql -U your_user -d your_database
|
||||||
|
|
||||||
|
# スキーマを適用
|
||||||
|
\i backend/sql/payroll_schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. サーバーの起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. フロントエンドへのアクセス
|
||||||
|
|
||||||
|
ブラウザで以下の URL にアクセス:
|
||||||
|
|
||||||
|
- メインメニュー: http://localhost:8000/static/payroll.html
|
||||||
|
- 従業員管理: http://localhost:8000/static/payroll-employees.html
|
||||||
|
- 給与設定: http://localhost:8000/static/payroll-salary-settings.html
|
||||||
|
- 月次給与計算: http://localhost:8000/static/payroll-calculation.html
|
||||||
|
- 税率・保険料設定: http://localhost:8000/static/payroll-settings.html
|
||||||
|
|
||||||
|
## API エンドポイント
|
||||||
|
|
||||||
|
### 従業員管理
|
||||||
|
|
||||||
|
- `POST /payroll/employees/` - 従業員の作成
|
||||||
|
- `GET /payroll/employees/` - 従業員一覧の取得
|
||||||
|
- `GET /payroll/employees/{employee_id}` - 従業員詳細の取得
|
||||||
|
- `PUT /payroll/employees/{employee_id}` - 従業員情報の更新
|
||||||
|
- `DELETE /payroll/employees/{employee_id}` - 従業員の削除
|
||||||
|
|
||||||
|
### 扶養家族管理
|
||||||
|
|
||||||
|
- `POST /payroll/employees/{employee_id}/dependents` - 扶養家族の追加
|
||||||
|
- `GET /payroll/employees/{employee_id}/dependents` - 扶養家族一覧の取得
|
||||||
|
- `PUT /payroll/dependents/{dependent_id}` - 扶養家族情報の更新
|
||||||
|
- `DELETE /payroll/dependents/{dependent_id}` - 扶養家族の削除
|
||||||
|
|
||||||
|
### 給与設定
|
||||||
|
|
||||||
|
- `POST /payroll/settings/salary` - 給与設定の作成
|
||||||
|
- `GET /payroll/settings/salary/{employee_id}` - 給与設定履歴の取得
|
||||||
|
- `GET /payroll/settings/salary/{employee_id}/current` - 現在の給与設定の取得
|
||||||
|
- `PUT /payroll/settings/salary/{setting_id}` - 給与設定の更新
|
||||||
|
|
||||||
|
### 保険料率管理
|
||||||
|
|
||||||
|
- `POST /payroll/settings/insurance-rates` - 保険料率の作成
|
||||||
|
- `GET /payroll/settings/insurance-rates` - 保険料率一覧の取得
|
||||||
|
- `GET /payroll/settings/insurance-rates/{rate_type}` - 特定保険料率の取得
|
||||||
|
- `PUT /payroll/settings/insurance-rates/{rate_id}` - 保険料率の更新
|
||||||
|
|
||||||
|
### 所得税率表管理
|
||||||
|
|
||||||
|
- `POST /payroll/settings/income-tax` - 所得税率表の作成
|
||||||
|
- `POST /payroll/settings/income-tax/import` - CSV インポート
|
||||||
|
- `GET /payroll/settings/income-tax` - 所得税率表の取得
|
||||||
|
- `GET /payroll/settings/income-tax/calculate` - 所得税の計算
|
||||||
|
|
||||||
|
### 月次給与計算
|
||||||
|
|
||||||
|
- `POST /payroll/calculation/calculate` - 給与の計算
|
||||||
|
- `GET /payroll/calculation/` - 給与計算一覧の取得
|
||||||
|
- `GET /payroll/calculation/{payroll_id}` - 給与明細の取得
|
||||||
|
- `PUT /payroll/calculation/{payroll_id}` - 給与データの更新
|
||||||
|
- `POST /payroll/calculation/{payroll_id}/recalculate` - 給与の再計算
|
||||||
|
- `POST /payroll/calculation/{payroll_id}/approve` - 給与の承認
|
||||||
|
- `DELETE /payroll/calculation/{payroll_id}` - 給与データの削除
|
||||||
|
|
||||||
|
## 使い方
|
||||||
|
|
||||||
|
### 初回セットアップフロー
|
||||||
|
|
||||||
|
1. **保険料率の確認**
|
||||||
|
|
||||||
|
- 税率・保険料設定画面で、初期登録された保険料率を確認
|
||||||
|
- 必要に応じて更新
|
||||||
|
|
||||||
|
2. **所得税率表のインポート**
|
||||||
|
|
||||||
|
- CSV ファイルを準備(フォーマット: 月収 From,月収 To,扶養人数,税額)
|
||||||
|
- 税率・保険料設定画面で CSV インポート
|
||||||
|
|
||||||
|
3. **従業員の登録**
|
||||||
|
|
||||||
|
- 従業員管理画面で従業員を登録
|
||||||
|
- 必要に応じて扶養家族情報を追加
|
||||||
|
|
||||||
|
4. **給与設定**
|
||||||
|
- 給与設定画面で各従業員の給与設定を登録
|
||||||
|
- 基本給、手当などを設定
|
||||||
|
|
||||||
|
### 月次給与計算フロー
|
||||||
|
|
||||||
|
1. **勤怠情報の入力**
|
||||||
|
|
||||||
|
- 月次給与計算画面で「新規計算」をクリック
|
||||||
|
- 従業員を選択
|
||||||
|
- 出勤日数、勤務時間、残業時間などを入力
|
||||||
|
|
||||||
|
2. **給与の計算**
|
||||||
|
|
||||||
|
- 「計算実行」ボタンをクリック
|
||||||
|
- システムが自動的に給与を計算
|
||||||
|
|
||||||
|
3. **給与明細の確認**
|
||||||
|
|
||||||
|
- 計算結果を確認
|
||||||
|
- 必要に応じて「再計算」
|
||||||
|
|
||||||
|
4. **承認**
|
||||||
|
- 内容を確認後、「承認」ボタンをクリック
|
||||||
|
- 承認後は変更不可
|
||||||
|
|
||||||
|
## データベーススキーマ
|
||||||
|
|
||||||
|
### 主要テーブル
|
||||||
|
|
||||||
|
1. **employees** - 従業員マスタ
|
||||||
|
2. **dependents** - 扶養家族情報
|
||||||
|
3. **salary_settings** - 給与基本設定
|
||||||
|
4. **insurance_rates** - 社会保険料率マスタ
|
||||||
|
5. **income_tax_table** - 所得税率表
|
||||||
|
6. **monthly_payroll** - 月次給与計算ヘッダー
|
||||||
|
7. **payroll_details** - 給与明細項目詳細
|
||||||
|
|
||||||
|
## 計算ロジック
|
||||||
|
|
||||||
|
### 基本給の計算
|
||||||
|
|
||||||
|
- 月給制: 設定された基本給から欠勤日数を控除
|
||||||
|
- 時給制: 時給 × 勤務時間
|
||||||
|
|
||||||
|
### 残業手当
|
||||||
|
|
||||||
|
- 時給 × 残業時間 × 1.25
|
||||||
|
|
||||||
|
### 休日手当
|
||||||
|
|
||||||
|
- 時給 × 休日出勤時間 × 1.35
|
||||||
|
|
||||||
|
### 社会保険料
|
||||||
|
|
||||||
|
- 健康保険: 総支給額 × 従業員負担率
|
||||||
|
- 厚生年金: 総支給額 × 従業員負担率
|
||||||
|
- 雇用保険: 総支給額 × 従業員負担率
|
||||||
|
|
||||||
|
### 所得税
|
||||||
|
|
||||||
|
1. 課税対象額 = 総支給額 - 通勤手当(非課税限度額まで) - 社会保険料
|
||||||
|
2. 扶養家族数を取得
|
||||||
|
3. 所得税率表から該当する税額を取得
|
||||||
|
|
||||||
|
### 差引支給額
|
||||||
|
|
||||||
|
- 総支給額 - 総控除額
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 所得税率表は国税庁の「給与所得の源泉徴収税額表」に基づいて作成してください
|
||||||
|
- 社会保険料率は年度ごとに変更される可能性があるため、定期的な更新が必要です
|
||||||
|
- 承認済みの給与は変更・削除できません
|
||||||
|
- 扶養家族の情報は所得税計算に影響するため、正確に入力してください
|
||||||
|
|
||||||
|
## 今後の拡張機能
|
||||||
|
|
||||||
|
- 賞与計算機能
|
||||||
|
- 年末調整機能
|
||||||
|
- 給与明細の PDF 出力
|
||||||
|
- 銀行振込データの出力
|
||||||
|
- 給与台帳の出力
|
||||||
|
- 源泉徴収票の作成
|
||||||
605
frontend/payroll-calculation.html
Normal file
605
frontend/payroll-calculation.html
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>給与管理 - 月次給与計算</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<style>
|
||||||
|
.filters {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.filters label {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.payroll-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.payroll-item {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.payroll-item:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-draft {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.status-calculated {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.status-approved {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.payroll-detail {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.detail-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.total-row {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>給与管理 - 月次給与計算</h1>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<label>
|
||||||
|
対象年月:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="filterYear"
|
||||||
|
style="width: 100px"
|
||||||
|
placeholder="年"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="filterMonth"
|
||||||
|
style="width: 80px"
|
||||||
|
min="1"
|
||||||
|
max="12"
|
||||||
|
placeholder="月"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
|
||||||
|
<button class="btn btn-success" onclick="showCalculateForm()">
|
||||||
|
新規計算
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="payrollList" class="payroll-list"></div>
|
||||||
|
|
||||||
|
<!-- 給与計算フォーム(モーダル風) -->
|
||||||
|
<div
|
||||||
|
id="calculateModal"
|
||||||
|
style="
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 800px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h2>給与計算</h2>
|
||||||
|
<form id="calculateForm" onsubmit="calculatePayroll(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>従業員 *</label>
|
||||||
|
<select name="employee_id" id="employeeSelect" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>対象年 *</label>
|
||||||
|
<input type="number" name="payroll_year" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>対象月 *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="payroll_month"
|
||||||
|
min="1"
|
||||||
|
max="12"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>支給日 *</label>
|
||||||
|
<input type="date" name="payment_date" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>勤怠情報</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>出勤日数</label>
|
||||||
|
<input type="number" name="working_days" step="0.5" value="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>勤務時間</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="working_hours"
|
||||||
|
step="0.5"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>残業時間</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="overtime_hours"
|
||||||
|
step="0.5"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>休日出勤時間</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="holiday_hours"
|
||||||
|
step="0.5"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>遅刻時間</label>
|
||||||
|
<input type="number" name="late_hours" step="0.5" value="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>欠勤日数</label>
|
||||||
|
<input type="number" name="absent_days" step="0.5" value="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>その他</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他手当</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="other_allowance"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>住民税</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="resident_tax"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他控除</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="other_deduction"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">計算実行</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="closeCalculateForm()"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 給与明細表示 -->
|
||||||
|
<div
|
||||||
|
id="payrollDetail"
|
||||||
|
class="payroll-detail"
|
||||||
|
style="display: none"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = "http://localhost:8000";
|
||||||
|
|
||||||
|
async function loadEmployees() {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/employees/?is_active=true`
|
||||||
|
);
|
||||||
|
const employees = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById("employeeSelect");
|
||||||
|
select.innerHTML =
|
||||||
|
'<option value="">選択してください</option>' +
|
||||||
|
employees
|
||||||
|
.map(
|
||||||
|
(emp) =>
|
||||||
|
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPayrolls() {
|
||||||
|
const year = document.getElementById("filterYear").value;
|
||||||
|
const month = document.getElementById("filterMonth").value;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (year) params.append("payroll_year", year);
|
||||||
|
if (month) params.append("payroll_month", month);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/?${params}`
|
||||||
|
);
|
||||||
|
const payrolls = await response.json();
|
||||||
|
|
||||||
|
const html = payrolls
|
||||||
|
.map(
|
||||||
|
(p) => `
|
||||||
|
<div class="payroll-item" onclick="viewPayroll(${
|
||||||
|
p.payroll_id
|
||||||
|
})">
|
||||||
|
<div>
|
||||||
|
<strong>${p.payroll_year}年${
|
||||||
|
p.payroll_month
|
||||||
|
}月</strong>
|
||||||
|
<span class="status-badge status-${
|
||||||
|
p.status
|
||||||
|
}">${getStatusLabel(p.status)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
従業員ID: ${p.employee_id} | 支給日: ${
|
||||||
|
p.payment_date
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>差引支給額: ¥${Number(
|
||||||
|
p.net_payment
|
||||||
|
).toLocaleString()}</strong>
|
||||||
|
(総支給: ¥${Number(
|
||||||
|
p.total_payment
|
||||||
|
).toLocaleString()} - 控除: ¥${Number(
|
||||||
|
p.total_deduction
|
||||||
|
).toLocaleString()})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
document.getElementById("payrollList").innerHTML =
|
||||||
|
html || "<p>給与データがありません</p>";
|
||||||
|
} catch (error) {
|
||||||
|
alert("給与一覧の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
draft: "下書き",
|
||||||
|
calculated: "計算済み",
|
||||||
|
approved: "承認済み",
|
||||||
|
paid: "支払済み",
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCalculateForm() {
|
||||||
|
document.getElementById("calculateModal").style.display = "block";
|
||||||
|
loadEmployees();
|
||||||
|
|
||||||
|
// デフォルト値設定
|
||||||
|
const now = new Date();
|
||||||
|
document.querySelector('[name="payroll_year"]').value =
|
||||||
|
now.getFullYear();
|
||||||
|
document.querySelector('[name="payroll_month"]').value =
|
||||||
|
now.getMonth() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCalculateForm() {
|
||||||
|
document.getElementById("calculateModal").style.display = "none";
|
||||||
|
document.getElementById("calculateForm").reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculatePayroll(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/calculate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
alert("給与計算が完了しました");
|
||||||
|
closeCalculateForm();
|
||||||
|
viewPayroll(result.payroll_id);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("計算に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("計算に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewPayroll(payrollId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/${payrollId}`
|
||||||
|
);
|
||||||
|
const payroll = await response.json();
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<h2>${payroll.payroll_year}年${
|
||||||
|
payroll.payroll_month
|
||||||
|
}月 給与明細</h2>
|
||||||
|
<p>従業員ID: ${payroll.employee_id} | 支給日: ${
|
||||||
|
payroll.payment_date
|
||||||
|
}</p>
|
||||||
|
<p>ステータス: <span class="status-badge status-${
|
||||||
|
payroll.status
|
||||||
|
}">${getStatusLabel(payroll.status)}</span></p>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>勤怠情報</h3>
|
||||||
|
<div class="detail-row"><span>出勤日数:</span><span>${
|
||||||
|
payroll.working_days
|
||||||
|
}日</span></div>
|
||||||
|
<div class="detail-row"><span>勤務時間:</span><span>${
|
||||||
|
payroll.working_hours
|
||||||
|
}時間</span></div>
|
||||||
|
<div class="detail-row"><span>残業時間:</span><span>${
|
||||||
|
payroll.overtime_hours
|
||||||
|
}時間</span></div>
|
||||||
|
<div class="detail-row"><span>休日出勤:</span><span>${
|
||||||
|
payroll.holiday_hours
|
||||||
|
}時間</span></div>
|
||||||
|
<div class="detail-row"><span>欠勤日数:</span><span>${
|
||||||
|
payroll.absent_days
|
||||||
|
}日</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>支給項目</h3>
|
||||||
|
<div class="detail-row"><span>基本給:</span><span>¥${Number(
|
||||||
|
payroll.base_salary
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>残業手当:</span><span>¥${Number(
|
||||||
|
payroll.overtime_pay
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>休日手当:</span><span>¥${Number(
|
||||||
|
payroll.holiday_pay
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>通勤手当:</span><span>¥${Number(
|
||||||
|
payroll.commute_allowance
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>その他手当:</span><span>¥${Number(
|
||||||
|
payroll.other_allowance
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
|
||||||
|
payroll.total_payment
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>控除項目</h3>
|
||||||
|
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||||||
|
payroll.health_insurance
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
||||||
|
payroll.pension_insurance
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
||||||
|
payroll.employment_insurance
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>所得税:</span><span>¥${Number(
|
||||||
|
payroll.income_tax
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||||||
|
payroll.resident_tax
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
|
||||||
|
payroll.other_deduction
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
|
||||||
|
payroll.total_deduction
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" style="background:#e7f3ff;">
|
||||||
|
<div class="detail-row total-row" style="font-size:1.5em;">
|
||||||
|
<span>差引支給額:</span>
|
||||||
|
<span>¥${Number(
|
||||||
|
payroll.net_payment
|
||||||
|
).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
payroll.status === "calculated"
|
||||||
|
? `
|
||||||
|
<button class="btn btn-success" onclick="approvePayroll(${payroll.payroll_id})">承認</button>
|
||||||
|
<button class="btn btn-primary" onclick="recalculatePayroll(${payroll.payroll_id})">再計算</button>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("payrollDetail").innerHTML = html;
|
||||||
|
document.getElementById("payrollDetail").style.display = "block";
|
||||||
|
document
|
||||||
|
.getElementById("payrollDetail")
|
||||||
|
.scrollIntoView({ behavior: "smooth" });
|
||||||
|
} catch (error) {
|
||||||
|
alert("給与明細の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approvePayroll(payrollId) {
|
||||||
|
if (!confirm("この給与を承認しますか?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/${payrollId}/approve`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ approved_by: "admin" }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("承認しました");
|
||||||
|
viewPayroll(payrollId);
|
||||||
|
loadPayrolls();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("承認に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("承認に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recalculatePayroll(payrollId) {
|
||||||
|
if (!confirm("給与を再計算しますか?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/${payrollId}/recalculate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("再計算が完了しました");
|
||||||
|
viewPayroll(payrollId);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("再計算に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("再計算に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初期化
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById("filterYear").value = now.getFullYear();
|
||||||
|
document.getElementById("filterMonth").value = now.getMonth() + 1;
|
||||||
|
loadPayrolls();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
371
frontend/payroll-employees.html
Normal file
371
frontend/payroll-employees.html
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>給与管理 - 従業員管理</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<style>
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
}
|
||||||
|
.nav-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
.nav-tab.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.employee-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.employee-item {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.employee-item:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.dependent-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.dependent-item {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>給与管理 - 従業員管理</h1>
|
||||||
|
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<button class="nav-tab active" onclick="showTab('list')">
|
||||||
|
従業員一覧
|
||||||
|
</button>
|
||||||
|
<button class="nav-tab" onclick="showTab('create')">新規登録</button>
|
||||||
|
<button
|
||||||
|
class="nav-tab"
|
||||||
|
onclick="showTab('detail')"
|
||||||
|
id="detailTab"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
従業員詳細
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 従業員一覧タブ -->
|
||||||
|
<div id="tab-list" class="tab-content active">
|
||||||
|
<div class="filters">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="filterActive"
|
||||||
|
checked
|
||||||
|
onchange="loadEmployees()"
|
||||||
|
/>
|
||||||
|
在職中のみ表示
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="employeeList" class="employee-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新規登録タブ -->
|
||||||
|
<div id="tab-create" class="tab-content">
|
||||||
|
<h2>従業員の新規登録</h2>
|
||||||
|
<form id="createForm" onsubmit="createEmployee(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>従業員コード *</label>
|
||||||
|
<input type="text" name="employee_code" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>氏名 *</label>
|
||||||
|
<input type="text" name="name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>フリガナ</label>
|
||||||
|
<input type="text" name="name_kana" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>メールアドレス</label>
|
||||||
|
<input type="email" name="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>電話番号</label>
|
||||||
|
<input type="tel" name="phone" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>入社日 *</label>
|
||||||
|
<input type="date" name="hire_date" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>銀行名</label>
|
||||||
|
<input type="text" name="bank_name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>支店名</label>
|
||||||
|
<input type="text" name="bank_branch" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>口座番号</label>
|
||||||
|
<input type="text" name="bank_account_number" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>口座種別</label>
|
||||||
|
<select name="bank_account_type">
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="普通">普通</option>
|
||||||
|
<option value="当座">当座</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>備考</label>
|
||||||
|
<textarea name="notes" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">登録</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="showTab('list')"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 従業員詳細タブ -->
|
||||||
|
<div id="tab-detail" class="tab-content">
|
||||||
|
<div id="employeeDetail"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = "http://localhost:8000";
|
||||||
|
let currentEmployee = null;
|
||||||
|
|
||||||
|
function showTab(tabName) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".nav-tab")
|
||||||
|
.forEach((tab) => tab.classList.remove("active"));
|
||||||
|
document
|
||||||
|
.querySelectorAll(".tab-content")
|
||||||
|
.forEach((content) => content.classList.remove("active"));
|
||||||
|
|
||||||
|
if (tabName === "list") {
|
||||||
|
document.querySelector(".nav-tab").classList.add("active");
|
||||||
|
document.getElementById("tab-list").classList.add("active");
|
||||||
|
loadEmployees();
|
||||||
|
} else if (tabName === "create") {
|
||||||
|
document.querySelectorAll(".nav-tab")[1].classList.add("active");
|
||||||
|
document.getElementById("tab-create").classList.add("active");
|
||||||
|
} else if (tabName === "detail") {
|
||||||
|
document.getElementById("detailTab").classList.add("active");
|
||||||
|
document.getElementById("tab-detail").classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmployees() {
|
||||||
|
const isActive = document.getElementById("filterActive").checked;
|
||||||
|
const url = `${API_BASE}/payroll/employees/${
|
||||||
|
isActive ? "?is_active=true" : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const employees = await response.json();
|
||||||
|
|
||||||
|
const html = employees
|
||||||
|
.map(
|
||||||
|
(emp) => `
|
||||||
|
<div class="employee-item" onclick="viewEmployee(${
|
||||||
|
emp.employee_id
|
||||||
|
})">
|
||||||
|
<strong>${emp.employee_code}</strong> - ${emp.name}
|
||||||
|
${emp.name_kana ? `(${emp.name_kana})` : ""}
|
||||||
|
<br>
|
||||||
|
<small>入社日: ${emp.hire_date} ${
|
||||||
|
emp.termination_date ? ` | 退職日: ${emp.termination_date}` : ""
|
||||||
|
}</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
document.getElementById("employeeList").innerHTML =
|
||||||
|
html || "<p>従業員が登録されていません</p>";
|
||||||
|
} catch (error) {
|
||||||
|
alert("従業員一覧の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEmployee(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// 空の値を除外
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (data[key] === "") {
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/payroll/employees/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("従業員を登録しました");
|
||||||
|
form.reset();
|
||||||
|
showTab("list");
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("登録に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("登録に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewEmployee(employeeId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/employees/${employeeId}`
|
||||||
|
);
|
||||||
|
const employee = await response.json();
|
||||||
|
currentEmployee = employee;
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<h2>${employee.name} (${employee.employee_code})</h2>
|
||||||
|
<p><strong>フリガナ:</strong> ${
|
||||||
|
employee.name_kana || "-"
|
||||||
|
}</p>
|
||||||
|
<p><strong>メール:</strong> ${employee.email || "-"}</p>
|
||||||
|
<p><strong>電話:</strong> ${employee.phone || "-"}</p>
|
||||||
|
<p><strong>入社日:</strong> ${employee.hire_date}</p>
|
||||||
|
<p><strong>退職日:</strong> ${
|
||||||
|
employee.termination_date || "-"
|
||||||
|
}</p>
|
||||||
|
<p><strong>銀行:</strong> ${employee.bank_name || "-"} ${
|
||||||
|
employee.bank_branch || ""
|
||||||
|
}</p>
|
||||||
|
<p><strong>口座:</strong> ${
|
||||||
|
employee.bank_account_type || ""
|
||||||
|
} ${employee.bank_account_number || "-"}</p>
|
||||||
|
<p><strong>備考:</strong> ${employee.notes || "-"}</p>
|
||||||
|
|
||||||
|
<div class="dependent-list">
|
||||||
|
<h3>扶養家族</h3>
|
||||||
|
${
|
||||||
|
employee.dependents
|
||||||
|
.map(
|
||||||
|
(dep) => `
|
||||||
|
<div class="dependent-item">
|
||||||
|
<strong>${dep.name}</strong> (${
|
||||||
|
dep.relationship
|
||||||
|
})
|
||||||
|
${
|
||||||
|
dep.birth_date
|
||||||
|
? ` - 生年月日: ${dep.birth_date}`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
dep.is_spouse
|
||||||
|
? ' <span style="color:blue;">[配偶者]</span>'
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
dep.is_disabled
|
||||||
|
? ' <span style="color:red;">[障害者]</span>'
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<br><small>有効期間: ${dep.valid_from} ~ ${
|
||||||
|
dep.valid_to || "現在"
|
||||||
|
}</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("") || "<p>扶養家族が登録されていません</p>"
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" onclick="showTab('list')">戻る</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("employeeDetail").innerHTML = html;
|
||||||
|
document.getElementById("detailTab").style.display = "block";
|
||||||
|
showTab("detail");
|
||||||
|
} catch (error) {
|
||||||
|
alert("従業員情報の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初期化
|
||||||
|
loadEmployees();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
378
frontend/payroll-salary-settings.html
Normal file
378
frontend/payroll-salary-settings.html
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>給与管理 - 給与設定</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<style>
|
||||||
|
.employee-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
table th,
|
||||||
|
table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>給与管理 - 給与設定</h1>
|
||||||
|
|
||||||
|
<div class="employee-select">
|
||||||
|
<label><strong>従業員を選択:</strong></label>
|
||||||
|
<select id="employeeSelect" onchange="loadSalarySettings()">
|
||||||
|
<option value="">従業員を選択してください</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settingsContent" style="display: none">
|
||||||
|
<h2>給与設定履歴</h2>
|
||||||
|
<button class="btn btn-primary" onclick="showAddForm()">
|
||||||
|
新規設定追加
|
||||||
|
</button>
|
||||||
|
<div id="settingsList"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="addForm"
|
||||||
|
style="
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
border-radius: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3>給与設定の追加</h3>
|
||||||
|
<form onsubmit="saveSalarySetting(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>基本給 (円) *</label>
|
||||||
|
<input type="number" name="base_salary" step="0.01" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>時給 (円)</label>
|
||||||
|
<input type="number" name="hourly_rate" step="0.01" />
|
||||||
|
<small>時給制の場合に入力してください</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>雇用形態 *</label>
|
||||||
|
<select name="employment_type" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="正社員">正社員</option>
|
||||||
|
<option value="契約社員">契約社員</option>
|
||||||
|
<option value="パート">パート</option>
|
||||||
|
<option value="アルバイト">アルバイト</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>支給形態 *</label>
|
||||||
|
<select name="payment_type" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="月給">月給</option>
|
||||||
|
<option value="時給">時給</option>
|
||||||
|
<option value="日給">日給</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>通勤手当 (円)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="commute_allowance"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他手当 (円)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="other_allowance"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>適用開始日 *</label>
|
||||||
|
<input type="date" name="valid_from" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>適用終了日</label>
|
||||||
|
<input type="date" name="valid_to" />
|
||||||
|
<small>通常は空欄のままにしてください</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="hideAddForm()"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="currentSetting"
|
||||||
|
style="
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3>現在の給与設定</h3>
|
||||||
|
<div id="currentSettingContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = "http://localhost:8000";
|
||||||
|
let currentEmployeeId = null;
|
||||||
|
|
||||||
|
async function loadEmployees() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/employees/?is_active=true`
|
||||||
|
);
|
||||||
|
const employees = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById("employeeSelect");
|
||||||
|
select.innerHTML =
|
||||||
|
'<option value="">従業員を選択してください</option>' +
|
||||||
|
employees
|
||||||
|
.map(
|
||||||
|
(emp) =>
|
||||||
|
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
} catch (error) {
|
||||||
|
alert("従業員一覧の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSalarySettings() {
|
||||||
|
const employeeId = document.getElementById("employeeSelect").value;
|
||||||
|
if (!employeeId) {
|
||||||
|
document.getElementById("settingsContent").style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEmployeeId = employeeId;
|
||||||
|
document.getElementById("settingsContent").style.display = "block";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 給与設定履歴を取得
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/salary/${employeeId}`
|
||||||
|
);
|
||||||
|
const settings = await response.json();
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>適用期間</th>
|
||||||
|
<th>基本給</th>
|
||||||
|
<th>時給</th>
|
||||||
|
<th>雇用形態</th>
|
||||||
|
<th>支給形態</th>
|
||||||
|
<th>通勤手当</th>
|
||||||
|
<th>その他手当</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${settings
|
||||||
|
.map(
|
||||||
|
(s) => `
|
||||||
|
<tr>
|
||||||
|
<td>${s.valid_from} ~ ${
|
||||||
|
s.valid_to || "現在"
|
||||||
|
}</td>
|
||||||
|
<td>¥${Number(
|
||||||
|
s.base_salary
|
||||||
|
).toLocaleString()}</td>
|
||||||
|
<td>${
|
||||||
|
s.hourly_rate
|
||||||
|
? "¥" +
|
||||||
|
Number(s.hourly_rate).toLocaleString()
|
||||||
|
: "-"
|
||||||
|
}</td>
|
||||||
|
<td>${s.employment_type}</td>
|
||||||
|
<td>${s.payment_type}</td>
|
||||||
|
<td>¥${Number(
|
||||||
|
s.commute_allowance
|
||||||
|
).toLocaleString()}</td>
|
||||||
|
<td>¥${Number(
|
||||||
|
s.other_allowance
|
||||||
|
).toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("settingsList").innerHTML = html;
|
||||||
|
|
||||||
|
// 現在の設定を取得
|
||||||
|
loadCurrentSetting(employeeId);
|
||||||
|
} catch (error) {
|
||||||
|
alert("給与設定の取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCurrentSetting(employeeId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/salary/${employeeId}/current`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const setting = await response.json();
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:15px;">
|
||||||
|
<div><strong>基本給:</strong> ¥${Number(
|
||||||
|
setting.base_salary
|
||||||
|
).toLocaleString()}</div>
|
||||||
|
<div><strong>時給:</strong> ${
|
||||||
|
setting.hourly_rate
|
||||||
|
? "¥" +
|
||||||
|
Number(setting.hourly_rate).toLocaleString()
|
||||||
|
: "-"
|
||||||
|
}</div>
|
||||||
|
<div><strong>雇用形態:</strong> ${
|
||||||
|
setting.employment_type
|
||||||
|
}</div>
|
||||||
|
<div><strong>支給形態:</strong> ${
|
||||||
|
setting.payment_type
|
||||||
|
}</div>
|
||||||
|
<div><strong>通勤手当:</strong> ¥${Number(
|
||||||
|
setting.commute_allowance
|
||||||
|
).toLocaleString()}</div>
|
||||||
|
<div><strong>その他手当:</strong> ¥${Number(
|
||||||
|
setting.other_allowance
|
||||||
|
).toLocaleString()}</div>
|
||||||
|
<div><strong>適用期間:</strong> ${
|
||||||
|
setting.valid_from
|
||||||
|
} ~ ${setting.valid_to || "現在"}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("currentSettingContent").innerHTML = html;
|
||||||
|
} else {
|
||||||
|
document.getElementById("currentSettingContent").innerHTML =
|
||||||
|
"<p>給与設定が登録されていません</p>";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById("currentSettingContent").innerHTML =
|
||||||
|
"<p>給与設定の取得に失敗しました</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddForm() {
|
||||||
|
document.getElementById("addForm").style.display = "block";
|
||||||
|
// デフォルト値設定
|
||||||
|
document.querySelector('[name="valid_from"]').value = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAddForm() {
|
||||||
|
document.getElementById("addForm").style.display = "none";
|
||||||
|
document.querySelector("#addForm form").reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSalarySetting(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
data.employee_id = parseInt(currentEmployeeId);
|
||||||
|
data.base_salary = parseFloat(data.base_salary) || 0;
|
||||||
|
data.commute_allowance = parseFloat(data.commute_allowance) || 0;
|
||||||
|
data.other_allowance = parseFloat(data.other_allowance) || 0;
|
||||||
|
|
||||||
|
if (data.hourly_rate) {
|
||||||
|
data.hourly_rate = parseFloat(data.hourly_rate);
|
||||||
|
} else {
|
||||||
|
delete data.hourly_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.valid_to) {
|
||||||
|
delete data.valid_to;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/payroll/settings/salary`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("給与設定を保存しました");
|
||||||
|
hideAddForm();
|
||||||
|
loadSalarySettings();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("保存に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("保存に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初期化
|
||||||
|
loadEmployees();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1298
frontend/payroll-settings.html
Normal file
1298
frontend/payroll-settings.html
Normal file
File diff suppressed because it is too large
Load Diff
121
frontend/payroll.html
Normal file
121
frontend/payroll.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>給与管理システム</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<style>
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.menu-card {
|
||||||
|
padding: 30px;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.menu-card:hover {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
.menu-card h2 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.menu-card p {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.menu-card:hover p {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="../index.html" class="back-link">← メインメニューに戻る</a>
|
||||||
|
|
||||||
|
<h1>給与管理システム</h1>
|
||||||
|
<p>従業員の給与計算、扶養管理、保険料・税金の管理を行います</p>
|
||||||
|
|
||||||
|
<div class="menu-grid">
|
||||||
|
<div class="menu-card" onclick="location.href='payroll-employees.html'">
|
||||||
|
<div class="icon">👥</div>
|
||||||
|
<h2>従業員管理</h2>
|
||||||
|
<p>従業員の基本情報、扶養家族情報を管理します</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="menu-card"
|
||||||
|
onclick="location.href='payroll-salary-settings.html'"
|
||||||
|
>
|
||||||
|
<div class="icon">💰</div>
|
||||||
|
<h2>給与設定</h2>
|
||||||
|
<p>各従業員の給与設定、基本給、手当などを管理します</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="menu-card"
|
||||||
|
onclick="location.href='payroll-calculation.html'"
|
||||||
|
>
|
||||||
|
<div class="icon">🧮</div>
|
||||||
|
<h2>月次給与計算</h2>
|
||||||
|
<p>毎月の給与計算、給与明細の作成・承認を行います</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-card" onclick="location.href='payroll-settings.html'">
|
||||||
|
<div class="icon">⚙️</div>
|
||||||
|
<h2>税率・保険料設定</h2>
|
||||||
|
<p>社会保険料率、所得税率表の管理を行います</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-top: 50px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3>📋 給与システムの使い方</h3>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<strong>従業員管理</strong
|
||||||
|
>で従業員を登録し、扶養家族情報を入力します
|
||||||
|
</li>
|
||||||
|
<li><strong>給与設定</strong>で各従業員の基本給や手当を設定します</li>
|
||||||
|
<li>
|
||||||
|
<strong>税率・保険料設定</strong
|
||||||
|
>で社会保険料率や所得税率表を確認・更新します
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>月次給与計算</strong
|
||||||
|
>で勤怠情報を入力し、給与を計算・承認します
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
reports/tmp/01-07 (3).xls
Normal file
BIN
reports/tmp/01-07 (3).xls
Normal file
Binary file not shown.
2
メモ.txt
2
メモ.txt
@@ -78,6 +78,8 @@ command = C:\Users\zxh_y\AppData\Local\Programs\Python\Python312\python.exe -m v
|
|||||||
cd C:\workspace\njts-accounting-core\backend
|
cd C:\workspace\njts-accounting-core\backend
|
||||||
.venv\Scripts\activate
|
.venv\Scripts\activate
|
||||||
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
||||||
|
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
||||||
|
& .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
||||||
|
|
||||||
http://localhost:18080/docs
|
http://localhost:18080/docs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user