"""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()