From 8a00de8f036119f444152b663d3f31dc4fd64990 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 20 Jan 2026 11:15:49 +0900 Subject: [PATCH] =?UTF-8?q?=E7=B5=A6=E4=B8=8E=E7=AE=A1=E7=90=86=E3=82=B7?= =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=A0=E6=96=B0=E8=A6=8F=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/add_tax_year_field.py | 43 + backend/app/main.py | 36 +- backend/app/payroll/__init__.py | 3 + backend/app/payroll/bonus/__init__.py | 1 + backend/app/payroll/bonus/router.py | 139 ++ backend/app/payroll/bonus/schemas.py | 63 + backend/app/payroll/bonus/service.py | 235 +++ backend/app/payroll/calculation/__init__.py | 3 + backend/app/payroll/calculation/router.py | 284 ++++ backend/app/payroll/calculation/schemas.py | 129 ++ backend/app/payroll/calculation/service.py | 317 ++++ backend/app/payroll/employees/__init__.py | 3 + backend/app/payroll/employees/router.py | 196 +++ backend/app/payroll/employees/schemas.py | 109 ++ backend/app/payroll/settings/__init__.py | 3 + backend/app/payroll/settings/router.py | 869 +++++++++++ backend/app/payroll/settings/schemas.py | 220 +++ backend/app/static/payroll-calculation.html | 434 ++++++ backend/app/static/payroll-employees.html | 302 ++++ .../app/static/payroll-salary-settings.html | 291 ++++ backend/app/static/payroll-settings.html | 326 +++++ backend/app/static/payroll.html | 99 ++ backend/check_excel.py | 20 + backend/check_precision.py | 59 + backend/create_payroll_db.py | 72 + backend/fix_precision.py | 68 + backend/income_tax_sample.csv | 51 + backend/requirements.txt | Bin 686 -> 820 bytes backend/revert_precision.py | 56 + backend/sql/add_tax_year.sql | 17 + backend/sql/payroll_enhancements.sql | 116 ++ backend/sql/payroll_migration_20260119.sql | 172 +++ backend/sql/payroll_schema.sql | 238 +++ backend/sql/payroll_schema_complete.sql | 436 ++++++ backend/update_precision.py | 79 + docs/payroll_enhancements.md | 274 ++++ docs/payroll_quickstart.md | 171 +++ docs/payroll_system.md | 221 +++ frontend/payroll-calculation.html | 605 ++++++++ frontend/payroll-employees.html | 371 +++++ frontend/payroll-salary-settings.html | 378 +++++ frontend/payroll-settings.html | 1298 +++++++++++++++++ frontend/payroll.html | 121 ++ reports/tmp/01-07 (3).xls | Bin 0 -> 81408 bytes メモ.txt | 2 + 45 files changed, 8925 insertions(+), 5 deletions(-) create mode 100644 backend/add_tax_year_field.py create mode 100644 backend/app/payroll/__init__.py create mode 100644 backend/app/payroll/bonus/__init__.py create mode 100644 backend/app/payroll/bonus/router.py create mode 100644 backend/app/payroll/bonus/schemas.py create mode 100644 backend/app/payroll/bonus/service.py create mode 100644 backend/app/payroll/calculation/__init__.py create mode 100644 backend/app/payroll/calculation/router.py create mode 100644 backend/app/payroll/calculation/schemas.py create mode 100644 backend/app/payroll/calculation/service.py create mode 100644 backend/app/payroll/employees/__init__.py create mode 100644 backend/app/payroll/employees/router.py create mode 100644 backend/app/payroll/employees/schemas.py create mode 100644 backend/app/payroll/settings/__init__.py create mode 100644 backend/app/payroll/settings/router.py create mode 100644 backend/app/payroll/settings/schemas.py create mode 100644 backend/app/static/payroll-calculation.html create mode 100644 backend/app/static/payroll-employees.html create mode 100644 backend/app/static/payroll-salary-settings.html create mode 100644 backend/app/static/payroll-settings.html create mode 100644 backend/app/static/payroll.html create mode 100644 backend/check_excel.py create mode 100644 backend/check_precision.py create mode 100644 backend/create_payroll_db.py create mode 100644 backend/fix_precision.py create mode 100644 backend/income_tax_sample.csv create mode 100644 backend/revert_precision.py create mode 100644 backend/sql/add_tax_year.sql create mode 100644 backend/sql/payroll_enhancements.sql create mode 100644 backend/sql/payroll_migration_20260119.sql create mode 100644 backend/sql/payroll_schema.sql create mode 100644 backend/sql/payroll_schema_complete.sql create mode 100644 backend/update_precision.py create mode 100644 docs/payroll_enhancements.md create mode 100644 docs/payroll_quickstart.md create mode 100644 docs/payroll_system.md create mode 100644 frontend/payroll-calculation.html create mode 100644 frontend/payroll-employees.html create mode 100644 frontend/payroll-salary-settings.html create mode 100644 frontend/payroll-settings.html create mode 100644 frontend/payroll.html create mode 100644 reports/tmp/01-07 (3).xls diff --git a/backend/add_tax_year_field.py b/backend/add_tax_year_field.py new file mode 100644 index 0000000..7cbf8f2 --- /dev/null +++ b/backend/add_tax_year_field.py @@ -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完了!") diff --git a/backend/app/main.py b/backend/app/main.py index 553727d..1616c85 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -81,14 +81,40 @@ app.include_router(year_locks.router) from app.routers import cash app.include_router(cash.router) +# ───────────────────────── +# 給与モジュール (Payroll Module) +# ───────────────────────── +from app.payroll.employees.router import router as payroll_employees_router +app.include_router(payroll_employees_router) + +from app.payroll.settings.router import router as payroll_settings_router +app.include_router(payroll_settings_router) + +from app.payroll.calculation.router import router as payroll_calculation_router +app.include_router(payroll_calculation_router) + +from app.payroll.bonus.router import router as payroll_bonus_router +app.include_router(payroll_bonus_router) + from fastapi.staticfiles import StaticFiles +import os -app.mount( - "/static", - StaticFiles(directory="app/static", html=True), - name="static", -) +# 静的ファイル配信(コメントアウト - ディレクトリが存在しない) +# app.mount( +# "/static", +# StaticFiles(directory="app/static", html=True), +# name="static", +# ) + +# フロントエンドファイル配信(コメントアウト - file://プロトコルで直接アクセスするため不要) +# frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend") +# if os.path.exists(frontend_path): +# app.mount( +# "/", +# StaticFiles(directory=frontend_path, html=True), +# name="frontend", +# ) diff --git a/backend/app/payroll/__init__.py b/backend/app/payroll/__init__.py new file mode 100644 index 0000000..d80ee17 --- /dev/null +++ b/backend/app/payroll/__init__.py @@ -0,0 +1,3 @@ +""" +給与管理モジュール (Payroll Module) +""" diff --git a/backend/app/payroll/bonus/__init__.py b/backend/app/payroll/bonus/__init__.py new file mode 100644 index 0000000..50b9161 --- /dev/null +++ b/backend/app/payroll/bonus/__init__.py @@ -0,0 +1 @@ +# Bonus Module diff --git a/backend/app/payroll/bonus/router.py b/backend/app/payroll/bonus/router.py new file mode 100644 index 0000000..4f1446d --- /dev/null +++ b/backend/app/payroll/bonus/router.py @@ -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() diff --git a/backend/app/payroll/bonus/schemas.py b/backend/app/payroll/bonus/schemas.py new file mode 100644 index 0000000..ff7e4b8 --- /dev/null +++ b/backend/app/payroll/bonus/schemas.py @@ -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 diff --git a/backend/app/payroll/bonus/service.py b/backend/app/payroll/bonus/service.py new file mode 100644 index 0000000..198c59d --- /dev/null +++ b/backend/app/payroll/bonus/service.py @@ -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 + } diff --git a/backend/app/payroll/calculation/__init__.py b/backend/app/payroll/calculation/__init__.py new file mode 100644 index 0000000..084a561 --- /dev/null +++ b/backend/app/payroll/calculation/__init__.py @@ -0,0 +1,3 @@ +""" +月次給与計算 (Monthly Payroll Calculation) +""" diff --git a/backend/app/payroll/calculation/router.py b/backend/app/payroll/calculation/router.py new file mode 100644 index 0000000..fedc8e8 --- /dev/null +++ b/backend/app/payroll/calculation/router.py @@ -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() diff --git a/backend/app/payroll/calculation/schemas.py b/backend/app/payroll/calculation/schemas.py new file mode 100644 index 0000000..00e4ec9 --- /dev/null +++ b/backend/app/payroll/calculation/schemas.py @@ -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 diff --git a/backend/app/payroll/calculation/service.py b/backend/app/payroll/calculation/service.py new file mode 100644 index 0000000..d9128b0 --- /dev/null +++ b/backend/app/payroll/calculation/service.py @@ -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 + } diff --git a/backend/app/payroll/employees/__init__.py b/backend/app/payroll/employees/__init__.py new file mode 100644 index 0000000..a4950de --- /dev/null +++ b/backend/app/payroll/employees/__init__.py @@ -0,0 +1,3 @@ +""" +従業員管理 (Employee Management) +""" diff --git a/backend/app/payroll/employees/router.py b/backend/app/payroll/employees/router.py new file mode 100644 index 0000000..f2a3fe1 --- /dev/null +++ b/backend/app/payroll/employees/router.py @@ -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() diff --git a/backend/app/payroll/employees/schemas.py b/backend/app/payroll/employees/schemas.py new file mode 100644 index 0000000..5edda0e --- /dev/null +++ b/backend/app/payroll/employees/schemas.py @@ -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] = [] diff --git a/backend/app/payroll/settings/__init__.py b/backend/app/payroll/settings/__init__.py new file mode 100644 index 0000000..0bee403 --- /dev/null +++ b/backend/app/payroll/settings/__init__.py @@ -0,0 +1,3 @@ +""" +給与設定・税率・保険料管理 (Settings Management) +""" diff --git a/backend/app/payroll/settings/router.py b/backend/app/payroll/settings/router.py new file mode 100644 index 0000000..bbc8b82 --- /dev/null +++ b/backend/app/payroll/settings/router.py @@ -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() + diff --git a/backend/app/payroll/settings/schemas.py b/backend/app/payroll/settings/schemas.py new file mode 100644 index 0000000..d94a83c --- /dev/null +++ b/backend/app/payroll/settings/schemas.py @@ -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 diff --git a/backend/app/static/payroll-calculation.html b/backend/app/static/payroll-calculation.html new file mode 100644 index 0000000..4619a0f --- /dev/null +++ b/backend/app/static/payroll-calculation.html @@ -0,0 +1,434 @@ + + + + + + 給与管理 - 月次給与計算 + + + + +
+

給与管理 - 月次給与計算

+ +
+ + + + +
+ +
+ + + + + + +
+ + + + diff --git a/backend/app/static/payroll-employees.html b/backend/app/static/payroll-employees.html new file mode 100644 index 0000000..ae63b17 --- /dev/null +++ b/backend/app/static/payroll-employees.html @@ -0,0 +1,302 @@ + + + + + + 給与管理 - 従業員管理 + + + + +
+

給与管理 - 従業員管理

+ + + + +
+
+ +
+
+
+ + +
+

従業員の新規登録

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+
+
+ + + + diff --git a/backend/app/static/payroll-salary-settings.html b/backend/app/static/payroll-salary-settings.html new file mode 100644 index 0000000..0f9d7de --- /dev/null +++ b/backend/app/static/payroll-salary-settings.html @@ -0,0 +1,291 @@ + + + + + + 給与管理 - 給与設定 + + + + +
+

給与管理 - 給与設定

+ +
+ + +
+ + +
+ + + + diff --git a/backend/app/static/payroll-settings.html b/backend/app/static/payroll-settings.html new file mode 100644 index 0000000..3c4cc4f --- /dev/null +++ b/backend/app/static/payroll-settings.html @@ -0,0 +1,326 @@ + + + + + + 給与管理 - 設定 + + + + +
+

給与管理 - 設定

+ + + + +
+

社会保険料率設定

+ +
+ + +
+ + +
+

所得税率表

+
+ + +
+ + +
+
+
+ + + + diff --git a/backend/app/static/payroll.html b/backend/app/static/payroll.html new file mode 100644 index 0000000..f09ae88 --- /dev/null +++ b/backend/app/static/payroll.html @@ -0,0 +1,99 @@ + + + + + + 給与管理システム + + + + +
+ ← メインメニューに戻る + +

給与管理システム

+

従業員の給与計算、扶養管理、保険料・税金の管理を行います

+ + + +
+

📋 給与システムの使い方

+
    +
  1. 従業員管理で従業員を登録し、扶養家族情報を入力します
  2. +
  3. 給与設定で各従業員の基本給や手当を設定します
  4. +
  5. 税率・保険料設定で社会保険料率や所得税率表を確認・更新します
  6. +
  7. 月次給与計算で勤怠情報を入力し、給与を計算・承認します
  8. +
+
+
+ + diff --git a/backend/check_excel.py b/backend/check_excel.py new file mode 100644 index 0000000..7f0af5b --- /dev/null +++ b/backend/check_excel.py @@ -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)}") diff --git a/backend/check_precision.py b/backend/check_precision.py new file mode 100644 index 0000000..75ef7b3 --- /dev/null +++ b/backend/check_precision.py @@ -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() diff --git a/backend/create_payroll_db.py b/backend/create_payroll_db.py new file mode 100644 index 0000000..12ad93e --- /dev/null +++ b/backend/create_payroll_db.py @@ -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() diff --git a/backend/fix_precision.py b/backend/fix_precision.py new file mode 100644 index 0000000..f6bab3d --- /dev/null +++ b/backend/fix_precision.py @@ -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% と表示されます。") diff --git a/backend/income_tax_sample.csv b/backend/income_tax_sample.csv new file mode 100644 index 0000000..cd36884 --- /dev/null +++ b/backend/income_tax_sample.csv @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt index 1a34c782624dcae7065d78f56a64681cfc3bbdce..54628d197c4b49fea6360363e33c768ed4e3b3d1 100644 GIT binary patch delta 137 zcmZ3-x`l1RF6~r?T!uu3Ooki=U4}9sD+kC<0n#N5`3yx2wm@japvPbg#0C?$M$6_i z6fmSR?ji2owMS diff --git a/backend/revert_precision.py b/backend/revert_precision.py new file mode 100644 index 0000000..dea1c69 --- /dev/null +++ b/backend/revert_precision.py @@ -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桁に丸めることができます。") diff --git a/backend/sql/add_tax_year.sql b/backend/sql/add_tax_year.sql new file mode 100644 index 0000000..9b2454e --- /dev/null +++ b/backend/sql/add_tax_year.sql @@ -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 '適用年度'; diff --git a/backend/sql/payroll_enhancements.sql b/backend/sql/payroll_enhancements.sql new file mode 100644 index 0000000..2132aba --- /dev/null +++ b/backend/sql/payroll_enhancements.sql @@ -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歳以上)'); diff --git a/backend/sql/payroll_migration_20260119.sql b/backend/sql/payroll_migration_20260119.sql new file mode 100644 index 0000000..50cd425 --- /dev/null +++ b/backend/sql/payroll_migration_20260119.sql @@ -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 = '介護保険'; -- 介護保険料率の確認 diff --git a/backend/sql/payroll_schema.sql b/backend/sql/payroll_schema.sql new file mode 100644 index 0000000..8c5e516 --- /dev/null +++ b/backend/sql/payroll_schema.sql @@ -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%'); diff --git a/backend/sql/payroll_schema_complete.sql b/backend/sql/payroll_schema_complete.sql new file mode 100644 index 0000000..4341190 --- /dev/null +++ b/backend/sql/payroll_schema_complete.sql @@ -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; -- 扶養判定条件確認 diff --git a/backend/update_precision.py b/backend/update_precision.py new file mode 100644 index 0000000..ecf0074 --- /dev/null +++ b/backend/update_precision.py @@ -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完了!") diff --git a/docs/payroll_enhancements.md b/docs/payroll_enhancements.md new file mode 100644 index 0000000..2a5194f --- /dev/null +++ b/docs/payroll_enhancements.md @@ -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 接口调用。下一步建议创建前端管理画面以便更方便地操作这些功能。 diff --git a/docs/payroll_quickstart.md b/docs/payroll_quickstart.md new file mode 100644 index 0000000..3817b9e --- /dev/null +++ b/docs/payroll_quickstart.md @@ -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` を参照してください。 diff --git a/docs/payroll_system.md b/docs/payroll_system.md new file mode 100644 index 0000000..5ca0a5a --- /dev/null +++ b/docs/payroll_system.md @@ -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 出力 +- 銀行振込データの出力 +- 給与台帳の出力 +- 源泉徴収票の作成 diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html new file mode 100644 index 0000000..5bc8043 --- /dev/null +++ b/frontend/payroll-calculation.html @@ -0,0 +1,605 @@ + + + + + + 給与管理 - 月次給与計算 + + + + +
+

給与管理 - 月次給与計算

+ +
+ + + + +
+ +
+ + +
+

給与計算

+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

勤怠情報

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

その他

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + + +
+ + + + diff --git a/frontend/payroll-employees.html b/frontend/payroll-employees.html new file mode 100644 index 0000000..d338cb7 --- /dev/null +++ b/frontend/payroll-employees.html @@ -0,0 +1,371 @@ + + + + + + 給与管理 - 従業員管理 + + + + +
+

給与管理 - 従業員管理

+ + + + +
+
+ +
+
+
+ + +
+

従業員の新規登録

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+
+
+ + + + diff --git a/frontend/payroll-salary-settings.html b/frontend/payroll-salary-settings.html new file mode 100644 index 0000000..8305d7f --- /dev/null +++ b/frontend/payroll-salary-settings.html @@ -0,0 +1,378 @@ + + + + + + 給与管理 - 給与設定 + + + + +
+

給与管理 - 給与設定

+ +
+ + +
+ + +
+ + + + diff --git a/frontend/payroll-settings.html b/frontend/payroll-settings.html new file mode 100644 index 0000000..f42e281 --- /dev/null +++ b/frontend/payroll-settings.html @@ -0,0 +1,1298 @@ + + + + + + 給与管理 - 設定 + + + + +
+

給与管理 - 設定

+ + + + +
+

社会保険料率設定

+ +
+ 設定のポイント: +
    +
  • 年度ごとに設定します(複数の料率を入力可能)
  • +
  • 給与計算と賞与計算で異なる料率を設定できます
  • +
  • 事業所所在地(都道府県)ごとに設定できます
  • +
  • 介護保険の適用年齢は設定可能です(デフォルト: 40歳以上)
  • +
+
+ + +
+
+
+ + + ※空欄で全て表示 +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ + +
+

子ども・子育て拠出金率設定

+ +
+ 子ども・子育て拠出金について: +
    +
  • 事業主のみが負担する拠出金です(従業員負担なし)
  • +
  • 標準報酬月額の上限額を設定できます
  • +
  • 年度ごとに料率を設定します
  • +
+
+ + +
+ + +
+ + +
+

社会保険料上限設定

+ +
+ 上限設定について: +
    +
  • 健康保険と厚生年金の標準報酬月額上限を設定します
  • +
  • 通勤手当の非課税限度額を設定します
  • +
  • 年度ごとに変更される場合があります
  • +
+
+ + +
+ + +
+ + +
+

所得税率表

+
+ + +
+ + +
+
+
+ + + + diff --git a/frontend/payroll.html b/frontend/payroll.html new file mode 100644 index 0000000..606af90 --- /dev/null +++ b/frontend/payroll.html @@ -0,0 +1,121 @@ + + + + + + 給与管理システム + + + + +
+ ← メインメニューに戻る + +

給与管理システム

+

従業員の給与計算、扶養管理、保険料・税金の管理を行います

+ + + +
+

📋 給与システムの使い方

+
    +
  1. + 従業員管理で従業員を登録し、扶養家族情報を入力します +
  2. +
  3. 給与設定で各従業員の基本給や手当を設定します
  4. +
  5. + 税率・保険料設定で社会保険料率や所得税率表を確認・更新します +
  6. +
  7. + 月次給与計算で勤怠情報を入力し、給与を計算・承認します +
  8. +
+
+
+ + diff --git a/reports/tmp/01-07 (3).xls b/reports/tmp/01-07 (3).xls new file mode 100644 index 0000000000000000000000000000000000000000..0b4696a7b683d2dcfc6f82944e8ef209b283cf4a GIT binary patch literal 81408 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 literal 0 HcmV?d00001 diff --git a/メモ.txt b/メモ.txt index 618329f..c58aa64 100644 --- a/メモ.txt +++ b/メモ.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 .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 +& .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 127.0.0.1 --port 18080 http://localhost:18080/docs