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

@@ -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))
# ========================================
# 社会保険料上限設定管理
# ========================================

View File

@@ -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

View File

@@ -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'))
# 再度空チェック
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))