feat: 票据合规预审助手首版 v1.0.0(verify-invoice-default)
This commit is contained in:
366
scripts/service/standards_service.py
Normal file
366
scripts/service/standards_service.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user