This commit is contained in:
admin
2026-01-22 01:16:54 +09:00
parent 86020ada5c
commit 3d91356877
16 changed files with 1862 additions and 494 deletions

View File

@@ -123,14 +123,16 @@ app.mount(
# name="static", # name="static",
# ) # )
# フロントエンドファイル配信(コメントアウト - file://プロトコルで直接アクセスするため不要) # フロントエンドファイル配信
# frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend") frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
# if os.path.exists(frontend_path): if os.path.exists(frontend_path):
# app.mount( app.mount(
# "/", "/frontend",
# StaticFiles(directory=frontend_path, html=True), StaticFiles(directory=frontend_path, html=True),
# name="frontend", name="frontend",
# ) )
else:
print(f"Frontend path not found: {frontend_path}")

View File

@@ -128,6 +128,69 @@ def approve_bonus(bonus_id: int, approved_by: str):
return result return result
@router.post("/{bonus_id}/recalculate", response_model=schemas.BonusPayment)
def recalculate_bonus(bonus_id: int):
"""賞与を再計算"""
with get_connection() as conn:
with conn.cursor() as cur:
# 既存データを取得
cur.execute("SELECT * FROM bonus_payments WHERE bonus_id = %s", (bonus_id,))
existing = cur.fetchone()
if not existing:
raise HTTPException(status_code=404, detail="賞与データが見つかりません")
if existing["status"] == "approved":
raise HTTPException(status_code=400, detail="承認済みの賞与は再計算できません")
# 再計算
try:
calc_result = BonusCalculationService.calculate_bonus(
employee_id=existing["employee_id"],
bonus_year=existing["bonus_year"],
bonus_month=existing["bonus_month"],
bonus_type=existing["bonus_type"],
payment_date=existing["payment_date"],
base_bonus=existing["base_bonus"],
performance_bonus=existing["performance_bonus"],
other_bonus=existing["other_bonus"],
other_deduction=existing["other_deduction"],
calculated_by="recalculated"
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# 更新
cur.execute(
"""
UPDATE bonus_payments SET
base_bonus = %s, performance_bonus = %s, other_bonus = %s,
total_bonus = %s,
health_insurance = %s, care_insurance = %s, pension_insurance = %s,
employment_insurance = %s, income_tax = %s, other_deduction = %s,
total_deduction = %s, net_bonus = %s,
calculated_at = %s, calculated_by = %s
WHERE bonus_id = %s
RETURNING *
""",
(
calc_result["base_bonus"], calc_result["performance_bonus"],
calc_result["other_bonus"], calc_result["total_bonus"],
calc_result["health_insurance"], calc_result["care_insurance"],
calc_result["pension_insurance"], calc_result["employment_insurance"],
calc_result["income_tax"], calc_result["other_deduction"],
calc_result["total_deduction"], calc_result["net_bonus"],
datetime.now(), "recalculated",
bonus_id
)
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="賞与を更新できません")
conn.commit()
return result
@router.delete("/{bonus_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{bonus_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bonus(bonus_id: int): def delete_bonus(bonus_id: int):
"""賞与を削除""" """賞与を削除"""

View File

@@ -58,12 +58,15 @@ class BonusCalculationService:
cur.execute( cur.execute(
""" """
SELECT AVG(total_payment - commute_allowance) as avg_salary SELECT AVG(total_payment - commute_allowance) as avg_salary
FROM monthly_payroll FROM (
WHERE employee_id = %s SELECT total_payment, commute_allowance
AND payment_date < %s FROM monthly_payroll
AND status IN ('approved', 'paid') WHERE employee_id = %s
ORDER BY payment_date DESC AND payment_date < %s
LIMIT 3 AND status IN ('approved', 'paid')
ORDER BY payment_date DESC
LIMIT 3
) as recent_payroll
""", """,
(employee_id, payment_date) (employee_id, payment_date)
) )

View File

@@ -262,7 +262,7 @@ def approve_payroll(payroll_id: int, request: schemas.PayrollApprovalRequest):
return result return result
@router.delete("/{payroll_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{payroll_id}")
def delete_payroll(payroll_id: int): def delete_payroll(payroll_id: int):
"""給与データを削除""" """給与データを削除"""
with get_connection() as conn: with get_connection() as conn:
@@ -282,3 +282,5 @@ def delete_payroll(payroll_id: int):
cur.execute("DELETE FROM monthly_payroll WHERE payroll_id = %s", (payroll_id,)) cur.execute("DELETE FROM monthly_payroll WHERE payroll_id = %s", (payroll_id,))
conn.commit() conn.commit()
return {"message": "給与データを削除しました", "payroll_id": payroll_id}

View File

@@ -132,22 +132,25 @@ class PayrollCalculationService:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""給与を計算""" """給与を計算"""
# バリデーションエラーを集約
errors = []
# 給与設定を取得 # 給与設定を取得
salary_setting = PayrollCalculationService.get_salary_setting(employee_id, payment_date) salary_setting = PayrollCalculationService.get_salary_setting(employee_id, payment_date)
if not salary_setting: if not salary_setting:
raise ValueError("給与設定が見つかりません") errors.append("給与設定が見つかりません")
# 基本給の計算 # 基本給の計算
base_salary = Decimal(str(salary_setting["base_salary"])) base_salary = Decimal(str(salary_setting["base_salary"])) if salary_setting else Decimal("0")
# 時給の場合は勤務時間から計算 # 時給の場合は勤務時間から計算
if salary_setting["payment_type"] == "時給" and salary_setting["hourly_rate"]: if salary_setting and salary_setting["payment_type"] == "時給" and salary_setting["hourly_rate"]:
hourly_rate = Decimal(str(salary_setting["hourly_rate"])) hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
base_salary = hourly_rate * working_hours base_salary = hourly_rate * working_hours
# 欠勤控除(日給制の場合) # 欠勤控除(日給制の場合)
absence_deduction = Decimal("0") absence_deduction = Decimal("0")
if absent_days > 0 and salary_setting["payment_type"] == "月給": if absent_days > 0 and salary_setting and salary_setting["payment_type"] == "月給":
# 月給を営業日数で割って欠勤日数を掛ける(簡易計算) # 月給を営業日数で割って欠勤日数を掛ける(簡易計算)
daily_rate = base_salary / Decimal("22") # 月平均営業日数を22日と仮定 daily_rate = base_salary / Decimal("22") # 月平均営業日数を22日と仮定
absence_deduction = daily_rate * absent_days absence_deduction = daily_rate * absent_days
@@ -156,11 +159,11 @@ class PayrollCalculationService:
# 残業手当の計算(時給 × 1.25 # 残業手当の計算(時給 × 1.25
overtime_pay = Decimal("0") overtime_pay = Decimal("0")
if overtime_hours > 0: if overtime_hours > 0:
if salary_setting["hourly_rate"]: if salary_setting and salary_setting["hourly_rate"]:
hourly_rate = Decimal(str(salary_setting["hourly_rate"])) hourly_rate = Decimal(str(salary_setting["hourly_rate"]))
else: else:
# 月給を時給に換算月平均労働時間を160時間と仮定 # 月給を時給に換算月平均労働時間を160時間と仮定
hourly_rate = Decimal(str(salary_setting["base_salary"])) / Decimal("160") hourly_rate = (Decimal(str(salary_setting["base_salary"])) if salary_setting else Decimal("0")) / Decimal("160")
overtime_pay = hourly_rate * overtime_hours * Decimal("1.25") overtime_pay = hourly_rate * overtime_hours * Decimal("1.25")
# 休日手当の計算(時給 × 1.35 # 休日手当の計算(時給 × 1.35
@@ -181,8 +184,29 @@ class PayrollCalculationService:
commute_allowance + other_allowance commute_allowance + other_allowance
) )
# 社会保険料の計算(標準報酬月額を総支給額と仮定) # 標準報酬月額表から標準報酬月額を検索
standard_monthly_salary = total_payment standard_monthly_salary = total_payment
with get_connection() as conn:
with conn.cursor() as cur:
# 給与年月の標準報酬月額表から、総支給額が属する等級を検索
cur.execute(
"""
SELECT monthly_amount
FROM standard_remuneration
WHERE rate_year = %s
AND salary_from <= %s
AND (salary_to IS NULL OR salary_to > %s)
ORDER BY rate_year DESC, salary_from DESC
LIMIT 1
""",
(payment_date.year, total_payment, total_payment)
)
result = cur.fetchone()
if result:
standard_monthly_salary = Decimal(str(result["monthly_amount"]))
print(f"[DEBUG] 標準報酬月額表検索: 総支給額={total_payment} → 標準報酬月額={standard_monthly_salary}")
else:
print(f"[DEBUG] 標準報酬月額表検索: 該当等級なし(総支給額={total_payment}")
# 社会保険料上限を取得 # 社会保険料上限を取得
health_insurance_limit = PayrollCalculationService.get_insurance_limit( health_insurance_limit = PayrollCalculationService.get_insurance_limit(
@@ -218,22 +242,27 @@ class PayrollCalculationService:
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day): if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
age -= 1 age -= 1
# 健康保険(上限適用後の標準報酬月額で計算) # 健康保険(標準報酬月額表から検索した月額で計算)
health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date) health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date)
health_insurance = Decimal("0") health_insurance = Decimal("0")
if health_insurance_rate: if health_insurance_rate:
health_insurance = ( health_insurance = (
health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"])) health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"]))
).quantize(Decimal("1")) ).quantize(Decimal("0.01"))
else:
errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year}")
# 介護保険40歳以上65歳未満が対象、健康保険と同じ標準報酬月額) # 介護保険40歳以上65歳未満が対象、標準報酬月額表から検索した月額で計算
care_insurance = Decimal("0") care_insurance = Decimal("0")
if 40 <= age < 65: if 40 <= age < 65:
care_insurance_rate = PayrollCalculationService.get_insurance_rate("介護保険", payment_date) care_insurance_rate = PayrollCalculationService.get_insurance_rate("介護保険", payment_date)
if care_insurance_rate: if care_insurance_rate:
care_insurance = ( care_insurance = (
health_standard_salary * Decimal(str(care_insurance_rate["employee_rate"])) health_standard_salary * Decimal(str(care_insurance_rate["employee_rate"]))
).quantize(Decimal("1")) ).quantize(Decimal("0.01"))
print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}")
else:
errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year}")
# 厚生年金(上限適用後の標準報酬月額で計算) # 厚生年金(上限適用後の標準報酬月額で計算)
pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date) pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date)
@@ -241,15 +270,30 @@ class PayrollCalculationService:
if pension_insurance_rate: if pension_insurance_rate:
pension_insurance = ( pension_insurance = (
pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"])) pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"]))
).quantize(Decimal("1")) ).quantize(Decimal("0.01"))
print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}")
else:
errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year}")
# 雇用保険 # 雇用保険
employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date) employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date)
employment_insurance = Decimal("0") employment_insurance = Decimal("0")
if employment_insurance_rate:
# 雇用保険加入対象かどうか確認
is_employment_insurance_eligible = salary_setting.get("employment_insurance_eligible", True) if salary_setting else True
if is_employment_insurance_eligible and employment_insurance_rate:
employment_insurance = ( employment_insurance = (
total_payment * Decimal(str(employment_insurance_rate["employee_rate"])) total_payment * Decimal(str(employment_insurance_rate["employee_rate"]))
).quantize(Decimal("1")) ).quantize(Decimal("0.01"))
elif not is_employment_insurance_eligible:
print(f"[DEBUG] この従業員は雇用保険非対象です")
elif not employment_insurance_rate:
errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year}")
# エラーがある場合は、エラーメッセージを返す
if errors:
raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors))
# 所得税の計算 # 所得税の計算
dependents_count = PayrollCalculationService.get_active_dependents_count(employee_id, payment_date) dependents_count = PayrollCalculationService.get_active_dependents_count(employee_id, payment_date)

View File

@@ -12,6 +12,7 @@ class DependentBase(BaseModel):
name: str name: str
relationship: str # 配偶者/子/親/その他 relationship: str # 配偶者/子/親/その他
birth_date: Optional[date] = None birth_date: Optional[date] = None
mynumber: Optional[str] = None # マイナンバーカード番号
is_spouse: bool = False is_spouse: bool = False
is_disabled: bool = False is_disabled: bool = False
income_amount: Decimal = Decimal("0") income_amount: Decimal = Decimal("0")
@@ -29,6 +30,7 @@ class DependentUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
relationship: Optional[str] = None relationship: Optional[str] = None
birth_date: Optional[date] = None birth_date: Optional[date] = None
mynumber: Optional[str] = None
is_spouse: Optional[bool] = None is_spouse: Optional[bool] = None
is_disabled: Optional[bool] = None is_disabled: Optional[bool] = None
income_amount: Optional[Decimal] = None income_amount: Optional[Decimal] = None

View File

@@ -31,22 +31,52 @@ def create_salary_setting(setting: schemas.SalarySettingCreate):
if not cur.fetchone(): if not cur.fetchone():
raise HTTPException(status_code=404, detail="従業員が見つかりません") raise HTTPException(status_code=404, detail="従業員が見つかりません")
# employment_insurance_eligible カラムが存在するか確認
cur.execute( cur.execute(
""" "SELECT 1 FROM information_schema.columns WHERE table_name='salary_settings' AND column_name='employment_insurance_eligible'"
INSERT INTO salary_settings (
employee_id, base_salary, hourly_rate, employment_type,
payment_type, commute_allowance, other_allowance,
valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.employment_type, setting.payment_type,
setting.commute_allowance, setting.other_allowance,
setting.valid_from, setting.valid_to
),
) )
has_employment_insurance_column = cur.fetchone()
if has_employment_insurance_column:
# カラムが存在する場合
cur.execute(
"""
INSERT INTO salary_settings (
employee_id, base_salary, hourly_rate, employment_type,
payment_type, commute_allowance, other_allowance,
employment_insurance_eligible,
valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.employment_type, setting.payment_type,
setting.commute_allowance, setting.other_allowance,
setting.employment_insurance_eligible,
setting.valid_from, setting.valid_to
),
)
else:
# カラムが存在しない場合(互換性維持)
cur.execute(
"""
INSERT INTO salary_settings (
employee_id, base_salary, hourly_rate, employment_type,
payment_type, commute_allowance, other_allowance,
valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.employment_type, setting.payment_type,
setting.commute_allowance, setting.other_allowance,
setting.valid_from, setting.valid_to
),
)
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
result = cur.fetchone() result = cur.fetchone()
conn.commit() conn.commit()
return result return result
@@ -100,6 +130,25 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate)
if not update_data: if not update_data:
raise HTTPException(status_code=400, detail="更新するデータがありません") raise HTTPException(status_code=400, detail="更新するデータがありません")
# employment_insurance_eligible はまだデータベースに存在しない可能性があるため、除外
if 'employment_insurance_eligible' in update_data:
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT 1 FROM information_schema.columns WHERE table_name='salary_settings' AND column_name='employment_insurance_eligible'"
)
column_exists = cur.fetchone()
if not column_exists:
update_data.pop('employment_insurance_eligible')
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
except Exception as e:
print(f"[DEBUG] カラム確認エラー: {e}")
update_data.pop('employment_insurance_eligible', None)
if not update_data:
raise HTTPException(status_code=400, detail="更新するデータがありません")
set_clause = ", ".join([f"{key} = %s" for key in update_data.keys()]) set_clause = ", ".join([f"{key} = %s" for key in update_data.keys()])
values = list(update_data.values()) + [setting_id] values = list(update_data.values()) + [setting_id]
@@ -385,7 +434,9 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None):
income_from = None income_from = None
income_to = None income_to = None
for col_idx in range(min(3, sheet.ncols)): # B列col 1とC列col 2から月収範囲を読み込む
# A列は順序番号なので、B列から開始
for col_idx in range(1, min(3, sheet.ncols)):
cell_value = sheet.cell_value(row_idx, col_idx) cell_value = sheet.cell_value(row_idx, col_idx)
if cell_value: if cell_value:
val = str(cell_value).replace(',', '').replace('', '').strip() val = str(cell_value).replace(',', '').replace('', '').strip()
@@ -526,7 +577,9 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None):
income_from = None income_from = None
income_to = None income_to = None
for i in range(min(3, len(row))): # B列col 1とC列col 2から月収範囲を読み込む
# A列は順序番号なので、B列から開始
for i in range(1, min(3, len(row))):
if row[i].value: if row[i].value:
val = str(row[i].value).replace(',', '').replace('', '').strip() val = str(row[i].value).replace(',', '').replace('', '').strip()
try: try:
@@ -715,6 +768,28 @@ def calculate_income_tax(monthly_income: Decimal, dependents_count: int = 0, tar
} }
@router.delete("/income-tax/year/{year}")
def delete_income_tax_by_year(year: int):
"""指定された年度の所得税率表データをすべて削除"""
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM income_tax_table WHERE tax_year = %s",
(year,)
)
deleted_count = cur.rowcount
conn.commit()
return {
"message": f"{year}年度のデータを削除しました",
"year": year,
"deleted_count": deleted_count
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ======================================== # ========================================
# 社会保険料上限設定管理 # 社会保険料上限設定管理
# ======================================== # ========================================

View File

@@ -16,6 +16,7 @@ class SalarySettingBase(BaseModel):
payment_type: str # 月給/時給/日給 payment_type: str # 月給/時給/日給
commute_allowance: Decimal = Decimal("0") commute_allowance: Decimal = Decimal("0")
other_allowance: Decimal = Decimal("0") other_allowance: Decimal = Decimal("0")
employment_insurance_eligible: bool = True # 雇用保険加入対象
valid_from: date valid_from: date
valid_to: Optional[date] = None valid_to: Optional[date] = None
@@ -33,6 +34,7 @@ class SalarySettingUpdate(BaseModel):
payment_type: Optional[str] = None payment_type: Optional[str] = None
commute_allowance: Optional[Decimal] = None commute_allowance: Optional[Decimal] = None
other_allowance: Optional[Decimal] = None other_allowance: Optional[Decimal] = None
employment_insurance_eligible: Optional[bool] = None
valid_from: Optional[date] = None valid_from: Optional[date] = None
valid_to: Optional[date] = None valid_to: Optional[date] = None

View File

@@ -127,14 +127,61 @@ async def import_standard_remuneration(
detail=f"A1セルから年度を抽出できません: {a1_value}" detail=f"A1セルから年度を抽出できません: {a1_value}"
) )
# 第9行からメタデータ保険料率を抽出
metadata = {}
try:
row9 = list(ws.iter_rows(min_row=9, max_row=9, values_only=True))[0]
# F9, H9, J9 (インデックス 5, 7, 9) から値を取得
health_no_care_rate = row9[5] if len(row9) > 5 else None # F9
health_with_care_rate = row9[7] if len(row9) > 7 else None # H9
pension_rate = row9[9] if len(row9) > 9 else None # J9
print(f"[メタデータ] 第9行: F9={health_no_care_rate}, H9={health_with_care_rate}, J9={pension_rate}")
# パーセンテージをクリーニング(文字列から%を削除)
def clean_percentage(val):
if val is None:
return None
val_str = str(val).strip()
val_str = val_str.replace('%', '').replace('', '')
try:
return float(val_str)
except:
return None
metadata = {
'health_no_care_rate': clean_percentage(health_no_care_rate),
'health_with_care_rate': clean_percentage(health_with_care_rate),
'pension_rate': clean_percentage(pension_rate),
'source': str(a1_value) if a1_value else None # A1セルの内容を追加
}
print(f"[メタデータ クリーニング後] {metadata}")
except Exception as e:
print(f"[警告] メタデータ抽出エラー: {e}")
metadata = {}
# データを抽出12行目から開始 # データを抽出12行目から開始
data_rows = [] data_rows = []
errors = [] errors = []
rows_read = []
# まずヘッダー行を確認11行目
header_row = list(ws.iter_rows(min_row=11, max_row=11, values_only=True))[0]
# ヘッダー行の全列を表示
header_debug = f"ヘッダー(11行目): {list(header_row)}"
errors.append(header_debug)
# min_row=12で12行目から読み取りを開始 # min_row=12で12行目から読み取りを開始
# enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算 # enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算
for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)): for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)):
row_idx = 12 + idx # 実際のExcel行番号 row_idx = 12 + idx # 実際のExcel行番号
# 最初の1行だけ全列を記録デバッグ用
if idx == 0:
row_debug = f"Row {row_idx} (最初のデータ行): {list(row)}"
else:
row_debug = f"Row {row_idx}: {row[:10]}"
rows_read.append(row_debug)
# 等級が空の場合は終了 # 等級が空の場合は終了
if not row[0]: if not row[0]:
break break
@@ -144,34 +191,24 @@ async def import_standard_remuneration(
# 各値を取得し、Noneや空白を処理 # 各値を取得し、Noneや空白を処理
def clean_value(val, field_name=""): def clean_value(val, field_name=""):
if val is None or val == '': # 允许 '-', '', '―', 'ー', '', '' 视为 0
return None if val is None:
return 0
# 数値型の場合はそのまま返す
if isinstance(val, (int, float)): if isinstance(val, (int, float)):
return val return val
# 文字列の場合
if isinstance(val, str): if isinstance(val, str):
# 空白を除去
val = val.strip() val = val.strip()
# 空、ハイフン、全角ダッシュなどをNoneに
if val in ('', '-', '', '', '', ''): if val in ('', '-', '', '', '', ''):
return None return 0
# 「円」「未満」「以上」などの文字を除去
val = val.replace('', '').replace('未満', '').replace('以上', '') val = val.replace('', '').replace('未満', '').replace('以上', '')
# カンマを除去
val = val.replace(',', '').replace('', '') val = val.replace(',', '').replace('', '')
# 全角数字を半角に変換
val = val.translate(str.maketrans('', '0123456789')) val = val.translate(str.maketrans('', '0123456789'))
# 再度空チェック
if val == '': if val == '':
return None return 0
return val return val
return val return val
# デバッグ用に元のデータを保存最初の7列を確認 # デバッグ用に元のデータとclean_value结果を保存
raw_data = { raw_data = {
'col0_grade': row[0] if len(row) > 0 else None, 'col0_grade': row[0] if len(row) > 0 else None,
'col1': row[1] if len(row) > 1 else None, 'col1': row[1] if len(row) > 1 else None,
@@ -182,6 +219,18 @@ async def import_standard_remuneration(
'col6': row[6] if len(row) > 6 else None, 'col6': row[6] if len(row) > 6 else None,
'col7': row[7] if len(row) > 7 else None, 'col7': row[7] if len(row) > 7 else None,
} }
# 记录清洗后的值
cleaned_data = {
'grade': clean_value(row[0]) if len(row) > 0 else None,
'monthly_amount': clean_value(row[1]) if len(row) > 1 else None,
'salary_from': clean_value(row[2]) if len(row) > 2 else None,
'salary_to': clean_value(row[3]) if len(row) > 3 else None,
'health_no_care': clean_value(row[4]) if len(row) > 4 else None,
'health_with_care': clean_value(row[5]) if len(row) > 5 else None,
'pension': clean_value(row[6]) if len(row) > 6 else None,
}
# 追加调试信息
errors.append(f"{row_idx}: 原始数据={raw_data}, 清洗後={cleaned_data}")
# 報酬月額範囲の処理 # 報酬月額範囲の処理
# 複数のパターンに対応: # 複数のパターンに対応:
@@ -191,45 +240,51 @@ async def import_standard_remuneration(
monthly_amount = clean_value(row[1], 'monthly_amount') monthly_amount = clean_value(row[1], 'monthly_amount')
# 列3が「」かチェック # 列2と列3の内容をチェック
col2_raw = str(row[2]) if len(row) > 2 and row[2] is not None else ''
col3_raw = str(row[3]) if len(row) > 3 and row[3] is not None else '' col3_raw = str(row[3]) if len(row) > 3 and row[3] is not None else ''
col2_cleaned = col2_raw.strip()
col3_cleaned = col3_raw.strip() col3_cleaned = col3_raw.strip()
# 列2に範囲が含まれているかチェック # パターン判定:
col2_raw = str(row[2]) if len(row) > 2 and row[2] is not None else '' # パターンA: col2 = '', col3 = 数値 → [grade, monthly, ~, range_to, health_no_full, health_no_half, health_with_full, health_with_half, ...]
# パターンB: col2 = 数値, col3 = '' → [grade, monthly, range_from, ~, range_to, health_no_full, health_no_half, health_with_full, health_with_half, ...]
# パターンC: col2 = 数値, col3 = 数値 → [grade, monthly, range_from, range_to, health_no_full, health_no_half, health_with_full, health_with_half, ...]
if col3_cleaned in ('', '', '-'): if col2_cleaned in ('', '', '-'):
# パターンA: 列2=「~」, 列3=上限値
salary_from = 0 # または前の行から取得
salary_to = clean_value(row[3], 'salary_to')
# 保険料は列4以降
health_no_care = clean_value(row[4] if len(row) > 4 else None, 'health_no_care')
health_with_care = clean_value(row[6] if len(row) > 6 else None, 'health_with_care')
# デバッグ: row全体の長さとrow[8]の値を記録
print(f"[デバッグ パターンA] 行{row_idx}: len(row)={len(row)}, row全体={list(row)}")
print(f" row[8]={row[8] if len(row) > 8 else 'N/A (行が短い)'}")
pension = clean_value(row[8] if len(row) > 8 else None, 'pension')
if pension == 0:
print(f" ⚠️ 警告: ペンション値が0です。row[8]の元データ = {row[8] if len(row) > 8 else 'N/A'}")
print(f" clean_value後: pension={pension}, type={type(pension)}")
elif col3_cleaned in ('', '', '-'):
# パターンB: 列2=下限, 列3=「~」, 列4=上限 # パターンB: 列2=下限, 列3=「~」, 列4=上限
salary_from = clean_value(row[2], 'salary_from') salary_from = clean_value(row[2], 'salary_from')
salary_to = clean_value(row[4] if len(row) > 4 else None, 'salary_to') salary_to = clean_value(row[4] if len(row) > 4 else None, 'salary_to')
# 保険料は列5以降 # 保険料は列5以降
health_no_care = clean_value(row[5] if len(row) > 5 else None, 'health_no_care') 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') health_with_care = clean_value(row[7] if len(row) > 7 else None, 'health_with_care')
pension = clean_value(row[7] if len(row) > 7 else None, 'pension') pension = clean_value(row[9] if len(row) > 9 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: else:
# パターンC: 列2=下限、列3=上限の通常パターン # パターンC: 列2=下限、列3=上限の数値パターン
salary_from = clean_value(row[2], 'salary_from') salary_from = clean_value(row[2], 'salary_from')
salary_to = clean_value(row[3] if len(row) > 3 else None, 'salary_to') salary_to = clean_value(row[3], 'salary_to')
# 保険料は列4以降
health_no_care = clean_value(row[4] if len(row) > 4 else None, 'health_no_care') 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') health_with_care = clean_value(row[6] if len(row) > 6 else None, 'health_with_care')
pension = clean_value(row[6] if len(row) > 6 else None, 'pension') pension = clean_value(row[8] if len(row) > 8 else None, 'pension')
# 必須フィールドのチェック(詳細なエラーメッセージ) # 必須フィールドのチェック(詳細なエラーメッセージ)
missing_fields = [] missing_fields = []
@@ -298,6 +353,8 @@ async def import_standard_remuneration(
error_msg = "有効なデータが見つかりませんでした" error_msg = "有効なデータが見つかりませんでした"
if errors: if errors:
error_msg += "\n\nエラー詳細:\n" + "\n".join(errors[:15]) error_msg += "\n\nエラー詳細:\n" + "\n".join(errors[:15])
if rows_read:
error_msg += "\n\n読み込んだ行:\n" + "\n".join(rows_read[:10])
raise HTTPException(status_code=400, detail=error_msg) raise HTTPException(status_code=400, detail=error_msg)
# データベースに保存 # データベースに保存
@@ -326,13 +383,25 @@ async def import_standard_remuneration(
"year": rate_year, "year": rate_year,
"prefecture": prefecture, "prefecture": prefecture,
"imported_count": imported_count, "imported_count": imported_count,
"source": a1_value "source": a1_value,
"metadata": metadata,
"debug_info": {
"total_rows_read": len(rows_read),
"first_rows_sample": rows_read[:3],
"a1_value_debug": f"A1セル内容: {a1_value}"
}
} }
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail="年度は数値で入力してください") raise HTTPException(status_code=400, detail=f"年度は数値で入力してください: {str(e)}")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) error_detail = {
"error": str(e),
"error_type": type(e).__name__,
"rows_read_count": len(rows_read) if 'rows_read' in locals() else 0,
"first_rows_sample": rows_read[:3] if 'rows_read' in locals() else []
}
raise HTTPException(status_code=500, detail=error_detail)
@router.delete("") @router.delete("")
@@ -356,3 +425,43 @@ async def delete_standard_remuneration(year: int, prefecture: str):
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.delete("/all")
async def delete_all_standard_remuneration():
"""すべての標準報酬月額表データを削除"""
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM standard_remuneration")
deleted_count = cur.rowcount
conn.commit()
return {
"message": "すべてのデータを削除しました",
"deleted_count": deleted_count
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/year/{year}")
async def delete_standard_remuneration_by_year(year: int):
"""指定された年度の標準報酬月額表データをすべて削除"""
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM standard_remuneration WHERE rate_year = %s",
(year,)
)
deleted_count = cur.rowcount
conn.commit()
return {
"message": f"{year}年度のデータを削除しました",
"year": year,
"deleted_count": deleted_count
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,5 @@
-- 雇用保険加入対象フラグを salary_settings テーブルに追加
ALTER TABLE salary_settings
ADD COLUMN IF NOT EXISTS employment_insurance_eligible BOOLEAN DEFAULT TRUE;
COMMENT ON COLUMN salary_settings.employment_insurance_eligible IS '雇用保険加入対象: true=対象、false=非対象';

View File

@@ -0,0 +1,4 @@
-- マイナンバーカード番号カラムを扶養家族テーブルに追加
ALTER TABLE dependents ADD COLUMN IF NOT EXISTS mynumber VARCHAR(12);
COMMENT ON COLUMN dependents.mynumber IS 'マイナンバーカード番号12桁';

View File

@@ -68,6 +68,7 @@ CREATE TABLE IF NOT EXISTS salary_settings (
payment_type VARCHAR(20) NOT NULL, -- 月給/時給/日給 payment_type VARCHAR(20) NOT NULL, -- 月給/時給/日給
commute_allowance DECIMAL(12, 2) DEFAULT 0, commute_allowance DECIMAL(12, 2) DEFAULT 0,
other_allowance DECIMAL(12, 2) DEFAULT 0, other_allowance DECIMAL(12, 2) DEFAULT 0,
employment_insurance_eligible BOOLEAN DEFAULT TRUE, -- 雇用保険加入対象かどうか
valid_from DATE NOT NULL, valid_from DATE NOT NULL,
valid_to DATE, valid_to DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -78,6 +79,7 @@ CREATE TABLE IF NOT EXISTS salary_settings (
COMMENT ON TABLE salary_settings IS '給与基本設定'; COMMENT ON TABLE salary_settings IS '給与基本設定';
COMMENT ON COLUMN salary_settings.employment_type IS '雇用形態: 正社員/契約社員/パート/アルバイト'; COMMENT ON COLUMN salary_settings.employment_type IS '雇用形態: 正社員/契約社員/パート/アルバイト';
COMMENT ON COLUMN salary_settings.payment_type IS '支給形態: 月給/時給/日給'; COMMENT ON COLUMN salary_settings.payment_type IS '支給形態: 月給/時給/日給';
COMMENT ON COLUMN salary_settings.employment_insurance_eligible IS '雇用保険加入対象: true=対象、false=非対象';
-- ======================================== -- ========================================
-- 4. 社会保険料率マスタ (Insurance Rates) -- 4. 社会保険料率マスタ (Insurance Rates)

File diff suppressed because it is too large Load Diff

View File

@@ -916,7 +916,7 @@
employee.employee_id employee.employee_id
})" style="padding: 5px 10px; font-size: 12px;">扶養家族追加</button></h3> })" style="padding: 5px 10px; font-size: 12px;">扶養家族追加</button></h3>
${(() => { ${(() => {
// 源泉税控除対象人数を計算19歳以上または70歳以上) // 源泉税控除対象人数を計算16歳以上)
let koureiCount = 0; let koureiCount = 0;
if ( if (
employee.dependents && employee.dependents &&
@@ -926,10 +926,11 @@
const taxStatus = getTaxDeductionStatus( const taxStatus = getTaxDeductionStatus(
dep.birth_date dep.birth_date
); );
// 源泉税控除対象は19歳以上一般扶養親族16-18歳は住民税のみなので除外 // 源泉税控除対象は16歳以上一般扶養親族16-18歳、特定扶養親族19-22歳、老人扶養親族70歳以上
return ( return (
taxStatus?.isDeductible && taxStatus?.isDeductible &&
(taxStatus.type === "控除対象扶養親族" || (taxStatus.type === "一般扶養親族" ||
taxStatus.type === "控除対象扶養親族" ||
taxStatus.type === "老人扶養親族") taxStatus.type === "老人扶養親族")
); );
}).length; }).length;
@@ -939,7 +940,7 @@
<div style="background: #f0f8ff; padding: 10px; border-radius: 5px; margin-bottom: 15px;"> <div style="background: #f0f8ff; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<strong style="color: #0066cc;">甲欄扶養親族人数源泉税控除対象: ${koureiCount}</strong> <strong style="color: #0066cc;">甲欄扶養親族人数源泉税控除対象: ${koureiCount}</strong>
<small style="display: block; margin-top: 5px; color: #666;"> <small style="display: block; margin-top: 5px; color: #666;">
19歳以上の扶養親族が対象16-18は住民税のみ対象のため含まれません 16歳以上の扶養親族が対象一般扶養親族16-18特定扶養親族19-22老人扶養親族70歳以上
</small> </small>
</div> </div>
`; `;
@@ -962,7 +963,7 @@
: "" : ""
} }
${ ${
taxStatus?.isDeductible taxStatus?.label
? ` <span style="color:${taxStatus.color}; font-weight:bold;">[${taxStatus.label}]</span>` ? ` <span style="color:${taxStatus.color}; font-weight:bold;">[${taxStatus.label}]</span>`
: "" : ""
} }
@@ -1004,7 +1005,8 @@
); );
return ( return (
taxStatus?.isDeductible && taxStatus?.isDeductible &&
(taxStatus.type === "控除対象扶養親族" || (taxStatus.type === "一般扶養親族" ||
taxStatus.type === "控除対象扶養親族" ||
taxStatus.type === "老人扶養親族") taxStatus.type === "老人扶養親族")
); );
}).length; }).length;
@@ -1015,8 +1017,8 @@
<strong>📋 源泉税控除の説明:</strong> <strong>📋 源泉税控除の説明:</strong>
<ul style="margin: 10px 0 0 20px; font-size: 13px; line-height: 1.6;"> <ul style="margin: 10px 0 0 20px; font-size: 13px; line-height: 1.6;">
<li><strong style="color: purple;">老人扶養親族(70歳以上)</strong>: </li> <li><strong style="color: purple;">老人扶養親族(70歳以上)</strong>: </li>
<li><strong style="color: green;">控除対象扶養親族(19歳以上)</strong>: </li> <li><strong style="color: green;">控除対象扶養親族(19歳以上23歳未満)</strong>: </li>
<li><strong style="color: orange;">一般扶養親族(16-18)</strong>: ()</li> <li><strong style="color: orange;">一般扶養親族(16以上19歳未満)</strong>: </li>
<li><strong style="color: #999;">16歳未満</strong>: </li> <li><strong style="color: #999;">16歳未満</strong>: </li>
</ul> </ul>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;"> <p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
@@ -1253,6 +1255,11 @@
<label>生年月日</label> <label>生年月日</label>
<input type="date" name="birth_date" min="1900-01-01" max="2099-12-31" /> <input type="date" name="birth_date" min="1900-01-01" max="2099-12-31" />
</div> </div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input type="text" name="mynumber" placeholder="例123456789012" maxlength="12" />
<small>12桁の数字任意入力</small>
</div>
<div class="form-group"> <div class="form-group">
<label> <label>
<input type="checkbox" name="is_spouse" value="true" /> <input type="checkbox" name="is_spouse" value="true" />
@@ -1304,6 +1311,7 @@
name: formData.get("name"), name: formData.get("name"),
relationship: formData.get("relationship"), relationship: formData.get("relationship"),
birth_date: formData.get("birth_date") || null, birth_date: formData.get("birth_date") || null,
mynumber: formData.get("mynumber") || null,
is_spouse: formData.get("is_spouse") === "true", is_spouse: formData.get("is_spouse") === "true",
is_disabled: formData.get("is_disabled") === "true", is_disabled: formData.get("is_disabled") === "true",
income_amount: parseFloat(formData.get("income_amount")) || 0, income_amount: parseFloat(formData.get("income_amount")) || 0,
@@ -1335,19 +1343,16 @@
} }
} }
// その年の12月31日時点の年齢を計算 // 現在時点の年齢を計算
function calculateAgeAtYearEnd( function calculateAgeAtYearEnd(birthDate) {
birthDate,
targetYear = new Date().getFullYear()
) {
if (!birthDate) return null; if (!birthDate) return null;
const today = new Date();
const birth = new Date(birthDate); const birth = new Date(birthDate);
const yearEnd = new Date(targetYear, 11, 31); // 12月31日 let age = today.getFullYear() - birth.getFullYear();
let age = yearEnd.getFullYear() - birth.getFullYear(); const monthDiff = today.getMonth() - birth.getMonth();
const monthDiff = yearEnd.getMonth() - birth.getMonth();
if ( if (
monthDiff < 0 || monthDiff < 0 ||
(monthDiff === 0 && yearEnd.getDate() < birth.getDate()) (monthDiff === 0 && today.getDate() < birth.getDate())
) { ) {
age--; age--;
} }
@@ -1379,7 +1384,7 @@
return { return {
isDeductible: true, isDeductible: true,
type: "一般扶養親族", type: "一般扶養親族",
label: "一般扶養親族(住民税のみ)", label: "一般扶養親族(源泉税・住民税)",
color: "orange", color: "orange",
age, age,
}; };
@@ -1387,7 +1392,7 @@
return { return {
isDeductible: false, isDeductible: false,
type: null, type: null,
label: null, label: "源泉税控除対象外",
color: null, color: null,
age, age,
}; };
@@ -1458,6 +1463,13 @@
dependent.birth_date || "" dependent.birth_date || ""
}" min="1900-01-01" max="2099-12-31" /> }" min="1900-01-01" max="2099-12-31" />
</div> </div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input type="text" name="mynumber" value="${
dependent.mynumber || ""
}" placeholder="例123456789012" maxlength="12" />
<small>12桁の数字任意入力</small>
</div>
<div class="form-group"> <div class="form-group">
<label> <label>
<input type="checkbox" name="is_spouse" value="true" ${ <input type="checkbox" name="is_spouse" value="true" ${
@@ -1515,6 +1527,7 @@
name: formData.get("name"), name: formData.get("name"),
relationship: formData.get("relationship"), relationship: formData.get("relationship"),
birth_date: formData.get("birth_date") || null, birth_date: formData.get("birth_date") || null,
mynumber: formData.get("mynumber") || null,
is_spouse: formData.get("is_spouse") === "true", is_spouse: formData.get("is_spouse") === "true",
is_disabled: formData.get("is_disabled") === "true", is_disabled: formData.get("is_disabled") === "true",
income_amount: parseFloat(formData.get("income_amount")) || 0, income_amount: parseFloat(formData.get("income_amount")) || 0,

View File

@@ -159,6 +159,19 @@
value="0" value="0"
/> />
</div> </div>
<div class="form-group">
<label style="display: flex; align-items: center">
<input
type="checkbox"
name="employment_insurance_eligible"
id="employment_insurance_eligible"
checked
style="width: auto; margin-right: 8px"
/>
<span>雇用保険加入対象</span>
</label>
<small>チェックを入れると雇用保険に加入します</small>
</div>
<div class="form-group"> <div class="form-group">
<label>適用開始日 *</label> <label>適用開始日 *</label>
<input <input
@@ -267,6 +280,7 @@
<th>支給形態</th> <th>支給形態</th>
<th>通勤手当</th> <th>通勤手当</th>
<th>その他手当</th> <th>その他手当</th>
<th>雇用保険</th>
<th>適用期間</th> <th>適用期間</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
@@ -290,6 +304,11 @@
<td>${s.payment_type}</td> <td>${s.payment_type}</td>
<td>¥${Number(s.commute_allowance).toLocaleString()}</td> <td>¥${Number(s.commute_allowance).toLocaleString()}</td>
<td>¥${Number(s.other_allowance).toLocaleString()}</td> <td>¥${Number(s.other_allowance).toLocaleString()}</td>
<td>${
s.employment_insurance_eligible !== false
? "✓ 対象"
: "✗ 非対象"
}</td>
<td>${s.valid_from} ${s.valid_to || "現在"}</td> <td>${s.valid_from} ${s.valid_to || "現在"}</td>
<td> <td>
<button class="btn btn-primary" onclick="editSetting(${ <button class="btn btn-primary" onclick="editSetting(${
@@ -300,7 +319,7 @@
` `
) )
.join("") .join("")
: '<tr><td colspan="10" style="text-align:center;">給与設定が登録されていません</td></tr>' : '<tr><td colspan="11" style="text-align:center;">給与設定が登録されていません</td></tr>'
} }
</tbody> </tbody>
</table> </table>
@@ -339,6 +358,8 @@
setting.commute_allowance; setting.commute_allowance;
document.getElementById("other_allowance").value = document.getElementById("other_allowance").value =
setting.other_allowance; setting.other_allowance;
document.getElementById("employment_insurance_eligible").checked =
setting.employment_insurance_eligible !== false;
document.getElementById("valid_from").value = setting.valid_from; document.getElementById("valid_from").value = setting.valid_from;
document.getElementById("valid_to").value = document.getElementById("valid_to").value =
setting.valid_to || ""; setting.valid_to || "";
@@ -386,6 +407,9 @@
payment_type: formData.get("payment_type"), payment_type: formData.get("payment_type"),
commute_allowance: parseFloat(formData.get("commute_allowance")) || 0, commute_allowance: parseFloat(formData.get("commute_allowance")) || 0,
other_allowance: parseFloat(formData.get("other_allowance")) || 0, other_allowance: parseFloat(formData.get("other_allowance")) || 0,
employment_insurance_eligible: document.getElementById(
"employment_insurance_eligible"
).checked,
valid_from: formData.get("valid_from"), valid_from: formData.get("valid_from"),
}; };

View File

@@ -547,7 +547,7 @@
<!-- 所得税率表タブ --> <!-- 所得税率表タブ -->
<div id="tab-tax" class="tab-content"> <div id="tab-tax" class="tab-content">
<h2>所得税率表</h2> <h2>所得税率表(年度1月12月)</h2>
<button <button
class="btn btn-primary" class="btn btn-primary"
onclick="document.getElementById('csvFile').click()" onclick="document.getElementById('csvFile').click()"
@@ -567,13 +567,13 @@
<!-- 標準報酬月額表タブ --> <!-- 標準報酬月額表タブ -->
<div id="tab-standard-remuneration" class="tab-content"> <div id="tab-standard-remuneration" class="tab-content">
<h2>標準報酬月額表</h2> <h2>標準報酬月額表年度3月翌年2月</h2>
<div class="info-box"> <div class="info-box">
<strong>標準報酬月額表について:</strong> <strong>標準報酬月額表について:</strong>
<ul style="margin: 10px 0 0 20px"> <ul style="margin: 10px 0 0 20px">
<li>健康保険・厚生年金保険の保険料額表をインポートします</li> <li>健康保険・厚生年金保険の保険料額表をインポートします</li>
<li>都道府県ごとの料率表に対応しています</li> <li>都道府県ごとの料率表に対応しています</li>
<li>年度と都道府県を指定してください</li> <li>年度をクリックして展開してください</li>
</ul> </ul>
</div> </div>
@@ -594,7 +594,6 @@
</div> </div>
<div id="stdRemunerationYearsList" style="margin-top: 20px"></div> <div id="stdRemunerationYearsList" style="margin-top: 20px"></div>
<div id="stdRemunerationDataContainer" style="margin-top: 20px"></div>
</div> </div>
</div> </div>
@@ -635,7 +634,399 @@
} else if (tabName === "tax") { } else if (tabName === "tax") {
loadIncomeTax(); loadIncomeTax();
} else if (tabName === "standard-remuneration") { } else if (tabName === "standard-remuneration") {
loadStdRemunerationData(); loadStdRemunerationYears();
}
}
// =====================================================
// 標準報酬月額表関連
// =====================================================
async function loadStdRemunerationYears() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/years`
);
const years = await response.json();
if (!years || years.length === 0) {
document.getElementById("stdRemunerationYearsList").innerHTML =
"<p>データがありません。インポートしてください。</p>";
return;
}
const html = years
.sort((a, b) => b - a)
.map(
(year) => `
<div class="year-item" id="std-year-${year}">
<div class="year-header" style="display: flex; justify-content: space-between; align-items: center;">
<div style="cursor: pointer; flex: 1;" onclick="toggleStdRemunerationYear(${year})">
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
</div>
<button class="btn btn-danger btn-small" onclick="deleteStdRemunerationYear(${year})" style="margin-left: 10px;">
削除
</button>
</div>
<div class="year-data" id="std-data-${year}">
<div id="std-table-${year}">読み込み中...</div>
</div>
</div>
`
)
.join("");
document.getElementById("stdRemunerationYearsList").innerHTML = html;
} catch (error) {
console.error("年度リストの取得に失敗:", error);
document.getElementById("stdRemunerationYearsList").innerHTML =
'<p style="color:red;">年度リストの取得に失敗しました</p>';
}
}
async function toggleStdRemunerationYear(year) {
const yearItem = document.getElementById(`std-year-${year}`);
const wasExpanded = yearItem.classList.contains("expanded");
// 他の年度を閉じる
document
.querySelectorAll("#stdRemunerationYearsList .year-item")
.forEach((item) => {
item.classList.remove("expanded");
});
if (!wasExpanded) {
yearItem.classList.add("expanded");
currentExpandedStdYear = year;
await loadStdRemunerationYearData(year);
} else {
currentExpandedStdYear = null;
}
}
async function loadStdRemunerationYearData(year) {
const params = new URLSearchParams();
params.append("year", year);
const url = `${API_BASE}/payroll/settings/standard-remuneration?${params.toString()}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!Array.isArray(data) || data.length === 0) {
document.getElementById(`std-table-${year}`).innerHTML =
"<p>該当するデータがありません</p>";
return;
}
// セッションストレージからメタデータを取得
const metadata = JSON.parse(
sessionStorage.getItem("stdRemunMetadata") || "{}"
);
console.log("取得したメタデータ:", metadata);
// 都道府県ごとにグループ化
const prefectures = {};
data.forEach((row) => {
if (!prefectures[row.prefecture]) {
prefectures[row.prefecture] = [];
}
prefectures[row.prefecture].push(row);
});
// 都道府県ごとにテーブルを作成
let tablesHtml = "";
for (const [prefecture, prefData] of Object.entries(prefectures)) {
tablesHtml += `<h3 style="margin-top: 20px; margin-bottom: 10px; color: #333;">${prefecture}</h3>`;
tablesHtml += renderStdRemunerationTable(prefData, metadata);
}
document.getElementById(`std-table-${year}`).innerHTML = tablesHtml;
} catch (error) {
console.error("データ読み込みエラー:", error);
document.getElementById(
`std-table-${year}`
).innerHTML = `<p style="color:red;">データの取得に失敗しました: ${error.message}</p>`;
}
}
function renderStdRemunerationTable(data, metadata = {}) {
if (!data || data.length === 0) {
return "<p>データがありません。</p>";
}
// A1セルの内容を表示あれば
let sourceHtml = "";
console.log("renderStdRemunerationTable - metadata内容:", metadata);
console.log(
"renderStdRemunerationTable - metadata.source:",
metadata.source
);
if (metadata.source) {
sourceHtml = `<p style="margin-bottom: 15px; padding: 10px; background: #f0f8ff; border-left: 3px solid #0066cc; color: #333;"><strong>対象期間:</strong> ${metadata.source}</p>`;
} else {
console.warn("⚠️ metadata.sourceが空です");
}
// メタデータから保険料率を取得
const rates = {
healthNoCareRate: metadata.health_no_care_rate,
healthWithCareRate: metadata.health_with_care_rate,
pensionRate: metadata.pension_rate,
};
// メタデータはヘッダーに組み込むため、ここでは空にする
let metadataHtml = "";
// 等級でソート(最初の数字を抽出して数値順にソート)
const sortedData = data.sort((a, b) => {
const gradeA = String(a.grade).trim();
const gradeB = String(b.grade).trim();
// 括弧や記号を除いて、最初の数字を抽出
const numA = parseInt(gradeA.match(/\d+/)?.[0] || 0);
const numB = parseInt(gradeB.match(/\d+/)?.[0] || 0);
// 数値で比較
if (numA !== numB) {
return numA - numB;
}
// 同じ数値の場合は元の文字列で比較
return gradeA.localeCompare(gradeB, "ja");
});
// ペンション値の段階的な埋め充2段階
// ステップ1: 0の値に対して、下方を探して最初の非ゼロ値を使用向下查找
const step1Data = [...sortedData];
for (let i = 0; i < step1Data.length; i++) {
const pensionVal = parseFloat(step1Data[i].pension_insurance);
if (pensionVal === 0) {
// 下方を探して最初の非ゼロ値を見つける
let nextNonZero = 0;
for (let j = i + 1; j < step1Data.length; j++) {
const nextVal = parseFloat(step1Data[j].pension_insurance);
if (nextVal > 0) {
nextNonZero = nextVal;
break;
}
}
if (nextNonZero > 0) {
step1Data[i].pension_insurance = nextNonZero;
}
}
}
console.log("=== ステップ1完了下方探索===");
console.log(
"step1Data:",
step1Data.map((d) => ({
grade: d.grade,
pension: d.pension_insurance,
}))
);
// ステップ2: 非ゼロ値を下方へ延続前の有効値が0以下になったら、前の非ゼロ値を使用
const adjustedData = [...step1Data];
let lastValidPension = 0;
for (let i = 0; i < adjustedData.length; i++) {
const pensionVal = parseFloat(adjustedData[i].pension_insurance);
if (pensionVal > 0) {
// 非ゼロ値を見つけたら、それを記録
lastValidPension = pensionVal;
} else if (pensionVal === 0 && lastValidPension > 0) {
// 0だが前に有効な値がある場合、それを使用
adjustedData[i].pension_insurance = lastValidPension;
}
}
console.log("=== ステップ2完了下方延続===");
console.log(
"adjustedData:",
adjustedData.map((d) => ({
grade: d.grade,
pension: d.pension_insurance,
}))
);
console.log("調整後のデータ最初の3件:", adjustedData.slice(0, 3));
const thead = `
<thead>
<tr>
<th rowspan="3">等級</th>
<th rowspan="3">標準報酬月額</th>
<th rowspan="3">報酬月額 以上</th>
<th rowspan="3">報酬月額 未満</th>
<th colspan="2">健康保険料(介護保険なし)</th>
<th colspan="2">健康保険料(介護保険あり)</th>
<th colspan="2">厚生年金保険料</th>
</tr>
<tr style="background-color: #f0f0f0;">
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
${
rates.healthNoCareRate !== undefined
? rates.healthNoCareRate.toFixed(2) + "%"
: "-"
}
</th>
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
${
rates.healthWithCareRate !== undefined
? rates.healthWithCareRate.toFixed(2) + "%"
: "-"
}
</th>
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
${
rates.pensionRate !== undefined
? rates.pensionRate.toFixed(2) + "%"
: "-"
}
</th>
</tr>
<tr>
<th>全額</th>
<th>折半額</th>
<th>全額</th>
<th>折半額</th>
<th>全額</th>
<th>折半額</th>
</tr>
</thead>
`;
const tbody = `
<tbody>
${adjustedData
.map((row) => {
// 保険料を2で割って折半額を計算
const healthNoCareHalf = (
parseFloat(row.health_insurance_no_care) / 2
).toFixed(2);
const healthWithCareHalf = (
parseFloat(row.health_insurance_with_care) / 2
).toFixed(2);
const pensionHalf = (
parseFloat(row.pension_insurance) / 2
).toFixed(2);
return `
<tr>
<td>${row.grade ?? ""}</td>
<td>${Number(row.monthly_amount ?? 0).toLocaleString()}</td>
<td>${Number(row.salary_from ?? 0).toLocaleString()}</td>
<td>${Number(row.salary_to ?? 0).toLocaleString()}</td>
<td>${Number(
row.health_insurance_no_care ?? 0
).toLocaleString()}</td>
<td>${Number(healthNoCareHalf ?? 0).toLocaleString()}</td>
<td>${Number(
row.health_insurance_with_care ?? 0
).toLocaleString()}</td>
<td>${Number(healthWithCareHalf ?? 0).toLocaleString()}</td>
<td>${Number(
row.pension_insurance ?? 0
).toLocaleString()}</td>
<td>${Number(pensionHalf ?? 0).toLocaleString()}</td>
</tr>
`;
})
.join("")}
</tbody>
`;
return (
sourceHtml +
metadataHtml +
`<table style="width:100%; border-collapse: collapse; border: 1px solid #ccc;">${thead}${tbody}</table>`
);
}
async function loadStdRemunerationData() {
// 现在这个函数用于在导入后刷新年份列表
await loadStdRemunerationYears();
}
async function deleteStdRemunerationYear(year) {
const confirmed = confirm(
`⚠️ 警告\n\n${year}年度の標準報酬月額表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
);
if (!confirmed) {
console.log(`${year}年度の削除がキャンセルされました`);
return;
}
console.log(`=== ${year}年度データ削除開始 ===`);
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/year/${year}`,
{ method: "DELETE" }
);
console.log("削除レスポンスステータス:", response.status);
const result = await response.json();
console.log("削除結果:", result);
if (response.ok) {
alert(
`${year}年度のデータ(${result.deleted_count}件)を削除しました`
);
console.log(`=== ${year}年度データ削除完了 ===`);
// 年份リストをリロード
await loadStdRemunerationYears();
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
);
}
} catch (error) {
console.error("削除エラー:", error);
alert(`${year}年度のデータ削除に失敗しました:\n` + error.message);
}
}
async function clearAllStdRemunerationData() {
const confirmed = confirm(
"⚠️ 警告\n\n標準報酬月額表のすべてのデータを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか"
);
if (!confirmed) {
console.log("削除操作がキャンセルされました");
return;
}
console.log("=== 全データ削除開始 ===");
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/all`,
{ method: "DELETE" }
);
console.log("削除レスポンスステータス:", response.status);
const result = await response.json();
console.log("削除結果:", result);
if (response.ok) {
alert(
`${result.deleted_count}件のデータを削除しました\n\n再度Excelファイルをインポートしてください`
);
console.log("=== 全データ削除完了 ===");
// 年份列表をクリア
document.getElementById("stdRemunerationYearsList").innerHTML =
"<p>データが削除されました。Excelファイルを再度インポートしてください。</p>";
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
);
}
} catch (error) {
console.error("削除エラー:", error);
alert("全データ削除中にエラーが発生しました:\n" + error.message);
} }
} }
@@ -643,7 +1034,12 @@
// 保険料率関連 // 保険料率関連
// ===================================================== // =====================================================
function showInsuranceForm() { function showInsuranceForm() {
document.getElementById("insuranceForm").style.display = "block"; const el = document.getElementById("insuranceForm");
el.style.display = "block";
// 表示エリアへスクロールして最初の入力にフォーカス
el.scrollIntoView({ behavior: "smooth", block: "center" });
const firstInput = el.querySelector('[name="rate_year"]');
if (firstInput) firstInput.focus();
} }
function hideInsuranceForm() { function hideInsuranceForm() {
@@ -719,6 +1115,9 @@
}</td> }</td>
<td>${rate.notes || "-"}</td> <td>${rate.notes || "-"}</td>
<td> <td>
<button class="btn btn-primary btn-small" onclick="copyInsuranceRate(${
rate.rate_id
})">複製</button>
<button class="btn btn-primary btn-small" onclick="editInsuranceRate(${ <button class="btn btn-primary btn-small" onclick="editInsuranceRate(${
rate.rate_id rate.rate_id
})">編集</button> })">編集</button>
@@ -761,30 +1160,72 @@
document.getElementById("editRateId").value = rateId; document.getElementById("editRateId").value = rateId;
// フォームにデータを入力 // フォームにデータを入力
const form = document.getElementById("insuranceRateForm"); fillInsuranceForm(rate);
form.querySelector('[name="rate_year"]').value = rate.rate_year;
form.querySelector('[name="calculation_target"]').value =
rate.calculation_target;
form.querySelector('[name="rate_type"]').value = rate.rate_type;
form.querySelector('[name="prefecture"]').value = rate.prefecture;
form.querySelector('[name="employee_rate"]').value = (
rate.employee_rate * 100
).toFixed(3);
form.querySelector('[name="employer_rate"]').value = (
rate.employer_rate * 100
).toFixed(3);
form.querySelector('[name="care_insurance_age_threshold"]').value =
rate.care_insurance_age_threshold || "";
form.querySelector('[name="notes"]').value = rate.notes || "";
// フォームを表示 // フォームを表示
document.getElementById("insuranceForm").style.display = "block"; const el = document.getElementById("insuranceForm");
el.style.display = "block";
el.scrollIntoView({ behavior: "smooth", block: "center" });
const firstInput = el.querySelector('[name="rate_year"]');
if (firstInput) firstInput.focus();
} catch (error) { } catch (error) {
alert("データの取得に失敗しました"); alert("データの取得に失敗しました");
console.error(error); console.error(error);
} }
} }
async function copyInsuranceRate(rateId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates`
);
const rates = await response.json();
const rate = rates.find((r) => r.rate_id === rateId);
if (!rate) {
alert("データが見つかりません");
return;
}
// フォームを新規追加モードに切り替え
document.getElementById("insuranceFormTitle").innerText =
"保険料率の登録(複製)";
document.getElementById("editMode").value = "false";
document.getElementById("editRateId").value = "";
// フォームにデータを入力
fillInsuranceForm(rate);
// フォームを表示
const el = document.getElementById("insuranceForm");
el.style.display = "block";
el.scrollIntoView({ behavior: "smooth", block: "center" });
const firstInput = el.querySelector('[name="rate_year"]');
if (firstInput) firstInput.focus();
} catch (error) {
alert("データの取得に失敗しました");
console.error(error);
}
}
function fillInsuranceForm(rate) {
const form = document.getElementById("insuranceRateForm");
form.querySelector('[name="rate_year"]').value = rate.rate_year;
form.querySelector('[name="calculation_target"]').value =
rate.calculation_target;
form.querySelector('[name="rate_type"]').value = rate.rate_type;
form.querySelector('[name="prefecture"]').value = rate.prefecture;
form.querySelector('[name="employee_rate"]').value = (
rate.employee_rate * 100
).toFixed(3);
form.querySelector('[name="employer_rate"]').value = (
rate.employer_rate * 100
).toFixed(3);
form.querySelector('[name="care_insurance_age_threshold"]').value =
rate.care_insurance_age_threshold || "";
form.querySelector('[name="notes"]').value = rate.notes || "";
}
async function saveInsuranceRate(event) { async function saveInsuranceRate(event) {
event.preventDefault(); event.preventDefault();
const form = event.target; const form = event.target;
@@ -1275,6 +1716,7 @@
// 所得税率表関連 // 所得税率表関連
// ===================================================== // =====================================================
let currentExpandedYear = null; let currentExpandedYear = null;
let currentExpandedStdYear = null;
async function loadIncomeTax() { async function loadIncomeTax() {
await loadIncomeTaxYears(); await loadIncomeTaxYears();
@@ -1296,14 +1738,19 @@
const html = data.years const html = data.years
.map( .map(
(year) => ` (year) => `
<div class="year-item" id="year-${year}" onclick="toggleYear(${year})"> <div class="year-item" id="year-${year}">
<div class="year-header"> <div class="year-header" style="display: flex; justify-content: space-between; align-items: center;">
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span> <div style="cursor: pointer; flex: 1;" onclick="toggleYear(${year})">
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
</div>
<button class="btn btn-danger btn-small" onclick="deleteTaxDataYear(${year})" style="margin-left: 10px;">
削除
</button>
</div> </div>
<div class="year-data" id="data-${year}"> <div class="year-data" id="data-${year}">
<div class="filter-controls"> <div class="filter-controls">
<label>扶養人数でフィルター: </label> <label>扶養人数でフィルター: </label>
<select id="filter-${year}" onchange="loadYearData(${year})"> <select id="filter-${year}" onchange="loadYearData(${year})" onclick="event.stopPropagation()">
<option value="">全て</option> <option value="">全て</option>
<option value="0">0人</option> <option value="0">0人</option>
<option value="1">1人</option> <option value="1">1人</option>
@@ -1329,6 +1776,46 @@
} }
} }
async function deleteTaxDataYear(year) {
const confirmed = confirm(
`⚠️ 警告\n\n${year}年度の所得税率表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
);
if (!confirmed) {
console.log(`${year}年度の削除がキャンセルされました`);
return;
}
console.log(`=== ${year}年度所得税データ削除開始 ===`);
try {
const response = await fetch(
`${API_BASE}/payroll/settings/income-tax/year/${year}`,
{ method: "DELETE" }
);
console.log("削除レスポンスステータス:", response.status);
const result = await response.json();
console.log("削除結果:", result);
if (response.ok) {
alert(
`${year}年度のデータ(${result.deleted_count}件)を削除しました`
);
console.log(`=== ${year}年度所得税データ削除完了 ===`);
// 年份リストをリロード
await loadIncomeTaxYears();
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
);
}
} catch (error) {
console.error("削除エラー:", error);
alert(`${year}年度のデータ削除に失敗しました:\n` + error.message);
}
}
async function toggleYear(year) { async function toggleYear(year) {
const yearItem = document.getElementById(`year-${year}`); const yearItem = document.getElementById(`year-${year}`);
const wasExpanded = yearItem.classList.contains("expanded"); const wasExpanded = yearItem.classList.contains("expanded");
@@ -1445,181 +1932,19 @@
} }
// ===================================================== // =====================================================
// 標準報酬月額表関連の関数 // 標準報酬月額表関連
// ===================================================== // =====================================================
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) { async function importStdRemunerationTable(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
// 都道府県をプロンプトで取得
const prefecture = prompt( const prefecture = prompt(
"都道府県を入力してください(例: 東京):", "都道府県を入力してください(例: 東京、神奈川:",
"" ""
); );
if (!prefecture || !prefecture.trim()) { if (!prefecture || !prefecture.trim()) {
alert("都道府県を入力してください"); alert("都道府県を入力してください");
event.target.value = ""; event.target.value = "";
return; return;
} }
@@ -1628,6 +1953,10 @@
formData.append("file", file); formData.append("file", file);
formData.append("prefecture", prefecture.trim()); formData.append("prefecture", prefecture.trim());
console.log("=== インポート開始 ===");
console.log("ファイル:", file.name);
console.log("都道府県:", prefecture.trim());
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/import`, `${API_BASE}/payroll/settings/standard-remuneration/import`,
@@ -1637,31 +1966,59 @@
} }
); );
console.log("レスポンスステータス:", response.status);
const result = await response.json();
console.log("レスポンスボディ:", result);
if (response.ok) { if (response.ok) {
const result = await response.json(); let message = `✓ インポート成功!\n年度: ${result.year}\n都道府県: ${prefecture}\nインポート件数: ${result.imported_count}`;
alert( if (result.source) {
`インポートしました: ${result.imported_count}\n年度: ${ message += `\n取得元: ${result.source}`;
result.year }
}\n都道府県: ${prefecture}\n\n取得元: ${result.source || ""}` console.log("インポート成功:", message);
); console.log("デバッグ情報:", result.debug_info);
// メタデータをセッションストレージに保存
if (result.metadata) {
console.log("メタデータ保存:", result.metadata);
sessionStorage.setItem(
"stdRemunMetadata",
JSON.stringify(result.metadata)
);
}
alert(message);
// 年度リストをリロード新UIは検索条件がないため、直接リロード
loadStdRemunerationData(); loadStdRemunerationData();
} else { } else {
const error = await response.json(); // エラーレスポンスの詳細を表示
alert("インポートに失敗しました: " + JSON.stringify(error.detail)); const errorDetail = result.detail;
console.error("インポート失敗:", errorDetail);
// エラー詳細が配列またはオブジェクトの場合
let errorMessage = "インポートに失敗しました:\n\n";
if (Array.isArray(errorDetail)) {
errorMessage += errorDetail.slice(0, 10).join("\n");
} else if (typeof errorDetail === "object") {
errorMessage += JSON.stringify(errorDetail, null, 2).substring(
0,
1000
);
} else {
errorMessage += String(errorDetail).substring(0, 500);
}
console.error("詳細エラー:", errorMessage);
alert(errorMessage);
} }
} catch (error) { } catch (error) {
alert("インポートに失敗しました"); console.error("例外発生:", error);
console.error(error); alert("インポートに失敗しました: " + error.message);
} }
event.target.value = ""; event.target.value = "";
} }
function formatNumber(num) {
if (num === null || num === undefined) return "-";
return Number(num).toLocaleString("ja-JP");
}
// ===================================================== // =====================================================
// 初期化 // 初期化
// ===================================================== // =====================================================