修改
This commit is contained in:
@@ -31,22 +31,52 @@ def create_salary_setting(setting: schemas.SalarySettingCreate):
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(status_code=404, detail="従業員が見つかりません")
|
||||
|
||||
# employment_insurance_eligible カラムが存在するか確認
|
||||
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
|
||||
),
|
||||
"SELECT 1 FROM information_schema.columns WHERE table_name='salary_settings' AND column_name='employment_insurance_eligible'"
|
||||
)
|
||||
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()
|
||||
conn.commit()
|
||||
return result
|
||||
@@ -100,6 +130,25 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate)
|
||||
if not update_data:
|
||||
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()])
|
||||
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_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)
|
||||
if cell_value:
|
||||
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_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:
|
||||
val = str(row[i].value).replace(',', '').replace('円', '').strip()
|
||||
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))
|
||||
|
||||
|
||||
# ========================================
|
||||
# 社会保険料上限設定管理
|
||||
# ========================================
|
||||
|
||||
@@ -16,6 +16,7 @@ class SalarySettingBase(BaseModel):
|
||||
payment_type: str # 月給/時給/日給
|
||||
commute_allowance: Decimal = Decimal("0")
|
||||
other_allowance: Decimal = Decimal("0")
|
||||
employment_insurance_eligible: bool = True # 雇用保険加入対象
|
||||
valid_from: date
|
||||
valid_to: Optional[date] = None
|
||||
|
||||
@@ -33,6 +34,7 @@ class SalarySettingUpdate(BaseModel):
|
||||
payment_type: Optional[str] = None
|
||||
commute_allowance: Optional[Decimal] = None
|
||||
other_allowance: Optional[Decimal] = None
|
||||
employment_insurance_eligible: Optional[bool] = None
|
||||
valid_from: Optional[date] = None
|
||||
valid_to: Optional[date] = None
|
||||
|
||||
|
||||
@@ -127,14 +127,61 @@ async def import_standard_remuneration(
|
||||
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行目から開始)
|
||||
data_rows = []
|
||||
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行目から読み取りを開始
|
||||
# enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算
|
||||
for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)):
|
||||
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]:
|
||||
break
|
||||
@@ -144,34 +191,24 @@ async def import_standard_remuneration(
|
||||
|
||||
# 各値を取得し、Noneや空白を処理
|
||||
def clean_value(val, field_name=""):
|
||||
if val is None or val == '':
|
||||
return None
|
||||
|
||||
# 数値型の場合はそのまま返す
|
||||
# 允许 '-', '-', '―', 'ー', '−', '' 视为 0
|
||||
if val is None:
|
||||
return 0
|
||||
if isinstance(val, (int, float)):
|
||||
return val
|
||||
|
||||
# 文字列の場合
|
||||
if isinstance(val, str):
|
||||
# 空白を除去
|
||||
val = val.strip()
|
||||
# 空、ハイフン、全角ダッシュなどをNoneに
|
||||
if val in ('', '-', '-', '―', 'ー', '−'):
|
||||
return None
|
||||
# 「円」「未満」「以上」などの文字を除去
|
||||
return 0
|
||||
val = val.replace('円', '').replace('未満', '').replace('以上', '')
|
||||
# カンマを除去
|
||||
val = val.replace(',', '').replace(',', '')
|
||||
# 全角数字を半角に変換
|
||||
val = val.translate(str.maketrans('0123456789', '0123456789'))
|
||||
# 再度空チェック
|
||||
if val == '':
|
||||
return None
|
||||
return 0
|
||||
return val
|
||||
|
||||
return val
|
||||
|
||||
# デバッグ用に元のデータを保存(最初の7列を確認)
|
||||
# デバッグ用に元のデータとclean_value结果を保存
|
||||
raw_data = {
|
||||
'col0_grade': row[0] if len(row) > 0 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,
|
||||
'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')
|
||||
|
||||
# 列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 ''
|
||||
col2_cleaned = col2_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=上限
|
||||
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')
|
||||
health_with_care = clean_value(row[7] if len(row) > 7 else None, 'health_with_care')
|
||||
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:
|
||||
# パターンC: 列2=下限、列3=上限の通常パターン
|
||||
# パターン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')
|
||||
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[5] if len(row) > 5 else None, 'health_with_care')
|
||||
pension = clean_value(row[6] if len(row) > 6 else None, 'pension')
|
||||
health_with_care = clean_value(row[6] if len(row) > 6 else None, 'health_with_care')
|
||||
pension = clean_value(row[8] if len(row) > 8 else None, 'pension')
|
||||
|
||||
# 必須フィールドのチェック(詳細なエラーメッセージ)
|
||||
missing_fields = []
|
||||
@@ -298,6 +353,8 @@ async def import_standard_remuneration(
|
||||
error_msg = "有効なデータが見つかりませんでした"
|
||||
if errors:
|
||||
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)
|
||||
|
||||
# データベースに保存
|
||||
@@ -326,13 +383,25 @@ async def import_standard_remuneration(
|
||||
"year": rate_year,
|
||||
"prefecture": prefecture,
|
||||
"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:
|
||||
raise HTTPException(status_code=400, detail="年度は数値で入力してください")
|
||||
raise HTTPException(status_code=400, detail=f"年度は数値で入力してください: {str(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("")
|
||||
@@ -356,3 +425,43 @@ async def delete_standard_remuneration(year: int, prefecture: str):
|
||||
}
|
||||
except Exception as 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))
|
||||
|
||||
Reference in New Issue
Block a user