This commit is contained in:
admin
2026-05-14 22:02:06 +09:00
parent 78022b3b15
commit b9f994b1e1
22 changed files with 1225 additions and 126 deletions

View File

@@ -131,6 +131,40 @@ def approve_bonus(bonus_id: int, approved_by: str):
return result
@router.put("/{bonus_id}", response_model=schemas.BonusPayment)
def update_bonus(bonus_id: int, request: schemas.BonusUpdateRequest):
"""賞与の支給項目を更新(再計算は別途実行)"""
with get_connection() as conn:
with conn.cursor() as cur:
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(
"""
UPDATE bonus_payments SET
base_bonus = %s, performance_bonus = %s, other_bonus = %s,
other_deduction = %s,
payment_date = COALESCE(%s, payment_date),
bonus_type = COALESCE(%s, bonus_type)
WHERE bonus_id = %s
RETURNING *
""",
(
request.base_bonus, request.performance_bonus, request.other_bonus,
request.other_deduction,
request.payment_date, request.bonus_type,
bonus_id
)
)
result = cur.fetchone()
conn.commit()
return result
@router.post("/{bonus_id}/recalculate", response_model=schemas.BonusPayment)
def recalculate_bonus(bonus_id: int):
"""賞与を再計算"""
@@ -170,7 +204,8 @@ def recalculate_bonus(bonus_id: int):
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,
employment_insurance = %s, child_support = %s,
income_tax = %s, other_deduction = %s,
total_deduction = %s, net_bonus = %s,
calculated_at = %s, calculated_by = %s
WHERE bonus_id = %s
@@ -181,6 +216,7 @@ def recalculate_bonus(bonus_id: int):
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(), "recalculated",

View File

@@ -38,6 +38,16 @@ class BonusCalculationRequest(BaseModel):
calculated_by: str = "system"
class BonusUpdateRequest(BaseModel):
"""賞与支給項目の更新リクエスト"""
base_bonus: Decimal
performance_bonus: Decimal = Decimal("0")
other_bonus: Decimal = Decimal("0")
other_deduction: Decimal = Decimal("0")
payment_date: Optional[date] = None
bonus_type: Optional[str] = None
class BonusPayment(BonusPaymentBase):
"""賞与計算結果"""
bonus_id: int
@@ -47,6 +57,7 @@ class BonusPayment(BonusPaymentBase):
care_insurance: Decimal
pension_insurance: Decimal
employment_insurance: Decimal
child_support: Decimal = Decimal("0")
income_tax: Decimal
other_deduction: Decimal
total_deduction: Decimal

View File

@@ -89,27 +89,27 @@ class BonusCalculationService:
@staticmethod
def get_previous_salary_avg(employee_id: int, payment_date: date) -> Decimal:
"""前月までの3ヶ月平均給与を取得(賞与所得税計算用)"""
"""前月の社会保険料等控除後の給与を取得(賞与所得税計算用)
税法上は「前月分の社会保険料等控除後の給与」= 直前1ヶ月分のみを使用する。
ステータス問わず最新の月次給与レコードを参照する。
"""
with get_connection() as conn:
with conn.cursor() as cur:
# 前月までの3ヶ月の社会保険料等控除後の給与を取得
# 賞与支払日より前の直近1ヶ月分の給与を取得ステータス問わず
cur.execute(
"""
SELECT AVG(total_payment - commute_allowance - health_insurance - care_insurance - pension_insurance - employment_insurance - COALESCE(child_support, 0)) as avg_salary
FROM (
SELECT total_payment, commute_allowance, health_insurance, care_insurance, pension_insurance, employment_insurance, child_support
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
SELECT total_payment - commute_allowance - health_insurance - care_insurance - pension_insurance - employment_insurance - COALESCE(child_support, 0) as net_salary
FROM monthly_payroll
WHERE employee_id = %s
AND payment_date < %s
ORDER BY payment_date DESC
LIMIT 1
""",
(employee_id, payment_date)
)
result = cur.fetchone()
return Decimal(str(result["avg_salary"])) if result and result["avg_salary"] else Decimal("0")
return Decimal(str(result["net_salary"])) if result and result["net_salary"] else Decimal("0")
@staticmethod
def calculate_bonus_income_tax(
@@ -230,11 +230,11 @@ class BonusCalculationService:
total_bonus * Decimal(str(employment_insurance_rate["employee_rate"]))
).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
# 子ども・子育て支援金令和8年4月分以降のみ適用)
# 賞与の場合: 社会保険料率設定insurance_ratesの料率 × 賞与総額で計算
# 子ども・子育て支援金令和8年5月支給分以降のみ適用)
# 賞与は bonus_month でなく実際の支給日payment_dateで判定する
child_support = Decimal("0")
is_child_support_applicable = (
(bonus_year == 2026 and bonus_month >= 5) or bonus_year >= 2027
(payment_date.year == 2026 and payment_date.month >= 5) or payment_date.year >= 2027
)
if is_child_support_applicable:
child_support_rate = BonusCalculationService.get_insurance_rate("子ども・子育て支援金", payment_date)

View File

@@ -93,6 +93,7 @@ class BonusVoucherData(BaseModel):
bonus_year: int
bonus_month: int
bonus_type: str = "" # 夏季賞与/冬季賞与/決算賞与/その他
payment_date: Optional[str] = None # 実際の支給日
# 支給項目
base_bonus: Decimal = Decimal(0) # 基本賞与額
performance_bonus: Decimal = Decimal(0) # 業績賞与
@@ -103,6 +104,7 @@ class BonusVoucherData(BaseModel):
care_insurance: Decimal = Decimal(0)
pension_insurance: Decimal = Decimal(0)
employment_insurance: Decimal = Decimal(0)
child_support: Decimal = Decimal(0) # 子ども・子育て支援金
income_tax: Decimal = Decimal(0) # 賞与所得税
other_deduction: Decimal = Decimal(0)
total_deduction: Decimal = Decimal(0)

View File

@@ -200,9 +200,10 @@ def get_bonus_data_for_period(
cur.execute("""
SELECT
e.employee_id, e.employee_code, e.name,
bp.bonus_year, bp.bonus_month, bp.bonus_type,
bp.bonus_year, bp.bonus_month, bp.bonus_type, bp.payment_date,
bp.base_bonus, bp.performance_bonus, bp.other_bonus, bp.total_bonus,
bp.health_insurance, bp.care_insurance, bp.pension_insurance, bp.employment_insurance,
COALESCE(bp.child_support, 0) as child_support,
bp.income_tax, bp.other_deduction, bp.total_deduction,
bp.net_bonus
FROM employees e
@@ -226,6 +227,7 @@ def get_bonus_data_for_period(
bonus_year=row.get("bonus_year"),
bonus_month=row.get("bonus_month"),
bonus_type=row.get("bonus_type", ""),
payment_date=str(row.get("payment_date")) if row.get("payment_date") else None,
# 支給項目
base_bonus=Decimal(str(row.get("base_bonus", 0))) if row.get("base_bonus") else Decimal(0),
performance_bonus=Decimal(str(row.get("performance_bonus", 0))) if row.get("performance_bonus") else Decimal(0),
@@ -236,6 +238,7 @@ def get_bonus_data_for_period(
care_insurance=Decimal(str(row.get("care_insurance", 0))) if row.get("care_insurance") else Decimal(0),
pension_insurance=Decimal(str(row.get("pension_insurance", 0))) if row.get("pension_insurance") else Decimal(0),
employment_insurance=Decimal(str(row.get("employment_insurance", 0))) if row.get("employment_insurance") else Decimal(0),
child_support=Decimal(str(row.get("child_support", 0))) if row.get("child_support") else Decimal(0),
income_tax=Decimal(str(row.get("income_tax", 0))) if row.get("income_tax") else Decimal(0),
other_deduction=Decimal(str(row.get("other_deduction", 0))) if row.get("other_deduction") else Decimal(0),
total_deduction=Decimal(str(row.get("total_deduction", 0))) if row.get("total_deduction") else Decimal(0),
@@ -254,23 +257,25 @@ def get_bonus_data_for_period(
bonus_year=row[3],
bonus_month=row[4],
bonus_type=row[5] if row[5] else "",
payment_date=str(row[6]) if row[6] else None,
# 支給項目
base_bonus=Decimal(str(row[6])) if row[6] else Decimal(0),
performance_bonus=Decimal(str(row[7])) if row[7] else Decimal(0),
other_bonus=Decimal(str(row[8])) if row[8] else Decimal(0),
total_bonus=Decimal(str(row[9])) if row[9] else Decimal(0),
base_bonus=Decimal(str(row[7])) if row[7] else Decimal(0),
performance_bonus=Decimal(str(row[8])) if row[8] else Decimal(0),
other_bonus=Decimal(str(row[9])) if row[9] else Decimal(0),
total_bonus=Decimal(str(row[10])) if row[10] else Decimal(0),
# 控除項目
health_insurance=Decimal(str(row[10])) if row[10] else Decimal(0),
care_insurance=Decimal(str(row[11])) if row[11] else Decimal(0),
pension_insurance=Decimal(str(row[12])) if row[12] else Decimal(0),
employment_insurance=Decimal(str(row[13])) if row[13] else Decimal(0),
income_tax=Decimal(str(row[14])) if row[14] else Decimal(0),
other_deduction=Decimal(str(row[15])) if row[15] else Decimal(0),
total_deduction=Decimal(str(row[16])) if row[16] else Decimal(0),
net_bonus=Decimal(str(row[17])) if row[17] else Decimal(0),
health_insurance=Decimal(str(row[11])) if row[11] else Decimal(0),
care_insurance=Decimal(str(row[12])) if row[12] else Decimal(0),
pension_insurance=Decimal(str(row[13])) if row[13] else Decimal(0),
employment_insurance=Decimal(str(row[14])) if row[14] else Decimal(0),
child_support=Decimal(str(row[15])) if row[15] else Decimal(0),
income_tax=Decimal(str(row[16])) if row[16] else Decimal(0),
other_deduction=Decimal(str(row[17])) if row[17] else Decimal(0),
total_deduction=Decimal(str(row[18])) if row[18] else Decimal(0),
net_bonus=Decimal(str(row[19])) if row[19] else Decimal(0),
# レガシー互換性用
bonus_amount=Decimal(str(row[9])) if row[9] else Decimal(0),
tax_amount=Decimal(str(row[16])) if row[16] else Decimal(0)
bonus_amount=Decimal(str(row[10])) if row[10] else Decimal(0),
tax_amount=Decimal(str(row[18])) if row[18] else Decimal(0)
)
data.append(bonus_obj)
return data