feat: 票据合规预审助手首版 v1.0.0(verify-invoice-default)

This commit is contained in:
chenfuqiang
2026-09-13 13:48:32 +08:00
commit ddf8044dc0
207 changed files with 31090 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
"""经费标准库 / 税号白名单 / 节假日 / 初审记录 的查询与维护。
预审内核只读本模块数据;维护操作(改标准、增删白名单、查验回填)在此实现。
"""
from __future__ import annotations
import json
import sqlite3
from typing import Any, Optional
from db.connection import get_conn, init_db
from util.logging_config import get_skill_logger
from util.timeutil import now_unix
VERIFY_PENDING = "待查验"
VERIFY_PASS = "已查验通过"
VERIFY_FAIL = "已查验不符"
def _log():
try:
return get_skill_logger()
except RuntimeError:
return None
def list_standards(category: Optional[str] = None) -> list[dict[str, Any]]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
if category:
cur.execute(
"SELECT id, category, city_tier, city_example, staff_title, max_amount, unit, basis_doc, clause, effective_date FROM expense_standards WHERE category = ? ORDER BY category, city_tier, staff_title",
(category,),
)
else:
cur.execute(
"SELECT id, category, city_tier, city_example, staff_title, max_amount, unit, basis_doc, clause, effective_date FROM expense_standards ORDER BY category, city_tier, staff_title"
)
rows = cur.fetchall()
finally:
conn.close()
return [
{
"id": r[0],
"category": r[1],
"city_tier": r[2],
"city_example": r[3],
"staff_title": r[4],
"max_amount": r[5],
"unit": r[6],
"basis_doc": r[7],
"clause": r[8],
"effective_date": r[9],
}
for r in rows
]
def upsert_standard(
*,
category: str,
city_tier: str,
staff_title: str,
max_amount: float,
unit: str = "",
basis_doc: str = "",
clause: str = "",
city_example: str = "",
) -> dict[str, Any]:
"""按 (类别, 城市等级, 职级) 幂等更新标准值。"""
if not category or not city_tier or not staff_title:
raise ValueError("类别 / 城市等级 / 职级 均不能为空")
if max_amount <= 0:
raise ValueError("标准值必须大于 0")
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute(
"SELECT id FROM expense_standards WHERE category = ? AND city_tier = ? AND staff_title = ?",
(category, city_tier, staff_title),
)
row = cur.fetchone()
if row:
cur.execute(
"""
UPDATE expense_standards
SET max_amount = ?, unit = ?, basis_doc = ?, clause = ?,
city_example = COALESCE(NULLIF(?, ''), city_example), updated_at = ?
WHERE id = ?
""",
(max_amount, unit, basis_doc, clause, city_example, now_unix(), int(row[0])),
)
standard_id = int(row[0])
action = "updated"
else:
cur.execute(
"""
INSERT INTO expense_standards
(category, city_tier, city_example, staff_title, max_amount, unit, basis_doc, clause, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(category, city_tier, city_example, staff_title, max_amount, unit, basis_doc, clause, now_unix(), now_unix()),
)
standard_id = int(cur.lastrowid)
action = "created"
conn.commit()
finally:
conn.close()
log = _log()
if log:
log.info(
"standard_upserted standard_id=%s action=%s category=%s tier=%s title=%s stage=standard",
standard_id,
action,
category,
city_tier,
staff_title,
)
return {"ok": True, "id": standard_id, "action": action, "category": category, "city_tier": city_tier, "staff_title": staff_title, "max_amount": max_amount}
def list_whitelist() -> list[dict[str, Any]]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT id, tax_no, org_name, note, created_at FROM taxid_whitelist ORDER BY id")
rows = cur.fetchall()
finally:
conn.close()
return [{"id": r[0], "tax_no": r[1], "org_name": r[2], "note": r[3], "created_at": r[4]} for r in rows]
def add_whitelist(tax_no: str, org_name: str, note: str = "") -> dict[str, Any]:
tax_no = (tax_no or "").strip()
if not tax_no:
raise ValueError("税号不能为空")
if not org_name:
raise ValueError("单位名称不能为空")
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT id FROM taxid_whitelist WHERE tax_no = ?", (tax_no,))
if cur.fetchone():
raise ValueError(f"税号已存在:{tax_no}")
cur.execute(
"INSERT INTO taxid_whitelist (tax_no, org_name, note, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(tax_no, org_name, note, now_unix(), now_unix()),
)
whitelist_id = int(cur.lastrowid)
conn.commit()
finally:
conn.close()
return {"ok": True, "id": whitelist_id, "tax_no": tax_no, "org_name": org_name}
def remove_whitelist(tax_no: str) -> dict[str, Any]:
tax_no = (tax_no or "").strip()
if not tax_no:
raise ValueError("税号不能为空")
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("DELETE FROM taxid_whitelist WHERE tax_no = ?", (tax_no,))
deleted = cur.rowcount
conn.commit()
finally:
conn.close()
if not deleted:
raise ValueError(f"白名单中不存在该税号:{tax_no}")
return {"ok": True, "removed": tax_no}
def list_holidays(year: Optional[str] = None) -> list[dict[str, Any]]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
if year:
cur.execute(
"SELECT id, holiday_date, holiday_name FROM holiday_calendar WHERE holiday_date LIKE ? ORDER BY holiday_date",
(f"{year}%",),
)
else:
cur.execute("SELECT id, holiday_date, holiday_name FROM holiday_calendar ORDER BY holiday_date")
rows = cur.fetchall()
finally:
conn.close()
return [{"id": r[0], "holiday_date": r[1], "holiday_name": r[2]} for r in rows]
def list_reports(limit: int = 20, verdict: Optional[str] = None) -> list[dict[str, Any]]:
init_db()
limit = max(1, min(int(limit), 200))
conn = get_conn()
try:
cur = conn.cursor()
if verdict:
cur.execute(
"""
SELECT id, batch_no, invoice_number, seller_name, amount, verdict, verify_status, summary, report_file, created_at
FROM prereview_reports WHERE verdict = ? ORDER BY id DESC LIMIT ?
""",
(verdict, limit),
)
else:
cur.execute(
"""
SELECT id, batch_no, invoice_number, seller_name, amount, verdict, verify_status, summary, report_file, created_at
FROM prereview_reports ORDER BY id DESC LIMIT ?
""",
(limit,),
)
rows = cur.fetchall()
finally:
conn.close()
return [
{
"id": r[0],
"batch_no": r[1],
"invoice_number": r[2],
"seller_name": r[3],
"amount": r[4],
"verdict": r[5],
"verify_status": r[6],
"summary": r[7],
"report_file": r[8],
"created_at": r[9],
}
for r in rows
]
def get_report(report_id: int) -> dict[str, Any]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT * FROM prereview_reports WHERE id = ?", (report_id,))
row = cur.fetchone()
if not row:
raise ValueError(f"初审报告不存在:{report_id}")
columns = [d[0] for d in cur.description]
report = dict(zip(columns, row))
invoice: dict[str, Any] = {}
if report.get("invoice_id"):
cur.execute("SELECT * FROM invoice_records WHERE id = ?", (report["invoice_id"],))
irow = cur.fetchone()
if irow:
icolumns = [d[0] for d in cur.description]
invoice = dict(zip(icolumns, irow))
finally:
conn.close()
for key in ("pass_json", "warn_json", "reject_json"):
try:
report[key] = json.loads(report.get(key) or "[]")
except (TypeError, ValueError):
report[key] = []
report["invoice"] = invoice
return report
def mark_verified(report_id: int, result: str, note: str = "") -> dict[str, Any]:
"""回填人工查验结果;查验“不符”则结论升级为驳回。"""
result = (result or "").strip().lower()
if result not in ("pass", "fail"):
raise ValueError("查验结果只接受 pass / fail")
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT id, verdict, reject_json FROM prereview_reports WHERE id = ?", (report_id,))
row = cur.fetchone()
if not row:
raise ValueError(f"初审报告不存在:{report_id}")
_report_id, verdict, reject_json = int(row[0]), str(row[1]), row[2] or "[]"
if result == "pass":
verify_status = VERIFY_PASS
else:
verify_status = VERIFY_FAIL
try:
rejects = json.loads(reject_json)
except (TypeError, ValueError):
rejects = []
rejects.append(
{
"rule_id": "V1",
"name": "发票真伪",
"level": "reject",
"detail": "人工查验回填:票面信息与查验平台不符,判定为问题票据。",
"rule_ref": "全国增值税发票查验平台(inv-veri.chinatax.gov.cn)",
}
)
reject_json = json.dumps(rejects, ensure_ascii=False)
new_verdict = "驳回" if result == "fail" else verdict
cur.execute(
"""
UPDATE prereview_reports
SET verify_status = ?, verify_note = ?, verify_updated_at = ?,
verdict = ?, reject_json = ?
WHERE id = ?
""",
(verify_status, note, now_unix(), new_verdict, reject_json, report_id),
)
conn.commit()
finally:
conn.close()
log = _log()
if log:
log.info(
"verify_marked report_id=%s result=%s verdict=%s stage=verify",
report_id,
result,
new_verdict,
)
return {
"ok": True,
"report_id": report_id,
"verify_status": verify_status,
"verdict": new_verdict,
"note": note,
}
def stats() -> dict[str, Any]:
init_db()
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT verdict, COUNT(*) FROM prereview_reports GROUP BY verdict")
by_verdict = {str(r[0]): int(r[1]) for r in cur.fetchall()}
cur.execute("SELECT COUNT(*) FROM invoice_records")
invoice_total = int(cur.fetchone()[0])
cur.execute("SELECT COUNT(*) FROM taxid_whitelist")
whitelist_total = int(cur.fetchone()[0])
cur.execute("SELECT COUNT(*) FROM expense_standards")
standards_total = int(cur.fetchone()[0])
cur.execute(
"""
SELECT seller_name, COUNT(*) AS cnt FROM invoice_records
WHERE seller_name != ''
GROUP BY seller_name ORDER BY cnt DESC LIMIT 5
"""
)
top_sellers = [{"seller_name": r[0], "count": int(r[1])} for r in cur.fetchall()]
finally:
conn.close()
total_reports = sum(by_verdict.values())
return {
"ok": True,
"invoice_total": invoice_total,
"report_total": total_reports,
"verdict_counts": by_verdict,
"reject_rate": round(by_verdict.get("驳回", 0) / total_reports, 4) if total_reports else 0.0,
"whitelist_total": whitelist_total,
"standards_total": standards_total,
"top_sellers": top_sellers,
}