feat: 子ども・子育て支援金を給与・賞与計算に追加(令和8年4月以降)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user