diff --git a/backend/app/payroll/bonus/router.py b/backend/app/payroll/bonus/router.py index 01611e1..5a7237d 100644 --- a/backend/app/payroll/bonus/router.py +++ b/backend/app/payroll/bonus/router.py @@ -38,12 +38,14 @@ def calculate_bonus(request: schemas.BonusCalculationRequest): employee_id, bonus_year, bonus_month, bonus_type, payment_date, status, base_bonus, performance_bonus, other_bonus, total_bonus, health_insurance, care_insurance, pension_insurance, employment_insurance, + child_support, income_tax, other_deduction, total_deduction, net_bonus, calculated_at, calculated_by ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s ) @@ -56,6 +58,7 @@ def calculate_bonus(request: schemas.BonusCalculationRequest): 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["child_support"], calc_result["income_tax"], calc_result["other_deduction"], calc_result["total_deduction"], calc_result["net_bonus"], datetime.now(), calc_result["calculated_by"] @@ -196,7 +199,12 @@ def delete_bonus(bonus_id: int): """賞与を削除""" with get_connection() as conn: with conn.cursor() as cur: - cur.execute("DELETE FROM bonus_payments WHERE bonus_id = %s AND status = 'draft'", (bonus_id,)) - if cur.rowcount == 0: + # 承認済みは削除不可 + cur.execute("SELECT status FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) + row = cur.fetchone() + if not row: raise HTTPException(status_code=404, detail="賞与が見つかりません、または削除できません") + if row["status"] == "approved": + raise HTTPException(status_code=400, detail="承認済みの賞与は削除できません") + cur.execute("DELETE FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) conn.commit() diff --git a/backend/app/payroll/bonus/service.py b/backend/app/payroll/bonus/service.py index 13a3f09..c2330c2 100644 --- a/backend/app/payroll/bonus/service.py +++ b/backend/app/payroll/bonus/service.py @@ -139,12 +139,12 @@ class BonusCalculationService: # 総支給額 total_bonus = base_bonus + performance_bonus + other_bonus - # 社会保険料上限を取得 + # 社会保険料上限を取得(賞与専用の標準賞与額上限を使用) health_insurance_limit = BonusCalculationService.get_insurance_limit( - "健康保険標準報酬月額上限", payment_date + "健康保険標準賞与額上限", payment_date ) pension_insurance_limit = BonusCalculationService.get_insurance_limit( - "厚生年金標準報酬月額上限", payment_date + "厚生年金標準賞与額上限", payment_date ) # 賞与の標準賞与額(上限適用) @@ -203,6 +203,34 @@ class BonusCalculationService: total_bonus * Decimal(str(employment_insurance_rate["employee_rate"])) ).quantize(Decimal("1")) + # 子ども・子育て支援金(令和8年4月分以降のみ適用) + # 賞与の場合: 標準報酬月額表の淨化率を使い、total_bonus × 子育て率 / 2 + child_support = Decimal("0") + is_child_support_applicable = ( + (bonus_year == 2026 and bonus_month >= 4) or bonus_year >= 2027 + ) + if is_child_support_applicable: + with get_connection() as conn: + with conn.cursor() as cur: + # 標準報酬月額表から子育て率を取得(最上位等級rowの child_support/monthly_amountで率を計算) + cur.execute( + """ + SELECT child_support, monthly_amount + FROM standard_remuneration + WHERE rate_year = %s + AND monthly_amount > 0 AND child_support > 0 + ORDER BY monthly_amount DESC + LIMIT 1 + """, + (payment_date.year,) + ) + sr = cur.fetchone() + if sr and sr["monthly_amount"] and sr["monthly_amount"] > 0: + cs_rate = Decimal(str(sr["child_support"])) / Decimal(str(sr["monthly_amount"])) + # 標準賞与額に封上限なし(法定上賞与には標準賞与額上限なし) + child_support = (total_bonus * cs_rate / Decimal("2")).quantize(Decimal("1")) + print(f"[DEBUG] 賞与子育て支援金: 率={cs_rate}, 賞与総額={total_bonus} → 従業員負担={child_support}") + # 所得税の計算(賞与特有の計算) from ..calculation.service import PayrollCalculationService dependents_count = PayrollCalculationService.get_active_dependents_count(employee_id, payment_date) @@ -214,7 +242,8 @@ class BonusCalculationService: health_insurance - care_insurance - pension_insurance - - employment_insurance + employment_insurance - + child_support ) income_tax = BonusCalculationService.calculate_bonus_income_tax( @@ -229,6 +258,7 @@ class BonusCalculationService: care_insurance + pension_insurance + employment_insurance + + child_support + income_tax + other_deduction ) @@ -255,6 +285,7 @@ class BonusCalculationService: "care_insurance": care_insurance, "pension_insurance": pension_insurance, "employment_insurance": employment_insurance, + "child_support": child_support, "income_tax": income_tax, "other_deduction": other_deduction, "total_deduction": total_deduction, diff --git a/backend/app/payroll/calculation/router.py b/backend/app/payroll/calculation/router.py index 700aa62..f23ac6c 100644 --- a/backend/app/payroll/calculation/router.py +++ b/backend/app/payroll/calculation/router.py @@ -67,7 +67,8 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest): employee_id, payroll_year, payroll_month, payment_date, status, working_days, working_hours, overtime_hours, holiday_hours, late_hours, absent_days, base_salary, overtime_pay, holiday_pay, commute_allowance, other_allowance, total_payment, - health_insurance, care_insurance, pension_insurance, employment_insurance, + health_insurance, care_insurance, pension_insurance, employment_insurance, + child_support, income_tax, resident_tax, other_deduction, total_deduction, net_payment, calculated_at, calculated_by ) VALUES ( @@ -75,6 +76,7 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest): %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s, %s ) @@ -89,6 +91,7 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest): calc_result["commute_allowance"], calc_result["other_allowance"], calc_result["total_payment"], calc_result["health_insurance"], calc_result["care_insurance"], calc_result["pension_insurance"], calc_result["employment_insurance"], + calc_result["child_support"], calc_result["income_tax"], calc_result["resident_tax"], calc_result["other_deduction"], calc_result["total_deduction"], calc_result["net_payment"], datetime.now(), calc_result["calculated_by"] diff --git a/backend/app/payroll/calculation/service.py b/backend/app/payroll/calculation/service.py index e93ea4f..1692dbd 100644 --- a/backend/app/payroll/calculation/service.py +++ b/backend/app/payroll/calculation/service.py @@ -334,9 +334,11 @@ class PayrollCalculationService: if result: standard_monthly_salary = Decimal(str(result["monthly_amount"])) - print(f"[DEBUG] 標準報酬月額表検索: 総支給額={total_payment} → 標準報酬月額={standard_monthly_salary}") + child_support_full = Decimal(str(result["child_support"])) if result["child_support"] is not None else Decimal("0") + print(f"[DEBUG] 標準報酬月額表検索: 総支給額={total_payment} → 標準報酬月額={standard_monthly_salary}, 子育て支援金全額={child_support_full}") else: print(f"[DEBUG] 標準報酬月額表検索: 該当等級なし(総支給額={total_payment})") + child_support_full = Decimal("0") # 社会保険料上限を取得 @@ -410,6 +412,15 @@ class PayrollCalculationService: employment_insurance_rate = PayrollCalculationService.get_insurance_rate("雇用保険", payment_date) employment_insurance = Decimal("0") + # 子ども・子育て支援金(令和8年4月分以降のみ適用) + child_support = Decimal("0") + is_child_support_applicable = ( + (payroll_year == 2026 and payroll_month >= 4) or payroll_year >= 2027 + ) + if is_child_support_applicable and calc_social_insurance: + child_support = (child_support_full / Decimal("2")).quantize(Decimal("0.01")) + print(f"[DEBUG] 子育て支援金: 全額={child_support_full} → 折半額(従業員負担)={child_support}") + # 雇用保険加入対象かどうか確認 is_employment_insurance_eligible = salary_setting.get("employment_insurance_eligible", True) if salary_setting else True @@ -422,12 +433,13 @@ class PayrollCalculationService: elif not employment_insurance_rate and calc_social_insurance: errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year})") - # 社会保険計算スキップ時は全て。0にする + # 社会保険計算スキップ時は全て0にする if not calc_social_insurance: health_insurance = Decimal("0") care_insurance = Decimal("0") pension_insurance = Decimal("0") employment_insurance = Decimal("0") + child_support = Decimal("0") # エラーがある場合は、エラーメッセージを返す if errors: @@ -447,7 +459,8 @@ class PayrollCalculationService: health_insurance - care_insurance - pension_insurance - - employment_insurance + employment_insurance - + child_support ) if calc_income_tax: @@ -465,6 +478,7 @@ class PayrollCalculationService: care_insurance + pension_insurance + employment_insurance + + child_support + income_tax + resident_tax + other_deduction @@ -501,6 +515,7 @@ class PayrollCalculationService: "care_insurance": care_insurance, "pension_insurance": pension_insurance, "employment_insurance": employment_insurance, + "child_support": child_support, "income_tax": income_tax, "resident_tax": resident_tax, "other_deduction": other_deduction, diff --git a/backend/app/payroll/settings/standard_remuneration_router.py b/backend/app/payroll/settings/standard_remuneration_router.py index bd045cf..a136391 100644 --- a/backend/app/payroll/settings/standard_remuneration_router.py +++ b/backend/app/payroll/settings/standard_remuneration_router.py @@ -17,6 +17,7 @@ class StandardRemunerationRow(BaseModel): salary_to: Optional[Decimal] health_insurance_no_care: Decimal health_insurance_with_care: Decimal + child_support: Decimal pension_insurance: Decimal @@ -43,7 +44,7 @@ async def get_standard_remuneration(year: int, prefecture: Optional[str] = None) if prefecture: query = """ SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, - health_insurance_no_care, health_insurance_with_care, pension_insurance + health_insurance_no_care, health_insurance_with_care, child_support, pension_insurance FROM standard_remuneration WHERE rate_year = %s AND prefecture = %s ORDER BY prefecture, @@ -54,7 +55,7 @@ async def get_standard_remuneration(year: int, prefecture: Optional[str] = None) else: query = """ SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, - health_insurance_no_care, health_insurance_with_care, pension_insurance + health_insurance_no_care, health_insurance_with_care, child_support, pension_insurance FROM standard_remuneration WHERE rate_year = %s ORDER BY prefecture, @@ -76,6 +77,7 @@ async def get_standard_remuneration(year: int, prefecture: Optional[str] = None) "salary_to": row["salary_to"], "health_insurance_no_care": row["health_insurance_no_care"], "health_insurance_with_care": row["health_insurance_with_care"], + "child_support": row["child_support"], "pension_insurance": row["pension_insurance"] }) @@ -127,33 +129,38 @@ async def import_standard_remuneration( detail=f"A1セルから年度を抽出できません: {a1_value}" ) - # 第9行からメタデータ(保険料率)を抽出 + # 第8行からメタデータ(保険料率)を抽出 + # 列配置: F8(idx5)=健康保険(介護なし), H8(idx7)=健康保険(介護あり), J8(idx9)=子育て支援金, L8(idx11)=厚生年金 + # 値は0-1スケール(例: 0.0985)なので×100してパーセント表示 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 + row8 = list(ws.iter_rows(min_row=8, max_row=8, values_only=True))[0] + health_no_care_rate = row8[5] if len(row8) > 5 else None # F8 + health_with_care_rate = row8[7] if len(row8) > 7 else None # H8 + child_support_rate = row8[9] if len(row8) > 9 else None # J8 + pension_rate = row8[11] if len(row8) > 11 else None # L8 - print(f"[メタデータ] 第9行: F9={health_no_care_rate}, H9={health_with_care_rate}, J9={pension_rate}") + print(f"[メタデータ] 第8行: F8={health_no_care_rate}, H8={health_with_care_rate}, J8={child_support_rate}, L8={pension_rate}") - # パーセンテージをクリーニング(文字列から%を削除) def clean_percentage(val): if val is None: return None - val_str = str(val).strip() - val_str = val_str.replace('%', '').replace('%', '') + if isinstance(val, float): + return round(val * 100, 4) # 0.0985 → 9.85 + val_str = str(val).strip().replace('%', '').replace('%', '') try: - return float(val_str) + f = float(val_str) + # 1未満なら0-1スケールと判断して×100 + return round(f * 100, 4) if f < 1 else f except: return None metadata = { 'health_no_care_rate': clean_percentage(health_no_care_rate), 'health_with_care_rate': clean_percentage(health_with_care_rate), + 'child_support_rate': clean_percentage(child_support_rate), 'pension_rate': clean_percentage(pension_rate), - 'source': str(a1_value) if a1_value else None # A1セルの内容を追加 + 'source': str(a1_value) if a1_value else None } print(f"[メタデータ クリーニング後] {metadata}") except Exception as e: @@ -165,16 +172,15 @@ async def import_standard_remuneration( 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)}" + # まずヘッダー行を確認(10行目) + header_row = list(ws.iter_rows(min_row=10, max_row=10, values_only=True))[0] + header_debug = f"ヘッダー(10行目): {list(header_row)}" errors.append(header_debug) - # min_row=12で12行目から読み取りを開始 + # min_row=11で11行目(等級1)から読み取りを開始 # enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算 - for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)): - row_idx = 12 + idx # 実際のExcel行番号 + for idx, row in enumerate(ws.iter_rows(min_row=11, values_only=True)): + row_idx = 11 + idx # 実際のExcel行番号 # 最初の1行だけ全列を記録(デバッグ用) if idx == 0: row_debug = f"Row {row_idx} (最初のデータ行): {list(row)}" @@ -251,40 +257,40 @@ async def import_standard_remuneration( # パターン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, ...] + # r8ippan3.xlsx形式: + # col A(0)=等級, col B(1)=月額, col C(2)=円以上, col D(3)='~', col E(4)=円未満 + # col F(5)=健保介護なし全額, col G(6)=折半, col H(7)=健保介護あり全額, col I(8)=折半 + # col J(9)=子育て支援金全額, col K(10)=折半, col L(11)=厚生年金全額, col M(12)=折半 + # ※等級1のみcol C(2)が空でcol D(3)='~' + # ※等級1〜3は厚生年金col L(11)が空(最低標準報酬月額88,000円のため) + if col2_cleaned in ('~', '〜', '-'): - # パターンA: 列2=「~」, 列3=上限値 - salary_from = 0 # または前の行から取得 + # パターンA: 列C=「~」, 列D=上限値(古い形式用フォールバック) + 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)}") + child_support = clean_value(row[8] if len(row) > 8 else None, 'child_support') + pension = clean_value(row[10] if len(row) > 10 else None, 'pension') elif col3_cleaned in ('~', '〜', '-'): - # パターンB: 列2=下限, 列3=「~」, 列4=上限 + # パターンB: 列C=下限(またはNone), 列D=「~」, 列E=上限 + # r8ippan3.xlsx の標準形式 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[7] if len(row) > 7 else None, 'health_with_care') - pension = clean_value(row[9] if len(row) > 9 else None, 'pension') + child_support = clean_value(row[9] if len(row) > 9 else None, 'child_support') + pension = clean_value(row[11] if len(row) > 11 else None, 'pension') else: - # パターンC: 列2=下限、列3=上限の数値パターン + # パターンC: 列C=下限、列D=上限の数値パターン salary_from = clean_value(row[2], 'salary_from') 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') - pension = clean_value(row[8] if len(row) > 8 else None, 'pension') + child_support = clean_value(row[8] if len(row) > 8 else None, 'child_support') + pension = clean_value(row[10] if len(row) > 10 else None, 'pension') # 必須フィールドのチェック(詳細なエラーメッセージ) missing_fields = [] @@ -342,7 +348,7 @@ async def import_standard_remuneration( data_rows.append(( rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, - health_no_care, health_with_care, pension + health_no_care, health_with_care, child_support, pension )) except Exception as e: @@ -370,8 +376,8 @@ async def import_standard_remuneration( insert_query = """ INSERT INTO standard_remuneration (rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, - health_insurance_no_care, health_insurance_with_care, pension_insurance) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + health_insurance_no_care, health_insurance_with_care, child_support, pension_insurance) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cur.executemany(insert_query, data_rows) diff --git a/backend/app/static/payroll-calculation.html b/backend/app/static/payroll-calculation.html index edc6548..116e09a 100644 --- a/backend/app/static/payroll-calculation.html +++ b/backend/app/static/payroll-calculation.html @@ -2,10 +2,16 @@
+