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

350
scripts/db/connection.py Normal file
View File

@@ -0,0 +1,350 @@
"""SQLite 连接与幂等初始化(票据合规预审助手)。
业务表 5 张:
- invoice_records(发票登记,只读,预审自动写入)
- prereview_reports(初审意见,结论只读;查验回填走 mark-verified)
- expense_standards(经费标准库,可编辑)
- taxid_whitelist(税号白名单,可编辑)
- holiday_calendar(节假日,可编辑)
表间禁止 SQLite 外键,仅存业务 id(见 references/SCHEMA.md)。
"""
from __future__ import annotations
import sqlite3
from db.display_metadata import (
init_display_metadata,
seed_column_metadata_batch,
upsert_table_metadata,
)
from db.seed_data import seed_all
from db.timestamp_columns import init_updated_at_trigger
from util.runtime_paths import get_db_path
def get_conn() -> sqlite3.Connection:
return sqlite3.connect(get_db_path())
_INVOICE_RECORDS_DDL = """
CREATE TABLE IF NOT EXISTS invoice_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL,
invoice_type TEXT,
invoice_code TEXT,
invoice_number TEXT NOT NULL,
issue_date TEXT,
amount REAL,
amount_in_words TEXT,
tax_rate REAL,
buyer_name TEXT,
buyer_tax_no TEXT,
seller_name TEXT,
seller_tax_no TEXT,
expense_category TEXT,
city TEXT,
days INTEGER,
purpose TEXT,
project_no TEXT,
trip_context_json TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_INVOICE_RECORDS_INDEX = """
CREATE INDEX IF NOT EXISTS idx_invoice_records_batch
ON invoice_records(batch_no)
"""
_PREREVIEW_REPORTS_DDL = """
CREATE TABLE IF NOT EXISTS prereview_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL,
invoice_id INTEGER,
invoice_number TEXT,
seller_name TEXT,
amount REAL,
verdict TEXT NOT NULL,
pass_json TEXT,
warn_json TEXT,
reject_json TEXT,
summary TEXT,
verify_status TEXT NOT NULL DEFAULT '待查验',
verify_note TEXT,
verify_updated_at INTEGER,
report_file TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_PREREVIEW_REPORTS_INDEX = """
CREATE INDEX IF NOT EXISTS idx_prereview_reports_batch
ON prereview_reports(batch_no)
"""
_EXPENSE_STANDARDS_DDL = """
CREATE TABLE IF NOT EXISTS expense_standards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
city_tier TEXT NOT NULL,
city_example TEXT,
staff_title TEXT NOT NULL,
max_amount REAL NOT NULL,
unit TEXT,
basis_doc TEXT,
clause TEXT,
effective_date INTEGER,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
UNIQUE (category, city_tier, staff_title)
)
"""
_TAXID_WHITELIST_DDL = """
CREATE TABLE IF NOT EXISTS taxid_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tax_no TEXT NOT NULL UNIQUE,
org_name TEXT,
note TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_HOLIDAY_CALENDAR_DDL = """
CREATE TABLE IF NOT EXISTS holiday_calendar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
holiday_date TEXT NOT NULL UNIQUE,
holiday_name TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)
"""
_TS_COLUMN_NAMES = {"created_at", "updated_at", "effective_date", "verify_updated_at"}
def _col(column_name: str, display_name: str, description: str, **extra: object) -> dict[str, object]:
base: dict[str, object] = {
"column_name": column_name,
"display_name": display_name,
"description": description,
"visible": 1,
"searchable": 0,
"editable": 0,
}
base.update(extra)
if column_name in _TS_COLUMN_NAMES:
base["display_type"] = "datetime_unix_seconds"
return base
_INVOICE_RECORDS_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("batch_no", "预审批次号", "同一次预审请求的批次号,同批多张发票共享。", searchable=1),
_col("invoice_type", "发票类型", "如增值税电子普通发票、增值税专用发票、出租车票等。", searchable=1),
_col("invoice_code", "发票代码", "票面发票代码(数电票可能为空)。", searchable=1),
_col("invoice_number", "发票号码", "票面发票号码。", searchable=1),
_col("issue_date", "开票日期", "票面开票日期(YYYY-MM-DD)。", searchable=1),
_col("amount", "金额", "票面价税合计或金额(元)。", searchable=1, display_type="number"),
_col("amount_in_words", "大写金额", "票面大写金额(默认隐藏)。", visible=0),
_col("tax_rate", "税率", "票面税率(小数,如 0.06 表示 6%)。", display_type="number"),
_col("buyer_name", "购买方名称", "票面购买方全称。", searchable=1),
_col("buyer_tax_no", "购买方税号", "购买方统一社会信用代码,用于白名单比对。", searchable=1),
_col("seller_name", "销售方名称", "票面销售方全称。", searchable=1),
_col("seller_tax_no", "销售方税号", "销售方统一社会信用代码,用于集中开票风控。", searchable=1),
_col("expense_category", "费用类别", "报销费用类别,如住宿费、交通费、会议费。", searchable=1),
_col("city", "消费城市", "发票对应消费城市(住宿/交通判断用)。", searchable=1),
_col("days", "天数", "该发票覆盖的天数(住宿费核算用,缺省按 1 天)。", searchable=1, display_type="number"),
_col("purpose", "报销事由", "用户填报的报销事由文字。", searchable=1),
_col("project_no", "预算项目编号", "系统带入的预算项目编号。", searchable=1),
_col("trip_context_json", "差旅上下文", "出差起止、目的地、职级等上下文(JSON)。", visible=0, display_type="textarea"),
_col("created_at", "创建时间", "记录入库时间(Unix 秒级时间戳,自动生成)。"),
)
_PREREVIEW_REPORTS_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "报告编号", "主键,初审意见单编号。", searchable=1),
_col("batch_no", "预审批次号", "同一次预审请求的批次号。", searchable=1),
_col("invoice_id", "发票编号", "关联发票登记表主键。", searchable=1),
_col("invoice_number", "发票号码", "本报告对应的发票号码。", searchable=1),
_col("seller_name", "销售方名称", "本报告对应的销售方。", searchable=1),
_col("amount", "金额", "本报告对应的票面金额(元)。", searchable=1, display_type="number"),
_col(
"verdict",
"预审结论",
"通过 / 待确认 / 驳回。",
searchable=1,
options_json='[{"value":"通过","label":"✅ 通过"},{"value":"待确认","label":"⚠️ 待确认"},{"value":"驳回","label":"❌ 驳回"}]',
),
_col("pass_json", "通过项", "通过项清单(JSON 数组)。", visible=0, display_type="textarea"),
_col("warn_json", "待确认项", "待确认项清单(JSON 数组,附规则引用)。", visible=0, display_type="textarea"),
_col("reject_json", "驳回项", "驳回项清单(JSON 数组,附违反条款)。", visible=0, display_type="textarea"),
_col("summary", "结论摘要", "结论一句话摘要。", searchable=1),
_col(
"verify_status",
"真伪查验",
"待查验 / 已查验通过 / 已查验不符。",
searchable=1,
options_json='[{"value":"待查验","label":"待查验"},{"value":"已查验通过","label":"已查验通过"},{"value":"已查验不符","label":"已查验不符"}]',
),
_col("verify_note", "查验备注", "人工查验回填的备注。", searchable=1),
_col("verify_updated_at", "查验回填时间", "最近一次人工查验回填时间(Unix 秒级)。"),
_col("report_file", "意见单文件", "《初审意见单》HTML 文件路径。", visible=0),
_col("created_at", "创建时间", "报告创建时间(Unix 秒级时间戳,自动生成)。"),
)
_EXPENSE_STANDARDS_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("category", "费用类别", "住宿费 / 交通费等。", searchable=1),
_col("city_tier", "城市等级", "一线城市 / 省会及计划单列市 / 其他地区 / 通用。", searchable=1),
_col("city_example", "城市示例", "该等级覆盖的城市(逗号分隔),用于城市归档。", searchable=1, editable=1),
_col("staff_title", "职级", "如 副高及以上 / 中级及以下。", searchable=1),
_col("max_amount", "标准值", "住宿费为上限金额(元);交通费为席位等级代码。", searchable=1, editable=1, display_type="number"),
_col("unit", "单位", "如 元/人/天。", searchable=1, editable=1),
_col("basis_doc", "依据文件", "标准出处文件名,如《差旅费管理办法》。", searchable=1, editable=1),
_col("clause", "条款", "标准出处条款号,如 第十条。", searchable=1, editable=1),
_col("effective_date", "生效日期", "标准生效日期(Unix 秒级时间戳)。", searchable=1, editable=1),
_col("created_at", "创建时间", "记录创建时间(Unix 秒级,自动生成)。"),
_col("updated_at", "更新时间", "记录最后更新时间(Unix 秒级,自动维护)。"),
)
_TAXID_WHITELIST_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("tax_no", "税号", "购买方统一社会信用代码(唯一)。", searchable=1),
_col("org_name", "单位名称", "税号对应单位全称。", searchable=1, editable=1),
_col("note", "备注", "维护说明。", searchable=1, editable=1),
_col("created_at", "创建时间", "记录创建时间(Unix 秒级,自动生成)。"),
_col("updated_at", "更新时间", "记录最后更新时间(Unix 秒级,自动维护)。"),
)
_HOLIDAY_CALENDAR_COLUMNS: tuple[dict[str, object], ...] = (
_col("id", "编号", "主键", searchable=1),
_col("holiday_date", "日期", "节假日日期(YYYY-MM-DD,唯一)。", searchable=1),
_col("holiday_name", "节假日名称", "如 春节、国庆节。", searchable=1, editable=1),
_col("created_at", "创建时间", "记录创建时间(Unix 秒级,自动生成)。"),
_col("updated_at", "更新时间", "记录最后更新时间(Unix 秒级,自动维护)。"),
)
def _init_invoice_records_metadata(cursor) -> None:
cursor.execute(_INVOICE_RECORDS_DDL)
cursor.execute(_INVOICE_RECORDS_INDEX)
upsert_table_metadata(
cursor,
table_name="invoice_records",
display_name="发票登记",
description="每次预审自动写入的发票主档,含票面要素与报销上下文。",
grid_help="按批次号或开票日期查看;本表由预审自动写入,请勿手工修改。",
visible=1,
readonly=1,
default_sort_json='[{"column":"id","direction":"desc"}]',
default_visible_columns_json='["id","batch_no","invoice_type","invoice_number","issue_date","amount","seller_name","expense_category","project_no"]',
)
seed_column_metadata_batch(
cursor,
table_name="invoice_records",
columns=_INVOICE_RECORDS_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _INVOICE_RECORDS_COLUMNS],
)
def _init_prereview_reports_metadata(cursor) -> None:
cursor.execute(_PREREVIEW_REPORTS_DDL)
cursor.execute(_PREREVIEW_REPORTS_INDEX)
upsert_table_metadata(
cursor,
table_name="prereview_reports",
display_name="初审意见",
description="每张发票的预审结论与红黄绿灯清单;人工查验结果在行级回填。",
grid_help="“待确认/驳回”的行请在行级按钮回填人工查验结果;HTML 意见单可用顶栏按钮重建。",
visible=1,
readonly=1,
default_sort_json='[{"column":"id","direction":"desc"}]',
default_visible_columns_json='["id","invoice_number","seller_name","amount","verdict","verify_status","summary","created_at"]',
)
seed_column_metadata_batch(
cursor,
table_name="prereview_reports",
columns=_PREREVIEW_REPORTS_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _PREREVIEW_REPORTS_COLUMNS],
)
def _init_expense_standards_metadata(cursor) -> None:
cursor.execute(_EXPENSE_STANDARDS_DDL)
init_updated_at_trigger(cursor, "expense_standards")
upsert_table_metadata(
cursor,
table_name="expense_standards",
display_name="经费标准库",
description="住宿费上限与交通席位等级标准(城市等级 × 职级),预审硬约束依据。",
grid_help="数值为示例种子,请以学校现行管理办法为准;可直接编辑标准值/单位/条款。",
visible=1,
readonly=0,
)
seed_column_metadata_batch(
cursor,
table_name="expense_standards",
columns=_EXPENSE_STANDARDS_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _EXPENSE_STANDARDS_COLUMNS],
)
def _init_taxid_whitelist_metadata(cursor) -> None:
cursor.execute(_TAXID_WHITELIST_DDL)
init_updated_at_trigger(cursor, "taxid_whitelist")
upsert_table_metadata(
cursor,
table_name="taxid_whitelist",
display_name="税号白名单",
description="准予报销的购买方税号清单;预审时逐张比对,不在名单内即驳回。",
grid_help="顶栏按钮新增税号;行级按钮可删除错误条目。",
visible=1,
readonly=0,
)
seed_column_metadata_batch(
cursor,
table_name="taxid_whitelist",
columns=_TAXID_WHITELIST_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _TAXID_WHITELIST_COLUMNS],
)
def _init_holiday_calendar_metadata(cursor) -> None:
cursor.execute(_HOLIDAY_CALENDAR_DDL)
init_updated_at_trigger(cursor, "holiday_calendar")
upsert_table_metadata(
cursor,
table_name="holiday_calendar",
display_name="节假日",
description="节假日开票风控所用的日历数据(周末之外的法定节假日)。",
grid_help="以国务院办公厅当年通知为准,请管理员按通知维护连休范围。",
visible=1,
readonly=0,
)
seed_column_metadata_batch(
cursor,
table_name="holiday_calendar",
columns=_HOLIDAY_CALENDAR_COLUMNS,
valid_column_names=[str(c["column_name"]) for c in _HOLIDAY_CALENDAR_COLUMNS],
)
def init_db() -> dict[str, int]:
"""幂等创建/迁移本地 SQLite、数据管理元数据与业务种子,返回种子统计。"""
conn = get_conn()
try:
cur = conn.cursor()
init_display_metadata(cur)
_init_invoice_records_metadata(cur)
_init_prereview_reports_metadata(cur)
_init_expense_standards_metadata(cur)
_init_taxid_whitelist_metadata(cur)
_init_holiday_calendar_metadata(cur)
seeded = seed_all(cur)
conn.commit()
return seeded
finally:
conn.close()