feat: 工资查询助手首版 v1.0.105
基于 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 测试全部通过
This commit is contained in:
161
scripts/service/tax_service.py
Normal file
161
scripts/service/tax_service.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""个税累计预扣计算服务。
|
||||
|
||||
按"累计预扣法"计算当月个税:
|
||||
累计预扣预缴应纳税所得额 = 累计收入 - 累计减除费用 - 累计专项扣除 - 累计专项附加扣除 - 累计依法确定的其他扣除
|
||||
当月应预扣预缴税额 = (累计预扣预缴应纳税所得额 × 预扣率 - 速算扣除数) - 已预扣预缴税额
|
||||
|
||||
简化假设:
|
||||
- 专项附加扣除与其他扣除暂不计入(数据不可得)
|
||||
- 累计减除费用 = 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,
|
||||
}
|
||||
Reference in New Issue
Block a user