修正
This commit is contained in:
36
backend/add_my_number.py
Normal file
36
backend/add_my_number.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import psycopg
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
# データベース接続
|
||||||
|
conn = psycopg.connect(
|
||||||
|
host="192.168.0.61",
|
||||||
|
port=55432,
|
||||||
|
dbname="njts_acct",
|
||||||
|
user="njts_app",
|
||||||
|
password="njts_app2025",
|
||||||
|
row_factory=dict_row
|
||||||
|
)
|
||||||
|
|
||||||
|
# SQLファイルを読み込んで実行
|
||||||
|
with open('sql/add_my_number_field.sql', encoding='utf-8') as f:
|
||||||
|
sql = f.read()
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(sql)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print("マイナンバーフィールドの追加が完了しました")
|
||||||
|
|
||||||
|
# 確認
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name, data_type, character_maximum_length
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'employees' AND column_name = 'my_number'
|
||||||
|
""")
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
print(f"✓ フィールド追加確認: {result}")
|
||||||
|
else:
|
||||||
|
print("✗ フィールドが見つかりません")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -81,6 +81,9 @@ app.include_router(year_locks.router)
|
|||||||
from app.routers import cash
|
from app.routers import cash
|
||||||
app.include_router(cash.router)
|
app.include_router(cash.router)
|
||||||
|
|
||||||
|
from app.routers import file_upload
|
||||||
|
app.include_router(file_upload.router)
|
||||||
|
|
||||||
# ─────────────────────────
|
# ─────────────────────────
|
||||||
# 給与モジュール (Payroll Module)
|
# 給与モジュール (Payroll Module)
|
||||||
# ─────────────────────────
|
# ─────────────────────────
|
||||||
@@ -90,6 +93,9 @@ app.include_router(payroll_employees_router)
|
|||||||
from app.payroll.settings.router import router as payroll_settings_router
|
from app.payroll.settings.router import router as payroll_settings_router
|
||||||
app.include_router(payroll_settings_router)
|
app.include_router(payroll_settings_router)
|
||||||
|
|
||||||
|
from app.payroll.settings.standard_remuneration_router import router as standard_remuneration_router
|
||||||
|
app.include_router(standard_remuneration_router)
|
||||||
|
|
||||||
from app.payroll.calculation.router import router as payroll_calculation_router
|
from app.payroll.calculation.router import router as payroll_calculation_router
|
||||||
app.include_router(payroll_calculation_router)
|
app.include_router(payroll_calculation_router)
|
||||||
|
|
||||||
@@ -99,6 +105,16 @@ app.include_router(payroll_bonus_router)
|
|||||||
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# アップロードファイル用の静的ファイル配信
|
||||||
|
uploads_dir = Path("uploads")
|
||||||
|
uploads_dir.mkdir(exist_ok=True)
|
||||||
|
app.mount(
|
||||||
|
"/uploads",
|
||||||
|
StaticFiles(directory="uploads"),
|
||||||
|
name="uploads",
|
||||||
|
)
|
||||||
|
|
||||||
# 静的ファイル配信(コメントアウト - ディレクトリが存在しない)
|
# 静的ファイル配信(コメントアウト - ディレクトリが存在しない)
|
||||||
# app.mount(
|
# app.mount(
|
||||||
|
|||||||
@@ -11,20 +11,22 @@ class BonusCalculationService:
|
|||||||
"""賞与計算サービス"""
|
"""賞与計算サービス"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_insurance_rate(rate_type: str, target_date: date) -> Dict[str, Any]:
|
def get_insurance_rate(rate_type: str, target_date: date, calculation_target: str = '賞与') -> Dict[str, Any]:
|
||||||
"""保険料率を取得"""
|
"""保険料率を取得(賞与用)"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
|
# rate_yearで年度を判定
|
||||||
|
target_year = target_date.year
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM insurance_rates
|
SELECT * FROM insurance_rates
|
||||||
WHERE rate_type = %s
|
WHERE rate_type = %s
|
||||||
AND valid_from <= %s
|
AND rate_year = %s
|
||||||
AND (valid_to IS NULL OR valid_to >= %s)
|
AND calculation_target = %s
|
||||||
ORDER BY valid_from DESC
|
ORDER BY rate_id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(rate_type, target_date, target_date)
|
(rate_type, target_year, calculation_target)
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
@@ -134,10 +136,15 @@ class BonusCalculationService:
|
|||||||
)
|
)
|
||||||
employee_info = cur.fetchone()
|
employee_info = cur.fetchone()
|
||||||
|
|
||||||
# 年齢を計算
|
# 年齢を計算(介護保険は40歳以上65歳未満が対象)
|
||||||
age = 0
|
age = 0
|
||||||
if employee_info and employee_info["birth_date"]:
|
if employee_info and employee_info["birth_date"]:
|
||||||
age = (payment_date - employee_info["birth_date"]).days // 365
|
birth_date = employee_info["birth_date"]
|
||||||
|
# より正確な年齢計算
|
||||||
|
age = payment_date.year - birth_date.year
|
||||||
|
# 誕生日がまだ来ていない場合は-1
|
||||||
|
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||||||
|
age -= 1
|
||||||
|
|
||||||
# 健康保険(賞与額の1/1000で計算)
|
# 健康保険(賞与額の1/1000で計算)
|
||||||
health_insurance_rate = BonusCalculationService.get_insurance_rate("健康保険", payment_date)
|
health_insurance_rate = BonusCalculationService.get_insurance_rate("健康保険", payment_date)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class MonthlyPayroll(MonthlyPayrollBase):
|
|||||||
|
|
||||||
# 控除項目
|
# 控除項目
|
||||||
health_insurance: Decimal
|
health_insurance: Decimal
|
||||||
|
care_insurance: Decimal
|
||||||
pension_insurance: Decimal
|
pension_insurance: Decimal
|
||||||
employment_insurance: Decimal
|
employment_insurance: Decimal
|
||||||
income_tax: Decimal
|
income_tax: Decimal
|
||||||
|
|||||||
@@ -12,17 +12,21 @@ class PayrollCalculationService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_active_dependents_count(employee_id: int, target_date: date) -> int:
|
def get_active_dependents_count(employee_id: int, target_date: date) -> int:
|
||||||
"""有効な扶養家族数を取得"""
|
"""有効な扶養家族数を取得(源泉税控除対象のみ:19歳以上)"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
|
# その年の12月31日時点で19歳以上の扶養家族のみをカウント
|
||||||
|
# 16-18歳は住民税のみ対象なので源泉税控除対象外
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT COUNT(*) as count FROM dependents
|
SELECT COUNT(*) as count FROM dependents
|
||||||
WHERE employee_id = %s
|
WHERE employee_id = %s
|
||||||
AND valid_from <= %s
|
AND valid_from <= %s
|
||||||
AND (valid_to IS NULL OR valid_to >= %s)
|
AND (valid_to IS NULL OR valid_to >= %s)
|
||||||
|
AND birth_date IS NOT NULL
|
||||||
|
AND EXTRACT(YEAR FROM AGE(DATE(EXTRACT(YEAR FROM %s) || '-12-31'), birth_date)) >= 19
|
||||||
""",
|
""",
|
||||||
(employee_id, target_date, target_date)
|
(employee_id, target_date, target_date, target_date)
|
||||||
)
|
)
|
||||||
result = cur.fetchone()
|
result = cur.fetchone()
|
||||||
return result["count"] if result else 0
|
return result["count"] if result else 0
|
||||||
@@ -46,20 +50,22 @@ class PayrollCalculationService:
|
|||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_insurance_rate(rate_type: str, target_date: date) -> Dict[str, Any]:
|
def get_insurance_rate(rate_type: str, target_date: date, calculation_target: str = '給与') -> Dict[str, Any]:
|
||||||
"""保険料率を取得"""
|
"""保険料率を取得"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
|
# rate_yearで年度を判定
|
||||||
|
target_year = target_date.year
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM insurance_rates
|
SELECT * FROM insurance_rates
|
||||||
WHERE rate_type = %s
|
WHERE rate_type = %s
|
||||||
AND valid_from <= %s
|
AND rate_year = %s
|
||||||
AND (valid_to IS NULL OR valid_to >= %s)
|
AND calculation_target = %s
|
||||||
ORDER BY valid_from DESC
|
ORDER BY rate_id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(rate_type, target_date, target_date)
|
(rate_type, target_year, calculation_target)
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
@@ -202,10 +208,15 @@ class PayrollCalculationService:
|
|||||||
)
|
)
|
||||||
employee_info = cur.fetchone()
|
employee_info = cur.fetchone()
|
||||||
|
|
||||||
# 年齢を計算(介護保険は40歳以上が対象)
|
# 年齢を計算(介護保険は40歳以上65歳未満が対象)
|
||||||
age = 0
|
age = 0
|
||||||
if employee_info and employee_info["birth_date"]:
|
if employee_info and employee_info["birth_date"]:
|
||||||
age = (payment_date - employee_info["birth_date"]).days // 365
|
birth_date = employee_info["birth_date"]
|
||||||
|
# より正確な年齢計算
|
||||||
|
age = payment_date.year - birth_date.year
|
||||||
|
# 誕生日がまだ来ていない場合は-1
|
||||||
|
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||||||
|
age -= 1
|
||||||
|
|
||||||
# 健康保険(上限適用後の標準報酬月額で計算)
|
# 健康保険(上限適用後の標準報酬月額で計算)
|
||||||
health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date)
|
health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date)
|
||||||
|
|||||||
@@ -18,19 +18,24 @@ def create_employee(employee: schemas.EmployeeCreate):
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO employees (
|
INSERT INTO employees (
|
||||||
employee_code, name, name_kana, birth_date, gender,
|
employee_code, name, name_kana, birth_date, gender, my_number,
|
||||||
email, phone, postal_code, address, hire_date, termination_date, is_active,
|
email, phone, postal_code, address, hire_date, termination_date, is_active,
|
||||||
bank_name, bank_branch, bank_account_number, bank_account_type, notes
|
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)
|
photo_url, my_number_card_image_url, residence_card_image_url,
|
||||||
|
health_insurance_card_image_url, other_id_image_url
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
employee.employee_code, employee.name, employee.name_kana,
|
employee.employee_code, employee.name, employee.name_kana,
|
||||||
employee.birth_date, employee.gender,
|
employee.birth_date, employee.gender, employee.my_number,
|
||||||
employee.email, employee.phone, employee.postal_code, employee.address,
|
employee.email, employee.phone, employee.postal_code, employee.address,
|
||||||
employee.hire_date, employee.termination_date, employee.is_active,
|
employee.hire_date, employee.termination_date, employee.is_active,
|
||||||
employee.bank_name, employee.bank_branch,
|
employee.bank_name, employee.bank_branch,
|
||||||
employee.bank_account_number, employee.bank_account_type, employee.notes
|
employee.bank_account_number, employee.bank_account_type, employee.notes,
|
||||||
|
employee.photo_url, employee.my_number_card_image_url,
|
||||||
|
employee.residence_card_image_url, employee.health_insurance_card_image_url,
|
||||||
|
employee.other_id_image_url
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
result = cur.fetchone()
|
result = cur.fetchone()
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class EmployeeBase(BaseModel):
|
|||||||
name_kana: Optional[str] = None
|
name_kana: Optional[str] = None
|
||||||
birth_date: Optional[date] = None
|
birth_date: Optional[date] = None
|
||||||
gender: Optional[str] = None
|
gender: Optional[str] = None
|
||||||
|
my_number: Optional[str] = None
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
postal_code: Optional[str] = None
|
postal_code: Optional[str] = None
|
||||||
@@ -66,6 +67,11 @@ class EmployeeBase(BaseModel):
|
|||||||
bank_account_number: Optional[str] = None
|
bank_account_number: Optional[str] = None
|
||||||
bank_account_type: Optional[str] = None # 普通/当座
|
bank_account_type: Optional[str] = None # 普通/当座
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
photo_url: Optional[str] = None
|
||||||
|
my_number_card_image_url: Optional[str] = None
|
||||||
|
residence_card_image_url: Optional[str] = None
|
||||||
|
health_insurance_card_image_url: Optional[str] = None
|
||||||
|
other_id_image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class EmployeeCreate(EmployeeBase):
|
class EmployeeCreate(EmployeeBase):
|
||||||
@@ -80,6 +86,7 @@ class EmployeeUpdate(BaseModel):
|
|||||||
name_kana: Optional[str] = None
|
name_kana: Optional[str] = None
|
||||||
birth_date: Optional[date] = None
|
birth_date: Optional[date] = None
|
||||||
gender: Optional[str] = None
|
gender: Optional[str] = None
|
||||||
|
my_number: Optional[str] = None
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
postal_code: Optional[str] = None
|
postal_code: Optional[str] = None
|
||||||
@@ -92,6 +99,11 @@ class EmployeeUpdate(BaseModel):
|
|||||||
bank_account_number: Optional[str] = None
|
bank_account_number: Optional[str] = None
|
||||||
bank_account_type: Optional[str] = None
|
bank_account_type: Optional[str] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
photo_url: Optional[str] = None
|
||||||
|
my_number_card_image_url: Optional[str] = None
|
||||||
|
residence_card_image_url: Optional[str] = None
|
||||||
|
health_insurance_card_image_url: Optional[str] = None
|
||||||
|
other_id_image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Employee(EmployeeBase):
|
class Employee(EmployeeBase):
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None):
|
|||||||
# .xlsファイルの処理完了
|
# .xlsファイルの処理完了
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"インポート完了: {imported_count}件のデータをインポートしました")
|
print(f"インポート完了: {imported_count}件のデータをインポートしました")
|
||||||
return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました"}
|
return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# .xlsx形式 - openpyxlを使用
|
# .xlsx形式 - openpyxlを使用
|
||||||
@@ -568,7 +568,7 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None):
|
|||||||
# .xlsxファイルの処理完了
|
# .xlsxファイルの処理完了
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"インポート完了: {imported_count}件のデータをインポートしました")
|
print(f"インポート完了: {imported_count}件のデータをインポートしました")
|
||||||
return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました"}
|
return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year}
|
||||||
|
|
||||||
# CSV file処理
|
# CSV file処理
|
||||||
else:
|
else:
|
||||||
@@ -611,35 +611,75 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None):
|
|||||||
return {"message": f"{extracted_year}年度のデータを{imported_count}件インポートしました(既存データを置き換え)", "year": extracted_year}
|
return {"message": f"{extracted_year}年度のデータを{imported_count}件インポートしました(既存データを置き換え)", "year": extracted_year}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/income-tax", response_model=List[schemas.IncomeTaxTable])
|
@router.get("/income-tax/years")
|
||||||
def get_income_tax_table(target_date: date = None, dependents_count: int = None):
|
def get_income_tax_years():
|
||||||
"""所得税率表を取得"""
|
"""所得税率表の年度一覧を取得"""
|
||||||
if target_date is None:
|
|
||||||
target_date = date.today()
|
|
||||||
|
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if dependents_count is not None:
|
cur.execute(
|
||||||
cur.execute(
|
"""
|
||||||
"""
|
SELECT DISTINCT tax_year
|
||||||
SELECT * FROM income_tax_table
|
FROM income_tax_table
|
||||||
WHERE valid_from <= %s
|
WHERE tax_year IS NOT NULL
|
||||||
AND (valid_to IS NULL OR valid_to >= %s)
|
ORDER BY tax_year DESC
|
||||||
AND dependents_count = %s
|
"""
|
||||||
ORDER BY monthly_income_from
|
)
|
||||||
""",
|
years = [row['tax_year'] for row in cur.fetchall()]
|
||||||
(target_date, target_date, dependents_count)
|
return {"years": years}
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.get("/income-tax", response_model=List[schemas.IncomeTaxTable])
|
||||||
|
def get_income_tax_table(target_date: date = None, dependents_count: int = None, tax_year: int = None):
|
||||||
|
"""所得税率表を取得"""
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# tax_yearが指定されている場合はそれを優先
|
||||||
|
if tax_year is not None:
|
||||||
|
if dependents_count is not None:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM income_tax_table
|
||||||
|
WHERE tax_year = %s
|
||||||
|
AND dependents_count = %s
|
||||||
|
ORDER BY monthly_income_from
|
||||||
|
""",
|
||||||
|
(tax_year, dependents_count)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM income_tax_table
|
||||||
|
WHERE tax_year = %s
|
||||||
|
ORDER BY dependents_count, monthly_income_from
|
||||||
|
""",
|
||||||
|
(tax_year,)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
# target_dateで検索
|
||||||
"""
|
if target_date is None:
|
||||||
SELECT * FROM income_tax_table
|
target_date = date.today()
|
||||||
WHERE valid_from <= %s
|
|
||||||
AND (valid_to IS NULL OR valid_to >= %s)
|
if dependents_count is not None:
|
||||||
ORDER BY dependents_count, monthly_income_from
|
cur.execute(
|
||||||
""",
|
"""
|
||||||
(target_date, target_date)
|
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()
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
358
backend/app/payroll/settings/standard_remuneration_router.py
Normal file
358
backend/app/payroll/settings/standard_remuneration_router.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from decimal import Decimal
|
||||||
|
import openpyxl
|
||||||
|
from io import BytesIO
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/payroll/settings/standard-remuneration", tags=["standard_remuneration"])
|
||||||
|
|
||||||
|
|
||||||
|
class StandardRemunerationRow(BaseModel):
|
||||||
|
grade: str
|
||||||
|
monthly_amount: Decimal
|
||||||
|
salary_from: Decimal
|
||||||
|
salary_to: Optional[Decimal]
|
||||||
|
health_insurance_no_care: Decimal
|
||||||
|
health_insurance_with_care: Decimal
|
||||||
|
pension_insurance: Decimal
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/years")
|
||||||
|
async def get_years():
|
||||||
|
"""登録済みの年度一覧を取得"""
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT DISTINCT rate_year FROM standard_remuneration ORDER BY rate_year DESC")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
years = [row["rate_year"] for row in rows]
|
||||||
|
return years
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def get_standard_remuneration(year: int, prefecture: Optional[str] = None):
|
||||||
|
"""指定された年度・都道府県の標準報酬月額表を取得"""
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if prefecture:
|
||||||
|
query = """
|
||||||
|
SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to,
|
||||||
|
health_insurance_no_care, health_insurance_with_care, pension_insurance
|
||||||
|
FROM standard_remuneration
|
||||||
|
WHERE rate_year = %s AND prefecture = %s
|
||||||
|
ORDER BY prefecture,
|
||||||
|
CASE WHEN grade ~ '^[0-9]+$' THEN CAST(grade AS INTEGER) ELSE 999 END,
|
||||||
|
grade
|
||||||
|
"""
|
||||||
|
cur.execute(query, (year, prefecture))
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to,
|
||||||
|
health_insurance_no_care, health_insurance_with_care, pension_insurance
|
||||||
|
FROM standard_remuneration
|
||||||
|
WHERE rate_year = %s
|
||||||
|
ORDER BY prefecture,
|
||||||
|
CASE WHEN grade ~ '^[0-9]+$' THEN CAST(grade AS INTEGER) ELSE 999 END,
|
||||||
|
grade
|
||||||
|
"""
|
||||||
|
cur.execute(query, (year,))
|
||||||
|
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
result.append({
|
||||||
|
"rate_year": row["rate_year"],
|
||||||
|
"prefecture": row["prefecture"],
|
||||||
|
"grade": row["grade"],
|
||||||
|
"monthly_amount": row["monthly_amount"],
|
||||||
|
"salary_from": row["salary_from"],
|
||||||
|
"salary_to": row["salary_to"],
|
||||||
|
"health_insurance_no_care": row["health_insurance_no_care"],
|
||||||
|
"health_insurance_with_care": row["health_insurance_with_care"],
|
||||||
|
"pension_insurance": row["pension_insurance"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/import")
|
||||||
|
async def import_standard_remuneration(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
prefecture: str = Form(...)
|
||||||
|
):
|
||||||
|
"""Excelファイルから標準報酬月額表をインポート(年度はA1セルから自動取得)"""
|
||||||
|
try:
|
||||||
|
# Excelファイルを読み込み
|
||||||
|
contents = await file.read()
|
||||||
|
wb = openpyxl.load_workbook(BytesIO(contents), data_only=True)
|
||||||
|
|
||||||
|
# 指定された都道府県のシートを取得
|
||||||
|
if prefecture not in wb.sheetnames:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"シート '{prefecture}' が見つかりません。利用可能なシート: {', '.join(wb.sheetnames)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ws = wb[prefecture]
|
||||||
|
|
||||||
|
# A1セルから年度を取得
|
||||||
|
# 例: "令和7年3月分(4月納付分)からの健康保険・厚生年金保険の保険料額表(東京都)"
|
||||||
|
a1_value = ws["A1"].value
|
||||||
|
if not a1_value:
|
||||||
|
raise HTTPException(status_code=400, detail="A1セルに年度情報がありません")
|
||||||
|
|
||||||
|
# 令和年号から西暦年度を計算(例: 令和7年 = 2025年度)
|
||||||
|
import re
|
||||||
|
match = re.search(r'令和(\d+)年', str(a1_value))
|
||||||
|
if match:
|
||||||
|
reiwa_year = int(match.group(1))
|
||||||
|
rate_year = 2018 + reiwa_year # 令和元年 = 2019年
|
||||||
|
else:
|
||||||
|
# 西暦が直接記載されている場合
|
||||||
|
match = re.search(r'(\d{4})年', str(a1_value))
|
||||||
|
if match:
|
||||||
|
rate_year = int(match.group(1))
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"A1セルから年度を抽出できません: {a1_value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# データを抽出(12行目から開始)
|
||||||
|
data_rows = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# min_row=12で12行目から読み取りを開始
|
||||||
|
# enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算
|
||||||
|
for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)):
|
||||||
|
row_idx = 12 + idx # 実際のExcel行番号
|
||||||
|
# 等級が空の場合は終了
|
||||||
|
if not row[0]:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
grade = str(row[0]).strip()
|
||||||
|
|
||||||
|
# 各値を取得し、Noneや空白を処理
|
||||||
|
def clean_value(val, field_name=""):
|
||||||
|
if val is None or val == '':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 数値型の場合はそのまま返す
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return val
|
||||||
|
|
||||||
|
# 文字列の場合
|
||||||
|
if isinstance(val, str):
|
||||||
|
# 空白を除去
|
||||||
|
val = val.strip()
|
||||||
|
# 空、ハイフン、全角ダッシュなどをNoneに
|
||||||
|
if val in ('', '-', '-', '―', 'ー', '−'):
|
||||||
|
return None
|
||||||
|
# 「円」「未満」「以上」などの文字を除去
|
||||||
|
val = val.replace('円', '').replace('未満', '').replace('以上', '')
|
||||||
|
# カンマを除去
|
||||||
|
val = val.replace(',', '').replace(',', '')
|
||||||
|
# 全角数字を半角に変換
|
||||||
|
val = val.translate(str.maketrans('0123456789', '0123456789'))
|
||||||
|
# 再度空チェック
|
||||||
|
if val == '':
|
||||||
|
return None
|
||||||
|
return val
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
# デバッグ用に元のデータを保存(最初の7列を確認)
|
||||||
|
raw_data = {
|
||||||
|
'col0_grade': row[0] if len(row) > 0 else None,
|
||||||
|
'col1': row[1] if len(row) > 1 else None,
|
||||||
|
'col2': row[2] if len(row) > 2 else None,
|
||||||
|
'col3': row[3] if len(row) > 3 else None,
|
||||||
|
'col4': row[4] if len(row) > 4 else None,
|
||||||
|
'col5': row[5] if len(row) > 5 else None,
|
||||||
|
'col6': row[6] if len(row) > 6 else None,
|
||||||
|
'col7': row[7] if len(row) > 7 else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 報酬月額範囲の処理
|
||||||
|
# 複数のパターンに対応:
|
||||||
|
# パターンA: 列2に範囲("58,000円~63,000円")
|
||||||
|
# パターンB: 列2=下限, 列3=「~」, 列4=上限
|
||||||
|
# パターンC: 列2=下限, 列3=上限
|
||||||
|
|
||||||
|
monthly_amount = clean_value(row[1], 'monthly_amount')
|
||||||
|
|
||||||
|
# 列3が「~」かチェック
|
||||||
|
col3_raw = str(row[3]) if len(row) > 3 and row[3] is not None else ''
|
||||||
|
col3_cleaned = col3_raw.strip()
|
||||||
|
|
||||||
|
# 列2に範囲が含まれているかチェック
|
||||||
|
col2_raw = str(row[2]) if len(row) > 2 and row[2] is not None else ''
|
||||||
|
|
||||||
|
if col3_cleaned in ('~', '〜', '-'):
|
||||||
|
# パターンB: 列2=下限, 列3=「~」, 列4=上限
|
||||||
|
salary_from = clean_value(row[2], 'salary_from')
|
||||||
|
salary_to = clean_value(row[4] if len(row) > 4 else None, 'salary_to')
|
||||||
|
# 保険料は列5以降
|
||||||
|
health_no_care = clean_value(row[5] if len(row) > 5 else None, 'health_no_care')
|
||||||
|
health_with_care = clean_value(row[6] if len(row) > 6 else None, 'health_with_care')
|
||||||
|
pension = clean_value(row[7] if len(row) > 7 else None, 'pension')
|
||||||
|
|
||||||
|
elif '~' in col2_raw or '〜' in col2_raw:
|
||||||
|
# パターンA: 列2に範囲が1セルに入っている
|
||||||
|
range_text = col2_raw.replace('円', '').replace('未満', '').replace('以上', '')
|
||||||
|
parts = range_text.replace('〜', '~').split('~')
|
||||||
|
|
||||||
|
if len(parts) >= 2:
|
||||||
|
salary_from = clean_value(parts[0].strip(), 'salary_from')
|
||||||
|
salary_to = clean_value(parts[1].strip(), 'salary_to')
|
||||||
|
else:
|
||||||
|
salary_from = clean_value(row[2], 'salary_from')
|
||||||
|
salary_to = None
|
||||||
|
|
||||||
|
# 保険料は列3以降
|
||||||
|
health_no_care = clean_value(row[3] if len(row) > 3 else None, 'health_no_care')
|
||||||
|
health_with_care = clean_value(row[4] if len(row) > 4 else None, 'health_with_care')
|
||||||
|
pension = clean_value(row[5] if len(row) > 5 else None, 'pension')
|
||||||
|
else:
|
||||||
|
# パターンC: 列2=下限、列3=上限の通常パターン
|
||||||
|
salary_from = clean_value(row[2], 'salary_from')
|
||||||
|
salary_to = clean_value(row[3] if len(row) > 3 else None, 'salary_to')
|
||||||
|
health_no_care = clean_value(row[4] if len(row) > 4 else None, 'health_no_care')
|
||||||
|
health_with_care = clean_value(row[5] if len(row) > 5 else None, 'health_with_care')
|
||||||
|
pension = clean_value(row[6] if len(row) > 6 else None, 'pension')
|
||||||
|
|
||||||
|
# 必須フィールドのチェック(詳細なエラーメッセージ)
|
||||||
|
missing_fields = []
|
||||||
|
if monthly_amount is None:
|
||||||
|
missing_fields.append(f"標準報酬月額(列2)={repr(raw_data['col1'])}")
|
||||||
|
if salary_from is None:
|
||||||
|
missing_fields.append(f"報酬月額下限={repr(raw_data['col2'])}")
|
||||||
|
if health_no_care is None:
|
||||||
|
missing_fields.append(f"健保(介護なし)={repr(raw_data.get('col4') or raw_data.get('col3'))}")
|
||||||
|
if health_with_care is None:
|
||||||
|
missing_fields.append(f"健保(介護あり)={repr(raw_data.get('col5') or raw_data.get('col4'))}")
|
||||||
|
if pension is None:
|
||||||
|
missing_fields.append(f"厚生年金={repr(raw_data.get('col6') or raw_data.get('col5'))}")
|
||||||
|
|
||||||
|
if missing_fields:
|
||||||
|
errors.append(f"行{row_idx}: 必須フィールドが空: {', '.join(missing_fields)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Decimal変換(より詳細なエラー処理)
|
||||||
|
try:
|
||||||
|
monthly_amount = Decimal(str(monthly_amount))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 標準報酬月額の変換エラー: {repr(raw_data['col1'])} -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
salary_from = Decimal(str(salary_from))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 報酬月額下限の変換エラー: {repr(raw_data['col2'])} -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
salary_to = Decimal(str(salary_to)) if salary_to is not None else None
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 報酬月額上限の変換エラー: 範囲から抽出または{repr(raw_data.get('col3'))} -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
health_no_care = Decimal(str(health_no_care))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 健保(介護なし)の変換エラー: {repr(health_no_care)} (元データ列={raw_data}) -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
health_with_care = Decimal(str(health_with_care))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 健保(介護あり)の変換エラー: {repr(health_with_care)} (元データ列={raw_data}) -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
pension = Decimal(str(pension))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 厚生年金の変換エラー: {repr(pension)} (元データ列={raw_data}) -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
data_rows.append((
|
||||||
|
rate_year, prefecture, grade, monthly_amount, salary_from, salary_to,
|
||||||
|
health_no_care, health_with_care, pension
|
||||||
|
))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"行{row_idx}: 予期しないエラー: {type(e).__name__}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not data_rows:
|
||||||
|
error_msg = "有効なデータが見つかりませんでした"
|
||||||
|
if errors:
|
||||||
|
error_msg += "\n\nエラー詳細:\n" + "\n".join(errors[:15])
|
||||||
|
raise HTTPException(status_code=400, detail=error_msg)
|
||||||
|
|
||||||
|
# データベースに保存
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# 既存データを削除
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM standard_remuneration WHERE rate_year = %s AND prefecture = %s",
|
||||||
|
(rate_year, prefecture)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 新しいデータを挿入
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO standard_remuneration
|
||||||
|
(rate_year, prefecture, grade, monthly_amount, salary_from, salary_to,
|
||||||
|
health_insurance_no_care, health_insurance_with_care, pension_insurance)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.executemany(insert_query, data_rows)
|
||||||
|
conn.commit()
|
||||||
|
imported_count = len(data_rows)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "インポートが完了しました",
|
||||||
|
"year": rate_year,
|
||||||
|
"prefecture": prefecture,
|
||||||
|
"imported_count": imported_count,
|
||||||
|
"source": a1_value
|
||||||
|
}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail="年度は数値で入力してください")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("")
|
||||||
|
async def delete_standard_remuneration(year: int, prefecture: str):
|
||||||
|
"""指定された年度・都道府県の標準報酬月額表を削除"""
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM standard_remuneration WHERE rate_year = %s AND prefecture = %s",
|
||||||
|
(year, prefecture)
|
||||||
|
)
|
||||||
|
deleted_count = cur.rowcount
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "削除しました",
|
||||||
|
"year": year,
|
||||||
|
"prefecture": prefecture,
|
||||||
|
"deleted_count": deleted_count
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
173
backend/app/routers/file_upload.py
Normal file
173
backend/app/routers/file_upload.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
"""
|
||||||
|
ファイルアップロード管理
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/files", tags=["File Upload"])
|
||||||
|
|
||||||
|
# アップロード先のベースディレクトリ
|
||||||
|
UPLOAD_BASE_DIR = Path("uploads")
|
||||||
|
UPLOAD_BASE_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 許可されるファイルタイプ
|
||||||
|
ALLOWED_EXTENSIONS = {
|
||||||
|
'image': {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'},
|
||||||
|
'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_file_extension(filename: str) -> str:
|
||||||
|
"""ファイル拡張子を取得"""
|
||||||
|
return Path(filename).suffix.lower()
|
||||||
|
|
||||||
|
def is_allowed_file(filename: str, file_type: str = None) -> bool:
|
||||||
|
"""許可されたファイルタイプか確認"""
|
||||||
|
ext = get_file_extension(filename)
|
||||||
|
if file_type == 'image':
|
||||||
|
return ext in ALLOWED_EXTENSIONS['image']
|
||||||
|
elif file_type == 'document':
|
||||||
|
return ext in ALLOWED_EXTENSIONS['document']
|
||||||
|
else:
|
||||||
|
# すべての許可された拡張子
|
||||||
|
all_extensions = set()
|
||||||
|
for exts in ALLOWED_EXTENSIONS.values():
|
||||||
|
all_extensions.update(exts)
|
||||||
|
return ext in all_extensions
|
||||||
|
|
||||||
|
def generate_unique_filename(original_filename: str, prefix: str = "") -> str:
|
||||||
|
"""一意のファイル名を生成"""
|
||||||
|
ext = get_file_extension(original_filename)
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
return f"{prefix}_{timestamp}_{unique_id}{ext}"
|
||||||
|
return f"{timestamp}_{unique_id}{ext}"
|
||||||
|
|
||||||
|
@router.post("/upload/employee/{employee_code}")
|
||||||
|
async def upload_employee_file(
|
||||||
|
employee_code: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
file_type: str = "image"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
従業員関連ファイルをアップロード
|
||||||
|
|
||||||
|
Args:
|
||||||
|
employee_code: 従業員コード
|
||||||
|
file: アップロードするファイル
|
||||||
|
file_type: ファイルタイプ (image, document)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
アップロードされたファイルのパス
|
||||||
|
"""
|
||||||
|
# ファイルタイプチェック
|
||||||
|
if not is_allowed_file(file.filename, file_type):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"許可されていないファイル形式です。{file_type}タイプの許可される拡張子: {ALLOWED_EXTENSIONS.get(file_type, 'なし')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存先ディレクトリ作成
|
||||||
|
employee_dir = UPLOAD_BASE_DIR / "employees" / employee_code
|
||||||
|
employee_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 一意のファイル名生成
|
||||||
|
unique_filename = generate_unique_filename(file.filename, employee_code)
|
||||||
|
file_path = employee_dir / unique_filename
|
||||||
|
|
||||||
|
# ファイル保存
|
||||||
|
try:
|
||||||
|
with file_path.open("wb") as buffer:
|
||||||
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}")
|
||||||
|
|
||||||
|
# 相対パスを返す(データベースに保存する値)
|
||||||
|
relative_path = f"/uploads/employees/{employee_code}/{unique_filename}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"file_path": relative_path,
|
||||||
|
"filename": unique_filename,
|
||||||
|
"original_filename": file.filename,
|
||||||
|
"size": file_path.stat().st_size
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/upload/general")
|
||||||
|
async def upload_general_file(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
category: str = "general"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
一般ファイルをアップロード
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: アップロードするファイル
|
||||||
|
category: カテゴリー (general, reports, temp など)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
アップロードされたファイルのパス
|
||||||
|
"""
|
||||||
|
# ファイルタイプチェック
|
||||||
|
if not is_allowed_file(file.filename):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"許可されていないファイル形式です"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存先ディレクトリ作成
|
||||||
|
category_dir = UPLOAD_BASE_DIR / category
|
||||||
|
category_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 一意のファイル名生成
|
||||||
|
unique_filename = generate_unique_filename(file.filename)
|
||||||
|
file_path = category_dir / unique_filename
|
||||||
|
|
||||||
|
# ファイル保存
|
||||||
|
try:
|
||||||
|
with file_path.open("wb") as buffer:
|
||||||
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}")
|
||||||
|
|
||||||
|
# 相対パスを返す
|
||||||
|
relative_path = f"/uploads/{category}/{unique_filename}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"file_path": relative_path,
|
||||||
|
"filename": unique_filename,
|
||||||
|
"original_filename": file.filename,
|
||||||
|
"size": file_path.stat().st_size
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.delete("/delete")
|
||||||
|
async def delete_file(file_path: str):
|
||||||
|
"""
|
||||||
|
ファイルを削除
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 削除するファイルのパス (/uploads/... 形式)
|
||||||
|
"""
|
||||||
|
# セキュリティ: パストラバーサル攻撃を防ぐ
|
||||||
|
if ".." in file_path or file_path.startswith("/"):
|
||||||
|
file_path = file_path.lstrip("/")
|
||||||
|
|
||||||
|
full_path = Path(file_path)
|
||||||
|
|
||||||
|
# uploadsディレクトリ内のファイルのみ削除可能
|
||||||
|
if not str(full_path).startswith("uploads"):
|
||||||
|
raise HTTPException(status_code=400, detail="無効なファイルパスです")
|
||||||
|
|
||||||
|
if not full_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="ファイルが見つかりません")
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_path.unlink()
|
||||||
|
return {"success": True, "message": "ファイルを削除しました"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"ファイル削除に失敗しました: {str(e)}")
|
||||||
25
backend/check_insurance_rates.py
Normal file
25
backend/check_insurance_rates.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import psycopg
|
||||||
|
|
||||||
|
conn = psycopg.connect(
|
||||||
|
host='192.168.0.61',
|
||||||
|
port=55432,
|
||||||
|
dbname='njts_acct',
|
||||||
|
user='njts_app',
|
||||||
|
password='njts_app2025'
|
||||||
|
)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'insurance_rates'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print('insurance_ratesテーブルのカラム:')
|
||||||
|
for r in rows:
|
||||||
|
print(f' {r[0]}: {r[1]}')
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
33
backend/check_photo_data.py
Normal file
33
backend/check_photo_data.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import psycopg
|
||||||
|
|
||||||
|
conn = psycopg.connect(
|
||||||
|
host='192.168.0.61',
|
||||||
|
port=55432,
|
||||||
|
dbname='njts_acct',
|
||||||
|
user='njts_app',
|
||||||
|
password='njts_app2025'
|
||||||
|
)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT employee_id, employee_code, name,
|
||||||
|
photo_url IS NOT NULL as has_photo,
|
||||||
|
CASE WHEN photo_url IS NOT NULL THEN LENGTH(photo_url) ELSE 0 END as photo_length,
|
||||||
|
CASE WHEN photo_url IS NOT NULL THEN SUBSTRING(photo_url, 1, 50) ELSE NULL END as photo_preview
|
||||||
|
FROM employees
|
||||||
|
WHERE employee_code IN ('20190001', '20190002')
|
||||||
|
ORDER BY employee_code
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print('従業員の写真データ状況:')
|
||||||
|
print('-' * 80)
|
||||||
|
for r in rows:
|
||||||
|
print(f'ID: {r[0]}, Code: {r[1]}, Name: {r[2]}')
|
||||||
|
print(f' Has Photo: {r[3]}, Photo Length: {r[4]}')
|
||||||
|
if r[5]:
|
||||||
|
print(f' Photo Preview: {r[5]}...')
|
||||||
|
print()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
51
backend/check_std_table.py
Normal file
51
backend/check_std_table.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import sys
|
||||||
|
sys.path.insert(0, r'c:\workspace\njts-accounting-core\backend')
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# テーブルの存在確認
|
||||||
|
cur.execute("""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_name = 'standard_remuneration'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
exists = cur.fetchone()["exists"]
|
||||||
|
print(f"テーブル存在: {exists}")
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# データ件数確認
|
||||||
|
cur.execute("SELECT COUNT(*) as count FROM standard_remuneration")
|
||||||
|
count = cur.fetchone()["count"]
|
||||||
|
print(f"データ件数: {count}")
|
||||||
|
|
||||||
|
# 年度リスト取得
|
||||||
|
cur.execute("SELECT DISTINCT rate_year FROM standard_remuneration ORDER BY rate_year DESC")
|
||||||
|
years = cur.fetchall()
|
||||||
|
print(f"年度リスト: {years}")
|
||||||
|
else:
|
||||||
|
print("テーブルが存在しません。作成します...")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_remuneration (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
rate_year INTEGER NOT NULL,
|
||||||
|
prefecture VARCHAR(50) NOT NULL,
|
||||||
|
grade VARCHAR(20) NOT NULL,
|
||||||
|
monthly_amount DECIMAL(12,2) NOT NULL,
|
||||||
|
salary_from DECIMAL(12,2) NOT NULL,
|
||||||
|
salary_to DECIMAL(12,2),
|
||||||
|
health_insurance_no_care DECIMAL(12,2) NOT NULL,
|
||||||
|
health_insurance_with_care DECIMAL(12,2) NOT NULL,
|
||||||
|
pension_insurance DECIMAL(12,2) NOT NULL,
|
||||||
|
UNIQUE(rate_year, prefecture, grade)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
print("テーブルを作成しました")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"エラー: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
53
backend/create_std_remuneration_table.py
Normal file
53
backend/create_std_remuneration_table.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import psycopg2
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": os.getenv("DB_HOST", "192.168.0.61"),
|
||||||
|
"port": os.getenv("DB_PORT", "55432"),
|
||||||
|
"database": os.getenv("DB_NAME", "njts_acct"),
|
||||||
|
"user": os.getenv("DB_USER", "njts_app"),
|
||||||
|
"password": os.getenv("DB_PASSWORD", "njts_app2025"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_standard_remuneration_table():
|
||||||
|
"""標準報酬月額表テーブルを作成"""
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# テーブル作成
|
||||||
|
sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_remuneration (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
rate_year INTEGER NOT NULL,
|
||||||
|
prefecture VARCHAR(50) NOT NULL,
|
||||||
|
grade VARCHAR(20) NOT NULL,
|
||||||
|
monthly_amount DECIMAL(12, 2) NOT NULL,
|
||||||
|
salary_from DECIMAL(12, 2) NOT NULL,
|
||||||
|
salary_to DECIMAL(12, 2),
|
||||||
|
health_insurance_no_care DECIMAL(12, 2) NOT NULL,
|
||||||
|
health_insurance_with_care DECIMAL(12, 2) NOT NULL,
|
||||||
|
pension_insurance DECIMAL(12, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(rate_year, prefecture, grade)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standard_remuneration_year_pref
|
||||||
|
ON standard_remuneration(rate_year, prefecture);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standard_remuneration_salary
|
||||||
|
ON standard_remuneration(rate_year, prefecture, salary_from, salary_to);
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.execute(sql)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print("✅ standard_remuneration テーブルを作成しました")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_standard_remuneration_table()
|
||||||
58
backend/create_std_table.py
Normal file
58
backend/create_std_table.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 環境変数を設定
|
||||||
|
os.environ["DB_HOST"] = "192.168.0.61"
|
||||||
|
os.environ["DB_PORT"] = "55432"
|
||||||
|
os.environ["DB_NAME"] = "njts_acct"
|
||||||
|
os.environ["DB_USER"] = "njts_app"
|
||||||
|
os.environ["DB_PASSWORD"] = "njts_app2025"
|
||||||
|
|
||||||
|
sys.path.insert(0, r'c:\workspace\njts-accounting-core\backend')
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
print("standard_remunerationテーブルを作成中...")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_remuneration (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
rate_year INTEGER NOT NULL,
|
||||||
|
prefecture VARCHAR(50) NOT NULL,
|
||||||
|
grade VARCHAR(20) NOT NULL,
|
||||||
|
monthly_amount DECIMAL(12,2) NOT NULL,
|
||||||
|
salary_from DECIMAL(12,2) NOT NULL,
|
||||||
|
salary_to DECIMAL(12,2),
|
||||||
|
health_insurance_no_care DECIMAL(12,2) NOT NULL,
|
||||||
|
health_insurance_with_care DECIMAL(12,2) NOT NULL,
|
||||||
|
pension_insurance DECIMAL(12,2) NOT NULL,
|
||||||
|
UNIQUE(rate_year, prefecture, grade)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# インデックスを作成
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_std_rem_year_pref
|
||||||
|
ON standard_remuneration(rate_year, prefecture)
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("✓ テーブルとインデックスを作成しました")
|
||||||
|
|
||||||
|
# テーブルの確認
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'standard_remuneration'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
columns = cur.fetchall()
|
||||||
|
print("\n作成されたカラム:")
|
||||||
|
for col in columns:
|
||||||
|
print(f" - {col['column_name']}: {col['data_type']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"エラー: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
128
backend/debug_care_insurance.py
Normal file
128
backend/debug_care_insurance.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""
|
||||||
|
介護保険計算のデバッグスクリプト
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
# 従業員20190001の情報を確認
|
||||||
|
employee_code = "20190001"
|
||||||
|
payment_date = date(2025, 12, 31)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"従業員コード: {employee_code}")
|
||||||
|
print(f"支給日: {payment_date}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# 従業員情報を取得
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT employee_id, employee_code, name, birth_date
|
||||||
|
FROM employees
|
||||||
|
WHERE employee_code = %s
|
||||||
|
""",
|
||||||
|
(employee_code,)
|
||||||
|
)
|
||||||
|
employee = cur.fetchone()
|
||||||
|
|
||||||
|
if not employee:
|
||||||
|
print("従業員が見つかりません")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print(f"\n【従業員情報】")
|
||||||
|
print(f" ID: {employee['employee_id']}")
|
||||||
|
print(f" コード: {employee['employee_code']}")
|
||||||
|
print(f" 氏名: {employee['name']}")
|
||||||
|
print(f" 生年月日: {employee['birth_date']}")
|
||||||
|
|
||||||
|
# 年齢を計算
|
||||||
|
if employee['birth_date']:
|
||||||
|
birth_date = employee['birth_date']
|
||||||
|
age = payment_date.year - birth_date.year
|
||||||
|
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||||||
|
age -= 1
|
||||||
|
print(f" 年齢({payment_date}時点): {age}歳")
|
||||||
|
print(f" 介護保険対象: {'はい (40-64歳)' if 40 <= age < 65 else 'いいえ'}")
|
||||||
|
else:
|
||||||
|
print(f" 生年月日が未設定です")
|
||||||
|
age = 0
|
||||||
|
|
||||||
|
# 介護保険料率を確認(2025年)
|
||||||
|
print(f"\n【介護保険料率 - 2025年】")
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM insurance_rates
|
||||||
|
WHERE rate_type = '介護保険'
|
||||||
|
AND rate_year = 2025
|
||||||
|
ORDER BY rate_id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rates_2025 = cur.fetchall()
|
||||||
|
|
||||||
|
if rates_2025:
|
||||||
|
for rate in rates_2025:
|
||||||
|
print(f" ID: {rate['rate_id']}")
|
||||||
|
print(f" 計算対象: {rate['calculation_target']}")
|
||||||
|
print(f" 都道府県: {rate['prefecture']}")
|
||||||
|
print(f" 従業員負担率: {float(rate['employee_rate']) * 100:.3f}%")
|
||||||
|
print(f" 会社負担率: {float(rate['employer_rate']) * 100:.3f}%")
|
||||||
|
print(f" 年齢下限: {rate['care_insurance_age_threshold']}歳以上")
|
||||||
|
print(f" 備考: {rate['notes']}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(" ⚠️ 2025年の介護保険料率が見つかりません!")
|
||||||
|
|
||||||
|
# 介護保険料率を確認(2026年)
|
||||||
|
print(f"【介護保険料率 - 2026年】")
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM insurance_rates
|
||||||
|
WHERE rate_type = '介護保険'
|
||||||
|
AND rate_year = 2026
|
||||||
|
ORDER BY rate_id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rates_2026 = cur.fetchall()
|
||||||
|
|
||||||
|
if rates_2026:
|
||||||
|
for rate in rates_2026:
|
||||||
|
print(f" ID: {rate['rate_id']}")
|
||||||
|
print(f" 計算対象: {rate['calculation_target']}")
|
||||||
|
print(f" 都道府県: {rate['prefecture']}")
|
||||||
|
print(f" 従業員負担率: {float(rate['employee_rate']) * 100:.3f}%")
|
||||||
|
print(f" 会社負担率: {float(rate['employer_rate']) * 100:.3f}%")
|
||||||
|
print(f" 年齢下限: {rate['care_insurance_age_threshold']}歳以上")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(" 2026年の介護保険料率が見つかりません")
|
||||||
|
|
||||||
|
# 2025年12月の給与計算データを確認
|
||||||
|
print(f"\n【2025年12月の給与計算】")
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM monthly_payroll
|
||||||
|
WHERE employee_id = %s
|
||||||
|
AND payroll_year = 2025
|
||||||
|
AND payroll_month = 12
|
||||||
|
ORDER BY payroll_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(employee['employee_id'],)
|
||||||
|
)
|
||||||
|
payroll = cur.fetchone()
|
||||||
|
|
||||||
|
if payroll:
|
||||||
|
print(f" Payroll ID: {payroll['payroll_id']}")
|
||||||
|
print(f" 基本給: ¥{float(payroll['base_salary']):,.0f}")
|
||||||
|
print(f" 健康保険: ¥{float(payroll['health_insurance']):,.0f}")
|
||||||
|
print(f" 介護保険: ¥{float(payroll['care_insurance']):,.0f}")
|
||||||
|
print(f" 厚生年金: ¥{float(payroll['pension_insurance']):,.0f}")
|
||||||
|
print(f" 雇用保険: ¥{float(payroll['employment_insurance']):,.0f}")
|
||||||
|
print(f" 計算日: {payroll['created_at']}")
|
||||||
|
else:
|
||||||
|
print(" 給与計算データが見つかりません")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("診断完了")
|
||||||
|
print("=" * 60)
|
||||||
42
backend/fix_photo_columns.py
Normal file
42
backend/fix_photo_columns.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import psycopg
|
||||||
|
|
||||||
|
conn = psycopg.connect(
|
||||||
|
host='192.168.0.61',
|
||||||
|
port=55432,
|
||||||
|
dbname='njts_acct',
|
||||||
|
user='njts_app',
|
||||||
|
password='njts_app2025'
|
||||||
|
)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# ALTER TABLE実行
|
||||||
|
cur.execute("""
|
||||||
|
ALTER TABLE employees
|
||||||
|
ALTER COLUMN photo_url TYPE TEXT,
|
||||||
|
ALTER COLUMN my_number_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN residence_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN health_insurance_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN other_id_image_url TYPE TEXT
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print('写真カラムのデータ型をTEXTに変更しました')
|
||||||
|
|
||||||
|
# 確認
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name, data_type, character_maximum_length
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'employees'
|
||||||
|
AND column_name LIKE '%_url'
|
||||||
|
ORDER BY column_name
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = cur.fetchall()
|
||||||
|
print('\nカラム情報:')
|
||||||
|
for r in result:
|
||||||
|
length = r[2] if r[2] else '制限なし'
|
||||||
|
print(f' {r[0]}: {r[1]} ({length})')
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
15
backend/sql/add_employee_photos.sql
Normal file
15
backend/sql/add_employee_photos.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- 従業員の写真・身分証明書画像フィールドを追加
|
||||||
|
-- Add employee photo and ID document image fields
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD COLUMN IF NOT EXISTS photo_url VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS my_number_card_image_url VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS residence_card_image_url VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS health_insurance_card_image_url VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS other_id_image_url VARCHAR(500);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN employees.photo_url IS '本人写真のURL';
|
||||||
|
COMMENT ON COLUMN employees.my_number_card_image_url IS 'マイナンバーカード画像のURL';
|
||||||
|
COMMENT ON COLUMN employees.residence_card_image_url IS '在留カード画像のURL';
|
||||||
|
COMMENT ON COLUMN employees.health_insurance_card_image_url IS '健康保険証画像のURL';
|
||||||
|
COMMENT ON COLUMN employees.other_id_image_url IS 'その他の身分証明書画像のURL';
|
||||||
12
backend/sql/add_my_number_field.sql
Normal file
12
backend/sql/add_my_number_field.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- マイナンバーカード番号フィールドを追加
|
||||||
|
-- Add my_number field to employees table
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD COLUMN IF NOT EXISTS my_number VARCHAR(12);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN employees.my_number IS 'マイナンバー(個人番号)12桁';
|
||||||
|
|
||||||
|
-- マイナンバーは機密情報なので、ユニーク制約を追加
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_employees_my_number
|
||||||
|
ON employees(my_number)
|
||||||
|
WHERE my_number IS NOT NULL;
|
||||||
32
backend/sql/create_standard_remuneration.sql
Normal file
32
backend/sql/create_standard_remuneration.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- 標準報酬月額表テーブル
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_remuneration (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
rate_year INTEGER NOT NULL,
|
||||||
|
prefecture VARCHAR(50) NOT NULL,
|
||||||
|
grade VARCHAR(20) NOT NULL,
|
||||||
|
monthly_amount DECIMAL(12, 2) NOT NULL,
|
||||||
|
salary_from DECIMAL(12, 2) NOT NULL,
|
||||||
|
salary_to DECIMAL(12, 2),
|
||||||
|
health_insurance_no_care DECIMAL(12, 2) NOT NULL,
|
||||||
|
health_insurance_with_care DECIMAL(12, 2) NOT NULL,
|
||||||
|
pension_insurance DECIMAL(12, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(rate_year, prefecture, grade)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standard_remuneration_year_pref
|
||||||
|
ON standard_remuneration(rate_year, prefecture);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_standard_remuneration_salary
|
||||||
|
ON standard_remuneration(rate_year, prefecture, salary_from, salary_to);
|
||||||
|
|
||||||
|
COMMENT ON TABLE standard_remuneration IS '健康保険・厚生年金保険の標準報酬月額表';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.rate_year IS '適用年度';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.prefecture IS '都道府県名';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.grade IS '等級';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.monthly_amount IS '標準報酬月額';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.salary_from IS '報酬月額(下限)';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.salary_to IS '報酬月額(上限、最高等級の場合はNULL)';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.health_insurance_no_care IS '健康保険料(介護保険なし)';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.health_insurance_with_care IS '健康保険料(介護保険あり)';
|
||||||
|
COMMENT ON COLUMN standard_remuneration.pension_insurance IS '厚生年金保険料';
|
||||||
15
backend/sql/fix_photo_column_size.sql
Normal file
15
backend/sql/fix_photo_column_size.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- 写真フィールドのデータ型をVARCHAR(500)からTEXTに変更
|
||||||
|
-- Base64エンコードされた画像データは数万文字になるため
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ALTER COLUMN photo_url TYPE TEXT,
|
||||||
|
ALTER COLUMN my_number_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN residence_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN health_insurance_card_image_url TYPE TEXT,
|
||||||
|
ALTER COLUMN other_id_image_url TYPE TEXT;
|
||||||
|
|
||||||
|
-- 確認
|
||||||
|
SELECT column_name, data_type, character_maximum_length
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'employees'
|
||||||
|
AND column_name LIKE '%_url';
|
||||||
34
backend/sql/insert_2025_insurance_rates.sql
Normal file
34
backend/sql/insert_2025_insurance_rates.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
-- 2025年度の保険料率を追加
|
||||||
|
|
||||||
|
-- 健康保険(2025年度 東京都)
|
||||||
|
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
|
||||||
|
(2025, '健康保険', '給与', '東京都', 0.04985, 0.04985, NULL, '健康保険料率 9.97% (労使折半) - 給与'),
|
||||||
|
(2025, '健康保険', '賞与', '東京都', 0.04985, 0.04985, NULL, '健康保険料率 9.97% (労使折半) - 賞与')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 介護保険(2025年度)
|
||||||
|
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
|
||||||
|
(2025, '介護保険', '給与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 給与'),
|
||||||
|
(2025, '介護保険', '賞与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 賞与')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 厚生年金(2025年度)
|
||||||
|
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
|
||||||
|
(2025, '厚生年金', '給与', '東京都', 0.09150, 0.09150, NULL, '厚生年金保険料率 18.3% (労使折半) - 給与'),
|
||||||
|
(2025, '厚生年金', '賞与', '東京都', 0.09150, 0.09150, NULL, '厚生年金保険料率 18.3% (労使折半) - 賞与')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 雇用保険(2025年度)
|
||||||
|
INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES
|
||||||
|
(2025, '雇用保険', '給与', '東京都', 0.006, 0.009, NULL, '雇用保険料率 従業員0.6% 会社0.9% - 給与'),
|
||||||
|
(2025, '雇用保険', '賞与', '東京都', 0.006, 0.009, NULL, '雇用保険料率 従業員0.6% 会社0.9% - 賞与')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 確認
|
||||||
|
SELECT rate_year, rate_type, calculation_target,
|
||||||
|
ROUND(employee_rate * 100, 3) as employee_pct,
|
||||||
|
ROUND(employer_rate * 100, 3) as employer_pct,
|
||||||
|
care_insurance_age_threshold
|
||||||
|
FROM insurance_rates
|
||||||
|
WHERE rate_year = 2025
|
||||||
|
ORDER BY rate_type, calculation_target;
|
||||||
94
backend/test_care_insurance.py
Normal file
94
backend/test_care_insurance.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"""介護保険デバッグスクリプト"""
|
||||||
|
import psycopg
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# 接続
|
||||||
|
conn = psycopg.connect('host=192.168.0.61 port=55432 dbname=njts_acct user=njts_app password=njts_app2025')
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 従業員情報取得
|
||||||
|
cur.execute("SELECT employee_id, employee_code, name, birth_date FROM employees WHERE employee_code = '20190001'")
|
||||||
|
emp = cur.fetchone()
|
||||||
|
print(f'従業員: ID={emp[0]}, コード={emp[1]}, 名前={emp[2]}, 生年月日={emp[3]}')
|
||||||
|
|
||||||
|
# 年齢計算
|
||||||
|
birth_date = emp[3]
|
||||||
|
payment_date = date(2025, 12, 25)
|
||||||
|
age = payment_date.year - birth_date.year
|
||||||
|
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||||||
|
age -= 1
|
||||||
|
print(f'\n支給日: {payment_date}')
|
||||||
|
print(f'年齢: {age}歳')
|
||||||
|
print(f'介護保険対象: {40 <= age < 65}')
|
||||||
|
|
||||||
|
# 2025年介護保険料率取得
|
||||||
|
print('\n=== 介護保険料率検索 ===')
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rate_id, rate_year, rate_type, calculation_target, employee_rate, employer_rate, prefecture
|
||||||
|
FROM insurance_rates
|
||||||
|
WHERE rate_type = '介護保険'
|
||||||
|
AND rate_year = 2025
|
||||||
|
AND calculation_target = '給与'
|
||||||
|
ORDER BY rate_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
rate = cur.fetchone()
|
||||||
|
if rate:
|
||||||
|
print(f'料率ID: {rate[0]}')
|
||||||
|
print(f'年度: {rate[1]}')
|
||||||
|
print(f'種類: {rate[2]}')
|
||||||
|
print(f'計算対象: {rate[3]}')
|
||||||
|
print(f'従業員負担率: {rate[4]} ({float(rate[4])*100:.3f}%)')
|
||||||
|
print(f'会社負担率: {rate[5]} ({float(rate[5])*100:.3f}%)')
|
||||||
|
print(f'都道府県: {rate[6]}')
|
||||||
|
else:
|
||||||
|
print('料率が見つかりません!')
|
||||||
|
|
||||||
|
# 給与設定取得
|
||||||
|
print('\n=== 給与設定 ===')
|
||||||
|
cur.execute("""
|
||||||
|
SELECT base_salary 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
|
||||||
|
""", (emp[0], payment_date, payment_date))
|
||||||
|
salary = cur.fetchone()
|
||||||
|
if salary:
|
||||||
|
print(f'基本給: ¥{salary[0]:,}')
|
||||||
|
else:
|
||||||
|
print('給与設定が見つかりません!')
|
||||||
|
|
||||||
|
# 計算
|
||||||
|
if rate and salary:
|
||||||
|
print('\n=== 介護保険計算 ===')
|
||||||
|
base = Decimal(str(salary[0]))
|
||||||
|
rate_val = Decimal(str(rate[4])) # employee_rate
|
||||||
|
care_ins = (base * rate_val).quantize(Decimal('1'))
|
||||||
|
print(f'計算式: ¥{base:,} × {rate_val} = ¥{care_ins:,}')
|
||||||
|
|
||||||
|
# 実際の給与データ確認
|
||||||
|
print('\n=== 実際の給与データ ===')
|
||||||
|
cur.execute("""
|
||||||
|
SELECT payroll_id, payment_date, base_salary, health_insurance, care_insurance, status
|
||||||
|
FROM monthly_payroll
|
||||||
|
WHERE employee_id = %s
|
||||||
|
AND EXTRACT(YEAR FROM payment_date) = 2025
|
||||||
|
AND EXTRACT(MONTH FROM payment_date) = 12
|
||||||
|
ORDER BY payroll_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""", (emp[0],))
|
||||||
|
payroll = cur.fetchone()
|
||||||
|
if payroll:
|
||||||
|
print(f'給与ID: {payroll[0]}')
|
||||||
|
print(f'支給日: {payroll[1]}')
|
||||||
|
print(f'基本給: ¥{payroll[2]:,}')
|
||||||
|
print(f'健康保険: ¥{payroll[3]:,}')
|
||||||
|
print(f'介護保険: ¥{payroll[4]:,}')
|
||||||
|
print(f'ステータス: {payroll[5]}')
|
||||||
|
else:
|
||||||
|
print('2025年12月の給与データが見つかりません!')
|
||||||
|
|
||||||
|
conn.close()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
期間:
|
期間:
|
||||||
<input type="date" id="from" />
|
<input type="date" id="from" min="1900-01-01" max="2099-12-31" />
|
||||||
〜
|
〜
|
||||||
<input type="date" id="to" />
|
<input type="date" id="to" min="1900-01-01" max="2099-12-31" />
|
||||||
</label>
|
</label>
|
||||||
<button onclick="loadTrialBalance()">表示</button>
|
<button onclick="loadTrialBalance()">表示</button>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<h2>修正仕訳入力</h2>
|
<h2>修正仕訳入力</h2>
|
||||||
|
|
||||||
<label>仕訳日</label>
|
<label>仕訳日</label>
|
||||||
<input type="date" id="entryDate" min="1900-01-01" max="9999-12-31" />
|
<input type="date" id="entryDate" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<label>摘要</label>
|
<label>摘要</label>
|
||||||
<input type="text" id="description" style="width: 60%" />
|
<input type="text" id="description" style="width: 60%" />
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
id="entryDate"
|
id="entryDate"
|
||||||
min="1900-01-01"
|
min="1900-01-01"
|
||||||
max="2999-12-31"
|
max="2099-12-31"
|
||||||
onchange="onEntryDateChanged()"
|
onchange="onEntryDateChanged()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,10 +264,10 @@
|
|||||||
type="date"
|
type="date"
|
||||||
id="searchFromDate"
|
id="searchFromDate"
|
||||||
min="1900-01-01"
|
min="1900-01-01"
|
||||||
max="2999-12-31"
|
max="2099-12-31"
|
||||||
/>
|
/>
|
||||||
~
|
~
|
||||||
<input type="date" id="searchToDate" min="1900-01-01" max="2999-12-31" />
|
<input type="date" id="searchToDate" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<label style="margin-left: 16px">摘要</label>
|
<label style="margin-left: 16px">摘要</label>
|
||||||
<input type="text" id="searchKeyword" style="min-width: 600px" />
|
<input type="text" id="searchKeyword" style="min-width: 600px" />
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
<h2>仕訳一覧</h2>
|
<h2>仕訳一覧</h2>
|
||||||
|
|
||||||
<label>期間</label>
|
<label>期間</label>
|
||||||
<input type="date" id="fromDate" min="1900-01-01" max="2999-12-31" /> ~
|
<input type="date" id="fromDate" min="1900-01-01" max="2099-12-31" /> ~
|
||||||
<input type="date" id="toDate" min="1900-01-01" max="2999-12-31" />
|
<input type="date" id="toDate" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<label>摘要</label>
|
<label>摘要</label>
|
||||||
<input type="text" id="keyword" />
|
<input type="text" id="keyword" />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
日付:
|
日付:
|
||||||
<input type="date" id="journal-date" />
|
<input type="date" id="journal-date" min="1900-01-01" max="2099-12-31" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|||||||
@@ -78,9 +78,9 @@
|
|||||||
<select id="accountSelect"></select>
|
<select id="accountSelect"></select>
|
||||||
|
|
||||||
<label>期間:</label>
|
<label>期間:</label>
|
||||||
<input type="date" id="fromDate" />
|
<input type="date" id="fromDate" min="1900-01-01" max="2099-12-31" />
|
||||||
〜
|
〜
|
||||||
<input type="date" id="toDate" />
|
<input type="date" id="toDate" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<button onclick="reloadTransactions()">表示</button>
|
<button onclick="reloadTransactions()">表示</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,10 +108,22 @@
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
}
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<a href="payroll.html" class="back-link">← 給与管理メニューに戻る</a>
|
||||||
|
|
||||||
<h1>給与管理 - 月次給与計算</h1>
|
<h1>給与管理 - 月次給与計算</h1>
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
@@ -188,7 +200,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>支給日 *</label>
|
<label>支給日 *</label>
|
||||||
<input type="date" name="payment_date" required />
|
<input
|
||||||
|
type="date"
|
||||||
|
name="payment_date"
|
||||||
|
required
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -292,7 +310,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = "http://localhost:8000";
|
const API_BASE = "http://localhost:18080";
|
||||||
|
|
||||||
async function loadEmployees() {
|
async function loadEmployees() {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -436,19 +454,59 @@
|
|||||||
);
|
);
|
||||||
const payroll = await response.json();
|
const payroll = await response.json();
|
||||||
|
|
||||||
|
// 従業員情報と扶養家族情報を取得
|
||||||
|
const empResponse = await fetch(
|
||||||
|
`${API_BASE}/payroll/employees/${payroll.employee_id}`
|
||||||
|
);
|
||||||
|
const employee = await empResponse.json();
|
||||||
|
|
||||||
|
// 源泉税控除対象人数を計算(19歳以上のみ)
|
||||||
|
const dependentCount = employee.dependents
|
||||||
|
? employee.dependents.filter((d) => {
|
||||||
|
// 有効期間チェック
|
||||||
|
if (d.valid_to && new Date(d.valid_to) < new Date())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 生年月日から年齢を計算(その年の12月31日時点)
|
||||||
|
if (!d.birth_date) return false;
|
||||||
|
const birthDate = new Date(d.birth_date);
|
||||||
|
const targetYear = payroll.payroll_year;
|
||||||
|
const yearEnd = new Date(targetYear, 11, 31); // 12月31日
|
||||||
|
let age = yearEnd.getFullYear() - birthDate.getFullYear();
|
||||||
|
const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
|
||||||
|
if (
|
||||||
|
monthDiff < 0 ||
|
||||||
|
(monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())
|
||||||
|
) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 19歳以上のみ源泉税控除対象(16-18歳は住民税のみ)
|
||||||
|
return age >= 19;
|
||||||
|
}).length
|
||||||
|
: 0;
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<h2>${payroll.payroll_year}年${
|
<h2>${payroll.payroll_year}年${
|
||||||
payroll.payroll_month
|
payroll.payroll_month
|
||||||
}月 給与明細</h2>
|
}月 給与明細</h2>
|
||||||
<p>従業員ID: ${payroll.employee_id} | 支給日: ${
|
<p><strong>従業員:</strong> ${employee.employee_code} - ${
|
||||||
|
employee.name
|
||||||
|
} | <strong>支給日:</strong> ${
|
||||||
payroll.payment_date
|
payroll.payment_date
|
||||||
}</p>
|
} | <strong>甲欄扶養親族人数(源泉税控除対象):</strong> ${dependentCount}人</p>
|
||||||
<p>ステータス: <span class="status-badge status-${
|
<p>ステータス: <span class="status-badge status-${
|
||||||
payroll.status
|
payroll.status
|
||||||
}">${getStatusLabel(payroll.status)}</span></p>
|
}">${getStatusLabel(payroll.status)}</span></p>
|
||||||
|
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>勤怠情報</h3>
|
<h3>勤怠情報 ${
|
||||||
|
payroll.status === "calculated"
|
||||||
|
? '<button class="btn btn-primary" onclick="editPayroll(' +
|
||||||
|
payroll.payroll_id +
|
||||||
|
')" style="padding: 5px 10px; font-size: 12px; float: right;">編集</button>'
|
||||||
|
: ""
|
||||||
|
}</h3>
|
||||||
<div class="detail-row"><span>出勤日数:</span><span>${
|
<div class="detail-row"><span>出勤日数:</span><span>${
|
||||||
payroll.working_days
|
payroll.working_days
|
||||||
}日</span></div>
|
}日</span></div>
|
||||||
@@ -493,15 +551,27 @@
|
|||||||
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||||||
payroll.health_insurance
|
payroll.health_insurance
|
||||||
).toLocaleString()}</span></div>
|
).toLocaleString()}</span></div>
|
||||||
|
<div class="detail-row"><span>介護保険:</span><span>¥${Number(
|
||||||
|
payroll.care_insurance || 0
|
||||||
|
).toLocaleString()}</span></div>
|
||||||
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
||||||
payroll.pension_insurance
|
payroll.pension_insurance
|
||||||
).toLocaleString()}</span></div>
|
).toLocaleString()}</span></div>
|
||||||
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
||||||
payroll.employment_insurance
|
payroll.employment_insurance
|
||||||
).toLocaleString()}</span></div>
|
).toLocaleString()}</span></div>
|
||||||
<div class="detail-row"><span>所得税:</span><span>¥${Number(
|
<div class="detail-row">
|
||||||
payroll.income_tax
|
<span>所得税 <small style="color:#666;">(甲欄${dependentCount}人)</small>:</span>
|
||||||
).toLocaleString()}</span></div>
|
<span>¥${Number(
|
||||||
|
payroll.income_tax
|
||||||
|
).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 5px 10px; background: #f0f8ff; border-radius: 3px; margin: 5px 0; font-size: 12px; color: #666;">
|
||||||
|
<small>
|
||||||
|
※課税対象額から源泉徴収税額表(甲欄)を参照して計算<br>
|
||||||
|
扶養親族${dependentCount}人(19歳以上のみ対象)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||||||
payroll.resident_tax
|
payroll.resident_tax
|
||||||
).toLocaleString()}</span></div>
|
).toLocaleString()}</span></div>
|
||||||
@@ -544,6 +614,119 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function editPayroll(payrollId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/${payrollId}`
|
||||||
|
);
|
||||||
|
const payroll = await response.json();
|
||||||
|
|
||||||
|
// 編集フォームを表示
|
||||||
|
const html = `
|
||||||
|
<div style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 800px; max-height: 90vh; overflow-y: auto; background: white; padding: 30px; border: 2px solid #007bff; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 1000;">
|
||||||
|
<h2>給与データ編集</h2>
|
||||||
|
<form id="editPayrollForm" onsubmit="savePayrollEdit(event, ${payrollId})">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>勤怠情報</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>出勤日数</label>
|
||||||
|
<input type="number" name="working_days" step="0.5" value="${payroll.working_days}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>勤務時間</label>
|
||||||
|
<input type="number" name="working_hours" step="0.5" value="${payroll.working_hours}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>残業時間</label>
|
||||||
|
<input type="number" name="overtime_hours" step="0.5" value="${payroll.overtime_hours}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>休日出勤時間</label>
|
||||||
|
<input type="number" name="holiday_hours" step="0.5" value="${payroll.holiday_hours}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>遅刻時間</label>
|
||||||
|
<input type="number" name="late_hours" step="0.5" value="${payroll.late_hours}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>欠勤日数</label>
|
||||||
|
<input type="number" name="absent_days" step="0.5" value="${payroll.absent_days}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h3>その他</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他手当</label>
|
||||||
|
<input type="number" name="other_allowance" step="0.01" value="${payroll.other_allowance}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>住民税</label>
|
||||||
|
<input type="number" name="resident_tax" step="0.01" value="${payroll.resident_tax}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他控除</label>
|
||||||
|
<input type="number" name="other_deduction" step="0.01" value="${payroll.other_deduction}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">保存して再計算</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const editDiv = document.createElement("div");
|
||||||
|
editDiv.innerHTML = html;
|
||||||
|
document.body.appendChild(editDiv);
|
||||||
|
} catch (error) {
|
||||||
|
alert("給与データの取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePayrollEdit(event, payrollId) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("給与データを更新しました");
|
||||||
|
form.closest("div").remove();
|
||||||
|
|
||||||
|
// 再計算
|
||||||
|
await recalculatePayroll(payrollId);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("更新に失敗しました: " + error.detail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("更新に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function approvePayroll(payrollId) {
|
async function approvePayroll(payrollId) {
|
||||||
if (!confirm("この給与を承認しますか?")) return;
|
if (!confirm("この給与を承認しますか?")) return;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -56,311 +56,374 @@
|
|||||||
table th {
|
table th {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<a href="payroll.html" class="back-link">← 給与管理メニューに戻る</a>
|
||||||
|
|
||||||
<h1>給与管理 - 給与設定</h1>
|
<h1>給与管理 - 給与設定</h1>
|
||||||
|
|
||||||
<div class="employee-select">
|
<button class="btn btn-primary" onclick="showAddForm()">
|
||||||
<label><strong>従業員を選択:</strong></label>
|
新規設定追加
|
||||||
<select id="employeeSelect" onchange="loadSalarySettings()">
|
</button>
|
||||||
<option value="">従業員を選択してください</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settingsContent" style="display: none">
|
<div id="settingsList" style="margin-top: 20px"></div>
|
||||||
<h2>給与設定履歴</h2>
|
|
||||||
<button class="btn btn-primary" onclick="showAddForm()">
|
|
||||||
新規設定追加
|
|
||||||
</button>
|
|
||||||
<div id="settingsList"></div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="addForm"
|
id="addForm"
|
||||||
style="
|
style="
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 2px solid #007bff;
|
border: 2px solid #007bff;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<h3>給与設定の追加</h3>
|
<h3 id="formTitle">給与設定の追加</h3>
|
||||||
<form onsubmit="saveSalarySetting(event)">
|
<form onsubmit="saveSalarySetting(event)">
|
||||||
<div class="form-group">
|
<input type="hidden" name="setting_id" id="setting_id" />
|
||||||
<label>基本給 (円) *</label>
|
|
||||||
<input type="number" name="base_salary" step="0.01" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>時給 (円)</label>
|
|
||||||
<input type="number" name="hourly_rate" step="0.01" />
|
|
||||||
<small>時給制の場合に入力してください</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>雇用形態 *</label>
|
|
||||||
<select name="employment_type" required>
|
|
||||||
<option value="">選択してください</option>
|
|
||||||
<option value="正社員">正社員</option>
|
|
||||||
<option value="契約社員">契約社員</option>
|
|
||||||
<option value="パート">パート</option>
|
|
||||||
<option value="アルバイト">アルバイト</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>支給形態 *</label>
|
|
||||||
<select name="payment_type" required>
|
|
||||||
<option value="">選択してください</option>
|
|
||||||
<option value="月給">月給</option>
|
|
||||||
<option value="時給">時給</option>
|
|
||||||
<option value="日給">日給</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>通勤手当 (円)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="commute_allowance"
|
|
||||||
step="0.01"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>その他手当 (円)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="other_allowance"
|
|
||||||
step="0.01"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>適用開始日 *</label>
|
|
||||||
<input type="date" name="valid_from" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>適用終了日</label>
|
|
||||||
<input type="date" name="valid_to" />
|
|
||||||
<small>通常は空欄のままにしてください</small>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">保存</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
onclick="hideAddForm()"
|
|
||||||
>
|
|
||||||
キャンセル
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div class="form-group">
|
||||||
id="currentSetting"
|
<label>従業員 *</label>
|
||||||
style="
|
<select name="employee_id" id="employee_id" required>
|
||||||
margin-top: 30px;
|
<option value="">選択してください</option>
|
||||||
padding: 20px;
|
</select>
|
||||||
background: #e7f3ff;
|
</div>
|
||||||
border-radius: 5px;
|
<div class="form-group">
|
||||||
"
|
<label>基本給 (円) *</label>
|
||||||
>
|
<input
|
||||||
<h3>現在の給与設定</h3>
|
type="number"
|
||||||
<div id="currentSettingContent"></div>
|
name="base_salary"
|
||||||
</div>
|
id="base_salary"
|
||||||
|
step="0.01"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>時給 (円)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="hourly_rate"
|
||||||
|
id="hourly_rate"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
<small>時給制の場合に入力してください</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>雇用形態 *</label>
|
||||||
|
<select name="employment_type" id="employment_type" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="正社員">正社員</option>
|
||||||
|
<option value="契約社員">契約社員</option>
|
||||||
|
<option value="パート">パート</option>
|
||||||
|
<option value="アルバイト">アルバイト</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>支給形態 *</label>
|
||||||
|
<select name="payment_type" id="payment_type" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="月給">月給</option>
|
||||||
|
<option value="時給">時給</option>
|
||||||
|
<option value="日給">日給</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>通勤手当 (円)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="commute_allowance"
|
||||||
|
id="commute_allowance"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>その他手当 (円)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="other_allowance"
|
||||||
|
id="other_allowance"
|
||||||
|
step="0.01"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>適用開始日 *</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="valid_from"
|
||||||
|
id="valid_from"
|
||||||
|
required
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>適用終了日</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="valid_to"
|
||||||
|
id="valid_to"
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
/>
|
||||||
|
<small>通常は空欄のままにしてください</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="hideAddForm()"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = "http://localhost:8000";
|
const API_BASE = "http://localhost:18080";
|
||||||
let currentEmployeeId = null;
|
let employees = [];
|
||||||
|
|
||||||
async function loadEmployees() {
|
async function loadEmployees() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/payroll/employees/?is_active=true`
|
`${API_BASE}/payroll/employees/?is_active=true`
|
||||||
);
|
);
|
||||||
const employees = await response.json();
|
employees = await response.json();
|
||||||
|
|
||||||
const select = document.getElementById("employeeSelect");
|
const select = document.getElementById("employee_id");
|
||||||
select.innerHTML =
|
select.innerHTML =
|
||||||
'<option value="">従業員を選択してください</option>' +
|
'<option value="">選択してください</option>' +
|
||||||
employees
|
employees
|
||||||
.map(
|
.map(
|
||||||
(emp) =>
|
(emp) =>
|
||||||
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
|
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
loadAllSalarySettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("従業員一覧の取得に失敗しました");
|
alert("従業員一覧の取得に失敗しました");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSalarySettings() {
|
async function loadAllSalarySettings() {
|
||||||
const employeeId = document.getElementById("employeeSelect").value;
|
|
||||||
if (!employeeId) {
|
|
||||||
document.getElementById("settingsContent").style.display = "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentEmployeeId = employeeId;
|
|
||||||
document.getElementById("settingsContent").style.display = "block";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 給与設定履歴を取得
|
// すべての従業員の給与設定を取得
|
||||||
const response = await fetch(
|
const allSettings = [];
|
||||||
`${API_BASE}/payroll/settings/salary/${employeeId}`
|
for (const emp of employees) {
|
||||||
);
|
try {
|
||||||
const settings = await response.json();
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/salary/${emp.employee_id}/current`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const setting = await response.json();
|
||||||
|
allSettings.push({
|
||||||
|
...setting,
|
||||||
|
employee_code: emp.employee_code,
|
||||||
|
employee_name: emp.name,
|
||||||
|
employee_id: emp.employee_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`従業員 ${emp.employee_id} の給与設定取得失敗`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const html = `
|
displayAllSettings(allSettings);
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>適用期間</th>
|
|
||||||
<th>基本給</th>
|
|
||||||
<th>時給</th>
|
|
||||||
<th>雇用形態</th>
|
|
||||||
<th>支給形態</th>
|
|
||||||
<th>通勤手当</th>
|
|
||||||
<th>その他手当</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${settings
|
|
||||||
.map(
|
|
||||||
(s) => `
|
|
||||||
<tr>
|
|
||||||
<td>${s.valid_from} ~ ${
|
|
||||||
s.valid_to || "現在"
|
|
||||||
}</td>
|
|
||||||
<td>¥${Number(
|
|
||||||
s.base_salary
|
|
||||||
).toLocaleString()}</td>
|
|
||||||
<td>${
|
|
||||||
s.hourly_rate
|
|
||||||
? "¥" +
|
|
||||||
Number(s.hourly_rate).toLocaleString()
|
|
||||||
: "-"
|
|
||||||
}</td>
|
|
||||||
<td>${s.employment_type}</td>
|
|
||||||
<td>${s.payment_type}</td>
|
|
||||||
<td>¥${Number(
|
|
||||||
s.commute_allowance
|
|
||||||
).toLocaleString()}</td>
|
|
||||||
<td>¥${Number(
|
|
||||||
s.other_allowance
|
|
||||||
).toLocaleString()}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById("settingsList").innerHTML = html;
|
|
||||||
|
|
||||||
// 現在の設定を取得
|
|
||||||
loadCurrentSetting(employeeId);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("給与設定の取得に失敗しました");
|
alert("給与設定の取得に失敗しました");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCurrentSetting(employeeId) {
|
function displayAllSettings(settings) {
|
||||||
|
const html = `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>従業員コード</th>
|
||||||
|
<th>氏名</th>
|
||||||
|
<th>基本給</th>
|
||||||
|
<th>時給</th>
|
||||||
|
<th>雇用形態</th>
|
||||||
|
<th>支給形態</th>
|
||||||
|
<th>通勤手当</th>
|
||||||
|
<th>その他手当</th>
|
||||||
|
<th>適用期間</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${
|
||||||
|
settings.length > 0
|
||||||
|
? settings
|
||||||
|
.map(
|
||||||
|
(s) => `
|
||||||
|
<tr>
|
||||||
|
<td>${s.employee_code}</td>
|
||||||
|
<td>${s.employee_name}</td>
|
||||||
|
<td>¥${Number(s.base_salary).toLocaleString()}</td>
|
||||||
|
<td>${
|
||||||
|
s.hourly_rate
|
||||||
|
? "¥" + Number(s.hourly_rate).toLocaleString()
|
||||||
|
: "-"
|
||||||
|
}</td>
|
||||||
|
<td>${s.employment_type}</td>
|
||||||
|
<td>${s.payment_type}</td>
|
||||||
|
<td>¥${Number(s.commute_allowance).toLocaleString()}</td>
|
||||||
|
<td>¥${Number(s.other_allowance).toLocaleString()}</td>
|
||||||
|
<td>${s.valid_from} ~ ${s.valid_to || "現在"}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" onclick="editSetting(${
|
||||||
|
s.setting_id
|
||||||
|
})" style="padding: 5px 10px; font-size: 12px;">編集</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: '<tr><td colspan="10" style="text-align:center;">給与設定が登録されていません</td></tr>'
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("settingsList").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editSetting(settingId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
// 設定IDから詳細を取得する必要がありますが、現在のAPIでは難しいので
|
||||||
`${API_BASE}/payroll/settings/salary/${employeeId}/current`
|
// 全従業員の設定を取得して該当するものを探す
|
||||||
);
|
for (const emp of employees) {
|
||||||
if (response.ok) {
|
const response = await fetch(
|
||||||
const setting = await response.json();
|
`${API_BASE}/payroll/settings/salary/${emp.employee_id}`
|
||||||
|
);
|
||||||
|
const settings = await response.json();
|
||||||
|
const setting = settings.find((s) => s.setting_id === settingId);
|
||||||
|
|
||||||
const html = `
|
if (setting) {
|
||||||
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:15px;">
|
// フォームに値を設定
|
||||||
<div><strong>基本給:</strong> ¥${Number(
|
document.getElementById("formTitle").textContent =
|
||||||
setting.base_salary
|
"給与設定の編集";
|
||||||
).toLocaleString()}</div>
|
document.getElementById("setting_id").value = setting.setting_id;
|
||||||
<div><strong>時給:</strong> ${
|
document.getElementById("employee_id").value = emp.employee_id;
|
||||||
setting.hourly_rate
|
document.getElementById("employee_id").disabled = true; // 従業員は変更不可
|
||||||
? "¥" +
|
document.getElementById("base_salary").value =
|
||||||
Number(setting.hourly_rate).toLocaleString()
|
setting.base_salary;
|
||||||
: "-"
|
document.getElementById("hourly_rate").value =
|
||||||
}</div>
|
setting.hourly_rate || "";
|
||||||
<div><strong>雇用形態:</strong> ${
|
document.getElementById("employment_type").value =
|
||||||
setting.employment_type
|
setting.employment_type;
|
||||||
}</div>
|
document.getElementById("payment_type").value =
|
||||||
<div><strong>支給形態:</strong> ${
|
setting.payment_type;
|
||||||
setting.payment_type
|
document.getElementById("commute_allowance").value =
|
||||||
}</div>
|
setting.commute_allowance;
|
||||||
<div><strong>通勤手当:</strong> ¥${Number(
|
document.getElementById("other_allowance").value =
|
||||||
setting.commute_allowance
|
setting.other_allowance;
|
||||||
).toLocaleString()}</div>
|
document.getElementById("valid_from").value = setting.valid_from;
|
||||||
<div><strong>その他手当:</strong> ¥${Number(
|
document.getElementById("valid_to").value =
|
||||||
setting.other_allowance
|
setting.valid_to || "";
|
||||||
).toLocaleString()}</div>
|
|
||||||
<div><strong>適用期間:</strong> ${
|
|
||||||
setting.valid_from
|
|
||||||
} ~ ${setting.valid_to || "現在"}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById("currentSettingContent").innerHTML = html;
|
document.getElementById("addForm").style.display = "block";
|
||||||
} else {
|
return;
|
||||||
document.getElementById("currentSettingContent").innerHTML =
|
}
|
||||||
"<p>給与設定が登録されていません</p>";
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
document.getElementById("currentSettingContent").innerHTML =
|
alert("給与設定の取得に失敗しました");
|
||||||
"<p>給与設定の取得に失敗しました</p>";
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddForm() {
|
function showAddForm() {
|
||||||
|
document.getElementById("formTitle").textContent = "給与設定の追加";
|
||||||
document.getElementById("addForm").style.display = "block";
|
document.getElementById("addForm").style.display = "block";
|
||||||
|
document.querySelector("#addForm form").reset();
|
||||||
|
document.getElementById("setting_id").value = "";
|
||||||
|
document.getElementById("employee_id").disabled = false;
|
||||||
// デフォルト値設定
|
// デフォルト値設定
|
||||||
document.querySelector('[name="valid_from"]').value = new Date()
|
document.getElementById("valid_from").value = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.split("T")[0];
|
.split("T")[0];
|
||||||
|
document.getElementById("commute_allowance").value = "0";
|
||||||
|
document.getElementById("other_allowance").value = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAddForm() {
|
function hideAddForm() {
|
||||||
document.getElementById("addForm").style.display = "none";
|
document.getElementById("addForm").style.display = "none";
|
||||||
document.querySelector("#addForm form").reset();
|
document.querySelector("#addForm form").reset();
|
||||||
|
document.getElementById("employee_id").disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSalarySetting(event) {
|
async function saveSalarySetting(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const data = Object.fromEntries(formData.entries());
|
const settingId = document.getElementById("setting_id").value;
|
||||||
|
|
||||||
data.employee_id = parseInt(currentEmployeeId);
|
const data = {
|
||||||
data.base_salary = parseFloat(data.base_salary) || 0;
|
employee_id: parseInt(formData.get("employee_id")),
|
||||||
data.commute_allowance = parseFloat(data.commute_allowance) || 0;
|
base_salary: parseFloat(formData.get("base_salary")) || 0,
|
||||||
data.other_allowance = parseFloat(data.other_allowance) || 0;
|
employment_type: formData.get("employment_type"),
|
||||||
|
payment_type: formData.get("payment_type"),
|
||||||
|
commute_allowance: parseFloat(formData.get("commute_allowance")) || 0,
|
||||||
|
other_allowance: parseFloat(formData.get("other_allowance")) || 0,
|
||||||
|
valid_from: formData.get("valid_from"),
|
||||||
|
};
|
||||||
|
|
||||||
if (data.hourly_rate) {
|
if (formData.get("hourly_rate")) {
|
||||||
data.hourly_rate = parseFloat(data.hourly_rate);
|
data.hourly_rate = parseFloat(formData.get("hourly_rate"));
|
||||||
} else {
|
|
||||||
delete data.hourly_rate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.valid_to) {
|
if (formData.get("valid_to")) {
|
||||||
delete data.valid_to;
|
data.valid_to = formData.get("valid_to");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/payroll/settings/salary`, {
|
let response;
|
||||||
method: "POST",
|
if (settingId) {
|
||||||
headers: { "Content-Type": "application/json" },
|
// 更新
|
||||||
body: JSON.stringify(data),
|
response = await fetch(
|
||||||
});
|
`${API_BASE}/payroll/settings/salary/${settingId}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 新規追加
|
||||||
|
response = await fetch(`${API_BASE}/payroll/settings/salary`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert("給与設定を保存しました");
|
alert(
|
||||||
|
settingId ? "給与設定を更新しました" : "給与設定を保存しました"
|
||||||
|
);
|
||||||
hideAddForm();
|
hideAddForm();
|
||||||
loadSalarySettings();
|
loadAllSalarySettings();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert("保存に失敗しました: " + error.detail);
|
alert("保存に失敗しました: " + error.detail);
|
||||||
|
|||||||
@@ -114,10 +114,57 @@
|
|||||||
border-left: 4px solid #007bff;
|
border-left: 4px solid #007bff;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
.year-item {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.year-item:hover {
|
||||||
|
background: #e8f4ff;
|
||||||
|
}
|
||||||
|
.year-item.expanded {
|
||||||
|
background: #e8f4ff;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
.year-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.year-data {
|
||||||
|
display: none;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.year-item.expanded .year-data {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.filter-controls {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<a href="payroll.html" class="back-link">← 給与管理メニューに戻る</a>
|
||||||
|
|
||||||
<h1>給与管理 - 設定</h1>
|
<h1>給与管理 - 設定</h1>
|
||||||
|
|
||||||
<div class="nav-tabs">
|
<div class="nav-tabs">
|
||||||
@@ -129,6 +176,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="nav-tab" onclick="showTab('limits')">上限設定</button>
|
<button class="nav-tab" onclick="showTab('limits')">上限設定</button>
|
||||||
<button class="nav-tab" onclick="showTab('tax')">所得税率表</button>
|
<button class="nav-tab" onclick="showTab('tax')">所得税率表</button>
|
||||||
|
<button class="nav-tab" onclick="showTab('standard-remuneration')">
|
||||||
|
標準報酬月額表
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保険料率タブ -->
|
<!-- 保険料率タブ -->
|
||||||
@@ -459,11 +509,22 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>適用開始日 *</label>
|
<label>適用開始日 *</label>
|
||||||
<input type="date" name="valid_from" required />
|
<input
|
||||||
|
type="date"
|
||||||
|
name="valid_from"
|
||||||
|
required
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>適用終了日</label>
|
<label>適用終了日</label>
|
||||||
<input type="date" name="valid_to" />
|
<input
|
||||||
|
type="date"
|
||||||
|
name="valid_to"
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -487,18 +548,6 @@
|
|||||||
<!-- 所得税率表タブ -->
|
<!-- 所得税率表タブ -->
|
||||||
<div id="tab-tax" class="tab-content">
|
<div id="tab-tax" class="tab-content">
|
||||||
<h2>所得税率表</h2>
|
<h2>所得税率表</h2>
|
||||||
<div class="form-group" style="max-width: 300px">
|
|
||||||
<label>扶養人数でフィルター</label>
|
|
||||||
<select id="filterDependents" onchange="loadIncomeTax()">
|
|
||||||
<option value="">全て</option>
|
|
||||||
<option value="0">0人</option>
|
|
||||||
<option value="1">1人</option>
|
|
||||||
<option value="2">2人</option>
|
|
||||||
<option value="3">3人</option>
|
|
||||||
<option value="4">4人</option>
|
|
||||||
<option value="5">5人以上</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onclick="document.getElementById('csvFile').click()"
|
onclick="document.getElementById('csvFile').click()"
|
||||||
@@ -512,7 +561,40 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
onchange="importTaxTable(event)"
|
onchange="importTaxTable(event)"
|
||||||
/>
|
/>
|
||||||
<div id="taxList"></div>
|
<div id="taxYearsList" style="margin-top: 20px"></div>
|
||||||
|
<div id="taxDataContainer" style="margin-top: 20px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 標準報酬月額表タブ -->
|
||||||
|
<div id="tab-standard-remuneration" class="tab-content">
|
||||||
|
<h2>標準報酬月額表</h2>
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>標準報酬月額表について:</strong>
|
||||||
|
<ul style="margin: 10px 0 0 20px">
|
||||||
|
<li>健康保険・厚生年金保険の保険料額表をインポートします</li>
|
||||||
|
<li>都道府県ごとの料率表に対応しています</li>
|
||||||
|
<li>年度と都道府県を指定してください</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick="document.getElementById('stdRemunerationFile').click()"
|
||||||
|
>
|
||||||
|
Excel/CSVインポート
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="stdRemunerationFile"
|
||||||
|
accept=".csv,.xlsx,.xls"
|
||||||
|
style="display: none"
|
||||||
|
onchange="importStdRemunerationTable(event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stdRemunerationYearsList" style="margin-top: 20px"></div>
|
||||||
|
<div id="stdRemunerationDataContainer" style="margin-top: 20px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -535,6 +617,7 @@
|
|||||||
"child-support": 1,
|
"child-support": 1,
|
||||||
limits: 2,
|
limits: 2,
|
||||||
tax: 3,
|
tax: 3,
|
||||||
|
"standard-remuneration": 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
document
|
document
|
||||||
@@ -551,6 +634,8 @@
|
|||||||
loadLimitSettings();
|
loadLimitSettings();
|
||||||
} else if (tabName === "tax") {
|
} else if (tabName === "tax") {
|
||||||
loadIncomeTax();
|
loadIncomeTax();
|
||||||
|
} else if (tabName === "standard-remuneration") {
|
||||||
|
loadStdRemunerationData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1189,21 +1274,95 @@
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
// 所得税率表関連
|
// 所得税率表関連
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
let currentExpandedYear = null;
|
||||||
|
|
||||||
async function loadIncomeTax() {
|
async function loadIncomeTax() {
|
||||||
const dependentsFilter =
|
await loadIncomeTaxYears();
|
||||||
document.getElementById("filterDependents").value;
|
}
|
||||||
const params = dependentsFilter
|
|
||||||
? `?dependents_count=${dependentsFilter}`
|
async function loadIncomeTaxYears() {
|
||||||
: "";
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/income-tax/years`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.years || data.years.length === 0) {
|
||||||
|
document.getElementById("taxYearsList").innerHTML =
|
||||||
|
"<p>データがありません。</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = data.years
|
||||||
|
.map(
|
||||||
|
(year) => `
|
||||||
|
<div class="year-item" id="year-${year}" onclick="toggleYear(${year})">
|
||||||
|
<div class="year-header">
|
||||||
|
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
|
||||||
|
</div>
|
||||||
|
<div class="year-data" id="data-${year}">
|
||||||
|
<div class="filter-controls">
|
||||||
|
<label>扶養人数でフィルター: </label>
|
||||||
|
<select id="filter-${year}" onchange="loadYearData(${year})">
|
||||||
|
<option value="">全て</option>
|
||||||
|
<option value="0">0人</option>
|
||||||
|
<option value="1">1人</option>
|
||||||
|
<option value="2">2人</option>
|
||||||
|
<option value="3">3人</option>
|
||||||
|
<option value="4">4人</option>
|
||||||
|
<option value="5">5人</option>
|
||||||
|
<option value="6">6人</option>
|
||||||
|
<option value="7">7人</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="table-${year}">読み込み中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
document.getElementById("taxYearsList").innerHTML = html;
|
||||||
|
} catch (error) {
|
||||||
|
alert("年度リストの取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleYear(year) {
|
||||||
|
const yearItem = document.getElementById(`year-${year}`);
|
||||||
|
const wasExpanded = yearItem.classList.contains("expanded");
|
||||||
|
|
||||||
|
// 他の年度を閉じる
|
||||||
|
document.querySelectorAll(".year-item").forEach((item) => {
|
||||||
|
item.classList.remove("expanded");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wasExpanded) {
|
||||||
|
yearItem.classList.add("expanded");
|
||||||
|
currentExpandedYear = year;
|
||||||
|
await loadYearData(year);
|
||||||
|
} else {
|
||||||
|
currentExpandedYear = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadYearData(year) {
|
||||||
|
const filterSelect = document.getElementById(`filter-${year}`);
|
||||||
|
const dependentsFilter = filterSelect ? filterSelect.value : "";
|
||||||
|
const params = new URLSearchParams({ tax_year: year });
|
||||||
|
if (dependentsFilter) {
|
||||||
|
params.append("dependents_count", dependentsFilter);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/payroll/settings/income-tax${params}`
|
`${API_BASE}/payroll/settings/income-tax?${params}`
|
||||||
);
|
);
|
||||||
const taxes = await response.json();
|
const taxes = await response.json();
|
||||||
|
|
||||||
if (taxes.length === 0) {
|
if (taxes.length === 0) {
|
||||||
document.getElementById("taxList").innerHTML =
|
document.getElementById(`table-${year}`).innerHTML =
|
||||||
"<p>データがありません。</p>";
|
"<p>データがありません。</p>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1223,27 +1382,25 @@
|
|||||||
${taxes
|
${taxes
|
||||||
.map(
|
.map(
|
||||||
(tax) => `
|
(tax) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${tax.dependents_count}人</td>
|
<td>${tax.dependents_count}人</td>
|
||||||
<td>¥${Number(
|
<td>¥${Number(
|
||||||
tax.monthly_income_from
|
tax.monthly_income_from
|
||||||
).toLocaleString()}</td>
|
).toLocaleString()}</td>
|
||||||
<td>¥${Number(
|
<td>¥${Number(tax.monthly_income_to).toLocaleString()}</td>
|
||||||
tax.monthly_income_to
|
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
|
||||||
).toLocaleString()}</td>
|
<td>${tax.valid_from} ~ ${tax.valid_to || "現在"}</td>
|
||||||
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
|
</tr>
|
||||||
<td>${tax.valid_from} ~ ${tax.valid_to || "現在"}</td>
|
`
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)
|
)
|
||||||
.join("")}
|
.join("")}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.getElementById("taxList").innerHTML = html;
|
document.getElementById(`table-${year}`).innerHTML = html;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("所得税率表の取得に失敗しました");
|
alert("データの取得に失敗しました");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1274,7 +1431,7 @@
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
alert(result.message + `\n年度: ${result.year}`);
|
alert(result.message + `\n年度: ${result.year}`);
|
||||||
loadIncomeTax();
|
loadIncomeTaxYears();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
|
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
|
||||||
@@ -1287,6 +1444,224 @@
|
|||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// 標準報酬月額表関連の関数
|
||||||
|
// =====================================================
|
||||||
|
async function loadStdRemunerationData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/standard-remuneration/years`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to load years");
|
||||||
|
}
|
||||||
|
const years = await response.json();
|
||||||
|
|
||||||
|
if (!years || years.length === 0) {
|
||||||
|
document.getElementById("stdRemunerationYearsList").innerHTML =
|
||||||
|
"<p>データがありません。Excelファイルをインポートしてください。</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = years
|
||||||
|
.map(
|
||||||
|
(year) => `
|
||||||
|
<div class="year-item" id="std-year-${year}">
|
||||||
|
<div class="year-header" onclick="toggleStdRemunerationYear(${year})">
|
||||||
|
<span class="year-title">${year}年度</span>
|
||||||
|
<span class="expand-icon">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="year-content" id="std-content-${year}"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
document.getElementById("stdRemunerationYearsList").innerHTML = html;
|
||||||
|
} catch (error) {
|
||||||
|
alert("年度リストの取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleStdRemunerationYear(year) {
|
||||||
|
const yearItem = document.getElementById(`std-year-${year}`);
|
||||||
|
const wasExpanded = yearItem.classList.contains("expanded");
|
||||||
|
|
||||||
|
// 他の年度を閉じる
|
||||||
|
document.querySelectorAll(".year-item").forEach((item) => {
|
||||||
|
item.classList.remove("expanded");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wasExpanded) {
|
||||||
|
yearItem.classList.add("expanded");
|
||||||
|
await loadStdRemunerationYearData(year);
|
||||||
|
} else {
|
||||||
|
document.getElementById(`std-content-${year}`).innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStdRemunerationYearData(year) {
|
||||||
|
const filterSelect = document.getElementById(`std-filter-${year}`);
|
||||||
|
const prefectureFilter = filterSelect ? filterSelect.value : "";
|
||||||
|
const params = new URLSearchParams({ year: year });
|
||||||
|
if (prefectureFilter) {
|
||||||
|
params.append("prefecture", prefectureFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/standard-remuneration?${params}`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
document.getElementById(`std-content-${year}`).innerHTML =
|
||||||
|
'<p class="no-data">データがありません</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 都道府県でグループ化
|
||||||
|
const groupedByPrefecture = {};
|
||||||
|
data.forEach((row) => {
|
||||||
|
if (!groupedByPrefecture[row.prefecture]) {
|
||||||
|
groupedByPrefecture[row.prefecture] = [];
|
||||||
|
}
|
||||||
|
groupedByPrefecture[row.prefecture].push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 都道府県一覧を取得
|
||||||
|
const prefectures = Object.keys(groupedByPrefecture).sort();
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="filter-section">
|
||||||
|
<label>都道府県で絞り込み:</label>
|
||||||
|
<select id="std-filter-${year}" onchange="loadStdRemunerationYearData(${year})" style="margin-left: 10px">
|
||||||
|
<option value="">すべて</option>
|
||||||
|
${prefectures
|
||||||
|
.map(
|
||||||
|
(pref) =>
|
||||||
|
`<option value="${pref}" ${
|
||||||
|
prefectureFilter === pref ? "selected" : ""
|
||||||
|
}>${pref}</option>`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
${Object.entries(groupedByPrefecture)
|
||||||
|
.map(
|
||||||
|
([prefecture, rows]) => `
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
<h4>${prefecture}</h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2">等級</th>
|
||||||
|
<th rowspan="2">標準報酬月額</th>
|
||||||
|
<th colspan="2">報酬月額</th>
|
||||||
|
<th colspan="2">健康保険料</th>
|
||||||
|
<th rowspan="2">厚生年金保険料</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>以上</th>
|
||||||
|
<th>未満</th>
|
||||||
|
<th>介護保険なし</th>
|
||||||
|
<th>介護保険あり</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rows
|
||||||
|
.map(
|
||||||
|
(row) => `
|
||||||
|
<tr>
|
||||||
|
<td>${row.grade}</td>
|
||||||
|
<td class="number">${formatNumber(
|
||||||
|
row.monthly_amount
|
||||||
|
)}</td>
|
||||||
|
<td class="number">${formatNumber(row.salary_from)}</td>
|
||||||
|
<td class="number">${
|
||||||
|
row.salary_to ? formatNumber(row.salary_to) : "〜"
|
||||||
|
}</td>
|
||||||
|
<td class="number">${formatNumber(
|
||||||
|
row.health_insurance_no_care
|
||||||
|
)}</td>
|
||||||
|
<td class="number">${formatNumber(
|
||||||
|
row.health_insurance_with_care
|
||||||
|
)}</td>
|
||||||
|
<td class="number">${formatNumber(
|
||||||
|
row.pension_insurance
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById(`std-content-${year}`).innerHTML = html;
|
||||||
|
} catch (error) {
|
||||||
|
alert("データの取得に失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importStdRemunerationTable(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const prefecture = prompt(
|
||||||
|
"都道府県名を入力してください(例: 東京):",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
if (!prefecture || !prefecture.trim()) {
|
||||||
|
alert("都道府県名を入力してください");
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("prefecture", prefecture.trim());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/payroll/settings/standard-remuneration/import`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
alert(
|
||||||
|
`インポートしました: ${result.imported_count}件\n年度: ${
|
||||||
|
result.year
|
||||||
|
}年\n都道府県: ${prefecture}\n\n取得元: ${result.source || ""}`
|
||||||
|
);
|
||||||
|
loadStdRemunerationData();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("インポートに失敗しました");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num) {
|
||||||
|
if (num === null || num === undefined) return "-";
|
||||||
|
return Number(num).toLocaleString("ja-JP");
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// 初期化
|
// 初期化
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|||||||
@@ -99,10 +99,10 @@
|
|||||||
|
|
||||||
<div class="search-form">
|
<div class="search-form">
|
||||||
<label for="dateFrom">開始年月日:</label>
|
<label for="dateFrom">開始年月日:</label>
|
||||||
<input type="date" id="dateFrom" min="1900-01-01" max="9999-12-31" />
|
<input type="date" id="dateFrom" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<label for="dateTo">終了年月日:</label>
|
<label for="dateTo">終了年月日:</label>
|
||||||
<input type="date" id="dateTo" min="1900-01-01" max="9999-12-31" />
|
<input type="date" id="dateTo" min="1900-01-01" max="2099-12-31" />
|
||||||
|
|
||||||
<button id="btnSearch">検索</button>
|
<button id="btnSearch">検索</button>
|
||||||
<button id="btnFullYear" class="secondary">全年度表示</button>
|
<button id="btnFullYear" class="secondary">全年度表示</button>
|
||||||
|
|||||||
BIN
reports/tmp/r7ippan3.xlsx
Normal file
BIN
reports/tmp/r7ippan3.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user