feat: 税务合规智能体—税负监控+发票校验+社保比对
This commit is contained in:
@@ -0,0 +1,884 @@
|
||||
"""税务合规智能体 API — 管理会计OS
|
||||
① 税负监控: 税务记录CRUD + 税负率计算(实缴/收入×100%) + 行业基准预警(增值税3.5%/所得税2.5%, 超±20%预警)
|
||||
② 发票校验: 发票录入 + 批量校验(发票号33位数字 / 金额与报销单匹配 / 供应商与合同匹配) + 异常查询
|
||||
③ 社保比对: 缴费记录CRUD + 比对(基数与工资匹配60%~300% / 单位缴纳比例24.5% / 漏缴提醒) + 异常查询
|
||||
看板: /tax/dashboard 聚合税负趋势+行业对比 + 发票异常列表 + 社保异常列表
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, date
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import TaxRecord, InvoiceCheck, SocialSecurity, ExpenseReimbursement
|
||||
|
||||
router = APIRouter(prefix="/api/cma/tax", tags=["税务合规"])
|
||||
|
||||
# ── 行业平均税负率参考值 (%) ──
|
||||
TAX_BENCHMARKS = {
|
||||
"vat": 3.5, # 增值税平均税负率
|
||||
"income": 2.5, # 企业所得税平均税负率
|
||||
"surtax": 0.5, # 附加税平均税负率
|
||||
}
|
||||
TAX_TYPE_LABELS = {"vat": "增值税", "income": "所得税", "surtax": "附加税"}
|
||||
INVOICE_TYPE_LABELS = {"vat": "增值税专用发票", "vat_normal": "增值税普通发票", "electronic": "电子发票", "other": "其他"}
|
||||
SS_COMPANY_RATE = 24.5 # 单位缴纳比例: 养老16% + 医疗8% + 失业0.5%
|
||||
SS_RATE_TOLERANCE = 1.0 # 比例允许偏差 ±1%
|
||||
SS_BASE_LOW = 0.6 # 基数下限 = 工资60%
|
||||
SS_BASE_HIGH = 3.0 # 基数上限 = 工资300%
|
||||
INVOICE_NO_LEN = 33 # 发票号位数(全数字)
|
||||
|
||||
|
||||
# ── 序列化 ──
|
||||
def _tax_to_dict(t: TaxRecord) -> dict:
|
||||
return {
|
||||
"id": t.id,
|
||||
"entity_id": t.entity_id,
|
||||
"period": t.period,
|
||||
"tax_type": t.tax_type,
|
||||
"tax_type_label": TAX_TYPE_LABELS.get(t.tax_type, t.tax_type),
|
||||
"tax_payable": t.tax_payable,
|
||||
"tax_paid": t.tax_paid,
|
||||
"tax_rate": t.tax_rate,
|
||||
"income": t.income,
|
||||
"tax_burden_rate": t.tax_burden_rate,
|
||||
"benchmark": TAX_BENCHMARKS.get(t.tax_type),
|
||||
"burden_status": t.burden_status,
|
||||
"warning_msg": t.warning_msg,
|
||||
"remark": t.remark,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _invoice_to_dict(i: InvoiceCheck) -> dict:
|
||||
return {
|
||||
"id": i.id,
|
||||
"entity_id": i.entity_id,
|
||||
"invoice_no": i.invoice_no,
|
||||
"amount": i.amount,
|
||||
"invoice_type": i.invoice_type,
|
||||
"invoice_type_label": INVOICE_TYPE_LABELS.get(i.invoice_type, i.invoice_type),
|
||||
"invoice_date": i.invoice_date.isoformat() if i.invoice_date else None,
|
||||
"supplier": i.supplier,
|
||||
"reimb_no": i.reimb_no,
|
||||
"contract_no": i.contract_no,
|
||||
"check_status": i.check_status,
|
||||
"check_result": i.check_result,
|
||||
"check_reason": i.check_reason,
|
||||
"checked_at": i.checked_at.isoformat() if i.checked_at else None,
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _ss_to_dict(s: SocialSecurity) -> dict:
|
||||
return {
|
||||
"id": s.id,
|
||||
"entity_id": s.entity_id,
|
||||
"employee": s.employee,
|
||||
"period": s.period,
|
||||
"base_amount": s.base_amount,
|
||||
"salary": s.salary,
|
||||
"company_amount": s.company_amount,
|
||||
"personal_amount": s.personal_amount,
|
||||
"company_rate": s.company_rate,
|
||||
"check_status": s.check_status,
|
||||
"warning_msg": s.warning_msg,
|
||||
"remark": s.remark,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _parse_date(v) -> datetime | None:
|
||||
if not v:
|
||||
return None
|
||||
try:
|
||||
if isinstance(v, datetime):
|
||||
return v
|
||||
if isinstance(v, date):
|
||||
return datetime(v.year, v.month, v.day)
|
||||
return datetime.fromisoformat(str(v)[:10])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ① 税负监控 — 税务记录 CRUD + 税负率计算 + 行业基准预警
|
||||
# ============================================================
|
||||
|
||||
def _calc_burden(t: TaxRecord):
|
||||
"""计算单条税务记录的税负率 + 行业基准预警"""
|
||||
rate = None
|
||||
if t.income and t.income > 0:
|
||||
rate = round(t.tax_paid / t.income * 100, 2)
|
||||
t.tax_burden_rate = rate
|
||||
bench = TAX_BENCHMARKS.get(t.tax_type)
|
||||
t.burden_status = "normal"
|
||||
t.warning_msg = None
|
||||
if rate is None or bench is None:
|
||||
return
|
||||
low, high = bench * 0.8, bench * 1.2
|
||||
if rate < low or rate > high:
|
||||
t.burden_status = "alert"
|
||||
t.warning_msg = (
|
||||
f"税负率{rate}%超出行业均值{bench}%的±20%区间({low}%~{high}%),"
|
||||
f"{'偏高需核查进项/优惠' if rate > high else '偏低需核查申报完整性'}"
|
||||
)
|
||||
|
||||
|
||||
def _apply_burden_to_all(db: Session, entity_id: int):
|
||||
"""重算某企业全部税务记录的税负率与预警"""
|
||||
records = db.query(TaxRecord).filter(TaxRecord.entity_id == entity_id).all()
|
||||
for t in records:
|
||||
_calc_burden(t)
|
||||
db.commit()
|
||||
return records
|
||||
|
||||
|
||||
@router.get("/records")
|
||||
def list_tax_records(
|
||||
period: str = Query(None),
|
||||
tax_type: str = Query(None),
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""查询税务记录列表"""
|
||||
q = db.query(TaxRecord)
|
||||
if entity_id:
|
||||
q = q.filter(TaxRecord.entity_id == entity_id)
|
||||
if period:
|
||||
q = q.filter(TaxRecord.period == period)
|
||||
if tax_type:
|
||||
q = q.filter(TaxRecord.tax_type == tax_type)
|
||||
records = q.order_by(TaxRecord.period.desc(), TaxRecord.id.asc()).all()
|
||||
return {"data": [_tax_to_dict(t) for t in records], "total": len(records)}
|
||||
|
||||
|
||||
@router.post("/records")
|
||||
def create_tax_record(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""新增税务记录"""
|
||||
period = data.get("period")
|
||||
tax_type = data.get("tax_type")
|
||||
if not period or not tax_type:
|
||||
raise HTTPException(400, "缺少必要参数: period, tax_type")
|
||||
if tax_type not in TAX_TYPE_LABELS:
|
||||
raise HTTPException(400, f"无效税种: {tax_type},可选 vat/income/surtax")
|
||||
t = TaxRecord(
|
||||
entity_id=data.get("entity_id") or 1,
|
||||
period=period,
|
||||
tax_type=tax_type,
|
||||
tax_payable=data.get("tax_payable") or 0,
|
||||
tax_paid=data.get("tax_paid") or 0,
|
||||
tax_rate=data.get("tax_rate"),
|
||||
income=data.get("income") or 0,
|
||||
remark=data.get("remark"),
|
||||
)
|
||||
_calc_burden(t)
|
||||
db.add(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return {"message": "税务记录已创建", "data": _tax_to_dict(t)}
|
||||
|
||||
|
||||
@router.put("/records/{record_id}")
|
||||
def update_tax_record(
|
||||
record_id: int,
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""更新税务记录(自动重算税负率与预警)"""
|
||||
t = db.query(TaxRecord).filter(TaxRecord.id == record_id).first()
|
||||
if not t:
|
||||
raise HTTPException(404, "税务记录不存在")
|
||||
if "period" in data:
|
||||
t.period = data["period"]
|
||||
if "tax_type" in data:
|
||||
if data["tax_type"] not in TAX_TYPE_LABELS:
|
||||
raise HTTPException(400, f"无效税种: {data['tax_type']}")
|
||||
t.tax_type = data["tax_type"]
|
||||
if "tax_payable" in data:
|
||||
t.tax_payable = data["tax_payable"] or 0
|
||||
if "tax_paid" in data:
|
||||
t.tax_paid = data["tax_paid"] or 0
|
||||
if "tax_rate" in data:
|
||||
t.tax_rate = data.get("tax_rate")
|
||||
if "income" in data:
|
||||
t.income = data["income"] or 0
|
||||
if "remark" in data:
|
||||
t.remark = data.get("remark")
|
||||
_calc_burden(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return {"message": "税务记录已更新", "data": _tax_to_dict(t)}
|
||||
|
||||
|
||||
@router.delete("/records/{record_id}")
|
||||
def delete_tax_record(
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""删除税务记录"""
|
||||
t = db.query(TaxRecord).filter(TaxRecord.id == record_id).first()
|
||||
if not t:
|
||||
raise HTTPException(404, "税务记录不存在")
|
||||
db.delete(t)
|
||||
db.commit()
|
||||
return {"message": "税务记录已删除"}
|
||||
|
||||
|
||||
@router.get("/burden")
|
||||
def burden_analysis(
|
||||
entity_id: int = Query(None),
|
||||
tax_type: str = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""税负率计算 + 行业对比 + 预警列表(前端趋势图数据源)"""
|
||||
q = db.query(TaxRecord)
|
||||
if entity_id:
|
||||
q = q.filter(TaxRecord.entity_id == entity_id)
|
||||
if tax_type:
|
||||
q = q.filter(TaxRecord.tax_type == tax_type)
|
||||
records = q.order_by(TaxRecord.period.asc(), TaxRecord.id.asc()).all()
|
||||
for t in records:
|
||||
_calc_burden(t)
|
||||
db.commit()
|
||||
|
||||
# 按期间聚合税负率(每种税一个序列)
|
||||
trend_map: dict[str, dict] = {}
|
||||
for t in records:
|
||||
if t.tax_burden_rate is None:
|
||||
continue
|
||||
entry = trend_map.setdefault(t.period, {"period": t.period})
|
||||
entry[f"{t.tax_type}_rate"] = t.tax_burden_rate
|
||||
entry[f"{t.tax_type}_benchmark"] = TAX_BENCHMARKS.get(t.tax_type)
|
||||
trend = sorted(trend_map.values(), key=lambda x: x["period"])
|
||||
|
||||
alerts = [t for t in records if t.burden_status == "alert"]
|
||||
return {
|
||||
"trend": trend,
|
||||
"benchmarks": TAX_BENCHMARKS,
|
||||
"records": [_tax_to_dict(t) for t in records],
|
||||
"alerts": [_tax_to_dict(t) for t in alerts],
|
||||
"alert_count": len(alerts),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/check")
|
||||
def run_tax_check(
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""手动触发税负率重算与预警检查"""
|
||||
records = _apply_burden_to_all(db, entity_id or 1)
|
||||
alerts = [t for t in records if t.burden_status == "alert"]
|
||||
return {"message": f"税负检查完成,共{len(records)}条记录,{len(alerts)}条预警", "alert_count": len(alerts)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ② 发票校验 — 录入 + 批量校验 + 异常查询
|
||||
# ============================================================
|
||||
|
||||
def _check_invoice(db: Session, inv: InvoiceCheck):
|
||||
"""发票校验规则:号码格式 / 金额与报销单匹配 / 供应商与合同匹配"""
|
||||
results: list[dict] = []
|
||||
status = "valid"
|
||||
|
||||
# 规则1: 发票号格式(33位数字)
|
||||
no = (inv.invoice_no or "").strip()
|
||||
if len(no) != INVOICE_NO_LEN or not no.isdigit():
|
||||
status = "invalid"
|
||||
results.append({
|
||||
"rule": "发票号格式",
|
||||
"passed": False,
|
||||
"message": f"发票号格式错误:应为{INVOICE_NO_LEN}位纯数字,当前{len(no)}位",
|
||||
})
|
||||
else:
|
||||
results.append({"rule": "发票号格式", "passed": True, "message": "33位数字格式正确"})
|
||||
|
||||
# 规则2: 金额与报销单匹配
|
||||
if inv.reimb_no:
|
||||
reimb = db.query(ExpenseReimbursement).filter(ExpenseReimbursement.reimb_no == inv.reimb_no).first()
|
||||
if not reimb:
|
||||
status = "invalid"
|
||||
results.append({"rule": "报销单匹配", "passed": False, "message": f"报销单{inv.reimb_no}不存在"})
|
||||
elif abs((inv.amount or 0) - (reimb.amount or 0)) > 0.01:
|
||||
status = "invalid"
|
||||
results.append({
|
||||
"rule": "报销单匹配",
|
||||
"passed": False,
|
||||
"message": f"发票金额{inv.amount}元与报销单{inv.reimb_no}金额{reimb.amount}元不符",
|
||||
})
|
||||
else:
|
||||
results.append({"rule": "报销单匹配", "passed": True, "message": f"与报销单{inv.reimb_no}金额一致"})
|
||||
|
||||
# 规则3: 供应商与合同匹配
|
||||
if inv.contract_no:
|
||||
if not (inv.supplier or "").strip():
|
||||
status = "invalid"
|
||||
results.append({"rule": "供应商匹配", "passed": False, "message": f"合同{inv.contract_no}未关联供应商"})
|
||||
else:
|
||||
# 同合同下其他发票的供应商一致性
|
||||
others = (
|
||||
db.query(InvoiceCheck)
|
||||
.filter(
|
||||
InvoiceCheck.contract_no == inv.contract_no,
|
||||
InvoiceCheck.id != inv.id,
|
||||
InvoiceCheck.supplier.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
mismatch = [o.supplier for o in others if o.supplier != inv.supplier]
|
||||
if mismatch:
|
||||
status = "invalid"
|
||||
results.append({
|
||||
"rule": "供应商匹配",
|
||||
"passed": False,
|
||||
"message": f"供应商{inv.supplier}与合同{inv.contract_no}下其他发票供应商{mismatch[0]}不一致",
|
||||
})
|
||||
else:
|
||||
results.append({"rule": "供应商匹配", "passed": True, "message": f"供应商与合同{inv.contract_no}匹配"})
|
||||
|
||||
inv.check_result = results
|
||||
inv.check_status = status
|
||||
inv.check_reason = ";".join(r["message"] for r in results if not r["passed"]) or None
|
||||
inv.checked_at = datetime.now()
|
||||
|
||||
|
||||
@router.get("/invoices")
|
||||
def list_invoices(
|
||||
status: str = Query(None),
|
||||
keyword: str = Query(None),
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""发票列表"""
|
||||
q = db.query(InvoiceCheck)
|
||||
if entity_id:
|
||||
q = q.filter(InvoiceCheck.entity_id == entity_id)
|
||||
if status:
|
||||
q = q.filter(InvoiceCheck.check_status == status)
|
||||
if keyword:
|
||||
kw = f"%{keyword}%"
|
||||
q = q.filter(
|
||||
(InvoiceCheck.invoice_no.like(kw))
|
||||
| (InvoiceCheck.supplier.like(kw))
|
||||
| (InvoiceCheck.reimb_no.like(kw))
|
||||
)
|
||||
invoices = q.order_by(InvoiceCheck.id.desc()).all()
|
||||
return {"data": [_invoice_to_dict(i) for i in invoices], "total": len(invoices)}
|
||||
|
||||
|
||||
@router.post("/invoices")
|
||||
def create_invoice(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""录入发票(自动执行校验)"""
|
||||
invoice_no = data.get("invoice_no")
|
||||
if not invoice_no:
|
||||
raise HTTPException(400, "缺少必要参数: invoice_no")
|
||||
if data.get("amount") is None:
|
||||
raise HTTPException(400, "缺少必要参数: amount")
|
||||
inv = InvoiceCheck(
|
||||
entity_id=data.get("entity_id") or 1,
|
||||
invoice_no=str(invoice_no).strip(),
|
||||
amount=data.get("amount"),
|
||||
invoice_type=data.get("invoice_type") or "vat",
|
||||
invoice_date=_parse_date(data.get("invoice_date")),
|
||||
supplier=data.get("supplier"),
|
||||
reimb_no=data.get("reimb_no"),
|
||||
contract_no=data.get("contract_no"),
|
||||
)
|
||||
_check_invoice(db, inv)
|
||||
db.add(inv)
|
||||
db.commit()
|
||||
db.refresh(inv)
|
||||
return {"message": "发票已录入并校验", "data": _invoice_to_dict(inv)}
|
||||
|
||||
|
||||
@router.put("/invoices/{invoice_id}")
|
||||
def update_invoice(
|
||||
invoice_id: int,
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""更新发票(自动重新校验)"""
|
||||
inv = db.query(InvoiceCheck).filter(InvoiceCheck.id == invoice_id).first()
|
||||
if not inv:
|
||||
raise HTTPException(404, "发票不存在")
|
||||
if "invoice_no" in data:
|
||||
inv.invoice_no = str(data["invoice_no"]).strip()
|
||||
if "amount" in data:
|
||||
inv.amount = data["amount"]
|
||||
if "invoice_type" in data:
|
||||
inv.invoice_type = data["invoice_type"]
|
||||
if "invoice_date" in data:
|
||||
inv.invoice_date = _parse_date(data.get("invoice_date"))
|
||||
if "supplier" in data:
|
||||
inv.supplier = data.get("supplier")
|
||||
if "reimb_no" in data:
|
||||
inv.reimb_no = data.get("reimb_no")
|
||||
if "contract_no" in data:
|
||||
inv.contract_no = data.get("contract_no")
|
||||
_check_invoice(db, inv)
|
||||
db.commit()
|
||||
db.refresh(inv)
|
||||
return {"message": "发票已更新并重新校验", "data": _invoice_to_dict(inv)}
|
||||
|
||||
|
||||
@router.delete("/invoices/{invoice_id}")
|
||||
def delete_invoice(
|
||||
invoice_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""删除发票"""
|
||||
inv = db.query(InvoiceCheck).filter(InvoiceCheck.id == invoice_id).first()
|
||||
if not inv:
|
||||
raise HTTPException(404, "发票不存在")
|
||||
db.delete(inv)
|
||||
db.commit()
|
||||
return {"message": "发票已删除"}
|
||||
|
||||
|
||||
@router.post("/invoices/check")
|
||||
def batch_check_invoices(
|
||||
invoice_id: int = Query(None),
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""批量校验发票(默认校验全部 pending,可指定单张)"""
|
||||
q = db.query(InvoiceCheck)
|
||||
if invoice_id:
|
||||
q = q.filter(InvoiceCheck.id == invoice_id)
|
||||
elif entity_id:
|
||||
q = q.filter(InvoiceCheck.entity_id == entity_id)
|
||||
invoices = q.all()
|
||||
for inv in invoices:
|
||||
_check_invoice(db, inv)
|
||||
db.commit()
|
||||
abnormal = [i for i in invoices if i.check_status in ("invalid", "warning")]
|
||||
return {
|
||||
"message": f"批量校验完成:{len(invoices)}张,异常{len(abnormal)}张",
|
||||
"total": len(invoices),
|
||||
"abnormal_count": len(abnormal),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/invoices/abnormal")
|
||||
def list_abnormal_invoices(
|
||||
entity_id: int = Query(None),
|
||||
limit: int = Query(50),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""查询异常发票(invalid/warning)"""
|
||||
q = db.query(InvoiceCheck).filter(InvoiceCheck.check_status.in_(["invalid", "warning"]))
|
||||
if entity_id:
|
||||
q = q.filter(InvoiceCheck.entity_id == entity_id)
|
||||
invoices = q.order_by(InvoiceCheck.id.desc()).limit(limit).all()
|
||||
return {"data": [_invoice_to_dict(i) for i in invoices], "total": len(invoices)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ③ 社保比对 — 缴费记录 CRUD + 比对 + 异常查询
|
||||
# ============================================================
|
||||
|
||||
def _check_ss(db: Session, s: SocialSecurity, all_records: list | None = None):
|
||||
"""社保比对规则:基数与工资匹配 / 单位缴纳比例 / 漏缴(含月份断层检测)"""
|
||||
warnings: list[str] = []
|
||||
alerts: list[str] = []
|
||||
|
||||
# 规则1: 缴费基数与工资匹配(60%~300%区间)
|
||||
if s.salary and s.salary > 0 and s.base_amount and s.base_amount > 0:
|
||||
low, high = s.salary * SS_BASE_LOW, s.salary * SS_BASE_HIGH
|
||||
if s.base_amount < low or s.base_amount > high:
|
||||
warnings.append(
|
||||
f"缴费基数{s.base_amount}元超出工资{s.salary}元的{int(SS_BASE_LOW*100)}%~{int(SS_BASE_HIGH*100)}%区间({low:.0f}~{high:.0f})"
|
||||
)
|
||||
|
||||
# 规则2: 单位缴纳比例(养老16%+医疗8%+失业0.5%≈24.5%)
|
||||
rate = None
|
||||
if s.base_amount and s.base_amount > 0 and s.company_amount is not None:
|
||||
rate = round(s.company_amount / s.base_amount * 100, 2)
|
||||
s.company_rate = rate
|
||||
if rate is not None and abs(rate - SS_COMPANY_RATE) > SS_RATE_TOLERANCE:
|
||||
warnings.append(f"单位缴纳比例{rate}%与标准{SS_COMPANY_RATE}%±{SS_RATE_TOLERANCE}%不符")
|
||||
|
||||
# 规则3: 漏缴检测
|
||||
if not s.base_amount or s.base_amount <= 0 or (not s.company_amount and not s.personal_amount):
|
||||
alerts.append(f"{s.employee}本期缴费基数为0或未缴费(疑似漏缴)")
|
||||
|
||||
# 规则3b: 月份断层检测(同人相邻记录期间间隔>1个月 → 漏缴)
|
||||
if all_records is not None:
|
||||
periods = sorted(
|
||||
r.period for r in all_records
|
||||
if r.employee == s.employee and r.period != s.period
|
||||
)
|
||||
prev = None
|
||||
for p in periods:
|
||||
if prev is not None:
|
||||
try:
|
||||
py, pm = map(int, prev.split("-"))
|
||||
cy, cm = map(int, p.split("-"))
|
||||
gap = (cy - py) * 12 + (cm - pm)
|
||||
if gap > 1:
|
||||
alerts.append(f"{s.employee}在{prev}至{p}之间漏缴{max(0, gap-1)}个月")
|
||||
except Exception:
|
||||
pass
|
||||
prev = p
|
||||
|
||||
if alerts:
|
||||
s.check_status = "alert"
|
||||
elif warnings:
|
||||
s.check_status = "warning"
|
||||
else:
|
||||
s.check_status = "normal"
|
||||
s.warning_msg = ";".join(alerts + warnings) or None
|
||||
|
||||
|
||||
@router.get("/ss")
|
||||
def list_ss_records(
|
||||
period: str = Query(None),
|
||||
employee: str = Query(None),
|
||||
status: str = Query(None),
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""社保缴费记录列表"""
|
||||
q = db.query(SocialSecurity)
|
||||
if entity_id:
|
||||
q = q.filter(SocialSecurity.entity_id == entity_id)
|
||||
if period:
|
||||
q = q.filter(SocialSecurity.period == period)
|
||||
if employee:
|
||||
q = q.filter(SocialSecurity.employee.like(f"%{employee}%"))
|
||||
if status:
|
||||
q = q.filter(SocialSecurity.check_status == status)
|
||||
records = q.order_by(SocialSecurity.period.desc(), SocialSecurity.id.asc()).all()
|
||||
return {"data": [_ss_to_dict(s) for s in records], "total": len(records)}
|
||||
|
||||
|
||||
@router.post("/ss")
|
||||
def create_ss_record(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""新增社保缴费记录(自动比对)"""
|
||||
employee = data.get("employee")
|
||||
period = data.get("period")
|
||||
if not employee or not period:
|
||||
raise HTTPException(400, "缺少必要参数: employee, period")
|
||||
all_records = db.query(SocialSecurity).filter(SocialSecurity.entity_id == (data.get("entity_id") or 1)).all()
|
||||
s = SocialSecurity(
|
||||
entity_id=data.get("entity_id") or 1,
|
||||
employee=employee,
|
||||
period=period,
|
||||
base_amount=data.get("base_amount") or 0,
|
||||
salary=data.get("salary"),
|
||||
company_amount=data.get("company_amount") or 0,
|
||||
personal_amount=data.get("personal_amount") or 0,
|
||||
remark=data.get("remark"),
|
||||
)
|
||||
_check_ss(db, s, all_records)
|
||||
db.add(s)
|
||||
db.commit()
|
||||
db.refresh(s)
|
||||
return {"message": "社保记录已创建", "data": _ss_to_dict(s)}
|
||||
|
||||
|
||||
@router.put("/ss/{ss_id}")
|
||||
def update_ss_record(
|
||||
ss_id: int,
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""更新社保记录(自动重新比对)"""
|
||||
s = db.query(SocialSecurity).filter(SocialSecurity.id == ss_id).first()
|
||||
if not s:
|
||||
raise HTTPException(404, "社保记录不存在")
|
||||
if "employee" in data:
|
||||
s.employee = data["employee"]
|
||||
if "period" in data:
|
||||
s.period = data["period"]
|
||||
if "base_amount" in data:
|
||||
s.base_amount = data["base_amount"] or 0
|
||||
if "salary" in data:
|
||||
s.salary = data.get("salary")
|
||||
if "company_amount" in data:
|
||||
s.company_amount = data["company_amount"] or 0
|
||||
if "personal_amount" in data:
|
||||
s.personal_amount = data["personal_amount"] or 0
|
||||
if "remark" in data:
|
||||
s.remark = data.get("remark")
|
||||
all_records = db.query(SocialSecurity).filter(SocialSecurity.entity_id == s.entity_id).all()
|
||||
_check_ss(db, s, all_records)
|
||||
db.commit()
|
||||
db.refresh(s)
|
||||
return {"message": "社保记录已更新", "data": _ss_to_dict(s)}
|
||||
|
||||
|
||||
@router.delete("/ss/{ss_id}")
|
||||
def delete_ss_record(
|
||||
ss_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""删除社保记录"""
|
||||
s = db.query(SocialSecurity).filter(SocialSecurity.id == ss_id).first()
|
||||
if not s:
|
||||
raise HTTPException(404, "社保记录不存在")
|
||||
db.delete(s)
|
||||
db.commit()
|
||||
return {"message": "社保记录已删除"}
|
||||
|
||||
|
||||
@router.post("/ss/check")
|
||||
def batch_check_ss(
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""批量社保比对"""
|
||||
q = db.query(SocialSecurity)
|
||||
if entity_id:
|
||||
q = q.filter(SocialSecurity.entity_id == entity_id)
|
||||
records = q.all()
|
||||
for s in records:
|
||||
_check_ss(db, s, records)
|
||||
db.commit()
|
||||
abnormal = [s for s in records if s.check_status in ("alert", "warning")]
|
||||
return {
|
||||
"message": f"社保比对完成:{len(records)}条,异常{len(abnormal)}条",
|
||||
"total": len(records),
|
||||
"abnormal_count": len(abnormal),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ss/abnormal")
|
||||
def list_abnormal_ss(
|
||||
entity_id: int = Query(None),
|
||||
limit: int = Query(50),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""查询社保异常(alert/warning)"""
|
||||
q = db.query(SocialSecurity).filter(SocialSecurity.check_status.in_(["alert", "warning"]))
|
||||
if entity_id:
|
||||
q = q.filter(SocialSecurity.entity_id == entity_id)
|
||||
records = q.order_by(SocialSecurity.period.desc(), SocialSecurity.id.desc()).limit(limit).all()
|
||||
return {"data": [_ss_to_dict(s) for s in records], "total": len(records)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ④ 税务看板聚合
|
||||
# ============================================================
|
||||
|
||||
@router.get("/dashboard")
|
||||
def tax_dashboard(
|
||||
entity_id: int = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_auth),
|
||||
):
|
||||
"""税务合规看板聚合:税负趋势+行业对比 / 发票异常 / 社保异常"""
|
||||
eid = entity_id or 1
|
||||
|
||||
# 税负监控
|
||||
records = db.query(TaxRecord).filter(TaxRecord.entity_id == eid).order_by(TaxRecord.period.asc()).all()
|
||||
for t in records:
|
||||
_calc_burden(t)
|
||||
db.commit()
|
||||
trend_map: dict[str, dict] = {}
|
||||
for t in records:
|
||||
if t.tax_burden_rate is None:
|
||||
continue
|
||||
entry = trend_map.setdefault(t.period, {"period": t.period})
|
||||
entry[f"{t.tax_type}_rate"] = t.tax_burden_rate
|
||||
entry[f"{t.tax_type}_benchmark"] = TAX_BENCHMARKS.get(t.tax_type)
|
||||
burden_trend = sorted(trend_map.values(), key=lambda x: x["period"])
|
||||
latest_period = max((t.period for t in records), default=None)
|
||||
latest = next((x for x in burden_trend if x["period"] == latest_period), None)
|
||||
tax_alerts = [t for t in records if t.burden_status == "alert"]
|
||||
|
||||
# 发票
|
||||
invoices = db.query(InvoiceCheck).filter(InvoiceCheck.entity_id == eid).all()
|
||||
inv_status_count = {"pending": 0, "valid": 0, "invalid": 0, "warning": 0}
|
||||
for i in invoices:
|
||||
inv_status_count[i.check_status] = inv_status_count.get(i.check_status, 0) + 1
|
||||
inv_abnormal = (
|
||||
db.query(InvoiceCheck)
|
||||
.filter(InvoiceCheck.entity_id == eid, InvoiceCheck.check_status.in_(["invalid", "warning"]))
|
||||
.order_by(InvoiceCheck.id.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
# 社保
|
||||
ss_records = db.query(SocialSecurity).filter(SocialSecurity.entity_id == eid).all()
|
||||
ss_status_count = {"normal": 0, "warning": 0, "alert": 0}
|
||||
for s in ss_records:
|
||||
ss_status_count[s.check_status] = ss_status_count.get(s.check_status, 0) + 1
|
||||
ss_abnormal = (
|
||||
db.query(SocialSecurity)
|
||||
.filter(SocialSecurity.entity_id == eid, SocialSecurity.check_status.in_(["alert", "warning"]))
|
||||
.order_by(SocialSecurity.period.desc(), SocialSecurity.id.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"entity_id": eid,
|
||||
"burden": {
|
||||
"trend": burden_trend,
|
||||
"latest": latest,
|
||||
"benchmarks": TAX_BENCHMARKS,
|
||||
"alert_count": len(tax_alerts),
|
||||
"alerts": [_tax_to_dict(t) for t in tax_alerts[:10]],
|
||||
},
|
||||
"invoice": {
|
||||
"total": len(invoices),
|
||||
"status_count": inv_status_count,
|
||||
"abnormal": [_invoice_to_dict(i) for i in inv_abnormal],
|
||||
},
|
||||
"ss": {
|
||||
"total": len(ss_records),
|
||||
"status_count": ss_status_count,
|
||||
"abnormal": [_ss_to_dict(s) for s in ss_abnormal],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 演示数据(幂等)
|
||||
# ============================================================
|
||||
|
||||
@router.post("/demo-data")
|
||||
def seed_demo_data(
|
||||
entity_id: int = Query(1),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(require_role("ceo", "finance")),
|
||||
):
|
||||
"""生成税务合规演示数据(幂等:已存在期间+税种则跳过)"""
|
||||
eid = entity_id or 1
|
||||
created = {"tax": 0, "invoice": 0, "ss": 0}
|
||||
|
||||
# 税负记录: 2026-01 ~ 2026-07,其中02月增值税故意偏高触发预警
|
||||
tax_demo = [
|
||||
("2026-01", "vat", 52000, 48800, 1380000, 13),
|
||||
("2026-02", "vat", 69000, 66000, 1460000, 13),
|
||||
("2026-03", "vat", 41000, 39800, 1280000, 13),
|
||||
("2026-04", "vat", 47500, 46200, 1350000, 13),
|
||||
("2026-05", "vat", 53000, 51000, 1420000, 13),
|
||||
("2026-06", "vat", 49800, 48200, 1400000, 13),
|
||||
("2026-01", "income", 36000, 33500, 1380000, 25),
|
||||
("2026-02", "income", 38000, 35500, 1460000, 25),
|
||||
("2026-03", "income", 33000, 31000, 1280000, 25),
|
||||
("2026-04", "income", 35000, 32800, 1350000, 25),
|
||||
("2026-05", "income", 37000, 34800, 1420000, 25),
|
||||
("2026-06", "income", 36500, 34000, 1400000, 25),
|
||||
("2026-01", "surtax", 6240, 5900, 1380000, 12),
|
||||
("2026-02", "surtax", 8280, 7900, 1460000, 12),
|
||||
("2026-03", "surtax", 4920, 4700, 1280000, 12),
|
||||
("2026-04", "surtax", 5700, 5500, 1350000, 12),
|
||||
("2026-05", "surtax", 6360, 6100, 1420000, 12),
|
||||
("2026-06", "surtax", 5976, 5800, 1400000, 12),
|
||||
]
|
||||
for period, ttype, payable, paid, income, rate in tax_demo:
|
||||
exists = (
|
||||
db.query(TaxRecord)
|
||||
.filter(TaxRecord.entity_id == eid, TaxRecord.period == period, TaxRecord.tax_type == ttype)
|
||||
.first()
|
||||
)
|
||||
if exists:
|
||||
continue
|
||||
t = TaxRecord(entity_id=eid, period=period, tax_type=ttype, tax_payable=payable,
|
||||
tax_paid=paid, tax_rate=rate, income=income)
|
||||
_calc_burden(t)
|
||||
db.add(t)
|
||||
created["tax"] += 1
|
||||
|
||||
# 报销单(供发票"金额与报销单匹配"规则使用)
|
||||
reimb_demo = [
|
||||
{"reimb_no": "BX202606150001", "applicant": "张伟", "department": "采购部", "expense_type": "office",
|
||||
"title": "6月办公用品采购", "amount": 5600, "status": "approved"},
|
||||
{"reimb_no": "BX202606200002", "applicant": "李娜", "department": "市场部", "expense_type": "management",
|
||||
"title": "6月市场信息服务费", "amount": 3200, "status": "approved"},
|
||||
{"reimb_no": "BX202607050003", "applicant": "王强", "department": "供应链部", "expense_type": "office",
|
||||
"title": "7月供应链物流服务", "amount": 14000, "status": "pending"},
|
||||
]
|
||||
for d in reimb_demo:
|
||||
exists = db.query(ExpenseReimbursement).filter(ExpenseReimbursement.reimb_no == d["reimb_no"]).first()
|
||||
if exists:
|
||||
continue
|
||||
db.add(ExpenseReimbursement(**d))
|
||||
|
||||
# 发票: 正常2张(金额匹配) + 格式错误1张 + 金额不符1张 + 供应商与合同不符1张
|
||||
invoice_demo = [
|
||||
{"invoice_no": "9" * 33, "amount": 5600, "invoice_type": "vat", "invoice_date": "2026-06-15",
|
||||
"supplier": "北京云启科技有限公司", "reimb_no": "BX202606150001", "contract_no": "HT-2026-018"},
|
||||
{"invoice_no": "8" * 33, "amount": 3200, "invoice_type": "electronic", "invoice_date": "2026-06-20",
|
||||
"supplier": "上海数联信息服务有限公司", "reimb_no": "BX202606200002", "contract_no": "HT-2026-021"},
|
||||
{"invoice_no": "12345ABC", "amount": 1800, "invoice_type": "vat", "invoice_date": "2026-07-02",
|
||||
"supplier": "广州锐思咨询有限公司", "reimb_no": None, "contract_no": None},
|
||||
{"invoice_no": "7" * 33, "amount": 15000, "invoice_type": "vat", "invoice_date": "2026-07-05",
|
||||
"supplier": "深圳恒达供应链有限公司", "reimb_no": "BX202607050003", "contract_no": "HT-2026-030"},
|
||||
{"invoice_no": "6" * 33, "amount": 9800, "invoice_type": "vat", "invoice_date": "2026-07-08",
|
||||
"supplier": None, "reimb_no": None, "contract_no": "HT-2026-033"},
|
||||
]
|
||||
for d in invoice_demo:
|
||||
exists = (
|
||||
db.query(InvoiceCheck)
|
||||
.filter(InvoiceCheck.entity_id == eid, InvoiceCheck.invoice_no == d["invoice_no"])
|
||||
.first()
|
||||
)
|
||||
if exists:
|
||||
continue
|
||||
inv = InvoiceCheck(entity_id=eid, **d)
|
||||
_check_invoice(db, inv)
|
||||
db.add(inv)
|
||||
created["invoice"] += 1
|
||||
|
||||
# 社保: 正常3人×2月 + 基数不符1条 + 漏缴1条
|
||||
ss_demo = [
|
||||
{"employee": "张伟", "period": "2026-06", "base_amount": 12000, "salary": 12000, "company_amount": 2940, "personal_amount": 1248},
|
||||
{"employee": "张伟", "period": "2026-07", "base_amount": 12000, "salary": 12000, "company_amount": 2940, "personal_amount": 1248},
|
||||
{"employee": "李娜", "period": "2026-06", "base_amount": 18000, "salary": 18000, "company_amount": 4410, "personal_amount": 1872},
|
||||
{"employee": "李娜", "period": "2026-07", "base_amount": 18000, "salary": 18000, "company_amount": 4410, "personal_amount": 1872},
|
||||
{"employee": "王强", "period": "2026-06", "base_amount": 9000, "salary": 9000, "company_amount": 2205, "personal_amount": 936},
|
||||
{"employee": "王强", "period": "2026-07", "base_amount": 9000, "salary": 9000, "company_amount": 2205, "personal_amount": 936},
|
||||
{"employee": "赵敏", "period": "2026-06", "base_amount": 5000, "salary": 20000, "company_amount": 1225, "personal_amount": 520},
|
||||
{"employee": "孙磊", "period": "2026-05", "base_amount": 0, "salary": 15000, "company_amount": 0, "personal_amount": 0},
|
||||
]
|
||||
for d in ss_demo:
|
||||
exists = (
|
||||
db.query(SocialSecurity)
|
||||
.filter(SocialSecurity.entity_id == eid, SocialSecurity.employee == d["employee"],
|
||||
SocialSecurity.period == d["period"])
|
||||
.first()
|
||||
)
|
||||
if exists:
|
||||
continue
|
||||
s = SocialSecurity(entity_id=eid, **d)
|
||||
all_records = db.query(SocialSecurity).filter(SocialSecurity.entity_id == eid).all()
|
||||
_check_ss(db, s, all_records)
|
||||
db.add(s)
|
||||
created["ss"] += 1
|
||||
db.commit()
|
||||
return {"message": "演示数据生成完成", "created": created}
|
||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from dotenv import load_dotenv
|
||||
from app.database import init_db
|
||||
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results, expenses, cash
|
||||
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results, expenses, cash, tax_compliance
|
||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||
from scripts.erp_sync import run_sync as run_erp_sync
|
||||
from app.auth_middleware import require_auth
|
||||
@@ -75,6 +75,7 @@ app.include_router(bot_iron_law.router)
|
||||
app.include_router(analysis_results.router)
|
||||
app.include_router(expenses.router)
|
||||
app.include_router(cash.router)
|
||||
app.include_router(tax_compliance.router)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
@@ -592,3 +592,66 @@ class ExpenseReimbursement(Base):
|
||||
created_by = Column(String(100), nullable=True, comment="提交人")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 税务合规智能体 (2026-08)
|
||||
# ① 税负监控 ② 发票校验 ③ 社保比对
|
||||
# ============================================================
|
||||
|
||||
class TaxRecord(Base):
|
||||
"""税务记录 — 税负监控:应纳税额/实缴额/税负率 vs 行业基准"""
|
||||
__tablename__ = "tax_records"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, comment="企业ID")
|
||||
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
|
||||
tax_type = Column(String(30), nullable=False, comment="税种: vat增值税/income所得税/surtax附加税")
|
||||
tax_payable = Column(Float, default=0, comment="应纳税额")
|
||||
tax_paid = Column(Float, default=0, comment="实缴税额")
|
||||
tax_rate = Column(Float, nullable=True, comment="适用税率 %")
|
||||
income = Column(Float, default=0, comment="当期收入/计税收入(税负率分母)")
|
||||
tax_burden_rate = Column(Float, nullable=True, comment="税负率 % = 实缴税额/收入×100")
|
||||
burden_status = Column(String(20), default="normal", comment="normal正常/warning超基准±20%内/alert超基准±20%")
|
||||
warning_msg = Column(String(500), nullable=True, comment="预警信息")
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class InvoiceCheck(Base):
|
||||
"""发票校验 — 录入后按规则自动校验:号码格式/金额匹配报销单/供应商匹配合同"""
|
||||
__tablename__ = "invoice_check"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, comment="企业ID")
|
||||
invoice_no = Column(String(50), nullable=False, comment="发票号码")
|
||||
amount = Column(Float, nullable=False, comment="发票金额")
|
||||
invoice_type = Column(String(30), default="vat", comment="发票类型: vat专用/vat普通/electronic电子/other其他")
|
||||
invoice_date = Column(DateTime, nullable=True, comment="开票日期")
|
||||
supplier = Column(String(200), nullable=True, comment="供应商名称")
|
||||
reimb_no = Column(String(50), nullable=True, comment="关联报销单号")
|
||||
contract_no = Column(String(50), nullable=True, comment="关联合同编号")
|
||||
check_status = Column(String(20), default="pending", comment="校验状态: pending待校验/valid通过/invalid异常/warning提醒")
|
||||
check_result = Column(JSON, nullable=True, comment="校验明细: [{rule, passed, message}]")
|
||||
check_reason = Column(String(1000), nullable=True, comment="异常原因汇总")
|
||||
checked_at = Column(DateTime, nullable=True, comment="校验时间")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class SocialSecurity(Base):
|
||||
"""社保缴费记录 — 社保比对:基数与工资匹配/单位缴纳比例/漏缴提醒"""
|
||||
__tablename__ = "social_security"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, comment="企业ID")
|
||||
employee = Column(String(100), nullable=False, comment="人员姓名")
|
||||
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
|
||||
base_amount = Column(Float, default=0, comment="缴费基数")
|
||||
salary = Column(Float, nullable=True, comment="申报工资")
|
||||
company_amount = Column(Float, default=0, comment="单位缴纳金额")
|
||||
personal_amount = Column(Float, default=0, comment="个人缴纳金额")
|
||||
company_rate = Column(Float, nullable=True, comment="单位缴纳比例 % (养老16%+医疗8%+失业0.5%≈24.5%)")
|
||||
check_status = Column(String(20), default="normal", comment="normal正常/warning基数或比例异常/alert漏缴")
|
||||
warning_msg = Column(String(500), nullable=True, comment="异常提醒: 漏缴/基数不符/比例异常")
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
Reference in New Issue
Block a user