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:
skill-dev
2026-09-07 17:02:05 +08:00
commit 3db196e49d
205 changed files with 29927 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
"""工资条核算引擎(唯一业务内核)。
职责:
- 接收结构化字段(应发项、扣除项、period、自报实发)
- 确定性核算:应发合计 / 扣除合计 / 实发 / 核对差异
- 缺扣除项时按费率估算(社保/公积金/个税累计预扣)
- 落库到 payslip_snapshots(含完整明细 JSON)
不做:LLM 文本解析、自然语言生成。
"""
from __future__ import annotations
import json
import time
from typing import Any, Optional
from db.connection import get_conn, init_db
from service.config_service import get_config_value
from service.tax_service import compute_cumulative_tax
from util.constants import SKILL_SLUG
from util.logging_config import get_skill_logger
def _clamp_base(base: float, lower: float, upper: float) -> float:
"""按上下限裁剪缴费基数。"""
if base < lower:
return lower
if base > upper:
return upper
return base
def _estimate_social(base_salary: float, region: str = "default") -> dict[str, Any]:
"""按费率估算社保个人部分。"""
pension_rate = float(get_config_value("social_pension_rate", region) or 0.08)
medical_rate = float(get_config_value("social_medical_rate", region) or 0.02)
unemploy_rate = float(get_config_value("social_unemployment_rate", region) or 0.005)
base_min = float(get_config_value("social_base_min", region) or 5000)
base_max = float(get_config_value("social_base_max", region) or 30000)
base = _clamp_base(base_salary, base_min, base_max)
pension = round(base * pension_rate, 2)
medical = round(base * medical_rate, 2)
unemployment = round(base * unemploy_rate, 2)
personal_total = round(pension + medical + unemployment, 2)
return {
"base_used": round(base, 2),
"pension": pension,
"medical": medical,
"unemployment": unemployment,
"personal_total": personal_total,
"estimated": True,
}
def _estimate_housing_fund(base_salary: float, region: str = "default") -> dict[str, Any]:
"""按费率估算公积金个人部分。"""
rate = float(get_config_value("housing_fund_rate", region) or 0.07)
base_min = float(get_config_value("housing_fund_base_min", region) or 2500)
base_max = float(get_config_value("housing_fund_base_max", region) or 30000)
base = _clamp_base(base_salary, base_min, base_max)
personal = round(base * rate, 2)
return {
"base_used": round(base, 2),
"personal_rate": rate,
"personal": personal,
"estimated": True,
}
def calculate_payslip(
*,
base_salary: float = 0.0,
performance: float = 0.0,
full_attendance: float = 0.0,
meal_allowance: float = 0.0,
transport_allowance: float = 0.0,
overtime_pay: float = 0.0,
other_gross: float = 0.0,
# 扣除项(可选,None 表示缺值→估算)
social_personal: Optional[float] = None,
housing_fund_personal: Optional[float] = None,
tax: Optional[float] = None,
leave_deduction: float = 0.0,
other_deduction: float = 0.0,
period: str = "",
reported_net: Optional[float] = None,
raw_payload: str = "",
region: str = "default",
) -> dict[str, Any]:
"""工资条核算入口。
缺扣除项时按费率估算;估算项标 estimated=True,note 附"仅供参考"。
落库到 payslip_snapshots,返回结构化 JSON。
"""
log = get_skill_logger()
log.info(
"payslip_calculate_start period=%s base=%s reported_net=%s region=%s",
period, base_salary, reported_net, region,
)
# 确保 DB 已初始化(agent/CLI 都可能首次调用)
init_db()
# 应发合计
bonus_total = round(performance + full_attendance, 2)
allowance_total = round(meal_allowance + transport_allowance, 2)
gross_total = round(
base_salary + performance + full_attendance
+ meal_allowance + transport_allowance + overtime_pay + other_gross,
2,
)
# 扣除项:缺失则估算
estimated = False
social_detail: dict[str, Any]
if social_personal is None:
social_detail = _estimate_social(base_salary, region)
estimated = True
else:
social_detail = {"personal_total": round(float(social_personal), 2), "estimated": False}
housing_detail: dict[str, Any]
if housing_fund_personal is None:
housing_detail = _estimate_housing_fund(base_salary, region)
estimated = True
else:
housing_detail = {"personal": round(float(housing_fund_personal), 2), "estimated": False}
# 个税:缺失则按累计预扣估算
tax_detail: dict[str, Any]
if tax is None:
social_for_tax = float(social_detail.get("personal_total", 0))
housing_for_tax = float(housing_detail.get("personal", 0))
tax_result = compute_cumulative_tax(
current_gross=gross_total,
period=period or "1970-01",
current_social=social_for_tax,
current_housing_fund=housing_for_tax,
region=region,
)
tax_detail = {
"actual": tax_result["actual_tax"],
"cumulative_taxable": tax_result["cumulative_taxable"],
"brackets_used": tax_result["brackets_used"],
"has_history": tax_result["has_history"],
"estimated": True,
"note": tax_result["note"],
}
estimated = True
else:
tax_detail = {"actual": round(float(tax), 2), "estimated": False}
social_total = float(social_detail.get("personal_total", 0))
housing_total = float(housing_detail.get("personal", 0))
tax_total = float(tax_detail.get("actual", 0))
leave_total = float(leave_deduction)
other_ded_total = float(other_deduction)
deduction_total = round(
social_total + housing_total + tax_total + leave_total + other_ded_total, 2
)
net_salary = round(gross_total - deduction_total, 2)
# 核对
check: dict[str, Any] = {"reported": None, "calculated": net_salary, "match": None, "diff": None}
if reported_net is not None:
reported_net = round(float(reported_net), 2)
diff = round(reported_net - net_salary, 2)
check = {
"reported": reported_net,
"calculated": net_salary,
"match": abs(diff) < 0.01,
"diff": diff,
}
# 完整明细(供历史查询与个税累计)
detail = {
"gross": {
"base_salary": base_salary,
"performance": performance,
"full_attendance": full_attendance,
"meal_allowance": meal_allowance,
"transport_allowance": transport_allowance,
"overtime_pay": overtime_pay,
"other_gross": other_gross,
"bonus_total": bonus_total,
"allowance_total": allowance_total,
"total": gross_total,
},
"deduction": {
"social": social_detail,
"housing_fund": housing_detail,
"tax": tax_detail,
"leave": leave_total,
"other": other_ded_total,
"total": deduction_total,
},
"net": net_salary,
"check": check,
"region": region,
}
estimates = []
if estimated:
notes = []
if social_detail.get("estimated"):
estimates.append({"item": "社保个人", "value": social_total, "note": "按费率估算,仅供参考"})
if housing_detail.get("estimated"):
estimates.append({"item": "公积金个人", "value": housing_total, "note": "按费率估算,仅供参考"})
if tax_detail.get("estimated"):
estimates.append({"item": "个税", "value": tax_total, "note": tax_detail.get("note", "仅供参考")})
if not estimates:
notes.append("所有扣除项均已由用户给定,无估算")
else:
estimates.append({"item": "无估算", "value": 0, "note": "所有扣除项均由用户给定"})
# 落库
snapshot_date = int(time.time())
conn = get_conn()
snapshot_id = None
try:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO payslip_snapshots (
snapshot_date, period, base_salary,
gross_total, deduction_total, net_salary,
reported_net, check_match, check_diff,
detail_json, raw_payload
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
snapshot_date, period or None, base_salary,
gross_total, deduction_total, net_salary,
reported_net,
int(check["match"]) if check["match"] is not None else None,
check["diff"],
json.dumps(detail, ensure_ascii=False),
raw_payload or None,
),
)
snapshot_id = cur.lastrowid
conn.commit()
finally:
conn.close()
log.info(
"payslip_calculate_done period=%s gross=%s deduction=%s net=%s match=%s snapshot_id=%s",
period, gross_total, deduction_total, net_salary, check["match"], snapshot_id,
)
return {
"ok": True,
"skill": SKILL_SLUG,
"period": period,
"gross": detail["gross"],
"deduction": detail["deduction"],
"net": net_salary,
"check": check,
"estimates": estimates,
"snapshot_id": snapshot_id,
}
def calculate_payslip_simple(
*,
base_salary: float,
period: str,
reported_net: Optional[float] = None,
region: str = "default",
) -> dict[str, Any]:
"""快速估算:仅基本工资+period,按费率估全部扣除项。"""
return calculate_payslip(
base_salary=base_salary,
period=period,
reported_net=reported_net,
region=region,
)
def list_snapshots(period: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
"""列出解析历史(供数据管理 / CLI 查看)。"""
conn = get_conn()
try:
cur = conn.cursor()
if period:
cur.execute(
"""
SELECT id, snapshot_date, period, base_salary, gross_total,
deduction_total, net_salary, reported_net, check_match, check_diff
FROM payslip_snapshots WHERE period=?
ORDER BY snapshot_date DESC LIMIT ?
""",
(period, limit),
)
else:
cur.execute(
"""
SELECT id, snapshot_date, period, base_salary, gross_total,
deduction_total, net_salary, reported_net, check_match, check_diff
FROM payslip_snapshots
ORDER BY snapshot_date DESC LIMIT ?
""",
(limit,),
)
rows = cur.fetchall()
return [
{
"id": r[0],
"snapshot_date": r[1],
"period": r[2],
"base_salary": r[3],
"gross_total": r[4],
"deduction_total": r[5],
"net_salary": r[6],
"reported_net": r[7],
"check_match": r[8],
"check_diff": r[9],
}
for r in rows
]
finally:
conn.close()