367 lines
12 KiB
Python
367 lines
12 KiB
Python
"""经费标准库 / 税号白名单 / 节假日 / 初审记录 的查询与维护。
|
||
|
||
预审内核只读本模块数据;维护操作(改标准、增删白名单、查验回填)在此实现。
|
||
"""
|
||
|
||
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,
|
||
}
|