Files
verify-invoice-default/scripts/db/connection.py

351 lines
15 KiB
Python
Raw Permalink 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.
"""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()