修改
This commit is contained in:
@@ -123,14 +123,16 @@ app.mount(
|
||||
# name="static",
|
||||
# )
|
||||
|
||||
# フロントエンドファイル配信(コメントアウト - file://プロトコルで直接アクセスするため不要)
|
||||
# frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
|
||||
# if os.path.exists(frontend_path):
|
||||
# app.mount(
|
||||
# "/",
|
||||
# StaticFiles(directory=frontend_path, html=True),
|
||||
# name="frontend",
|
||||
# )
|
||||
# フロントエンドファイル配信
|
||||
frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
|
||||
if os.path.exists(frontend_path):
|
||||
app.mount(
|
||||
"/frontend",
|
||||
StaticFiles(directory=frontend_path, html=True),
|
||||
name="frontend",
|
||||
)
|
||||
else:
|
||||
print(f"Frontend path not found: {frontend_path}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -128,6 +128,69 @@ def approve_bonus(bonus_id: int, approved_by: str):
|
||||
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)
|
||||
def delete_bonus(bonus_id: int):
|
||||
"""賞与を削除"""
|
||||
|
||||
@@ -58,12 +58,15 @@ class BonusCalculationService:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT AVG(total_payment - commute_allowance) as avg_salary
|
||||
FROM monthly_payroll
|
||||
WHERE employee_id = %s
|
||||
AND payment_date < %s
|
||||
AND status IN ('approved', 'paid')
|
||||
ORDER BY payment_date DESC
|
||||
LIMIT 3
|
||||
FROM (
|
||||
SELECT total_payment, commute_allowance
|
||||
FROM monthly_payroll
|
||||
WHERE employee_id = %s
|
||||
AND payment_date < %s
|
||||
AND status IN ('approved', 'paid')
|
||||
ORDER BY payment_date DESC
|
||||
LIMIT 3
|
||||
) as recent_payroll
|
||||
""",
|
||||
(employee_id, payment_date)
|
||||
)
|
||||
|
||||
@@ -262,7 +262,7 @@ def approve_payroll(payroll_id: int, request: schemas.PayrollApprovalRequest):
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{payroll_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/{payroll_id}")
|
||||
def delete_payroll(payroll_id: int):
|
||||
"""給与データを削除"""
|
||||
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,))
|
||||
conn.commit()
|
||||
|
||||
return {"message": "給与データを削除しました", "payroll_id": payroll_id}
|
||||
|
||||
@@ -132,22 +132,25 @@ class PayrollCalculationService:
|
||||
) -> Dict[str, Any]:
|
||||
"""給与を計算"""
|
||||
|
||||
# バリデーションエラーを集約
|
||||
errors = []
|
||||
|
||||
# 給与設定を取得
|
||||
salary_setting = PayrollCalculationService.get_salary_setting(employee_id, payment_date)
|
||||
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"]))
|
||||
base_salary = hourly_rate * working_hours
|
||||
|
||||
# 欠勤控除(日給制の場合)
|
||||
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日と仮定
|
||||
absence_deduction = daily_rate * absent_days
|
||||
@@ -156,11 +159,11 @@ class PayrollCalculationService:
|
||||
# 残業手当の計算(時給 × 1.25)
|
||||
overtime_pay = Decimal("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"]))
|
||||
else:
|
||||
# 月給を時給に換算(月平均労働時間を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")
|
||||
|
||||
# 休日手当の計算(時給 × 1.35)
|
||||
@@ -181,8 +184,29 @@ class PayrollCalculationService:
|
||||
commute_allowance + other_allowance
|
||||
)
|
||||
|
||||
# 社会保険料の計算(標準報酬月額を総支給額と仮定)
|
||||
# 標準報酬月額表から標準報酬月額を検索
|
||||
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(
|
||||
@@ -218,22 +242,27 @@ class PayrollCalculationService:
|
||||
if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day):
|
||||
age -= 1
|
||||
|
||||
# 健康保険(上限適用後の標準報酬月額で計算)
|
||||
# 健康保険(標準報酬月額表から検索した月額で計算)
|
||||
health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date)
|
||||
health_insurance = Decimal("0")
|
||||
if health_insurance_rate:
|
||||
health_insurance = (
|
||||
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")
|
||||
if 40 <= age < 65:
|
||||
care_insurance_rate = PayrollCalculationService.get_insurance_rate("介護保険", payment_date)
|
||||
if care_insurance_rate:
|
||||
care_insurance = (
|
||||
health_standard_salary * Decimal(str(care_insurance_rate["employee_rate"]))
|
||||
).quantize(Decimal("1"))
|
||||
).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)
|
||||
@@ -241,15 +270,30 @@ class PayrollCalculationService:
|
||||
if pension_insurance_rate:
|
||||
pension_insurance = (
|
||||
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 = 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 = (
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ class DependentBase(BaseModel):
|
||||
name: str
|
||||
relationship: str # 配偶者/子/親/その他
|
||||
birth_date: Optional[date] = None
|
||||
mynumber: Optional[str] = None # マイナンバーカード番号
|
||||
is_spouse: bool = False
|
||||
is_disabled: bool = False
|
||||
income_amount: Decimal = Decimal("0")
|
||||
@@ -29,6 +30,7 @@ class DependentUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
relationship: Optional[str] = None
|
||||
birth_date: Optional[date] = None
|
||||
mynumber: Optional[str] = None
|
||||
is_spouse: Optional[bool] = None
|
||||
is_disabled: Optional[bool] = None
|
||||
income_amount: Optional[Decimal] = None
|
||||
|
||||
@@ -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))
|
||||
|
||||
5
backend/sql/add_employment_insurance_eligible.sql
Normal file
5
backend/sql/add_employment_insurance_eligible.sql
Normal 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=非対象';
|
||||
4
backend/sql/add_mynumber_to_dependents.sql
Normal file
4
backend/sql/add_mynumber_to_dependents.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- マイナンバーカード番号カラムを扶養家族テーブルに追加
|
||||
ALTER TABLE dependents ADD COLUMN IF NOT EXISTS mynumber VARCHAR(12);
|
||||
|
||||
COMMENT ON COLUMN dependents.mynumber IS 'マイナンバーカード番号(12桁)';
|
||||
@@ -68,6 +68,7 @@ CREATE TABLE IF NOT EXISTS salary_settings (
|
||||
payment_type VARCHAR(20) NOT NULL, -- 月給/時給/日給
|
||||
commute_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_to DATE,
|
||||
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 COLUMN salary_settings.employment_type IS '雇用形態: 正社員/契約社員/パート/アルバイト';
|
||||
COMMENT ON COLUMN salary_settings.payment_type IS '支給形態: 月給/時給/日給';
|
||||
COMMENT ON COLUMN salary_settings.employment_insurance_eligible IS '雇用保険加入対象: true=対象、false=非対象';
|
||||
|
||||
-- ========================================
|
||||
-- 4. 社会保険料率マスタ (Insurance Rates)
|
||||
|
||||
Reference in New Issue
Block a user