Files
verify-invoice-default/scripts/service/standards_service.py

367 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""经费标准库 / 税号白名单 / 节假日 / 初审记录 的查询与维护。
预审内核只读本模块数据;维护操作(改标准、增删白名单、查验回填)在此实现。
"""
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,
}