基于 skill-template 脚手架开发,包含: - payroll_config(费率配置,可编辑)+ payslip_snapshots(解析历史,只读)2 张业务表 - 全国默认 + 北上广 4 套费率(社保/公积金/个税起征点/7 级税率表) - 5 个业务 Action:calculate-payslip-simple / calculate-payslip-full / explain-term / view-config / edit-config - 个税累计预扣法计算,支持多月历史累加 - 20 个薪资名词库(社保/公积金/个税/全勤/加班费等) - 完整市场文档(README/TUTORIAL/DEMO/CHANGELOG) - 296 测试全部通过
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""个税累计预扣计算服务。
|
||
|
||
按"累计预扣法"计算当月个税:
|
||
累计预扣预缴应纳税所得额 = 累计收入 - 累计减除费用 - 累计专项扣除 - 累计专项附加扣除 - 累计依法确定的其他扣除
|
||
当月应预扣预缴税额 = (累计预扣预缴应纳税所得额 × 预扣率 - 速算扣除数) - 已预扣预缴税额
|
||
|
||
简化假设:
|
||
- 专项附加扣除与其他扣除暂不计入(数据不可得)
|
||
- 累计减除费用 = 5000 × 当年至本月份数
|
||
- 累计专项扣除 = 累计社保+公积金个人部分(从历史 snapshots 读取)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from typing import Any
|
||
|
||
from service.config_service import get_config_value, get_tax_brackets
|
||
from db.connection import get_conn
|
||
from util.logging_config import get_skill_logger
|
||
|
||
log = get_skill_logger()
|
||
|
||
|
||
def _month_index(period: str) -> int:
|
||
"""从 'YYYY-MM' 提取月份数(1-12)。"""
|
||
try:
|
||
return int(period.split("-")[1])
|
||
except (ValueError, IndexError):
|
||
return 1
|
||
|
||
|
||
def _year_of_period(period: str) -> str:
|
||
"""从 'YYYY-MM' 提取年份。"""
|
||
try:
|
||
return period.split("-")[0]
|
||
except (ValueError, IndexError):
|
||
return "1970"
|
||
|
||
|
||
def get_cumulative_deduction_before(period: str) -> dict[str, float]:
|
||
"""读取同一年的历史 snapshots,累计社保/公积金个人部分。
|
||
|
||
period 之前的同一年 snapshots(按 period 字符串排序 < 当前 period)。
|
||
返回 {social_cum, housing_fund_cum, tax_cum}。
|
||
"""
|
||
year = _year_of_period(period)
|
||
conn = get_conn()
|
||
try:
|
||
cur = conn.cursor()
|
||
cur.execute(
|
||
"""
|
||
SELECT detail_json FROM payslip_snapshots
|
||
WHERE period LIKE ? AND period < ?
|
||
ORDER BY period ASC
|
||
""",
|
||
(f"{year}-%", period),
|
||
)
|
||
import json
|
||
social_cum = 0.0
|
||
housing_fund_cum = 0.0
|
||
tax_cum = 0.0
|
||
for (detail_str,) in cur.fetchall():
|
||
if not detail_str:
|
||
continue
|
||
try:
|
||
detail = json.loads(detail_str)
|
||
except (json.JSONDecodeError, TypeError):
|
||
continue
|
||
ded = detail.get("deduction", {}) if isinstance(detail, dict) else {}
|
||
social_cum += float(ded.get("social", {}).get("personal_total", 0) or 0)
|
||
housing_fund_cum += float(ded.get("housing_fund", {}).get("personal", 0) or 0)
|
||
tax_cum += float(ded.get("tax", {}).get("actual", 0) or 0)
|
||
return {"social_cum": social_cum, "housing_fund_cum": housing_fund_cum, "tax_cum": tax_cum}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def compute_cumulative_tax(
|
||
*,
|
||
current_gross: float,
|
||
period: str,
|
||
current_social: float,
|
||
current_housing_fund: float,
|
||
region: str = "default",
|
||
) -> dict[str, Any]:
|
||
"""按累计预扣法计算当月个税。
|
||
|
||
返回 {
|
||
actual_tax: float, # 本次应预扣个税
|
||
cumulative_income: float,
|
||
cumulative_deduction: float,
|
||
cumulative_taxable: float,
|
||
cumulative_tax: float, # 累计应预扣
|
||
prior_tax_paid: float, # 之前已预扣
|
||
brackets_used: dict, # 命中的税率档
|
||
has_history: bool, # 是否有历史累计数据
|
||
note: str,
|
||
}
|
||
"""
|
||
log.info(
|
||
"tax_compute_start period=%s gross=%s social=%s housing=%s region=%s",
|
||
period, current_gross, current_social, current_housing_fund, region,
|
||
)
|
||
threshold = get_config_value("tax_threshold", region) or 5000
|
||
brackets = get_tax_brackets(region)
|
||
|
||
month_idx = _month_index(period)
|
||
cumulative_threshold = float(threshold) * month_idx
|
||
|
||
prior = get_cumulative_deduction_before(period)
|
||
has_history = prior["social_cum"] > 0 or prior["tax_cum"] > 0
|
||
|
||
cumulative_income = current_gross + prior.get("income_cum", 0.0) if has_history else current_gross
|
||
# 简化:累计专项扣除 = 累计社保+公积金个人
|
||
cumulative_special_deduction = (
|
||
prior["social_cum"] + prior["housing_fund_cum"]
|
||
+ current_social + current_housing_fund
|
||
)
|
||
|
||
cumulative_taxable = cumulative_income - cumulative_threshold - cumulative_special_deduction
|
||
if cumulative_taxable < 0:
|
||
cumulative_taxable = 0.0
|
||
|
||
cumulative_tax = 0.0
|
||
bracket_hit = None
|
||
if brackets and cumulative_taxable > 0:
|
||
for bracket in brackets:
|
||
upper = bracket.get("upper")
|
||
rate = float(bracket.get("rate", 0))
|
||
quick = float(bracket.get("quick_deduction", 0))
|
||
if upper is None or cumulative_taxable <= float(upper):
|
||
cumulative_tax = cumulative_taxable * rate - quick
|
||
bracket_hit = bracket
|
||
break
|
||
if bracket_hit is None and brackets:
|
||
bracket_hit = brackets[-1]
|
||
rate = float(bracket_hit.get("rate", 0))
|
||
quick = float(bracket_hit.get("quick_deduction", 0))
|
||
cumulative_tax = cumulative_taxable * rate - quick
|
||
|
||
cumulative_tax = max(0.0, cumulative_tax)
|
||
prior_tax_paid = prior["tax_cum"] if has_history else 0.0
|
||
actual_tax = max(0.0, cumulative_tax - prior_tax_paid)
|
||
|
||
note = "仅供参考,非官方数据"
|
||
if not has_history:
|
||
note = "仅供参考(无累计历史数据,累计按本月单月计算),非官方数据"
|
||
|
||
return {
|
||
"actual_tax": round(actual_tax, 2),
|
||
"cumulative_income": round(cumulative_income, 2),
|
||
"cumulative_threshold": round(cumulative_threshold, 2),
|
||
"cumulative_special_deduction": round(cumulative_special_deduction, 2),
|
||
"cumulative_taxable": round(cumulative_taxable, 2),
|
||
"cumulative_tax": round(cumulative_tax, 2),
|
||
"prior_tax_paid": round(prior_tax_paid, 2),
|
||
"brackets_used": bracket_hit,
|
||
"has_history": has_history,
|
||
"note": note,
|
||
}
|