diff --git a/document/development/agent/agent plan/02_semantic_ontology.md b/document/development/agent/agent plan/02_semantic_ontology.md index 9fefedd..ee6ecf0 100644 --- a/document/development/agent/agent plan/02_semantic_ontology.md +++ b/document/development/agent/agent plan/02_semantic_ontology.md @@ -1,457 +1,457 @@ -# 语义本体协议设计 - -## 1. 定位 - -语义本体协议是用户问题、定时任务、规则中心、MCP、数据库查询和 Agent 之间的统一中间层。 - -它解决的问题是: - -- 用户到底在问哪个业务域? -- 这属于什么场景? -- 用户想做什么? -- 问题中涉及哪些对象? -- 有没有时间、金额、状态、部门等过滤条件? -- 是否涉及风险? -- 下一步应该查知识库、查数据库、跑规则、调 MCP,还是追问? - -## 2. 第一版核心字段 - -第一版建议只强制落 8 个字段。 - -```json -{ - "domain": "", - "scenario": "", - "intent": "", - "entities": [], - "time_range": {}, - "constraints": {}, - "risk_signals": [], - "next_step": "" -} -``` - -### 2.1 domain - -一级业务域。 - -建议枚举: - -```text -reimbursement -accounts_receivable -accounts_payable -general_finance -system_operation -``` - -含义: - -- `reimbursement`:报销、差旅、发票、补件。 -- `accounts_receivable`:应收账款、客户开票、收款、账龄。 -- `accounts_payable`:应付账款、供应商发票、付款、对账。 -- `general_finance`:通用财务知识、制度、统计。 -- `system_operation`:系统巡检、任务运行、规则维护、MCP 健康检查。 - -### 2.2 scenario - -细分场景。 - -报销: - -```text -travel_reimbursement -daily_expense -invoice_validation -attachment_review -policy_overrun -reimbursement_audit -``` - -应收: - -```text -customer_invoice -collection_followup -receivable_aging -payment_matching -bad_debt_risk -contract_receivable -``` - -应付: - -```text -vendor_invoice -payment_request -payable_aging -vendor_reconciliation -invoice_matching -cash_outflow_forecast -``` - -系统运营: - -```text -daily_risk_scan -daily_finance_statistics -knowledge_accumulation -mcp_health_check -rule_quality_review -``` - -### 2.3 intent - -用户或任务的意图。 - -建议枚举: - -```text -query -explain -create -validate -summarize -reconcile -monitor -predict -remind -generate -optimize -``` - -### 2.4 entities - -识别出的业务对象。 - -统一结构: - -```json -{ - "type": "invoice", - "value": "INV-202605001", - "normalized_value": "INV-202605001", - "role": "target", - "confidence": 0.92 -} -``` - -常见实体: - -```text -employee -department -customer -vendor -invoice -contract -reimbursement_request -payment_order -receipt -bank_transaction -cost_center -project -policy -approval_node -rule -task -``` - -### 2.5 time_range - -统一描述时间。 - -```json -{ - "raw": "上个月", - "start": "2026-04-01", - "end": "2026-04-30", - "granularity": "month" -} -``` - -Hermes 定时任务也使用同一字段。 - -例如每日风险巡检: - -```json -{ - "raw": "昨日", - "start": "2026-05-09", - "end": "2026-05-09", - "granularity": "day" -} -``` - -### 2.6 constraints - -查询、判断或执行条件。 - -```json -{ - "status": "overdue", - "aging_days": ">30", - "amount": { - "operator": ">", - "value": 50000, - "currency": "CNY" - }, - "department": "销售部", - "risk_level": ["medium", "high"] -} -``` - -### 2.7 risk_signals - -风险信号。 - -建议枚举: - -```text -duplicate_invoice -missing_attachment -policy_overrun -over_budget -overdue_receivable -bad_debt_risk -vendor_payment_risk -payment_mismatch -contract_mismatch -cashflow_pressure -mcp_unavailable -rule_quality_issue -``` - -### 2.8 next_step - -下一步动作。 - -建议枚举: - -```text -answer -ask_clarification -query_database -run_rule -call_mcp -search_knowledge -create_draft -create_task -generate_report -notify_user -escalate_to_human -``` - -## 3. 扩展字段 - -后续可以增加: - -```json -{ - "schema_version": "1.1", - "confidence": 0.86, - "ambiguity": [], - "missing_slots": [], - "required_capabilities": [], - "normalized_query": "", - "permission_scope": {}, - "audit_tags": [] -} -``` - -## 4. 混合语义解析架构 - -第一版可上线实现不应只依赖关键词和正则。 - -推荐采用: - -```text -输入上下文装配 - 用户文本 + 页面上下文 + 附件名称 + OCR/VLM 摘要 - ↓ -预抽取 - 时间、金额、单号、显式对象 - ↓ -LLM 结构化解析 - 输出 scenario / intent / entities / missing_slots / ambiguity - ↓ -Schema 校验 - JSON 解析、字段枚举、必填校验、类型归一化 - ↓ -规则兜底 - 模型失败、低置信度或字段缺失时回退到规则解析 - ↓ -澄清追问 - 低置信度、歧义、缺槽位时不允许直接查库 -``` - -设计原则: - -- 模型优先负责“理解意图和场景”。 -- 规则优先负责“校验、补全和兜底”。 -- 附件名称、OCR、VLM 结果只能作为证据,不等于已确认事实。 -- 所有语义输出都必须标记置信度和来源。 - -## 5. 推荐新增字段 - -为支持模型优先解析,建议在扩展字段中至少增加: - -```json -{ - "missing_slots": [], - "ambiguity": [], - "field_confidence": {}, - "field_source": {}, - "attachment_context": [], - "parse_strategy": "llm_primary_with_rule_fallback" -} -``` - -字段说明: - -- `missing_slots`:还缺哪些关键字段,例如费用类型、单据号、客户单位。 -- `ambiguity`:当前可能混淆的理解结果。 -- `field_confidence`:字段级置信度,而不是只给整体分数。 -- `field_source`:字段来自 `llm`、`rule`、`ocr`、`vlm` 还是 `user_context`。 -- `attachment_context`:本次可供语义解析使用的附件摘要。 -- `parse_strategy`:标记本次是模型主解析还是规则回退。 - -## 6. 叙述型财务输入 - -语义层必须支持“不是查询句”的自然叙述。 - -典型样例: - -```text -我今天去客户现场,招待了客户,花销了1000元 -我垫付了打车费和餐费,帮我看看怎么报 -上传了三张票,帮我整理成报销草稿 -``` - -这类输入不能默认识别成 `query`。 - -建议默认策略: - -- 优先识别为 `reimbursement` 域。 -- 场景优先落到 `daily_expense`、`travel_reimbursement` 或 `attachment_review`。 -- 意图优先落到 `create`、`generate` 或 `validate`。 -- 缺失关键字段时返回 `ask_clarification`,而不是直接查数据库。 - -## 7. 模糊短句与澄清规则 - -以下输入应优先追问: - -```text -我要报销 -这个为什么还没处理 -帮我看一下这个 -上传好了,下一步呢 -``` - -处理原则: - -- 不允许直接执行工具。 -- 不允许直接落到应收、应付查询。 -- 必须生成澄清问题。 -- 必须在审计中记录触发追问的原因。 - -扩展原则: - -- 先不要把所有字段都做成数据库列。 -- 语义结果建议存 JSONB。 -- 使用 `schema_version` 管理版本。 -- Orchestrator 只依赖稳定字段。 -- 新字段以可选方式加入,不影响老任务。 - -## 4. 示例 - -### 4.1 用户查询应收账龄 - -用户问: - -```text -上个月哪些客户应收逾期超过 30 天? -``` - -解析: - -```json -{ - "domain": "accounts_receivable", - "scenario": "receivable_aging", - "intent": "query", - "entities": [ - { - "type": "customer", - "value": "客户", - "role": "group_by" - } - ], - "time_range": { - "raw": "上个月", - "start": "2026-04-01", - "end": "2026-04-30", - "granularity": "month" - }, - "constraints": { - "aging_days": ">30", - "status": "overdue" - }, - "risk_signals": ["overdue_receivable"], - "next_step": "query_database" -} -``` - -### 4.2 用户解释发票拦截 - -用户问: - -```text -这张发票为什么报销被拦截? -``` - -解析: - -```json -{ - "domain": "reimbursement", - "scenario": "invoice_validation", - "intent": "explain", - "entities": [ - { - "type": "invoice", - "value": "这张发票", - "role": "target" - } - ], - "time_range": {}, - "constraints": {}, - "risk_signals": ["unknown"], - "next_step": "run_rule" -} -``` - -### 4.3 Hermes 每日风险巡检 - -任务配置: - -```json -{ - "domain": "reimbursement", - "scenario": "daily_risk_scan", - "intent": "monitor", - "entities": [], - "time_range": { - "raw": "昨日" - }, - "constraints": { - "risk_level": ["medium", "high"] - }, - "risk_signals": [ - "duplicate_invoice", - "missing_attachment", - "policy_overrun" - ], - "next_step": "run_rule" -} -``` +# 语义本体协议设计 + +## 1. 定位 + +语义本体协议是用户问题、定时任务、规则中心、MCP、数据库查询和 Agent 之间的统一中间层。 + +它解决的问题是: + +- 用户到底在问哪个业务域? +- 这属于什么场景? +- 用户想做什么? +- 问题中涉及哪些对象? +- 有没有时间、金额、状态、部门等过滤条件? +- 是否涉及风险? +- 下一步应该查知识库、查数据库、跑规则、调 MCP,还是追问? + +## 2. 第一版核心字段 + +第一版建议只强制落 8 个字段。 + +```json +{ + "domain": "", + "scenario": "", + "intent": "", + "entities": [], + "time_range": {}, + "constraints": {}, + "risk_signals": [], + "next_step": "" +} +``` + +### 2.1 domain + +一级业务域。 + +建议枚举: + +```text +reimbursement +accounts_receivable +accounts_payable +general_finance +system_operation +``` + +含义: + +- `reimbursement`:报销、差旅、发票、补件。 +- `accounts_receivable`:应收账款、客户开票、收款、账龄。 +- `accounts_payable`:应付账款、供应商发票、付款、对账。 +- `general_finance`:通用财务知识、制度、统计。 +- `system_operation`:系统巡检、任务运行、规则维护、MCP 健康检查。 + +### 2.2 scenario + +细分场景。 + +报销: + +```text +travel_reimbursement +daily_expense +invoice_validation +attachment_review +policy_overrun +reimbursement_audit +``` + +应收: + +```text +customer_invoice +collection_followup +receivable_aging +payment_matching +bad_debt_risk +contract_receivable +``` + +应付: + +```text +vendor_invoice +payment_request +payable_aging +vendor_reconciliation +invoice_matching +cash_outflow_forecast +``` + +系统运营: + +```text +daily_risk_scan +daily_finance_statistics +knowledge_accumulation +mcp_health_check +rule_quality_review +``` + +### 2.3 intent + +用户或任务的意图。 + +建议枚举: + +```text +query +explain +create +validate +summarize +reconcile +monitor +predict +remind +generate +optimize +``` + +### 2.4 entities + +识别出的业务对象。 + +统一结构: + +```json +{ + "type": "invoice", + "value": "INV-202605001", + "normalized_value": "INV-202605001", + "role": "target", + "confidence": 0.92 +} +``` + +常见实体: + +```text +employee +department +customer +vendor +invoice +contract +reimbursement_request +payment_order +receipt +bank_transaction +cost_center +project +policy +approval_node +rule +task +``` + +### 2.5 time_range + +统一描述时间。 + +```json +{ + "raw": "上个月", + "start": "2026-04-01", + "end": "2026-04-30", + "granularity": "month" +} +``` + +Hermes 定时任务也使用同一字段。 + +例如每日风险巡检: + +```json +{ + "raw": "昨日", + "start": "2026-05-09", + "end": "2026-05-09", + "granularity": "day" +} +``` + +### 2.6 constraints + +查询、判断或执行条件。 + +```json +{ + "status": "overdue", + "aging_days": ">30", + "amount": { + "operator": ">", + "value": 50000, + "currency": "CNY" + }, + "department": "销售部", + "risk_level": ["medium", "high"] +} +``` + +### 2.7 risk_signals + +风险信号。 + +建议枚举: + +```text +duplicate_invoice +missing_attachment +policy_overrun +over_budget +overdue_receivable +bad_debt_risk +vendor_payment_risk +payment_mismatch +contract_mismatch +cashflow_pressure +mcp_unavailable +rule_quality_issue +``` + +### 2.8 next_step + +下一步动作。 + +建议枚举: + +```text +answer +ask_clarification +query_database +run_rule +call_mcp +search_knowledge +create_draft +create_task +generate_report +notify_user +escalate_to_human +``` + +## 3. 扩展字段 + +后续可以增加: + +```json +{ + "schema_version": "1.1", + "confidence": 0.86, + "ambiguity": [], + "missing_slots": [], + "required_capabilities": [], + "normalized_query": "", + "permission_scope": {}, + "audit_tags": [] +} +``` + +## 4. 混合语义解析架构 + +第一版可上线实现不应只依赖关键词和正则。 + +推荐采用: + +```text +输入上下文装配 + 用户文本 + 页面上下文 + 附件名称 + OCR/VLM 摘要 + ↓ +预抽取 + 时间、金额、单号、显式对象 + ↓ +LLM 结构化解析 + 输出 scenario / intent / entities / missing_slots / ambiguity + ↓ +Schema 校验 + JSON 解析、字段枚举、必填校验、类型归一化 + ↓ +规则兜底 + 模型失败、低置信度或字段缺失时回退到规则解析 + ↓ +澄清追问 + 低置信度、歧义、缺槽位时不允许直接查库 +``` + +设计原则: + +- 模型优先负责“理解意图和场景”。 +- 规则优先负责“校验、补全和兜底”。 +- 附件名称、OCR、VLM 结果只能作为证据,不等于已确认事实。 +- 所有语义输出都必须标记置信度和来源。 + +## 5. 推荐新增字段 + +为支持模型优先解析,建议在扩展字段中至少增加: + +```json +{ + "missing_slots": [], + "ambiguity": [], + "field_confidence": {}, + "field_source": {}, + "attachment_context": [], + "parse_strategy": "llm_primary_with_rule_fallback" +} +``` + +字段说明: + +- `missing_slots`:还缺哪些关键字段,例如费用类型、单据号、客户单位。 +- `ambiguity`:当前可能混淆的理解结果。 +- `field_confidence`:字段级置信度,而不是只给整体分数。 +- `field_source`:字段来自 `llm`、`rule`、`ocr`、`vlm` 还是 `user_context`。 +- `attachment_context`:本次可供语义解析使用的附件摘要。 +- `parse_strategy`:标记本次是模型主解析还是规则回退。 + +## 6. 叙述型财务输入 + +语义层必须支持“不是查询句”的自然叙述。 + +典型样例: + +```text +我今天去客户现场,招待了客户,花销了1000元 +我垫付了打车费和餐费,帮我看看怎么报 +上传了三张票,帮我整理成报销草稿 +``` + +这类输入不能默认识别成 `query`。 + +建议默认策略: + +- 优先识别为 `reimbursement` 域。 +- 场景优先落到 `daily_expense`、`travel_reimbursement` 或 `attachment_review`。 +- 意图优先落到 `create`、`generate` 或 `validate`。 +- 缺失关键字段时返回 `ask_clarification`,而不是直接查数据库。 + +## 7. 模糊短句与澄清规则 + +以下输入应优先追问: + +```text +我要报销 +这个为什么还没处理 +帮我看一下这个 +上传好了,下一步呢 +``` + +处理原则: + +- 不允许直接执行工具。 +- 不允许直接落到应收、应付查询。 +- 必须生成澄清问题。 +- 必须在审计中记录触发追问的原因。 + +扩展原则: + +- 先不要把所有字段都做成数据库列。 +- 语义结果建议存 JSONB。 +- 使用 `schema_version` 管理版本。 +- Orchestrator 只依赖稳定字段。 +- 新字段以可选方式加入,不影响老任务。 + +## 4. 示例 + +### 4.1 用户查询应收账龄 + +用户问: + +```text +上个月哪些客户应收逾期超过 30 天? +``` + +解析: + +```json +{ + "domain": "accounts_receivable", + "scenario": "receivable_aging", + "intent": "query", + "entities": [ + { + "type": "customer", + "value": "客户", + "role": "group_by" + } + ], + "time_range": { + "raw": "上个月", + "start": "2026-04-01", + "end": "2026-04-30", + "granularity": "month" + }, + "constraints": { + "aging_days": ">30", + "status": "overdue" + }, + "risk_signals": ["overdue_receivable"], + "next_step": "query_database" +} +``` + +### 4.2 用户解释发票拦截 + +用户问: + +```text +这张发票为什么报销被拦截? +``` + +解析: + +```json +{ + "domain": "reimbursement", + "scenario": "invoice_validation", + "intent": "explain", + "entities": [ + { + "type": "invoice", + "value": "这张发票", + "role": "target" + } + ], + "time_range": {}, + "constraints": {}, + "risk_signals": ["unknown"], + "next_step": "run_rule" +} +``` + +### 4.3 Hermes 每日风险巡检 + +任务配置: + +```json +{ + "domain": "reimbursement", + "scenario": "daily_risk_scan", + "intent": "monitor", + "entities": [], + "time_range": { + "raw": "昨日" + }, + "constraints": { + "risk_level": ["medium", "high"] + }, + "risk_signals": [ + "duplicate_invoice", + "missing_attachment", + "policy_overrun" + ], + "next_step": "run_rule" +} +``` diff --git a/server/rules/finance-rules/公司差旅费报销规则.xlsx b/server/rules/finance-rules/公司差旅费报销规则.xlsx index 3d00070..cca003b 100644 Binary files a/server/rules/finance-rules/公司差旅费报销规则.xlsx and b/server/rules/finance-rules/公司差旅费报销规则.xlsx differ diff --git a/server/rules/risk-rules/risk.expense.consecutive_transport_receipts.json b/server/rules/risk-rules/risk.expense.consecutive_transport_receipts.json new file mode 100644 index 0000000..5354d20 --- /dev/null +++ b/server/rules/risk-rules/risk.expense.consecutive_transport_receipts.json @@ -0,0 +1,32 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.expense.consecutive_transport_receipts", + "name": "连号交通票据", + "enabled": true, + "risk_dimension": "consecutive_receipts", + "ontology_signal": "consecutive_transport_receipts", + "evaluator": "consecutive_transport_receipts", + "applies_to": { + "expense_types": ["transport", "travel"], + "min_attachments": 2 + }, + "inputs": { + "invoice_no": "attachment.invoice_no" + }, + "params": { + "min_consecutive_count": 3 + }, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "manual_review" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 三、车辆交通 / 连号票集中报销", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.expense.entertainment_missing_detail.json b/server/rules/risk-rules/risk.expense.entertainment_missing_detail.json new file mode 100644 index 0000000..d3735da --- /dev/null +++ b/server/rules/risk-rules/risk.expense.entertainment_missing_detail.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.expense.entertainment_missing_detail", + "name": "招待费事由不完整", + "enabled": true, + "risk_dimension": "entertainment_detail", + "ontology_signal": "entertainment_missing_detail", + "evaluator": "entertainment_reason_missing", + "applies_to": { + "domains": ["meal"] + }, + "inputs": { + "reason": "claim.reason_corpus" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 三、餐费招待 / 业务招待无事由对象", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.expense.meal_localized_as_travel.json b/server/rules/risk-rules/risk.expense.meal_localized_as_travel.json new file mode 100644 index 0000000..7dd9386 --- /dev/null +++ b/server/rules/risk-rules/risk.expense.meal_localized_as_travel.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.expense.meal_localized_as_travel", + "name": "同城餐饮混入差旅", + "enabled": true, + "risk_dimension": "meal_travel_mix", + "ontology_signal": "meal_as_travel", + "evaluator": "meal_as_travel_same_city", + "applies_to": { + "domains": ["travel"] + }, + "inputs": { + "declared": "claim.location", + "meal_city": "attachment.cities" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 三、餐费招待 / 同城餐饮归集异地差旅", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.expense.reason_too_brief.json b/server/rules/risk-rules/risk.expense.reason_too_brief.json new file mode 100644 index 0000000..3a7149b --- /dev/null +++ b/server/rules/risk-rules/risk.expense.reason_too_brief.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.expense.reason_too_brief", + "name": "报销事由过短", + "enabled": true, + "risk_dimension": "reason_quality", + "ontology_signal": "reason_too_brief", + "evaluator": "reason_too_brief", + "applies_to": {}, + "inputs": { + "reason": "claim.reason_corpus" + }, + "params": { + "min_reason_length": 6 + }, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 通用 / 事由不足以支撑真实性判断", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.claimant_buyer_name_match.json b/server/rules/risk-rules/risk.invoice.claimant_buyer_name_match.json new file mode 100644 index 0000000..5122744 --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.claimant_buyer_name_match.json @@ -0,0 +1,32 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.claimant_buyer_name_match", + "name": "报销人与发票抬头一致", + "enabled": true, + "risk_dimension": "identity_consistency", + "ontology_signal": "buyer_name_mismatch", + "evaluator": "identity_consistency", + "applies_to": { + "min_attachments": 1 + }, + "inputs": { + "claimant": "claim.employee_name", + "buyer": "attachment.buyer_name" + }, + "params": { + "allow_keywords": ["代报", "集团", "公司", "有限公司"] + }, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "manual_review" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 抬头错误", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.cross_year_invoice.json b/server/rules/risk-rules/risk.invoice.cross_year_invoice.json new file mode 100644 index 0000000..0f95492 --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.cross_year_invoice.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.cross_year_invoice", + "name": "跨年发票入账", + "enabled": true, + "risk_dimension": "cross_year_invoice", + "ontology_signal": "cross_year_invoice", + "evaluator": "cross_year_invoice", + "applies_to": { + "min_attachments": 1 + }, + "inputs": { + "invoice_date": "attachment.invoice_date", + "claim_date": ["claim.occurred_at", "item.item_date"] + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 跨年发票", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.document_expense_mismatch.json b/server/rules/risk-rules/risk.invoice.document_expense_mismatch.json new file mode 100644 index 0000000..487287c --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.document_expense_mismatch.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.document_expense_mismatch", + "name": "开票内容与报销场景不符", + "enabled": true, + "risk_dimension": "document_expense_mismatch", + "ontology_signal": "document_expense_mismatch", + "evaluator": "document_expense_mismatch", + "applies_to": { + "min_attachments": 1 + }, + "inputs": { + "document_type": "attachment.document_type", + "expense_type": ["claim.expense_type", "item.item_type"] + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 开票内容与业务不符", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.duplicate_invoice.json b/server/rules/risk-rules/risk.invoice.duplicate_invoice.json new file mode 100644 index 0000000..60632d9 --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.duplicate_invoice.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.duplicate_invoice", + "name": "发票重复报销", + "enabled": true, + "risk_dimension": "duplicate_invoice", + "ontology_signal": "duplicate_invoice", + "evaluator": "duplicate_invoice", + "applies_to": { + "min_attachments": 1 + }, + "inputs": { + "invoice_no": "attachment.invoice_no" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "block" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 重复报销", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.vague_goods_description.json b/server/rules/risk-rules/risk.invoice.vague_goods_description.json new file mode 100644 index 0000000..942e9bb --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.vague_goods_description.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.vague_goods_description", + "name": "发票品名过于笼统", + "enabled": true, + "risk_dimension": "vague_goods_description", + "ontology_signal": "vague_goods_description", + "evaluator": "vague_goods_description", + "applies_to": { + "expense_types": ["office", "other"], + "min_attachments": 1 + }, + "inputs": { + "ocr": "attachment.ocr_text" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 品名笼统", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.invoice.void_or_red_invoice.json b/server/rules/risk-rules/risk.invoice.void_or_red_invoice.json new file mode 100644 index 0000000..9cb9185 --- /dev/null +++ b/server/rules/risk-rules/risk.invoice.void_or_red_invoice.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.invoice.void_or_red_invoice", + "name": "作废或红冲发票", + "enabled": true, + "risk_dimension": "void_or_red_invoice", + "ontology_signal": "void_or_red_invoice", + "evaluator": "invoice_void_or_red", + "applies_to": { + "min_attachments": 1 + }, + "inputs": { + "status": "attachment.invoice_status", + "ocr": "attachment.ocr_text" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "block" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 二、发票类 / 作废红冲发票", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.travel.base_location_overlap.json b/server/rules/risk-rules/risk.travel.base_location_overlap.json new file mode 100644 index 0000000..0bbffa3 --- /dev/null +++ b/server/rules/risk-rules/risk.travel.base_location_overlap.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.travel.base_location_overlap", + "name": "常驻地重合出差风险", + "enabled": true, + "risk_dimension": "base_location_overlap", + "ontology_signal": "base_location_overlap", + "evaluator": "base_location_overlap", + "applies_to": { + "domains": ["travel"] + }, + "inputs": { + "employee_base": "employee.location", + "declared": "claim.location" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "manual_review" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 一、出差类 / 两头在外", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.travel.destination_receipt_location.json b/server/rules/risk-rules/risk.travel.destination_receipt_location.json new file mode 100644 index 0000000..a54c346 --- /dev/null +++ b/server/rules/risk-rules/risk.travel.destination_receipt_location.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.travel.destination_receipt_location", + "name": "申报地点与票据地点一致", + "risk_dimension": "location_consistency", + "ontology_signal": "location_mismatch", + "evaluator": "location_consistency", + "inputs": { + "declared": "claim.location", + "evidence": ["attachment.cities", "item.item_location"] + }, + "params": { + "match_mode": "city_fuzzy", + "missing_evidence": "warn" + }, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "manual_review", + "message_template": "申报地点 {declared} 与票据识别地点 {evidence} 不一致" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "updated_at": "2026-05-18" + } +} diff --git a/server/rules/risk-rules/risk.travel.hotel_without_itinerary.json b/server/rules/risk-rules/risk.travel.hotel_without_itinerary.json new file mode 100644 index 0000000..1c5fce7 --- /dev/null +++ b/server/rules/risk-rules/risk.travel.hotel_without_itinerary.json @@ -0,0 +1,32 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.travel.hotel_without_itinerary", + "name": "住宿城市与行程不一致", + "enabled": true, + "risk_dimension": "hotel_itinerary", + "ontology_signal": "hotel_itinerary_mismatch", + "evaluator": "hotel_without_itinerary", + "applies_to": { + "domains": ["travel"], + "expense_types": ["hotel", "travel"] + }, + "inputs": { + "declared": "claim.location", + "hotel": "attachment.hotel_city", + "itinerary": "attachment.route_cities" + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "manual_review" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 三、住宿费 / 夜间异地住宿、酒店连续多天", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.travel.intracity_travel_claim.json b/server/rules/risk-rules/risk.travel.intracity_travel_claim.json new file mode 100644 index 0000000..64f2ddd --- /dev/null +++ b/server/rules/risk-rules/risk.travel.intracity_travel_claim.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.travel.intracity_travel_claim", + "name": "同城虚报差旅补贴", + "enabled": true, + "risk_dimension": "intracity_travel", + "ontology_signal": "intracity_travel", + "evaluator": "intracity_travel_claim", + "applies_to": { + "domains": ["travel"] + }, + "inputs": { + "declared": "claim.location", + "evidence": ["attachment.route", "attachment.cities"] + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "high", + "action": "manual_review" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 一、出差类 / 同城虚报差旅", + "updated_at": "2026-05-19" + } +} diff --git a/server/rules/risk-rules/risk.travel.multi_city_reason_required.json b/server/rules/risk-rules/risk.travel.multi_city_reason_required.json new file mode 100644 index 0000000..2554f1c --- /dev/null +++ b/server/rules/risk-rules/risk.travel.multi_city_reason_required.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1.0", + "rule_code": "risk.travel.multi_city_reason_required", + "name": "多城市行程需说明", + "enabled": true, + "risk_dimension": "multi_city_itinerary", + "ontology_signal": "multi_city_itinerary", + "evaluator": "multi_city_reason_required", + "applies_to": { + "domains": ["travel"] + }, + "inputs": { + "reason": "claim.reason_corpus", + "cities": ["attachment.cities", "item.item_location"] + }, + "params": {}, + "outcomes": { + "pass": { "severity": "none", "action": "continue" }, + "fail": { + "severity": "medium", + "action": "warn" + } + }, + "metadata": { + "owner": "风控与审计部", + "stability": "platform_builtin", + "source_ref": "常用risk.txt / 一、出差类 / 绕道出行、行程不符", + "updated_at": "2026-05-19" + } +} diff --git a/server/scripts/sync_platform_risk_rules.py b/server/scripts/sync_platform_risk_rules.py new file mode 100644 index 0000000..e8dd03b --- /dev/null +++ b/server/scripts/sync_platform_risk_rules.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Sync platform risk rule assets from server/rules/risk-rules/*.json.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +SERVER_SRC = Path(__file__).resolve().parents[1] / "src" +if str(SERVER_SRC) not in sys.path: + sys.path.insert(0, str(SERVER_SRC)) + +from app.db.session import get_session_factory # noqa: E402 +from app.services.agent_foundation import AgentFoundationService # noqa: E402 + + +def main() -> None: + db = get_session_factory()() + try: + count = AgentFoundationService(db).sync_platform_risk_rules_from_library() + db.commit() + print(f"Synced {count} risk rule manifest(s) from library.") + finally: + db.close() + + +if __name__ == "__main__": + main() diff --git a/server/scripts/test_rule_json_api.py b/server/scripts/test_rule_json_api.py new file mode 100644 index 0000000..5e243fa --- /dev/null +++ b/server/scripts/test_rule_json_api.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import json +import urllib.request + +base = "http://127.0.0.1:8000/api/v1" +items = json.loads(urllib.request.urlopen(f"{base}/agent-assets?asset_type=rule").read()) +risk = next((i for i in items if str(i.get("code", "")).startswith("risk.")), None) +print("risk asset:", risk.get("code") if risk else None) +if not risk: + raise SystemExit(1) +resp = urllib.request.urlopen(f"{base}/agent-assets/{risk['id']}/rule-json") +payload = json.loads(resp.read()) +print("rule-json ok:", payload.get("file_name"), payload.get("evaluator")) diff --git a/server/src/app/api/v1/endpoints/agent_assets.py b/server/src/app/api/v1/endpoints/agent_assets.py index cefc7e4..3f787bc 100644 --- a/server/src/app/api/v1/endpoints/agent_assets.py +++ b/server/src/app/api/v1/endpoints/agent_assets.py @@ -27,7 +27,6 @@ from app.schemas.agent_asset import ( AgentAssetRuleJsonWrite, AgentAssetSpreadsheetChangeRecordRead, AgentAssetUpdate, - AgentAssetVersionCompareRead, AgentAssetVersionCreate, AgentAssetVersionRead, AgentAssetVersionTimelineItemRead, @@ -167,7 +166,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config( db: DbSession, version: Annotated[ str | None, - Query(description="可选的规则版本号;不传时默认当前版本。"), + Query(description="兼容旧前端的可选参数;表格规则始终打开当前规则表。"), ] = None, ) -> AgentAssetOnlyOfficeConfigRead: try: @@ -184,7 +183,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config( "/{asset_id}/spreadsheet/content", response_class=FileResponse, summary="下载或预览规则 Excel 文件", - description="按版本返回规则的 Excel 快照,用于浏览器预览或下载。", + description="返回当前规则 Excel 文件,用于浏览器预览或下载。", ) def get_agent_asset_spreadsheet_content( asset_id: str, @@ -192,7 +191,7 @@ def get_agent_asset_spreadsheet_content( db: DbSession, version: Annotated[ str | None, - Query(description="可选的规则版本号;不传时默认当前版本。"), + Query(description="兼容旧前端的可选参数;不传时返回当前规则表。"), ] = None, ) -> FileResponse: try: @@ -215,18 +214,18 @@ def get_agent_asset_spreadsheet_content( def get_agent_asset_spreadsheet_onlyoffice_content( asset_id: str, db: DbSession, - version: Annotated[ - str, - Query(min_length=1, description="规则版本号。"), - ], access_token: Annotated[ str, Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"), ], + version: Annotated[ + str | None, + Query(description="兼容旧 ONLYOFFICE URL;当前表格模式不再使用。"), + ] = None, ) -> FileResponse: try: service = AgentAssetService(db) - service.validate_rule_spreadsheet_access_token(asset_id, version, access_token) + service.validate_rule_spreadsheet_access_token(asset_id, access_token) file_path, media_type, filename = service.get_rule_spreadsheet_content( asset_id, version=version, @@ -246,7 +245,7 @@ def get_agent_asset_spreadsheet_onlyoffice_content( response_model=AgentAssetRead, status_code=status.HTTP_201_CREATED, summary="上传规则 Excel 文件", - description="为指定规则上传新的 Excel 快照,并自动生成新规则版本。", + description="为指定规则上传新的 Excel 文件,并记录本次表格修改。", ) def upload_agent_asset_spreadsheet( asset_id: str, @@ -311,16 +310,16 @@ def import_agent_asset_spreadsheet_content( "/{asset_id}/spreadsheet/onlyoffice/callback", response_model=AgentAssetOnlyOfficeCallbackRead, summary="接收规则 Excel 的 ONLYOFFICE 回调", - description="接收 ONLYOFFICE 回写内容,并自动生成新的规则版本。", + description="接收 ONLYOFFICE 回写内容,并记录本次表格修改。", ) def handle_agent_asset_spreadsheet_onlyoffice_callback( asset_id: str, payload: AgentAssetOnlyOfficeCallbackWrite, db: DbSession, version: Annotated[ - str, - Query(min_length=1, description="打开编辑器时对应的规则版本号。"), - ], + str | None, + Query(description="兼容旧 ONLYOFFICE 回调;当前表格模式不再使用。"), + ] = None, actor_name: Annotated[ str | None, Query(description="发起编辑的用户显示名。"), @@ -601,25 +600,3 @@ def get_agent_asset_version_timeline( except Exception as exc: _handle_asset_error(exc) - -@router.get( - "/{asset_id}/versions/compare", - response_model=AgentAssetVersionCompareRead, - summary="比较两个规则表版本", - description="对比两个 Excel 规则表版本的工作表变化与单元格级差异。", -) -def compare_agent_asset_spreadsheet_versions( - asset_id: str, - _: CurrentUser, - db: DbSession, - base_version: Annotated[str, Query(min_length=1, description="基准版本号")], - target_version: Annotated[str, Query(min_length=1, description="对比版本号")], -) -> AgentAssetVersionCompareRead: - try: - return AgentAssetService(db).compare_spreadsheet_versions( - asset_id, - base_version=base_version, - target_version=target_version, - ) - except Exception as exc: - _handle_asset_error(exc) diff --git a/server/src/app/schemas/agent_asset.py b/server/src/app/schemas/agent_asset.py index 3bd7ee1..0a82126 100644 --- a/server/src/app/schemas/agent_asset.py +++ b/server/src/app/schemas/agent_asset.py @@ -133,22 +133,10 @@ class AgentAssetSpreadsheetDiffSheetRead(BaseModel): change_type: str -class AgentAssetVersionCompareRead(BaseModel): - base_version: str - target_version: str - added_sheet_count: int = 0 - removed_sheet_count: int = 0 - changed_sheet_count: int = 0 - changed_cell_count: int = 0 - sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list) - cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list) - - class AgentAssetSpreadsheetChangeRecordRead(BaseModel): id: str actor: str changed_at: datetime - version: str | None = None summary: str sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list) cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list) diff --git a/server/src/app/services/agent_assets.py b/server/src/app/services/agent_assets.py index 78e2a5e..9a21fee 100644 --- a/server/src/app/services/agent_assets.py +++ b/server/src/app/services/agent_assets.py @@ -36,7 +36,6 @@ from app.schemas.agent_asset import ( AgentAssetSpreadsheetDiffCellRead, AgentAssetSpreadsheetDiffSheetRead, AgentAssetUpdate, - AgentAssetVersionCompareRead, AgentAssetVersionCreate, AgentAssetVersionRead, AgentAssetVersionTimelineItemRead, @@ -511,18 +510,16 @@ class AgentAssetService: return self._build_onlyoffice_spreadsheet_config( asset_id=asset_id, current_user=current_user, - resolved_version=resolved_version, metadata=metadata, editable=resolved_version == PREVIEW_RULE_CURRENT_VERSION, ) asset = self._require_spreadsheet_rule(asset_id) - resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset) + _, metadata = self._resolve_current_spreadsheet_meta(asset) editable = self._can_edit_current_spreadsheet(current_user) return self._build_onlyoffice_spreadsheet_config( asset_id=asset.id, current_user=current_user, - resolved_version=resolved_version, metadata=metadata, editable=editable, ) @@ -555,7 +552,6 @@ class AgentAssetService: def validate_rule_spreadsheet_access_token( self, asset_id: str, - version: str, access_token: str, ) -> None: onlyoffice_settings = resolve_onlyoffice_settings() @@ -571,7 +567,6 @@ class AgentAssetService: if ( payload.get("scope") != "agent-asset-spreadsheet" or payload.get("asset_id") != asset_id - or payload.get("version") != version ): raise ValueError("ONLYOFFICE 文件访问令牌无效。") @@ -604,7 +599,6 @@ class AgentAssetService: ) changed_sheet_count = self._count_changed_sheets(sheet_changes, cell_changes) changed_cell_count = len(cell_changes) - next_version = self._next_available_version(asset) metadata = self._store_current_rule_spreadsheet( asset, @@ -613,45 +607,10 @@ class AgentAssetService: actor=actor, source=source, ) - snapshot_metadata = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot( - library=self._resolve_spreadsheet_rule_library(asset), - asset_id=asset.id, - version=next_version, - file_name=file_name, - content=content, - actor_name=actor, - source=source, - ) - operation_label = ( - change_note - or ( - "ONLYOFFICE 在线编辑" - if source == "onlyoffice" - else f"上传并覆盖当前规则表:{normalized_name}" - ) - ) summary = self._build_spreadsheet_change_summary( - operation_label, sheet_changes, cell_changes, ) - version_content = self.spreadsheet_manager.build_version_markdown( - rule_name=asset.name, - version=next_version, - metadata=snapshot_metadata, - ) - self.create_version( - asset.id, - AgentAssetVersionCreate( - version=next_version, - content=version_content, - content_type=AgentAssetContentType.MARKDOWN, - change_note=summary, - created_by=actor, - ), - actor=actor, - request_id=request_id, - ) self.audit_service.log_action( actor=actor, action="edit_rule_spreadsheet", @@ -660,13 +619,11 @@ class AgentAssetService: before_json={"storage_key": current_metadata.storage_key}, after_json={ "summary": summary, - "version": next_version, "changed_sheet_count": changed_sheet_count, "changed_cell_count": changed_cell_count, "sheet_changes": [item.model_dump() for item in sheet_changes], "cell_changes": [item.model_dump() for item in cell_changes[:500]], "storage_key": metadata.storage_key, - "snapshot_storage_key": snapshot_metadata.storage_key, }, request_id=request_id, ) @@ -705,7 +662,7 @@ class AgentAssetService: self, asset_id: str, *, - version: str, + version: str | None = None, payload: dict[str, Any], actor_name: str | None = None, ) -> None: @@ -721,8 +678,6 @@ class AgentAssetService: callback = self._parse_onlyoffice_callback(payload) if callback.status not in {2, 6} or not callback.download_url: return - if str(version or "").strip() not in {"", "current", self._resolve_working_version(asset)}: - return _, current_metadata = self._resolve_current_spreadsheet_meta(asset) request = Request( @@ -924,44 +879,6 @@ class AgentAssetService: return sorted(events, key=lambda item: item.event_time) - def compare_spreadsheet_versions( - self, - asset_id: str, - *, - base_version: str, - target_version: str, - ) -> AgentAssetVersionCompareRead: - self._ensure_ready() - asset = self._require_spreadsheet_rule(asset_id) - resolved_base, base_meta = self._resolve_spreadsheet_version_meta( - asset, - version=base_version, - ) - resolved_target, target_meta = self._resolve_spreadsheet_version_meta( - asset, - version=target_version, - ) - - base_workbook = self._load_spreadsheet_for_compare(base_meta) - target_workbook = self._load_spreadsheet_for_compare(target_meta) - sheet_changes, cell_changes = self._collect_workbook_changes( - base_workbook, - target_workbook, - ) - added_sheet_count = sum(1 for item in sheet_changes if item.change_type == "added") - removed_sheet_count = sum(1 for item in sheet_changes if item.change_type == "removed") - - return AgentAssetVersionCompareRead( - base_version=resolved_base, - target_version=resolved_target, - added_sheet_count=added_sheet_count, - removed_sheet_count=removed_sheet_count, - changed_sheet_count=self._count_changed_sheets(sheet_changes, cell_changes), - changed_cell_count=len(cell_changes), - sheet_changes=sheet_changes, - cell_changes=cell_changes[:500], - ) - def list_spreadsheet_change_records( self, asset_id: str, @@ -981,8 +898,7 @@ class AgentAssetService: id=log.id, actor=log.actor, changed_at=log.created_at, - version=str((log.after_json or {}).get("version") or "").strip() or None, - summary=str((log.after_json or {}).get("summary") or "ONLYOFFICE 在线编辑保存。"), + summary=str((log.after_json or {}).get("summary") or "表格内容已保存。"), sheet_changes=[ AgentAssetSpreadsheetDiffSheetRead.model_validate(item) for item in ((log.after_json or {}).get("sheet_changes") or []) @@ -1292,7 +1208,6 @@ class AgentAssetService: *, asset_id: str, current_user: CurrentUserContext, - resolved_version: str, metadata: RuleSpreadsheetMeta, editable: bool, ) -> AgentAssetOnlyOfficeConfigRead: @@ -1307,21 +1222,21 @@ class AgentAssetService: backend_base_url = onlyoffice_settings.backend_url.rstrip("/") public_url = onlyoffice_settings.public_url.rstrip("/") - access_token = self._build_onlyoffice_access_token(asset_id, resolved_version) + access_token = self._build_onlyoffice_access_token(asset_id) document_url = ( f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/content" - f"?version={resolved_version}&access_token={access_token}" + f"?access_token={access_token}" ) callback_url = ( f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/callback" - f"?version={resolved_version}&actor_name={quote(current_user.name)}" + f"?actor_name={quote(current_user.name)}" ) config: dict[str, Any] = { "documentType": "cell", "document": { "fileType": Path(metadata.file_name).suffix.lstrip(".").lower() or "xlsx", - "key": self._build_onlyoffice_document_key(asset_id, resolved_version, metadata), + "key": self._build_onlyoffice_document_key(asset_id, metadata), "title": metadata.file_name, "url": document_url, "permissions": { @@ -1462,19 +1377,6 @@ class AgentAssetService: major, minor, patch = [int(item) for item in parts] return f"v{major}.{minor}.{patch + 1}" - @staticmethod - def _can_edit_spreadsheet_version( - asset: AgentAsset, - current_user: CurrentUserContext, - version: str, - ) -> bool: - role_codes = {str(item).strip() for item in current_user.role_codes} - can_edit = current_user.is_admin or "manager" in role_codes or "finance" in role_codes - return ( - can_edit - and AgentAssetService._resolve_working_version(asset) == str(version or "").strip() - ) - @staticmethod def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool: role_codes = {str(item).strip() for item in current_user.role_codes} @@ -1483,23 +1385,21 @@ class AgentAssetService: @staticmethod def _build_onlyoffice_document_key( asset_id: str, - version: str, metadata: RuleSpreadsheetMeta, ) -> str: fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name - raw_key = f"{asset_id}-{version}-{fingerprint}" + raw_key = f"{asset_id}-{fingerprint}" return "".join( character if character.isalnum() or character in {"-", "_", ".", "="} else "_" for character in raw_key ) @staticmethod - def _build_onlyoffice_access_token(asset_id: str, version: str) -> str: + def _build_onlyoffice_access_token(asset_id: str) -> str: onlyoffice_settings = resolve_onlyoffice_settings() payload = { "scope": "agent-asset-spreadsheet", "asset_id": asset_id, - "version": version, } return jwt.encode(payload, onlyoffice_settings.jwt_secret, algorithm="HS256") @@ -1646,7 +1546,6 @@ class AgentAssetService: @staticmethod def _build_spreadsheet_change_summary( - operation_label: str, sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead], cell_changes: list[AgentAssetSpreadsheetDiffCellRead], ) -> str: @@ -1655,15 +1554,15 @@ class AgentAssetService: | {item.sheet_name for item in cell_changes} ) if not sheet_names: - return f"{operation_label}:文件内容已保存,未发现单元格级差异。" + return "文件内容已保存,未发现单元格级差异。" preview = "、".join(sheet_names[:3]) if len(sheet_names) > 3: preview = f"{preview} 等" sheet_text = f"涉及 {len(sheet_names)} 个工作表({preview})" if cell_changes: - return f"{operation_label}:{sheet_text},共 {len(cell_changes)} 处单元格改动。" - return f"{operation_label}:{sheet_text},工作表结构发生变化。" + return f"{sheet_text},共 {len(cell_changes)} 处单元格改动。" + return f"{sheet_text},工作表结构发生变化。" def _next_available_version(self, asset: AgentAsset) -> str: candidate = self._increment_version(self._resolve_working_version(asset)) diff --git a/server/src/app/services/audit.py b/server/src/app/services/audit.py index cabbb89..4207ce8 100644 --- a/server/src/app/services/audit.py +++ b/server/src/app/services/audit.py @@ -1,70 +1,70 @@ -from __future__ import annotations - -import uuid -from typing import Any - -from sqlalchemy.orm import Session - -from app.core.logging import get_logger -from app.models.audit_log import AuditLog -from app.repositories.audit_log import AuditLogRepository -from app.schemas.audit_log import AuditLogRead -from app.services.agent_foundation import AgentFoundationService - -logger = get_logger("app.services.audit") - - -class AuditLogService: - def __init__(self, db: Session) -> None: - self.db = db - self.repository = AuditLogRepository(db) - - def list_logs( - self, - *, - resource_type: str | None = None, - resource_id: str | None = None, - action: str | None = None, - limit: int = 50, - ) -> list[AuditLogRead]: - self._ensure_ready() - items = self.repository.list( - resource_type=resource_type, - resource_id=resource_id, - action=action, - limit=limit, - ) - return [AuditLogRead.model_validate(item) for item in items] - - def log_action( - self, - *, - actor: str, - action: str, - resource_type: str, - resource_id: str, - before_json: dict[str, Any] | None = None, - after_json: dict[str, Any] | None = None, - request_id: str | None = None, - ) -> AuditLog: - log = AuditLog( - actor=actor, - action=action, - resource_type=resource_type, - resource_id=resource_id, - before_json=before_json, - after_json=after_json, - request_id=request_id or uuid.uuid4().hex, - ) - created = self.repository.create(log) - logger.info( - "Created audit log id=%s action=%s resource=%s:%s", - created.id, - created.action, - created.resource_type, - created.resource_id, - ) - return created - - def _ensure_ready(self) -> None: - AgentFoundationService(self.db).ensure_foundation_ready() +from __future__ import annotations + +import uuid +from typing import Any + +from sqlalchemy.orm import Session + +from app.core.logging import get_logger +from app.models.audit_log import AuditLog +from app.repositories.audit_log import AuditLogRepository +from app.schemas.audit_log import AuditLogRead +from app.services.agent_foundation import AgentFoundationService + +logger = get_logger("app.services.audit") + + +class AuditLogService: + def __init__(self, db: Session) -> None: + self.db = db + self.repository = AuditLogRepository(db) + + def list_logs( + self, + *, + resource_type: str | None = None, + resource_id: str | None = None, + action: str | None = None, + limit: int = 50, + ) -> list[AuditLogRead]: + self._ensure_ready() + items = self.repository.list( + resource_type=resource_type, + resource_id=resource_id, + action=action, + limit=limit, + ) + return [AuditLogRead.model_validate(item) for item in items] + + def log_action( + self, + *, + actor: str, + action: str, + resource_type: str, + resource_id: str, + before_json: dict[str, Any] | None = None, + after_json: dict[str, Any] | None = None, + request_id: str | None = None, + ) -> AuditLog: + log = AuditLog( + actor=actor, + action=action, + resource_type=resource_type, + resource_id=resource_id, + before_json=before_json, + after_json=after_json, + request_id=request_id or uuid.uuid4().hex, + ) + created = self.repository.create(log) + logger.info( + "Created audit log id=%s action=%s resource=%s:%s", + created.id, + created.action, + created.resource_type, + created.resource_id, + ) + return created + + def _ensure_ready(self) -> None: + AgentFoundationService(self.db).ensure_foundation_ready() diff --git a/server/src/app/services/expense_claims.py b/server/src/app/services/expense_claims.py index b160213..08a89b4 100644 --- a/server/src/app/services/expense_claims.py +++ b/server/src/app/services/expense_claims.py @@ -1,3600 +1,3600 @@ -from __future__ import annotations - -import base64 -import binascii -import json -import mimetypes -import re -import shutil -from collections import defaultdict -from datetime import UTC, date, datetime, timedelta -from decimal import Decimal, InvalidOperation -from pathlib import Path -from types import SimpleNamespace -from typing import Any -from urllib.parse import quote - -from sqlalchemy import and_, func, or_, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, selectinload - -from app.api.deps import CurrentUserContext -from app.core.config import get_settings -from app.models.employee import Employee -from app.models.financial_record import ExpenseClaim, ExpenseClaimItem -from app.models.organization import OrganizationUnit -from app.schemas.ontology import OntologyEntity, OntologyParseResult -from app.schemas.reimbursement import ExpenseClaimItemCreate, ExpenseClaimItemUpdate -from app.services.agent_foundation import AgentFoundationService -from app.services.audit import AuditLogService -from app.services.document_intelligence import build_document_insight -from app.services.expense_rule_runtime import ( - DEFAULT_SCENE_RULE_ASSET_CODE, - ExpenseRuleRuntimeService, - RuntimeTravelPolicy, - build_default_expense_rule_catalog, - resolve_document_type_label, -) -from app.services.ocr import OcrService - -EXPENSE_TYPE_LABELS = { - "travel": "差旅", - "hotel": "住宿", - "transport": "交通", - "meal": "餐费", - "meeting": "会务", - "entertainment": "招待", - "office": "办公", - "training": "培训", - "communication": "通讯", - "welfare": "福利", -} - -PRIVILEGED_CLAIM_ROLE_CODES = {"finance"} -APPROVAL_VISIBLE_CLAIM_ROLE_CODES = {"manager", "approver"} -MAX_DRAFT_CLAIMS_PER_USER = 3 -LOCATION_REQUIRED_EXPENSE_TYPES = { - "travel", - "hotel", - "transport", - "meal", - "meeting", - "entertainment", -} - -EXPENSE_SCENE_KEYWORDS = { - "travel": ("差旅", "出差", "行程"), - "hotel": ("酒店", "住宿", "房费", "客房", "入住", "离店"), - "transport": ( - "交通", - "打车", - "出租车", - "网约车", - "滴滴", - "出行", - "高铁", - "动车", - "火车", - "机票", - "航班", - "行程单", - "登机", - "客票", - "公交", - "地铁", - "过路费", - "通行费", - "停车", - ), - "meal": ("餐饮", "餐费", "用餐", "外卖", "快餐", "酒楼", "饭店", "饭馆", "食品", "咖啡"), - "entertainment": ("招待", "宴请", "接待", "客户餐", "商务餐", "业务招待"), - "office": ("办公", "办公用品", "文具", "耗材", "打印", "纸张", "硒鼓", "墨盒", "鼠标", "键盘", "电脑"), - "meeting": ("会议", "会务", "会展", "会议室", "会场", "场地费", "论坛"), - "training": ("培训", "课程", "讲师", "教材", "学费", "认证"), -} - -EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES = { - "travel": {"travel", "hotel", "transport", "meal"}, - "hotel": {"hotel"}, - "transport": {"transport", "travel"}, - "meal": {"meal", "entertainment"}, - "entertainment": {"entertainment", "meal"}, - "office": {"office"}, - "meeting": {"meeting"}, - "training": {"training"}, -} - -DOCUMENT_SCENE_LABELS = { - "travel": "差旅", - "hotel": "住宿", - "transport": "交通", - "meal": "餐饮", - "entertainment": "业务招待", - "office": "办公用品", - "meeting": "会务", - "training": "培训", - "other": "其他票据", -} - -DOCUMENT_ASSOCIATION_REVIEW_ACTIONS = { - "link_to_existing_draft", - "create_new_claim_from_documents", -} -MAX_CLAIM_NO_RETRY_ATTEMPTS = 3 -DOCUMENT_AMOUNT_PATTERNS = ( - re.compile( - r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)" - r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)" - ), - re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)"), - re.compile(r"([0-9]+(?:[.,][0-9]{1,2})?)\s*元"), -) -DOCUMENT_DATE_PATTERN = re.compile(r"((?:20\d{2}|19\d{2})[-/年.](?:1[0-2]|0?[1-9])[-/月.](?:3[01]|[12]\d|0?[1-9])日?)") -SYSTEM_GENERATED_REASON_PREFIXES = ( - "我上传了", - "请按当前已识别信息", - "请把当前上传的票据", - "请基于当前上传的多张票据", - "我已核对右侧识别结果", - "请同步修正逐票据识别结果", - "我已修改识别信息", - "查看报销草稿", - "请解释一下当前这笔报销的合规风险和待补充项", -) -AI_REVIEW_LOOKBACK_DAYS = 90 -AI_REVIEW_REPEAT_RISK_WARNING_COUNT = 1 -AI_REVIEW_REPEAT_RISK_BLOCK_COUNT = 2 -TRAVEL_REVIEW_RELEVANT_EXPENSE_TYPES = {"travel", "hotel", "transport"} -TRAVEL_REVIEW_LONG_DISTANCE_DOCUMENT_TYPES = {"flight_itinerary", "train_ticket"} -TRAVEL_POLICY_CITY_TIERS = { - "北京": "tier_1", - "上海": "tier_1", - "广州": "tier_1", - "深圳": "tier_1", - "杭州": "tier_2", - "南京": "tier_2", - "苏州": "tier_2", - "武汉": "tier_2", - "成都": "tier_2", - "重庆": "tier_2", - "西安": "tier_2", - "天津": "tier_2", - "宁波": "tier_2", - "厦门": "tier_2", - "青岛": "tier_2", - "长沙": "tier_2", - "郑州": "tier_2", - "合肥": "tier_2", - "济南": "tier_2", - "沈阳": "tier_2", - "大连": "tier_2", - "福州": "tier_2", - "昆明": "tier_2", - "海口": "tier_2", - "三亚": "tier_2", - "无锡": "tier_2", - "东莞": "tier_2", - "佛山": "tier_2", -} -TRAVEL_POLICY_CITY_MATCH_ORDER = tuple( - sorted(TRAVEL_POLICY_CITY_TIERS.keys(), key=lambda item: len(item), reverse=True) -) -TRAVEL_POLICY_BAND_LABELS = { - "junior": "P1-P3", - "mid": "P4-P5", - "senior": "P6-P7", - "manager": "M1-M2", - "executive": "M3及以上 / D序列", -} -TRAVEL_POLICY_HOTEL_LIMITS = { - "junior": { - "tier_1": Decimal("450.00"), - "tier_2": Decimal("380.00"), - "tier_3": Decimal("320.00"), - }, - "mid": { - "tier_1": Decimal("550.00"), - "tier_2": Decimal("480.00"), - "tier_3": Decimal("380.00"), - }, - "senior": { - "tier_1": Decimal("700.00"), - "tier_2": Decimal("620.00"), - "tier_3": Decimal("520.00"), - }, - "manager": { - "tier_1": Decimal("900.00"), - "tier_2": Decimal("820.00"), - "tier_3": Decimal("720.00"), - }, - "executive": { - "tier_1": Decimal("1200.00"), - "tier_2": Decimal("1000.00"), - "tier_3": Decimal("900.00"), - }, -} -TRAVEL_POLICY_ALLOWED_TRANSPORT_LEVELS = { - "junior": {"flight": 1, "train": 1}, - "mid": {"flight": 1, "train": 1}, - "senior": {"flight": 2, "train": 2}, - "manager": {"flight": 3, "train": 3}, - "executive": {"flight": 4, "train": 3}, -} -TRAVEL_POLICY_ROUTE_EXCEPTION_KEYWORDS = ( - "中转", - "转机", - "经停", - "改签", - "多地出差", - "多城市", - "多站", - "异地返程", - "异地结束", - "临时变更", - "继续前往", - "第二站", -) -TRAVEL_POLICY_STANDARD_EXCEPTION_KEYWORDS = ( - "超标说明", - "无直达", - "展会高峰", - "会议高峰", - "协议酒店满房", - "客户指定", - "临时改签", - "行程变更", - "红眼航班", - "晚到店", -) -TRAVEL_POLICY_FLIGHT_CLASS_PATTERNS = ( - ("头等舱", 4), - ("公务舱", 3), - ("商务舱", 3), - ("超级经济舱", 2), - ("高端经济舱", 2), - ("明珠经济舱", 2), - ("经济舱", 1), -) -TRAVEL_POLICY_TRAIN_CLASS_PATTERNS = ( - ("商务座", 3), - ("一等座", 2), - ("软卧", 2), - ("二等座", 1), - ("二等卧", 1), - ("硬卧", 1), -) -TRAVEL_POLICY_HOTEL_NIGHT_PATTERN = re.compile(r"(\d+)\s*(?:晚|间夜)") - - -class ExpenseClaimService: - def __init__(self, db: Session) -> None: - self.db = db - self.audit_service = AuditLogService(db) - - def list_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]: - stmt = ( - select(ExpenseClaim) - .options( - selectinload(ExpenseClaim.items), - selectinload(ExpenseClaim.employee).selectinload(Employee.manager), - selectinload(ExpenseClaim.employee).selectinload(Employee.roles), - ) - .order_by(ExpenseClaim.created_at.desc(), ExpenseClaim.occurred_at.desc()) - ) - stmt = self._apply_claim_scope(stmt, current_user) - return list(self.db.scalars(stmt).all()) - - def get_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: - stmt = ( - select(ExpenseClaim) - .options( - selectinload(ExpenseClaim.items), - selectinload(ExpenseClaim.employee).selectinload(Employee.manager), - selectinload(ExpenseClaim.employee).selectinload(Employee.roles), - ) - .where(ExpenseClaim.id == claim_id) - ) - stmt = self._apply_claim_scope(stmt, current_user) - return self.db.scalar(stmt) - - def update_claim_item( - self, - *, - claim_id: str, - item_id: str, - payload: ExpenseClaimItemUpdate, - current_user: CurrentUserContext, - ) -> ExpenseClaim | None: - claim = self.get_claim(claim_id, current_user) - if claim is None: - return None - - self._ensure_draft_claim(claim) - item = next((entry for entry in claim.items if entry.id == item_id), None) - if item is None: - raise LookupError("Item not found") - - before_json = self._serialize_claim(claim) - - if payload.item_date is not None: - item.item_date = payload.item_date - if payload.item_type is not None: - item.item_type = self._normalize_optional_text(payload.item_type, fallback=item.item_type) or item.item_type - if payload.item_reason is not None: - item.item_reason = ( - self._normalize_optional_text(payload.item_reason, fallback=item.item_reason) or item.item_reason - ) - if payload.item_location is not None: - item.item_location = ( - self._normalize_optional_text(payload.item_location, fallback=item.item_location) or item.item_location - ) - if payload.item_amount is not None: - amount = payload.item_amount.quantize(Decimal("0.01")) - if amount <= Decimal("0.00"): - raise ValueError("费用金额必须大于 0。") - item.item_amount = amount - if payload.invoice_id is not None: - item.invoice_id = self._normalize_optional_text(payload.invoice_id, allow_empty=True) - - self._refresh_item_attachment_analysis(item) - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.item_update", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return claim - - def create_claim_item( - self, - *, - claim_id: str, - payload: ExpenseClaimItemCreate | None, - current_user: CurrentUserContext, - ) -> ExpenseClaim | None: - claim = self.get_claim(claim_id, current_user) - if claim is None: - return None - - self._ensure_draft_claim(claim) - before_json = self._serialize_claim(claim) - payload = payload or ExpenseClaimItemCreate() - - occurred_at = claim.occurred_at if claim.occurred_at is not None else datetime.now(UTC) - item_amount = Decimal("0.00") - if payload.item_amount is not None: - item_amount = payload.item_amount.quantize(Decimal("0.01")) - if item_amount < Decimal("0.00"): - raise ValueError("费用金额不能小于 0。") - - item = ExpenseClaimItem( - claim_id=claim.id, - item_date=payload.item_date or occurred_at.date(), - item_type=self._normalize_optional_text( - payload.item_type, - fallback=str(claim.expense_type or "").strip() or "other", - ) - or "other", - item_reason=self._normalize_optional_text(payload.item_reason, fallback="") or "", - item_location=self._normalize_optional_text(payload.item_location, fallback="") or "", - item_amount=item_amount, - invoice_id=self._normalize_optional_text(payload.invoice_id, allow_empty=True), - ) - claim.items.append(item) - self.db.add(item) - - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.item_create", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return claim - - def delete_claim_item( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> dict[str, Any] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - self._ensure_draft_claim(claim) - before_json = self._serialize_claim(claim) - item_label = str(item.item_reason or "").strip() or self._resolve_expense_type_label(item.item_type) - - self._delete_item_attachment_files(item) - claim.items = [entry for entry in claim.items if entry.id != item.id] - self.db.delete(item) - - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.item_delete", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return { - "message": f"费用明细“{item_label}”已删除。", - "claim_id": claim.id, - "item_id": item.id, - } - - def upload_claim_item_attachment( - self, - *, - claim_id: str, - item_id: str, - filename: str, - content: bytes, - media_type: str | None, - current_user: CurrentUserContext, - ) -> dict[str, Any] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - self._ensure_draft_claim(claim) - normalized_name = self._normalize_attachment_filename(filename) - if not content: - raise ValueError("上传文件不能为空。") - - before_json = self._serialize_claim(claim) - attachment_dir = self._build_item_attachment_dir(claim.id, item.id) - shutil.rmtree(attachment_dir, ignore_errors=True) - attachment_dir.mkdir(parents=True, exist_ok=True) - - file_path = attachment_dir / normalized_name - file_path.write_bytes(content) - resolved_media_type = self._resolve_attachment_media_type( - normalized_name, - fallback=media_type, - ) - - attachment_analysis = self._build_fallback_attachment_analysis( - media_type=media_type, - item=item, - ) - ocr_document = None - document_info = None - requirement_check = None - ocr_status = "empty" - ocr_error = "" - try: - ocr_result = OcrService(self.db).recognize_files( - [(normalized_name, content, media_type or "application/octet-stream")] - ) - documents = list(ocr_result.documents or []) - if documents: - ocr_document = documents[0] - ocr_status = "recognized" - document_info = self._build_attachment_document_info(ocr_document) - requirement_check = self._build_attachment_requirement_check( - item=item, - document_info=document_info, - ) - attachment_analysis = self._build_attachment_analysis( - document=ocr_document, - item=item, - document_info=document_info, - requirement_check=requirement_check, - ) - except Exception as exc: # pragma: no cover - fallback path depends on OCR runtime - ocr_status = "failed" - ocr_error = str(exc) - attachment_analysis = self._build_failed_ocr_attachment_analysis( - media_type=media_type, - error_message=ocr_error, - item=item, - ) - - item.invoice_id = self._to_attachment_storage_key(file_path) - preview_meta = self._build_attachment_preview_meta( - file_path=file_path, - media_type=resolved_media_type, - ocr_document=ocr_document, - ) - meta = { - "file_name": normalized_name, - "storage_key": item.invoice_id, - "media_type": resolved_media_type, - "size_bytes": len(content), - "uploaded_at": datetime.now(UTC).isoformat(), - "previewable": bool(preview_meta["previewable"]), - "preview_kind": str(preview_meta["preview_kind"]), - "preview_storage_key": str(preview_meta["preview_storage_key"]), - "preview_media_type": str(preview_meta["preview_media_type"]), - "preview_file_name": str(preview_meta["preview_file_name"]), - "analysis": attachment_analysis, - "document_info": document_info, - "requirement_check": requirement_check, - "ocr_status": ocr_status, - "ocr_error": ocr_error, - "ocr_text": str(getattr(ocr_document, "text", "") or ""), - "ocr_summary": str(getattr(ocr_document, "summary", "") or ""), - "ocr_avg_score": float(getattr(ocr_document, "avg_score", 0.0) or 0.0), - "ocr_line_count": int(getattr(ocr_document, "line_count", 0) or 0), - "ocr_classification_source": str(getattr(ocr_document, "classification_source", "") or ""), - "ocr_classification_confidence": float(getattr(ocr_document, "classification_confidence", 0.0) or 0.0), - "ocr_classification_evidence": [ - str(item) - for item in getattr(ocr_document, "classification_evidence", []) or [] - if str(item).strip() - ], - "ocr_warnings": [str(item) for item in getattr(ocr_document, "warnings", []) or []], - } - self._write_attachment_meta(file_path, meta) - - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.attachment_upload", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return { - "message": f"{normalized_name} 已上传并关联到当前费用明细。", - "claim_id": claim.id, - "item_id": item.id, - "invoice_id": item.invoice_id, - "attachment": self._build_attachment_payload(item), - } - - def get_claim_item_attachment_meta( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> dict[str, Any] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - return self._build_attachment_payload(item) - - def get_claim_item_attachment_content( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> tuple[Path, str, str] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - return self._resolve_item_attachment_content(item) - - def get_claim_item_attachment_preview_content( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> tuple[Path, str, str] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - return self._resolve_item_attachment_preview_content(item) - - def delete_claim_item_attachment( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> dict[str, Any] | None: - claim, item = self._get_claim_item_or_raise( - claim_id=claim_id, - item_id=item_id, - current_user=current_user, - ) - if claim is None: - return None - - self._ensure_draft_claim(claim) - before_json = self._serialize_claim(claim) - previous_name = self._resolve_attachment_display_name(item.invoice_id) - self._delete_item_attachment_files(item) - item.invoice_id = None - - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.attachment_delete", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return { - "message": f"{previous_name or '附件'} 已删除。", - "claim_id": claim.id, - "item_id": item.id, - "invoice_id": item.invoice_id, - "attachment": None, - } - - def submit_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: - claim = self.get_claim(claim_id, current_user) - if claim is None: - return None - - self._ensure_draft_claim(claim) - self._sync_claim_from_items(claim) - missing_fields = self._validate_claim_for_submission(claim) - if missing_fields: - raise ValueError("提交前请先补全信息:" + ";".join(missing_fields)) - - before_json = self._serialize_claim(claim) - # TODO: 后续恢复 AI 验审逻辑 - # review_result = self._run_ai_submission_review(claim) - manager_name = self._resolve_claim_manager_name(claim) or "审批人" - claim.status = "submitted" - claim.approval_stage = "直属领导审批" - claim.risk_flags_json = list(claim.risk_flags_json or []) - claim.submitted_at = datetime.now(UTC) - - self.db.commit() - self.db.refresh(claim) - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.submit", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - ) - - return claim - - def save_or_submit_from_ontology( - self, - *, - run_id: str, - user_id: str | None, - message: str, - ontology: OntologyParseResult, - context_json: dict[str, Any], - ) -> dict[str, Any]: - result = self.upsert_draft_from_ontology( - run_id=run_id, - user_id=user_id, - message=message, - ontology=ontology, - context_json=context_json, - ) - - review_action = str(context_json.get("review_action") or "").strip() - if review_action != "next_step": - return result - - claim_id = str(result.get("claim_id") or "").strip() - if not claim_id or result.get("draft_limit_reached"): - return result - - current_user = CurrentUserContext( - username=str(user_id or context_json.get("name") or "anonymous").strip() or "anonymous", - name=str(context_json.get("name") or user_id or "anonymous").strip() or "anonymous", - role_codes=[ - str(item).strip() - for item in list(context_json.get("role_codes") or []) - if str(item).strip() - ], - is_admin=bool(context_json.get("is_admin")), - ) - - try: - claim = self.submit_claim(claim_id, current_user) - except ValueError as exc: - return { - **result, - "message": str(exc), - "submission_blocked": True, - "draft_only": False, - } - - if claim is None: - return { - **result, - "message": "未找到可提交的报销单,请刷新后重试。", - "submission_blocked": True, - "draft_only": False, - } - - if str(claim.status or "").strip().lower() != "submitted": - review_message = "" - for flag in list(claim.risk_flags_json or []): - if not isinstance(flag, dict): - continue - if str(flag.get("source") or "").strip() != "submission_review": - continue - review_message = str(flag.get("message") or "").strip() - if review_message: - break - return { - "message": review_message or f"报销单 {claim.claim_no} 经 AI验审后转为待补充,请先修正后再提交。", - "submission_blocked": True, - "draft_only": False, - "claim_id": claim.id, - "claim_no": claim.claim_no, - "status": claim.status, - "approval_stage": claim.approval_stage, - "amount": float(claim.amount), - "invoice_count": int(claim.invoice_count or 0), - } - - return { - "message": ( - f"报销单 {claim.claim_no} 已完成 AI验审," - f"当前节点为 {claim.approval_stage or '审批中'}。" - ), - "draft_only": False, - "claim_id": claim.id, - "claim_no": claim.claim_no, - "status": claim.status, - "approval_stage": claim.approval_stage, - "amount": float(claim.amount), - "invoice_count": int(claim.invoice_count or 0), - } - - def delete_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: - claim = self.get_claim(claim_id, current_user) - if claim is None: - return None - - self._ensure_draft_claim(claim) - before_json = self._serialize_claim(claim) - resource_id = claim.id - - self._delete_claim_attachment_root(claim.id) - self.db.delete(claim) - self.db.commit() - - self.audit_service.log_action( - actor=current_user.name or current_user.username, - action="expense_claim.delete", - resource_type="expense_claim", - resource_id=resource_id, - before_json=before_json, - after_json=None, - ) - - return claim - - def upsert_draft_from_ontology( - self, - *, - run_id: str, - user_id: str | None, - message: str, - ontology: OntologyParseResult, - context_json: dict[str, Any], - ) -> dict[str, Any]: - self._ensure_ready() - context_json = dict(context_json or {}) - retry_count = self._resolve_claim_no_retry_count(context_json) - - review_action = str(context_json.get("review_action") or "").strip() - attachment_names = self._resolve_attachment_names(context_json) - context_documents = self._resolve_context_documents(context_json) - - employee = self._resolve_employee( - ontology=ontology, - context_json=context_json, - user_id=user_id, - ) - draft_owner_name = ( - employee.name - if employee is not None - else self._resolve_employee_name( - ontology=ontology, - context_json=context_json, - user_id=user_id, - ) - ) - - association_candidate = self._find_association_candidate( - ontology=ontology, - context_json=context_json, - user_id=user_id, - employee=employee, - ) - if self._should_defer_multi_document_association( - context_json=context_json, - review_action=review_action, - association_candidate=association_candidate, - context_documents=context_documents, - ): - document_count = max(len(context_documents), len(attachment_names), self._resolve_attachment_count(context_json)) - return { - "message": ( - f"检测到你已有草稿 {association_candidate.claim_no}," - f"当前新上传了 {document_count} 张票据,请先选择关联到现有草稿,或单独建立新的报销单。" - ), - "draft_only": False, - "status": "pending_association_decision", - "pending_association_decision": True, - "association_candidate_claim_id": association_candidate.id, - "association_candidate_claim_no": association_candidate.claim_no, - } - - claim = self._find_target_claim( - ontology=ontology, - context_json=context_json, - review_action=review_action, - association_candidate=association_candidate, - ) - is_new_claim = claim is None - before_json = self._serialize_claim(claim) if claim is not None else None - if is_new_claim: - existing_draft_count = self._count_draft_claims_for_owner( - employee=employee, - user_id=user_id, - ) - if existing_draft_count >= MAX_DRAFT_CLAIMS_PER_USER: - return { - "message": ( - f"你当前已保存 {MAX_DRAFT_CLAIMS_PER_USER} 个草稿,请先完成已保存的草稿," - "才能再次新建草稿。" - ), - "draft_limit_reached": True, - "draft_only": False, - "status": "blocked", - "draft_count": existing_draft_count, - "max_draft_count": MAX_DRAFT_CLAIMS_PER_USER, - } - - amount = self._resolve_amount(ontology.entities, context_json=context_json) - occurred_at = self._resolve_occurred_at(ontology, context_json=context_json) - expense_type = self._resolve_expense_type(ontology.entities, context_json=context_json) - location = self._resolve_location(message=message, context_json=context_json) - reason = self._resolve_reason( - message=message, - context_json=context_json, - allow_message_fallback=is_new_claim, - ) - attachment_count = len(attachment_names) or self._resolve_attachment_count(context_json) - - final_amount = amount if amount is not None else (claim.amount if claim is not None else Decimal("0.00")) - final_occurred_at = ( - occurred_at if occurred_at is not None else (claim.occurred_at if claim is not None else datetime.now(UTC)) - ) - final_expense_type = expense_type or (claim.expense_type if claim is not None else "other") - final_location = location or (claim.location if claim is not None else "待补充") - final_reason = reason or (claim.reason if claim is not None else "待补充") - final_attachment_count = ( - attachment_count if attachment_count > 0 else int(claim.invoice_count or 0) if claim is not None else 0 - ) - final_risk_flags = list(ontology.risk_flags) or ( - list(claim.risk_flags_json or []) if claim is not None else [] - ) - - try: - if claim is None: - claim = ExpenseClaim( - claim_no=self._generate_claim_no(final_occurred_at), - employee_id=employee.id if employee is not None else None, - employee_name=draft_owner_name, - department_id=employee.organization_unit_id if employee is not None else None, - department_name=self._resolve_department_name( - employee=employee, - context_json=context_json, - ), - project_code=self._resolve_project_code(ontology.entities), - expense_type=final_expense_type, - reason=final_reason, - location=final_location, - amount=final_amount, - currency="CNY", - invoice_count=final_attachment_count, - occurred_at=final_occurred_at, - status="draft", - approval_stage="待提交", - risk_flags_json=final_risk_flags, - ) - self.db.add(claim) - else: - claim.employee_id = employee.id if employee is not None else claim.employee_id - claim.employee_name = ( - employee.name - if employee is not None - else self._resolve_employee_name( - ontology=ontology, - context_json=context_json, - user_id=user_id, - fallback=claim.employee_name, - ) - ) - claim.department_id = employee.organization_unit_id if employee is not None else claim.department_id - claim.department_name = self._resolve_department_name( - employee=employee, - context_json=context_json, - fallback=claim.department_name, - ) - claim.project_code = self._resolve_project_code(ontology.entities) or claim.project_code - claim.expense_type = final_expense_type - claim.reason = final_reason - claim.location = final_location - claim.amount = final_amount - claim.invoice_count = final_attachment_count - claim.occurred_at = final_occurred_at - claim.status = "draft" - claim.approval_stage = "待提交" - claim.risk_flags_json = final_risk_flags - - self.db.flush() - if context_documents or attachment_names: - document_specs = self._build_context_item_specs( - context_documents=context_documents, - attachment_names=attachment_names, - occurred_at=final_occurred_at, - expense_type=final_expense_type, - amount=final_amount, - reason=final_reason, - location=final_location, - ) - else: - document_specs = [] - - if document_specs and (is_new_claim or review_action in DOCUMENT_ASSOCIATION_REVIEW_ACTIONS): - if review_action == "link_to_existing_draft" and claim.items: - self._append_document_items( - claim=claim, - item_specs=document_specs, - ) - else: - self._replace_claim_items( - claim=claim, - item_specs=document_specs, - ) - self._sync_claim_from_items(claim) - else: - self._upsert_primary_item( - claim=claim, - occurred_at=final_occurred_at, - expense_type=final_expense_type, - amount=final_amount, - reason=final_reason, - location=final_location, - attachment_names=attachment_names, - ) - self._sync_claim_from_items(claim) - self.db.commit() - self.db.refresh(claim) - except IntegrityError as exc: - self.db.rollback() - if ( - is_new_claim - and retry_count < MAX_CLAIM_NO_RETRY_ATTEMPTS - and self._is_claim_no_conflict_error(exc) - ): - retry_context = dict(context_json) - retry_context["_claim_no_retry_count"] = retry_count + 1 - return self.upsert_draft_from_ontology( - run_id=run_id, - user_id=user_id, - message=message, - ontology=ontology, - context_json=retry_context, - ) - raise - except Exception: - self.db.rollback() - raise - - self.audit_service.log_action( - actor=user_id or claim.employee_name or "anonymous", - action="expense_claim.draft_upsert", - resource_type="expense_claim", - resource_id=claim.id, - before_json=before_json, - after_json=self._serialize_claim(claim), - request_id=run_id, - ) - - return { - "message": ( - f"已{'创建' if is_new_claim else '更新'}报销草稿 {claim.claim_no},当前状态为 draft。" - "你可以继续补充费用明细、客户单位和票据附件。" - ), - "draft_only": True, - "claim_id": claim.id, - "claim_no": claim.claim_no, - "status": claim.status, - "amount": float(claim.amount), - "invoice_count": int(claim.invoice_count or 0), - } - - def _find_target_claim( - self, - *, - ontology: OntologyParseResult, - context_json: dict[str, Any], - review_action: str = "", - association_candidate: ExpenseClaim | None = None, - ) -> ExpenseClaim | None: - if review_action == "create_new_claim_from_documents": - return None - if review_action == "link_to_existing_draft" and association_candidate is not None: - return association_candidate - - draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() - if draft_claim_id: - claim = self.db.get(ExpenseClaim, draft_claim_id) - if claim is not None and str(claim.status or "").strip() == "draft": - return claim - return None - - claim_codes = [ - item.normalized_value - for item in ontology.entities - if item.type == "expense_claim" and item.normalized_value - ] - if not claim_codes: - return None - - stmt = ( - select(ExpenseClaim) - .where(ExpenseClaim.claim_no.in_(claim_codes)) - .where(ExpenseClaim.status == "draft") - .limit(1) - ) - return self.db.scalar(stmt) - - def _find_association_candidate( - self, - *, - ontology: OntologyParseResult, - context_json: dict[str, Any], - user_id: str | None, - employee: Employee | None, - ) -> ExpenseClaim | None: - draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() - if draft_claim_id: - claim = self.db.get(ExpenseClaim, draft_claim_id) - if claim is not None and str(claim.status or "").strip() == "draft": - return claim - - owner_filters = self._build_draft_owner_filters( - employee=employee, - user_id=user_id, - ) - if not owner_filters: - fallback_name = self._resolve_employee_name( - ontology=ontology, - context_json=context_json, - user_id=user_id, - fallback="", - ) - if fallback_name: - owner_filters = [ExpenseClaim.employee_name == fallback_name] - - if not owner_filters: - return None - - stmt = ( - select(ExpenseClaim) - .where(ExpenseClaim.status == "draft") - .where(or_(*owner_filters)) - .order_by(ExpenseClaim.updated_at.desc(), ExpenseClaim.created_at.desc()) - .limit(1) - ) - return self.db.scalar(stmt) - - def _should_defer_multi_document_association( - self, - *, - context_json: dict[str, Any], - review_action: str, - association_candidate: ExpenseClaim | None, - context_documents: list[dict[str, Any]], - ) -> bool: - if association_candidate is None: - return False - if review_action in DOCUMENT_ASSOCIATION_REVIEW_ACTIONS: - return False - document_count = max( - len(context_documents), - len(self._resolve_attachment_names(context_json)), - self._resolve_attachment_count(context_json), - ) - return document_count > 1 - - def _resolve_context_documents(self, context_json: dict[str, Any]) -> list[dict[str, Any]]: - documents = context_json.get("ocr_documents") - if not isinstance(documents, list): - documents = [] - - normalized: list[dict[str, Any]] = [] - for index, item in enumerate(documents[:10], start=1): - if not isinstance(item, dict): - continue - normalized.append( - { - "index": index, - "filename": str(item.get("filename") or "").strip(), - "summary": str(item.get("summary") or "").strip(), - "text": str(item.get("text") or "").strip(), - "document_type": str(item.get("document_type") or "").strip(), - "scene_code": str(item.get("scene_code") or "").strip(), - "scene_label": str(item.get("scene_label") or "").strip(), - "document_fields": self._normalize_document_fields(item.get("document_fields")), - } - ) - - overrides = context_json.get("review_document_form_values") - if not isinstance(overrides, list) or not normalized: - return normalized - - override_map: dict[tuple[int, str], dict[str, Any]] = {} - for item in overrides: - if not isinstance(item, dict): - continue - filename = str(item.get("filename") or "").strip() - index = int(item.get("index") or 0) - if not filename and index <= 0: - continue - override_map[(index, filename)] = item - - for item in normalized: - override = override_map.get((int(item["index"]), str(item["filename"]))) - if override is None: - override = override_map.get((int(item["index"]), "")) - if override is None: - continue - summary = str(override.get("summary") or "").strip() - scene_label = str(override.get("scene_label") or "").strip() - fields = override.get("fields") - if summary: - item["summary"] = summary - if scene_label: - item["scene_label"] = scene_label - if isinstance(fields, list): - item["document_fields"] = self._normalize_document_fields(fields) - - return normalized - - @staticmethod - def _normalize_document_fields(raw_fields: Any) -> list[dict[str, str]]: - if not isinstance(raw_fields, list): - return [] - normalized: list[dict[str, str]] = [] - for field in raw_fields: - if not isinstance(field, dict): - continue - label = str(field.get("label") or "").strip() - value = str(field.get("value") or "").strip() - key = str(field.get("key") or label or "").strip() - if not label or not value: - continue - normalized.append( - { - "key": key, - "label": label, - "value": value, - } - ) - return normalized - - def _build_context_item_specs( - self, - *, - context_documents: list[dict[str, Any]], - attachment_names: list[str], - occurred_at: datetime, - expense_type: str, - amount: Decimal, - reason: str, - location: str, - ) -> list[dict[str, Any]]: - specs: list[dict[str, Any]] = [] - if context_documents: - for document in context_documents: - specs.append( - { - "item_date": self._resolve_document_item_date(document, fallback=occurred_at.date()), - "item_type": self._resolve_document_item_type(document, fallback=expense_type), - "item_reason": reason, - "item_location": location, - "item_amount": self._resolve_document_item_amount(document), - "invoice_id": str(document.get("filename") or "").strip() or None, - } - ) - elif attachment_names: - for attachment_name in attachment_names: - specs.append( - { - "item_date": occurred_at.date(), - "item_type": expense_type, - "item_reason": reason, - "item_location": location, - "item_amount": None, - "invoice_id": attachment_name, - } - ) - - if not specs: - return [] - - total_recognized = sum( - spec["item_amount"] for spec in specs if isinstance(spec.get("item_amount"), Decimal) - ) - missing_specs = [spec for spec in specs if spec.get("item_amount") is None] - if missing_specs: - remaining = (amount - total_recognized).quantize(Decimal("0.01")) - if remaining > Decimal("0.00"): - missing_specs[0]["item_amount"] = remaining - - for spec in specs: - if spec.get("item_amount") is None: - spec["item_amount"] = Decimal("0.00") - - return specs - - def _replace_claim_items( - self, - *, - claim: ExpenseClaim, - item_specs: list[dict[str, Any]], - ) -> None: - existing_items = sorted( - list(claim.items), - key=lambda item: ( - item.item_date or date.max, - self._normalize_sort_datetime(item.created_at), - ), - ) - for index, spec in enumerate(item_specs): - item = existing_items[index] if index < len(existing_items) else None - if item is None: - item = ExpenseClaimItem(claim_id=claim.id) - claim.items.append(item) - self.db.add(item) - item.item_date = spec["item_date"] - item.item_type = spec["item_type"] - item.item_reason = spec["item_reason"] - item.item_location = spec["item_location"] - item.item_amount = spec["item_amount"] - item.invoice_id = spec["invoice_id"] - - for stale_item in existing_items[len(item_specs) :]: - claim.items.remove(stale_item) - self.db.delete(stale_item) - - def _append_document_items( - self, - *, - claim: ExpenseClaim, - item_specs: list[dict[str, Any]], - ) -> None: - existing_invoice_ids = { - str(item.invoice_id or "").strip() - for item in claim.items - if str(item.invoice_id or "").strip() - } - for spec in item_specs: - invoice_id = str(spec.get("invoice_id") or "").strip() - if invoice_id and invoice_id in existing_invoice_ids: - continue - claim.items.append( - ExpenseClaimItem( - claim_id=claim.id, - item_date=spec["item_date"], - item_type=spec["item_type"], - item_reason=spec["item_reason"], - item_location=spec["item_location"], - item_amount=spec["item_amount"], - invoice_id=spec["invoice_id"], - ) - ) - self.db.add(claim.items[-1]) - if invoice_id: - existing_invoice_ids.add(invoice_id) - - def _resolve_document_item_type(self, document: dict[str, Any], *, fallback: str) -> str: - scene_code = str(document.get("scene_code") or "").strip() - if scene_code in {"travel", "hotel", "transport", "meal", "office", "meeting", "training"}: - return scene_code - - document_type = str(document.get("document_type") or "").strip() - if document_type in {"flight_itinerary", "train_ticket"}: - return "travel" - if document_type in {"taxi_receipt", "parking_toll_receipt", "transport_receipt"}: - return "transport" - if document_type == "hotel_invoice": - return "hotel" - if document_type == "meal_receipt": - return "meal" - if document_type == "office_invoice": - return "office" - if document_type == "meeting_invoice": - return "meeting" - if document_type == "training_invoice": - return "training" - - scene_label = str(document.get("scene_label") or "").strip() - if "交通" in scene_label: - return "transport" - if "住宿" in scene_label: - return "hotel" - if "餐" in scene_label: - return "meal" - if "会务" in scene_label or "会议" in scene_label: - return "meeting" - if "培训" in scene_label: - return "training" - return fallback or "other" - - def _resolve_document_item_amount(self, document: dict[str, Any]) -> Decimal | None: - for field in list(document.get("document_fields") or []): - if not isinstance(field, dict): - continue - key = str(field.get("key") or "").strip().lower().replace("_", "") - label = str(field.get("label") or "").replace(" ", "") - value = self._parse_document_amount_value(str(field.get("value") or "")) - if value is None: - continue - if key in { - "amount", - "totalamount", - "paymentamount", - "paidamount", - "actualamount", - } or any( - token in label - for token in ("金额", "价税合计", "合计", "总额", "总计", "票价", "支付金额", "实付金额", "实收金额") - ): - return value - - text = " ".join( - [ - str(document.get("summary") or "").strip(), - str(document.get("text") or "").strip(), - ] - ).strip() - return self._parse_document_amount_value(text) - - def _parse_document_amount_value(self, value: str) -> Decimal | None: - raw_value = str(value or "").strip() - if not raw_value: - return None - for pattern in DOCUMENT_AMOUNT_PATTERNS: - match = pattern.search(raw_value) - if not match: - continue - numeric = str(match.group(1) or "").replace(",", ".").strip() - try: - amount = Decimal(numeric).quantize(Decimal("0.01")) - except (InvalidOperation, ValueError): - continue - if amount > Decimal("0.00"): - return amount - return None - - def _resolve_document_item_date(self, document: dict[str, Any], *, fallback: date) -> date: - for field in list(document.get("document_fields") or []): - if not isinstance(field, dict): - continue - key = str(field.get("key") or "").strip().lower().replace("_", "") - label = str(field.get("label") or "").replace(" ", "") - value = str(field.get("value") or "").strip() - if not value: - continue - if key in {"date", "time", "issuedat", "invoicedate"} or any( - token in label for token in ("日期", "时间", "开票日期", "发生时间") - ): - parsed = self._parse_document_date(value) - if parsed is not None: - return parsed - - parsed = self._parse_document_date( - " ".join( - [ - str(document.get("summary") or "").strip(), - str(document.get("text") or "").strip(), - ] - ).strip() - ) - return parsed or fallback - - @staticmethod - def _parse_document_date(value: str) -> date | None: - match = DOCUMENT_DATE_PATTERN.search(str(value or "")) - if not match: - return None - raw_value = str(match.group(1) or "").strip() - normalized = raw_value.replace("年", "-").replace("月", "-").replace("日", "") - normalized = normalized.replace("/", "-").replace(".", "-") - parts = [part for part in normalized.split("-") if part] - if len(parts) != 3: - return None - try: - return date(int(parts[0]), int(parts[1]), int(parts[2])) - except ValueError: - return None - - def _upsert_primary_item( - self, - *, - claim: ExpenseClaim, - occurred_at: datetime, - expense_type: str, - amount: Decimal, - reason: str, - location: str, - attachment_names: list[str], - ) -> None: - item = claim.items[0] if claim.items else None - if item is None: - item = ExpenseClaimItem( - claim_id=claim.id, - item_date=occurred_at.date(), - item_type=expense_type, - item_reason=reason, - item_location=location, - item_amount=amount, - invoice_id=attachment_names[0] if attachment_names else None, - ) - claim.items.append(item) - self.db.add(item) - return - - item.item_date = occurred_at.date() - item.item_type = expense_type - item.item_reason = reason - item.item_location = location - item.item_amount = amount - item.invoice_id = attachment_names[0] if attachment_names else item.invoice_id - - def _generate_claim_no(self, occurred_at: datetime) -> str: - month_code = occurred_at.strftime("%Y%m") - prefix = f"EXP-{month_code}-" - existing_claim_nos = list( - self.db.scalars( - select(ExpenseClaim.claim_no).where(ExpenseClaim.claim_no.like(f"{prefix}%")) - ) - ) - max_suffix = 0 - for claim_no in existing_claim_nos: - normalized = str(claim_no or "").strip() - if not normalized.startswith(prefix): - continue - suffix = normalized[len(prefix):] - if not suffix.isdigit(): - continue - max_suffix = max(max_suffix, int(suffix)) - return f"{prefix}{max_suffix + 1:03d}" - - @staticmethod - def _resolve_claim_no_retry_count(context_json: dict[str, Any]) -> int: - try: - return max(0, int(context_json.get("_claim_no_retry_count") or 0)) - except (TypeError, ValueError): - return 0 - - @staticmethod - def _is_claim_no_conflict_error(exc: IntegrityError) -> bool: - message = str(exc).lower() - return ( - "claim_no" in message - and ( - "unique" in message - or "duplicate key" in message - or "ix_expense_claims_claim_no" in message - or "expense_claims.claim_no" in message - ) - ) - - def _count_draft_claims_for_owner( - self, - *, - employee: Employee | None, - user_id: str | None, - ) -> int: - owner_filters = self._build_draft_owner_filters( - employee=employee, - user_id=user_id, - ) - if not owner_filters: - return 0 - - stmt = ( - select(func.count()) - .select_from(ExpenseClaim) - .where(ExpenseClaim.status == "draft") - .where(or_(*owner_filters)) - ) - return int(self.db.scalar(stmt) or 0) - - def _build_draft_owner_filters( - self, - *, - employee: Employee | None, - user_id: str | None, - ) -> list[Any]: - conditions: list[Any] = [] - seen: set[tuple[str, str]] = set() - - def add_condition(field_name: str, value: str | None) -> None: - normalized = str(value or "").strip() - if not normalized or normalized == "待补充": - return - - marker = (field_name, normalized.lower()) - if marker in seen: - return - seen.add(marker) - - if field_name == "employee_id": - conditions.append(ExpenseClaim.employee_id == normalized) - return - conditions.append(ExpenseClaim.employee_name == normalized) - - if employee is not None: - add_condition("employee_id", employee.id) - add_condition("employee_name", employee.email) - if self._employee_name_is_unique(employee): - add_condition("employee_name", employee.name) - - add_condition("employee_name", user_id) - return conditions - - def _resolve_employee( - self, - *, - ontology: OntologyParseResult, - context_json: dict[str, Any], - user_id: str | None, - ) -> Employee | None: - normalized_user_id = str(user_id or "").strip() - if normalized_user_id: - stmt = select(Employee).where(func.lower(Employee.email) == normalized_user_id.lower()).limit(1) - employee = self.db.scalar(stmt) - if employee is not None: - return employee - - employee_name = self._resolve_employee_name( - ontology=ontology, - context_json=context_json, - user_id=None, - ) - if not employee_name: - return None - - stmt = select(Employee).where(Employee.name == employee_name).limit(1) - return self.db.scalar(stmt) - - @staticmethod - def _resolve_employee_name( - *, - ontology: OntologyParseResult, - context_json: dict[str, Any], - user_id: str | None, - fallback: str = "待补充", - ) -> str: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - for key in ("reporter_name", "employee_name", "claimant_name"): - value = str(review_form_values.get(key) or "").strip() - if value: - return value - for item in ontology.entities: - if item.type == "employee" and item.value.strip(): - return item.value.strip() - for key in ("name", "user_name", "employee_name"): - value = str(context_json.get(key) or "").strip() - if value: - return value - return str(user_id or fallback).strip() or fallback - - @staticmethod - def _resolve_department_name( - *, - employee: Employee | None, - context_json: dict[str, Any], - fallback: str = "待补充", - ) -> str: - if employee is not None and employee.organization_unit is not None: - return employee.organization_unit.name - - request_context = context_json.get("request_context") - if isinstance(request_context, dict): - for key in ("department", "department_name", "deptName"): - value = str(request_context.get(key) or "").strip() - if value: - return value - - for key in ("department_name", "department"): - value = str(context_json.get(key) or "").strip() - if value: - return value - return fallback - - @staticmethod - def _resolve_project_code(entities: list[OntologyEntity]) -> str | None: - for item in entities: - if item.type == "project" and item.normalized_value.strip(): - return item.normalized_value.strip() - return None - - @staticmethod - def _resolve_expense_type( - entities: list[OntologyEntity], - *, - context_json: dict[str, Any], - ) -> str | None: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - compact = str( - review_form_values.get("expense_type") - or review_form_values.get("reimbursement_type") - or "" - ).replace(" ", "") - if compact: - if "招待" in compact or ("客户" in compact and any(word in compact for word in ("吃饭", "宴请", "请客", "用餐"))): - return "entertainment" - if any(word in compact for word in ("差旅", "出差", "机票", "行程")): - return "travel" - if any(word in compact for word in ("住宿", "酒店", "宾馆")): - return "hotel" - if any(word in compact for word in ("交通", "打车", "网约车", "出租车", "停车", "车费")): - return "transport" - if any(word in compact for word in ("餐费", "用餐", "午餐", "晚餐", "早餐", "伙食")): - return "meal" - if "会务" in compact: - return "meeting" - if any(word in compact for word in ("办公费", "办公用品", "文具", "耗材", "办公耗材", "打印纸", "办公设备", "键盘", "鼠标", "白板")): - return "office" - if any(word in compact for word in ("培训费", "培训", "讲师费", "课时费", "课程费")): - return "training" - if any(word in compact for word in ("通讯费", "话费", "流量费", "宽带费")): - return "communication" - if any(word in compact for word in ("福利费", "团建", "慰问", "节日福利", "体检费")): - return "welfare" - for item in entities: - if item.type == "expense_type": - normalized = item.normalized_value.strip() - if normalized: - return normalized - return None - - @staticmethod - def _resolve_reason( - *, - message: str, - context_json: dict[str, Any], - allow_message_fallback: bool, - ) -> str | None: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - for key in ("reason", "business_reason"): - value = str(review_form_values.get(key) or "").strip() - if value: - return value - - explicit_text = context_json.get("user_input_text") - if isinstance(explicit_text, str): - normalized_explicit_text = explicit_text.strip() - if normalized_explicit_text: - return normalized_explicit_text[:500] - return None - - request_context = context_json.get("request_context") - if ( - isinstance(request_context, dict) - and str(context_json.get("entry_source") or "").strip() == "detail" - ): - for key in ("reason", "title"): - value = str(request_context.get(key) or "").strip() - if value: - return value - if not allow_message_fallback: - return None - - normalized_message = str(message or "").strip() - compact_message = re.sub(r"\s+", "", normalized_message) - if compact_message.startswith(SYSTEM_GENERATED_REASON_PREFIXES): - return None - return normalized_message[:500] or None - - @staticmethod - def _resolve_location(*, message: str, context_json: dict[str, Any]) -> str | None: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - for key in ("business_location", "location"): - value = str(review_form_values.get(key) or "").strip() - if value: - return value - - request_context = context_json.get("request_context") - if ( - isinstance(request_context, dict) - and str(context_json.get("entry_source") or "").strip() == "detail" - ): - for key in ("city", "location"): - value = str(request_context.get(key) or "").strip() - if value: - return value - compact = str(message or "").replace(" ", "") - if "客户现场" in compact: - return "客户现场" - return None - - @staticmethod - def _resolve_occurred_at( - ontology: OntologyParseResult, - *, - context_json: dict[str, Any], - ) -> datetime | None: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - for key in ("occurred_date", "time_range", "business_time"): - value = str(review_form_values.get(key) or "").strip() - if not value: - continue - try: - parsed = date.fromisoformat(value) - return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC) - except ValueError: - continue - - start_date = ontology.time_range.start_date - if start_date: - try: - parsed = date.fromisoformat(start_date) - return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC) - except ValueError: - pass - return None - - @staticmethod - def _resolve_amount( - entities: list[OntologyEntity], - *, - context_json: dict[str, Any], - ) -> Decimal | None: - review_form_values = context_json.get("review_form_values") - if isinstance(review_form_values, dict): - raw_value = str(review_form_values.get("amount") or "").strip() - if raw_value: - compact = raw_value.replace("元", "").replace(",", "").strip() - try: - return Decimal(compact).quantize(Decimal("0.01")) - except (InvalidOperation, ValueError): - pass - for item in entities: - if item.type != "amount" or item.role == "threshold": - continue - try: - return Decimal(item.normalized_value).quantize(Decimal("0.01")) - except (InvalidOperation, ValueError): - continue - return None - - @staticmethod - def _resolve_attachment_names(context_json: dict[str, Any]) -> list[str]: - names = context_json.get("attachment_names") - if not isinstance(names, list): - return [] - return [str(name).strip() for name in names if str(name).strip()] - - def _resolve_attachment_count(self, context_json: dict[str, Any]) -> int: - names = self._resolve_attachment_names(context_json) - if names: - return len(names) - try: - return max(0, int(context_json.get("attachment_count") or 0)) - except (TypeError, ValueError): - return 0 - - def _get_claim_item_or_raise( - self, - *, - claim_id: str, - item_id: str, - current_user: CurrentUserContext, - ) -> tuple[ExpenseClaim | None, ExpenseClaimItem]: - claim = self.get_claim(claim_id, current_user) - if claim is None: - return None, None # type: ignore[return-value] - - item = next((entry for entry in claim.items if entry.id == item_id), None) - if item is None: - raise LookupError("Item not found") - return claim, item - - def _get_attachment_storage_root(self) -> Path: - return (get_settings().resolved_storage_root_dir / "expense_claims").resolve() - - def _build_item_attachment_dir(self, claim_id: str, item_id: str) -> Path: - return (self._get_attachment_storage_root() / claim_id / item_id).resolve() - - def _delete_claim_attachment_root(self, claim_id: str) -> None: - shutil.rmtree((self._get_attachment_storage_root() / claim_id).resolve(), ignore_errors=True) - - @staticmethod - def _normalize_attachment_filename(filename: str | None) -> str: - normalized = Path(str(filename or "").strip()).name - normalized = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", normalized).strip("._") - suffix = Path(normalized).suffix - if normalized: - return normalized - return f"attachment{suffix or '.bin'}" - - def _resolve_attachment_path(self, storage_key: str | None) -> Path | None: - normalized = str(storage_key or "").strip() - if not normalized: - return None - - root = self._get_attachment_storage_root() - path = (root / normalized).resolve() - try: - path.relative_to(root) - except ValueError as exc: - raise FileNotFoundError("Attachment path is invalid") from exc - return path - - def _to_attachment_storage_key(self, file_path: Path) -> str: - root = self._get_attachment_storage_root() - return file_path.resolve().relative_to(root).as_posix() - - def _resolve_item_attachment_content(self, item: ExpenseClaimItem) -> tuple[Path, str, str]: - file_path = self._resolve_attachment_path(item.invoice_id) - if file_path is None or not file_path.exists(): - raise FileNotFoundError("Attachment not found") - - metadata = self._read_attachment_meta(file_path) - filename = str(metadata.get("file_name") or file_path.name) - media_type = self._resolve_attachment_media_type( - filename, - fallback=str(metadata.get("media_type") or ""), - ) - return file_path, media_type, filename - - def _delete_item_attachment_files(self, item: ExpenseClaimItem) -> None: - file_path = self._resolve_attachment_path(item.invoice_id) - if file_path is None: - return - - root = self._get_attachment_storage_root() - if file_path.parent == root: - file_path.unlink(missing_ok=True) - self._attachment_meta_path(file_path).unlink(missing_ok=True) - return - - shutil.rmtree(file_path.parent, ignore_errors=True) - - @staticmethod - def _attachment_meta_path(file_path: Path) -> Path: - return file_path.with_name(f"{file_path.name}.meta.json") - - def _write_attachment_meta(self, file_path: Path, payload: dict[str, Any]) -> None: - meta_path = self._attachment_meta_path(file_path) - meta_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") - - def _read_attachment_meta(self, file_path: Path) -> dict[str, Any]: - meta_path = self._attachment_meta_path(file_path) - if not meta_path.exists(): - return {} - - try: - payload = json.loads(meta_path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError): - return {} - return payload if isinstance(payload, dict) else {} - - def _build_attachment_preview_meta( - self, - *, - file_path: Path, - media_type: str, - ocr_document: Any | None, - ) -> dict[str, Any]: - filename = file_path.name - storage_key = self._to_attachment_storage_key(file_path) - preview_kind = self._resolve_preview_kind(media_type, filename) - - preview_data_url = str(getattr(ocr_document, "preview_data_url", "") or "").strip() - preview_source_kind = str(getattr(ocr_document, "preview_kind", "") or "").strip() - if preview_source_kind == "image" and preview_data_url: - preview_asset = self._write_preview_asset_from_data_url( - attachment_dir=file_path.parent, - original_filename=filename, - preview_data_url=preview_data_url, - ) - if preview_asset is not None: - preview_path, preview_media_type, preview_file_name = preview_asset - return { - "previewable": True, - "preview_kind": "image", - "preview_storage_key": self._to_attachment_storage_key(preview_path), - "preview_media_type": preview_media_type, - "preview_file_name": preview_file_name, - } - - if preview_kind: - return { - "previewable": True, - "preview_kind": preview_kind, - "preview_storage_key": storage_key, - "preview_media_type": media_type, - "preview_file_name": filename, - } - - return { - "previewable": False, - "preview_kind": "", - "preview_storage_key": "", - "preview_media_type": "", - "preview_file_name": "", - } - - def _resolve_item_attachment_preview_content(self, item: ExpenseClaimItem) -> tuple[Path, str, str]: - file_path, media_type, filename = self._resolve_item_attachment_content(item) - metadata = self._read_attachment_meta(file_path) - preview_storage_key = str(metadata.get("preview_storage_key") or "").strip() - preview_file_name = str(metadata.get("preview_file_name") or "").strip() - preview_media_type = str(metadata.get("preview_media_type") or "").strip() - - if preview_storage_key: - preview_path = self._resolve_attachment_path(preview_storage_key) - if preview_path is not None and preview_path.exists(): - resolved_name = preview_file_name or preview_path.name - resolved_media_type = self._resolve_attachment_media_type( - resolved_name, - fallback=preview_media_type, - ) - return preview_path, resolved_media_type, resolved_name - - if self._is_previewable_media_type(media_type, filename): - return file_path, media_type, filename - - raise FileNotFoundError("Attachment preview not found") - - def _build_attachment_payload(self, item: ExpenseClaimItem) -> dict[str, Any]: - file_path, media_type, filename = self._resolve_item_attachment_content(item) - metadata = self._read_attachment_meta(file_path) - uploaded_at_value = metadata.get("uploaded_at") - uploaded_at = None - if isinstance(uploaded_at_value, str) and uploaded_at_value.strip(): - try: - uploaded_at = datetime.fromisoformat(uploaded_at_value) - except ValueError: - uploaded_at = None - - analysis = metadata.get("analysis") - if not isinstance(analysis, dict): - analysis = None - - document_info = metadata.get("document_info") - if not isinstance(document_info, dict): - document_info = None - - requirement_check = metadata.get("requirement_check") - if not isinstance(requirement_check, dict): - requirement_check = None - - preview_kind = str(metadata.get("preview_kind") or "").strip() - previewable = bool(metadata.get("previewable", self._is_previewable_media_type(media_type, filename))) - preview_url = self._build_attachment_preview_client_path(item.claim_id, item.id) if previewable else "" - - return { - "file_name": str(metadata.get("file_name") or filename), - "storage_key": str(item.invoice_id or ""), - "media_type": str(metadata.get("media_type") or media_type), - "size_bytes": int(metadata.get("size_bytes") or file_path.stat().st_size), - "uploaded_at": uploaded_at, - "previewable": previewable, - "preview_kind": preview_kind or self._resolve_preview_kind(media_type, filename), - "preview_url": preview_url, - "analysis": analysis, - "document_info": document_info, - "requirement_check": requirement_check, - } - - @staticmethod - def _resolve_preview_kind(media_type: str | None, filename: str) -> str: - resolved = str(media_type or "").strip() or (mimetypes.guess_type(filename)[0] or "") - if resolved.startswith("image/"): - return "image" - if resolved == "application/pdf": - return "pdf" - return "" - - @staticmethod - def _decode_data_url(payload: str) -> tuple[str, bytes] | None: - normalized = str(payload or "").strip() - matched = re.match(r"^data:(?P[\w.+-]+/[\w.+-]+);base64,(?P.+)$", normalized, flags=re.DOTALL) - if not matched: - return None - try: - content = base64.b64decode(matched.group("body"), validate=True) - except (binascii.Error, ValueError): - return None - return matched.group("media"), content - - def _write_preview_asset_from_data_url( - self, - *, - attachment_dir: Path, - original_filename: str, - preview_data_url: str, - ) -> tuple[Path, str, str] | None: - decoded = self._decode_data_url(preview_data_url) - if decoded is None: - return None - - preview_media_type, preview_content = decoded - suffix = mimetypes.guess_extension(preview_media_type) or ".bin" - preview_name = f"{Path(original_filename).stem}.preview{suffix}" - preview_path = attachment_dir / preview_name - preview_path.write_bytes(preview_content) - return preview_path, preview_media_type, preview_name - - @staticmethod - def _build_attachment_preview_client_path(claim_id: str, item_id: str) -> str: - return ( - "/reimbursements/claims/" - f"{quote(str(claim_id or '').strip(), safe='')}" - f"/items/{quote(str(item_id or '').strip(), safe='')}/attachment/preview" - ) - - @staticmethod - def _resolve_attachment_media_type(filename: str, *, fallback: str | None = None) -> str: - guessed = mimetypes.guess_type(filename)[0] - return str(guessed or fallback or "application/octet-stream") - - @staticmethod - def _is_previewable_media_type(media_type: str | None, filename: str) -> bool: - resolved = str(media_type or "").strip() or (mimetypes.guess_type(filename)[0] or "") - return resolved.startswith("image/") or resolved == "application/pdf" - - @staticmethod - def _resolve_attachment_display_name(storage_key: str | None) -> str: - return Path(str(storage_key or "").strip()).name - - def _build_attachment_document_info(self, document: Any) -> dict[str, Any]: - insight = build_document_insight( - filename=str(getattr(document, "filename", "") or ""), - summary=str(getattr(document, "summary", "") or ""), - text=str(getattr(document, "text", "") or ""), - ) - raw_fields = list(getattr(document, "document_fields", []) or []) - normalized_fields: list[dict[str, str]] = [] - for item in raw_fields: - key = "" - label = "" - value = "" - if isinstance(item, dict): - key = str(item.get("key") or "").strip() - label = str(item.get("label") or "").strip() - value = str(item.get("value") or "").strip() - else: - key = str(getattr(item, "key", "") or "").strip() - label = str(getattr(item, "label", "") or "").strip() - value = str(getattr(item, "value", "") or "").strip() - if key and label and value: - normalized_fields.append( - { - "key": key, - "label": label, - "value": value, - } - ) - - if not normalized_fields: - normalized_fields = [ - { - "key": field.key, - "label": field.label, - "value": field.value, - } - for field in insight.fields - if field.value - ] - - document_type = str(getattr(document, "document_type", "") or "").strip() - if document_type in {"", "other"}: - document_type = insight.document_type - - document_type_label = str(getattr(document, "document_type_label", "") or "").strip() - if not document_type_label or document_type_label == "其他单据": - document_type_label = insight.document_type_label - - scene_code = str(getattr(document, "scene_code", "") or "").strip() - if scene_code in {"", "other"}: - scene_code = insight.scene_code - - scene_label = str(getattr(document, "scene_label", "") or "").strip() - if not scene_label or scene_label == "其他票据": - scene_label = insight.scene_label - - return { - "document_type": document_type, - "document_type_label": document_type_label, - "scene_code": scene_code, - "scene_label": scene_label, - "fields": normalized_fields, - } - - def _build_attachment_requirement_check( - self, - *, - item: ExpenseClaimItem, - document_info: dict[str, Any], - ) -> dict[str, Any]: - expense_type = str(item.item_type or "").strip().lower() or "other" - policy = self._get_expense_scene_policy(expense_type) - expense_label = policy.label if policy is not None else self._resolve_expense_type_label(expense_type) - allowed_scenes = set(policy.allowed_scene_codes) if policy is not None else set() - allowed_document_types = set(policy.allowed_document_types) if policy is not None else set() - allowed_scene_labels = [self._resolve_document_scene_label(code) for code in sorted(allowed_scenes)] - allowed_document_type_labels = [ - resolve_document_type_label(document_type) - for document_type in sorted(allowed_document_types) - ] - recognized_scene_code = str(document_info.get("scene_code") or "other").strip() or "other" - recognized_scene_label = str( - document_info.get("scene_label") or self._resolve_document_scene_label(recognized_scene_code) - ).strip() - recognized_document_type = str(document_info.get("document_type") or "other").strip() or "other" - recognized_document_type_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据" - matches = ( - (not allowed_scenes and not allowed_document_types) - or recognized_scene_code in allowed_scenes - or recognized_document_type in allowed_document_types - ) - - if matches: - if allowed_scene_labels or allowed_document_type_labels: - message = ( - f"当前费用项目为{expense_label},已识别为{recognized_document_type_label}," - f"符合当前{expense_label}场景的附件要求。" - ) - else: - message = f"当前费用项目为{expense_label},已识别为{recognized_document_type_label}。" - else: - expected_parts = [label + "相关票据" for label in allowed_scene_labels] - expected_parts.extend(allowed_document_type_labels) - expected_text = "、".join(dict.fromkeys(part for part in expected_parts if part)) or "对应场景票据" - message = ( - f"当前费用项目为{expense_label},要求上传{expected_text};" - f"当前识别为{recognized_document_type_label},不符合当前场景,建议过滤或更换附件。" - ) - - return { - "matches": matches, - "current_expense_type": expense_type, - "current_expense_type_label": expense_label, - "allowed_scene_labels": allowed_scene_labels, - "allowed_document_type_labels": allowed_document_type_labels, - "recognized_scene_code": recognized_scene_code, - "recognized_scene_label": recognized_scene_label, - "recognized_document_type": recognized_document_type, - "recognized_document_type_label": recognized_document_type_label, - "mismatch_severity": policy.attachment_mismatch_severity if policy is not None else "high", - "rule_code": policy.rule_code if policy is not None else DEFAULT_SCENE_RULE_ASSET_CODE, - "rule_name": policy.rule_name if policy is not None else "报销场景提交与附件标准", - "message": message, - } - - @staticmethod - def _resolve_document_scene_label(scene_code: str) -> str: - normalized = str(scene_code or "").strip().lower() - return DOCUMENT_SCENE_LABELS.get(normalized, "其他票据") - - @staticmethod - def _extract_amount_candidates(text: str) -> list[Decimal]: - values: list[Decimal] = [] - seen: set[Decimal] = set() - - def append_candidate(raw: str) -> None: - compact = str(raw or "").replace(",", ".").strip() - if not compact: - return - try: - candidate = Decimal(compact).quantize(Decimal("0.01")) - except (InvalidOperation, ValueError): - return - if candidate in seen: - return - seen.add(candidate) - values.append(candidate) - - for pattern in ( - r"(?:金额|价税合计|合计|小写|实收金额|支付金额|订单金额|总额|票价|房费|餐费)[::\s¥¥]*([0-9]{1,6}(?:[.,][0-9]{1,2})?)", - r"[¥¥]\s*([0-9]{1,6}(?:[.,][0-9]{1,2})?)", - r"([0-9]{1,6}(?:[.,][0-9]{1,2})?)\s*元", - ): - for raw in re.findall(pattern, text, flags=re.IGNORECASE): - append_candidate(raw) - - if values: - return values - - for raw in re.findall(r"(? bool: - return bool(re.search(r"(20\d{2}[年/\-.]\d{1,2}[月/\-.]\d{1,2}日?)", text)) - - @staticmethod - def _normalize_match_text(text: str) -> str: - return re.sub(r"\s+", "", str(text or "")).lower() - - @staticmethod - def _resolve_expense_type_label(expense_type: str | None) -> str: - normalized = str(expense_type or "").strip().lower() - return EXPENSE_TYPE_LABELS.get(normalized, "其他") - - def _resolve_allowed_document_scenes(self, expense_type: str | None) -> set[str]: - normalized = str(expense_type or "").strip().lower() - policy = self._get_expense_scene_policy(normalized) - return set(policy.allowed_scene_codes) if policy is not None else set() - - def _detect_expense_scenes(self, text: str) -> dict[str, list[str]]: - normalized = self._normalize_match_text(text) - if not normalized: - return {} - - matches: dict[str, list[str]] = {} - for scene, keywords in EXPENSE_SCENE_KEYWORDS.items(): - matched = [keyword for keyword in keywords if keyword in normalized] - if matched: - matches[scene] = matched[:3] - return matches - - def _format_scene_labels(self, scene_codes: set[str]) -> str: - labels = [self._resolve_expense_type_label(code) for code in scene_codes] - unique_labels = list(dict.fromkeys(label for label in labels if label)) - return "、".join(unique_labels) if unique_labels else "其他" - - def _build_purpose_mismatch_point( - self, - *, - item: ExpenseClaimItem, - document_scenes: set[str], - ) -> str | None: - if not document_scenes: - return None - - allowed_scenes = self._resolve_allowed_document_scenes(item.item_type) - reason_text = str(item.item_reason or "").strip() - reason_scenes = set(self._detect_expense_scenes(reason_text).keys()) - document_scene_labels = self._format_scene_labels(document_scenes) - - if reason_scenes and document_scenes.isdisjoint(reason_scenes): - return ( - f"用途字段:用户填写用途“{reason_text[:24]}”与票据内容不一致," - f"当前附件更像{document_scene_labels}相关材料。" - ) - - if allowed_scenes and document_scenes.isdisjoint(allowed_scenes): - expense_label = self._resolve_expense_type_label(item.item_type) - return f"用途字段:当前费用项目为{expense_label},但附件内容更像{document_scene_labels}相关票据。" - - return None - - def _build_fallback_attachment_analysis( - self, - *, - media_type: str | None, - item: ExpenseClaimItem, - ) -> dict[str, Any]: - return { - "severity": "medium", - "label": "中风险", - "headline": "AI提示:附件已上传,待识别结果", - "summary": "附件已成功保存,但当前尚未拿到有效识别结果,建议人工先核对票据内容。", - "points": [ - f"附件格式:{self._resolve_attachment_media_type('attachment', fallback=media_type)}", - f"费用金额:当前明细金额为 {item.item_amount} 元", - ], - "suggestion": "建议打开附件确认金额、日期和票据类型是否完整,再继续提交审批。", - } - - def _build_failed_ocr_attachment_analysis( - self, - *, - media_type: str | None, - error_message: str, - item: ExpenseClaimItem, - ) -> dict[str, Any]: - return { - "severity": "medium", - "label": "中风险", - "headline": "AI提示:附件已上传,但识别失败", - "summary": "文件已经保存成功,但本次 AI 识别未完成,因此无法给出完整票据核验结论。", - "points": [ - f"识别异常:{error_message or 'OCR 服务暂不可用'}", - f"费用金额:当前明细金额为 {item.item_amount} 元", - f"附件格式:{self._resolve_attachment_media_type('attachment', fallback=media_type)}", - ], - "suggestion": "建议重新上传更清晰的票据图片,或稍后重试识别后再提交。", - } - - def _build_attachment_analysis( - self, - *, - document: Any, - item: ExpenseClaimItem, - document_info: dict[str, Any] | None = None, - requirement_check: dict[str, Any] | None = None, - ) -> dict[str, Any]: - warnings = [str(value).strip() for value in list(getattr(document, "warnings", []) or []) if str(value).strip()] - text = " ".join( - [ - str(getattr(document, "summary", "") or "").strip(), - str(getattr(document, "text", "") or "").strip(), - ] - ).strip() - compact_text = text.replace(" ", "") - avg_score = float(getattr(document, "avg_score", 0.0) or 0.0) - line_count = int(getattr(document, "line_count", 0) or 0) - document_info = document_info or self._build_attachment_document_info(document) - requirement_check = requirement_check or self._build_attachment_requirement_check( - item=item, - document_info=document_info, - ) - document_scene_matches = self._detect_expense_scenes(text) - purpose_mismatch_point = self._build_purpose_mismatch_point( - item=item, - document_scenes=set(document_scene_matches.keys()), - ) - recognized_document_type = str(document_info.get("document_type") or "other").strip().lower() or "other" - recognized_document_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据" - requirement_matches = bool(requirement_check.get("matches")) - mismatch_severity = str(requirement_check.get("mismatch_severity") or "high").strip().lower() or "high" - - has_ticket_keyword = any( - keyword in compact_text - for keyword in ( - "发票", - "票据", - "增值税", - "电子行程单", - "购买方", - "销售方", - "税额", - "价税", - "票号", - "发票代码", - "凭证", - ) - ) - amount_candidates = self._extract_amount_candidates(text) - item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) - has_matching_amount = any(abs(candidate - item_amount) <= Decimal("1.00") for candidate in amount_candidates) - has_date_text = self._has_date_like_text(text) - amount_mismatch = bool(amount_candidates) and item_amount > Decimal("0.00") and not has_matching_amount - - points: list[str] = [] - if warnings: - points.append(f"识别提示:{warnings[0]}") - if line_count == 0 or not compact_text: - points.append("附件内容:未识别到有效文字,当前附件更像普通图片或内容过于模糊。") - if recognized_document_type == "other" and not has_ticket_keyword: - points.append("票据类型:未识别到发票、票据、电子行程单等关键字,暂无法判断票据类型。") - if not amount_candidates: - points.append("金额字段:未识别到可用于核对的金额。") - elif amount_mismatch: - candidate_text = "、".join(str(candidate) for candidate in amount_candidates[:3]) - points.append(f"金额字段:附件识别金额 {candidate_text} 元与报销金额 {item_amount} 元不一致。") - if not has_date_text: - points.append("日期字段:未识别到开票日期或业务发生日期。") - if not requirement_matches: - points.append(f"附件类型要求:{requirement_check.get('message')}") - if purpose_mismatch_point: - points.append(purpose_mismatch_point) - if avg_score and avg_score < 0.72: - points.append(f"识别质量:OCR 置信度偏低({avg_score:.0%}),可能影响票据核验准确性。") - - issue_count = len(points) - if issue_count == 0: - return { - "severity": "pass", - "label": "AI提示符合条件", - "headline": "AI提示:附件符合基础校验条件", - "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", - "points": [ - f"票据类型:已识别为{recognized_document_label}。", - f"附件类型要求:{requirement_check.get('message')}", - f"金额字段:已识别到与当前明细接近的金额 {item_amount} 元。", - ], - "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。", - } - - severity = "low" - label = "低风险" - headline = "AI提示:附件存在轻微待核对项" - summary = "当前附件已识别出部分票据要素,但仍建议人工继续复核。" - - if ( - line_count == 0 - or not compact_text - or (recognized_document_type == "other" and not has_ticket_keyword and issue_count >= 2) - or (not requirement_matches and mismatch_severity == "high") - or (purpose_mismatch_point and amount_mismatch) - ): - severity = "high" - label = "高风险" - headline = "AI提示:附件不符合票据校验条件" - summary = "当前附件存在明显异常,票据类型与当前费用场景不匹配,或无法作为有效报销材料。" - elif ( - purpose_mismatch_point - or amount_mismatch - or issue_count >= 2 - or warnings - or (avg_score and avg_score < 0.72) - or (not requirement_matches and mismatch_severity in {"medium", "low"}) - ): - severity = "medium" - label = "中风险" - headline = "AI提示:附件存在明显待整改项" - summary = "当前附件可见部分内容,但金额、用途、日期或附件类型仍有缺失或不一致。" - - suggestion = { - "high": "建议过滤当前不匹配的票据,重新上传符合当前费用场景的清晰原件。", - "medium": "建议根据风险点补齐清晰票据,或修正金额、日期、费用说明后再提交。", - "low": "建议人工再次核对金额和业务说明,确认后可继续流转。", - }[severity] - - return { - "severity": severity, - "label": label, - "headline": headline, - "summary": summary, - "points": points, - "suggestion": suggestion, - } - - @staticmethod - def _serialize_claim(claim: ExpenseClaim) -> dict[str, Any]: - return { - "id": claim.id, - "claim_no": claim.claim_no, - "employee_name": claim.employee_name, - "department_name": claim.department_name, - "project_code": claim.project_code, - "expense_type": claim.expense_type, - "reason": claim.reason, - "location": claim.location, - "amount": float(claim.amount), - "invoice_count": int(claim.invoice_count or 0), - "status": claim.status, - "approval_stage": claim.approval_stage, - "risk_flags_json": list(claim.risk_flags_json or []), - } - - @staticmethod - def _normalize_optional_text(value: str | None, *, fallback: str = "", allow_empty: bool = False) -> str | None: - normalized = str(value or "").strip() - if normalized: - return normalized - if allow_empty: - return None - return fallback - - @staticmethod - def _normalize_sort_datetime(value: datetime | None) -> datetime: - if value is None: - return datetime.max.replace(tzinfo=UTC) - if value.tzinfo is None: - return value.replace(tzinfo=UTC) - return value - - @staticmethod - def _is_missing_value(value: Any) -> bool: - text = str(value or "").strip() - if not text: - return True - compact = text.replace(" ", "") - return compact in {"待补充", "暂无", "无", "未知", "处理中"} - - def _ensure_draft_claim(self, claim: ExpenseClaim) -> None: - normalized_status = str(claim.status or "").strip().lower() - if normalized_status not in {"draft", "supplement"}: - raise ValueError("只有草稿或待补充状态的报销单才允许执行该操作。") - - def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]: - base_flags = list(claim.risk_flags_json or []) - attachment_flags = [ - flag - for flag in base_flags - if isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis" - ] - preserved_flags = [ - flag - for flag in base_flags - if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "submission_review") - ] - - review_flags: list[dict[str, Any]] = [] - blocking_reasons: list[str] = [] - - high_attachment_flags = [ - flag - for flag in attachment_flags - if str(flag.get("severity") or "").strip().lower() == "high" - ] - medium_attachment_flags = [ - flag - for flag in attachment_flags - if str(flag.get("severity") or "").strip().lower() == "medium" - ] - if high_attachment_flags: - blocking_reasons.append("存在高风险票据,需先补充或更换附件后再提交。") - review_flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "AI验审拦截", - "message": f"AI验审发现 {len(high_attachment_flags)} 条高风险附件,已退回待补充。", - } - ) - elif medium_attachment_flags: - review_flags.append( - { - "source": "submission_review", - "severity": "medium", - "label": "AI验审提醒", - "message": f"AI验审发现 {len(medium_attachment_flags)} 条中风险附件,已随单流转给审批人复核。", - } - ) - - manager_name = self._resolve_claim_manager_name(claim) - if not manager_name: - blocking_reasons.append("未识别到该员工的直属领导,暂时无法流转到领导审批。") - review_flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "审批链缺失", - "message": "AI验审通过前检查到直属领导缺失,当前无法继续流转审批链。", - } - ) - - historical_risk_count = self._count_recent_risky_claims(claim) - if historical_risk_count >= AI_REVIEW_REPEAT_RISK_BLOCK_COUNT: - review_flags.append( - { - "source": "submission_review", - "severity": "medium", - "label": "历史风险偏高", - "message": ( - f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," - "本次已追加到审批链重点关注。" - ), - } - ) - elif historical_risk_count >= AI_REVIEW_REPEAT_RISK_WARNING_COUNT: - review_flags.append( - { - "source": "submission_review", - "severity": "low", - "label": "历史风险提醒", - "message": ( - f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," - "建议直属领导重点复核。" - ), - } - ) - - travel_review = self._run_travel_policy_review(claim) - blocking_reasons.extend(travel_review["blocking_reasons"]) - review_flags.extend(travel_review["flags"]) - - scene_policy_review = self._run_scene_policy_review(claim) - blocking_reasons.extend(scene_policy_review["blocking_reasons"]) - review_flags.extend(scene_policy_review["flags"]) - - if blocking_reasons: - summary_message = "AI验审未通过:" + ";".join(dict.fromkeys(blocking_reasons)) - review_flags.insert( - 0, - { - "source": "submission_review", - "severity": "high", - "label": "AI验审未通过", - "message": summary_message, - }, - ) - return { - "status": "supplement", - "approval_stage": "待补充", - "risk_flags": preserved_flags + review_flags, - "message": summary_message, - "passed": False, - } - - return { - "status": "submitted", - "approval_stage": "直属领导审批", - "risk_flags": preserved_flags + review_flags, - "message": ( - f"报销单 {claim.claim_no} 已完成 AI验审," - f"现已提交给直属领导 {manager_name or '审批人'} 审批。" - ), - "passed": True, - } - - @staticmethod - def _resolve_claim_manager_name(claim: ExpenseClaim) -> str: - if claim.employee is not None: - if claim.employee.manager is not None and claim.employee.manager.name: - return str(claim.employee.manager.name).strip() - if claim.employee.organization_unit is not None and claim.employee.organization_unit.manager_name: - return str(claim.employee.organization_unit.manager_name).strip() - return "" - - def _count_recent_risky_claims(self, claim: ExpenseClaim) -> int: - filters = [] - if claim.employee_id: - filters.append(ExpenseClaim.employee_id == claim.employee_id) - elif claim.employee_name: - filters.append(ExpenseClaim.employee_name == claim.employee_name) - if not filters: - return 0 - - since = datetime.now(UTC) - timedelta(days=AI_REVIEW_LOOKBACK_DAYS) - stmt = ( - select(ExpenseClaim) - .where(or_(*filters)) - .where(ExpenseClaim.id != claim.id) - .where(ExpenseClaim.occurred_at >= since) - ) - recent_claims = list(self.db.scalars(stmt).all()) - return sum(1 for item in recent_claims if list(item.risk_flags_json or [])) - - def _run_scene_policy_review(self, claim: ExpenseClaim) -> dict[str, list[Any]]: - catalog = self._get_expense_rule_catalog() - flags: list[dict[str, Any]] = [] - blocking_reasons: list[str] = [] - reason_corpus = self._build_scene_reason_corpus(claim) - scene_totals: dict[str, Decimal] = defaultdict(lambda: Decimal("0.00")) - scene_warned: set[str] = set() - - for item in claim.items: - item_type = str(item.item_type or claim.expense_type or "other").strip().lower() or "other" - policy = catalog.get_scene_policy(item_type) - if policy is None: - continue - - scene_totals[item_type] += Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) - - if policy.always_warn and item_type not in scene_warned: - scene_warned.add(item_type) - flags.append( - { - "source": "submission_review", - "severity": "medium", - "label": f"{policy.label}人工重点复核", - "message": policy.always_warn_message or f"{policy.label}默认需要人工重点复核。", - "rule_code": policy.rule_code, - } - ) - - item_limit = policy.item_amount_limit - item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) - if item_limit is not None and item_amount > Decimal("0.00"): - exceeded = self._evaluate_amount_limit( - amount=item_amount, - limit_config=item_limit, - reason_text="\n".join( - part - for part in [reason_corpus, str(item.item_reason or "").strip()] - if part - ), - ) - if exceeded is not None: - severity, threshold = exceeded - label = ( - f"{policy.label}金额超标待说明" - if severity == "high" - else f"{policy.label}金额超标提醒" - ) - message = ( - f"{policy.label}当前识别金额为 {item_amount} 元," - f"已超过制度阈值 {threshold} 元。" - ) - if severity == "high": - message += " 当前未识别到例外说明,请先补充原因。" - blocking_reasons.append(f"{policy.label}金额超出制度阈值,且未补充例外说明。") - else: - message += " 已识别到例外说明,请审批人重点复核。" - flags.append( - { - "source": "submission_review", - "severity": severity, - "label": label, - "message": message, - "rule_code": policy.rule_code, - } - ) - - for scene_code, total_amount in scene_totals.items(): - policy = catalog.get_scene_policy(scene_code) - if policy is None or policy.claim_amount_limit is None or total_amount <= Decimal("0.00"): - continue - exceeded = self._evaluate_amount_limit( - amount=total_amount, - limit_config=policy.claim_amount_limit, - reason_text=reason_corpus, - ) - if exceeded is None: - continue - - severity, threshold = exceeded - label = f"{policy.label}合计超标待说明" if severity == "high" else f"{policy.label}合计超标提醒" - message = ( - f"{policy.label}当前合计金额为 {total_amount} 元," - f"已超过制度阈值 {threshold} 元。" - ) - if severity == "high": - message += " 当前未识别到例外说明,请先补充原因。" - blocking_reasons.append(f"{policy.label}合计金额超出制度阈值,且未补充例外说明。") - else: - message += " 已识别到例外说明,请审批人重点复核。" - flags.append( - { - "source": "submission_review", - "severity": severity, - "label": label, - "message": message, - "rule_code": policy.rule_code, - } - ) - - return { - "flags": flags, - "blocking_reasons": list(dict.fromkeys(reason for reason in blocking_reasons if reason)), - } - - @staticmethod - def _evaluate_amount_limit( - *, - amount: Decimal, - limit_config: Any, - reason_text: str, - ) -> tuple[str, Decimal] | None: - block_amount = getattr(limit_config, "block_amount", None) - warn_amount = getattr(limit_config, "warn_amount", None) - exception_keywords = list(getattr(limit_config, "exception_keywords", []) or []) - has_exception = ExpenseClaimService._text_contains_keywords(reason_text, exception_keywords) - - if block_amount is not None and amount > Decimal(block_amount): - return ("medium" if has_exception else "high", Decimal(block_amount)) - if warn_amount is not None and amount > Decimal(warn_amount): - return ("medium", Decimal(warn_amount)) - return None - - def _run_travel_policy_review(self, claim: ExpenseClaim) -> dict[str, list[Any]]: - policy = self._get_expense_rule_catalog().travel_policy - if policy is None: - return {"flags": [], "blocking_reasons": []} - contexts = [ - context - for context in self._build_claim_attachment_contexts(claim) - if self._is_travel_policy_relevant_context(context, policy) - ] - if not contexts: - return {"flags": [], "blocking_reasons": []} - - reason_corpus = self._build_travel_reason_corpus(claim) - has_route_exception = self._text_contains_keywords( - reason_corpus, - policy.route_exception_keywords, - ) - has_standard_exception = self._text_contains_keywords( - reason_corpus, - policy.standard_exception_keywords, - ) - grade_band = self._resolve_travel_policy_band(claim.employee_grade) - band_label = policy.band_labels.get(grade_band or "", str(claim.employee_grade or "").strip() or "当前职级") - - itinerary_segments: list[dict[str, Any]] = [] - itinerary_cities: list[str] = [] - hotel_contexts: list[dict[str, Any]] = [] - flags: list[dict[str, Any]] = [] - blocking_reasons: list[str] = [] - - for context in contexts: - route_segment = self._extract_route_segment(context, policy) - if route_segment and self._is_long_distance_travel_context(context, policy): - itinerary_segments.append( - { - "item": context["item"], - "origin": route_segment[0], - "destination": route_segment[1], - } - ) - itinerary_cities.extend([route_segment[0], route_segment[1]]) - - scene_code = str(context["document_info"].get("scene_code") or "").strip().lower() - document_type = str(context["document_info"].get("document_type") or "").strip().lower() - item_type = str(context["item"].item_type or "").strip().lower() - if "hotel" in {scene_code, document_type, item_type} or document_type == "hotel_invoice": - hotel_contexts.append(context) - - unique_itinerary_cities = list(dict.fromkeys(city for city in itinerary_cities if city)) - expected_destination_city = self._resolve_expected_travel_city( - claim, - contexts, - unique_itinerary_cities, - policy, - ) - - if itinerary_segments: - unique_destinations = list( - dict.fromkeys(segment["destination"] for segment in itinerary_segments if segment["destination"]) - ) - first_origin = str(itinerary_segments[0]["origin"] or "").strip() - last_destination = str(itinerary_segments[-1]["destination"] or "").strip() - - for previous, current in zip(itinerary_segments, itinerary_segments[1:]): - previous_destination = str(previous["destination"] or "").strip() - current_origin = str(current["origin"] or "").strip() - if previous_destination and current_origin and previous_destination != current_origin: - message = ( - f"差旅行程未形成连续链路:上一段到达 {previous_destination}," - f"下一段却从 {current_origin} 出发,请补充中转或改签说明。" - ) - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "行程闭环异常", - "message": message, - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("差旅行程未形成连续闭环,请补充中转、改签或异地出发原因。") - break - - if ( - expected_destination_city - and last_destination - and last_destination not in {expected_destination_city, first_origin} - ): - message = ( - f"差旅行程终点识别为 {last_destination}," - f"与申报目的地 {expected_destination_city} 不一致,请补充多地出差或后续行程说明。" - ) - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "行程终点异常", - "message": message, - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("差旅行程终点与申报目的地不一致,请补充多地出差说明或补齐后续票据。") - - expected_city_set = { - city - for city in (expected_destination_city, first_origin) - if city - } - extra_destinations = [ - city - for city in unique_destinations - if city and city not in expected_city_set - ] - if extra_destinations and not has_route_exception: - destinations_text = "、".join(extra_destinations[:3]) - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "多城市行程待说明", - "message": ( - f"检测到本次差旅涉及 {destinations_text} 多个目的地," - "但当前报销事由未说明中转、多地拜访或改签原因。" - ), - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("检测到多城市差旅行程,但当前未补充中转或多地出差说明。") - - allowed_hotel_cities = { - city - for city in [expected_destination_city, *unique_itinerary_cities] - if city - } - for context in hotel_contexts: - hotel_city = self._extract_hotel_city(context, policy) - if hotel_city and allowed_hotel_cities and hotel_city not in allowed_hotel_cities: - expected_text = "、".join(sorted(allowed_hotel_cities)) - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "酒店地点异常", - "message": ( - f"酒店票据识别城市为 {hotel_city}," - f"与当前差旅目的地/行程城市 {expected_text} 不一致,请补充异地住宿原因。" - ), - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("酒店票据地点与差旅目的地不一致,请补充异地住宿原因或更换附件。") - - if grade_band is None: - continue - - baseline_city = hotel_city or expected_destination_city - city_tier = policy.city_tiers.get(str(baseline_city or "").strip(), "tier_3") - cap = Decimal(policy.hotel_limits[grade_band][city_tier]) - night_count = self._extract_hotel_night_count(context) - item_amount = Decimal(context["item"].item_amount or Decimal("0.00")).quantize(Decimal("0.01")) - nightly_amount = (item_amount / Decimal(max(night_count, 1))).quantize(Decimal("0.01")) - - if nightly_amount <= cap: - continue - - city_tier_label = { - "tier_1": "一线城市", - "tier_2": "重点城市", - "tier_3": "其他城市", - }.get(city_tier, "当前城市") - hotel_message = ( - f"{band_label} 职级在{city_tier_label}的住宿标准为 {cap} 元/晚," - f"当前酒店识别金额约 {nightly_amount} 元/晚。" - ) - item_reason = str(context["item"].item_reason or "").strip() - item_has_exception = self._text_contains_keywords(item_reason, policy.standard_exception_keywords) - if has_standard_exception or item_has_exception: - flags.append( - { - "source": "submission_review", - "severity": "medium", - "label": "住宿超标提醒", - "message": hotel_message + " 已识别到补充说明,请直属领导重点复核。", - "rule_code": policy.rule_code, - } - ) - else: - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "住宿超标待说明", - "message": hotel_message + " 当前未识别到超标说明,请先补充原因。", - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("住宿金额超出当前职级差标,且未补充超标说明。") - - if grade_band is not None: - for context in contexts: - transport_class = self._detect_transport_class(context, policy) - if transport_class is None: - continue - - transport_kind, class_label, class_level = transport_class - allowed_level = policy.transport_limits.get(grade_band, {}).get(transport_kind) - if allowed_level is None or class_level <= allowed_level: - continue - - item_reason = str(context["item"].item_reason or "").strip() - item_has_exception = self._text_contains_keywords(item_reason, policy.standard_exception_keywords) - message = f"{band_label} 职级当前默认不可报销 {class_label}。" - if has_standard_exception or item_has_exception: - flags.append( - { - "source": "submission_review", - "severity": "medium", - "label": "交通舱位超标提醒", - "message": message + " 已识别到补充说明,请审批人重点复核。", - "rule_code": policy.rule_code, - } - ) - else: - flags.append( - { - "source": "submission_review", - "severity": "high", - "label": "交通舱位超标待说明", - "message": message + " 当前未识别到例外说明,请先补充原因。", - "rule_code": policy.rule_code, - } - ) - blocking_reasons.append("交通舱位或席别超出当前职级差标,且未补充例外说明。") - - return { - "flags": flags, - "blocking_reasons": list(dict.fromkeys(reason for reason in blocking_reasons if reason)), - } - - def _build_claim_attachment_contexts(self, claim: ExpenseClaim) -> list[dict[str, Any]]: - contexts: list[dict[str, Any]] = [] - ordered_items = sorted( - claim.items, - key=lambda item: ( - item.item_date or date.max, - self._normalize_sort_datetime(item.created_at), - ), - ) - for index, item in enumerate(ordered_items, start=1): - file_path = self._resolve_attachment_path(item.invoice_id) - if file_path is None or not file_path.exists(): - continue - - metadata = self._read_attachment_meta(file_path) - document_info = metadata.get("document_info") - contexts.append( - { - "index": index, - "item": item, - "document_info": document_info if isinstance(document_info, dict) else {}, - "ocr_text": str(metadata.get("ocr_text") or ""), - "ocr_summary": str(metadata.get("ocr_summary") or ""), - } - ) - return contexts - - def _is_travel_policy_relevant_context( - self, - context: dict[str, Any], - policy: RuntimeTravelPolicy, - ) -> bool: - item = context.get("item") - document_info = context.get("document_info") or {} - item_type = str(getattr(item, "item_type", "") or "").strip().lower() - scene_code = str(document_info.get("scene_code") or "").strip().lower() - document_type = str(document_info.get("document_type") or "").strip().lower() - return ( - item_type in set(policy.relevant_expense_types) - or scene_code in set(policy.relevant_expense_types) - or document_type in {"hotel_invoice", *set(policy.long_distance_document_types)} - ) - - @staticmethod - def _resolve_document_field_value(document_info: dict[str, Any], key: str) -> str: - normalized_key = str(key or "").strip().lower() - for field in list(document_info.get("fields") or []): - if not isinstance(field, dict): - continue - field_key = str(field.get("key") or "").strip().lower() - if field_key == normalized_key: - return str(field.get("value") or "").strip() - return "" - - @staticmethod - def _text_contains_keywords(text: str, keywords: tuple[str, ...] | list[str]) -> bool: - compact = re.sub(r"\s+", "", str(text or "")) - if not compact: - return False - return any(keyword in compact for keyword in keywords) - - def _build_travel_reason_corpus(self, claim: ExpenseClaim) -> str: - parts = [str(claim.reason or "").strip(), str(claim.location or "").strip()] - for item in claim.items: - parts.append(str(item.item_reason or "").strip()) - parts.append(str(item.item_location or "").strip()) - return "\n".join(part for part in parts if part) - - @staticmethod - def _resolve_travel_policy_band(grade: str | None) -> str | None: - normalized = str(grade or "").strip().upper() - if not normalized: - return None - - p_match = re.search(r"P(\d+)", normalized) - if p_match: - level = int(p_match.group(1)) - if level <= 3: - return "junior" - if level <= 5: - return "mid" - return "senior" - - m_match = re.search(r"M(\d+)", normalized) - if m_match: - level = int(m_match.group(1)) - if level <= 2: - return "manager" - return "executive" - - if normalized.startswith("D"): - return "executive" - return None - - def _resolve_expected_travel_city( - self, - claim: ExpenseClaim, - contexts: list[dict[str, Any]], - itinerary_cities: list[str], - policy: RuntimeTravelPolicy, - ) -> str: - claim_city = self._extract_city_from_text(str(claim.location or ""), policy) - if claim_city: - return claim_city - - for context in contexts: - hotel_city = self._extract_hotel_city(context, policy) - if hotel_city: - return hotel_city - - if len(itinerary_cities) >= 2 and itinerary_cities[1]: - return itinerary_cities[1] - for city in itinerary_cities: - if city: - return city - return "" - - def _extract_route_segment( - self, - context: dict[str, Any], - policy: RuntimeTravelPolicy, - ) -> tuple[str, str] | None: - document_info = context["document_info"] - route_value = self._resolve_document_field_value(document_info, "route") - if not route_value or "-" not in route_value: - return None - - origin_text, destination_text = [segment.strip() for segment in route_value.split("-", 1)] - origin_city = self._extract_city_from_text(origin_text, policy) - destination_city = self._extract_city_from_text(destination_text, policy) - if not origin_city or not destination_city or origin_city == destination_city: - return None - return origin_city, destination_city - - def _extract_hotel_city(self, context: dict[str, Any], policy: RuntimeTravelPolicy) -> str: - document_info = context["document_info"] - item = context["item"] - merchant_name = self._resolve_document_field_value(document_info, "merchant_name") - for candidate in ( - merchant_name, - str(item.item_location or ""), - str(context.get("ocr_summary") or ""), - str(context.get("ocr_text") or ""), - ): - city = self._extract_city_from_text(candidate, policy) - if city: - return city - return "" - - @staticmethod - def _extract_city_from_text(text: str, policy: RuntimeTravelPolicy) -> str: - normalized = str(text or "").strip() - if not normalized: - return "" - city_match_order = sorted(policy.city_tiers.keys(), key=lambda item: len(item), reverse=True) - for city in city_match_order: - if city in normalized: - return city - return "" - - @staticmethod - def _extract_hotel_night_count(context: dict[str, Any]) -> int: - text = " ".join( - [ - str(context.get("ocr_summary") or "").strip(), - str(context.get("ocr_text") or "").strip(), - ] - ).strip() - match = TRAVEL_POLICY_HOTEL_NIGHT_PATTERN.search(text) - if not match: - return 1 - try: - return max(1, int(match.group(1))) - except (TypeError, ValueError): - return 1 - - def _detect_transport_class( - self, - context: dict[str, Any], - policy: RuntimeTravelPolicy, - ) -> tuple[str, str, int] | None: - document_info = context["document_info"] - document_type = str(document_info.get("document_type") or "").strip().lower() - text = " ".join( - [ - str(context.get("ocr_summary") or "").strip(), - str(context.get("ocr_text") or "").strip(), - ] - ).strip() - compact_text = re.sub(r"\s+", "", text) - if not compact_text: - return None - - if document_type == "flight_itinerary": - for config in policy.flight_classes: - label = str(config.keyword or "").strip() - level = int(config.level) - if label in compact_text: - return "flight", label, level - return None - - if document_type == "train_ticket": - for config in policy.train_classes: - label = str(config.keyword or "").strip() - level = int(config.level) - if label in compact_text: - return "train", label, level - return None - - return None - - def _is_long_distance_travel_context( - self, - context: dict[str, Any], - policy: RuntimeTravelPolicy, - ) -> bool: - document_info = context["document_info"] - document_type = str(document_info.get("document_type") or "").strip().lower() - scene_code = str(document_info.get("scene_code") or "").strip().lower() - if document_type in set(policy.long_distance_document_types): - return True - return scene_code == "travel" - - def _sync_claim_from_items(self, claim: ExpenseClaim) -> None: - if not claim.items: - claim.amount = Decimal("0.00") - claim.invoice_count = 0 - claim.risk_flags_json = self._merge_claim_attachment_risk_flags(claim, []) - return - - ordered_items = sorted( - claim.items, - key=lambda item: ( - item.item_date or date.max, - self._normalize_sort_datetime(item.created_at), - ), - ) - primary_item = ordered_items[0] - total_amount = sum((item.item_amount for item in ordered_items), Decimal("0.00")) - - claim.amount = total_amount.quantize(Decimal("0.01")) - claim.invoice_count = sum(1 for item in ordered_items if str(item.invoice_id or "").strip()) - claim.occurred_at = datetime( - primary_item.item_date.year, - primary_item.item_date.month, - primary_item.item_date.day, - tzinfo=UTC, - ) - claim.expense_type = str(primary_item.item_type or claim.expense_type or "other").strip() or "other" - claim.reason = ( - self._normalize_optional_text(primary_item.item_reason, fallback=claim.reason or "待补充") or "待补充" - ) - claim.location = ( - self._normalize_optional_text(primary_item.item_location, fallback=claim.location or "待补充") - or "待补充" - ) - claim.risk_flags_json = self._merge_claim_attachment_risk_flags( - claim, - self._build_claim_attachment_risk_flags(ordered_items), - ) - if str(claim.status or "").strip().lower() == "draft": - claim.approval_stage = "待提交" - - def _refresh_item_attachment_analysis(self, item: ExpenseClaimItem) -> None: - file_path = self._resolve_attachment_path(item.invoice_id) - if file_path is None or not file_path.exists(): - return - - metadata = self._read_attachment_meta(file_path) - media_type = str(metadata.get("media_type") or self._resolve_attachment_media_type(file_path.name)).strip() - ocr_status = str(metadata.get("ocr_status") or "").strip().lower() - - if ocr_status == "failed": - analysis = self._build_failed_ocr_attachment_analysis( - media_type=media_type, - error_message=str(metadata.get("ocr_error") or ""), - item=item, - ) - elif ocr_status == "recognized" or any( - ( - str(metadata.get("ocr_text") or "").strip(), - str(metadata.get("ocr_summary") or "").strip(), - int(metadata.get("ocr_line_count") or 0), - list(metadata.get("ocr_warnings") or []), - ) - ): - stored_document_info = metadata.get("document_info") - if not isinstance(stored_document_info, dict): - stored_document_info = {} - document = SimpleNamespace( - filename=str(metadata.get("file_name") or file_path.name), - text=str(metadata.get("ocr_text") or ""), - summary=str(metadata.get("ocr_summary") or ""), - avg_score=float(metadata.get("ocr_avg_score") or 0.0), - line_count=int(metadata.get("ocr_line_count") or 0), - document_type=str(stored_document_info.get("document_type") or ""), - document_type_label=str(stored_document_info.get("document_type_label") or ""), - scene_code=str(stored_document_info.get("scene_code") or ""), - scene_label=str(stored_document_info.get("scene_label") or ""), - document_fields=list(stored_document_info.get("fields") or []), - warnings=[str(value) for value in list(metadata.get("ocr_warnings") or []) if str(value).strip()], - ) - document_info = self._build_attachment_document_info(document) - requirement_check = self._build_attachment_requirement_check( - item=item, - document_info=document_info, - ) - analysis = self._build_attachment_analysis( - document=document, - item=item, - document_info=document_info, - requirement_check=requirement_check, - ) - metadata["document_info"] = document_info - metadata["requirement_check"] = requirement_check - else: - analysis = self._build_fallback_attachment_analysis(media_type=media_type, item=item) - - metadata["analysis"] = analysis - self._write_attachment_meta(file_path, metadata) - - def _build_claim_attachment_risk_flags(self, ordered_items: list[ExpenseClaimItem]) -> list[dict[str, Any]]: - derived_flags: list[dict[str, Any]] = [] - for index, item in enumerate(ordered_items, start=1): - file_path = self._resolve_attachment_path(item.invoice_id) - if file_path is None or not file_path.exists(): - continue - - metadata = self._read_attachment_meta(file_path) - analysis = metadata.get("analysis") - if not isinstance(analysis, dict): - continue - - severity = str(analysis.get("severity") or "").strip().lower() - if severity in {"", "pass", "low"}: - continue - - summary = str(analysis.get("summary") or analysis.get("headline") or "").strip() or "附件存在待核对风险。" - label = str(analysis.get("label") or ("高风险" if severity == "high" else "中风险")).strip() - derived_flags.append( - { - "source": "attachment_analysis", - "item_id": item.id, - "severity": severity, - "label": label, - "message": f"费用明细第 {index} 条:{summary}", - } - ) - return derived_flags - - def _get_expense_rule_catalog(self) -> Any: - cached = getattr(self, "_expense_rule_catalog", None) - if cached is not None: - return cached - - db = getattr(self, "db", None) - if db is None: - catalog = build_default_expense_rule_catalog() - else: - catalog = ExpenseRuleRuntimeService(db).load_catalog() - setattr(self, "_expense_rule_catalog", catalog) - return catalog - - def _get_expense_scene_policy(self, expense_type: str | None) -> Any | None: - return self._get_expense_rule_catalog().get_scene_policy(expense_type) - - def _resolve_min_attachment_count(self, expense_type: str | None) -> int: - policy = self._get_expense_scene_policy(expense_type) - if policy is None: - return 1 - return max(0, int(policy.min_attachment_count or 0)) - - def _build_scene_reason_corpus(self, claim: ExpenseClaim) -> str: - parts = [str(claim.reason or "").strip(), str(claim.location or "").strip()] - for item in claim.items: - parts.append(str(item.item_reason or "").strip()) - parts.append(str(item.item_location or "").strip()) - return "\n".join(part for part in parts if part) - - @staticmethod - def _merge_claim_attachment_risk_flags( - claim: ExpenseClaim, - attachment_risk_flags: list[dict[str, Any]], - ) -> list[Any]: - preserved_flags = [ - flag - for flag in list(claim.risk_flags_json or []) - if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis") - ] - return preserved_flags + attachment_risk_flags - - def _validate_claim_for_submission(self, claim: ExpenseClaim) -> list[str]: - issues: list[str] = [] - claim_location_required = self._is_location_required_expense_type(claim.expense_type) - claim_min_attachment_count = self._resolve_min_attachment_count(claim.expense_type) - - if self._is_missing_value(claim.employee_name): - issues.append("申请人未完善") - if self._is_missing_value(claim.department_name): - issues.append("所属部门未完善") - if self._is_missing_value(claim.expense_type): - issues.append("报销类型未完善") - if self._is_missing_value(claim.reason): - issues.append("报销事由未完善") - if claim_location_required and self._is_missing_value(claim.location): - issues.append("业务地点未完善") - if claim.amount is None or claim.amount <= Decimal("0.00"): - issues.append("报销金额未完善") - if claim.occurred_at is None: - issues.append("发生时间未完善") - if int(claim.invoice_count or 0) < claim_min_attachment_count: - issues.append("票据附件数量不足") - if not claim.items: - issues.append("费用明细不能为空") - - for index, item in enumerate(claim.items, start=1): - prefix = f"费用明细第 {index} 条" - item_location_required = self._is_location_required_expense_type(item.item_type or claim.expense_type) - if item.item_date is None: - issues.append(f"{prefix}缺少日期") - if self._is_missing_value(item.item_type): - issues.append(f"{prefix}缺少费用项目") - if self._is_missing_value(item.item_reason): - issues.append(f"{prefix}缺少说明") - if item_location_required and self._is_missing_value(item.item_location): - issues.append(f"{prefix}缺少地点") - if item.item_amount is None or item.item_amount <= Decimal("0.00"): - issues.append(f"{prefix}缺少金额") - if self._is_missing_value(item.invoice_id): - issues.append(f"{prefix}缺少票据标识") - - return issues - - def _is_location_required_expense_type(self, expense_type: str | None) -> bool: - policy = self._get_expense_scene_policy(expense_type) - if policy is None: - return str(expense_type or "").strip().lower() in LOCATION_REQUIRED_EXPENSE_TYPES - return bool(policy.location_required) - - @staticmethod - def _has_privileged_claim_access(current_user: CurrentUserContext) -> bool: - if current_user.is_admin: - return True - role_codes = { - str(item).strip().lower() - for item in current_user.role_codes - if str(item).strip() - } - return bool(role_codes & PRIVILEGED_CLAIM_ROLE_CODES) - - @staticmethod - def _normalize_role_codes(current_user: CurrentUserContext) -> set[str]: - return { - str(item).strip().lower() - for item in current_user.role_codes - if str(item).strip() - } - - def _resolve_current_employee(self, current_user: CurrentUserContext) -> Employee | None: - username = str(current_user.username or "").strip() - if not username: - return None - return self.db.scalar( - select(Employee) - .where(func.lower(Employee.email) == username.lower()) - .limit(1) - ) - - def _employee_name_is_unique(self, employee: Employee) -> bool: - normalized_name = str(employee.name or "").strip() - if not normalized_name: - return False - - same_name_count = int( - self.db.scalar( - select(func.count()).select_from(Employee).where(Employee.name == normalized_name) - ) - or 0 - ) - return same_name_count == 1 - - def _apply_claim_scope(self, stmt: Any, current_user: CurrentUserContext) -> Any: - if self._has_privileged_claim_access(current_user): - return stmt - - conditions = [] - username = str(current_user.username or "").strip() - employee = self._resolve_current_employee(current_user) - - def add_condition(field_name: str, value: str | None) -> None: - normalized = str(value or "").strip() - if not normalized: - return - if field_name == "employee_id": - conditions.append(ExpenseClaim.employee_id == normalized) - return - conditions.append(ExpenseClaim.employee_name == normalized) - - if employee is not None: - add_condition("employee_id", employee.id) - add_condition("employee_name", employee.email) - if self._employee_name_is_unique(employee): - add_condition("employee_name", employee.name) - else: - add_condition("employee_id", username) - add_condition("employee_name", username) - - if not conditions: - return stmt.where(ExpenseClaim.id == "__no_visible_claim__") - - role_codes = self._normalize_role_codes(current_user) - if role_codes & APPROVAL_VISIBLE_CLAIM_ROLE_CODES: - pending_leader_approval = and_( - ExpenseClaim.status == "submitted", - ExpenseClaim.approval_stage == "直属领导审批", - ) - if employee is not None: - subordinate_ids = select(Employee.id).where(Employee.manager_id == employee.id) - conditions.append(and_(pending_leader_approval, ExpenseClaim.employee_id.in_(subordinate_ids))) - manager_name = str( - employee.name if employee is not None and employee.name else current_user.name or "" - ).strip() - if manager_name: - managed_department_ids = select(OrganizationUnit.id).where(OrganizationUnit.manager_name == manager_name) - managed_department_names = select(OrganizationUnit.name).where(OrganizationUnit.manager_name == manager_name) - conditions.append(and_(pending_leader_approval, ExpenseClaim.department_id.in_(managed_department_ids))) - conditions.append(and_(pending_leader_approval, ExpenseClaim.department_name.in_(managed_department_names))) - - return stmt.where(or_(*conditions)) - - def _ensure_ready(self) -> None: - AgentFoundationService(self.db).ensure_foundation_ready() +from __future__ import annotations + +import base64 +import binascii +import json +import mimetypes +import re +import shutil +from collections import defaultdict +from datetime import UTC, date, datetime, timedelta +from decimal import Decimal, InvalidOperation +from pathlib import Path +from types import SimpleNamespace +from typing import Any +from urllib.parse import quote + +from sqlalchemy import and_, func, or_, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session, selectinload + +from app.api.deps import CurrentUserContext +from app.core.config import get_settings +from app.models.employee import Employee +from app.models.financial_record import ExpenseClaim, ExpenseClaimItem +from app.models.organization import OrganizationUnit +from app.schemas.ontology import OntologyEntity, OntologyParseResult +from app.schemas.reimbursement import ExpenseClaimItemCreate, ExpenseClaimItemUpdate +from app.services.agent_foundation import AgentFoundationService +from app.services.audit import AuditLogService +from app.services.document_intelligence import build_document_insight +from app.services.expense_rule_runtime import ( + DEFAULT_SCENE_RULE_ASSET_CODE, + ExpenseRuleRuntimeService, + RuntimeTravelPolicy, + build_default_expense_rule_catalog, + resolve_document_type_label, +) +from app.services.ocr import OcrService + +EXPENSE_TYPE_LABELS = { + "travel": "差旅", + "hotel": "住宿", + "transport": "交通", + "meal": "餐费", + "meeting": "会务", + "entertainment": "招待", + "office": "办公", + "training": "培训", + "communication": "通讯", + "welfare": "福利", +} + +PRIVILEGED_CLAIM_ROLE_CODES = {"finance"} +APPROVAL_VISIBLE_CLAIM_ROLE_CODES = {"manager", "approver"} +MAX_DRAFT_CLAIMS_PER_USER = 3 +LOCATION_REQUIRED_EXPENSE_TYPES = { + "travel", + "hotel", + "transport", + "meal", + "meeting", + "entertainment", +} + +EXPENSE_SCENE_KEYWORDS = { + "travel": ("差旅", "出差", "行程"), + "hotel": ("酒店", "住宿", "房费", "客房", "入住", "离店"), + "transport": ( + "交通", + "打车", + "出租车", + "网约车", + "滴滴", + "出行", + "高铁", + "动车", + "火车", + "机票", + "航班", + "行程单", + "登机", + "客票", + "公交", + "地铁", + "过路费", + "通行费", + "停车", + ), + "meal": ("餐饮", "餐费", "用餐", "外卖", "快餐", "酒楼", "饭店", "饭馆", "食品", "咖啡"), + "entertainment": ("招待", "宴请", "接待", "客户餐", "商务餐", "业务招待"), + "office": ("办公", "办公用品", "文具", "耗材", "打印", "纸张", "硒鼓", "墨盒", "鼠标", "键盘", "电脑"), + "meeting": ("会议", "会务", "会展", "会议室", "会场", "场地费", "论坛"), + "training": ("培训", "课程", "讲师", "教材", "学费", "认证"), +} + +EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES = { + "travel": {"travel", "hotel", "transport", "meal"}, + "hotel": {"hotel"}, + "transport": {"transport", "travel"}, + "meal": {"meal", "entertainment"}, + "entertainment": {"entertainment", "meal"}, + "office": {"office"}, + "meeting": {"meeting"}, + "training": {"training"}, +} + +DOCUMENT_SCENE_LABELS = { + "travel": "差旅", + "hotel": "住宿", + "transport": "交通", + "meal": "餐饮", + "entertainment": "业务招待", + "office": "办公用品", + "meeting": "会务", + "training": "培训", + "other": "其他票据", +} + +DOCUMENT_ASSOCIATION_REVIEW_ACTIONS = { + "link_to_existing_draft", + "create_new_claim_from_documents", +} +MAX_CLAIM_NO_RETRY_ATTEMPTS = 3 +DOCUMENT_AMOUNT_PATTERNS = ( + re.compile( + r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)" + r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)" + ), + re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)"), + re.compile(r"([0-9]+(?:[.,][0-9]{1,2})?)\s*元"), +) +DOCUMENT_DATE_PATTERN = re.compile(r"((?:20\d{2}|19\d{2})[-/年.](?:1[0-2]|0?[1-9])[-/月.](?:3[01]|[12]\d|0?[1-9])日?)") +SYSTEM_GENERATED_REASON_PREFIXES = ( + "我上传了", + "请按当前已识别信息", + "请把当前上传的票据", + "请基于当前上传的多张票据", + "我已核对右侧识别结果", + "请同步修正逐票据识别结果", + "我已修改识别信息", + "查看报销草稿", + "请解释一下当前这笔报销的合规风险和待补充项", +) +AI_REVIEW_LOOKBACK_DAYS = 90 +AI_REVIEW_REPEAT_RISK_WARNING_COUNT = 1 +AI_REVIEW_REPEAT_RISK_BLOCK_COUNT = 2 +TRAVEL_REVIEW_RELEVANT_EXPENSE_TYPES = {"travel", "hotel", "transport"} +TRAVEL_REVIEW_LONG_DISTANCE_DOCUMENT_TYPES = {"flight_itinerary", "train_ticket"} +TRAVEL_POLICY_CITY_TIERS = { + "北京": "tier_1", + "上海": "tier_1", + "广州": "tier_1", + "深圳": "tier_1", + "杭州": "tier_2", + "南京": "tier_2", + "苏州": "tier_2", + "武汉": "tier_2", + "成都": "tier_2", + "重庆": "tier_2", + "西安": "tier_2", + "天津": "tier_2", + "宁波": "tier_2", + "厦门": "tier_2", + "青岛": "tier_2", + "长沙": "tier_2", + "郑州": "tier_2", + "合肥": "tier_2", + "济南": "tier_2", + "沈阳": "tier_2", + "大连": "tier_2", + "福州": "tier_2", + "昆明": "tier_2", + "海口": "tier_2", + "三亚": "tier_2", + "无锡": "tier_2", + "东莞": "tier_2", + "佛山": "tier_2", +} +TRAVEL_POLICY_CITY_MATCH_ORDER = tuple( + sorted(TRAVEL_POLICY_CITY_TIERS.keys(), key=lambda item: len(item), reverse=True) +) +TRAVEL_POLICY_BAND_LABELS = { + "junior": "P1-P3", + "mid": "P4-P5", + "senior": "P6-P7", + "manager": "M1-M2", + "executive": "M3及以上 / D序列", +} +TRAVEL_POLICY_HOTEL_LIMITS = { + "junior": { + "tier_1": Decimal("450.00"), + "tier_2": Decimal("380.00"), + "tier_3": Decimal("320.00"), + }, + "mid": { + "tier_1": Decimal("550.00"), + "tier_2": Decimal("480.00"), + "tier_3": Decimal("380.00"), + }, + "senior": { + "tier_1": Decimal("700.00"), + "tier_2": Decimal("620.00"), + "tier_3": Decimal("520.00"), + }, + "manager": { + "tier_1": Decimal("900.00"), + "tier_2": Decimal("820.00"), + "tier_3": Decimal("720.00"), + }, + "executive": { + "tier_1": Decimal("1200.00"), + "tier_2": Decimal("1000.00"), + "tier_3": Decimal("900.00"), + }, +} +TRAVEL_POLICY_ALLOWED_TRANSPORT_LEVELS = { + "junior": {"flight": 1, "train": 1}, + "mid": {"flight": 1, "train": 1}, + "senior": {"flight": 2, "train": 2}, + "manager": {"flight": 3, "train": 3}, + "executive": {"flight": 4, "train": 3}, +} +TRAVEL_POLICY_ROUTE_EXCEPTION_KEYWORDS = ( + "中转", + "转机", + "经停", + "改签", + "多地出差", + "多城市", + "多站", + "异地返程", + "异地结束", + "临时变更", + "继续前往", + "第二站", +) +TRAVEL_POLICY_STANDARD_EXCEPTION_KEYWORDS = ( + "超标说明", + "无直达", + "展会高峰", + "会议高峰", + "协议酒店满房", + "客户指定", + "临时改签", + "行程变更", + "红眼航班", + "晚到店", +) +TRAVEL_POLICY_FLIGHT_CLASS_PATTERNS = ( + ("头等舱", 4), + ("公务舱", 3), + ("商务舱", 3), + ("超级经济舱", 2), + ("高端经济舱", 2), + ("明珠经济舱", 2), + ("经济舱", 1), +) +TRAVEL_POLICY_TRAIN_CLASS_PATTERNS = ( + ("商务座", 3), + ("一等座", 2), + ("软卧", 2), + ("二等座", 1), + ("二等卧", 1), + ("硬卧", 1), +) +TRAVEL_POLICY_HOTEL_NIGHT_PATTERN = re.compile(r"(\d+)\s*(?:晚|间夜)") + + +class ExpenseClaimService: + def __init__(self, db: Session) -> None: + self.db = db + self.audit_service = AuditLogService(db) + + def list_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]: + stmt = ( + select(ExpenseClaim) + .options( + selectinload(ExpenseClaim.items), + selectinload(ExpenseClaim.employee).selectinload(Employee.manager), + selectinload(ExpenseClaim.employee).selectinload(Employee.roles), + ) + .order_by(ExpenseClaim.created_at.desc(), ExpenseClaim.occurred_at.desc()) + ) + stmt = self._apply_claim_scope(stmt, current_user) + return list(self.db.scalars(stmt).all()) + + def get_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: + stmt = ( + select(ExpenseClaim) + .options( + selectinload(ExpenseClaim.items), + selectinload(ExpenseClaim.employee).selectinload(Employee.manager), + selectinload(ExpenseClaim.employee).selectinload(Employee.roles), + ) + .where(ExpenseClaim.id == claim_id) + ) + stmt = self._apply_claim_scope(stmt, current_user) + return self.db.scalar(stmt) + + def update_claim_item( + self, + *, + claim_id: str, + item_id: str, + payload: ExpenseClaimItemUpdate, + current_user: CurrentUserContext, + ) -> ExpenseClaim | None: + claim = self.get_claim(claim_id, current_user) + if claim is None: + return None + + self._ensure_draft_claim(claim) + item = next((entry for entry in claim.items if entry.id == item_id), None) + if item is None: + raise LookupError("Item not found") + + before_json = self._serialize_claim(claim) + + if payload.item_date is not None: + item.item_date = payload.item_date + if payload.item_type is not None: + item.item_type = self._normalize_optional_text(payload.item_type, fallback=item.item_type) or item.item_type + if payload.item_reason is not None: + item.item_reason = ( + self._normalize_optional_text(payload.item_reason, fallback=item.item_reason) or item.item_reason + ) + if payload.item_location is not None: + item.item_location = ( + self._normalize_optional_text(payload.item_location, fallback=item.item_location) or item.item_location + ) + if payload.item_amount is not None: + amount = payload.item_amount.quantize(Decimal("0.01")) + if amount <= Decimal("0.00"): + raise ValueError("费用金额必须大于 0。") + item.item_amount = amount + if payload.invoice_id is not None: + item.invoice_id = self._normalize_optional_text(payload.invoice_id, allow_empty=True) + + self._refresh_item_attachment_analysis(item) + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.item_update", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return claim + + def create_claim_item( + self, + *, + claim_id: str, + payload: ExpenseClaimItemCreate | None, + current_user: CurrentUserContext, + ) -> ExpenseClaim | None: + claim = self.get_claim(claim_id, current_user) + if claim is None: + return None + + self._ensure_draft_claim(claim) + before_json = self._serialize_claim(claim) + payload = payload or ExpenseClaimItemCreate() + + occurred_at = claim.occurred_at if claim.occurred_at is not None else datetime.now(UTC) + item_amount = Decimal("0.00") + if payload.item_amount is not None: + item_amount = payload.item_amount.quantize(Decimal("0.01")) + if item_amount < Decimal("0.00"): + raise ValueError("费用金额不能小于 0。") + + item = ExpenseClaimItem( + claim_id=claim.id, + item_date=payload.item_date or occurred_at.date(), + item_type=self._normalize_optional_text( + payload.item_type, + fallback=str(claim.expense_type or "").strip() or "other", + ) + or "other", + item_reason=self._normalize_optional_text(payload.item_reason, fallback="") or "", + item_location=self._normalize_optional_text(payload.item_location, fallback="") or "", + item_amount=item_amount, + invoice_id=self._normalize_optional_text(payload.invoice_id, allow_empty=True), + ) + claim.items.append(item) + self.db.add(item) + + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.item_create", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return claim + + def delete_claim_item( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> dict[str, Any] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + self._ensure_draft_claim(claim) + before_json = self._serialize_claim(claim) + item_label = str(item.item_reason or "").strip() or self._resolve_expense_type_label(item.item_type) + + self._delete_item_attachment_files(item) + claim.items = [entry for entry in claim.items if entry.id != item.id] + self.db.delete(item) + + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.item_delete", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return { + "message": f"费用明细“{item_label}”已删除。", + "claim_id": claim.id, + "item_id": item.id, + } + + def upload_claim_item_attachment( + self, + *, + claim_id: str, + item_id: str, + filename: str, + content: bytes, + media_type: str | None, + current_user: CurrentUserContext, + ) -> dict[str, Any] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + self._ensure_draft_claim(claim) + normalized_name = self._normalize_attachment_filename(filename) + if not content: + raise ValueError("上传文件不能为空。") + + before_json = self._serialize_claim(claim) + attachment_dir = self._build_item_attachment_dir(claim.id, item.id) + shutil.rmtree(attachment_dir, ignore_errors=True) + attachment_dir.mkdir(parents=True, exist_ok=True) + + file_path = attachment_dir / normalized_name + file_path.write_bytes(content) + resolved_media_type = self._resolve_attachment_media_type( + normalized_name, + fallback=media_type, + ) + + attachment_analysis = self._build_fallback_attachment_analysis( + media_type=media_type, + item=item, + ) + ocr_document = None + document_info = None + requirement_check = None + ocr_status = "empty" + ocr_error = "" + try: + ocr_result = OcrService(self.db).recognize_files( + [(normalized_name, content, media_type or "application/octet-stream")] + ) + documents = list(ocr_result.documents or []) + if documents: + ocr_document = documents[0] + ocr_status = "recognized" + document_info = self._build_attachment_document_info(ocr_document) + requirement_check = self._build_attachment_requirement_check( + item=item, + document_info=document_info, + ) + attachment_analysis = self._build_attachment_analysis( + document=ocr_document, + item=item, + document_info=document_info, + requirement_check=requirement_check, + ) + except Exception as exc: # pragma: no cover - fallback path depends on OCR runtime + ocr_status = "failed" + ocr_error = str(exc) + attachment_analysis = self._build_failed_ocr_attachment_analysis( + media_type=media_type, + error_message=ocr_error, + item=item, + ) + + item.invoice_id = self._to_attachment_storage_key(file_path) + preview_meta = self._build_attachment_preview_meta( + file_path=file_path, + media_type=resolved_media_type, + ocr_document=ocr_document, + ) + meta = { + "file_name": normalized_name, + "storage_key": item.invoice_id, + "media_type": resolved_media_type, + "size_bytes": len(content), + "uploaded_at": datetime.now(UTC).isoformat(), + "previewable": bool(preview_meta["previewable"]), + "preview_kind": str(preview_meta["preview_kind"]), + "preview_storage_key": str(preview_meta["preview_storage_key"]), + "preview_media_type": str(preview_meta["preview_media_type"]), + "preview_file_name": str(preview_meta["preview_file_name"]), + "analysis": attachment_analysis, + "document_info": document_info, + "requirement_check": requirement_check, + "ocr_status": ocr_status, + "ocr_error": ocr_error, + "ocr_text": str(getattr(ocr_document, "text", "") or ""), + "ocr_summary": str(getattr(ocr_document, "summary", "") or ""), + "ocr_avg_score": float(getattr(ocr_document, "avg_score", 0.0) or 0.0), + "ocr_line_count": int(getattr(ocr_document, "line_count", 0) or 0), + "ocr_classification_source": str(getattr(ocr_document, "classification_source", "") or ""), + "ocr_classification_confidence": float(getattr(ocr_document, "classification_confidence", 0.0) or 0.0), + "ocr_classification_evidence": [ + str(item) + for item in getattr(ocr_document, "classification_evidence", []) or [] + if str(item).strip() + ], + "ocr_warnings": [str(item) for item in getattr(ocr_document, "warnings", []) or []], + } + self._write_attachment_meta(file_path, meta) + + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.attachment_upload", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return { + "message": f"{normalized_name} 已上传并关联到当前费用明细。", + "claim_id": claim.id, + "item_id": item.id, + "invoice_id": item.invoice_id, + "attachment": self._build_attachment_payload(item), + } + + def get_claim_item_attachment_meta( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> dict[str, Any] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + return self._build_attachment_payload(item) + + def get_claim_item_attachment_content( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> tuple[Path, str, str] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + return self._resolve_item_attachment_content(item) + + def get_claim_item_attachment_preview_content( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> tuple[Path, str, str] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + return self._resolve_item_attachment_preview_content(item) + + def delete_claim_item_attachment( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> dict[str, Any] | None: + claim, item = self._get_claim_item_or_raise( + claim_id=claim_id, + item_id=item_id, + current_user=current_user, + ) + if claim is None: + return None + + self._ensure_draft_claim(claim) + before_json = self._serialize_claim(claim) + previous_name = self._resolve_attachment_display_name(item.invoice_id) + self._delete_item_attachment_files(item) + item.invoice_id = None + + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.attachment_delete", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return { + "message": f"{previous_name or '附件'} 已删除。", + "claim_id": claim.id, + "item_id": item.id, + "invoice_id": item.invoice_id, + "attachment": None, + } + + def submit_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: + claim = self.get_claim(claim_id, current_user) + if claim is None: + return None + + self._ensure_draft_claim(claim) + self._sync_claim_from_items(claim) + missing_fields = self._validate_claim_for_submission(claim) + if missing_fields: + raise ValueError("提交前请先补全信息:" + ";".join(missing_fields)) + + before_json = self._serialize_claim(claim) + # TODO: 后续恢复 AI 验审逻辑 + # review_result = self._run_ai_submission_review(claim) + manager_name = self._resolve_claim_manager_name(claim) or "审批人" + claim.status = "submitted" + claim.approval_stage = "直属领导审批" + claim.risk_flags_json = list(claim.risk_flags_json or []) + claim.submitted_at = datetime.now(UTC) + + self.db.commit() + self.db.refresh(claim) + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.submit", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + ) + + return claim + + def save_or_submit_from_ontology( + self, + *, + run_id: str, + user_id: str | None, + message: str, + ontology: OntologyParseResult, + context_json: dict[str, Any], + ) -> dict[str, Any]: + result = self.upsert_draft_from_ontology( + run_id=run_id, + user_id=user_id, + message=message, + ontology=ontology, + context_json=context_json, + ) + + review_action = str(context_json.get("review_action") or "").strip() + if review_action != "next_step": + return result + + claim_id = str(result.get("claim_id") or "").strip() + if not claim_id or result.get("draft_limit_reached"): + return result + + current_user = CurrentUserContext( + username=str(user_id or context_json.get("name") or "anonymous").strip() or "anonymous", + name=str(context_json.get("name") or user_id or "anonymous").strip() or "anonymous", + role_codes=[ + str(item).strip() + for item in list(context_json.get("role_codes") or []) + if str(item).strip() + ], + is_admin=bool(context_json.get("is_admin")), + ) + + try: + claim = self.submit_claim(claim_id, current_user) + except ValueError as exc: + return { + **result, + "message": str(exc), + "submission_blocked": True, + "draft_only": False, + } + + if claim is None: + return { + **result, + "message": "未找到可提交的报销单,请刷新后重试。", + "submission_blocked": True, + "draft_only": False, + } + + if str(claim.status or "").strip().lower() != "submitted": + review_message = "" + for flag in list(claim.risk_flags_json or []): + if not isinstance(flag, dict): + continue + if str(flag.get("source") or "").strip() != "submission_review": + continue + review_message = str(flag.get("message") or "").strip() + if review_message: + break + return { + "message": review_message or f"报销单 {claim.claim_no} 经 AI验审后转为待补充,请先修正后再提交。", + "submission_blocked": True, + "draft_only": False, + "claim_id": claim.id, + "claim_no": claim.claim_no, + "status": claim.status, + "approval_stage": claim.approval_stage, + "amount": float(claim.amount), + "invoice_count": int(claim.invoice_count or 0), + } + + return { + "message": ( + f"报销单 {claim.claim_no} 已完成 AI验审," + f"当前节点为 {claim.approval_stage or '审批中'}。" + ), + "draft_only": False, + "claim_id": claim.id, + "claim_no": claim.claim_no, + "status": claim.status, + "approval_stage": claim.approval_stage, + "amount": float(claim.amount), + "invoice_count": int(claim.invoice_count or 0), + } + + def delete_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None: + claim = self.get_claim(claim_id, current_user) + if claim is None: + return None + + self._ensure_draft_claim(claim) + before_json = self._serialize_claim(claim) + resource_id = claim.id + + self._delete_claim_attachment_root(claim.id) + self.db.delete(claim) + self.db.commit() + + self.audit_service.log_action( + actor=current_user.name or current_user.username, + action="expense_claim.delete", + resource_type="expense_claim", + resource_id=resource_id, + before_json=before_json, + after_json=None, + ) + + return claim + + def upsert_draft_from_ontology( + self, + *, + run_id: str, + user_id: str | None, + message: str, + ontology: OntologyParseResult, + context_json: dict[str, Any], + ) -> dict[str, Any]: + self._ensure_ready() + context_json = dict(context_json or {}) + retry_count = self._resolve_claim_no_retry_count(context_json) + + review_action = str(context_json.get("review_action") or "").strip() + attachment_names = self._resolve_attachment_names(context_json) + context_documents = self._resolve_context_documents(context_json) + + employee = self._resolve_employee( + ontology=ontology, + context_json=context_json, + user_id=user_id, + ) + draft_owner_name = ( + employee.name + if employee is not None + else self._resolve_employee_name( + ontology=ontology, + context_json=context_json, + user_id=user_id, + ) + ) + + association_candidate = self._find_association_candidate( + ontology=ontology, + context_json=context_json, + user_id=user_id, + employee=employee, + ) + if self._should_defer_multi_document_association( + context_json=context_json, + review_action=review_action, + association_candidate=association_candidate, + context_documents=context_documents, + ): + document_count = max(len(context_documents), len(attachment_names), self._resolve_attachment_count(context_json)) + return { + "message": ( + f"检测到你已有草稿 {association_candidate.claim_no}," + f"当前新上传了 {document_count} 张票据,请先选择关联到现有草稿,或单独建立新的报销单。" + ), + "draft_only": False, + "status": "pending_association_decision", + "pending_association_decision": True, + "association_candidate_claim_id": association_candidate.id, + "association_candidate_claim_no": association_candidate.claim_no, + } + + claim = self._find_target_claim( + ontology=ontology, + context_json=context_json, + review_action=review_action, + association_candidate=association_candidate, + ) + is_new_claim = claim is None + before_json = self._serialize_claim(claim) if claim is not None else None + if is_new_claim: + existing_draft_count = self._count_draft_claims_for_owner( + employee=employee, + user_id=user_id, + ) + if existing_draft_count >= MAX_DRAFT_CLAIMS_PER_USER: + return { + "message": ( + f"你当前已保存 {MAX_DRAFT_CLAIMS_PER_USER} 个草稿,请先完成已保存的草稿," + "才能再次新建草稿。" + ), + "draft_limit_reached": True, + "draft_only": False, + "status": "blocked", + "draft_count": existing_draft_count, + "max_draft_count": MAX_DRAFT_CLAIMS_PER_USER, + } + + amount = self._resolve_amount(ontology.entities, context_json=context_json) + occurred_at = self._resolve_occurred_at(ontology, context_json=context_json) + expense_type = self._resolve_expense_type(ontology.entities, context_json=context_json) + location = self._resolve_location(message=message, context_json=context_json) + reason = self._resolve_reason( + message=message, + context_json=context_json, + allow_message_fallback=is_new_claim, + ) + attachment_count = len(attachment_names) or self._resolve_attachment_count(context_json) + + final_amount = amount if amount is not None else (claim.amount if claim is not None else Decimal("0.00")) + final_occurred_at = ( + occurred_at if occurred_at is not None else (claim.occurred_at if claim is not None else datetime.now(UTC)) + ) + final_expense_type = expense_type or (claim.expense_type if claim is not None else "other") + final_location = location or (claim.location if claim is not None else "待补充") + final_reason = reason or (claim.reason if claim is not None else "待补充") + final_attachment_count = ( + attachment_count if attachment_count > 0 else int(claim.invoice_count or 0) if claim is not None else 0 + ) + final_risk_flags = list(ontology.risk_flags) or ( + list(claim.risk_flags_json or []) if claim is not None else [] + ) + + try: + if claim is None: + claim = ExpenseClaim( + claim_no=self._generate_claim_no(final_occurred_at), + employee_id=employee.id if employee is not None else None, + employee_name=draft_owner_name, + department_id=employee.organization_unit_id if employee is not None else None, + department_name=self._resolve_department_name( + employee=employee, + context_json=context_json, + ), + project_code=self._resolve_project_code(ontology.entities), + expense_type=final_expense_type, + reason=final_reason, + location=final_location, + amount=final_amount, + currency="CNY", + invoice_count=final_attachment_count, + occurred_at=final_occurred_at, + status="draft", + approval_stage="待提交", + risk_flags_json=final_risk_flags, + ) + self.db.add(claim) + else: + claim.employee_id = employee.id if employee is not None else claim.employee_id + claim.employee_name = ( + employee.name + if employee is not None + else self._resolve_employee_name( + ontology=ontology, + context_json=context_json, + user_id=user_id, + fallback=claim.employee_name, + ) + ) + claim.department_id = employee.organization_unit_id if employee is not None else claim.department_id + claim.department_name = self._resolve_department_name( + employee=employee, + context_json=context_json, + fallback=claim.department_name, + ) + claim.project_code = self._resolve_project_code(ontology.entities) or claim.project_code + claim.expense_type = final_expense_type + claim.reason = final_reason + claim.location = final_location + claim.amount = final_amount + claim.invoice_count = final_attachment_count + claim.occurred_at = final_occurred_at + claim.status = "draft" + claim.approval_stage = "待提交" + claim.risk_flags_json = final_risk_flags + + self.db.flush() + if context_documents or attachment_names: + document_specs = self._build_context_item_specs( + context_documents=context_documents, + attachment_names=attachment_names, + occurred_at=final_occurred_at, + expense_type=final_expense_type, + amount=final_amount, + reason=final_reason, + location=final_location, + ) + else: + document_specs = [] + + if document_specs and (is_new_claim or review_action in DOCUMENT_ASSOCIATION_REVIEW_ACTIONS): + if review_action == "link_to_existing_draft" and claim.items: + self._append_document_items( + claim=claim, + item_specs=document_specs, + ) + else: + self._replace_claim_items( + claim=claim, + item_specs=document_specs, + ) + self._sync_claim_from_items(claim) + else: + self._upsert_primary_item( + claim=claim, + occurred_at=final_occurred_at, + expense_type=final_expense_type, + amount=final_amount, + reason=final_reason, + location=final_location, + attachment_names=attachment_names, + ) + self._sync_claim_from_items(claim) + self.db.commit() + self.db.refresh(claim) + except IntegrityError as exc: + self.db.rollback() + if ( + is_new_claim + and retry_count < MAX_CLAIM_NO_RETRY_ATTEMPTS + and self._is_claim_no_conflict_error(exc) + ): + retry_context = dict(context_json) + retry_context["_claim_no_retry_count"] = retry_count + 1 + return self.upsert_draft_from_ontology( + run_id=run_id, + user_id=user_id, + message=message, + ontology=ontology, + context_json=retry_context, + ) + raise + except Exception: + self.db.rollback() + raise + + self.audit_service.log_action( + actor=user_id or claim.employee_name or "anonymous", + action="expense_claim.draft_upsert", + resource_type="expense_claim", + resource_id=claim.id, + before_json=before_json, + after_json=self._serialize_claim(claim), + request_id=run_id, + ) + + return { + "message": ( + f"已{'创建' if is_new_claim else '更新'}报销草稿 {claim.claim_no},当前状态为 draft。" + "你可以继续补充费用明细、客户单位和票据附件。" + ), + "draft_only": True, + "claim_id": claim.id, + "claim_no": claim.claim_no, + "status": claim.status, + "amount": float(claim.amount), + "invoice_count": int(claim.invoice_count or 0), + } + + def _find_target_claim( + self, + *, + ontology: OntologyParseResult, + context_json: dict[str, Any], + review_action: str = "", + association_candidate: ExpenseClaim | None = None, + ) -> ExpenseClaim | None: + if review_action == "create_new_claim_from_documents": + return None + if review_action == "link_to_existing_draft" and association_candidate is not None: + return association_candidate + + draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() + if draft_claim_id: + claim = self.db.get(ExpenseClaim, draft_claim_id) + if claim is not None and str(claim.status or "").strip() == "draft": + return claim + return None + + claim_codes = [ + item.normalized_value + for item in ontology.entities + if item.type == "expense_claim" and item.normalized_value + ] + if not claim_codes: + return None + + stmt = ( + select(ExpenseClaim) + .where(ExpenseClaim.claim_no.in_(claim_codes)) + .where(ExpenseClaim.status == "draft") + .limit(1) + ) + return self.db.scalar(stmt) + + def _find_association_candidate( + self, + *, + ontology: OntologyParseResult, + context_json: dict[str, Any], + user_id: str | None, + employee: Employee | None, + ) -> ExpenseClaim | None: + draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() + if draft_claim_id: + claim = self.db.get(ExpenseClaim, draft_claim_id) + if claim is not None and str(claim.status or "").strip() == "draft": + return claim + + owner_filters = self._build_draft_owner_filters( + employee=employee, + user_id=user_id, + ) + if not owner_filters: + fallback_name = self._resolve_employee_name( + ontology=ontology, + context_json=context_json, + user_id=user_id, + fallback="", + ) + if fallback_name: + owner_filters = [ExpenseClaim.employee_name == fallback_name] + + if not owner_filters: + return None + + stmt = ( + select(ExpenseClaim) + .where(ExpenseClaim.status == "draft") + .where(or_(*owner_filters)) + .order_by(ExpenseClaim.updated_at.desc(), ExpenseClaim.created_at.desc()) + .limit(1) + ) + return self.db.scalar(stmt) + + def _should_defer_multi_document_association( + self, + *, + context_json: dict[str, Any], + review_action: str, + association_candidate: ExpenseClaim | None, + context_documents: list[dict[str, Any]], + ) -> bool: + if association_candidate is None: + return False + if review_action in DOCUMENT_ASSOCIATION_REVIEW_ACTIONS: + return False + document_count = max( + len(context_documents), + len(self._resolve_attachment_names(context_json)), + self._resolve_attachment_count(context_json), + ) + return document_count > 1 + + def _resolve_context_documents(self, context_json: dict[str, Any]) -> list[dict[str, Any]]: + documents = context_json.get("ocr_documents") + if not isinstance(documents, list): + documents = [] + + normalized: list[dict[str, Any]] = [] + for index, item in enumerate(documents[:10], start=1): + if not isinstance(item, dict): + continue + normalized.append( + { + "index": index, + "filename": str(item.get("filename") or "").strip(), + "summary": str(item.get("summary") or "").strip(), + "text": str(item.get("text") or "").strip(), + "document_type": str(item.get("document_type") or "").strip(), + "scene_code": str(item.get("scene_code") or "").strip(), + "scene_label": str(item.get("scene_label") or "").strip(), + "document_fields": self._normalize_document_fields(item.get("document_fields")), + } + ) + + overrides = context_json.get("review_document_form_values") + if not isinstance(overrides, list) or not normalized: + return normalized + + override_map: dict[tuple[int, str], dict[str, Any]] = {} + for item in overrides: + if not isinstance(item, dict): + continue + filename = str(item.get("filename") or "").strip() + index = int(item.get("index") or 0) + if not filename and index <= 0: + continue + override_map[(index, filename)] = item + + for item in normalized: + override = override_map.get((int(item["index"]), str(item["filename"]))) + if override is None: + override = override_map.get((int(item["index"]), "")) + if override is None: + continue + summary = str(override.get("summary") or "").strip() + scene_label = str(override.get("scene_label") or "").strip() + fields = override.get("fields") + if summary: + item["summary"] = summary + if scene_label: + item["scene_label"] = scene_label + if isinstance(fields, list): + item["document_fields"] = self._normalize_document_fields(fields) + + return normalized + + @staticmethod + def _normalize_document_fields(raw_fields: Any) -> list[dict[str, str]]: + if not isinstance(raw_fields, list): + return [] + normalized: list[dict[str, str]] = [] + for field in raw_fields: + if not isinstance(field, dict): + continue + label = str(field.get("label") or "").strip() + value = str(field.get("value") or "").strip() + key = str(field.get("key") or label or "").strip() + if not label or not value: + continue + normalized.append( + { + "key": key, + "label": label, + "value": value, + } + ) + return normalized + + def _build_context_item_specs( + self, + *, + context_documents: list[dict[str, Any]], + attachment_names: list[str], + occurred_at: datetime, + expense_type: str, + amount: Decimal, + reason: str, + location: str, + ) -> list[dict[str, Any]]: + specs: list[dict[str, Any]] = [] + if context_documents: + for document in context_documents: + specs.append( + { + "item_date": self._resolve_document_item_date(document, fallback=occurred_at.date()), + "item_type": self._resolve_document_item_type(document, fallback=expense_type), + "item_reason": reason, + "item_location": location, + "item_amount": self._resolve_document_item_amount(document), + "invoice_id": str(document.get("filename") or "").strip() or None, + } + ) + elif attachment_names: + for attachment_name in attachment_names: + specs.append( + { + "item_date": occurred_at.date(), + "item_type": expense_type, + "item_reason": reason, + "item_location": location, + "item_amount": None, + "invoice_id": attachment_name, + } + ) + + if not specs: + return [] + + total_recognized = sum( + spec["item_amount"] for spec in specs if isinstance(spec.get("item_amount"), Decimal) + ) + missing_specs = [spec for spec in specs if spec.get("item_amount") is None] + if missing_specs: + remaining = (amount - total_recognized).quantize(Decimal("0.01")) + if remaining > Decimal("0.00"): + missing_specs[0]["item_amount"] = remaining + + for spec in specs: + if spec.get("item_amount") is None: + spec["item_amount"] = Decimal("0.00") + + return specs + + def _replace_claim_items( + self, + *, + claim: ExpenseClaim, + item_specs: list[dict[str, Any]], + ) -> None: + existing_items = sorted( + list(claim.items), + key=lambda item: ( + item.item_date or date.max, + self._normalize_sort_datetime(item.created_at), + ), + ) + for index, spec in enumerate(item_specs): + item = existing_items[index] if index < len(existing_items) else None + if item is None: + item = ExpenseClaimItem(claim_id=claim.id) + claim.items.append(item) + self.db.add(item) + item.item_date = spec["item_date"] + item.item_type = spec["item_type"] + item.item_reason = spec["item_reason"] + item.item_location = spec["item_location"] + item.item_amount = spec["item_amount"] + item.invoice_id = spec["invoice_id"] + + for stale_item in existing_items[len(item_specs) :]: + claim.items.remove(stale_item) + self.db.delete(stale_item) + + def _append_document_items( + self, + *, + claim: ExpenseClaim, + item_specs: list[dict[str, Any]], + ) -> None: + existing_invoice_ids = { + str(item.invoice_id or "").strip() + for item in claim.items + if str(item.invoice_id or "").strip() + } + for spec in item_specs: + invoice_id = str(spec.get("invoice_id") or "").strip() + if invoice_id and invoice_id in existing_invoice_ids: + continue + claim.items.append( + ExpenseClaimItem( + claim_id=claim.id, + item_date=spec["item_date"], + item_type=spec["item_type"], + item_reason=spec["item_reason"], + item_location=spec["item_location"], + item_amount=spec["item_amount"], + invoice_id=spec["invoice_id"], + ) + ) + self.db.add(claim.items[-1]) + if invoice_id: + existing_invoice_ids.add(invoice_id) + + def _resolve_document_item_type(self, document: dict[str, Any], *, fallback: str) -> str: + scene_code = str(document.get("scene_code") or "").strip() + if scene_code in {"travel", "hotel", "transport", "meal", "office", "meeting", "training"}: + return scene_code + + document_type = str(document.get("document_type") or "").strip() + if document_type in {"flight_itinerary", "train_ticket"}: + return "travel" + if document_type in {"taxi_receipt", "parking_toll_receipt", "transport_receipt"}: + return "transport" + if document_type == "hotel_invoice": + return "hotel" + if document_type == "meal_receipt": + return "meal" + if document_type == "office_invoice": + return "office" + if document_type == "meeting_invoice": + return "meeting" + if document_type == "training_invoice": + return "training" + + scene_label = str(document.get("scene_label") or "").strip() + if "交通" in scene_label: + return "transport" + if "住宿" in scene_label: + return "hotel" + if "餐" in scene_label: + return "meal" + if "会务" in scene_label or "会议" in scene_label: + return "meeting" + if "培训" in scene_label: + return "training" + return fallback or "other" + + def _resolve_document_item_amount(self, document: dict[str, Any]) -> Decimal | None: + for field in list(document.get("document_fields") or []): + if not isinstance(field, dict): + continue + key = str(field.get("key") or "").strip().lower().replace("_", "") + label = str(field.get("label") or "").replace(" ", "") + value = self._parse_document_amount_value(str(field.get("value") or "")) + if value is None: + continue + if key in { + "amount", + "totalamount", + "paymentamount", + "paidamount", + "actualamount", + } or any( + token in label + for token in ("金额", "价税合计", "合计", "总额", "总计", "票价", "支付金额", "实付金额", "实收金额") + ): + return value + + text = " ".join( + [ + str(document.get("summary") or "").strip(), + str(document.get("text") or "").strip(), + ] + ).strip() + return self._parse_document_amount_value(text) + + def _parse_document_amount_value(self, value: str) -> Decimal | None: + raw_value = str(value or "").strip() + if not raw_value: + return None + for pattern in DOCUMENT_AMOUNT_PATTERNS: + match = pattern.search(raw_value) + if not match: + continue + numeric = str(match.group(1) or "").replace(",", ".").strip() + try: + amount = Decimal(numeric).quantize(Decimal("0.01")) + except (InvalidOperation, ValueError): + continue + if amount > Decimal("0.00"): + return amount + return None + + def _resolve_document_item_date(self, document: dict[str, Any], *, fallback: date) -> date: + for field in list(document.get("document_fields") or []): + if not isinstance(field, dict): + continue + key = str(field.get("key") or "").strip().lower().replace("_", "") + label = str(field.get("label") or "").replace(" ", "") + value = str(field.get("value") or "").strip() + if not value: + continue + if key in {"date", "time", "issuedat", "invoicedate"} or any( + token in label for token in ("日期", "时间", "开票日期", "发生时间") + ): + parsed = self._parse_document_date(value) + if parsed is not None: + return parsed + + parsed = self._parse_document_date( + " ".join( + [ + str(document.get("summary") or "").strip(), + str(document.get("text") or "").strip(), + ] + ).strip() + ) + return parsed or fallback + + @staticmethod + def _parse_document_date(value: str) -> date | None: + match = DOCUMENT_DATE_PATTERN.search(str(value or "")) + if not match: + return None + raw_value = str(match.group(1) or "").strip() + normalized = raw_value.replace("年", "-").replace("月", "-").replace("日", "") + normalized = normalized.replace("/", "-").replace(".", "-") + parts = [part for part in normalized.split("-") if part] + if len(parts) != 3: + return None + try: + return date(int(parts[0]), int(parts[1]), int(parts[2])) + except ValueError: + return None + + def _upsert_primary_item( + self, + *, + claim: ExpenseClaim, + occurred_at: datetime, + expense_type: str, + amount: Decimal, + reason: str, + location: str, + attachment_names: list[str], + ) -> None: + item = claim.items[0] if claim.items else None + if item is None: + item = ExpenseClaimItem( + claim_id=claim.id, + item_date=occurred_at.date(), + item_type=expense_type, + item_reason=reason, + item_location=location, + item_amount=amount, + invoice_id=attachment_names[0] if attachment_names else None, + ) + claim.items.append(item) + self.db.add(item) + return + + item.item_date = occurred_at.date() + item.item_type = expense_type + item.item_reason = reason + item.item_location = location + item.item_amount = amount + item.invoice_id = attachment_names[0] if attachment_names else item.invoice_id + + def _generate_claim_no(self, occurred_at: datetime) -> str: + month_code = occurred_at.strftime("%Y%m") + prefix = f"EXP-{month_code}-" + existing_claim_nos = list( + self.db.scalars( + select(ExpenseClaim.claim_no).where(ExpenseClaim.claim_no.like(f"{prefix}%")) + ) + ) + max_suffix = 0 + for claim_no in existing_claim_nos: + normalized = str(claim_no or "").strip() + if not normalized.startswith(prefix): + continue + suffix = normalized[len(prefix):] + if not suffix.isdigit(): + continue + max_suffix = max(max_suffix, int(suffix)) + return f"{prefix}{max_suffix + 1:03d}" + + @staticmethod + def _resolve_claim_no_retry_count(context_json: dict[str, Any]) -> int: + try: + return max(0, int(context_json.get("_claim_no_retry_count") or 0)) + except (TypeError, ValueError): + return 0 + + @staticmethod + def _is_claim_no_conflict_error(exc: IntegrityError) -> bool: + message = str(exc).lower() + return ( + "claim_no" in message + and ( + "unique" in message + or "duplicate key" in message + or "ix_expense_claims_claim_no" in message + or "expense_claims.claim_no" in message + ) + ) + + def _count_draft_claims_for_owner( + self, + *, + employee: Employee | None, + user_id: str | None, + ) -> int: + owner_filters = self._build_draft_owner_filters( + employee=employee, + user_id=user_id, + ) + if not owner_filters: + return 0 + + stmt = ( + select(func.count()) + .select_from(ExpenseClaim) + .where(ExpenseClaim.status == "draft") + .where(or_(*owner_filters)) + ) + return int(self.db.scalar(stmt) or 0) + + def _build_draft_owner_filters( + self, + *, + employee: Employee | None, + user_id: str | None, + ) -> list[Any]: + conditions: list[Any] = [] + seen: set[tuple[str, str]] = set() + + def add_condition(field_name: str, value: str | None) -> None: + normalized = str(value or "").strip() + if not normalized or normalized == "待补充": + return + + marker = (field_name, normalized.lower()) + if marker in seen: + return + seen.add(marker) + + if field_name == "employee_id": + conditions.append(ExpenseClaim.employee_id == normalized) + return + conditions.append(ExpenseClaim.employee_name == normalized) + + if employee is not None: + add_condition("employee_id", employee.id) + add_condition("employee_name", employee.email) + if self._employee_name_is_unique(employee): + add_condition("employee_name", employee.name) + + add_condition("employee_name", user_id) + return conditions + + def _resolve_employee( + self, + *, + ontology: OntologyParseResult, + context_json: dict[str, Any], + user_id: str | None, + ) -> Employee | None: + normalized_user_id = str(user_id or "").strip() + if normalized_user_id: + stmt = select(Employee).where(func.lower(Employee.email) == normalized_user_id.lower()).limit(1) + employee = self.db.scalar(stmt) + if employee is not None: + return employee + + employee_name = self._resolve_employee_name( + ontology=ontology, + context_json=context_json, + user_id=None, + ) + if not employee_name: + return None + + stmt = select(Employee).where(Employee.name == employee_name).limit(1) + return self.db.scalar(stmt) + + @staticmethod + def _resolve_employee_name( + *, + ontology: OntologyParseResult, + context_json: dict[str, Any], + user_id: str | None, + fallback: str = "待补充", + ) -> str: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + for key in ("reporter_name", "employee_name", "claimant_name"): + value = str(review_form_values.get(key) or "").strip() + if value: + return value + for item in ontology.entities: + if item.type == "employee" and item.value.strip(): + return item.value.strip() + for key in ("name", "user_name", "employee_name"): + value = str(context_json.get(key) or "").strip() + if value: + return value + return str(user_id or fallback).strip() or fallback + + @staticmethod + def _resolve_department_name( + *, + employee: Employee | None, + context_json: dict[str, Any], + fallback: str = "待补充", + ) -> str: + if employee is not None and employee.organization_unit is not None: + return employee.organization_unit.name + + request_context = context_json.get("request_context") + if isinstance(request_context, dict): + for key in ("department", "department_name", "deptName"): + value = str(request_context.get(key) or "").strip() + if value: + return value + + for key in ("department_name", "department"): + value = str(context_json.get(key) or "").strip() + if value: + return value + return fallback + + @staticmethod + def _resolve_project_code(entities: list[OntologyEntity]) -> str | None: + for item in entities: + if item.type == "project" and item.normalized_value.strip(): + return item.normalized_value.strip() + return None + + @staticmethod + def _resolve_expense_type( + entities: list[OntologyEntity], + *, + context_json: dict[str, Any], + ) -> str | None: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + compact = str( + review_form_values.get("expense_type") + or review_form_values.get("reimbursement_type") + or "" + ).replace(" ", "") + if compact: + if "招待" in compact or ("客户" in compact and any(word in compact for word in ("吃饭", "宴请", "请客", "用餐"))): + return "entertainment" + if any(word in compact for word in ("差旅", "出差", "机票", "行程")): + return "travel" + if any(word in compact for word in ("住宿", "酒店", "宾馆")): + return "hotel" + if any(word in compact for word in ("交通", "打车", "网约车", "出租车", "停车", "车费")): + return "transport" + if any(word in compact for word in ("餐费", "用餐", "午餐", "晚餐", "早餐", "伙食")): + return "meal" + if "会务" in compact: + return "meeting" + if any(word in compact for word in ("办公费", "办公用品", "文具", "耗材", "办公耗材", "打印纸", "办公设备", "键盘", "鼠标", "白板")): + return "office" + if any(word in compact for word in ("培训费", "培训", "讲师费", "课时费", "课程费")): + return "training" + if any(word in compact for word in ("通讯费", "话费", "流量费", "宽带费")): + return "communication" + if any(word in compact for word in ("福利费", "团建", "慰问", "节日福利", "体检费")): + return "welfare" + for item in entities: + if item.type == "expense_type": + normalized = item.normalized_value.strip() + if normalized: + return normalized + return None + + @staticmethod + def _resolve_reason( + *, + message: str, + context_json: dict[str, Any], + allow_message_fallback: bool, + ) -> str | None: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + for key in ("reason", "business_reason"): + value = str(review_form_values.get(key) or "").strip() + if value: + return value + + explicit_text = context_json.get("user_input_text") + if isinstance(explicit_text, str): + normalized_explicit_text = explicit_text.strip() + if normalized_explicit_text: + return normalized_explicit_text[:500] + return None + + request_context = context_json.get("request_context") + if ( + isinstance(request_context, dict) + and str(context_json.get("entry_source") or "").strip() == "detail" + ): + for key in ("reason", "title"): + value = str(request_context.get(key) or "").strip() + if value: + return value + if not allow_message_fallback: + return None + + normalized_message = str(message or "").strip() + compact_message = re.sub(r"\s+", "", normalized_message) + if compact_message.startswith(SYSTEM_GENERATED_REASON_PREFIXES): + return None + return normalized_message[:500] or None + + @staticmethod + def _resolve_location(*, message: str, context_json: dict[str, Any]) -> str | None: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + for key in ("business_location", "location"): + value = str(review_form_values.get(key) or "").strip() + if value: + return value + + request_context = context_json.get("request_context") + if ( + isinstance(request_context, dict) + and str(context_json.get("entry_source") or "").strip() == "detail" + ): + for key in ("city", "location"): + value = str(request_context.get(key) or "").strip() + if value: + return value + compact = str(message or "").replace(" ", "") + if "客户现场" in compact: + return "客户现场" + return None + + @staticmethod + def _resolve_occurred_at( + ontology: OntologyParseResult, + *, + context_json: dict[str, Any], + ) -> datetime | None: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + for key in ("occurred_date", "time_range", "business_time"): + value = str(review_form_values.get(key) or "").strip() + if not value: + continue + try: + parsed = date.fromisoformat(value) + return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC) + except ValueError: + continue + + start_date = ontology.time_range.start_date + if start_date: + try: + parsed = date.fromisoformat(start_date) + return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC) + except ValueError: + pass + return None + + @staticmethod + def _resolve_amount( + entities: list[OntologyEntity], + *, + context_json: dict[str, Any], + ) -> Decimal | None: + review_form_values = context_json.get("review_form_values") + if isinstance(review_form_values, dict): + raw_value = str(review_form_values.get("amount") or "").strip() + if raw_value: + compact = raw_value.replace("元", "").replace(",", "").strip() + try: + return Decimal(compact).quantize(Decimal("0.01")) + except (InvalidOperation, ValueError): + pass + for item in entities: + if item.type != "amount" or item.role == "threshold": + continue + try: + return Decimal(item.normalized_value).quantize(Decimal("0.01")) + except (InvalidOperation, ValueError): + continue + return None + + @staticmethod + def _resolve_attachment_names(context_json: dict[str, Any]) -> list[str]: + names = context_json.get("attachment_names") + if not isinstance(names, list): + return [] + return [str(name).strip() for name in names if str(name).strip()] + + def _resolve_attachment_count(self, context_json: dict[str, Any]) -> int: + names = self._resolve_attachment_names(context_json) + if names: + return len(names) + try: + return max(0, int(context_json.get("attachment_count") or 0)) + except (TypeError, ValueError): + return 0 + + def _get_claim_item_or_raise( + self, + *, + claim_id: str, + item_id: str, + current_user: CurrentUserContext, + ) -> tuple[ExpenseClaim | None, ExpenseClaimItem]: + claim = self.get_claim(claim_id, current_user) + if claim is None: + return None, None # type: ignore[return-value] + + item = next((entry for entry in claim.items if entry.id == item_id), None) + if item is None: + raise LookupError("Item not found") + return claim, item + + def _get_attachment_storage_root(self) -> Path: + return (get_settings().resolved_storage_root_dir / "expense_claims").resolve() + + def _build_item_attachment_dir(self, claim_id: str, item_id: str) -> Path: + return (self._get_attachment_storage_root() / claim_id / item_id).resolve() + + def _delete_claim_attachment_root(self, claim_id: str) -> None: + shutil.rmtree((self._get_attachment_storage_root() / claim_id).resolve(), ignore_errors=True) + + @staticmethod + def _normalize_attachment_filename(filename: str | None) -> str: + normalized = Path(str(filename or "").strip()).name + normalized = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", normalized).strip("._") + suffix = Path(normalized).suffix + if normalized: + return normalized + return f"attachment{suffix or '.bin'}" + + def _resolve_attachment_path(self, storage_key: str | None) -> Path | None: + normalized = str(storage_key or "").strip() + if not normalized: + return None + + root = self._get_attachment_storage_root() + path = (root / normalized).resolve() + try: + path.relative_to(root) + except ValueError as exc: + raise FileNotFoundError("Attachment path is invalid") from exc + return path + + def _to_attachment_storage_key(self, file_path: Path) -> str: + root = self._get_attachment_storage_root() + return file_path.resolve().relative_to(root).as_posix() + + def _resolve_item_attachment_content(self, item: ExpenseClaimItem) -> tuple[Path, str, str]: + file_path = self._resolve_attachment_path(item.invoice_id) + if file_path is None or not file_path.exists(): + raise FileNotFoundError("Attachment not found") + + metadata = self._read_attachment_meta(file_path) + filename = str(metadata.get("file_name") or file_path.name) + media_type = self._resolve_attachment_media_type( + filename, + fallback=str(metadata.get("media_type") or ""), + ) + return file_path, media_type, filename + + def _delete_item_attachment_files(self, item: ExpenseClaimItem) -> None: + file_path = self._resolve_attachment_path(item.invoice_id) + if file_path is None: + return + + root = self._get_attachment_storage_root() + if file_path.parent == root: + file_path.unlink(missing_ok=True) + self._attachment_meta_path(file_path).unlink(missing_ok=True) + return + + shutil.rmtree(file_path.parent, ignore_errors=True) + + @staticmethod + def _attachment_meta_path(file_path: Path) -> Path: + return file_path.with_name(f"{file_path.name}.meta.json") + + def _write_attachment_meta(self, file_path: Path, payload: dict[str, Any]) -> None: + meta_path = self._attachment_meta_path(file_path) + meta_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + def _read_attachment_meta(self, file_path: Path) -> dict[str, Any]: + meta_path = self._attachment_meta_path(file_path) + if not meta_path.exists(): + return {} + + try: + payload = json.loads(meta_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return {} + return payload if isinstance(payload, dict) else {} + + def _build_attachment_preview_meta( + self, + *, + file_path: Path, + media_type: str, + ocr_document: Any | None, + ) -> dict[str, Any]: + filename = file_path.name + storage_key = self._to_attachment_storage_key(file_path) + preview_kind = self._resolve_preview_kind(media_type, filename) + + preview_data_url = str(getattr(ocr_document, "preview_data_url", "") or "").strip() + preview_source_kind = str(getattr(ocr_document, "preview_kind", "") or "").strip() + if preview_source_kind == "image" and preview_data_url: + preview_asset = self._write_preview_asset_from_data_url( + attachment_dir=file_path.parent, + original_filename=filename, + preview_data_url=preview_data_url, + ) + if preview_asset is not None: + preview_path, preview_media_type, preview_file_name = preview_asset + return { + "previewable": True, + "preview_kind": "image", + "preview_storage_key": self._to_attachment_storage_key(preview_path), + "preview_media_type": preview_media_type, + "preview_file_name": preview_file_name, + } + + if preview_kind: + return { + "previewable": True, + "preview_kind": preview_kind, + "preview_storage_key": storage_key, + "preview_media_type": media_type, + "preview_file_name": filename, + } + + return { + "previewable": False, + "preview_kind": "", + "preview_storage_key": "", + "preview_media_type": "", + "preview_file_name": "", + } + + def _resolve_item_attachment_preview_content(self, item: ExpenseClaimItem) -> tuple[Path, str, str]: + file_path, media_type, filename = self._resolve_item_attachment_content(item) + metadata = self._read_attachment_meta(file_path) + preview_storage_key = str(metadata.get("preview_storage_key") or "").strip() + preview_file_name = str(metadata.get("preview_file_name") or "").strip() + preview_media_type = str(metadata.get("preview_media_type") or "").strip() + + if preview_storage_key: + preview_path = self._resolve_attachment_path(preview_storage_key) + if preview_path is not None and preview_path.exists(): + resolved_name = preview_file_name or preview_path.name + resolved_media_type = self._resolve_attachment_media_type( + resolved_name, + fallback=preview_media_type, + ) + return preview_path, resolved_media_type, resolved_name + + if self._is_previewable_media_type(media_type, filename): + return file_path, media_type, filename + + raise FileNotFoundError("Attachment preview not found") + + def _build_attachment_payload(self, item: ExpenseClaimItem) -> dict[str, Any]: + file_path, media_type, filename = self._resolve_item_attachment_content(item) + metadata = self._read_attachment_meta(file_path) + uploaded_at_value = metadata.get("uploaded_at") + uploaded_at = None + if isinstance(uploaded_at_value, str) and uploaded_at_value.strip(): + try: + uploaded_at = datetime.fromisoformat(uploaded_at_value) + except ValueError: + uploaded_at = None + + analysis = metadata.get("analysis") + if not isinstance(analysis, dict): + analysis = None + + document_info = metadata.get("document_info") + if not isinstance(document_info, dict): + document_info = None + + requirement_check = metadata.get("requirement_check") + if not isinstance(requirement_check, dict): + requirement_check = None + + preview_kind = str(metadata.get("preview_kind") or "").strip() + previewable = bool(metadata.get("previewable", self._is_previewable_media_type(media_type, filename))) + preview_url = self._build_attachment_preview_client_path(item.claim_id, item.id) if previewable else "" + + return { + "file_name": str(metadata.get("file_name") or filename), + "storage_key": str(item.invoice_id or ""), + "media_type": str(metadata.get("media_type") or media_type), + "size_bytes": int(metadata.get("size_bytes") or file_path.stat().st_size), + "uploaded_at": uploaded_at, + "previewable": previewable, + "preview_kind": preview_kind or self._resolve_preview_kind(media_type, filename), + "preview_url": preview_url, + "analysis": analysis, + "document_info": document_info, + "requirement_check": requirement_check, + } + + @staticmethod + def _resolve_preview_kind(media_type: str | None, filename: str) -> str: + resolved = str(media_type or "").strip() or (mimetypes.guess_type(filename)[0] or "") + if resolved.startswith("image/"): + return "image" + if resolved == "application/pdf": + return "pdf" + return "" + + @staticmethod + def _decode_data_url(payload: str) -> tuple[str, bytes] | None: + normalized = str(payload or "").strip() + matched = re.match(r"^data:(?P[\w.+-]+/[\w.+-]+);base64,(?P.+)$", normalized, flags=re.DOTALL) + if not matched: + return None + try: + content = base64.b64decode(matched.group("body"), validate=True) + except (binascii.Error, ValueError): + return None + return matched.group("media"), content + + def _write_preview_asset_from_data_url( + self, + *, + attachment_dir: Path, + original_filename: str, + preview_data_url: str, + ) -> tuple[Path, str, str] | None: + decoded = self._decode_data_url(preview_data_url) + if decoded is None: + return None + + preview_media_type, preview_content = decoded + suffix = mimetypes.guess_extension(preview_media_type) or ".bin" + preview_name = f"{Path(original_filename).stem}.preview{suffix}" + preview_path = attachment_dir / preview_name + preview_path.write_bytes(preview_content) + return preview_path, preview_media_type, preview_name + + @staticmethod + def _build_attachment_preview_client_path(claim_id: str, item_id: str) -> str: + return ( + "/reimbursements/claims/" + f"{quote(str(claim_id or '').strip(), safe='')}" + f"/items/{quote(str(item_id or '').strip(), safe='')}/attachment/preview" + ) + + @staticmethod + def _resolve_attachment_media_type(filename: str, *, fallback: str | None = None) -> str: + guessed = mimetypes.guess_type(filename)[0] + return str(guessed or fallback or "application/octet-stream") + + @staticmethod + def _is_previewable_media_type(media_type: str | None, filename: str) -> bool: + resolved = str(media_type or "").strip() or (mimetypes.guess_type(filename)[0] or "") + return resolved.startswith("image/") or resolved == "application/pdf" + + @staticmethod + def _resolve_attachment_display_name(storage_key: str | None) -> str: + return Path(str(storage_key or "").strip()).name + + def _build_attachment_document_info(self, document: Any) -> dict[str, Any]: + insight = build_document_insight( + filename=str(getattr(document, "filename", "") or ""), + summary=str(getattr(document, "summary", "") or ""), + text=str(getattr(document, "text", "") or ""), + ) + raw_fields = list(getattr(document, "document_fields", []) or []) + normalized_fields: list[dict[str, str]] = [] + for item in raw_fields: + key = "" + label = "" + value = "" + if isinstance(item, dict): + key = str(item.get("key") or "").strip() + label = str(item.get("label") or "").strip() + value = str(item.get("value") or "").strip() + else: + key = str(getattr(item, "key", "") or "").strip() + label = str(getattr(item, "label", "") or "").strip() + value = str(getattr(item, "value", "") or "").strip() + if key and label and value: + normalized_fields.append( + { + "key": key, + "label": label, + "value": value, + } + ) + + if not normalized_fields: + normalized_fields = [ + { + "key": field.key, + "label": field.label, + "value": field.value, + } + for field in insight.fields + if field.value + ] + + document_type = str(getattr(document, "document_type", "") or "").strip() + if document_type in {"", "other"}: + document_type = insight.document_type + + document_type_label = str(getattr(document, "document_type_label", "") or "").strip() + if not document_type_label or document_type_label == "其他单据": + document_type_label = insight.document_type_label + + scene_code = str(getattr(document, "scene_code", "") or "").strip() + if scene_code in {"", "other"}: + scene_code = insight.scene_code + + scene_label = str(getattr(document, "scene_label", "") or "").strip() + if not scene_label or scene_label == "其他票据": + scene_label = insight.scene_label + + return { + "document_type": document_type, + "document_type_label": document_type_label, + "scene_code": scene_code, + "scene_label": scene_label, + "fields": normalized_fields, + } + + def _build_attachment_requirement_check( + self, + *, + item: ExpenseClaimItem, + document_info: dict[str, Any], + ) -> dict[str, Any]: + expense_type = str(item.item_type or "").strip().lower() or "other" + policy = self._get_expense_scene_policy(expense_type) + expense_label = policy.label if policy is not None else self._resolve_expense_type_label(expense_type) + allowed_scenes = set(policy.allowed_scene_codes) if policy is not None else set() + allowed_document_types = set(policy.allowed_document_types) if policy is not None else set() + allowed_scene_labels = [self._resolve_document_scene_label(code) for code in sorted(allowed_scenes)] + allowed_document_type_labels = [ + resolve_document_type_label(document_type) + for document_type in sorted(allowed_document_types) + ] + recognized_scene_code = str(document_info.get("scene_code") or "other").strip() or "other" + recognized_scene_label = str( + document_info.get("scene_label") or self._resolve_document_scene_label(recognized_scene_code) + ).strip() + recognized_document_type = str(document_info.get("document_type") or "other").strip() or "other" + recognized_document_type_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据" + matches = ( + (not allowed_scenes and not allowed_document_types) + or recognized_scene_code in allowed_scenes + or recognized_document_type in allowed_document_types + ) + + if matches: + if allowed_scene_labels or allowed_document_type_labels: + message = ( + f"当前费用项目为{expense_label},已识别为{recognized_document_type_label}," + f"符合当前{expense_label}场景的附件要求。" + ) + else: + message = f"当前费用项目为{expense_label},已识别为{recognized_document_type_label}。" + else: + expected_parts = [label + "相关票据" for label in allowed_scene_labels] + expected_parts.extend(allowed_document_type_labels) + expected_text = "、".join(dict.fromkeys(part for part in expected_parts if part)) or "对应场景票据" + message = ( + f"当前费用项目为{expense_label},要求上传{expected_text};" + f"当前识别为{recognized_document_type_label},不符合当前场景,建议过滤或更换附件。" + ) + + return { + "matches": matches, + "current_expense_type": expense_type, + "current_expense_type_label": expense_label, + "allowed_scene_labels": allowed_scene_labels, + "allowed_document_type_labels": allowed_document_type_labels, + "recognized_scene_code": recognized_scene_code, + "recognized_scene_label": recognized_scene_label, + "recognized_document_type": recognized_document_type, + "recognized_document_type_label": recognized_document_type_label, + "mismatch_severity": policy.attachment_mismatch_severity if policy is not None else "high", + "rule_code": policy.rule_code if policy is not None else DEFAULT_SCENE_RULE_ASSET_CODE, + "rule_name": policy.rule_name if policy is not None else "报销场景提交与附件标准", + "message": message, + } + + @staticmethod + def _resolve_document_scene_label(scene_code: str) -> str: + normalized = str(scene_code or "").strip().lower() + return DOCUMENT_SCENE_LABELS.get(normalized, "其他票据") + + @staticmethod + def _extract_amount_candidates(text: str) -> list[Decimal]: + values: list[Decimal] = [] + seen: set[Decimal] = set() + + def append_candidate(raw: str) -> None: + compact = str(raw or "").replace(",", ".").strip() + if not compact: + return + try: + candidate = Decimal(compact).quantize(Decimal("0.01")) + except (InvalidOperation, ValueError): + return + if candidate in seen: + return + seen.add(candidate) + values.append(candidate) + + for pattern in ( + r"(?:金额|价税合计|合计|小写|实收金额|支付金额|订单金额|总额|票价|房费|餐费)[::\s¥¥]*([0-9]{1,6}(?:[.,][0-9]{1,2})?)", + r"[¥¥]\s*([0-9]{1,6}(?:[.,][0-9]{1,2})?)", + r"([0-9]{1,6}(?:[.,][0-9]{1,2})?)\s*元", + ): + for raw in re.findall(pattern, text, flags=re.IGNORECASE): + append_candidate(raw) + + if values: + return values + + for raw in re.findall(r"(? bool: + return bool(re.search(r"(20\d{2}[年/\-.]\d{1,2}[月/\-.]\d{1,2}日?)", text)) + + @staticmethod + def _normalize_match_text(text: str) -> str: + return re.sub(r"\s+", "", str(text or "")).lower() + + @staticmethod + def _resolve_expense_type_label(expense_type: str | None) -> str: + normalized = str(expense_type or "").strip().lower() + return EXPENSE_TYPE_LABELS.get(normalized, "其他") + + def _resolve_allowed_document_scenes(self, expense_type: str | None) -> set[str]: + normalized = str(expense_type or "").strip().lower() + policy = self._get_expense_scene_policy(normalized) + return set(policy.allowed_scene_codes) if policy is not None else set() + + def _detect_expense_scenes(self, text: str) -> dict[str, list[str]]: + normalized = self._normalize_match_text(text) + if not normalized: + return {} + + matches: dict[str, list[str]] = {} + for scene, keywords in EXPENSE_SCENE_KEYWORDS.items(): + matched = [keyword for keyword in keywords if keyword in normalized] + if matched: + matches[scene] = matched[:3] + return matches + + def _format_scene_labels(self, scene_codes: set[str]) -> str: + labels = [self._resolve_expense_type_label(code) for code in scene_codes] + unique_labels = list(dict.fromkeys(label for label in labels if label)) + return "、".join(unique_labels) if unique_labels else "其他" + + def _build_purpose_mismatch_point( + self, + *, + item: ExpenseClaimItem, + document_scenes: set[str], + ) -> str | None: + if not document_scenes: + return None + + allowed_scenes = self._resolve_allowed_document_scenes(item.item_type) + reason_text = str(item.item_reason or "").strip() + reason_scenes = set(self._detect_expense_scenes(reason_text).keys()) + document_scene_labels = self._format_scene_labels(document_scenes) + + if reason_scenes and document_scenes.isdisjoint(reason_scenes): + return ( + f"用途字段:用户填写用途“{reason_text[:24]}”与票据内容不一致," + f"当前附件更像{document_scene_labels}相关材料。" + ) + + if allowed_scenes and document_scenes.isdisjoint(allowed_scenes): + expense_label = self._resolve_expense_type_label(item.item_type) + return f"用途字段:当前费用项目为{expense_label},但附件内容更像{document_scene_labels}相关票据。" + + return None + + def _build_fallback_attachment_analysis( + self, + *, + media_type: str | None, + item: ExpenseClaimItem, + ) -> dict[str, Any]: + return { + "severity": "medium", + "label": "中风险", + "headline": "AI提示:附件已上传,待识别结果", + "summary": "附件已成功保存,但当前尚未拿到有效识别结果,建议人工先核对票据内容。", + "points": [ + f"附件格式:{self._resolve_attachment_media_type('attachment', fallback=media_type)}", + f"费用金额:当前明细金额为 {item.item_amount} 元", + ], + "suggestion": "建议打开附件确认金额、日期和票据类型是否完整,再继续提交审批。", + } + + def _build_failed_ocr_attachment_analysis( + self, + *, + media_type: str | None, + error_message: str, + item: ExpenseClaimItem, + ) -> dict[str, Any]: + return { + "severity": "medium", + "label": "中风险", + "headline": "AI提示:附件已上传,但识别失败", + "summary": "文件已经保存成功,但本次 AI 识别未完成,因此无法给出完整票据核验结论。", + "points": [ + f"识别异常:{error_message or 'OCR 服务暂不可用'}", + f"费用金额:当前明细金额为 {item.item_amount} 元", + f"附件格式:{self._resolve_attachment_media_type('attachment', fallback=media_type)}", + ], + "suggestion": "建议重新上传更清晰的票据图片,或稍后重试识别后再提交。", + } + + def _build_attachment_analysis( + self, + *, + document: Any, + item: ExpenseClaimItem, + document_info: dict[str, Any] | None = None, + requirement_check: dict[str, Any] | None = None, + ) -> dict[str, Any]: + warnings = [str(value).strip() for value in list(getattr(document, "warnings", []) or []) if str(value).strip()] + text = " ".join( + [ + str(getattr(document, "summary", "") or "").strip(), + str(getattr(document, "text", "") or "").strip(), + ] + ).strip() + compact_text = text.replace(" ", "") + avg_score = float(getattr(document, "avg_score", 0.0) or 0.0) + line_count = int(getattr(document, "line_count", 0) or 0) + document_info = document_info or self._build_attachment_document_info(document) + requirement_check = requirement_check or self._build_attachment_requirement_check( + item=item, + document_info=document_info, + ) + document_scene_matches = self._detect_expense_scenes(text) + purpose_mismatch_point = self._build_purpose_mismatch_point( + item=item, + document_scenes=set(document_scene_matches.keys()), + ) + recognized_document_type = str(document_info.get("document_type") or "other").strip().lower() or "other" + recognized_document_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据" + requirement_matches = bool(requirement_check.get("matches")) + mismatch_severity = str(requirement_check.get("mismatch_severity") or "high").strip().lower() or "high" + + has_ticket_keyword = any( + keyword in compact_text + for keyword in ( + "发票", + "票据", + "增值税", + "电子行程单", + "购买方", + "销售方", + "税额", + "价税", + "票号", + "发票代码", + "凭证", + ) + ) + amount_candidates = self._extract_amount_candidates(text) + item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) + has_matching_amount = any(abs(candidate - item_amount) <= Decimal("1.00") for candidate in amount_candidates) + has_date_text = self._has_date_like_text(text) + amount_mismatch = bool(amount_candidates) and item_amount > Decimal("0.00") and not has_matching_amount + + points: list[str] = [] + if warnings: + points.append(f"识别提示:{warnings[0]}") + if line_count == 0 or not compact_text: + points.append("附件内容:未识别到有效文字,当前附件更像普通图片或内容过于模糊。") + if recognized_document_type == "other" and not has_ticket_keyword: + points.append("票据类型:未识别到发票、票据、电子行程单等关键字,暂无法判断票据类型。") + if not amount_candidates: + points.append("金额字段:未识别到可用于核对的金额。") + elif amount_mismatch: + candidate_text = "、".join(str(candidate) for candidate in amount_candidates[:3]) + points.append(f"金额字段:附件识别金额 {candidate_text} 元与报销金额 {item_amount} 元不一致。") + if not has_date_text: + points.append("日期字段:未识别到开票日期或业务发生日期。") + if not requirement_matches: + points.append(f"附件类型要求:{requirement_check.get('message')}") + if purpose_mismatch_point: + points.append(purpose_mismatch_point) + if avg_score and avg_score < 0.72: + points.append(f"识别质量:OCR 置信度偏低({avg_score:.0%}),可能影响票据核验准确性。") + + issue_count = len(points) + if issue_count == 0: + return { + "severity": "pass", + "label": "AI提示符合条件", + "headline": "AI提示:附件符合基础校验条件", + "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", + "points": [ + f"票据类型:已识别为{recognized_document_label}。", + f"附件类型要求:{requirement_check.get('message')}", + f"金额字段:已识别到与当前明细接近的金额 {item_amount} 元。", + ], + "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。", + } + + severity = "low" + label = "低风险" + headline = "AI提示:附件存在轻微待核对项" + summary = "当前附件已识别出部分票据要素,但仍建议人工继续复核。" + + if ( + line_count == 0 + or not compact_text + or (recognized_document_type == "other" and not has_ticket_keyword and issue_count >= 2) + or (not requirement_matches and mismatch_severity == "high") + or (purpose_mismatch_point and amount_mismatch) + ): + severity = "high" + label = "高风险" + headline = "AI提示:附件不符合票据校验条件" + summary = "当前附件存在明显异常,票据类型与当前费用场景不匹配,或无法作为有效报销材料。" + elif ( + purpose_mismatch_point + or amount_mismatch + or issue_count >= 2 + or warnings + or (avg_score and avg_score < 0.72) + or (not requirement_matches and mismatch_severity in {"medium", "low"}) + ): + severity = "medium" + label = "中风险" + headline = "AI提示:附件存在明显待整改项" + summary = "当前附件可见部分内容,但金额、用途、日期或附件类型仍有缺失或不一致。" + + suggestion = { + "high": "建议过滤当前不匹配的票据,重新上传符合当前费用场景的清晰原件。", + "medium": "建议根据风险点补齐清晰票据,或修正金额、日期、费用说明后再提交。", + "low": "建议人工再次核对金额和业务说明,确认后可继续流转。", + }[severity] + + return { + "severity": severity, + "label": label, + "headline": headline, + "summary": summary, + "points": points, + "suggestion": suggestion, + } + + @staticmethod + def _serialize_claim(claim: ExpenseClaim) -> dict[str, Any]: + return { + "id": claim.id, + "claim_no": claim.claim_no, + "employee_name": claim.employee_name, + "department_name": claim.department_name, + "project_code": claim.project_code, + "expense_type": claim.expense_type, + "reason": claim.reason, + "location": claim.location, + "amount": float(claim.amount), + "invoice_count": int(claim.invoice_count or 0), + "status": claim.status, + "approval_stage": claim.approval_stage, + "risk_flags_json": list(claim.risk_flags_json or []), + } + + @staticmethod + def _normalize_optional_text(value: str | None, *, fallback: str = "", allow_empty: bool = False) -> str | None: + normalized = str(value or "").strip() + if normalized: + return normalized + if allow_empty: + return None + return fallback + + @staticmethod + def _normalize_sort_datetime(value: datetime | None) -> datetime: + if value is None: + return datetime.max.replace(tzinfo=UTC) + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value + + @staticmethod + def _is_missing_value(value: Any) -> bool: + text = str(value or "").strip() + if not text: + return True + compact = text.replace(" ", "") + return compact in {"待补充", "暂无", "无", "未知", "处理中"} + + def _ensure_draft_claim(self, claim: ExpenseClaim) -> None: + normalized_status = str(claim.status or "").strip().lower() + if normalized_status not in {"draft", "supplement"}: + raise ValueError("只有草稿或待补充状态的报销单才允许执行该操作。") + + def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]: + base_flags = list(claim.risk_flags_json or []) + attachment_flags = [ + flag + for flag in base_flags + if isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis" + ] + preserved_flags = [ + flag + for flag in base_flags + if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "submission_review") + ] + + review_flags: list[dict[str, Any]] = [] + blocking_reasons: list[str] = [] + + high_attachment_flags = [ + flag + for flag in attachment_flags + if str(flag.get("severity") or "").strip().lower() == "high" + ] + medium_attachment_flags = [ + flag + for flag in attachment_flags + if str(flag.get("severity") or "").strip().lower() == "medium" + ] + if high_attachment_flags: + blocking_reasons.append("存在高风险票据,需先补充或更换附件后再提交。") + review_flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "AI验审拦截", + "message": f"AI验审发现 {len(high_attachment_flags)} 条高风险附件,已退回待补充。", + } + ) + elif medium_attachment_flags: + review_flags.append( + { + "source": "submission_review", + "severity": "medium", + "label": "AI验审提醒", + "message": f"AI验审发现 {len(medium_attachment_flags)} 条中风险附件,已随单流转给审批人复核。", + } + ) + + manager_name = self._resolve_claim_manager_name(claim) + if not manager_name: + blocking_reasons.append("未识别到该员工的直属领导,暂时无法流转到领导审批。") + review_flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "审批链缺失", + "message": "AI验审通过前检查到直属领导缺失,当前无法继续流转审批链。", + } + ) + + historical_risk_count = self._count_recent_risky_claims(claim) + if historical_risk_count >= AI_REVIEW_REPEAT_RISK_BLOCK_COUNT: + review_flags.append( + { + "source": "submission_review", + "severity": "medium", + "label": "历史风险偏高", + "message": ( + f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," + "本次已追加到审批链重点关注。" + ), + } + ) + elif historical_risk_count >= AI_REVIEW_REPEAT_RISK_WARNING_COUNT: + review_flags.append( + { + "source": "submission_review", + "severity": "low", + "label": "历史风险提醒", + "message": ( + f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," + "建议直属领导重点复核。" + ), + } + ) + + travel_review = self._run_travel_policy_review(claim) + blocking_reasons.extend(travel_review["blocking_reasons"]) + review_flags.extend(travel_review["flags"]) + + scene_policy_review = self._run_scene_policy_review(claim) + blocking_reasons.extend(scene_policy_review["blocking_reasons"]) + review_flags.extend(scene_policy_review["flags"]) + + if blocking_reasons: + summary_message = "AI验审未通过:" + ";".join(dict.fromkeys(blocking_reasons)) + review_flags.insert( + 0, + { + "source": "submission_review", + "severity": "high", + "label": "AI验审未通过", + "message": summary_message, + }, + ) + return { + "status": "supplement", + "approval_stage": "待补充", + "risk_flags": preserved_flags + review_flags, + "message": summary_message, + "passed": False, + } + + return { + "status": "submitted", + "approval_stage": "直属领导审批", + "risk_flags": preserved_flags + review_flags, + "message": ( + f"报销单 {claim.claim_no} 已完成 AI验审," + f"现已提交给直属领导 {manager_name or '审批人'} 审批。" + ), + "passed": True, + } + + @staticmethod + def _resolve_claim_manager_name(claim: ExpenseClaim) -> str: + if claim.employee is not None: + if claim.employee.manager is not None and claim.employee.manager.name: + return str(claim.employee.manager.name).strip() + if claim.employee.organization_unit is not None and claim.employee.organization_unit.manager_name: + return str(claim.employee.organization_unit.manager_name).strip() + return "" + + def _count_recent_risky_claims(self, claim: ExpenseClaim) -> int: + filters = [] + if claim.employee_id: + filters.append(ExpenseClaim.employee_id == claim.employee_id) + elif claim.employee_name: + filters.append(ExpenseClaim.employee_name == claim.employee_name) + if not filters: + return 0 + + since = datetime.now(UTC) - timedelta(days=AI_REVIEW_LOOKBACK_DAYS) + stmt = ( + select(ExpenseClaim) + .where(or_(*filters)) + .where(ExpenseClaim.id != claim.id) + .where(ExpenseClaim.occurred_at >= since) + ) + recent_claims = list(self.db.scalars(stmt).all()) + return sum(1 for item in recent_claims if list(item.risk_flags_json or [])) + + def _run_scene_policy_review(self, claim: ExpenseClaim) -> dict[str, list[Any]]: + catalog = self._get_expense_rule_catalog() + flags: list[dict[str, Any]] = [] + blocking_reasons: list[str] = [] + reason_corpus = self._build_scene_reason_corpus(claim) + scene_totals: dict[str, Decimal] = defaultdict(lambda: Decimal("0.00")) + scene_warned: set[str] = set() + + for item in claim.items: + item_type = str(item.item_type or claim.expense_type or "other").strip().lower() or "other" + policy = catalog.get_scene_policy(item_type) + if policy is None: + continue + + scene_totals[item_type] += Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) + + if policy.always_warn and item_type not in scene_warned: + scene_warned.add(item_type) + flags.append( + { + "source": "submission_review", + "severity": "medium", + "label": f"{policy.label}人工重点复核", + "message": policy.always_warn_message or f"{policy.label}默认需要人工重点复核。", + "rule_code": policy.rule_code, + } + ) + + item_limit = policy.item_amount_limit + item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01")) + if item_limit is not None and item_amount > Decimal("0.00"): + exceeded = self._evaluate_amount_limit( + amount=item_amount, + limit_config=item_limit, + reason_text="\n".join( + part + for part in [reason_corpus, str(item.item_reason or "").strip()] + if part + ), + ) + if exceeded is not None: + severity, threshold = exceeded + label = ( + f"{policy.label}金额超标待说明" + if severity == "high" + else f"{policy.label}金额超标提醒" + ) + message = ( + f"{policy.label}当前识别金额为 {item_amount} 元," + f"已超过制度阈值 {threshold} 元。" + ) + if severity == "high": + message += " 当前未识别到例外说明,请先补充原因。" + blocking_reasons.append(f"{policy.label}金额超出制度阈值,且未补充例外说明。") + else: + message += " 已识别到例外说明,请审批人重点复核。" + flags.append( + { + "source": "submission_review", + "severity": severity, + "label": label, + "message": message, + "rule_code": policy.rule_code, + } + ) + + for scene_code, total_amount in scene_totals.items(): + policy = catalog.get_scene_policy(scene_code) + if policy is None or policy.claim_amount_limit is None or total_amount <= Decimal("0.00"): + continue + exceeded = self._evaluate_amount_limit( + amount=total_amount, + limit_config=policy.claim_amount_limit, + reason_text=reason_corpus, + ) + if exceeded is None: + continue + + severity, threshold = exceeded + label = f"{policy.label}合计超标待说明" if severity == "high" else f"{policy.label}合计超标提醒" + message = ( + f"{policy.label}当前合计金额为 {total_amount} 元," + f"已超过制度阈值 {threshold} 元。" + ) + if severity == "high": + message += " 当前未识别到例外说明,请先补充原因。" + blocking_reasons.append(f"{policy.label}合计金额超出制度阈值,且未补充例外说明。") + else: + message += " 已识别到例外说明,请审批人重点复核。" + flags.append( + { + "source": "submission_review", + "severity": severity, + "label": label, + "message": message, + "rule_code": policy.rule_code, + } + ) + + return { + "flags": flags, + "blocking_reasons": list(dict.fromkeys(reason for reason in blocking_reasons if reason)), + } + + @staticmethod + def _evaluate_amount_limit( + *, + amount: Decimal, + limit_config: Any, + reason_text: str, + ) -> tuple[str, Decimal] | None: + block_amount = getattr(limit_config, "block_amount", None) + warn_amount = getattr(limit_config, "warn_amount", None) + exception_keywords = list(getattr(limit_config, "exception_keywords", []) or []) + has_exception = ExpenseClaimService._text_contains_keywords(reason_text, exception_keywords) + + if block_amount is not None and amount > Decimal(block_amount): + return ("medium" if has_exception else "high", Decimal(block_amount)) + if warn_amount is not None and amount > Decimal(warn_amount): + return ("medium", Decimal(warn_amount)) + return None + + def _run_travel_policy_review(self, claim: ExpenseClaim) -> dict[str, list[Any]]: + policy = self._get_expense_rule_catalog().travel_policy + if policy is None: + return {"flags": [], "blocking_reasons": []} + contexts = [ + context + for context in self._build_claim_attachment_contexts(claim) + if self._is_travel_policy_relevant_context(context, policy) + ] + if not contexts: + return {"flags": [], "blocking_reasons": []} + + reason_corpus = self._build_travel_reason_corpus(claim) + has_route_exception = self._text_contains_keywords( + reason_corpus, + policy.route_exception_keywords, + ) + has_standard_exception = self._text_contains_keywords( + reason_corpus, + policy.standard_exception_keywords, + ) + grade_band = self._resolve_travel_policy_band(claim.employee_grade) + band_label = policy.band_labels.get(grade_band or "", str(claim.employee_grade or "").strip() or "当前职级") + + itinerary_segments: list[dict[str, Any]] = [] + itinerary_cities: list[str] = [] + hotel_contexts: list[dict[str, Any]] = [] + flags: list[dict[str, Any]] = [] + blocking_reasons: list[str] = [] + + for context in contexts: + route_segment = self._extract_route_segment(context, policy) + if route_segment and self._is_long_distance_travel_context(context, policy): + itinerary_segments.append( + { + "item": context["item"], + "origin": route_segment[0], + "destination": route_segment[1], + } + ) + itinerary_cities.extend([route_segment[0], route_segment[1]]) + + scene_code = str(context["document_info"].get("scene_code") or "").strip().lower() + document_type = str(context["document_info"].get("document_type") or "").strip().lower() + item_type = str(context["item"].item_type or "").strip().lower() + if "hotel" in {scene_code, document_type, item_type} or document_type == "hotel_invoice": + hotel_contexts.append(context) + + unique_itinerary_cities = list(dict.fromkeys(city for city in itinerary_cities if city)) + expected_destination_city = self._resolve_expected_travel_city( + claim, + contexts, + unique_itinerary_cities, + policy, + ) + + if itinerary_segments: + unique_destinations = list( + dict.fromkeys(segment["destination"] for segment in itinerary_segments if segment["destination"]) + ) + first_origin = str(itinerary_segments[0]["origin"] or "").strip() + last_destination = str(itinerary_segments[-1]["destination"] or "").strip() + + for previous, current in zip(itinerary_segments, itinerary_segments[1:]): + previous_destination = str(previous["destination"] or "").strip() + current_origin = str(current["origin"] or "").strip() + if previous_destination and current_origin and previous_destination != current_origin: + message = ( + f"差旅行程未形成连续链路:上一段到达 {previous_destination}," + f"下一段却从 {current_origin} 出发,请补充中转或改签说明。" + ) + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "行程闭环异常", + "message": message, + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("差旅行程未形成连续闭环,请补充中转、改签或异地出发原因。") + break + + if ( + expected_destination_city + and last_destination + and last_destination not in {expected_destination_city, first_origin} + ): + message = ( + f"差旅行程终点识别为 {last_destination}," + f"与申报目的地 {expected_destination_city} 不一致,请补充多地出差或后续行程说明。" + ) + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "行程终点异常", + "message": message, + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("差旅行程终点与申报目的地不一致,请补充多地出差说明或补齐后续票据。") + + expected_city_set = { + city + for city in (expected_destination_city, first_origin) + if city + } + extra_destinations = [ + city + for city in unique_destinations + if city and city not in expected_city_set + ] + if extra_destinations and not has_route_exception: + destinations_text = "、".join(extra_destinations[:3]) + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "多城市行程待说明", + "message": ( + f"检测到本次差旅涉及 {destinations_text} 多个目的地," + "但当前报销事由未说明中转、多地拜访或改签原因。" + ), + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("检测到多城市差旅行程,但当前未补充中转或多地出差说明。") + + allowed_hotel_cities = { + city + for city in [expected_destination_city, *unique_itinerary_cities] + if city + } + for context in hotel_contexts: + hotel_city = self._extract_hotel_city(context, policy) + if hotel_city and allowed_hotel_cities and hotel_city not in allowed_hotel_cities: + expected_text = "、".join(sorted(allowed_hotel_cities)) + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "酒店地点异常", + "message": ( + f"酒店票据识别城市为 {hotel_city}," + f"与当前差旅目的地/行程城市 {expected_text} 不一致,请补充异地住宿原因。" + ), + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("酒店票据地点与差旅目的地不一致,请补充异地住宿原因或更换附件。") + + if grade_band is None: + continue + + baseline_city = hotel_city or expected_destination_city + city_tier = policy.city_tiers.get(str(baseline_city or "").strip(), "tier_3") + cap = Decimal(policy.hotel_limits[grade_band][city_tier]) + night_count = self._extract_hotel_night_count(context) + item_amount = Decimal(context["item"].item_amount or Decimal("0.00")).quantize(Decimal("0.01")) + nightly_amount = (item_amount / Decimal(max(night_count, 1))).quantize(Decimal("0.01")) + + if nightly_amount <= cap: + continue + + city_tier_label = { + "tier_1": "一线城市", + "tier_2": "重点城市", + "tier_3": "其他城市", + }.get(city_tier, "当前城市") + hotel_message = ( + f"{band_label} 职级在{city_tier_label}的住宿标准为 {cap} 元/晚," + f"当前酒店识别金额约 {nightly_amount} 元/晚。" + ) + item_reason = str(context["item"].item_reason or "").strip() + item_has_exception = self._text_contains_keywords(item_reason, policy.standard_exception_keywords) + if has_standard_exception or item_has_exception: + flags.append( + { + "source": "submission_review", + "severity": "medium", + "label": "住宿超标提醒", + "message": hotel_message + " 已识别到补充说明,请直属领导重点复核。", + "rule_code": policy.rule_code, + } + ) + else: + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "住宿超标待说明", + "message": hotel_message + " 当前未识别到超标说明,请先补充原因。", + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("住宿金额超出当前职级差标,且未补充超标说明。") + + if grade_band is not None: + for context in contexts: + transport_class = self._detect_transport_class(context, policy) + if transport_class is None: + continue + + transport_kind, class_label, class_level = transport_class + allowed_level = policy.transport_limits.get(grade_band, {}).get(transport_kind) + if allowed_level is None or class_level <= allowed_level: + continue + + item_reason = str(context["item"].item_reason or "").strip() + item_has_exception = self._text_contains_keywords(item_reason, policy.standard_exception_keywords) + message = f"{band_label} 职级当前默认不可报销 {class_label}。" + if has_standard_exception or item_has_exception: + flags.append( + { + "source": "submission_review", + "severity": "medium", + "label": "交通舱位超标提醒", + "message": message + " 已识别到补充说明,请审批人重点复核。", + "rule_code": policy.rule_code, + } + ) + else: + flags.append( + { + "source": "submission_review", + "severity": "high", + "label": "交通舱位超标待说明", + "message": message + " 当前未识别到例外说明,请先补充原因。", + "rule_code": policy.rule_code, + } + ) + blocking_reasons.append("交通舱位或席别超出当前职级差标,且未补充例外说明。") + + return { + "flags": flags, + "blocking_reasons": list(dict.fromkeys(reason for reason in blocking_reasons if reason)), + } + + def _build_claim_attachment_contexts(self, claim: ExpenseClaim) -> list[dict[str, Any]]: + contexts: list[dict[str, Any]] = [] + ordered_items = sorted( + claim.items, + key=lambda item: ( + item.item_date or date.max, + self._normalize_sort_datetime(item.created_at), + ), + ) + for index, item in enumerate(ordered_items, start=1): + file_path = self._resolve_attachment_path(item.invoice_id) + if file_path is None or not file_path.exists(): + continue + + metadata = self._read_attachment_meta(file_path) + document_info = metadata.get("document_info") + contexts.append( + { + "index": index, + "item": item, + "document_info": document_info if isinstance(document_info, dict) else {}, + "ocr_text": str(metadata.get("ocr_text") or ""), + "ocr_summary": str(metadata.get("ocr_summary") or ""), + } + ) + return contexts + + def _is_travel_policy_relevant_context( + self, + context: dict[str, Any], + policy: RuntimeTravelPolicy, + ) -> bool: + item = context.get("item") + document_info = context.get("document_info") or {} + item_type = str(getattr(item, "item_type", "") or "").strip().lower() + scene_code = str(document_info.get("scene_code") or "").strip().lower() + document_type = str(document_info.get("document_type") or "").strip().lower() + return ( + item_type in set(policy.relevant_expense_types) + or scene_code in set(policy.relevant_expense_types) + or document_type in {"hotel_invoice", *set(policy.long_distance_document_types)} + ) + + @staticmethod + def _resolve_document_field_value(document_info: dict[str, Any], key: str) -> str: + normalized_key = str(key or "").strip().lower() + for field in list(document_info.get("fields") or []): + if not isinstance(field, dict): + continue + field_key = str(field.get("key") or "").strip().lower() + if field_key == normalized_key: + return str(field.get("value") or "").strip() + return "" + + @staticmethod + def _text_contains_keywords(text: str, keywords: tuple[str, ...] | list[str]) -> bool: + compact = re.sub(r"\s+", "", str(text or "")) + if not compact: + return False + return any(keyword in compact for keyword in keywords) + + def _build_travel_reason_corpus(self, claim: ExpenseClaim) -> str: + parts = [str(claim.reason or "").strip(), str(claim.location or "").strip()] + for item in claim.items: + parts.append(str(item.item_reason or "").strip()) + parts.append(str(item.item_location or "").strip()) + return "\n".join(part for part in parts if part) + + @staticmethod + def _resolve_travel_policy_band(grade: str | None) -> str | None: + normalized = str(grade or "").strip().upper() + if not normalized: + return None + + p_match = re.search(r"P(\d+)", normalized) + if p_match: + level = int(p_match.group(1)) + if level <= 3: + return "junior" + if level <= 5: + return "mid" + return "senior" + + m_match = re.search(r"M(\d+)", normalized) + if m_match: + level = int(m_match.group(1)) + if level <= 2: + return "manager" + return "executive" + + if normalized.startswith("D"): + return "executive" + return None + + def _resolve_expected_travel_city( + self, + claim: ExpenseClaim, + contexts: list[dict[str, Any]], + itinerary_cities: list[str], + policy: RuntimeTravelPolicy, + ) -> str: + claim_city = self._extract_city_from_text(str(claim.location or ""), policy) + if claim_city: + return claim_city + + for context in contexts: + hotel_city = self._extract_hotel_city(context, policy) + if hotel_city: + return hotel_city + + if len(itinerary_cities) >= 2 and itinerary_cities[1]: + return itinerary_cities[1] + for city in itinerary_cities: + if city: + return city + return "" + + def _extract_route_segment( + self, + context: dict[str, Any], + policy: RuntimeTravelPolicy, + ) -> tuple[str, str] | None: + document_info = context["document_info"] + route_value = self._resolve_document_field_value(document_info, "route") + if not route_value or "-" not in route_value: + return None + + origin_text, destination_text = [segment.strip() for segment in route_value.split("-", 1)] + origin_city = self._extract_city_from_text(origin_text, policy) + destination_city = self._extract_city_from_text(destination_text, policy) + if not origin_city or not destination_city or origin_city == destination_city: + return None + return origin_city, destination_city + + def _extract_hotel_city(self, context: dict[str, Any], policy: RuntimeTravelPolicy) -> str: + document_info = context["document_info"] + item = context["item"] + merchant_name = self._resolve_document_field_value(document_info, "merchant_name") + for candidate in ( + merchant_name, + str(item.item_location or ""), + str(context.get("ocr_summary") or ""), + str(context.get("ocr_text") or ""), + ): + city = self._extract_city_from_text(candidate, policy) + if city: + return city + return "" + + @staticmethod + def _extract_city_from_text(text: str, policy: RuntimeTravelPolicy) -> str: + normalized = str(text or "").strip() + if not normalized: + return "" + city_match_order = sorted(policy.city_tiers.keys(), key=lambda item: len(item), reverse=True) + for city in city_match_order: + if city in normalized: + return city + return "" + + @staticmethod + def _extract_hotel_night_count(context: dict[str, Any]) -> int: + text = " ".join( + [ + str(context.get("ocr_summary") or "").strip(), + str(context.get("ocr_text") or "").strip(), + ] + ).strip() + match = TRAVEL_POLICY_HOTEL_NIGHT_PATTERN.search(text) + if not match: + return 1 + try: + return max(1, int(match.group(1))) + except (TypeError, ValueError): + return 1 + + def _detect_transport_class( + self, + context: dict[str, Any], + policy: RuntimeTravelPolicy, + ) -> tuple[str, str, int] | None: + document_info = context["document_info"] + document_type = str(document_info.get("document_type") or "").strip().lower() + text = " ".join( + [ + str(context.get("ocr_summary") or "").strip(), + str(context.get("ocr_text") or "").strip(), + ] + ).strip() + compact_text = re.sub(r"\s+", "", text) + if not compact_text: + return None + + if document_type == "flight_itinerary": + for config in policy.flight_classes: + label = str(config.keyword or "").strip() + level = int(config.level) + if label in compact_text: + return "flight", label, level + return None + + if document_type == "train_ticket": + for config in policy.train_classes: + label = str(config.keyword or "").strip() + level = int(config.level) + if label in compact_text: + return "train", label, level + return None + + return None + + def _is_long_distance_travel_context( + self, + context: dict[str, Any], + policy: RuntimeTravelPolicy, + ) -> bool: + document_info = context["document_info"] + document_type = str(document_info.get("document_type") or "").strip().lower() + scene_code = str(document_info.get("scene_code") or "").strip().lower() + if document_type in set(policy.long_distance_document_types): + return True + return scene_code == "travel" + + def _sync_claim_from_items(self, claim: ExpenseClaim) -> None: + if not claim.items: + claim.amount = Decimal("0.00") + claim.invoice_count = 0 + claim.risk_flags_json = self._merge_claim_attachment_risk_flags(claim, []) + return + + ordered_items = sorted( + claim.items, + key=lambda item: ( + item.item_date or date.max, + self._normalize_sort_datetime(item.created_at), + ), + ) + primary_item = ordered_items[0] + total_amount = sum((item.item_amount for item in ordered_items), Decimal("0.00")) + + claim.amount = total_amount.quantize(Decimal("0.01")) + claim.invoice_count = sum(1 for item in ordered_items if str(item.invoice_id or "").strip()) + claim.occurred_at = datetime( + primary_item.item_date.year, + primary_item.item_date.month, + primary_item.item_date.day, + tzinfo=UTC, + ) + claim.expense_type = str(primary_item.item_type or claim.expense_type or "other").strip() or "other" + claim.reason = ( + self._normalize_optional_text(primary_item.item_reason, fallback=claim.reason or "待补充") or "待补充" + ) + claim.location = ( + self._normalize_optional_text(primary_item.item_location, fallback=claim.location or "待补充") + or "待补充" + ) + claim.risk_flags_json = self._merge_claim_attachment_risk_flags( + claim, + self._build_claim_attachment_risk_flags(ordered_items), + ) + if str(claim.status or "").strip().lower() == "draft": + claim.approval_stage = "待提交" + + def _refresh_item_attachment_analysis(self, item: ExpenseClaimItem) -> None: + file_path = self._resolve_attachment_path(item.invoice_id) + if file_path is None or not file_path.exists(): + return + + metadata = self._read_attachment_meta(file_path) + media_type = str(metadata.get("media_type") or self._resolve_attachment_media_type(file_path.name)).strip() + ocr_status = str(metadata.get("ocr_status") or "").strip().lower() + + if ocr_status == "failed": + analysis = self._build_failed_ocr_attachment_analysis( + media_type=media_type, + error_message=str(metadata.get("ocr_error") or ""), + item=item, + ) + elif ocr_status == "recognized" or any( + ( + str(metadata.get("ocr_text") or "").strip(), + str(metadata.get("ocr_summary") or "").strip(), + int(metadata.get("ocr_line_count") or 0), + list(metadata.get("ocr_warnings") or []), + ) + ): + stored_document_info = metadata.get("document_info") + if not isinstance(stored_document_info, dict): + stored_document_info = {} + document = SimpleNamespace( + filename=str(metadata.get("file_name") or file_path.name), + text=str(metadata.get("ocr_text") or ""), + summary=str(metadata.get("ocr_summary") or ""), + avg_score=float(metadata.get("ocr_avg_score") or 0.0), + line_count=int(metadata.get("ocr_line_count") or 0), + document_type=str(stored_document_info.get("document_type") or ""), + document_type_label=str(stored_document_info.get("document_type_label") or ""), + scene_code=str(stored_document_info.get("scene_code") or ""), + scene_label=str(stored_document_info.get("scene_label") or ""), + document_fields=list(stored_document_info.get("fields") or []), + warnings=[str(value) for value in list(metadata.get("ocr_warnings") or []) if str(value).strip()], + ) + document_info = self._build_attachment_document_info(document) + requirement_check = self._build_attachment_requirement_check( + item=item, + document_info=document_info, + ) + analysis = self._build_attachment_analysis( + document=document, + item=item, + document_info=document_info, + requirement_check=requirement_check, + ) + metadata["document_info"] = document_info + metadata["requirement_check"] = requirement_check + else: + analysis = self._build_fallback_attachment_analysis(media_type=media_type, item=item) + + metadata["analysis"] = analysis + self._write_attachment_meta(file_path, metadata) + + def _build_claim_attachment_risk_flags(self, ordered_items: list[ExpenseClaimItem]) -> list[dict[str, Any]]: + derived_flags: list[dict[str, Any]] = [] + for index, item in enumerate(ordered_items, start=1): + file_path = self._resolve_attachment_path(item.invoice_id) + if file_path is None or not file_path.exists(): + continue + + metadata = self._read_attachment_meta(file_path) + analysis = metadata.get("analysis") + if not isinstance(analysis, dict): + continue + + severity = str(analysis.get("severity") or "").strip().lower() + if severity in {"", "pass", "low"}: + continue + + summary = str(analysis.get("summary") or analysis.get("headline") or "").strip() or "附件存在待核对风险。" + label = str(analysis.get("label") or ("高风险" if severity == "high" else "中风险")).strip() + derived_flags.append( + { + "source": "attachment_analysis", + "item_id": item.id, + "severity": severity, + "label": label, + "message": f"费用明细第 {index} 条:{summary}", + } + ) + return derived_flags + + def _get_expense_rule_catalog(self) -> Any: + cached = getattr(self, "_expense_rule_catalog", None) + if cached is not None: + return cached + + db = getattr(self, "db", None) + if db is None: + catalog = build_default_expense_rule_catalog() + else: + catalog = ExpenseRuleRuntimeService(db).load_catalog() + setattr(self, "_expense_rule_catalog", catalog) + return catalog + + def _get_expense_scene_policy(self, expense_type: str | None) -> Any | None: + return self._get_expense_rule_catalog().get_scene_policy(expense_type) + + def _resolve_min_attachment_count(self, expense_type: str | None) -> int: + policy = self._get_expense_scene_policy(expense_type) + if policy is None: + return 1 + return max(0, int(policy.min_attachment_count or 0)) + + def _build_scene_reason_corpus(self, claim: ExpenseClaim) -> str: + parts = [str(claim.reason or "").strip(), str(claim.location or "").strip()] + for item in claim.items: + parts.append(str(item.item_reason or "").strip()) + parts.append(str(item.item_location or "").strip()) + return "\n".join(part for part in parts if part) + + @staticmethod + def _merge_claim_attachment_risk_flags( + claim: ExpenseClaim, + attachment_risk_flags: list[dict[str, Any]], + ) -> list[Any]: + preserved_flags = [ + flag + for flag in list(claim.risk_flags_json or []) + if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis") + ] + return preserved_flags + attachment_risk_flags + + def _validate_claim_for_submission(self, claim: ExpenseClaim) -> list[str]: + issues: list[str] = [] + claim_location_required = self._is_location_required_expense_type(claim.expense_type) + claim_min_attachment_count = self._resolve_min_attachment_count(claim.expense_type) + + if self._is_missing_value(claim.employee_name): + issues.append("申请人未完善") + if self._is_missing_value(claim.department_name): + issues.append("所属部门未完善") + if self._is_missing_value(claim.expense_type): + issues.append("报销类型未完善") + if self._is_missing_value(claim.reason): + issues.append("报销事由未完善") + if claim_location_required and self._is_missing_value(claim.location): + issues.append("业务地点未完善") + if claim.amount is None or claim.amount <= Decimal("0.00"): + issues.append("报销金额未完善") + if claim.occurred_at is None: + issues.append("发生时间未完善") + if int(claim.invoice_count or 0) < claim_min_attachment_count: + issues.append("票据附件数量不足") + if not claim.items: + issues.append("费用明细不能为空") + + for index, item in enumerate(claim.items, start=1): + prefix = f"费用明细第 {index} 条" + item_location_required = self._is_location_required_expense_type(item.item_type or claim.expense_type) + if item.item_date is None: + issues.append(f"{prefix}缺少日期") + if self._is_missing_value(item.item_type): + issues.append(f"{prefix}缺少费用项目") + if self._is_missing_value(item.item_reason): + issues.append(f"{prefix}缺少说明") + if item_location_required and self._is_missing_value(item.item_location): + issues.append(f"{prefix}缺少地点") + if item.item_amount is None or item.item_amount <= Decimal("0.00"): + issues.append(f"{prefix}缺少金额") + if self._is_missing_value(item.invoice_id): + issues.append(f"{prefix}缺少票据标识") + + return issues + + def _is_location_required_expense_type(self, expense_type: str | None) -> bool: + policy = self._get_expense_scene_policy(expense_type) + if policy is None: + return str(expense_type or "").strip().lower() in LOCATION_REQUIRED_EXPENSE_TYPES + return bool(policy.location_required) + + @staticmethod + def _has_privileged_claim_access(current_user: CurrentUserContext) -> bool: + if current_user.is_admin: + return True + role_codes = { + str(item).strip().lower() + for item in current_user.role_codes + if str(item).strip() + } + return bool(role_codes & PRIVILEGED_CLAIM_ROLE_CODES) + + @staticmethod + def _normalize_role_codes(current_user: CurrentUserContext) -> set[str]: + return { + str(item).strip().lower() + for item in current_user.role_codes + if str(item).strip() + } + + def _resolve_current_employee(self, current_user: CurrentUserContext) -> Employee | None: + username = str(current_user.username or "").strip() + if not username: + return None + return self.db.scalar( + select(Employee) + .where(func.lower(Employee.email) == username.lower()) + .limit(1) + ) + + def _employee_name_is_unique(self, employee: Employee) -> bool: + normalized_name = str(employee.name or "").strip() + if not normalized_name: + return False + + same_name_count = int( + self.db.scalar( + select(func.count()).select_from(Employee).where(Employee.name == normalized_name) + ) + or 0 + ) + return same_name_count == 1 + + def _apply_claim_scope(self, stmt: Any, current_user: CurrentUserContext) -> Any: + if self._has_privileged_claim_access(current_user): + return stmt + + conditions = [] + username = str(current_user.username or "").strip() + employee = self._resolve_current_employee(current_user) + + def add_condition(field_name: str, value: str | None) -> None: + normalized = str(value or "").strip() + if not normalized: + return + if field_name == "employee_id": + conditions.append(ExpenseClaim.employee_id == normalized) + return + conditions.append(ExpenseClaim.employee_name == normalized) + + if employee is not None: + add_condition("employee_id", employee.id) + add_condition("employee_name", employee.email) + if self._employee_name_is_unique(employee): + add_condition("employee_name", employee.name) + else: + add_condition("employee_id", username) + add_condition("employee_name", username) + + if not conditions: + return stmt.where(ExpenseClaim.id == "__no_visible_claim__") + + role_codes = self._normalize_role_codes(current_user) + if role_codes & APPROVAL_VISIBLE_CLAIM_ROLE_CODES: + pending_leader_approval = and_( + ExpenseClaim.status == "submitted", + ExpenseClaim.approval_stage == "直属领导审批", + ) + if employee is not None: + subordinate_ids = select(Employee.id).where(Employee.manager_id == employee.id) + conditions.append(and_(pending_leader_approval, ExpenseClaim.employee_id.in_(subordinate_ids))) + manager_name = str( + employee.name if employee is not None and employee.name else current_user.name or "" + ).strip() + if manager_name: + managed_department_ids = select(OrganizationUnit.id).where(OrganizationUnit.manager_name == manager_name) + managed_department_names = select(OrganizationUnit.name).where(OrganizationUnit.manager_name == manager_name) + conditions.append(and_(pending_leader_approval, ExpenseClaim.department_id.in_(managed_department_ids))) + conditions.append(and_(pending_leader_approval, ExpenseClaim.department_name.in_(managed_department_names))) + + return stmt.where(or_(*conditions)) + + def _ensure_ready(self) -> None: + AgentFoundationService(self.db).ensure_foundation_ready() diff --git a/server/src/app/services/ontology.py b/server/src/app/services/ontology.py index 6af4748..81d2211 100644 --- a/server/src/app/services/ontology.py +++ b/server/src/app/services/ontology.py @@ -1,1770 +1,1770 @@ -from __future__ import annotations - -import calendar -import json -import re -from dataclasses import dataclass -from datetime import UTC, date, datetime, timedelta -from typing import Any - -from pydantic import BaseModel, ConfigDict, Field, ValidationError -from sqlalchemy import select -from sqlalchemy.orm import Session - -from app.core.agent_enums import ( - AgentName, - AgentPermissionLevel, - AgentRunSource, - AgentRunStatus, -) -from app.core.logging import get_logger -from app.models.employee import Employee -from app.models.financial_record import ( - AccountsPayableRecord, - AccountsReceivableRecord, - ExpenseClaim, -) -from app.models.organization import OrganizationUnit -from app.schemas.ontology import ( - OntologyConstraint, - OntologyEntity, - OntologyFieldError, - OntologyIntent, - OntologyMetric, - OntologyParseRequest, - OntologyParseResult, - OntologyPermission, - OntologyScenario, - OntologyTimeRange, -) -from app.services.agent_foundation import AgentFoundationService -from app.services.agent_runs import AgentRunService -from app.services.runtime_chat import RuntimeChatService - -logger = get_logger("app.services.ontology") - -DATE_RANGE_PATTERN = re.compile( - r"(?P\d{4}-\d{1,2}-\d{1,2})\s*(?:到|至|~|-)\s*(?P\d{4}-\d{1,2}-\d{1,2})" -) -EXPLICIT_MONTH_PATTERN = re.compile(r"(?P\d{4})年(?P\d{1,2})月") -EXPLICIT_DATE_PATTERN = re.compile( - r"(?P\d{4})[年/-](?P\d{1,2})[月/-](?P\d{1,2})日?" -) -MONTH_DAY_RANGE_PATTERN = re.compile( - r"(?P\d{1,2})月(?P\d{1,2})日?\s*(?:到|至|~|-)\s*" - r"(?P\d{1,2})月(?P\d{1,2})日?" -) -MONTH_DAY_PATTERN = re.compile(r"(?P\d{1,2})月(?P\d{1,2})日?") -AMOUNT_PATTERN = re.compile( - r"(?P超过|大于|高于|不少于|不低于|小于|低于|少于|至多|不超过|<=|>=|<|>|=|=)?\s*" - r"(?P\d+(?:\.\d+)?)\s*(?P万元|万|元)?" -) -TOP_N_PATTERN = re.compile(r"(?:top|TOP|前|最高的?|最低的?)\s*(?P\d+)") - -SCENARIO_KEYWORDS = { - "expense": ( - ("报销", 0.20), - ("报账", 0.20), - ("差旅", 0.20), - ("费用", 0.14), - ("发票", 0.14), - ("票据", 0.12), - ("借款", 0.12), - ("住宿", 0.10), - ("餐费", 0.10), - ("招待", 0.18), - ("招待费", 0.18), - ("花销", 0.16), - ("花了", 0.14), - ("支出", 0.14), - ("垫付", 0.14), - ), - "accounts_receivable": ( - ("应收", 0.22), - ("回款", 0.20), - ("收款", 0.18), - ("账龄", 0.18), - ("客户欠款", 0.22), - ), - "accounts_payable": ( - ("应付", 0.22), - ("付款", 0.20), - ("请款", 0.18), - ("供应商", 0.20), - ("待付", 0.16), - ("打款", 0.18), - ), - "knowledge": ( - ("制度", 0.20), - ("规则", 0.20), - ("办法", 0.18), - ("依据", 0.18), - ("政策", 0.16), - ("知识库", 0.18), - ), -} - -QUERY_KEYWORDS = ( - "查", - "查询", - "查看", - "列出", - "统计", - "汇总", - "多少", - "几笔", - "金额", - "明细", -) -EXPLAIN_KEYWORDS = ("为什么", "依据", "原因", "怎么处理", "是否可以", "能不能", "按什么规则") -COMPARE_KEYWORDS = ("对比", "比较", "相比", "差异", "变化") -RISK_KEYWORDS = ("风险", "异常", "重复", "超标", "超预算", "逾期", "验真", "巡检") -DRAFT_KEYWORDS = ("生成", "草稿", "起草", "拟一份", "创建", "发起", "准备") -DRAFT_FOLLOW_UP_KEYWORDS = ( - "继续", - "补充", - "补一下", - "修改", - "改成", - "改为", - "换成", - "更新", - "确认", - "提交", - "保存", - "客户是", - "地点是", - "金额是", - "日期是", - "时间是", -) -OPERATE_KEYWORDS = ( - "直接付款", - "帮我付款", - "安排付款", - "发起付款", - "直接审批", - "审批通过", - "帮我审批", - "驳回", - "上线", - "激活", - "停用", - "删除", -) - -EXPENSE_TYPE_KEYWORDS = { - "差旅": "travel", - "出差": "travel", - "住宿": "hotel", - "酒店": "hotel", - "交通": "transport", - "打车": "transport", - "网约车": "transport", - "出租车": "transport", - "停车费": "transport", - "餐费": "meal", - "用餐": "meal", - "会务": "meeting", - "招待费": "entertainment", - "招待": "entertainment", - "宴请": "entertainment", - "办公费": "office", - "办公用品": "office", - "文具": "office", - "耗材": "office", - "办公耗材": "office", - "打印纸": "office", - "办公设备": "office", - "培训费": "training", - "培训": "training", - "通讯费": "communication", - "话费": "communication", - "福利费": "welfare", - "团建": "welfare", -} - -EXPENSE_NARRATIVE_KEYWORDS = ( - "报销", - "报账", - "招待", - "招待费", - "花销", - "花了", - "支出", - "垫付", - "打车", - "车费", - "餐费", - "吃饭", - "用餐", - "宴请", - "请客", - "住宿", - "发票", - "票据", - "差旅", - "客户现场", -) - -AR_CORE_KEYWORDS = ("应收", "回款", "收款", "账龄", "欠款", "未回款") -AP_CORE_KEYWORDS = ("应付", "付款", "请款", "待付", "打款", "未付款") -GENERIC_EXPENSE_PROMPTS = { - "报销", - "我要报销", - "我想报销", - "帮我报销", - "我要申请报销", - "发起报销", - "提交报销", -} -MISSING_SLOT_LABELS = { - "expense_type": "费用类型", - "amount": "金额", - "customer_name": "客户单位", - "vendor_name": "供应商", - "participants": "参与人员", - "attachments": "票据附件", - "time_range": "发生时间", - "reason": "事由说明", - "document_id": "单据号", -} - -STATUS_KEYWORDS = { - "逾期": "overdue", - "待审批": "pending", - "待审": "pending", - "已审批": "approved", - "已通过": "approved", - "已付款": "paid", - "未付款": "unpaid", - "未回款": "unreceived", -} - -PRIVILEGED_ROLE_CODES = {"manager", "finance", "approver", "executive"} -CONTEXTUAL_SCENARIOS = {"expense", "accounts_receivable", "accounts_payable", "knowledge"} -KNOWLEDGE_INTENTS = {"query", "explain", "compare"} - - -@dataclass(slots=True) -class ReferenceCatalog: - employees: list[str] - departments: list[str] - customers: list[str] - vendors: list[str] - projects: list[str] - - -class LlmOntologyEntityHint(BaseModel): - model_config = ConfigDict(extra="ignore") - - type: str - value: str - normalized_value: str | None = None - role: str = "target" - confidence: float = Field(default=0.72, ge=0.0, le=1.0) - - -class LlmOntologyParseResult(BaseModel): - model_config = ConfigDict(extra="ignore") - - scenario: OntologyScenario = Field(default="unknown") - intent: OntologyIntent = Field(default="query") - confidence: float = Field(default=0.0, ge=0.0, le=1.0) - clarification_required: bool = False - clarification_question: str | None = None - missing_slots: list[str] = Field(default_factory=list) - ambiguity: list[str] = Field(default_factory=list) - entity_hints: list[LlmOntologyEntityHint] = Field(default_factory=list) - - -class SemanticOntologyService: - def __init__(self, db: Session) -> None: - self.db = db - self.run_service = AgentRunService(db) - self.runtime_chat_service = RuntimeChatService(db) - - def parse(self, payload: OntologyParseRequest) -> OntologyParseResult: - analyzed = self._analyze(payload) - run = self.run_service.create_run( - agent=AgentName.ORCHESTRATOR.value, - source=AgentRunSource.USER_MESSAGE.value, - user_id=payload.user_id, - ontology_json=self._build_ontology_json(analyzed), - route_json={ - "stage": "semantic_parse", - "clarification_required": analyzed["clarification_required"], - "field_error_count": len(analyzed["field_errors"]), - }, - permission_level=analyzed["permission"].level, - status=( - AgentRunStatus.BLOCKED.value - if analyzed["clarification_required"] - or analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value - else AgentRunStatus.SUCCEEDED.value - ), - result_summary=self._build_result_summary( - analyzed["scenario"], - analyzed["intent"], - analyzed["permission"].level, - analyzed["confidence"], - ), - error_message=( - analyzed["permission"].reason - if analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value - else None - ), - ) - self._record_semantic_parse( - run_id=run.run_id, - payload=payload, - analyzed=analyzed, - ) - return self._build_result(analyzed, run.run_id) - - def parse_for_run(self, payload: OntologyParseRequest, *, run_id: str) -> OntologyParseResult: - analyzed = self._analyze(payload) - self._record_semantic_parse(run_id=run_id, payload=payload, analyzed=analyzed) - return self._build_result(analyzed, run_id) - - def _analyze(self, payload: OntologyParseRequest) -> dict[str, object]: - query = payload.query.strip() - if not query: - raise ValueError("query 不能为空。") - - AgentFoundationService(self.db).ensure_foundation_ready() - context_json = payload.context_json or {} - reference = self._load_reference_catalog() - compact_query = self._compact(query) - entities = self._extract_entities(query, compact_query, reference) - rule_scenario, scenario_score = self._detect_scenario(compact_query) - time_range, _time_score = self._extract_time_range( - query, - compact_query, - context_json=context_json, - ) - session_scenario = self._resolve_session_type_scenario(context_json) - context_scenario = self._resolve_context_scenario(context_json) - if session_scenario == "knowledge": - rule_scenario = "knowledge" - scenario_score = max(scenario_score, 0.34) - if rule_scenario == "unknown" and context_scenario is not None: - rule_scenario = context_scenario - scenario_score = max(scenario_score, 0.14) - if rule_scenario == "unknown": - inferred_scenario = self._infer_scenario_from_entities(entities) - if inferred_scenario is not None: - rule_scenario = inferred_scenario - scenario_score = 0.18 - - if session_scenario != "knowledge" and self._looks_like_expense_narrative( - compact_query, - scenario=rule_scenario, - entities=entities, - time_range=time_range, - ): - rule_scenario = "expense" - scenario_score = max(scenario_score, 0.24) - - rule_intent, intent_score = self._detect_intent( - compact_query, - scenario=rule_scenario, - entities=entities, - time_range=time_range, - ) - if session_scenario != "knowledge" and self._should_inherit_expense_draft( - compact_query, - scenario=rule_scenario, - entities=entities, - time_range=time_range, - context_json=context_json, - ): - rule_scenario = "expense" - rule_intent = "draft" - scenario_score = max(scenario_score, 0.18) - intent_score = max(intent_score, 0.18) - metrics = self._extract_metrics(compact_query) - constraints = self._extract_constraints(compact_query, entities) - model_parse = None - if session_scenario != "knowledge": - model_parse = self._parse_with_model( - payload=payload, - query=query, - compact_query=compact_query, - fallback_scenario=rule_scenario, - fallback_intent=rule_intent, - entities=entities, - time_range=time_range, - metrics=metrics, - constraints=constraints, - ) - scenario = self._resolve_scenario(rule_scenario, model_parse) - if session_scenario == "knowledge": - scenario = "knowledge" - entities = self._merge_entities( - entities, - model_parse.entity_hints if model_parse is not None else [], - ) - intent = self._resolve_intent( - compact_query, - fallback_intent=rule_intent, - scenario=scenario, - entities=entities, - time_range=time_range, - model_parse=model_parse, - ) - missing_slots = self._normalize_short_text_list( - model_parse.missing_slots if model_parse is not None else [] - ) - missing_slots = self._normalize_short_text_list( - missing_slots - + self._infer_default_missing_slots( - compact_query, - scenario=scenario, - intent=intent, - entities=entities, - time_range=time_range, - context_json=context_json, - ) - ) - relax_knowledge_follow_up = self._should_relax_knowledge_follow_up_clarification( - compact_query=compact_query, - scenario=scenario, - context_json=context_json, - missing_slots=missing_slots, - ) - if relax_knowledge_follow_up: - missing_slots = [item for item in missing_slots if item != "expense_type"] - ambiguity = self._normalize_short_text_list( - model_parse.ambiguity if model_parse is not None else [] - ) - risk_flags = self._extract_risk_flags(compact_query, scenario) - permission = self._resolve_permission( - compact_query, - context_json, - intent, - ) - - field_errors = self._build_field_errors( - scenario=scenario, - intent=intent, - entities=entities, - permission=permission, - missing_slots=missing_slots, - ambiguity=ambiguity, - ) - clarification_required, clarification_question = self._build_clarification( - scenario=scenario, - intent=intent, - entities=entities, - permission=permission, - missing_slots=missing_slots, - ambiguity=ambiguity, - allow_incomplete_draft=self._allow_incomplete_draft( - context_json, - scenario=scenario, - intent=intent, - ), - model_clarification_required=bool( - model_parse is not None - and model_parse.clarification_required - ), - model_clarification_question=( - model_parse.clarification_question if model_parse is not None else None - ), - ) - if relax_knowledge_follow_up: - clarification_required = False - clarification_question = None - fallback_confidence = self._compute_confidence( - scenario=scenario, - scenario_score=scenario_score, - intent_score=intent_score, - entities=entities, - time_range=time_range, - metrics=metrics, - constraints=constraints, - risk_flags=risk_flags, - clarification_required=clarification_required, - permission=permission, - ) - confidence = self._resolve_confidence( - model_confidence=( - model_parse.confidence - if model_parse is not None - else None - ), - fallback_confidence=fallback_confidence, - clarification_required=clarification_required, - permission=permission, - ) - return { - "scenario": scenario, - "intent": intent, - "entities": entities, - "time_range": time_range, - "metrics": metrics, - "constraints": constraints, - "risk_flags": risk_flags, - "permission": permission, - "confidence": confidence, - "missing_slots": missing_slots, - "ambiguity": ambiguity, - "parse_strategy": "llm_primary" if model_parse is not None else "rule_fallback", - "clarification_required": clarification_required, - "clarification_question": clarification_question, - "field_errors": field_errors, - } - - @staticmethod - def _should_relax_knowledge_follow_up_clarification( - *, - compact_query: str, - scenario: str, - context_json: dict[str, Any], - missing_slots: list[str], - ) -> bool: - if scenario != "knowledge" or "expense_type" not in missing_slots: - return False - history = context_json.get("conversation_history") - if not isinstance(history, list): - return False - has_previous_user_turn = any( - isinstance(item, dict) - and str(item.get("role") or "").strip() == "user" - and str(item.get("content") or "").strip() - for item in history - ) - if not has_previous_user_turn: - return False - follow_up_markers = ("那", "那么", "这个", "这种", "呢", "的话", "p", "P") - return any(marker in compact_query for marker in follow_up_markers) - - def _record_semantic_parse( - self, - *, - run_id: str, - payload: OntologyParseRequest, - analyzed: dict[str, object], - ) -> None: - self.run_service.record_semantic_parse( - run_id=run_id, - user_id=payload.user_id, - raw_query=payload.query.strip(), - scenario=str(analyzed["scenario"]), - intent=str(analyzed["intent"]), - entities_json=[item.model_dump() for item in analyzed["entities"]], - time_range_json=analyzed["time_range"].model_dump(), - metrics_json=[item.model_dump() for item in analyzed["metrics"]], - constraints_json=[item.model_dump() for item in analyzed["constraints"]], - risk_flags_json=list(analyzed["risk_flags"]), - permission_json=analyzed["permission"].model_dump(), - confidence=float(analyzed["confidence"]), - ) - logger.info( - "Parsed ontology run_id=%s scenario=%s intent=%s permission=%s", - run_id, - analyzed["scenario"], - analyzed["intent"], - analyzed["permission"].level, - ) - - @staticmethod - def _build_ontology_json(analyzed: dict[str, object]) -> dict[str, object]: - return { - "scenario": analyzed["scenario"], - "intent": analyzed["intent"], - "entities": [item.model_dump() for item in analyzed["entities"]], - "time_range": analyzed["time_range"].model_dump(), - "metrics": [item.model_dump() for item in analyzed["metrics"]], - "constraints": [item.model_dump() for item in analyzed["constraints"]], - "risk_flags": list(analyzed["risk_flags"]), - "permission": analyzed["permission"].model_dump(), - "missing_slots": list(analyzed["missing_slots"]), - "ambiguity": list(analyzed["ambiguity"]), - "parse_strategy": analyzed["parse_strategy"], - "confidence": analyzed["confidence"], - } - - @staticmethod - def _build_result(analyzed: dict[str, object], run_id: str) -> OntologyParseResult: - return OntologyParseResult( - scenario=analyzed["scenario"], - intent=analyzed["intent"], - entities=analyzed["entities"], - time_range=analyzed["time_range"], - metrics=analyzed["metrics"], - constraints=analyzed["constraints"], - risk_flags=analyzed["risk_flags"], - permission=analyzed["permission"], - confidence=analyzed["confidence"], - missing_slots=analyzed["missing_slots"], - ambiguity=analyzed["ambiguity"], - parse_strategy=analyzed["parse_strategy"], - clarification_required=analyzed["clarification_required"], - clarification_question=analyzed["clarification_question"], - run_id=run_id, - field_errors=analyzed["field_errors"], - ) - - def _load_reference_catalog(self) -> ReferenceCatalog: - employees = self._read_distinct_values(select(Employee.name)) - departments = self._read_distinct_values(select(OrganizationUnit.name)) - departments += self._read_distinct_values(select(ExpenseClaim.department_name)) - customers = self._read_distinct_values(select(AccountsReceivableRecord.customer_name)) - vendors = self._read_distinct_values(select(AccountsPayableRecord.vendor_name)) - projects = self._read_distinct_values(select(ExpenseClaim.project_code)) - - return ReferenceCatalog( - employees=self._dedupe_and_sort(employees), - departments=self._dedupe_and_sort(departments), - customers=self._dedupe_and_sort(customers), - vendors=self._dedupe_and_sort(vendors), - projects=self._dedupe_and_sort(projects), - ) - - def _read_distinct_values(self, stmt) -> list[str]: - values = self.db.scalars(stmt.distinct()).all() - return [str(item).strip() for item in values if item] - - @staticmethod - def _dedupe_and_sort(values: list[str]) -> list[str]: - items = {str(item).strip() for item in values if str(item).strip()} - return sorted(items, key=lambda item: (-len(item), item)) - - @staticmethod - def _compact(text: str) -> str: - return re.sub(r"\s+", "", text).lower() - - @staticmethod - def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None: - value = str(context_json.get("conversation_scenario") or "").strip() - if value in CONTEXTUAL_SCENARIOS: - return value - return None - - @staticmethod - def _resolve_session_type_scenario(context_json: dict[str, Any]) -> str | None: - value = str(context_json.get("session_type") or "").strip() - if value == "knowledge": - return "knowledge" - return None - - - def _detect_scenario(self, compact_query: str) -> tuple[str, float]: - scores = {key: 0.0 for key in SCENARIO_KEYWORDS} - for scenario, keywords in SCENARIO_KEYWORDS.items(): - for keyword, weight in keywords: - if keyword in compact_query: - scores[scenario] += weight - - best_scenario = max(scores, key=scores.get) - best_score = scores[best_scenario] - if best_score <= 0: - return "unknown", 0.0 - - if best_scenario == "knowledge": - business_scores = [ - scores["expense"], - scores["accounts_receivable"], - scores["accounts_payable"], - ] - if max(business_scores) > 0: - best_scenario = ("expense", "accounts_receivable", "accounts_payable")[ - business_scores.index(max(business_scores)) - ] - best_score = max(business_scores) - - return best_scenario, round(min(best_score, 0.34), 2) - - def _detect_intent( - self, - compact_query: str, - *, - scenario: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - ) -> tuple[str, float]: - if any(keyword in compact_query for keyword in OPERATE_KEYWORDS): - return "operate", 0.30 - if any(keyword in compact_query for keyword in DRAFT_KEYWORDS): - return "draft", 0.26 - if scenario == "expense" and self._is_generic_expense_prompt(compact_query): - return "draft", 0.24 - if any(keyword in compact_query for keyword in COMPARE_KEYWORDS): - return "compare", 0.24 - if any(keyword in compact_query for keyword in EXPLAIN_KEYWORDS): - return "explain", 0.22 - if any(keyword in compact_query for keyword in RISK_KEYWORDS): - return "risk_check", 0.24 - if any(keyword in compact_query for keyword in QUERY_KEYWORDS): - return "query", 0.20 - if self._looks_like_expense_narrative( - compact_query, - scenario=scenario, - entities=entities, - time_range=time_range, - ): - return "draft", 0.22 - return "query", 0.10 - - @staticmethod - def _looks_like_follow_up_message(compact_query: str) -> bool: - if not compact_query: - return False - if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS): - return True - if compact_query.startswith(("那", "这", "它", "这个", "那个")): - return True - - has_domain_keyword = any( - keyword in compact_query - for keyword, _weight in ( - *SCENARIO_KEYWORDS["expense"], - *SCENARIO_KEYWORDS["accounts_receivable"], - *SCENARIO_KEYWORDS["accounts_payable"], - *SCENARIO_KEYWORDS["knowledge"], - ) - ) - return len(compact_query) <= 12 and not has_domain_keyword - - def _should_inherit_expense_draft( - self, - compact_query: str, - *, - scenario: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - context_json: dict[str, Any], - ) -> bool: - context_scenario = self._resolve_context_scenario(context_json) - draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() - if context_scenario != "expense" and not draft_claim_id: - return False - - if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS): - return True - if self._looks_like_expense_narrative( - compact_query, - scenario="expense", - entities=entities, - time_range=time_range, - ): - return True - if self._looks_like_follow_up_message(compact_query): - return True - - if any(keyword in compact_query for keyword in OPERATE_KEYWORDS): - return False - if any(keyword in compact_query for keyword in COMPARE_KEYWORDS + RISK_KEYWORDS): - return False - if any(keyword in compact_query for keyword in QUERY_KEYWORDS): - return False - - return bool( - draft_claim_id - and any( - item.type - in {"amount", "customer", "employee", "expense_type", "project", "invoice"} - for item in entities - ) - ) - - @staticmethod - def _is_generic_expense_prompt(compact_query: str) -> bool: - return compact_query in GENERIC_EXPENSE_PROMPTS - - @staticmethod - def _looks_like_expense_narrative( - compact_query: str, - *, - scenario: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - ) -> bool: - if scenario not in {"expense", "accounts_receivable", "accounts_payable", "unknown"}: - return False - - if any(keyword in compact_query for keyword in AR_CORE_KEYWORDS + AP_CORE_KEYWORDS): - return False - - entity_types = {item.type for item in entities} - has_expense_signal = any( - keyword in compact_query for keyword in EXPENSE_NARRATIVE_KEYWORDS - ) or "expense_type" in entity_types - has_context_signal = bool(time_range.start_date) or "amount" in entity_types - - return has_expense_signal and has_context_signal - - def _parse_with_model( - self, - *, - payload: OntologyParseRequest, - query: str, - compact_query: str, - fallback_scenario: str, - fallback_intent: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - metrics: list[OntologyMetric], - constraints: list[OntologyConstraint], - ) -> LlmOntologyParseResult | None: - messages = self._build_model_messages( - payload=payload, - query=query, - compact_query=compact_query, - fallback_scenario=fallback_scenario, - fallback_intent=fallback_intent, - entities=entities, - time_range=time_range, - metrics=metrics, - constraints=constraints, - ) - response_text = self.runtime_chat_service.complete( - messages, - max_tokens=600, - temperature=0.0, - ) - payload_json = self._extract_json_payload(response_text) - if payload_json is None: - return None - - try: - return LlmOntologyParseResult.model_validate(payload_json) - except ValidationError as exc: - logger.warning("Semantic model output validation failed: %s", exc) - return None - - @staticmethod - def _build_model_messages( - *, - payload: OntologyParseRequest, - query: str, - compact_query: str, - fallback_scenario: str, - fallback_intent: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - metrics: list[OntologyMetric], - constraints: list[OntologyConstraint], - ) -> list[dict[str, str]]: - facts = { - "query": query, - "compact_query": compact_query, - "context": { - "entry_source": payload.context_json.get("entry_source"), - "attachment_names": payload.context_json.get("attachment_names", []), - "attachment_count": payload.context_json.get("attachment_count", 0), - "ocr_summary": payload.context_json.get("ocr_summary", ""), - "ocr_documents": payload.context_json.get("ocr_documents", []), - "request_context": payload.context_json.get("request_context"), - "role_codes": payload.context_json.get("role_codes", []), - "conversation_id": payload.context_json.get("conversation_id"), - "conversation_scenario": payload.context_json.get("conversation_scenario"), - "conversation_intent": payload.context_json.get("conversation_intent"), - "draft_claim_id": payload.context_json.get("draft_claim_id"), - "review_action": payload.context_json.get("review_action"), - "review_form_values": payload.context_json.get("review_form_values"), - "conversation_history": payload.context_json.get("conversation_history", []), - }, - "rule_candidates": { - "scenario": fallback_scenario, - "intent": fallback_intent, - "entities": [item.model_dump(mode="json") for item in entities], - "time_range": time_range.model_dump(mode="json"), - "metrics": [item.model_dump(mode="json") for item in metrics], - "constraints": [item.model_dump(mode="json") for item in constraints], - }, - } - - system_prompt = ( - "你是企业财务共享平台的语义解析器。" - "你的任务是把用户输入解析为固定 JSON,用于后续路由、追问和权限判断。" - "只输出 JSON 对象,不要输出 Markdown、代码块、解释、标题或 。" - "场景 scenario 只能是:expense, accounts_receivable, " - "accounts_payable, knowledge, unknown。" - "意图 intent 只能是:query, explain, compare, risk_check, draft, operate。" - "如果用户是在描述一笔待处理费用、待报销事项、上传票据或希望整理报销," - "即使没有明确说“生成草稿”,也优先使用 expense + draft。" - "如果提供了 conversation_history,必须把最近轮次作为当前追问的上下文," - "正确理解“这个”“那笔”“改成 800”“继续补充”这类省略表达。" - "出现“客户”不等于应收,出现“供应商”不等于应付,必须结合动作词和业务目标判断。" - "只有明确查询、统计、列出、多少、明细、对比时才优先使用 query 或 compare。" - "附件名称和 OCR 摘要只作为辅助证据,不能编造未出现的事实。" - "信息不足时 clarification_required=true,并给出一句简短中文追问。" - "missing_slots 使用简短 snake_case,例如 expense_type, amount, " - "customer_name, participants, attachments。" - "entity_hints 只填写你比较确定的业务对象;如果不确定,可以返回空数组。" - ) - user_prompt = ( - "请根据以下事实输出 JSON:\n" - f"{json.dumps(facts, ensure_ascii=False, indent=2, default=str)}\n\n" - "输出格式:\n" - "{\n" - ' "scenario": "expense",\n' - ' "intent": "draft",\n' - ' "confidence": 0.88,\n' - ' "clarification_required": true,\n' - ' "clarification_question": "请补充客户单位、参与人员和票据附件。",\n' - ' "missing_slots": ["customer_name", "participants", "attachments"],\n' - ' "ambiguity": [],\n' - ' "entity_hints": [\n' - ' {"type": "expense_type", "value": "招待", ' - '"normalized_value": "entertainment", "role": "filter", ' - '"confidence": 0.86}\n' - " ]\n" - "}" - ) - return [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ] - - @staticmethod - def _extract_json_payload(response_text: str | None) -> dict[str, Any] | None: - if not response_text: - return None - - cleaned = re.sub(r".*?", "", response_text, flags=re.DOTALL | re.IGNORECASE) - cleaned = cleaned.strip() - if not cleaned: - return None - - fenced_match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", cleaned, flags=re.DOTALL) - candidates = [fenced_match.group(1)] if fenced_match else [] - candidates.extend([cleaned]) - - start = cleaned.find("{") - end = cleaned.rfind("}") - if start != -1 and end != -1 and end > start: - candidates.append(cleaned[start : end + 1]) - - for candidate in candidates: - try: - parsed = json.loads(candidate) - except json.JSONDecodeError: - continue - if isinstance(parsed, dict): - return parsed - - return None - - @staticmethod - def _resolve_scenario( - fallback_scenario: str, - model_parse: LlmOntologyParseResult | None, - ) -> str: - if model_parse is None: - return fallback_scenario - if model_parse.scenario == "unknown" and fallback_scenario != "unknown": - return fallback_scenario - return model_parse.scenario - - def _resolve_intent( - self, - compact_query: str, - *, - fallback_intent: str, - scenario: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - model_parse: LlmOntologyParseResult | None, - ) -> str: - candidate = model_parse.intent if model_parse is not None else fallback_intent - if scenario == "knowledge": - if candidate in KNOWLEDGE_INTENTS: - return candidate - if fallback_intent in KNOWLEDGE_INTENTS: - return fallback_intent - return "query" - if candidate == "query" and scenario == "expense": - if self._is_generic_expense_prompt(compact_query) or fallback_intent == "draft": - return "draft" - return candidate - - @staticmethod - def _merge_entities( - base_entities: list[OntologyEntity], - entity_hints: list[LlmOntologyEntityHint], - ) -> list[OntologyEntity]: - merged: dict[tuple[str, str], OntologyEntity] = { - (item.type, item.normalized_value): item for item in base_entities - } - - for hint in entity_hints: - value = str(hint.value or "").strip() - if not value: - continue - normalized_value = str(hint.normalized_value or value).strip() - key = (str(hint.type).strip(), normalized_value) - candidate = OntologyEntity( - type=str(hint.type).strip(), - value=value, - normalized_value=normalized_value, - role=str(hint.role or "target").strip() or "target", - confidence=float(hint.confidence), - ) - existing = merged.get(key) - if existing is None or existing.confidence < candidate.confidence: - merged[key] = candidate - - return list(merged.values()) - - @staticmethod - def _normalize_short_text_list(values: list[str]) -> list[str]: - normalized: list[str] = [] - seen: set[str] = set() - for value in values: - cleaned = str(value or "").strip() - if not cleaned or cleaned in seen: - continue - normalized.append(cleaned) - seen.add(cleaned) - return normalized[:6] - - def _infer_default_missing_slots( - self, - compact_query: str, - *, - scenario: str, - intent: str, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - context_json: dict[str, Any], - ) -> list[str]: - if scenario != "expense" or intent != "draft": - return [] - - entity_types = {item.type for item in entities} - attachment_count = int(context_json.get("attachment_count") or 0) - missing_slots: list[str] = [] - - if self._is_generic_expense_prompt(compact_query): - if "expense_type" not in entity_types: - missing_slots.append("expense_type") - if "amount" not in entity_types: - missing_slots.append("amount") - if not time_range.start_date: - missing_slots.append("time_range") - missing_slots.append("reason") - if attachment_count <= 0: - missing_slots.append("attachments") - return missing_slots - - if any( - item.normalized_value == "entertainment" - for item in entities - if item.type == "expense_type" - ): - if "customer" not in entity_types: - missing_slots.append("customer_name") - missing_slots.append("participants") - if attachment_count <= 0: - missing_slots.append("attachments") - - return missing_slots - - @staticmethod - def _resolve_confidence( - *, - model_confidence: float | None, - fallback_confidence: float, - clarification_required: bool, - permission: OntologyPermission, - ) -> float: - confidence = fallback_confidence if model_confidence is None else float(model_confidence) - confidence = max(0.0, min(confidence, 0.98)) - if permission.level == AgentPermissionLevel.FORBIDDEN.value: - confidence = max(confidence, 0.86) - if clarification_required and permission.level != AgentPermissionLevel.FORBIDDEN.value: - confidence = min(confidence, 0.58) - return round(confidence, 2) - - def _extract_entities( - self, - query: str, - compact_query: str, - reference: ReferenceCatalog, - ) -> list[OntologyEntity]: - entities: dict[tuple[str, str], OntologyEntity] = {} - - def upsert(entity: OntologyEntity) -> None: - key = (entity.type, entity.normalized_value) - if key not in entities: - entities[key] = entity - - for match in re.finditer(r"客户\s*([A-Za-z0-9一二三四五六七八九十]+)", query): - suffix = match.group(1).strip() - normalized = f"客户{suffix}".replace(" ", "") - upsert(self._make_entity("customer", match.group(0).strip(), normalized, role="filter")) - labeled_customer_match = re.search(r"客户名称[::]\s*(?P[^\n,。;]+)", query) - if labeled_customer_match: - customer_name = labeled_customer_match.group("name").strip() - upsert(self._make_entity("customer", customer_name, customer_name, role="filter")) - - for match in re.finditer(r"供应商\s*([A-Za-z0-9一二三四五六七八九十]+)", query): - suffix = match.group(1).strip() - normalized = f"供应商{suffix}".replace(" ", "") - upsert(self._make_entity("vendor", match.group(0).strip(), normalized, role="filter")) - - employee_match = re.search( - r"(?P[赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦许何吕施张孔曹严华金魏陶姜" - r"戚谢邹喻柏水窦章云苏潘葛范彭郎鲁韦昌马苗凤花方俞任袁柳鲍史唐费廉岑" - r"薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅卞康伍余元卜顾孟平黄和穆萧尹姚邵" - r"湛汪祁毛禹狄米贝明臧计成戴宋庞熊纪舒屈项祝董梁杜阮蓝闵席季强贾路江" - r"童颜郭梅盛林钟徐邱骆高夏蔡田樊胡凌霍虞万支柯管卢莫房裘缪解应宗丁宣" - r"邓洪包左石崔吉龚程嵇邢裴陆荣翁荀羊惠甄曲家封芮储靳汲邴糜松井段富巫" - r"乌焦巴弓牧隗山谷车侯伊宫宁仇栾刘景詹束龙叶司黎薄印白怀蒲邰从鄂索咸" - r"籍卓蔺屠蒙池乔阴胥能苍双闻莘党翟谭贡姬申扶堵冉宰郦雍桑桂牛寿通边扈" - r"燕冀浦尚农温别庄晏柴瞿阎连茹习艾容向古易慎戈廖庾终暨居衡步都耿满弘" - r"匡国文寇广禄阙东欧殳沃利蔚越夔隆师巩聂晁勾敖融冷辛阚那简饶曾关蒯相" - r"查后荆游竺权盖益桓公][\u4e00-\u9fa5]{1,2})(?=\s*(?:\d{4}年|\d{1,2}月|本月|" - r"上月|本周|报销|差旅|费用|申请))", - query, - ) - if employee_match: - name = employee_match.group("name") - upsert(self._make_entity("employee", name, name, role="filter")) - - for name in reference.employees: - if self._compact(name) in compact_query: - upsert(self._make_entity("employee", name, name, role="filter")) - for name in reference.departments: - if self._compact(name) in compact_query: - upsert(self._make_entity("department", name, name, role="filter")) - for name in reference.customers: - if self._compact(name) in compact_query: - upsert(self._make_entity("customer", name, name, role="filter")) - for name in reference.vendors: - if self._compact(name) in compact_query: - upsert(self._make_entity("vendor", name, name, role="filter")) - for code in reference.projects: - if self._compact(code) in compact_query: - upsert(self._make_entity("project", code, code, role="filter")) - - for code in re.findall(r"PRJ-[A-Z]+-\d+", query, flags=re.IGNORECASE): - upsert(self._make_entity("project", code, code.upper(), role="filter")) - for code in re.findall(r"EXP-\d{6}-\d{3}", query, flags=re.IGNORECASE): - upsert(self._make_entity("expense_claim", code, code.upper())) - for code in re.findall(r"AR-\d{6}-\d{3}", query, flags=re.IGNORECASE): - upsert(self._make_entity("receivable", code, code.upper())) - for code in re.findall(r"AP-\d{6}-\d{3}", query, flags=re.IGNORECASE): - upsert(self._make_entity("payable", code, code.upper())) - for code in re.findall(r"INV-[A-Z]+-\d+", query, flags=re.IGNORECASE): - upsert(self._make_entity("invoice", code, code.upper())) - for code in re.findall(r"CTR-[A-Z]+-\d+", query, flags=re.IGNORECASE): - upsert(self._make_entity("contract", code, code.upper())) - - for label, normalized in EXPENSE_TYPE_KEYWORDS.items(): - if label in query: - upsert(self._make_entity("expense_type", label, normalized, role="filter")) - - has_customer_entertainment_signal = "客户" in query and any( - keyword in query for keyword in ("吃饭", "用餐", "餐饮", "宴请", "请客", "招待") - ) - if has_customer_entertainment_signal: - upsert( - self._make_entity( - "expense_type", - "客户招待", - "entertainment", - role="filter", - confidence=0.96, - ) - ) - - if any(keyword in query for keyword in ("打车", "网约车", "出租车", "车费", "停车费", "过路费")): - upsert(self._make_entity("expense_type", "交通", "transport", role="filter", confidence=0.9)) - - if any(keyword in query for keyword in ("出差", "机票", "火车", "高铁", "行程单")): - upsert(self._make_entity("expense_type", "差旅", "travel", role="filter", confidence=0.88)) - - if any(keyword in query for keyword in ("酒店", "住宿", "宾馆")): - upsert(self._make_entity("expense_type", "住宿", "hotel", role="filter", confidence=0.86)) - - if ( - not has_customer_entertainment_signal - and any(keyword in query for keyword in ("餐费", "用餐", "午餐", "晚餐", "早餐", "餐饮")) - ): - upsert(self._make_entity("expense_type", "餐费", "meal", role="filter", confidence=0.84)) - - if any( - keyword in query - for keyword in ("办公用品", "文具", "耗材", "办公耗材", "打印纸", "办公设备", "键盘", "鼠标", "白板") - ): - upsert(self._make_entity("expense_type", "办公费", "office", role="filter", confidence=0.87)) - - if any(keyword in query for keyword in ("培训", "讲师费", "课时费", "课程费")): - upsert(self._make_entity("expense_type", "培训费", "training", role="filter", confidence=0.84)) - - if any(keyword in query for keyword in ("通讯费", "话费", "流量费", "宽带费")): - upsert(self._make_entity("expense_type", "通讯费", "communication", role="filter", confidence=0.84)) - - if any(keyword in query for keyword in ("福利费", "团建", "慰问", "节日福利", "体检费")): - upsert(self._make_entity("expense_type", "福利费", "welfare", role="filter", confidence=0.84)) - - for amount in self._extract_amount_entities(query): - upsert(amount) - - return list(entities.values()) - - def _extract_amount_entities(self, query: str) -> list[OntologyEntity]: - entities: list[OntologyEntity] = [] - for match in AMOUNT_PATTERN.finditer(query): - raw_value = match.group("value") - unit = match.group("unit") - prefix = match.group("prefix") - if raw_value is None: - continue - if prefix is None and unit is None: - continue - - amount_value = self._normalize_amount(raw_value, unit) - display_value = f"{raw_value}{unit or ''}" - role = "threshold" if prefix else "target" - entities.append( - self._make_entity( - "amount", - display_value, - str(amount_value), - role=role, - confidence=0.9, - ) - ) - return entities - - @staticmethod - def _make_entity( - entity_type: str, - value: str, - normalized_value: str, - *, - role: str = "target", - confidence: float = 0.92, - ) -> OntologyEntity: - return OntologyEntity( - type=entity_type, - value=value, - normalized_value=normalized_value, - role=role, - confidence=confidence, - ) - - @staticmethod - def _infer_scenario_from_entities(entities: list[OntologyEntity]) -> str | None: - entity_types = {item.type for item in entities} - if entity_types & {"vendor", "payable"}: - return "accounts_payable" - if entity_types & {"customer", "receivable", "contract"}: - return "accounts_receivable" - if entity_types & {"employee", "expense_claim", "expense_type"}: - return "expense" - return None - - def _extract_time_range( - self, - query: str, - compact_query: str, - *, - context_json: dict[str, Any], - ) -> tuple[OntologyTimeRange, float]: - today = self._resolve_reference_today(context_json) - - direct_mappings = [ - ("大前天", self._single_day_range(today - timedelta(days=3), "大前天", "day")), - ("前天", self._single_day_range(today - timedelta(days=2), "前天", "day")), - ("昨日", self._single_day_range(today - timedelta(days=1), "昨日", "day")), - ("昨天", self._single_day_range(today - timedelta(days=1), "昨天", "day")), - ("今天", self._single_day_range(today, "今天", "day")), - ("明天", self._single_day_range(today + timedelta(days=1), "明天", "day")), - ("后天", self._single_day_range(today + timedelta(days=2), "后天", "day")), - ("大后天", self._single_day_range(today + timedelta(days=3), "大后天", "day")), - ] - for keyword, value in direct_mappings: - if keyword in query: - return value, 0.10 - - if "本周" in query or "这周" in query or "本星期" in query: - start = today - timedelta(days=today.weekday()) - end = start + timedelta(days=6) - return self._range(start, end, "本周", "week"), 0.10 - if "上周" in query: - end = today - timedelta(days=today.weekday() + 1) - start = end - timedelta(days=6) - return self._range(start, end, "上周", "week"), 0.10 - if "本月" in query or "这个月" in query: - start = date(today.year, today.month, 1) - end = date(today.year, today.month, calendar.monthrange(today.year, today.month)[1]) - return self._range(start, end, "本月", "month"), 0.10 - if "上月" in query: - year = today.year if today.month > 1 else today.year - 1 - month = today.month - 1 if today.month > 1 else 12 - start = date(year, month, 1) - end = date(year, month, calendar.monthrange(year, month)[1]) - return self._range(start, end, "上月", "month"), 0.10 - if "本季度" in query or "这个季度" in query: - quarter = (today.month - 1) // 3 - start_month = quarter * 3 + 1 - end_month = start_month + 2 - start = date(today.year, start_month, 1) - end = date(today.year, end_month, calendar.monthrange(today.year, end_month)[1]) - return self._range(start, end, "本季度", "quarter"), 0.10 - if "今年" in query: - return ( - self._range(date(today.year, 1, 1), date(today.year, 12, 31), "今年", "year"), - 0.10, - ) - - match = DATE_RANGE_PATTERN.search(query) - if match: - start = self._parse_iso_date(match.group("start")) - end = self._parse_iso_date(match.group("end")) - if start and end: - return self._range(start, end, match.group(0), "custom"), 0.10 - - match = EXPLICIT_DATE_PATTERN.search(query) - if match: - explicit = date( - int(match.group("year")), - int(match.group("month")), - int(match.group("day")), - ) - return self._single_day_range(explicit, match.group(0), "day"), 0.10 - - match = EXPLICIT_MONTH_PATTERN.search(query) - if match: - year = int(match.group("year")) - month = int(match.group("month")) - start = date(year, month, 1) - end = date(year, month, calendar.monthrange(year, month)[1]) - return self._range(start, end, match.group(0), "month"), 0.10 - - match = MONTH_DAY_RANGE_PATTERN.search(query) - if match: - start = date(today.year, int(match.group("start_month")), int(match.group("start_day"))) - end = date(today.year, int(match.group("end_month")), int(match.group("end_day"))) - return self._range(start, end, match.group(0), "custom"), 0.10 - - match = MONTH_DAY_PATTERN.search(compact_query) - if match: - explicit = date(today.year, int(match.group("month")), int(match.group("day"))) - return self._single_day_range(explicit, match.group(0), "day"), 0.08 - - month_match = re.search(r"(?P\d{1,2})月", compact_query) - if month_match: - month = int(month_match.group("month")) - start = date(today.year, month, 1) - end = date(today.year, month, calendar.monthrange(today.year, month)[1]) - return self._range(start, end, month_match.group(0), "month"), 0.08 - - return OntologyTimeRange(), 0.0 - - @staticmethod - def _resolve_reference_today(context_json: dict[str, Any]) -> date: - client_now_iso = str(context_json.get("client_now_iso") or "").strip() - if not client_now_iso: - return datetime.now(UTC).date() - - normalized = client_now_iso.replace("Z", "+00:00") - try: - client_now = datetime.fromisoformat(normalized) - except ValueError: - return datetime.now(UTC).date() - - if client_now.tzinfo is None: - client_now = client_now.replace(tzinfo=UTC) - - try: - offset_minutes = int(context_json.get("client_timezone_offset_minutes") or 0) - except (TypeError, ValueError): - offset_minutes = 0 - - local_now = client_now - timedelta(minutes=offset_minutes) - return local_now.date() - - @staticmethod - def _single_day_range(target: date, raw: str, granularity: str) -> OntologyTimeRange: - return OntologyTimeRange( - raw=raw, - start_date=target.isoformat(), - end_date=target.isoformat(), - granularity=granularity, - ) - - @staticmethod - def _range(start: date, end: date, raw: str, granularity: str) -> OntologyTimeRange: - return OntologyTimeRange( - raw=raw, - start_date=start.isoformat(), - end_date=end.isoformat(), - granularity=granularity, - ) - - @staticmethod - def _parse_iso_date(value: str) -> date | None: - try: - return date.fromisoformat(value) - except ValueError: - return None - - def _extract_metrics(self, compact_query: str) -> list[OntologyMetric]: - metrics: dict[str, OntologyMetric] = {} - - def upsert(metric: OntologyMetric) -> None: - metrics[metric.name] = metric - - if any( - keyword in compact_query - for keyword in ("多少钱", "金额", "总额", "支出", "回款", "应收", "应付") - ): - upsert(OntologyMetric(name="amount", aggregation="sum", unit="CNY")) - if any(keyword in compact_query for keyword in ("多少笔", "几笔", "数量", "条数", "单数")): - upsert(OntologyMetric(name="count", aggregation="count", unit="records")) - if "超标" in compact_query or "超预算" in compact_query: - upsert(OntologyMetric(name="amount_over_limit")) - if "逾期" in compact_query or "账龄" in compact_query: - upsert(OntologyMetric(name="overdue")) - if "重复" in compact_query: - upsert(OntologyMetric(name="duplicate_expense")) - - top_match = TOP_N_PATTERN.search(compact_query) - if top_match: - metrics["amount"] = OntologyMetric( - name="amount", - aggregation="sum", - unit="CNY", - sort="desc" if "最低" not in compact_query else "asc", - top_n=int(top_match.group("top")), - ) - - return list(metrics.values()) - - def _extract_constraints( - self, - compact_query: str, - entities: list[OntologyEntity], - ) -> list[OntologyConstraint]: - constraints: dict[tuple[str, str, str, str | None], OntologyConstraint] = {} - - def upsert(constraint: OntologyConstraint) -> None: - key = ( - constraint.field, - constraint.operator, - str(constraint.value), - constraint.currency, - ) - if key not in constraints: - constraints[key] = constraint - - for entity in entities: - if entity.type in { - "employee", - "department", - "customer", - "vendor", - "project", - "expense_type", - }: - upsert( - OntologyConstraint( - field=entity.type, - operator="=", - value=entity.normalized_value, - ) - ) - - for keyword, normalized in STATUS_KEYWORDS.items(): - if keyword in compact_query: - upsert(OntologyConstraint(field="status", operator="=", value=normalized)) - - for amount_match in AMOUNT_PATTERN.finditer(compact_query): - if not amount_match.group("prefix"): - continue - - operator = self._normalize_operator(amount_match.group("prefix")) - value = self._normalize_amount(amount_match.group("value"), amount_match.group("unit")) - upsert( - OntologyConstraint( - field="amount", - operator=operator, - value=value, - currency="CNY", - ) - ) - break - - top_match = TOP_N_PATTERN.search(compact_query) - if top_match: - top_n = int(top_match.group("top")) - upsert(OntologyConstraint(field="top_n", operator="=", value=top_n)) - upsert( - OntologyConstraint( - field="sort_by", - operator="desc" if "最低" not in compact_query else "asc", - value="amount", - ) - ) - - return list(constraints.values()) - - def _extract_risk_flags(self, compact_query: str, scenario: str) -> list[str]: - risk_flags: list[str] = [] - - def append(flag: str) -> None: - if flag not in risk_flags: - risk_flags.append(flag) - - if "重复" in compact_query: - append("duplicate_expense") - if any( - keyword in compact_query - for keyword in ("发票异常", "票据异常", "验真失败", "附件缺失", "补件") - ): - append("invoice_anomaly") - if any(keyword in compact_query for keyword in ("超标", "超预算", "超限")): - append("amount_over_limit") - if scenario == "accounts_receivable" and any( - keyword in compact_query for keyword in ("逾期", "账龄", "欠款", "未回款") - ): - append("ar_overdue") - if scenario == "accounts_payable" and any( - keyword in compact_query for keyword in ("逾期", "待付", "付款风险", "未付款") - ): - append("ap_overdue") - - return risk_flags - - def _resolve_permission( - self, - compact_query: str, - context_json: dict, - intent: str, - ) -> OntologyPermission: - role_codes = { - str(item).strip().lower() - for item in context_json.get("role_codes", []) - if str(item).strip() - } - is_admin = bool(context_json.get("is_admin")) - privileged = is_admin or bool(role_codes & PRIVILEGED_ROLE_CODES) - - if intent in {"query", "explain", "compare", "risk_check"}: - return OntologyPermission( - level=AgentPermissionLevel.READ.value, - allowed=True, - reason="只读查询。", - ) - if intent == "draft": - return OntologyPermission( - level=AgentPermissionLevel.DRAFT_WRITE.value, - allowed=True, - reason="允许生成草稿,但不会直接提交业务动作。", - ) - - if any(keyword in compact_query for keyword in OPERATE_KEYWORDS) or "付款" in compact_query: - if privileged: - return OntologyPermission( - level=AgentPermissionLevel.APPROVAL_REQUIRED.value, - allowed=False, - reason="涉及付款、审批或上线动作,必须进入人工审批链。", - ) - return OntologyPermission( - level=AgentPermissionLevel.FORBIDDEN.value, - allowed=False, - reason="当前账号缺少财务或审批权限,只能查看结果或生成草稿。", - ) - - return OntologyPermission( - level=AgentPermissionLevel.APPROVAL_REQUIRED.value, - allowed=False, - reason="操作类请求需要人工审批确认。", - ) - - def _build_field_errors( - self, - *, - scenario: str, - intent: str, - entities: list[OntologyEntity], - permission: OntologyPermission, - missing_slots: list[str], - ambiguity: list[str], - ) -> list[OntologyFieldError]: - errors: list[OntologyFieldError] = [] - if scenario == "unknown": - errors.append( - OntologyFieldError( - field="scenario", - code="scenario_unknown", - message="未识别出明确业务场景,请补充是报销、应收、应付还是制度问题。", - ) - ) - if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2: - errors.append( - OntologyFieldError( - field="entities", - code="compare_target_missing", - message="对比类问题请至少给出两个对象,或给出更明确的对比范围。", - ) - ) - if missing_slots: - errors.append( - OntologyFieldError( - field="missing_slots", - code="required_slot_missing", - message=( - "继续处理前还缺少关键信息:" - f"{'、'.join(self._display_slot_label(item) for item in missing_slots)}。" - ), - ) - ) - if ambiguity: - errors.append( - OntologyFieldError( - field="ambiguity", - code="ambiguity_detected", - message=f"当前问题存在歧义:{';'.join(ambiguity)}。", - ) - ) - if permission.level == AgentPermissionLevel.FORBIDDEN.value: - errors.append( - OntologyFieldError( - field="permission", - code="permission_forbidden", - message=permission.reason, - ) - ) - return errors - - def _build_clarification( - self, - *, - scenario: str, - intent: str, - entities: list[OntologyEntity], - permission: OntologyPermission, - missing_slots: list[str], - ambiguity: list[str], - allow_incomplete_draft: bool, - model_clarification_required: bool, - model_clarification_question: str | None, - ) -> tuple[bool, str | None]: - if permission.level == AgentPermissionLevel.FORBIDDEN.value: - return True, "当前动作超出权限范围。是否改为生成草稿或建议?" - if scenario == "knowledge" and intent in {"query", "explain"}: - return False, None - if model_clarification_required: - question = str(model_clarification_question or "").strip() - if question: - return True, question - if missing_slots: - return True, self._build_missing_slot_question(missing_slots) - if ambiguity: - return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。" - if scenario == "unknown": - return True, "请说明这是报销、应收、应付,还是制度知识问题?" - if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2: - return True, "请补充需要对比的两个对象,例如两个客户、两个供应商或两个员工。" - if allow_incomplete_draft and scenario == "expense" and intent == "draft": - return False, None - if missing_slots: - return True, self._build_missing_slot_question(missing_slots) - if ambiguity: - return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。" - return False, None - - @staticmethod - def _allow_incomplete_draft( - context_json: dict[str, Any], - *, - scenario: str, - intent: str, - ) -> bool: - if scenario != "expense" or intent != "draft": - return False - return str(context_json.get("review_action") or "").strip() == "save_draft" - - @staticmethod - def _display_slot_label(slot: str) -> str: - return MISSING_SLOT_LABELS.get(slot, slot) - - def _build_missing_slot_question(self, missing_slots: list[str]) -> str: - labels = [self._display_slot_label(item) for item in missing_slots[:4]] - if not labels: - return "请补充更多上下文后再继续。" - return f"请补充{'、'.join(labels)},我再继续帮你解析和处理。" - - @staticmethod - def _compute_confidence( - *, - scenario: str, - scenario_score: float, - intent_score: float, - entities: list[OntologyEntity], - time_range: OntologyTimeRange, - metrics: list[OntologyMetric], - constraints: list[OntologyConstraint], - risk_flags: list[str], - clarification_required: bool, - permission: OntologyPermission, - ) -> float: - confidence = 0.18 + scenario_score + intent_score - confidence += min(0.16, len(entities) * 0.04) - if time_range.start_date: - confidence += 0.10 - if metrics: - confidence += 0.06 - if constraints: - confidence += 0.06 - if risk_flags: - confidence += 0.08 - if permission.level == AgentPermissionLevel.FORBIDDEN.value: - confidence = max(confidence, 0.86) - - if scenario == "unknown": - confidence = min(confidence, 0.45) - if clarification_required and permission.level != AgentPermissionLevel.FORBIDDEN.value: - confidence = min(confidence, 0.58) - - return round(min(confidence, 0.98), 2) - - @staticmethod - def _build_result_summary( - scenario: str, - intent: str, - permission_level: str, - confidence: float, - ) -> str: - return ( - f"语义解析完成:scenario={scenario}, intent={intent}, " - f"permission={permission_level}, confidence={confidence:.2f}" - ) - - @staticmethod - def _normalize_operator(value: str) -> str: - mapping = { - "超过": ">", - "大于": ">", - "高于": ">", - ">": ">", - ">=": ">=", - "不少于": ">=", - "不低于": ">=", - "小于": "<", - "低于": "<", - "少于": "<", - "<": "<", - "<=": "<=", - "至多": "<=", - "不超过": "<=", - "=": "=", - "=": "=", - } - return mapping.get(value, value) - - @staticmethod - def _normalize_amount(raw_value: str | None, unit: str | None) -> int | float: - numeric = float(raw_value or 0) - if unit in {"万", "万元"}: - numeric *= 10000 - return int(numeric) if numeric.is_integer() else round(numeric, 2) +from __future__ import annotations + +import calendar +import json +import re +from dataclasses import dataclass +from datetime import UTC, date, datetime, timedelta +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, ValidationError +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.agent_enums import ( + AgentName, + AgentPermissionLevel, + AgentRunSource, + AgentRunStatus, +) +from app.core.logging import get_logger +from app.models.employee import Employee +from app.models.financial_record import ( + AccountsPayableRecord, + AccountsReceivableRecord, + ExpenseClaim, +) +from app.models.organization import OrganizationUnit +from app.schemas.ontology import ( + OntologyConstraint, + OntologyEntity, + OntologyFieldError, + OntologyIntent, + OntologyMetric, + OntologyParseRequest, + OntologyParseResult, + OntologyPermission, + OntologyScenario, + OntologyTimeRange, +) +from app.services.agent_foundation import AgentFoundationService +from app.services.agent_runs import AgentRunService +from app.services.runtime_chat import RuntimeChatService + +logger = get_logger("app.services.ontology") + +DATE_RANGE_PATTERN = re.compile( + r"(?P\d{4}-\d{1,2}-\d{1,2})\s*(?:到|至|~|-)\s*(?P\d{4}-\d{1,2}-\d{1,2})" +) +EXPLICIT_MONTH_PATTERN = re.compile(r"(?P\d{4})年(?P\d{1,2})月") +EXPLICIT_DATE_PATTERN = re.compile( + r"(?P\d{4})[年/-](?P\d{1,2})[月/-](?P\d{1,2})日?" +) +MONTH_DAY_RANGE_PATTERN = re.compile( + r"(?P\d{1,2})月(?P\d{1,2})日?\s*(?:到|至|~|-)\s*" + r"(?P\d{1,2})月(?P\d{1,2})日?" +) +MONTH_DAY_PATTERN = re.compile(r"(?P\d{1,2})月(?P\d{1,2})日?") +AMOUNT_PATTERN = re.compile( + r"(?P超过|大于|高于|不少于|不低于|小于|低于|少于|至多|不超过|<=|>=|<|>|=|=)?\s*" + r"(?P\d+(?:\.\d+)?)\s*(?P万元|万|元)?" +) +TOP_N_PATTERN = re.compile(r"(?:top|TOP|前|最高的?|最低的?)\s*(?P\d+)") + +SCENARIO_KEYWORDS = { + "expense": ( + ("报销", 0.20), + ("报账", 0.20), + ("差旅", 0.20), + ("费用", 0.14), + ("发票", 0.14), + ("票据", 0.12), + ("借款", 0.12), + ("住宿", 0.10), + ("餐费", 0.10), + ("招待", 0.18), + ("招待费", 0.18), + ("花销", 0.16), + ("花了", 0.14), + ("支出", 0.14), + ("垫付", 0.14), + ), + "accounts_receivable": ( + ("应收", 0.22), + ("回款", 0.20), + ("收款", 0.18), + ("账龄", 0.18), + ("客户欠款", 0.22), + ), + "accounts_payable": ( + ("应付", 0.22), + ("付款", 0.20), + ("请款", 0.18), + ("供应商", 0.20), + ("待付", 0.16), + ("打款", 0.18), + ), + "knowledge": ( + ("制度", 0.20), + ("规则", 0.20), + ("办法", 0.18), + ("依据", 0.18), + ("政策", 0.16), + ("知识库", 0.18), + ), +} + +QUERY_KEYWORDS = ( + "查", + "查询", + "查看", + "列出", + "统计", + "汇总", + "多少", + "几笔", + "金额", + "明细", +) +EXPLAIN_KEYWORDS = ("为什么", "依据", "原因", "怎么处理", "是否可以", "能不能", "按什么规则") +COMPARE_KEYWORDS = ("对比", "比较", "相比", "差异", "变化") +RISK_KEYWORDS = ("风险", "异常", "重复", "超标", "超预算", "逾期", "验真", "巡检") +DRAFT_KEYWORDS = ("生成", "草稿", "起草", "拟一份", "创建", "发起", "准备") +DRAFT_FOLLOW_UP_KEYWORDS = ( + "继续", + "补充", + "补一下", + "修改", + "改成", + "改为", + "换成", + "更新", + "确认", + "提交", + "保存", + "客户是", + "地点是", + "金额是", + "日期是", + "时间是", +) +OPERATE_KEYWORDS = ( + "直接付款", + "帮我付款", + "安排付款", + "发起付款", + "直接审批", + "审批通过", + "帮我审批", + "驳回", + "上线", + "激活", + "停用", + "删除", +) + +EXPENSE_TYPE_KEYWORDS = { + "差旅": "travel", + "出差": "travel", + "住宿": "hotel", + "酒店": "hotel", + "交通": "transport", + "打车": "transport", + "网约车": "transport", + "出租车": "transport", + "停车费": "transport", + "餐费": "meal", + "用餐": "meal", + "会务": "meeting", + "招待费": "entertainment", + "招待": "entertainment", + "宴请": "entertainment", + "办公费": "office", + "办公用品": "office", + "文具": "office", + "耗材": "office", + "办公耗材": "office", + "打印纸": "office", + "办公设备": "office", + "培训费": "training", + "培训": "training", + "通讯费": "communication", + "话费": "communication", + "福利费": "welfare", + "团建": "welfare", +} + +EXPENSE_NARRATIVE_KEYWORDS = ( + "报销", + "报账", + "招待", + "招待费", + "花销", + "花了", + "支出", + "垫付", + "打车", + "车费", + "餐费", + "吃饭", + "用餐", + "宴请", + "请客", + "住宿", + "发票", + "票据", + "差旅", + "客户现场", +) + +AR_CORE_KEYWORDS = ("应收", "回款", "收款", "账龄", "欠款", "未回款") +AP_CORE_KEYWORDS = ("应付", "付款", "请款", "待付", "打款", "未付款") +GENERIC_EXPENSE_PROMPTS = { + "报销", + "我要报销", + "我想报销", + "帮我报销", + "我要申请报销", + "发起报销", + "提交报销", +} +MISSING_SLOT_LABELS = { + "expense_type": "费用类型", + "amount": "金额", + "customer_name": "客户单位", + "vendor_name": "供应商", + "participants": "参与人员", + "attachments": "票据附件", + "time_range": "发生时间", + "reason": "事由说明", + "document_id": "单据号", +} + +STATUS_KEYWORDS = { + "逾期": "overdue", + "待审批": "pending", + "待审": "pending", + "已审批": "approved", + "已通过": "approved", + "已付款": "paid", + "未付款": "unpaid", + "未回款": "unreceived", +} + +PRIVILEGED_ROLE_CODES = {"manager", "finance", "approver", "executive"} +CONTEXTUAL_SCENARIOS = {"expense", "accounts_receivable", "accounts_payable", "knowledge"} +KNOWLEDGE_INTENTS = {"query", "explain", "compare"} + + +@dataclass(slots=True) +class ReferenceCatalog: + employees: list[str] + departments: list[str] + customers: list[str] + vendors: list[str] + projects: list[str] + + +class LlmOntologyEntityHint(BaseModel): + model_config = ConfigDict(extra="ignore") + + type: str + value: str + normalized_value: str | None = None + role: str = "target" + confidence: float = Field(default=0.72, ge=0.0, le=1.0) + + +class LlmOntologyParseResult(BaseModel): + model_config = ConfigDict(extra="ignore") + + scenario: OntologyScenario = Field(default="unknown") + intent: OntologyIntent = Field(default="query") + confidence: float = Field(default=0.0, ge=0.0, le=1.0) + clarification_required: bool = False + clarification_question: str | None = None + missing_slots: list[str] = Field(default_factory=list) + ambiguity: list[str] = Field(default_factory=list) + entity_hints: list[LlmOntologyEntityHint] = Field(default_factory=list) + + +class SemanticOntologyService: + def __init__(self, db: Session) -> None: + self.db = db + self.run_service = AgentRunService(db) + self.runtime_chat_service = RuntimeChatService(db) + + def parse(self, payload: OntologyParseRequest) -> OntologyParseResult: + analyzed = self._analyze(payload) + run = self.run_service.create_run( + agent=AgentName.ORCHESTRATOR.value, + source=AgentRunSource.USER_MESSAGE.value, + user_id=payload.user_id, + ontology_json=self._build_ontology_json(analyzed), + route_json={ + "stage": "semantic_parse", + "clarification_required": analyzed["clarification_required"], + "field_error_count": len(analyzed["field_errors"]), + }, + permission_level=analyzed["permission"].level, + status=( + AgentRunStatus.BLOCKED.value + if analyzed["clarification_required"] + or analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value + else AgentRunStatus.SUCCEEDED.value + ), + result_summary=self._build_result_summary( + analyzed["scenario"], + analyzed["intent"], + analyzed["permission"].level, + analyzed["confidence"], + ), + error_message=( + analyzed["permission"].reason + if analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value + else None + ), + ) + self._record_semantic_parse( + run_id=run.run_id, + payload=payload, + analyzed=analyzed, + ) + return self._build_result(analyzed, run.run_id) + + def parse_for_run(self, payload: OntologyParseRequest, *, run_id: str) -> OntologyParseResult: + analyzed = self._analyze(payload) + self._record_semantic_parse(run_id=run_id, payload=payload, analyzed=analyzed) + return self._build_result(analyzed, run_id) + + def _analyze(self, payload: OntologyParseRequest) -> dict[str, object]: + query = payload.query.strip() + if not query: + raise ValueError("query 不能为空。") + + AgentFoundationService(self.db).ensure_foundation_ready() + context_json = payload.context_json or {} + reference = self._load_reference_catalog() + compact_query = self._compact(query) + entities = self._extract_entities(query, compact_query, reference) + rule_scenario, scenario_score = self._detect_scenario(compact_query) + time_range, _time_score = self._extract_time_range( + query, + compact_query, + context_json=context_json, + ) + session_scenario = self._resolve_session_type_scenario(context_json) + context_scenario = self._resolve_context_scenario(context_json) + if session_scenario == "knowledge": + rule_scenario = "knowledge" + scenario_score = max(scenario_score, 0.34) + if rule_scenario == "unknown" and context_scenario is not None: + rule_scenario = context_scenario + scenario_score = max(scenario_score, 0.14) + if rule_scenario == "unknown": + inferred_scenario = self._infer_scenario_from_entities(entities) + if inferred_scenario is not None: + rule_scenario = inferred_scenario + scenario_score = 0.18 + + if session_scenario != "knowledge" and self._looks_like_expense_narrative( + compact_query, + scenario=rule_scenario, + entities=entities, + time_range=time_range, + ): + rule_scenario = "expense" + scenario_score = max(scenario_score, 0.24) + + rule_intent, intent_score = self._detect_intent( + compact_query, + scenario=rule_scenario, + entities=entities, + time_range=time_range, + ) + if session_scenario != "knowledge" and self._should_inherit_expense_draft( + compact_query, + scenario=rule_scenario, + entities=entities, + time_range=time_range, + context_json=context_json, + ): + rule_scenario = "expense" + rule_intent = "draft" + scenario_score = max(scenario_score, 0.18) + intent_score = max(intent_score, 0.18) + metrics = self._extract_metrics(compact_query) + constraints = self._extract_constraints(compact_query, entities) + model_parse = None + if session_scenario != "knowledge": + model_parse = self._parse_with_model( + payload=payload, + query=query, + compact_query=compact_query, + fallback_scenario=rule_scenario, + fallback_intent=rule_intent, + entities=entities, + time_range=time_range, + metrics=metrics, + constraints=constraints, + ) + scenario = self._resolve_scenario(rule_scenario, model_parse) + if session_scenario == "knowledge": + scenario = "knowledge" + entities = self._merge_entities( + entities, + model_parse.entity_hints if model_parse is not None else [], + ) + intent = self._resolve_intent( + compact_query, + fallback_intent=rule_intent, + scenario=scenario, + entities=entities, + time_range=time_range, + model_parse=model_parse, + ) + missing_slots = self._normalize_short_text_list( + model_parse.missing_slots if model_parse is not None else [] + ) + missing_slots = self._normalize_short_text_list( + missing_slots + + self._infer_default_missing_slots( + compact_query, + scenario=scenario, + intent=intent, + entities=entities, + time_range=time_range, + context_json=context_json, + ) + ) + relax_knowledge_follow_up = self._should_relax_knowledge_follow_up_clarification( + compact_query=compact_query, + scenario=scenario, + context_json=context_json, + missing_slots=missing_slots, + ) + if relax_knowledge_follow_up: + missing_slots = [item for item in missing_slots if item != "expense_type"] + ambiguity = self._normalize_short_text_list( + model_parse.ambiguity if model_parse is not None else [] + ) + risk_flags = self._extract_risk_flags(compact_query, scenario) + permission = self._resolve_permission( + compact_query, + context_json, + intent, + ) + + field_errors = self._build_field_errors( + scenario=scenario, + intent=intent, + entities=entities, + permission=permission, + missing_slots=missing_slots, + ambiguity=ambiguity, + ) + clarification_required, clarification_question = self._build_clarification( + scenario=scenario, + intent=intent, + entities=entities, + permission=permission, + missing_slots=missing_slots, + ambiguity=ambiguity, + allow_incomplete_draft=self._allow_incomplete_draft( + context_json, + scenario=scenario, + intent=intent, + ), + model_clarification_required=bool( + model_parse is not None + and model_parse.clarification_required + ), + model_clarification_question=( + model_parse.clarification_question if model_parse is not None else None + ), + ) + if relax_knowledge_follow_up: + clarification_required = False + clarification_question = None + fallback_confidence = self._compute_confidence( + scenario=scenario, + scenario_score=scenario_score, + intent_score=intent_score, + entities=entities, + time_range=time_range, + metrics=metrics, + constraints=constraints, + risk_flags=risk_flags, + clarification_required=clarification_required, + permission=permission, + ) + confidence = self._resolve_confidence( + model_confidence=( + model_parse.confidence + if model_parse is not None + else None + ), + fallback_confidence=fallback_confidence, + clarification_required=clarification_required, + permission=permission, + ) + return { + "scenario": scenario, + "intent": intent, + "entities": entities, + "time_range": time_range, + "metrics": metrics, + "constraints": constraints, + "risk_flags": risk_flags, + "permission": permission, + "confidence": confidence, + "missing_slots": missing_slots, + "ambiguity": ambiguity, + "parse_strategy": "llm_primary" if model_parse is not None else "rule_fallback", + "clarification_required": clarification_required, + "clarification_question": clarification_question, + "field_errors": field_errors, + } + + @staticmethod + def _should_relax_knowledge_follow_up_clarification( + *, + compact_query: str, + scenario: str, + context_json: dict[str, Any], + missing_slots: list[str], + ) -> bool: + if scenario != "knowledge" or "expense_type" not in missing_slots: + return False + history = context_json.get("conversation_history") + if not isinstance(history, list): + return False + has_previous_user_turn = any( + isinstance(item, dict) + and str(item.get("role") or "").strip() == "user" + and str(item.get("content") or "").strip() + for item in history + ) + if not has_previous_user_turn: + return False + follow_up_markers = ("那", "那么", "这个", "这种", "呢", "的话", "p", "P") + return any(marker in compact_query for marker in follow_up_markers) + + def _record_semantic_parse( + self, + *, + run_id: str, + payload: OntologyParseRequest, + analyzed: dict[str, object], + ) -> None: + self.run_service.record_semantic_parse( + run_id=run_id, + user_id=payload.user_id, + raw_query=payload.query.strip(), + scenario=str(analyzed["scenario"]), + intent=str(analyzed["intent"]), + entities_json=[item.model_dump() for item in analyzed["entities"]], + time_range_json=analyzed["time_range"].model_dump(), + metrics_json=[item.model_dump() for item in analyzed["metrics"]], + constraints_json=[item.model_dump() for item in analyzed["constraints"]], + risk_flags_json=list(analyzed["risk_flags"]), + permission_json=analyzed["permission"].model_dump(), + confidence=float(analyzed["confidence"]), + ) + logger.info( + "Parsed ontology run_id=%s scenario=%s intent=%s permission=%s", + run_id, + analyzed["scenario"], + analyzed["intent"], + analyzed["permission"].level, + ) + + @staticmethod + def _build_ontology_json(analyzed: dict[str, object]) -> dict[str, object]: + return { + "scenario": analyzed["scenario"], + "intent": analyzed["intent"], + "entities": [item.model_dump() for item in analyzed["entities"]], + "time_range": analyzed["time_range"].model_dump(), + "metrics": [item.model_dump() for item in analyzed["metrics"]], + "constraints": [item.model_dump() for item in analyzed["constraints"]], + "risk_flags": list(analyzed["risk_flags"]), + "permission": analyzed["permission"].model_dump(), + "missing_slots": list(analyzed["missing_slots"]), + "ambiguity": list(analyzed["ambiguity"]), + "parse_strategy": analyzed["parse_strategy"], + "confidence": analyzed["confidence"], + } + + @staticmethod + def _build_result(analyzed: dict[str, object], run_id: str) -> OntologyParseResult: + return OntologyParseResult( + scenario=analyzed["scenario"], + intent=analyzed["intent"], + entities=analyzed["entities"], + time_range=analyzed["time_range"], + metrics=analyzed["metrics"], + constraints=analyzed["constraints"], + risk_flags=analyzed["risk_flags"], + permission=analyzed["permission"], + confidence=analyzed["confidence"], + missing_slots=analyzed["missing_slots"], + ambiguity=analyzed["ambiguity"], + parse_strategy=analyzed["parse_strategy"], + clarification_required=analyzed["clarification_required"], + clarification_question=analyzed["clarification_question"], + run_id=run_id, + field_errors=analyzed["field_errors"], + ) + + def _load_reference_catalog(self) -> ReferenceCatalog: + employees = self._read_distinct_values(select(Employee.name)) + departments = self._read_distinct_values(select(OrganizationUnit.name)) + departments += self._read_distinct_values(select(ExpenseClaim.department_name)) + customers = self._read_distinct_values(select(AccountsReceivableRecord.customer_name)) + vendors = self._read_distinct_values(select(AccountsPayableRecord.vendor_name)) + projects = self._read_distinct_values(select(ExpenseClaim.project_code)) + + return ReferenceCatalog( + employees=self._dedupe_and_sort(employees), + departments=self._dedupe_and_sort(departments), + customers=self._dedupe_and_sort(customers), + vendors=self._dedupe_and_sort(vendors), + projects=self._dedupe_and_sort(projects), + ) + + def _read_distinct_values(self, stmt) -> list[str]: + values = self.db.scalars(stmt.distinct()).all() + return [str(item).strip() for item in values if item] + + @staticmethod + def _dedupe_and_sort(values: list[str]) -> list[str]: + items = {str(item).strip() for item in values if str(item).strip()} + return sorted(items, key=lambda item: (-len(item), item)) + + @staticmethod + def _compact(text: str) -> str: + return re.sub(r"\s+", "", text).lower() + + @staticmethod + def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None: + value = str(context_json.get("conversation_scenario") or "").strip() + if value in CONTEXTUAL_SCENARIOS: + return value + return None + + @staticmethod + def _resolve_session_type_scenario(context_json: dict[str, Any]) -> str | None: + value = str(context_json.get("session_type") or "").strip() + if value == "knowledge": + return "knowledge" + return None + + + def _detect_scenario(self, compact_query: str) -> tuple[str, float]: + scores = {key: 0.0 for key in SCENARIO_KEYWORDS} + for scenario, keywords in SCENARIO_KEYWORDS.items(): + for keyword, weight in keywords: + if keyword in compact_query: + scores[scenario] += weight + + best_scenario = max(scores, key=scores.get) + best_score = scores[best_scenario] + if best_score <= 0: + return "unknown", 0.0 + + if best_scenario == "knowledge": + business_scores = [ + scores["expense"], + scores["accounts_receivable"], + scores["accounts_payable"], + ] + if max(business_scores) > 0: + best_scenario = ("expense", "accounts_receivable", "accounts_payable")[ + business_scores.index(max(business_scores)) + ] + best_score = max(business_scores) + + return best_scenario, round(min(best_score, 0.34), 2) + + def _detect_intent( + self, + compact_query: str, + *, + scenario: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + ) -> tuple[str, float]: + if any(keyword in compact_query for keyword in OPERATE_KEYWORDS): + return "operate", 0.30 + if any(keyword in compact_query for keyword in DRAFT_KEYWORDS): + return "draft", 0.26 + if scenario == "expense" and self._is_generic_expense_prompt(compact_query): + return "draft", 0.24 + if any(keyword in compact_query for keyword in COMPARE_KEYWORDS): + return "compare", 0.24 + if any(keyword in compact_query for keyword in EXPLAIN_KEYWORDS): + return "explain", 0.22 + if any(keyword in compact_query for keyword in RISK_KEYWORDS): + return "risk_check", 0.24 + if any(keyword in compact_query for keyword in QUERY_KEYWORDS): + return "query", 0.20 + if self._looks_like_expense_narrative( + compact_query, + scenario=scenario, + entities=entities, + time_range=time_range, + ): + return "draft", 0.22 + return "query", 0.10 + + @staticmethod + def _looks_like_follow_up_message(compact_query: str) -> bool: + if not compact_query: + return False + if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS): + return True + if compact_query.startswith(("那", "这", "它", "这个", "那个")): + return True + + has_domain_keyword = any( + keyword in compact_query + for keyword, _weight in ( + *SCENARIO_KEYWORDS["expense"], + *SCENARIO_KEYWORDS["accounts_receivable"], + *SCENARIO_KEYWORDS["accounts_payable"], + *SCENARIO_KEYWORDS["knowledge"], + ) + ) + return len(compact_query) <= 12 and not has_domain_keyword + + def _should_inherit_expense_draft( + self, + compact_query: str, + *, + scenario: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + context_json: dict[str, Any], + ) -> bool: + context_scenario = self._resolve_context_scenario(context_json) + draft_claim_id = str(context_json.get("draft_claim_id") or "").strip() + if context_scenario != "expense" and not draft_claim_id: + return False + + if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS): + return True + if self._looks_like_expense_narrative( + compact_query, + scenario="expense", + entities=entities, + time_range=time_range, + ): + return True + if self._looks_like_follow_up_message(compact_query): + return True + + if any(keyword in compact_query for keyword in OPERATE_KEYWORDS): + return False + if any(keyword in compact_query for keyword in COMPARE_KEYWORDS + RISK_KEYWORDS): + return False + if any(keyword in compact_query for keyword in QUERY_KEYWORDS): + return False + + return bool( + draft_claim_id + and any( + item.type + in {"amount", "customer", "employee", "expense_type", "project", "invoice"} + for item in entities + ) + ) + + @staticmethod + def _is_generic_expense_prompt(compact_query: str) -> bool: + return compact_query in GENERIC_EXPENSE_PROMPTS + + @staticmethod + def _looks_like_expense_narrative( + compact_query: str, + *, + scenario: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + ) -> bool: + if scenario not in {"expense", "accounts_receivable", "accounts_payable", "unknown"}: + return False + + if any(keyword in compact_query for keyword in AR_CORE_KEYWORDS + AP_CORE_KEYWORDS): + return False + + entity_types = {item.type for item in entities} + has_expense_signal = any( + keyword in compact_query for keyword in EXPENSE_NARRATIVE_KEYWORDS + ) or "expense_type" in entity_types + has_context_signal = bool(time_range.start_date) or "amount" in entity_types + + return has_expense_signal and has_context_signal + + def _parse_with_model( + self, + *, + payload: OntologyParseRequest, + query: str, + compact_query: str, + fallback_scenario: str, + fallback_intent: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + metrics: list[OntologyMetric], + constraints: list[OntologyConstraint], + ) -> LlmOntologyParseResult | None: + messages = self._build_model_messages( + payload=payload, + query=query, + compact_query=compact_query, + fallback_scenario=fallback_scenario, + fallback_intent=fallback_intent, + entities=entities, + time_range=time_range, + metrics=metrics, + constraints=constraints, + ) + response_text = self.runtime_chat_service.complete( + messages, + max_tokens=600, + temperature=0.0, + ) + payload_json = self._extract_json_payload(response_text) + if payload_json is None: + return None + + try: + return LlmOntologyParseResult.model_validate(payload_json) + except ValidationError as exc: + logger.warning("Semantic model output validation failed: %s", exc) + return None + + @staticmethod + def _build_model_messages( + *, + payload: OntologyParseRequest, + query: str, + compact_query: str, + fallback_scenario: str, + fallback_intent: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + metrics: list[OntologyMetric], + constraints: list[OntologyConstraint], + ) -> list[dict[str, str]]: + facts = { + "query": query, + "compact_query": compact_query, + "context": { + "entry_source": payload.context_json.get("entry_source"), + "attachment_names": payload.context_json.get("attachment_names", []), + "attachment_count": payload.context_json.get("attachment_count", 0), + "ocr_summary": payload.context_json.get("ocr_summary", ""), + "ocr_documents": payload.context_json.get("ocr_documents", []), + "request_context": payload.context_json.get("request_context"), + "role_codes": payload.context_json.get("role_codes", []), + "conversation_id": payload.context_json.get("conversation_id"), + "conversation_scenario": payload.context_json.get("conversation_scenario"), + "conversation_intent": payload.context_json.get("conversation_intent"), + "draft_claim_id": payload.context_json.get("draft_claim_id"), + "review_action": payload.context_json.get("review_action"), + "review_form_values": payload.context_json.get("review_form_values"), + "conversation_history": payload.context_json.get("conversation_history", []), + }, + "rule_candidates": { + "scenario": fallback_scenario, + "intent": fallback_intent, + "entities": [item.model_dump(mode="json") for item in entities], + "time_range": time_range.model_dump(mode="json"), + "metrics": [item.model_dump(mode="json") for item in metrics], + "constraints": [item.model_dump(mode="json") for item in constraints], + }, + } + + system_prompt = ( + "你是企业财务共享平台的语义解析器。" + "你的任务是把用户输入解析为固定 JSON,用于后续路由、追问和权限判断。" + "只输出 JSON 对象,不要输出 Markdown、代码块、解释、标题或 。" + "场景 scenario 只能是:expense, accounts_receivable, " + "accounts_payable, knowledge, unknown。" + "意图 intent 只能是:query, explain, compare, risk_check, draft, operate。" + "如果用户是在描述一笔待处理费用、待报销事项、上传票据或希望整理报销," + "即使没有明确说“生成草稿”,也优先使用 expense + draft。" + "如果提供了 conversation_history,必须把最近轮次作为当前追问的上下文," + "正确理解“这个”“那笔”“改成 800”“继续补充”这类省略表达。" + "出现“客户”不等于应收,出现“供应商”不等于应付,必须结合动作词和业务目标判断。" + "只有明确查询、统计、列出、多少、明细、对比时才优先使用 query 或 compare。" + "附件名称和 OCR 摘要只作为辅助证据,不能编造未出现的事实。" + "信息不足时 clarification_required=true,并给出一句简短中文追问。" + "missing_slots 使用简短 snake_case,例如 expense_type, amount, " + "customer_name, participants, attachments。" + "entity_hints 只填写你比较确定的业务对象;如果不确定,可以返回空数组。" + ) + user_prompt = ( + "请根据以下事实输出 JSON:\n" + f"{json.dumps(facts, ensure_ascii=False, indent=2, default=str)}\n\n" + "输出格式:\n" + "{\n" + ' "scenario": "expense",\n' + ' "intent": "draft",\n' + ' "confidence": 0.88,\n' + ' "clarification_required": true,\n' + ' "clarification_question": "请补充客户单位、参与人员和票据附件。",\n' + ' "missing_slots": ["customer_name", "participants", "attachments"],\n' + ' "ambiguity": [],\n' + ' "entity_hints": [\n' + ' {"type": "expense_type", "value": "招待", ' + '"normalized_value": "entertainment", "role": "filter", ' + '"confidence": 0.86}\n' + " ]\n" + "}" + ) + return [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + @staticmethod + def _extract_json_payload(response_text: str | None) -> dict[str, Any] | None: + if not response_text: + return None + + cleaned = re.sub(r".*?", "", response_text, flags=re.DOTALL | re.IGNORECASE) + cleaned = cleaned.strip() + if not cleaned: + return None + + fenced_match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", cleaned, flags=re.DOTALL) + candidates = [fenced_match.group(1)] if fenced_match else [] + candidates.extend([cleaned]) + + start = cleaned.find("{") + end = cleaned.rfind("}") + if start != -1 and end != -1 and end > start: + candidates.append(cleaned[start : end + 1]) + + for candidate in candidates: + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + return parsed + + return None + + @staticmethod + def _resolve_scenario( + fallback_scenario: str, + model_parse: LlmOntologyParseResult | None, + ) -> str: + if model_parse is None: + return fallback_scenario + if model_parse.scenario == "unknown" and fallback_scenario != "unknown": + return fallback_scenario + return model_parse.scenario + + def _resolve_intent( + self, + compact_query: str, + *, + fallback_intent: str, + scenario: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + model_parse: LlmOntologyParseResult | None, + ) -> str: + candidate = model_parse.intent if model_parse is not None else fallback_intent + if scenario == "knowledge": + if candidate in KNOWLEDGE_INTENTS: + return candidate + if fallback_intent in KNOWLEDGE_INTENTS: + return fallback_intent + return "query" + if candidate == "query" and scenario == "expense": + if self._is_generic_expense_prompt(compact_query) or fallback_intent == "draft": + return "draft" + return candidate + + @staticmethod + def _merge_entities( + base_entities: list[OntologyEntity], + entity_hints: list[LlmOntologyEntityHint], + ) -> list[OntologyEntity]: + merged: dict[tuple[str, str], OntologyEntity] = { + (item.type, item.normalized_value): item for item in base_entities + } + + for hint in entity_hints: + value = str(hint.value or "").strip() + if not value: + continue + normalized_value = str(hint.normalized_value or value).strip() + key = (str(hint.type).strip(), normalized_value) + candidate = OntologyEntity( + type=str(hint.type).strip(), + value=value, + normalized_value=normalized_value, + role=str(hint.role or "target").strip() or "target", + confidence=float(hint.confidence), + ) + existing = merged.get(key) + if existing is None or existing.confidence < candidate.confidence: + merged[key] = candidate + + return list(merged.values()) + + @staticmethod + def _normalize_short_text_list(values: list[str]) -> list[str]: + normalized: list[str] = [] + seen: set[str] = set() + for value in values: + cleaned = str(value or "").strip() + if not cleaned or cleaned in seen: + continue + normalized.append(cleaned) + seen.add(cleaned) + return normalized[:6] + + def _infer_default_missing_slots( + self, + compact_query: str, + *, + scenario: str, + intent: str, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + context_json: dict[str, Any], + ) -> list[str]: + if scenario != "expense" or intent != "draft": + return [] + + entity_types = {item.type for item in entities} + attachment_count = int(context_json.get("attachment_count") or 0) + missing_slots: list[str] = [] + + if self._is_generic_expense_prompt(compact_query): + if "expense_type" not in entity_types: + missing_slots.append("expense_type") + if "amount" not in entity_types: + missing_slots.append("amount") + if not time_range.start_date: + missing_slots.append("time_range") + missing_slots.append("reason") + if attachment_count <= 0: + missing_slots.append("attachments") + return missing_slots + + if any( + item.normalized_value == "entertainment" + for item in entities + if item.type == "expense_type" + ): + if "customer" not in entity_types: + missing_slots.append("customer_name") + missing_slots.append("participants") + if attachment_count <= 0: + missing_slots.append("attachments") + + return missing_slots + + @staticmethod + def _resolve_confidence( + *, + model_confidence: float | None, + fallback_confidence: float, + clarification_required: bool, + permission: OntologyPermission, + ) -> float: + confidence = fallback_confidence if model_confidence is None else float(model_confidence) + confidence = max(0.0, min(confidence, 0.98)) + if permission.level == AgentPermissionLevel.FORBIDDEN.value: + confidence = max(confidence, 0.86) + if clarification_required and permission.level != AgentPermissionLevel.FORBIDDEN.value: + confidence = min(confidence, 0.58) + return round(confidence, 2) + + def _extract_entities( + self, + query: str, + compact_query: str, + reference: ReferenceCatalog, + ) -> list[OntologyEntity]: + entities: dict[tuple[str, str], OntologyEntity] = {} + + def upsert(entity: OntologyEntity) -> None: + key = (entity.type, entity.normalized_value) + if key not in entities: + entities[key] = entity + + for match in re.finditer(r"客户\s*([A-Za-z0-9一二三四五六七八九十]+)", query): + suffix = match.group(1).strip() + normalized = f"客户{suffix}".replace(" ", "") + upsert(self._make_entity("customer", match.group(0).strip(), normalized, role="filter")) + labeled_customer_match = re.search(r"客户名称[::]\s*(?P[^\n,。;]+)", query) + if labeled_customer_match: + customer_name = labeled_customer_match.group("name").strip() + upsert(self._make_entity("customer", customer_name, customer_name, role="filter")) + + for match in re.finditer(r"供应商\s*([A-Za-z0-9一二三四五六七八九十]+)", query): + suffix = match.group(1).strip() + normalized = f"供应商{suffix}".replace(" ", "") + upsert(self._make_entity("vendor", match.group(0).strip(), normalized, role="filter")) + + employee_match = re.search( + r"(?P[赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦许何吕施张孔曹严华金魏陶姜" + r"戚谢邹喻柏水窦章云苏潘葛范彭郎鲁韦昌马苗凤花方俞任袁柳鲍史唐费廉岑" + r"薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅卞康伍余元卜顾孟平黄和穆萧尹姚邵" + r"湛汪祁毛禹狄米贝明臧计成戴宋庞熊纪舒屈项祝董梁杜阮蓝闵席季强贾路江" + r"童颜郭梅盛林钟徐邱骆高夏蔡田樊胡凌霍虞万支柯管卢莫房裘缪解应宗丁宣" + r"邓洪包左石崔吉龚程嵇邢裴陆荣翁荀羊惠甄曲家封芮储靳汲邴糜松井段富巫" + r"乌焦巴弓牧隗山谷车侯伊宫宁仇栾刘景詹束龙叶司黎薄印白怀蒲邰从鄂索咸" + r"籍卓蔺屠蒙池乔阴胥能苍双闻莘党翟谭贡姬申扶堵冉宰郦雍桑桂牛寿通边扈" + r"燕冀浦尚农温别庄晏柴瞿阎连茹习艾容向古易慎戈廖庾终暨居衡步都耿满弘" + r"匡国文寇广禄阙东欧殳沃利蔚越夔隆师巩聂晁勾敖融冷辛阚那简饶曾关蒯相" + r"查后荆游竺权盖益桓公][\u4e00-\u9fa5]{1,2})(?=\s*(?:\d{4}年|\d{1,2}月|本月|" + r"上月|本周|报销|差旅|费用|申请))", + query, + ) + if employee_match: + name = employee_match.group("name") + upsert(self._make_entity("employee", name, name, role="filter")) + + for name in reference.employees: + if self._compact(name) in compact_query: + upsert(self._make_entity("employee", name, name, role="filter")) + for name in reference.departments: + if self._compact(name) in compact_query: + upsert(self._make_entity("department", name, name, role="filter")) + for name in reference.customers: + if self._compact(name) in compact_query: + upsert(self._make_entity("customer", name, name, role="filter")) + for name in reference.vendors: + if self._compact(name) in compact_query: + upsert(self._make_entity("vendor", name, name, role="filter")) + for code in reference.projects: + if self._compact(code) in compact_query: + upsert(self._make_entity("project", code, code, role="filter")) + + for code in re.findall(r"PRJ-[A-Z]+-\d+", query, flags=re.IGNORECASE): + upsert(self._make_entity("project", code, code.upper(), role="filter")) + for code in re.findall(r"EXP-\d{6}-\d{3}", query, flags=re.IGNORECASE): + upsert(self._make_entity("expense_claim", code, code.upper())) + for code in re.findall(r"AR-\d{6}-\d{3}", query, flags=re.IGNORECASE): + upsert(self._make_entity("receivable", code, code.upper())) + for code in re.findall(r"AP-\d{6}-\d{3}", query, flags=re.IGNORECASE): + upsert(self._make_entity("payable", code, code.upper())) + for code in re.findall(r"INV-[A-Z]+-\d+", query, flags=re.IGNORECASE): + upsert(self._make_entity("invoice", code, code.upper())) + for code in re.findall(r"CTR-[A-Z]+-\d+", query, flags=re.IGNORECASE): + upsert(self._make_entity("contract", code, code.upper())) + + for label, normalized in EXPENSE_TYPE_KEYWORDS.items(): + if label in query: + upsert(self._make_entity("expense_type", label, normalized, role="filter")) + + has_customer_entertainment_signal = "客户" in query and any( + keyword in query for keyword in ("吃饭", "用餐", "餐饮", "宴请", "请客", "招待") + ) + if has_customer_entertainment_signal: + upsert( + self._make_entity( + "expense_type", + "客户招待", + "entertainment", + role="filter", + confidence=0.96, + ) + ) + + if any(keyword in query for keyword in ("打车", "网约车", "出租车", "车费", "停车费", "过路费")): + upsert(self._make_entity("expense_type", "交通", "transport", role="filter", confidence=0.9)) + + if any(keyword in query for keyword in ("出差", "机票", "火车", "高铁", "行程单")): + upsert(self._make_entity("expense_type", "差旅", "travel", role="filter", confidence=0.88)) + + if any(keyword in query for keyword in ("酒店", "住宿", "宾馆")): + upsert(self._make_entity("expense_type", "住宿", "hotel", role="filter", confidence=0.86)) + + if ( + not has_customer_entertainment_signal + and any(keyword in query for keyword in ("餐费", "用餐", "午餐", "晚餐", "早餐", "餐饮")) + ): + upsert(self._make_entity("expense_type", "餐费", "meal", role="filter", confidence=0.84)) + + if any( + keyword in query + for keyword in ("办公用品", "文具", "耗材", "办公耗材", "打印纸", "办公设备", "键盘", "鼠标", "白板") + ): + upsert(self._make_entity("expense_type", "办公费", "office", role="filter", confidence=0.87)) + + if any(keyword in query for keyword in ("培训", "讲师费", "课时费", "课程费")): + upsert(self._make_entity("expense_type", "培训费", "training", role="filter", confidence=0.84)) + + if any(keyword in query for keyword in ("通讯费", "话费", "流量费", "宽带费")): + upsert(self._make_entity("expense_type", "通讯费", "communication", role="filter", confidence=0.84)) + + if any(keyword in query for keyword in ("福利费", "团建", "慰问", "节日福利", "体检费")): + upsert(self._make_entity("expense_type", "福利费", "welfare", role="filter", confidence=0.84)) + + for amount in self._extract_amount_entities(query): + upsert(amount) + + return list(entities.values()) + + def _extract_amount_entities(self, query: str) -> list[OntologyEntity]: + entities: list[OntologyEntity] = [] + for match in AMOUNT_PATTERN.finditer(query): + raw_value = match.group("value") + unit = match.group("unit") + prefix = match.group("prefix") + if raw_value is None: + continue + if prefix is None and unit is None: + continue + + amount_value = self._normalize_amount(raw_value, unit) + display_value = f"{raw_value}{unit or ''}" + role = "threshold" if prefix else "target" + entities.append( + self._make_entity( + "amount", + display_value, + str(amount_value), + role=role, + confidence=0.9, + ) + ) + return entities + + @staticmethod + def _make_entity( + entity_type: str, + value: str, + normalized_value: str, + *, + role: str = "target", + confidence: float = 0.92, + ) -> OntologyEntity: + return OntologyEntity( + type=entity_type, + value=value, + normalized_value=normalized_value, + role=role, + confidence=confidence, + ) + + @staticmethod + def _infer_scenario_from_entities(entities: list[OntologyEntity]) -> str | None: + entity_types = {item.type for item in entities} + if entity_types & {"vendor", "payable"}: + return "accounts_payable" + if entity_types & {"customer", "receivable", "contract"}: + return "accounts_receivable" + if entity_types & {"employee", "expense_claim", "expense_type"}: + return "expense" + return None + + def _extract_time_range( + self, + query: str, + compact_query: str, + *, + context_json: dict[str, Any], + ) -> tuple[OntologyTimeRange, float]: + today = self._resolve_reference_today(context_json) + + direct_mappings = [ + ("大前天", self._single_day_range(today - timedelta(days=3), "大前天", "day")), + ("前天", self._single_day_range(today - timedelta(days=2), "前天", "day")), + ("昨日", self._single_day_range(today - timedelta(days=1), "昨日", "day")), + ("昨天", self._single_day_range(today - timedelta(days=1), "昨天", "day")), + ("今天", self._single_day_range(today, "今天", "day")), + ("明天", self._single_day_range(today + timedelta(days=1), "明天", "day")), + ("后天", self._single_day_range(today + timedelta(days=2), "后天", "day")), + ("大后天", self._single_day_range(today + timedelta(days=3), "大后天", "day")), + ] + for keyword, value in direct_mappings: + if keyword in query: + return value, 0.10 + + if "本周" in query or "这周" in query or "本星期" in query: + start = today - timedelta(days=today.weekday()) + end = start + timedelta(days=6) + return self._range(start, end, "本周", "week"), 0.10 + if "上周" in query: + end = today - timedelta(days=today.weekday() + 1) + start = end - timedelta(days=6) + return self._range(start, end, "上周", "week"), 0.10 + if "本月" in query or "这个月" in query: + start = date(today.year, today.month, 1) + end = date(today.year, today.month, calendar.monthrange(today.year, today.month)[1]) + return self._range(start, end, "本月", "month"), 0.10 + if "上月" in query: + year = today.year if today.month > 1 else today.year - 1 + month = today.month - 1 if today.month > 1 else 12 + start = date(year, month, 1) + end = date(year, month, calendar.monthrange(year, month)[1]) + return self._range(start, end, "上月", "month"), 0.10 + if "本季度" in query or "这个季度" in query: + quarter = (today.month - 1) // 3 + start_month = quarter * 3 + 1 + end_month = start_month + 2 + start = date(today.year, start_month, 1) + end = date(today.year, end_month, calendar.monthrange(today.year, end_month)[1]) + return self._range(start, end, "本季度", "quarter"), 0.10 + if "今年" in query: + return ( + self._range(date(today.year, 1, 1), date(today.year, 12, 31), "今年", "year"), + 0.10, + ) + + match = DATE_RANGE_PATTERN.search(query) + if match: + start = self._parse_iso_date(match.group("start")) + end = self._parse_iso_date(match.group("end")) + if start and end: + return self._range(start, end, match.group(0), "custom"), 0.10 + + match = EXPLICIT_DATE_PATTERN.search(query) + if match: + explicit = date( + int(match.group("year")), + int(match.group("month")), + int(match.group("day")), + ) + return self._single_day_range(explicit, match.group(0), "day"), 0.10 + + match = EXPLICIT_MONTH_PATTERN.search(query) + if match: + year = int(match.group("year")) + month = int(match.group("month")) + start = date(year, month, 1) + end = date(year, month, calendar.monthrange(year, month)[1]) + return self._range(start, end, match.group(0), "month"), 0.10 + + match = MONTH_DAY_RANGE_PATTERN.search(query) + if match: + start = date(today.year, int(match.group("start_month")), int(match.group("start_day"))) + end = date(today.year, int(match.group("end_month")), int(match.group("end_day"))) + return self._range(start, end, match.group(0), "custom"), 0.10 + + match = MONTH_DAY_PATTERN.search(compact_query) + if match: + explicit = date(today.year, int(match.group("month")), int(match.group("day"))) + return self._single_day_range(explicit, match.group(0), "day"), 0.08 + + month_match = re.search(r"(?P\d{1,2})月", compact_query) + if month_match: + month = int(month_match.group("month")) + start = date(today.year, month, 1) + end = date(today.year, month, calendar.monthrange(today.year, month)[1]) + return self._range(start, end, month_match.group(0), "month"), 0.08 + + return OntologyTimeRange(), 0.0 + + @staticmethod + def _resolve_reference_today(context_json: dict[str, Any]) -> date: + client_now_iso = str(context_json.get("client_now_iso") or "").strip() + if not client_now_iso: + return datetime.now(UTC).date() + + normalized = client_now_iso.replace("Z", "+00:00") + try: + client_now = datetime.fromisoformat(normalized) + except ValueError: + return datetime.now(UTC).date() + + if client_now.tzinfo is None: + client_now = client_now.replace(tzinfo=UTC) + + try: + offset_minutes = int(context_json.get("client_timezone_offset_minutes") or 0) + except (TypeError, ValueError): + offset_minutes = 0 + + local_now = client_now - timedelta(minutes=offset_minutes) + return local_now.date() + + @staticmethod + def _single_day_range(target: date, raw: str, granularity: str) -> OntologyTimeRange: + return OntologyTimeRange( + raw=raw, + start_date=target.isoformat(), + end_date=target.isoformat(), + granularity=granularity, + ) + + @staticmethod + def _range(start: date, end: date, raw: str, granularity: str) -> OntologyTimeRange: + return OntologyTimeRange( + raw=raw, + start_date=start.isoformat(), + end_date=end.isoformat(), + granularity=granularity, + ) + + @staticmethod + def _parse_iso_date(value: str) -> date | None: + try: + return date.fromisoformat(value) + except ValueError: + return None + + def _extract_metrics(self, compact_query: str) -> list[OntologyMetric]: + metrics: dict[str, OntologyMetric] = {} + + def upsert(metric: OntologyMetric) -> None: + metrics[metric.name] = metric + + if any( + keyword in compact_query + for keyword in ("多少钱", "金额", "总额", "支出", "回款", "应收", "应付") + ): + upsert(OntologyMetric(name="amount", aggregation="sum", unit="CNY")) + if any(keyword in compact_query for keyword in ("多少笔", "几笔", "数量", "条数", "单数")): + upsert(OntologyMetric(name="count", aggregation="count", unit="records")) + if "超标" in compact_query or "超预算" in compact_query: + upsert(OntologyMetric(name="amount_over_limit")) + if "逾期" in compact_query or "账龄" in compact_query: + upsert(OntologyMetric(name="overdue")) + if "重复" in compact_query: + upsert(OntologyMetric(name="duplicate_expense")) + + top_match = TOP_N_PATTERN.search(compact_query) + if top_match: + metrics["amount"] = OntologyMetric( + name="amount", + aggregation="sum", + unit="CNY", + sort="desc" if "最低" not in compact_query else "asc", + top_n=int(top_match.group("top")), + ) + + return list(metrics.values()) + + def _extract_constraints( + self, + compact_query: str, + entities: list[OntologyEntity], + ) -> list[OntologyConstraint]: + constraints: dict[tuple[str, str, str, str | None], OntologyConstraint] = {} + + def upsert(constraint: OntologyConstraint) -> None: + key = ( + constraint.field, + constraint.operator, + str(constraint.value), + constraint.currency, + ) + if key not in constraints: + constraints[key] = constraint + + for entity in entities: + if entity.type in { + "employee", + "department", + "customer", + "vendor", + "project", + "expense_type", + }: + upsert( + OntologyConstraint( + field=entity.type, + operator="=", + value=entity.normalized_value, + ) + ) + + for keyword, normalized in STATUS_KEYWORDS.items(): + if keyword in compact_query: + upsert(OntologyConstraint(field="status", operator="=", value=normalized)) + + for amount_match in AMOUNT_PATTERN.finditer(compact_query): + if not amount_match.group("prefix"): + continue + + operator = self._normalize_operator(amount_match.group("prefix")) + value = self._normalize_amount(amount_match.group("value"), amount_match.group("unit")) + upsert( + OntologyConstraint( + field="amount", + operator=operator, + value=value, + currency="CNY", + ) + ) + break + + top_match = TOP_N_PATTERN.search(compact_query) + if top_match: + top_n = int(top_match.group("top")) + upsert(OntologyConstraint(field="top_n", operator="=", value=top_n)) + upsert( + OntologyConstraint( + field="sort_by", + operator="desc" if "最低" not in compact_query else "asc", + value="amount", + ) + ) + + return list(constraints.values()) + + def _extract_risk_flags(self, compact_query: str, scenario: str) -> list[str]: + risk_flags: list[str] = [] + + def append(flag: str) -> None: + if flag not in risk_flags: + risk_flags.append(flag) + + if "重复" in compact_query: + append("duplicate_expense") + if any( + keyword in compact_query + for keyword in ("发票异常", "票据异常", "验真失败", "附件缺失", "补件") + ): + append("invoice_anomaly") + if any(keyword in compact_query for keyword in ("超标", "超预算", "超限")): + append("amount_over_limit") + if scenario == "accounts_receivable" and any( + keyword in compact_query for keyword in ("逾期", "账龄", "欠款", "未回款") + ): + append("ar_overdue") + if scenario == "accounts_payable" and any( + keyword in compact_query for keyword in ("逾期", "待付", "付款风险", "未付款") + ): + append("ap_overdue") + + return risk_flags + + def _resolve_permission( + self, + compact_query: str, + context_json: dict, + intent: str, + ) -> OntologyPermission: + role_codes = { + str(item).strip().lower() + for item in context_json.get("role_codes", []) + if str(item).strip() + } + is_admin = bool(context_json.get("is_admin")) + privileged = is_admin or bool(role_codes & PRIVILEGED_ROLE_CODES) + + if intent in {"query", "explain", "compare", "risk_check"}: + return OntologyPermission( + level=AgentPermissionLevel.READ.value, + allowed=True, + reason="只读查询。", + ) + if intent == "draft": + return OntologyPermission( + level=AgentPermissionLevel.DRAFT_WRITE.value, + allowed=True, + reason="允许生成草稿,但不会直接提交业务动作。", + ) + + if any(keyword in compact_query for keyword in OPERATE_KEYWORDS) or "付款" in compact_query: + if privileged: + return OntologyPermission( + level=AgentPermissionLevel.APPROVAL_REQUIRED.value, + allowed=False, + reason="涉及付款、审批或上线动作,必须进入人工审批链。", + ) + return OntologyPermission( + level=AgentPermissionLevel.FORBIDDEN.value, + allowed=False, + reason="当前账号缺少财务或审批权限,只能查看结果或生成草稿。", + ) + + return OntologyPermission( + level=AgentPermissionLevel.APPROVAL_REQUIRED.value, + allowed=False, + reason="操作类请求需要人工审批确认。", + ) + + def _build_field_errors( + self, + *, + scenario: str, + intent: str, + entities: list[OntologyEntity], + permission: OntologyPermission, + missing_slots: list[str], + ambiguity: list[str], + ) -> list[OntologyFieldError]: + errors: list[OntologyFieldError] = [] + if scenario == "unknown": + errors.append( + OntologyFieldError( + field="scenario", + code="scenario_unknown", + message="未识别出明确业务场景,请补充是报销、应收、应付还是制度问题。", + ) + ) + if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2: + errors.append( + OntologyFieldError( + field="entities", + code="compare_target_missing", + message="对比类问题请至少给出两个对象,或给出更明确的对比范围。", + ) + ) + if missing_slots: + errors.append( + OntologyFieldError( + field="missing_slots", + code="required_slot_missing", + message=( + "继续处理前还缺少关键信息:" + f"{'、'.join(self._display_slot_label(item) for item in missing_slots)}。" + ), + ) + ) + if ambiguity: + errors.append( + OntologyFieldError( + field="ambiguity", + code="ambiguity_detected", + message=f"当前问题存在歧义:{';'.join(ambiguity)}。", + ) + ) + if permission.level == AgentPermissionLevel.FORBIDDEN.value: + errors.append( + OntologyFieldError( + field="permission", + code="permission_forbidden", + message=permission.reason, + ) + ) + return errors + + def _build_clarification( + self, + *, + scenario: str, + intent: str, + entities: list[OntologyEntity], + permission: OntologyPermission, + missing_slots: list[str], + ambiguity: list[str], + allow_incomplete_draft: bool, + model_clarification_required: bool, + model_clarification_question: str | None, + ) -> tuple[bool, str | None]: + if permission.level == AgentPermissionLevel.FORBIDDEN.value: + return True, "当前动作超出权限范围。是否改为生成草稿或建议?" + if scenario == "knowledge" and intent in {"query", "explain"}: + return False, None + if model_clarification_required: + question = str(model_clarification_question or "").strip() + if question: + return True, question + if missing_slots: + return True, self._build_missing_slot_question(missing_slots) + if ambiguity: + return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。" + if scenario == "unknown": + return True, "请说明这是报销、应收、应付,还是制度知识问题?" + if intent == "compare" and len([item for item in entities if item.type != "amount"]) < 2: + return True, "请补充需要对比的两个对象,例如两个客户、两个供应商或两个员工。" + if allow_incomplete_draft and scenario == "expense" and intent == "draft": + return False, None + if missing_slots: + return True, self._build_missing_slot_question(missing_slots) + if ambiguity: + return True, f"当前问题存在歧义,请进一步说明:{';'.join(ambiguity)}。" + return False, None + + @staticmethod + def _allow_incomplete_draft( + context_json: dict[str, Any], + *, + scenario: str, + intent: str, + ) -> bool: + if scenario != "expense" or intent != "draft": + return False + return str(context_json.get("review_action") or "").strip() == "save_draft" + + @staticmethod + def _display_slot_label(slot: str) -> str: + return MISSING_SLOT_LABELS.get(slot, slot) + + def _build_missing_slot_question(self, missing_slots: list[str]) -> str: + labels = [self._display_slot_label(item) for item in missing_slots[:4]] + if not labels: + return "请补充更多上下文后再继续。" + return f"请补充{'、'.join(labels)},我再继续帮你解析和处理。" + + @staticmethod + def _compute_confidence( + *, + scenario: str, + scenario_score: float, + intent_score: float, + entities: list[OntologyEntity], + time_range: OntologyTimeRange, + metrics: list[OntologyMetric], + constraints: list[OntologyConstraint], + risk_flags: list[str], + clarification_required: bool, + permission: OntologyPermission, + ) -> float: + confidence = 0.18 + scenario_score + intent_score + confidence += min(0.16, len(entities) * 0.04) + if time_range.start_date: + confidence += 0.10 + if metrics: + confidence += 0.06 + if constraints: + confidence += 0.06 + if risk_flags: + confidence += 0.08 + if permission.level == AgentPermissionLevel.FORBIDDEN.value: + confidence = max(confidence, 0.86) + + if scenario == "unknown": + confidence = min(confidence, 0.45) + if clarification_required and permission.level != AgentPermissionLevel.FORBIDDEN.value: + confidence = min(confidence, 0.58) + + return round(min(confidence, 0.98), 2) + + @staticmethod + def _build_result_summary( + scenario: str, + intent: str, + permission_level: str, + confidence: float, + ) -> str: + return ( + f"语义解析完成:scenario={scenario}, intent={intent}, " + f"permission={permission_level}, confidence={confidence:.2f}" + ) + + @staticmethod + def _normalize_operator(value: str) -> str: + mapping = { + "超过": ">", + "大于": ">", + "高于": ">", + ">": ">", + ">=": ">=", + "不少于": ">=", + "不低于": ">=", + "小于": "<", + "低于": "<", + "少于": "<", + "<": "<", + "<=": "<=", + "至多": "<=", + "不超过": "<=", + "=": "=", + "=": "=", + } + return mapping.get(value, value) + + @staticmethod + def _normalize_amount(raw_value: str | None, unit: str | None) -> int | float: + numeric = float(raw_value or 0) + if unit in {"万", "万元"}: + numeric *= 10000 + return int(numeric) if numeric.is_integer() else round(numeric, 2) diff --git a/server/src/app/services/user_agent.py b/server/src/app/services/user_agent.py index 22f3741..357fad0 100644 --- a/server/src/app/services/user_agent.py +++ b/server/src/app/services/user_agent.py @@ -177,16 +177,16 @@ SLOT_LABELS = { } DATE_TEXT_PATTERN = re.compile(r"(\d{4}[年/-]\d{1,2}[月/-]\d{1,2}日?)") -AMOUNT_TEXT_PATTERN = re.compile( - r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)" -) +AMOUNT_TEXT_PATTERN = re.compile( + r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)" +) DOCUMENT_AMOUNT_PATTERN = re.compile( r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)" r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)" ) DOCUMENT_CURRENCY_AMOUNT_PATTERN = re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)") -SOURCE_LABELS = { +SOURCE_LABELS = { "user_text": "用户描述", "user_form": "用户修改", "ocr": "票据识别", @@ -215,7 +215,7 @@ INFERRED_REASON_LABELS = { "welfare": "员工福利", "other": "其他费用", } -SYSTEM_GENERATED_REASON_PREFIXES = ( +SYSTEM_GENERATED_REASON_PREFIXES = ( "我上传了", "请按当前已识别信息", "请把当前上传的票据", @@ -225,20 +225,20 @@ SYSTEM_GENERATED_REASON_PREFIXES = ( "我已修改识别信息", "查看报销草稿", "请解释一下当前这笔报销的合规风险和待补充项", -) -AMOUNT_UNIT_ALIASES = { - "员": "元", - "圆": "元", - "园": "元", - "块": "元", - "块钱": "元", - "元整": "元", - "万员": "万元", - "万圆": "万元", - "万园": "万元", - "万块": "万元", - "万元整": "万元", -} +) +AMOUNT_UNIT_ALIASES = { + "员": "元", + "圆": "元", + "园": "元", + "块": "元", + "块钱": "元", + "元整": "元", + "万员": "万元", + "万圆": "万元", + "万园": "万元", + "万块": "万元", + "万元整": "万元", +} class UserAgentService: @@ -1742,7 +1742,7 @@ class UserAgentService: if is_submitted: body = ( f"主题:{subject}\n" - f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n" + f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n" "建议:后续可在个人报销列表中跟踪审批进度,必要时再补充说明或附件。\n" f"原始问题:{payload.message}" ) @@ -2381,7 +2381,7 @@ class UserAgentService: if review_action == "next_step": if draft_payload is not None and draft_payload.status == "submitted": stage_text = draft_payload.approval_stage or "审批中" - return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip() + return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip() if payload.tool_payload.get("submission_blocked"): return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。" return ( @@ -2947,19 +2947,19 @@ class UserAgentService: "expense_type_code": "", } participants: list[str] = [] - for item in payload.ontology.entities: - if item.type == "employee" and not values["employee_name"]: - values["employee_name"] = item.value - elif item.type == "customer" and not values["customer"]: - values["customer"] = item.value - elif item.type == "amount" and item.role != "threshold" and not values["amount"]: - normalized_amount = str(item.normalized_value or "").strip() - values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value - elif item.type == "expense_type" and not values["expense_type_code"]: - values["expense_type_code"] = item.normalized_value - values["expense_type"] = EXPENSE_TYPE_LABELS.get( - item.normalized_value, - item.value, + for item in payload.ontology.entities: + if item.type == "employee" and not values["employee_name"]: + values["employee_name"] = item.value + elif item.type == "customer" and not values["customer"]: + values["customer"] = item.value + elif item.type == "amount" and item.role != "threshold" and not values["amount"]: + normalized_amount = str(item.normalized_value or "").strip() + values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value + elif item.type == "expense_type" and not values["expense_type_code"]: + values["expense_type_code"] = item.normalized_value + values["expense_type"] = EXPENSE_TYPE_LABELS.get( + item.normalized_value, + item.value, ) elif item.type in {"participant", "person"} and item.value.strip(): participants.append(item.value.strip()) @@ -3189,7 +3189,24 @@ class UserAgentService: evidence="来源于用户修改后的结构化表单。", ) + inferred_reason = self._infer_reason_from_claim_groups( + claim_groups=claim_groups, + ) reason_value = self._resolve_reason_text(self._resolve_reason_source_text(payload)) + if inferred_reason: + return self._build_slot_value( + value=inferred_reason, + raw_value=reason_value or inferred_reason, + normalized_value=inferred_reason, + source="ocr", + confidence=0.82, + evidence=( + "系统已根据票据识别结果预置场景类型;原始描述仍保留为补充说明。" + if reason_value + else "系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。" + ), + ) + if reason_value: return self._build_slot_value( value=reason_value, @@ -3199,19 +3216,6 @@ class UserAgentService: confidence=0.76, evidence="系统从用户原始描述中提取了本次费用事由,建议继续核对。", ) - - inferred_reason = self._infer_reason_from_claim_groups( - claim_groups=claim_groups, - ) - if inferred_reason: - return self._build_slot_value( - value=inferred_reason, - raw_value=inferred_reason, - normalized_value=inferred_reason, - source="ocr", - confidence=0.68, - evidence="系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。", - ) return self._build_slot_value() def _build_amount_slot( @@ -3358,17 +3362,17 @@ class UserAgentService: return self._build_slot_value() @staticmethod - def _normalize_amount_text(value: str) -> str: - cleaned = str(value or "").strip() - if not cleaned: - return "" - for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True): - cleaned = cleaned.replace(alias, canonical) - match = AMOUNT_TEXT_PATTERN.search(cleaned) - if not match: - return cleaned - number = float(match.group(1)) - return f"{number:.2f}元" + def _normalize_amount_text(value: str) -> str: + cleaned = str(value or "").strip() + if not cleaned: + return "" + for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True): + cleaned = cleaned.replace(alias, canonical) + match = AMOUNT_TEXT_PATTERN.search(cleaned) + if not match: + return cleaned + number = float(match.group(1)) + return f"{number:.2f}元" @staticmethod def _normalize_expense_type_input(value: str) -> tuple[str, str]: diff --git a/server/src/x_financial_server.egg-info/SOURCES.txt b/server/src/x_financial_server.egg-info/SOURCES.txt index 1a0f5a3..c7b0476 100644 --- a/server/src/x_financial_server.egg-info/SOURCES.txt +++ b/server/src/x_financial_server.egg-info/SOURCES.txt @@ -1,139 +1,139 @@ -README.md -pyproject.toml -src/app/__init__.py -src/app/main.py -src/app/api/__init__.py -src/app/api/deps.py -src/app/api/router.py -src/app/api/v1/__init__.py -src/app/api/v1/router.py -src/app/api/v1/endpoints/__init__.py -src/app/api/v1/endpoints/agent_assets.py -src/app/api/v1/endpoints/agent_runs.py -src/app/api/v1/endpoints/audit_logs.py -src/app/api/v1/endpoints/auth.py -src/app/api/v1/endpoints/bootstrap.py -src/app/api/v1/endpoints/employees.py -src/app/api/v1/endpoints/health.py -src/app/api/v1/endpoints/knowledge.py -src/app/api/v1/endpoints/ocr.py -src/app/api/v1/endpoints/ontology.py -src/app/api/v1/endpoints/orchestrator.py -src/app/api/v1/endpoints/reimbursements.py -src/app/api/v1/endpoints/settings.py -src/app/api/v1/endpoints/system_logs.py -src/app/core/__init__.py -src/app/core/admin_secret.py -src/app/core/agent_enums.py -src/app/core/bootstrap.py -src/app/core/config.py -src/app/core/logging.py -src/app/core/openapi.py -src/app/core/secret_box.py -src/app/core/security.py -src/app/db/__init__.py -src/app/db/base.py -src/app/db/base_class.py -src/app/db/session.py -src/app/middleware/__init__.py -src/app/middleware/logging.py -src/app/models/__init__.py -src/app/models/agent_asset.py -src/app/models/agent_conversation.py -src/app/models/agent_run.py -src/app/models/approval.py -src/app/models/audit_log.py -src/app/models/employee.py -src/app/models/employee_change_log.py -src/app/models/financial_record.py -src/app/models/organization.py -src/app/models/reimbursement.py -src/app/models/role.py -src/app/models/system_model_setting.py -src/app/models/system_setting.py -src/app/models/system_setting_secret.py -src/app/repositories/__init__.py -src/app/repositories/agent_asset.py -src/app/repositories/agent_run.py -src/app/repositories/audit_log.py -src/app/repositories/employee.py -src/app/repositories/reimbursement.py -src/app/repositories/settings.py -src/app/schemas/__init__.py -src/app/schemas/agent_asset.py -src/app/schemas/agent_run.py -src/app/schemas/audit_log.py -src/app/schemas/auth.py -src/app/schemas/bootstrap.py -src/app/schemas/common.py -src/app/schemas/employee.py -src/app/schemas/knowledge.py -src/app/schemas/ocr.py -src/app/schemas/ontology.py -src/app/schemas/orchestrator.py -src/app/schemas/reimbursement.py -src/app/schemas/settings.py -src/app/schemas/system_log.py -src/app/schemas/user_agent.py -src/app/services/__init__.py -src/app/services/agent_asset_spreadsheet.py -src/app/services/agent_assets.py -src/app/services/agent_conversations.py -src/app/services/agent_foundation.py -src/app/services/agent_runs.py -src/app/services/audit.py -src/app/services/auth.py -src/app/services/document_intelligence.py -src/app/services/employee.py -src/app/services/employee_seed.py -src/app/services/expense_claims.py -src/app/services/expense_rule_runtime.py -src/app/services/hermes_sync.py -src/app/services/knowledge.py -src/app/services/knowledge_index_tasks.py -src/app/services/knowledge_normalizer.py -src/app/services/knowledge_rag.py -src/app/services/knowledge_scheduler.py -src/app/services/knowledge_sync.py -src/app/services/model_connectivity.py -src/app/services/ocr.py -src/app/services/ontology.py -src/app/services/orchestrator.py -src/app/services/reimbursement.py -src/app/services/runtime_chat.py -src/app/services/settings.py -src/app/services/system_hermes.py -src/app/services/system_logs.py -src/app/services/user_agent.py -src/x_financial_server.egg-info/PKG-INFO -src/x_financial_server.egg-info/SOURCES.txt -src/x_financial_server.egg-info/dependency_links.txt -src/x_financial_server.egg-info/requires.txt -src/x_financial_server.egg-info/top_level.txt -tests/test_agent_asset_onlyoffice_key.py -tests/test_agent_asset_service.py -tests/test_agent_asset_spreadsheet_import.py -tests/test_agent_foundation_endpoints.py -tests/test_agent_runs_service.py -tests/test_auth_service.py -tests/test_config_settings_reload.py -tests/test_document_intelligence.py -tests/test_employee_service.py -tests/test_env_file_precedence.py -tests/test_expense_claim_service.py -tests/test_imports.py -tests/test_knowledge_normalizer.py -tests/test_knowledge_onlyoffice_config.py -tests/test_knowledge_rag_service.py -tests/test_knowledge_service.py -tests/test_ocr_endpoints.py -tests/test_ocr_service.py -tests/test_ontology_service.py -tests/test_openapi_schema.py -tests/test_reimbursement_endpoints.py -tests/test_runtime_chat_service.py -tests/test_server_start_dependencies.py -tests/test_settings_persistence.py -tests/test_settings_service.py -tests/test_system_logs_service.py +README.md +pyproject.toml +src/app/__init__.py +src/app/main.py +src/app/api/__init__.py +src/app/api/deps.py +src/app/api/router.py +src/app/api/v1/__init__.py +src/app/api/v1/router.py +src/app/api/v1/endpoints/__init__.py +src/app/api/v1/endpoints/agent_assets.py +src/app/api/v1/endpoints/agent_runs.py +src/app/api/v1/endpoints/audit_logs.py +src/app/api/v1/endpoints/auth.py +src/app/api/v1/endpoints/bootstrap.py +src/app/api/v1/endpoints/employees.py +src/app/api/v1/endpoints/health.py +src/app/api/v1/endpoints/knowledge.py +src/app/api/v1/endpoints/ocr.py +src/app/api/v1/endpoints/ontology.py +src/app/api/v1/endpoints/orchestrator.py +src/app/api/v1/endpoints/reimbursements.py +src/app/api/v1/endpoints/settings.py +src/app/api/v1/endpoints/system_logs.py +src/app/core/__init__.py +src/app/core/admin_secret.py +src/app/core/agent_enums.py +src/app/core/bootstrap.py +src/app/core/config.py +src/app/core/logging.py +src/app/core/openapi.py +src/app/core/secret_box.py +src/app/core/security.py +src/app/db/__init__.py +src/app/db/base.py +src/app/db/base_class.py +src/app/db/session.py +src/app/middleware/__init__.py +src/app/middleware/logging.py +src/app/models/__init__.py +src/app/models/agent_asset.py +src/app/models/agent_conversation.py +src/app/models/agent_run.py +src/app/models/approval.py +src/app/models/audit_log.py +src/app/models/employee.py +src/app/models/employee_change_log.py +src/app/models/financial_record.py +src/app/models/organization.py +src/app/models/reimbursement.py +src/app/models/role.py +src/app/models/system_model_setting.py +src/app/models/system_setting.py +src/app/models/system_setting_secret.py +src/app/repositories/__init__.py +src/app/repositories/agent_asset.py +src/app/repositories/agent_run.py +src/app/repositories/audit_log.py +src/app/repositories/employee.py +src/app/repositories/reimbursement.py +src/app/repositories/settings.py +src/app/schemas/__init__.py +src/app/schemas/agent_asset.py +src/app/schemas/agent_run.py +src/app/schemas/audit_log.py +src/app/schemas/auth.py +src/app/schemas/bootstrap.py +src/app/schemas/common.py +src/app/schemas/employee.py +src/app/schemas/knowledge.py +src/app/schemas/ocr.py +src/app/schemas/ontology.py +src/app/schemas/orchestrator.py +src/app/schemas/reimbursement.py +src/app/schemas/settings.py +src/app/schemas/system_log.py +src/app/schemas/user_agent.py +src/app/services/__init__.py +src/app/services/agent_asset_spreadsheet.py +src/app/services/agent_assets.py +src/app/services/agent_conversations.py +src/app/services/agent_foundation.py +src/app/services/agent_runs.py +src/app/services/audit.py +src/app/services/auth.py +src/app/services/document_intelligence.py +src/app/services/employee.py +src/app/services/employee_seed.py +src/app/services/expense_claims.py +src/app/services/expense_rule_runtime.py +src/app/services/hermes_sync.py +src/app/services/knowledge.py +src/app/services/knowledge_index_tasks.py +src/app/services/knowledge_normalizer.py +src/app/services/knowledge_rag.py +src/app/services/knowledge_scheduler.py +src/app/services/knowledge_sync.py +src/app/services/model_connectivity.py +src/app/services/ocr.py +src/app/services/ontology.py +src/app/services/orchestrator.py +src/app/services/reimbursement.py +src/app/services/runtime_chat.py +src/app/services/settings.py +src/app/services/system_hermes.py +src/app/services/system_logs.py +src/app/services/user_agent.py +src/x_financial_server.egg-info/PKG-INFO +src/x_financial_server.egg-info/SOURCES.txt +src/x_financial_server.egg-info/dependency_links.txt +src/x_financial_server.egg-info/requires.txt +src/x_financial_server.egg-info/top_level.txt +tests/test_agent_asset_onlyoffice_key.py +tests/test_agent_asset_service.py +tests/test_agent_asset_spreadsheet_import.py +tests/test_agent_foundation_endpoints.py +tests/test_agent_runs_service.py +tests/test_auth_service.py +tests/test_config_settings_reload.py +tests/test_document_intelligence.py +tests/test_employee_service.py +tests/test_env_file_precedence.py +tests/test_expense_claim_service.py +tests/test_imports.py +tests/test_knowledge_normalizer.py +tests/test_knowledge_onlyoffice_config.py +tests/test_knowledge_rag_service.py +tests/test_knowledge_service.py +tests/test_ocr_endpoints.py +tests/test_ocr_service.py +tests/test_ontology_service.py +tests/test_openapi_schema.py +tests/test_reimbursement_endpoints.py +tests/test_runtime_chat_service.py +tests/test_server_start_dependencies.py +tests/test_settings_persistence.py +tests/test_settings_service.py +tests/test_system_logs_service.py tests/test_user_agent_service.py \ No newline at end of file diff --git a/server/storage/expense_claims/0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf.meta.json b/server/storage/expense_claims/0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf.meta.json index 1fa587e..f868345 100644 --- a/server/storage/expense_claims/0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf.meta.json +++ b/server/storage/expense_claims/0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf.meta.json @@ -1,84 +1,84 @@ -{ - "file_name": "行程单_2_鄂AX9877.pdf", - "storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf", - "media_type": "application/pdf", - "size_bytes": 32459, - "uploaded_at": "2026-05-16T08:41:42.540134+00:00", - "previewable": true, - "preview_kind": "image", - "preview_storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.preview.png", - "preview_media_type": "image/png", - "preview_file_name": "行程单_2_鄂AX9877.preview.png", - "analysis": { - "severity": "pass", - "label": "AI提示符合条件", - "headline": "AI提示:附件符合基础校验条件", - "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", - "points": [ - "票据类型:已识别为出租车/网约车票据。", - "附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。", - "金额字段:已识别到与当前明细接近的金额 35.53 元。" - ], - "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。" - }, - "document_info": { - "document_type": "taxi_receipt", - "document_type_label": "出租车/网约车票据", - "scene_code": "transport", - "scene_label": "交通票据", - "fields": [ - { - "key": "amount", - "label": "金额", - "value": "35.53元" - }, - { - "key": "date", - "label": "日期", - "value": "2026-03-04" - }, - { - "key": "merchant_name", - "label": "商户", - "value": "全季酒店" - } - ] - }, - "requirement_check": { - "matches": true, - "current_expense_type": "transport", - "current_expense_type_label": "交通费", - "allowed_scene_labels": [ - "交通" - ], - "allowed_document_type_labels": [ - "停车/通行费票据", - "一般收据/凭证", - "出租车/网约车票据", - "增值税发票" - ], - "recognized_scene_code": "transport", - "recognized_scene_label": "交通票据", - "recognized_document_type": "taxi_receipt", - "recognized_document_type_label": "出租车/网约车票据", - "mismatch_severity": "high", - "rule_code": "rule.expense.scene_submission_standard", - "rule_name": "报销场景提交与附件标准", - "message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。" - }, - "ocr_status": "recognized", - "ocr_error": "", - "ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-04\n【行程时间:2026-03-0407:05至2026-03-0407:33\n|行程人手机号:18602700270\n1共计1单行程,合计35.53元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-04\n1\n滴滴出行\n武汉市\n全季酒店武汉工程大学店\n武汉站\n35.53元\n07:05\n页码:1/1", - "ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY", - "ocr_avg_score": 0.9819406509399414, - "ocr_line_count": 25, - "ocr_classification_source": "rule", - "ocr_classification_confidence": 0.88, - "ocr_classification_evidence": [ - "滴滴出行", - "滴滴", - "打车", - "上车" - ], - "ocr_warnings": [] +{ + "file_name": "行程单_2_鄂AX9877.pdf", + "storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf", + "media_type": "application/pdf", + "size_bytes": 32459, + "uploaded_at": "2026-05-16T08:41:42.540134+00:00", + "previewable": true, + "preview_kind": "image", + "preview_storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.preview.png", + "preview_media_type": "image/png", + "preview_file_name": "行程单_2_鄂AX9877.preview.png", + "analysis": { + "severity": "pass", + "label": "AI提示符合条件", + "headline": "AI提示:附件符合基础校验条件", + "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", + "points": [ + "票据类型:已识别为出租车/网约车票据。", + "附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。", + "金额字段:已识别到与当前明细接近的金额 35.53 元。" + ], + "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。" + }, + "document_info": { + "document_type": "taxi_receipt", + "document_type_label": "出租车/网约车票据", + "scene_code": "transport", + "scene_label": "交通票据", + "fields": [ + { + "key": "amount", + "label": "金额", + "value": "35.53元" + }, + { + "key": "date", + "label": "日期", + "value": "2026-03-04" + }, + { + "key": "merchant_name", + "label": "商户", + "value": "全季酒店" + } + ] + }, + "requirement_check": { + "matches": true, + "current_expense_type": "transport", + "current_expense_type_label": "交通费", + "allowed_scene_labels": [ + "交通" + ], + "allowed_document_type_labels": [ + "停车/通行费票据", + "一般收据/凭证", + "出租车/网约车票据", + "增值税发票" + ], + "recognized_scene_code": "transport", + "recognized_scene_label": "交通票据", + "recognized_document_type": "taxi_receipt", + "recognized_document_type_label": "出租车/网约车票据", + "mismatch_severity": "high", + "rule_code": "rule.expense.scene_submission_standard", + "rule_name": "报销场景提交与附件标准", + "message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。" + }, + "ocr_status": "recognized", + "ocr_error": "", + "ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-04\n【行程时间:2026-03-0407:05至2026-03-0407:33\n|行程人手机号:18602700270\n1共计1单行程,合计35.53元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-04\n1\n滴滴出行\n武汉市\n全季酒店武汉工程大学店\n武汉站\n35.53元\n07:05\n页码:1/1", + "ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY", + "ocr_avg_score": 0.9819406509399414, + "ocr_line_count": 25, + "ocr_classification_source": "rule", + "ocr_classification_confidence": 0.88, + "ocr_classification_evidence": [ + "滴滴出行", + "滴滴", + "打车", + "上车" + ], + "ocr_warnings": [] } \ No newline at end of file diff --git a/server/storage/expense_claims/281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf.meta.json b/server/storage/expense_claims/281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf.meta.json index 91b784f..80d8cb2 100644 --- a/server/storage/expense_claims/281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf.meta.json +++ b/server/storage/expense_claims/281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf.meta.json @@ -1,84 +1,84 @@ -{ - "file_name": "行程单_1_鄂A1S987.pdf", - "storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf", - "media_type": "application/pdf", - "size_bytes": 34880, - "uploaded_at": "2026-05-16T08:17:53.656595+00:00", - "previewable": true, - "preview_kind": "image", - "preview_storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.preview.png", - "preview_media_type": "image/png", - "preview_file_name": "行程单_1_鄂A1S987.preview.png", - "analysis": { - "severity": "pass", - "label": "AI提示符合条件", - "headline": "AI提示:附件符合基础校验条件", - "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", - "points": [ - "票据类型:已识别为出租车/网约车票据。", - "附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。", - "金额字段:已识别到与当前明细接近的金额 10.30 元。" - ], - "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。" - }, - "document_info": { - "document_type": "taxi_receipt", - "document_type_label": "出租车/网约车票据", - "scene_code": "transport", - "scene_label": "交通票据", - "fields": [ - { - "key": "amount", - "label": "金额", - "value": "10.3元" - }, - { - "key": "date", - "label": "日期", - "value": "2026-03-01" - }, - { - "key": "merchant_name", - "label": "商户", - "value": "全季酒店" - } - ] - }, - "requirement_check": { - "matches": true, - "current_expense_type": "transport", - "current_expense_type_label": "交通费", - "allowed_scene_labels": [ - "交通" - ], - "allowed_document_type_labels": [ - "停车/通行费票据", - "一般收据/凭证", - "出租车/网约车票据", - "增值税发票" - ], - "recognized_scene_code": "transport", - "recognized_scene_label": "交通票据", - "recognized_document_type": "taxi_receipt", - "recognized_document_type_label": "出租车/网约车票据", - "mismatch_severity": "high", - "rule_code": "rule.expense.scene_submission_standard", - "rule_name": "报销场景提交与附件标准", - "message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。" - }, - "ocr_status": "recognized", - "ocr_error": "", - "ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-01\n【行程时间:2026-03-0113:23至2026-03-0113:40\n行程人手机号:18602700270\n|共计1单行程,合计10.30元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-01\n1\n滴滴出行\n13:23\n武汉市\n金融港北地铁站\n全季酒店武汉工程大学店\n10.30元\n页码:1/1", - "ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY", - "ocr_avg_score": 0.9844024634361267, - "ocr_line_count": 25, - "ocr_classification_source": "rule", - "ocr_classification_confidence": 0.88, - "ocr_classification_evidence": [ - "滴滴出行", - "滴滴", - "打车", - "上车" - ], - "ocr_warnings": [] +{ + "file_name": "行程单_1_鄂A1S987.pdf", + "storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf", + "media_type": "application/pdf", + "size_bytes": 34880, + "uploaded_at": "2026-05-16T08:17:53.656595+00:00", + "previewable": true, + "preview_kind": "image", + "preview_storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.preview.png", + "preview_media_type": "image/png", + "preview_file_name": "行程单_1_鄂A1S987.preview.png", + "analysis": { + "severity": "pass", + "label": "AI提示符合条件", + "headline": "AI提示:附件符合基础校验条件", + "summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。", + "points": [ + "票据类型:已识别为出租车/网约车票据。", + "附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。", + "金额字段:已识别到与当前明细接近的金额 10.30 元。" + ], + "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。" + }, + "document_info": { + "document_type": "taxi_receipt", + "document_type_label": "出租车/网约车票据", + "scene_code": "transport", + "scene_label": "交通票据", + "fields": [ + { + "key": "amount", + "label": "金额", + "value": "10.3元" + }, + { + "key": "date", + "label": "日期", + "value": "2026-03-01" + }, + { + "key": "merchant_name", + "label": "商户", + "value": "全季酒店" + } + ] + }, + "requirement_check": { + "matches": true, + "current_expense_type": "transport", + "current_expense_type_label": "交通费", + "allowed_scene_labels": [ + "交通" + ], + "allowed_document_type_labels": [ + "停车/通行费票据", + "一般收据/凭证", + "出租车/网约车票据", + "增值税发票" + ], + "recognized_scene_code": "transport", + "recognized_scene_label": "交通票据", + "recognized_document_type": "taxi_receipt", + "recognized_document_type_label": "出租车/网约车票据", + "mismatch_severity": "high", + "rule_code": "rule.expense.scene_submission_standard", + "rule_name": "报销场景提交与附件标准", + "message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。" + }, + "ocr_status": "recognized", + "ocr_error": "", + "ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-01\n【行程时间:2026-03-0113:23至2026-03-0113:40\n行程人手机号:18602700270\n|共计1单行程,合计10.30元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-01\n1\n滴滴出行\n13:23\n武汉市\n金融港北地铁站\n全季酒店武汉工程大学店\n10.30元\n页码:1/1", + "ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY", + "ocr_avg_score": 0.9844024634361267, + "ocr_line_count": 25, + "ocr_classification_source": "rule", + "ocr_classification_confidence": 0.88, + "ocr_classification_evidence": [ + "滴滴出行", + "滴滴", + "打车", + "上车" + ], + "ocr_warnings": [] } \ No newline at end of file diff --git a/server/storage/knowledge/.index.json b/server/storage/knowledge/.index.json index 63bd204..9e38834 100644 --- a/server/storage/knowledge/.index.json +++ b/server/storage/knowledge/.index.json @@ -35,13 +35,13 @@ "updated_at": "2026-05-17T13:00:09.485818+00:00", "uploaded_by": "admin", "version_number": 1, - "ingest_status": 1, - "ingest_status_updated_at": "2026-05-17T13:00:09.485818+00:00", + "ingest_status": 4, + "ingest_status_updated_at": "2026-05-19T16:00:57.418443+00:00", "ingest_completed_at": "", "ingest_document_name": "", "ingest_document_updated_at": "", "ingest_document_sha256": "", - "ingest_agent_run_id": "" + "ingest_agent_run_id": "run_57f2d8727aaa4374" } ] } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/graph_chunk_entity_relation.graphml b/server/storage/knowledge/.lightrag/x_financial_knowledge/graph_chunk_entity_relation.graphml index 1295642..8735b8b 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/graph_chunk_entity_relation.graphml +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/graph_chunk_entity_relation.graphml @@ -1,2692 +1,2692 @@ - - - - - - - - - - - - - - - - - - - 远光软件股份有限公司 - organization - 远光软件股份有限公司is a company that issued the Company Expenditure Management Measures (2024) to regulate expenditure and reimbursement standards.<SEP>Yuan Guang Software Co., Ltd. is a company that has established internal expense reimbursement and management regulations.<SEP>YuanGuang Software Co., Ltd. is the company that issued the expense reimbursement management regulations to optimize business development, standardize expenditure and reimbursement processes, and prevent operational risks. - chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第一章总则 - content - Chapter 1 General Provisions contains the purpose, scope, and management principles of the expense reimbursement regulations. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第二条目的 - content - Article 2 Purpose describes the objective of adapting to business development needs, optimizing expenditure and reimbursement standards, and standardizing approval processes. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第二条范围 - content - Article 2 Scope defines the applicable scope of the expense reimbursement regulations. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第三条管理原则 - content - Article 3 Management Principles establishes the principles for managing the expense reimbursement system. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第二章职责分工 - content - Chapter 2 Responsibilities Division outlines the responsibilities of various departments and personnel in the expense management system. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第四条归口管理部门主要职责 - content - Article 4 Main Responsibilities of the Designated Management Department specifies the duties of the designated management department. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 计划财务部 - organization - 计划财务部is a department of远光软件股份有限公司with main responsibilities related to financial planning and expenditure management.<SEP>计划财务部负责制定、修订、解释及实施协调工作。<SEP>Planning and Finance Department is a key management department responsible for expense reimbursement duties.<SEP>Planning and Finance Department is one of the departments responsible for financial management and expense approval, determining expenditure scope, standards, methods, and management processes.<SEP>计划财务部is responsible for employee advance funds, travel expenses, transportation, foreign travel, meal allowances, taxes, audits, and financial expenses. - chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011991 - - - - 第五条计划财务部主要职责 - content - Article 5 Main Responsibilities of Planning and Finance Department outlines the specific duties of the Planning and Finance Department. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011992 - - - - 第六条经办部门(个人)主要职责 - content - Article 6 Main Responsibilities of Operating Departments (Individuals) describes the duties of operating departments and individuals handling expenses. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011992 - - - - 第七条各级管理人员主要职责 - content - Article 7 Main Responsibilities of Management Personnel at All Levels specifies the duties of managers at all levels. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011992 - - - - 第三章支出报销申请与审批 - content - Chapter 3 Expense Reimbursement Application and Approval covers the processes and regulations for expense reimbursement applications and approvals. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011992 - - - - 第八条支出报销申请 - content - Article 8 Expense Reimbursement Application establishes the procedures for submitting expense reimbursement requests. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011992 - - - - 第九条支出报销审批 - content - Article 9 Expense Reimbursement Approval defines the approval process for expense reimbursement requests. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011993 - - - - 第十条支出成本中心归属 - content - Article 10 Cost Center Attribution determines which cost centers expenses should be attributed to. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011993 - - - - 第四章重点支出管理规定 - content - Chapter 4 Key Expense Management Regulations contains specific management provisions for important types of - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第十一条备用金借款 - content - Article 11 Petty Cash Loans provides management regulations for petty cash borrowing. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第十二条市内交通费 - content - Article 12 Local Transportation Fees specifies the regulations for local transportation expense reimbursement. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第十三条差旅费 - content - Article 13 Travel Expenses establishes the management regulations for travel-related expenses. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第十四条业务招待费 - content - Article 14 Business Entertainment Expenses provides management regulations for business entertainment costs. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第五章附则 - content - Chapter 5 Supplementary Provisions contains the final provisions including administration, implementation, and attachments of the regulations. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第二十三条本办法的归口与实施 - content - Article 23 Administration and Implementation designates the department responsible for administration and specifies the effective date of these measures. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011994 - - - - 第二十四条附件 - content - Article 24 Appendices indicates that supplementary materials and attachments are part of these regulations. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011995 - - - - 交通工具等级标准 - data - Transportation Level Standards is a data table specifying the permitted transportation modes (airplane, train, ship, other transportation) based on employee rank levels. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011995 - - - - 出差补贴标准 - data - Travel Allowance Standards is a data table detailing meal allowances, basic travel allowances, and total allowances by region (Hong Kong/Macau/Taiwan, municipalities/special zones/Tibet, other regions, and foreign countries). - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011996 - - - - 经济舱 - data - Economy Class is the permitted airplane class for all employee levels according to transportation standards. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011996 - - - - 火车硬席 - data - Hard Seat Train is a permitted train transportation mode including hard sleeper, hard seat, high-speed rail second-class, and all soft-seat train second-class soft seats. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011996 - - - - 三等舱 - data - Third Class Cabin is the permitted ship accommodation class for all employee levels. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011996 - - - - 凭据报销 - method - Receipt-Based Reimbursement is the method for other transportation expenses (excluding cars), requiring submission of receipts for reimbursement. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - 餐补 - data - 餐补是指出差人员因工作需要在外出差期间的餐饮费用补贴,根据地区不同分为75元、65元、55元三个标准,但西藏地区标准为140元。组织安排统一安排餐食的情况下不再报销餐补。<SEP>Meal Allowance is a type of travel subsidy that varies by region: 75 for Hong Kong/Macau/Taiwan, 65 for municipalities/special zones/Tibet, 55 for other regions, and 140 for foreign countries. - chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - 基本补助 - data - Basic Allowance is a standard travel subsidy of 35 regardless of region. - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - 公司支出管理办法 - concept - 公司支出管理办法is a corporate expense management policy document that outlines reimbursement standards, approval authority, and departmental responsibilities for expense management. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - 办公室(党委办公室) - organization - 办公室(党委办公室)is a department responsible for party building expenses and official vehicle expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - 工会委员会 - organization - 工会委员会is responsible for trade union expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - 营销中心 - organization - 营销中心is responsible for bidding business expenses, institutional marketing expenses, customer training, and sales refund expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - 品牌及市场运营中心 - organization - 品牌及市场运营中心是对外捐赠支出的归口管理部门,负责审核捐赠申请。<SEP>品牌及市场运营中心is responsible for advertising expenses, business promotion expenses, and external donation expenses. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - 组织人事部 - organization - 组织人事部负责职工福利费的年度计划管理,并规定薪酬福利支事的审批程序标准。<SEP>组织人事部是公司负责人事管理的部门,确定调动工作人员的报销标准。<SEP>Personnel Department determines reimbursement standards for assigned personnel working at remote locations.<SEP>组织人事部is responsible for salary and benefits (excluding canteen and meal allowances), external labor, employee insurance, relocation allowances, and other benefits expenses. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011999 - - - - 人力资源服务部 - organization - 人力资源服务部is responsible for recruitment business and employee education training expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - 产业投资部 - organization - 产业投资部is responsible for equity investment expenses and merger business expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - 证券与法律事务部 - organization - 证券与法律事务部is responsible for legal affairs expenses, listing information disclosure expenses, and trademark registration expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - 产品规划设计部 - organization - 产品规划设计部is responsible for intellectual property expenses, information technology consulting expenses, and research and development expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - DAP研发中心 - organization - DAP研发中心is responsible for testing, evaluation, and analysis expenses paid to external units during research and development. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 信息管理部 - organization - 信息管理部is responsible for IT asset acquisition, leasing, operations, maintenance, repairs, and network usage expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 后勤服务部 - organization - 后勤服务部is responsible for non-IT asset management, canteen expenses, office expenses, and business travel settlement. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 审批权限 - concept - 审批权限refers to the approval authority levels for different management positions regarding expense approvals within their responsibilities. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 报销标准 - concept - 报销标准refers to reimbursement standards including travel expense transportation methods and allowances. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012002 - - - - 差旅费 - concept - Travel expenses is a category of expenditure managed under the measures.<SEP>差旅费是指因公出差发生的各项费用,包括交通费、住宿费、餐补等,公司制定了详细的报销规定和注意事项。<SEP>差旅费是指员工因公出差所发生的交通费、住宿费、出差补贴等费用。<SEP>Travel expenses are regulated under the key expense management provisions, with specific standards for business trips.<SEP>差旅费refers to travel expenses including transportation standards and allowances for company leadership. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012002 - - - - 全资子公司 - organization - 全资子公司is a subsidiary type wholly owned by远光软件股份有限公司, included in the scope of the expenditure management measures.<SEP>全资子公司refers to wholly-owned subsidiaries whose approval authority is managed by parent company personnel. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012003 - - - - 人事归口管理部门 - organization - 人事归口管理部门refers to the centralized HR management department responsible for salary, bonuses, and benefits approval. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012003 - - - - 母公司 - organization - 母公司refers to the parent company that oversees subsidiary management and authorization. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012003 - - - - 逐级审批规则 - concept - 逐级审批规则refers to the level-by-level approval rules that form the basis of business workflow execution, following organizational relationships. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 终审岗 - concept - 终审岗refers to the final approval position in the hierarchical approval process. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 财务入账条件 - concept - 财务入账条件refers to the conditions required for financial recording of settlement business expenses. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 薪酬福利支出分配计划 - concept - 薪酬福利支出分配计划refers to the salary and benefits expense distribution plan approved according to HR management department regulations. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 公司1号文 - content - 公司1号文refers to Company Document No. 1 which specifies job authorization standards for subsidiary department managers. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 总监 - concept - 总监refers to the director-level position with corresponding approval authority for subsidiary management. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 一级部门总经理 - concept - 一级部门总经理refers to the level-1 department general manager position with corresponding approval authority. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - P8 - concept - P8 refers to the position level of company leadership in the organizational hierarchy. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 报销标准变化情况 - concept - Reimbursement standard changes is a key topic in the regulations, adjusting reimbursement rules for official vehicle subsidies and other expenses.<SEP>报销标准变化情况refers to the changes in reimbursement standards, including travel expense transportation and allowance modifications. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 取消报销规定 - concept - 取消报销规定refers to the cancelled reimbursement regulations, such as the official vehicle allowance now included in salary. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 新增报销规定 - concept - 新增报销规定refers to new reimbursement regulations added to the company expense management policy. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 因公用车补贴 - concept - 因公用车补贴refers to the official vehicle allowance that was cancelled and integrated into salary payment. - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012006 - - - - 异地挂职锻炼补贴标准 - concept - Remote assignment training allowance standards define the reimbursement rules for transportation, meal allowances, and basic subsidies during temporary assignments.<SEP>异地挂职锻炼补贴标准refers to the subsidy standard for off-site training and development exercises arranged by the organization. - chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - 组织安排 - concept - 组织安排refers to organizational arrangements for employee training, relocation, - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - Company - organization - The company is an organization that establishes expenditure management principles, approval authority, and departmental responsibilities for financial operations. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - Management Personnel At All Levels - person - Management personnel at all levels should fulfill expenditure reimbursement approval authority within authorized approval scope and responsibilities, bearing approval responsibilities. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - Centralized Management department - organization - The centralized management department is responsible for determining expenditure scope, standards, methods, and management processes for various expenditure businesses based on business management needs. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - Planning and Finance Department - organization - The Planning and Finance Department is responsible for clarifying expenditure reimbursement approval processes, audit points, and reimbursement documentation standards. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - Operating Department Individual - person - The operating department (individual) is responsible for conducting expenditure business activities within departmental job responsibilities and authorized business scope, adhering to budget-first, thrifty, and benefit-priority principles. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012008 - - - - Operator - person - The operator should timely fill out system documents, submit business original documents, obtain real, compliant, related, and complete business original documents, and verify invoice authenticity. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - First Approver - person - The first approver should comprehensively audit the authenticity, compliance, necessity, and rationality of expenditure business, and bear approval responsibility. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - Subsequent Approver - person - The subsequent approver should audit the necessity and rationality of expenditure business, and bear approval responsibility. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - Financial Information System - artifact - The financial information system is the platform through which expenditure reimbursement applications are submitted via system documents. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - Business Original Documents - data - Business original documents are the supporting materials for expenditure reimbursement, including invoices and related vouchers that must be complete, compliant, related, and complete. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - VAT Special Invoice - content - VAT special invoice is a tax document required for most expenditures, and if not obtained when required, the operator should explain the reason in the system document. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - Expenditure Reimbursement Application - concept - The expenditure reimbursement application is submitted through the financial information system, and financial原则上does not accept paper application documents. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - Expenditure Authorization Approval Scope - concept - Expenditure authorization approval scope refers to the authorized limits for expenditure approval at various management levels. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011997 - - - - Separation of Approval and Processing Principle - concept - The separation of approval and processing principle requires that the expenditure operator and approver cannot be the same person. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - Three Flows Consistency Principle - concept - The three flows consistency principle requires alignment of invoice flow, capital flow, and material flow during business operations. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - Employee Remuneration - concept - Employee remuneration is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011998 - - - - Personal Service Compensation - concept - Personal service compensation is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011999 - - - - Travel Allowance - concept - Travel allowance is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011999 - - - - Special Subsidy - concept - Special subsidy is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011999 - - - - Current Account Payment - concept - Current account payment is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779011999 - - - - Trade Union Fund - concept - Trade union fund is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Employee Welfare - concept - Employee welfare is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Staff Activities - concept - Staff activities is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Business Entertainment - concept - Business entertainment is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Transportation Tickets - concept - Transportation tickets is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Government Fees - concept - Government fees is a type of expenditure exempt from VAT special invoice requirements. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Tax Control System Details - data - Tax control system details is a supplementary document required for aggregated VAT invoices. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - Three Working Days Deadline - concept - The three working days deadline refers to the timeframe within which the operator must supplement incomplete business original documents after financial review. - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - 广告费 - concept - 广告费是指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业务佐证材料。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012000 - - - - 业务宣传费 - concept - 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 培训费 - concept - Training expenses is a category of expenditure managed under the measures.<SEP>培训费是指公司安排、主办或承办培训发生的学费、书籍杂费、师资费、资料费、场地费、设备材料费、住宿费、交通费等,以及员工根据工作需要,经归口管理部门批准参加外部培训、考证、教育产生的相关费用。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012001 - - - - 通信费 - concept - Communication fees is a category of expenditure managed under the measures.<SEP>通信费是指为满足公司日常办公需要发生的电话、传真、集团网、网络连接等支出。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012002 - - - - 邮递费 - concept - 邮递费是指公司因业务需要邮递物品而支付的费用,包括邮件费、托运费、快递费等。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012004 - - - - 薪酬福利支出 - concept - 薪酬福利支出是指公司相关制度规定的职工薪资、奖励提成、福利费支出,以及临时的奖励及福利支出。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 对外捐赠支出 - concept - 对外捐赠支出是指公司对外捐赠的费用,由品牌及市场运营中心归口管理,需严格预算单控。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 涉外业务汇率标准 - concept - 涉外业务汇率标准规定以外币结算时按支付凭据所载汇率折算,未载明汇率的按中国银行外汇折算价执行。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012005 - - - - 公司员工教育培训管理办法 - content - 《公司员工教育培训管理办法》是规定培训费报销资格认定标准的制度文件。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012006 - - - - 公司员工因公通讯费用实施细则 - content - 《公司员工因公通讯费用实施细则》是规定员工通讯费执行标准的制度文件。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012006 - - - - 公司团建管理办法 - content - 《公司团建管理办法》是规定职工活动支出执行标准的制度文件。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012006 - - - - 工会经费管理办法 - content - 《工会经费管理办法》是规定职工活动支出执行标准的制度文件。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012006 - - - - 中国银行外汇折算价 - data - 中国银行外汇折算价是涉外业务汇率折算的标准依据。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012019 - - - - 中国外汇交易中心参考汇率 - data - 中国外汇交易中心参考汇率是中国银行无折算价币种的备选汇率依据。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012007 - - - - 第十七条 - concept - 第十七条规定培训费的定义、范围及报销标准。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012008 - - - - 第十八条 - concept - 第十八条规定通信费的定义及执行标准。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012008 - - - - 第十九条 - concept - 第十九条规定邮递费的定义及报销凭据要求。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012008 - - - - 第二十条 - concept - 第二十条规定薪酬福利支出的分类、执行标准及审批程序。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012008 - - - - 第二十一条 - concept - 第二十一条规定对外捐赠支出的归口管理、预算控制及审批流程。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - 第二十二条 - concept - 第二十二条规定涉外业务汇率标准及结算方式。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - 第二十三条 - concept - 第二十三条规定办法的归口管理部门及实施时间。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - 第二十四条 - concept - 第二十四条规定办法的附件内容。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - 附表1:员工支出报销审批权限表 - content - 附表1规定了员工支出报销的审批权限表,明确各部门经理、总监、副总裁等审批层级及权限。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012009 - - - - 附表2:岗位支出报销审批权限表 - content - 附表2规定了岗位支出报销的审批权限表。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012011 - - - - 附表3:支出归口管理部门与归口业务范围 - content - 附表3规定了支出归口管理部门及其对应的业务范围。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012011 - - - - 业务招待 - concept - 业务招待是员工支出类别之一,对应审批权限额度分别为0.5万元、1万元、2万元、3万元、15万元。<SEP>业务招待是报销审批权限表中的支出项目类别。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - 报销资格 - concept - 报销资格是指员工申请费用报销所需满足的条件和标准。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - 捐赠申请 - concept - 捐赠申请是指业务部门向公司提出对外捐赠申请的文件,需说明捐赠事由、对象、金额等内容。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - 预算调整决策程序 - concept - 预算调整决策程序是指针对未纳入预算的对外捐赠事项所履行的预算调整审批流程。 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - Departments And Units - organization - Departments and units are organizational entities within the company that must strictly enforce travel approval procedures and control the number and duration of business trips. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - Company Business Travel System - concept - The company business travel system is an internal approval platform through which employees below department deputy level must obtain pre-approval for business travel. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - Transportation Level Standards - concept - Transportation level standards is a table specifying the allowed transportation modes and classes for employees based on their rank for domestic and international travel. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012012 - - - - Hotel Accommodation Standards - concept - Hotel accommodation standards are tables specifying the maximum hotel accommodation expenses allowed for employees based on their rank and destination. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012013 - - - - Travel Allowance Standards - concept - Travel allowance standards are tables specifying the daily allowance amounts for employees, including meal allowances and basic travel allowances, categorized by travel destination. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012013 - - - - Company Property Rental Management - concept - Company property rental management refers to a policy document that governs the application and reimbursement standards for remote work housing. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012013 - - - - Business Trip Approval - concept - Business trip approval is a required procedural step that employees must follow before undertaking official travel. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012013 - - - - Company Leadership - person - Company leadership refers to the highest-ranking executives (P8 and above) who have the most generous travel expense standards. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012013 - - - - Senior Managers - person - Senior managers (P7 level) are executives with the second-highest travel expense standards after company leadership. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012014 - - - - Middle Managers - person - Middle managers (P5-P6 level) and external experts have moderate travel expense standards. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012014 - - - - Basic Level Managers - person - Basic level managers (P4) are supervisors with lower travel expense standards and additional approval requirements for air travel. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012015 - - - - Other Employees - person - Other employees (P1-P3 level) have the most restrictive travel expense standards and must fly economy class at 50% discount or below. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012015 - - - - Remote Work Housing - artifact - Remote work housing refers to accommodation provided to employees on long-term business travel assignments, governed by company rental management policies. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012015 - - - - Commercial Insurance - content - Commercial insurance including transportation insurance has been purchased by the company for employees, and insurance fees during business trips are non-reimbursable. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012015 - - - - Transportation Cost Reimbursement - concept - Transportation cost reimbursement covers expenses for air travel, train, ship, and other transportation modes based on employee rank and travel standards. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012016 - - - - Accommodation Cost Reimbursement - concept - Accommodation cost reimbursement covers hotel lodging expenses according to location category and employee rank, with approval required for excess amounts. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012016 - - - - Hong Kong, Macau, And Taiwan Region - location - Hong Kong, Macau, and Taiwan region is a specific travel destination category in the expense reimbursement tables, listed separately from domestic mainland destinations. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012016 - - - - Directly-Controlled Municipalities And Special Administrative Regions - location - Directly-controlled municipalities and special administrative regions refer to major cities like Beijing, Shanghai, Tianjin, Chongqing, and Shenzhen that have higher accommodation standards than provincial capitals. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012016 - - - - Provincial Capitals - location - Provincial capitals are cities designated as provincial-level administrative centers that have moderate accommodation standards in the policy. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012016 - - - - Other Areas - location - Other areas refer to locations outside directly-controlled municipalities, special administrative regions, and provincial capitals that have the lowest accommodation standards in the policy. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012017 - - - - Night High-Speed Rail Provision - concept - Night high-speed rail provision is a special allowance permitting employees to choose sleeper seats when traveling on high-speed trains for 6 hours or more at night. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012017 - - - - Taxi Usage Regulations - concept - Taxi usage regulations specify the limited circumstances under which taxi expenses can be reimbursed, including emergency official business, client接送, late night work after 22:00, and other special circumstances. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012017 - - - - Self-Driving Travel Provisions - concept - Self-driving travel provisions outline the reimbursement rules for road tolls, parking fees, fuel costs, and electricity expenses when employees use personal vehicles for business travel, with department head approval required. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012017 - - - - Remote Work Housing Rental Expenses - data - Remote work housing rental expenses include rent, initial housing configuration costs, property management fees, heating fees, utility and gas expenses including shared portions, and internet broadband fees. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012018 - - - - External Conference Accommodation - concept - External conference accommodation refers to hotel expenses when employees attend conferences or training sessions, which may be reimbursed at the conference organizer's stated standards if accommodations are uniformly arranged. - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012018 - - - - 取消报销规定内容 - concept - The content about canceling reimbursement regulations refers to the removal of official vehicle subsidy-related expressions since the subsidy has been included in wages. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012018 - - - - 新增规定内容 - concept - Newly added regulations content covers remote assignment allowance standards and standardized business travel booking procedures. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012018 - - - - 商旅订票规范 - concept - Business travel booking standards specify that official business trips should use the commercial travel system for unified booking. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012018 - - - - 审批权限变化情况 - concept - Changes in approval authority mainly involve adjusting bid security deposit approval limits and clarifying expense approval workflow procedures. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012019 - - - - 投标保证金 - data - Bid security deposit is a type of expense requiring approval, with different approval limits assigned to different management levels. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012019 - - - - 审批流转程序 - method - Approval workflow process is the multi-level approval rule based on organizational relationships, with provisions for special matters to bypass levels. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012019 - - - - 出差规定 - concept - Business travel regulations define the reimbursement standards for transportation, meal allowances, and basic subsidies during transit periods. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012019 - - - - 财务信息化系统 - method - Financial information system is the system used for submitting expense reimbursement applications electronically. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012020 - - - - 支出报销申请与审批 - concept - Expenditure reimbursement application and approval is a key process regulated by the measures.<SEP>Expense reimbursement application and approval is a chapter of the regulations covering application methods and approval processes. - chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012020 - - - - 重点支出管理规定 - concept - Key expense management regulations is a chapter covering petty cash loans, local transportation fees, and travel expenses. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012020 - - - - 备用金借款 - concept - Petty cash loans is a category of key expenditure regulated by the measures.<SEP>备用金借款是公司借支给正式员工用于支付与公司经济业务相关且必须预支的费用,遵循"前款不清、后款不借"原则。<SEP>Petty cash loan is one of the key expense items regulated under the management provisions. - chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012020 - - - - 市内交通费 - concept - Local transportation fees is a category of expenditure managed under the measures.<SEP>市内交通费指员工为公司生产经营活动在工作所在地发生的交通费用,不包括正常上下班交通费。<SEP>Local transportation fees are regulated under the key expense management provisions. - chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012022 - - - - 审批权限表 - content - Approval authority table is an attachment showing approval limits for different management positions regarding bid security deposits. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012022 - - - - 1 Yuan Per Person Per Kilometer Reimbursement - data - 1 yuan per person per kilometer reimbursement standard is the new rule for official vehicle subsidy calculation, with amounts exceeding the standard to be borne by the employee. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Official Vehicle Subsidy - content - Official vehicle subsidy has been included in wages, leading to the cancellation of related reimbursement regulations. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Department Manager - person - Department Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Director - person - Director is a management position with approval authority for bid security deposits up to 50,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - First-Level Department General Manager - person - First-Level Department General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Institution General Manager - person - Institution General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Business Division General Manager - person - Business Division General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Vice President - person - Vice President is a senior management position with approval authority for bid security deposits up to 100,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Chief Engineer - person - Chief Engineer is a senior management position with approval authority for bid security deposits up to 100,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012023 - - - - Senior Vice President - person - Senior Vice President is a senior management position with approval authority for bid security deposits up to 200,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012024 - - - - President - person - President is the highest executive position with approval authority for bid security deposits up to 500,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012024 - - - - Committee Chairpersons - person - Committee Chairpersons are senior management positions with approval authority for bid security deposits up to 200,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012024 - - - - Bid Security Deposit Approval Limits Table - content - The bid security deposit approval limits table specifies different approval authority limits for various management positions, ranging from 50,000 to 5,000,000 yuan. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012024 - - - - 50000 Yuan Approval Limit - data - 50,000 yuan is the bid security deposit approval limit for department managers, directors, and first-level department general managers. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012024 - - - - 100000 Yuan Approval Limit - data - 100,000 yuan is the bid security deposit approval limit for vice presidents and chief engineers. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012025 - - - - 200000 Yuan Approval Limit - data - 200,000 yuan is the bid security deposit approval limit for senior vice presidents and committee chairpersons. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012025 - - - - 5000000 Yuan Approval Limit - data - 5,000,000 yuan is the bid security deposit approval limit for the president. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012025 - - - - Commercial Travel System - method - Commercial travel system is the unified booking platform that should be used for official business travel arrangements. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012026 - - - - Multi-Level Approval Rule - method - Multi-level approval rule is based on organizational relationships, requiring expenses to be approved sequentially through management levels. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012026 - - - - Final Approval Position - person - Final approval position is the highest approval authority that can be bypassed for special matters after decision-making. - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012026 - - - - Company Hotel Accommodation Limit Standards - data - Company hotel accommodation limit - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012026 - - - - 公司支出管理办法(2024) - content - 公司支出管理办法(2024)is a regulation issued by远光软件股份有限公司to optimize expenditure and reimbursement standards, standardize approval processes, and prevent operational risks. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012026 - - - - 国家电网公司 - organization - 国家电网公司is a reference entity whose management regulations are consulted in the development of the expenditure management measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 国网数科公司 - organization - 国网数科公司is a reference entity whose management regulations are consulted in the development of the expenditure management measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 办法 - concept - The original "Company Expenditure Management Measures" that has been superseded by the 2024 version. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 2024年4月17日 - event - 2024年4月17日is the date when the Company Expenditure Management Measures (2024) were issued by远光软件股份有限公司. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012027 - - - - 预算先行 - concept - Budget-first is one of the core management principles in the expenditure management system, requiring expenditures to be within budget targets. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012028 - - - - 厉行节约 - concept - Rigorous economy is a management principle requiring departments and positions to adhere to cost-saving and efficiency-first principles. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012028 - - - - 分级授权 - concept - Hierarchical authorization is a management principle where management personnel exercise expenditure approval authority based on their positions. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012028 - - - - 分类控制 - concept - Classified control is a management principle for standardized expenditure business activities. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012028 - - - - 批办分离 - concept - Separation of approval and execution is a management principle in the expenditure management system. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 业务招待费 - concept - Business entertainment expenses is a category of expenditure managed under the measures.<SEP>业务招待费是指各级单位正常生产经营管理过程中需要发生的、用于必要招待的各项费用,包括用于接待客户和相关单位的餐饮等合理支出。 - chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 会议费 - concept - Meeting expenses is a category of expenditure managed under the measures.<SEP>会议费是指公司主办或承办会议发生的会场租金、文件资料、设备租金等支出,以及参加外部会议所发生的会务相关支出。 - chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 广告宣传费 - concept - Advertising expenses is a category of expenditure managed under the measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 控股子公司 - organization - 控股子公司is a controlled subsidiary of远光软件股份有限公司that should follow the expenditure management measures and report to计划财务部. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 分支机构 - organization - 分支机构refers to non-legal-person branches of远光软件股份有限公司included in the scope of the expenditure management measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 经办部门 - organization - 经办部门is a department or individual responsible for handling expenditure business activities under the measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012029 - - - - 各级管理人员 - concept - 各级管理人员refers to management personnel at all levels who exercise expenditure approval authority within their authorization scope. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012030 - - - - 归口管理部门 - organization - 归口管理部门is the central management department responsible for expenditure management under the measures. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012044 - - - - 远光制度〔2024〕14号 - content - 远光制度〔2024〕14号is the document number assigned to the Company Expenditure Management Measures (2024) issued by远光软件股份有限公司. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012044 - - - - 修订说明 - content - 修订说明is an appendix to the Company Expenditure Management Measures (2024) explaining the revisions made. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012032 - - - - 员工支出报销审批权限表 - content - 员工支出报销审批权限表is an appendix table defining employee-level expenditure reimbursement approval authority.<SEP>员工支出报销审批权限表是一份规定公司员工各项支出报销审批层级的制度文件,详细列示了不同支出项目的审批权限金额标准。 - chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012032 - - - - 岗位支出报销审批权限表 - content - 岗位支出报销审批权限表is an appendix table defining position-level expenditure reimbursement approval authority.<SEP>岗位支出报销审批权限表是一份规定公司各岗位支出报销审批层级的制度文件,涵盖资本性支出、收益性支出、财务专用等类别。 - chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012032 - - - - 支出归口管理部门与归口业务范围 - content - 支出归口管理部门与归口业务范围is an appendix table defining the mapping between central management departments and their corresponding business scopes. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012033 - - - - 效益优先 - concept - 效益优先is a management principle requiring departments and positions to prioritize efficiency in expenditure activities. - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012033 - - - - 公司各部门 - organization - 公司各部门refers to all departments within远光软件股份有限公司, which are recipients of the Company Expenditure Management Measures (2024). - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012033 - - - - 因公借款 - concept - 因公借款是员工支出类别之一,对应审批权限额度分别为0.5万元、1万元、2万元、3万元、15万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012033 - - - - 其他支出(员工) - concept - 其他支出是员工支出类别之一,对应审批权限额度分别为1万元、2万元、3万元、5万元、50万元,并包含差旅费、市内交通、客服及商务等补充说明。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012034 - - - - 资产采购 - concept - 资产采购属于资本性支出类别,包括固定资产、无形资产、低值易耗品采购,对应审批权限为1万元、2万元、10万元、15万元、200万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012034 - - - - 基建工程 - concept - 基建工程属于资本性支出类别,对应审批权限为5万元、10万元、50万元,但前两级审批权限为空。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012034 - - - - 股权投资、兼并收购 - concept - 股权投资、兼并收购属于资本性支出类别,全部审批权限为空,需由董事长单独审批。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 材料采购 - concept - 材料采购属于收益性支出类别,包括生产采购(原材料、辅助材料、机物料)和项目采购(设备、软件、公有云资源),对应审批权限为10万元、20万元、30万元、500万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 分包外包(内部单位) - concept - 分包外包(内部单位)属于收益性支出类别,包括研发、实施、运维、服务等分包外包,对应审批权限为10万元、20万元、30万元、500万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 分包外包(外部单位) - concept - 分包外包(外部单位)属于收益性支出类别,包括委托加工、项目土建装修等,对应审批权限为10万元、20万元、30万元、200万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 保证金 - concept - 保证金属于收益性支出类别,包括投标保证金、质保金、履约保证金及招标相关保证金,对应审批权限为5万元、50万元、100万元、200万元、500万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 销售退款 - concept - 销售退款属于收益性支出类别,包括销售退款和代付款(代收后),对应审批权限为5万元、10万元、30万元、50万元、200万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 房屋租金 - concept - 房屋租金属于收益性支出类别,不含水电及杂费,需经业务归口部门审批,对应审批权限为5万元、10万元、20万元、30万元、200万元。 - chunk-9841d66d8fb8548aab40220663a51693 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 基本出差补贴 - concept - 基本出差补贴是指出差期间每天给予的基本生活补助,标准为35元,适用于所有出差地区。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012048 - - - - 商旅系统 - artifact - 商旅系统是公司统一的差旅预订和审批平台,出差人员原则上应通过该系统预定交通和住宿,特殊情况未通过系统下单的应邮件知会商旅客服并抄送部门负责人。<SEP>商旅系统是公司用于事前审批员工出差申请的系统平台。 - chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012049 - - - - 业务佐证材料 - data - 业务佐证材料是指用于证明出差真实性、费用合理性等的材料,包括登机牌、通行记录、支付记录、审批邮件、订单详情等。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012036 - - - - 探亲路费 - concept - 探亲路费是指员工因私回家探亲产生的交通费用,应遵循公司员工探亲管理办法规定,不得以因公差旅方式报销,不享受出差补贴。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012037 - - - - 调动工作 - event - 调动工作是指公司组织安排员工到异地挂职锻炼、培养锻炼、人才帮扶、借调支援等,在途期间按出差规定执行,在异地工作期间按组织人事部确定的标准执行。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012037 - - - - 直辖市 - location - 直辖市是中国行政区划的一种,指由中央政府直接管辖的城市,包括北京、上海、天津、重庆等。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012037 - - - - 特区 - location - 特区在这里指深圳等经济特区,是享有特殊经济政策的国家级区域。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012037 - - - - 西藏 - location - 西藏是中国的一个自治区,在差旅补贴标准中作为特殊地区标注,餐补标准为140元。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012037 - - - - 第十四条 - concept - 第十四条是公司差旅费报销规定中关于业务招待费的条款,定义了业务招待费的范围和报销要求。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 第十五条 - concept - 第十五条是公司差旅费报销规定中关于会议费的条款,规定了公司主办或承办会议的报销标准。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 第十六条 - concept - 第十六条是公司差旅费报销规定中关于广告宣传费的条款,包括广告费和业务宣传费的管理规定。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 公司总裁 - person - 公司总裁是公司最高管理人员,负责批准预算调整申请。<SEP>公司总裁是公司最高管理层人员,对经费预算30000元及以上的内部会议、研讨与集中培训具有审批权限。 - chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 商旅客服 - person - 商旅客服是商旅系统的服务人员,负责处理差旅预订异常情况,未通过系统下单的需邮件知会。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012038 - - - - 公司员工探亲管理办法 - content - 《公司员工探亲管理办法》是公司制定的关于员工探亲路费报销的管理规定,探亲路费应严格遵循此办法。 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 增值税发票 - content - 增值税发票是指企业开具的税务发票,用于记录商品或服务的销售和税收信息。文中提到汇总开具的增值税发票应附税控系统明细清单。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 税控系统明细清单 - content - 税控系统明细清单是由税控系统生成的详细清单,汇总开具增值税发票时需要附上该清单。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 财务 - organization - 财务是指公司内部的财务部门,负责审核业务原始凭据、影像扫描、审核与支付等报销相关工作。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 经办人 - person - 经办人是指负责具体业务办理并提交报销申请的人员,需确保原始凭据完整并按时补充。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 业务原始凭据 - content - 业务原始凭据是指证明经济业务发生的原始文件,财务审核时需要检查其完整性。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 报销申请时限 - concept - 报销申请时限是指从业务完成日到附件影像资料挂接系统单据日的期间,公司各类支出报销结算申请时限为三个月。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 预付款项 - concept - 预付款项是指公司预先支付给员工或供应商的款项,原则上应在次月底前完成结算。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012039 - - - - 公司 - organization - 公司是指雇用员工并制定报销政策的企业主体,通过公对私或公对公方式进行支出结算。 - chunk-061324cc36078214691a6fc1cd0aaeea<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012040 - - - - 员工 - person - 员工是公司的正式成员,可以申请备用金借款、差旅费报销等支出业务。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012040 - - - - 供应商 - organization - 供应商是公司采购商品或服务的外部合作方,岗位支出业务原则上采用公对公方式与其直接结算。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012040 - - - - 支出报销审批 - event - 支出报销审批是指按照审批权限对报销申请进行审核批准的过程,包括预算内和预算外支出的不同审批流程。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012041 - - - - 预算内支出 - concept - 预算内支出是指在已批准预算范围内的支出,按附表1、附表2执行审批权限。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 正式员工 - person - 正式员工是公司的正式成员,有资格申请备用金借款,非正式员工不得申请。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 分管领导 - person - 分管领导是负责特定业务领域的公司管理人员,需对季度不能及时报账核销的备用金借款进行延期审批。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 一万元 - data - 一万元是员工备用金借款额度的上限。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 出租车 - method - 出租车是市内交通出行方式之一,仅限紧急公务、接送客户、夜间工作至22:00后等特别情形使用。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 部门负责人 - person - 部门负责人负责从严管理出租车市内交通使用,确保符合使用规定。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 出差审批程序 - method - 出差审批程序是出差前必须完成的审批流程,各部门应严格执行以从严控制出差人数和天数。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 交通费 - concept - 交通费是差旅费的组成部分,包括出差发生的各种交通支出。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012042 - - - - 住宿费 - concept - 住宿费是差旅费的组成部分,指出差期间的住宿支出。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012043 - - - - 出差补贴 - concept - 出差补贴是差旅费的组成部分,用于补贴员工出差期间的相关开支。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012043 - - - - 成本中心归属 - concept - 成本中心归属基于责任原则与受益原则确定,特殊情况由业务部门协商确定。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012043 - - - - 责任原则 - concept - 责任原则是确定支出成本中心归属的原则之一。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012043 - - - - 受益原则 - concept - 受益原则是确定支出成本中心归属的原则之一。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012044 - - - - 结算起点 - data - 结算起点是1000元,低于此金额且无法直接结算的小额支出需附付款凭据截图。 - chunk-061324cc36078214691a6fc1cd0aaeea - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012044 - - - - Procurement Management Regulations - chunk-74c01decac4a10cd40a491786743b0ee - Operating departments must strictly follow company bidding, procurement, and material management regulations for procurement of materials and services. - UNKNOWN - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012081 - - - - Tax Authority Recognized Invoice - chunk-74c01decac4a10cd40a491786743b0ee - Expenditures other than employee remuneration, personal service compensation, travel allowance, special subsidy, and current account payment require tax authority-recognized invoices. - UNKNOWN - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012094 - - - - Financial Review - chunk-74c01decac4a10cd40a491786743b0ee - Financial review can reject applications if documents are incomplete, incorrectly filled, or non-compliant. - UNKNOWN - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012085 - - - - 1.0 - 远光软件股份有限公司is the issuer of the expense reimbursement regulations, with Chapter 1 General Provisions establishing the foundational framework. - organizational hierarchy,regulation issuer - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012088 - - - - 1.0 - 远光软件股份有限公司established the Planning and Finance Department to handle financial management and expense-related responsibilities. - financial authority,organizational structure - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012076 - - - - 1.0 - Yuan Guang Software Co., Ltd. has implemented changes in reimbursement standards as part of its internal policy adjustments. - company governance,policy adjustment - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012084 - - - - 1.0 - 远光软件股份有限公司issued the Company Expenditure Management Measures (2024) to regulate company expenditures. - document issuance,policy implementation - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012085 - - - - 1.0 - 远光软件股份有限公司assigned document number远光制度〔2024〕14号to the expenditure management measures. - document numbering,regulatory framework - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012086 - - - - 1.0 - Article 3 Management Principles provides the foundational principles for the entire expense reimbursement system. - chapter article relationship,regulatory foundation - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012076 - - - - 1.0 - The designated management department determines expenditure scope, standards, methods, and management processes in accordance with company financial, procurement, and human resources policies. - department responsibility,regulatory compliance - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012079 - - - - 1.0 - 控股子公司must report expenditure management measures to计划财务部for filing after approval. - compliance,reporting relationship - chunk-dd87aa5bc62cc9587ecb4c26d35a5263 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012099 - - - - 1.0 - Article 11 Petty Cash Loans is one of the key expense management regulations under Chapter - chapter article relationship,specific regulation - chunk-aa5435156b829944c173fa1d2d7a93d4 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012088 - - - - 1.0 - 办公室(党委办公室)manages party building and official vehicle expenses according to the company expense management policy. - expense management,policy execution - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012088 - - - - 1.0 - 工会委员会manages trade union expenses according to the company expense management policy. - expense management,policy execution - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012079 - - - - 1.0 - 营销中心manages bidding - expense management,policy execution - chunk-afc57a0e9548d1f484da6df6c182676b - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012079 - - - - 1.0 - 组织人事部负责确定调动工作人员的报销标准,对异地挂职锻炼等人员的费用报销进行规范管理。 - 标准制定、人事管理 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012090 - - - - 1.0 - 商旅系统是公司差旅费管理的核心平台,用于统一预定和审批差旅相关费用。 - 系统支持、费用管控 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012086 - - - - 1.0 - 业务招待费是公司费用管理体系的重要组成部分,与差旅费并列作为公司主要费用报销项目。 - 费用类别、公司运营 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012089 - - - - 1.0 - 会议费是公司费用管理体系的重要组成部分,与差旅费并列作为公司主要费用报销项目,需要事前审批并附业务佐证材料。 - 费用类别、公司运营 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012092 - - - - 1.0 - The reimbursement standard changes include the cancellation of official vehicle subsidy reimbursement since it has been incorporated into wages. - expense reduction,policy modification - chunk-18d968b78afe916b419c1b5973421ebe - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012085 - - - - 1.0 - Management personnel at all levels exercise approval authority within the company's expenditure authorization approval scope. - hierarchical authority,organizational structure - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012076 - - - - 1.0 - The centralized management department implements expenditure centralized management on behalf of the company. - centralized management,departmental responsibility - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012077 - - - - 1.0 - The Planning and Finance Department assists the centralized management department and is responsible for financial reimbursement auditing and payment settlement. - departmental responsibility,financial oversight - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012077 - - - - 1.0 - The operating department (individual) conducts expenditure business activities within the company's authorization. - business execution,departmental responsibility - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012078 - - - - 1.0 - Operating departments must strictly follow company bidding, procurement, and material management regulations for procurement of materials and services. - business execution,procurement compliance - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012083 - - - - 1.0 - The operator submits the expenditure reimbursement application through the financial information system and provides business original documents. - documentation responsibility,workflow initiation - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012079 - - - - 1.0 - The operator is responsible for obtaining real - authenticity responsibility,document preparation - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012094 - - - - 1.0 - The operator has three working days to supplement incomplete business original documents after financial review. - documentation compliance,workflow timeline - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012083 - - - - 1.0 - Financial review can reject applications if documents are incomplete, incorrectly filled, or non-compliant. - document verification,rejection handling - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012088 - - - - 1.0 - Aggregated VAT invoices must be accompanied by tax control system detail lists. - documentation requirement,tax compliance - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012079 - - - - 1.0 - Expenditures other than employee remuneration, personal service compensation, travel allowance, special subsidy, and current account payment require tax authority-recognized invoices. - compliance requirement,documentation standard - chunk-74c01decac4a10cd40a491786743b0ee - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012082 - - - - 1.0 - 公司通过第十七条对培训费进行规范管理。 - 合规管理,规章制定 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012084 - - - - 1.0 - 公司通过第十八条对通信费进行规范管理。 - 合规管理,规章制定 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012084 - - - - 1.0 - 公司通过第十九条对邮递费进行规范管理。 - 合规管理,规章制定 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012084 - - - - 1.0 - 公司通过第二十条对薪酬福利支出进行规范管理。 - 薪酬管理,规章制定 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012086 - - - - 1.0 - 公司通过第二十一条对对外捐赠支出进行规范管理。 - 捐赠管理,规章制定 - chunk-e9438f69c9e221d9f0f00a05ad84eac6 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012089 - - - - 1.0 - Departments and units must implement the night high-speed rail provision allowing employees to choose sleeper seats when high-speed train travel exceeds 6 hours at night. - policy implementation,travel standards - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012084 - - - - 1.0 - Departments and units are responsible for strictly managing taxi usage, limiting reimbursements to emergency situations, client接送, late night work, and other special circumstances. - cost control,policy enforcement - chunk-613d6dfd4c5e9c807229a3147f96b584 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012099 - - - - 1.0 - 第十四条专门定义了业务招待费的概念和报销要求,明确了接待客户和相关单位的餐饮等合理支出范围。 - 条款规定、费用管理 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012092 - - - - 1.0 - 第十五条专门规定了会议费的报销范围和报销要求,包括公司主办或承办会议以及参加外部会议的报销标准。 - 条款规定、费用管理 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012092 - - - - 1.0 - 公司总裁对经费预算30000元及以上的内部会议具有审批权限,体现了公司对大额会议支出的管控。 - 审批权限、预算管理 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012093 - - - - 1.0 - 第十六条专门规定了广告宣传费的定义和管理要求,包括广告费和业务宣传费两类。 - 条款规定、费用管理 - chunk-d26b288ed4001dc5c504dce0eb841362 - /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf - 1779012092 - - - - + + + + + + + + + + + + + + + + + + + 远光软件股份有限公司 + organization + 远光软件股份有限公司is a company that issued the Company Expenditure Management Measures (2024) to regulate expenditure and reimbursement standards.<SEP>Yuan Guang Software Co., Ltd. is a company that has established internal expense reimbursement and management regulations.<SEP>YuanGuang Software Co., Ltd. is the company that issued the expense reimbursement management regulations to optimize business development, standardize expenditure and reimbursement processes, and prevent operational risks. + chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第一章总则 + content + Chapter 1 General Provisions contains the purpose, scope, and management principles of the expense reimbursement regulations. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第二条目的 + content + Article 2 Purpose describes the objective of adapting to business development needs, optimizing expenditure and reimbursement standards, and standardizing approval processes. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第二条范围 + content + Article 2 Scope defines the applicable scope of the expense reimbursement regulations. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第三条管理原则 + content + Article 3 Management Principles establishes the principles for managing the expense reimbursement system. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第二章职责分工 + content + Chapter 2 Responsibilities Division outlines the responsibilities of various departments and personnel in the expense management system. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第四条归口管理部门主要职责 + content + Article 4 Main Responsibilities of the Designated Management Department specifies the duties of the designated management department. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 计划财务部 + organization + 计划财务部is a department of远光软件股份有限公司with main responsibilities related to financial planning and expenditure management.<SEP>计划财务部负责制定、修订、解释及实施协调工作。<SEP>Planning and Finance Department is a key management department responsible for expense reimbursement duties.<SEP>Planning and Finance Department is one of the departments responsible for financial management and expense approval, determining expenditure scope, standards, methods, and management processes.<SEP>计划财务部is responsible for employee advance funds, travel expenses, transportation, foreign travel, meal allowances, taxes, audits, and financial expenses. + chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011991 + + + + 第五条计划财务部主要职责 + content + Article 5 Main Responsibilities of Planning and Finance Department outlines the specific duties of the Planning and Finance Department. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011992 + + + + 第六条经办部门(个人)主要职责 + content + Article 6 Main Responsibilities of Operating Departments (Individuals) describes the duties of operating departments and individuals handling expenses. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011992 + + + + 第七条各级管理人员主要职责 + content + Article 7 Main Responsibilities of Management Personnel at All Levels specifies the duties of managers at all levels. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011992 + + + + 第三章支出报销申请与审批 + content + Chapter 3 Expense Reimbursement Application and Approval covers the processes and regulations for expense reimbursement applications and approvals. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011992 + + + + 第八条支出报销申请 + content + Article 8 Expense Reimbursement Application establishes the procedures for submitting expense reimbursement requests. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011992 + + + + 第九条支出报销审批 + content + Article 9 Expense Reimbursement Approval defines the approval process for expense reimbursement requests. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011993 + + + + 第十条支出成本中心归属 + content + Article 10 Cost Center Attribution determines which cost centers expenses should be attributed to. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011993 + + + + 第四章重点支出管理规定 + content + Chapter 4 Key Expense Management Regulations contains specific management provisions for important types of + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第十一条备用金借款 + content + Article 11 Petty Cash Loans provides management regulations for petty cash borrowing. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第十二条市内交通费 + content + Article 12 Local Transportation Fees specifies the regulations for local transportation expense reimbursement. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第十三条差旅费 + content + Article 13 Travel Expenses establishes the management regulations for travel-related expenses. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第十四条业务招待费 + content + Article 14 Business Entertainment Expenses provides management regulations for business entertainment costs. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第五章附则 + content + Chapter 5 Supplementary Provisions contains the final provisions including administration, implementation, and attachments of the regulations. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第二十三条本办法的归口与实施 + content + Article 23 Administration and Implementation designates the department responsible for administration and specifies the effective date of these measures. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011994 + + + + 第二十四条附件 + content + Article 24 Appendices indicates that supplementary materials and attachments are part of these regulations. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011995 + + + + 交通工具等级标准 + data + Transportation Level Standards is a data table specifying the permitted transportation modes (airplane, train, ship, other transportation) based on employee rank levels. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011995 + + + + 出差补贴标准 + data + Travel Allowance Standards is a data table detailing meal allowances, basic travel allowances, and total allowances by region (Hong Kong/Macau/Taiwan, municipalities/special zones/Tibet, other regions, and foreign countries). + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011996 + + + + 经济舱 + data + Economy Class is the permitted airplane class for all employee levels according to transportation standards. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011996 + + + + 火车硬席 + data + Hard Seat Train is a permitted train transportation mode including hard sleeper, hard seat, high-speed rail second-class, and all soft-seat train second-class soft seats. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011996 + + + + 三等舱 + data + Third Class Cabin is the permitted ship accommodation class for all employee levels. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011996 + + + + 凭据报销 + method + Receipt-Based Reimbursement is the method for other transportation expenses (excluding cars), requiring submission of receipts for reimbursement. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + 餐补 + data + 餐补是指出差人员因工作需要在外出差期间的餐饮费用补贴,根据地区不同分为75元、65元、55元三个标准,但西藏地区标准为140元。组织安排统一安排餐食的情况下不再报销餐补。<SEP>Meal Allowance is a type of travel subsidy that varies by region: 75 for Hong Kong/Macau/Taiwan, 65 for municipalities/special zones/Tibet, 55 for other regions, and 140 for foreign countries. + chunk-aa5435156b829944c173fa1d2d7a93d4<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + 基本补助 + data + Basic Allowance is a standard travel subsidy of 35 regardless of region. + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + 公司支出管理办法 + concept + 公司支出管理办法is a corporate expense management policy document that outlines reimbursement standards, approval authority, and departmental responsibilities for expense management. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + 办公室(党委办公室) + organization + 办公室(党委办公室)is a department responsible for party building expenses and official vehicle expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + 工会委员会 + organization + 工会委员会is responsible for trade union expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + 营销中心 + organization + 营销中心is responsible for bidding business expenses, institutional marketing expenses, customer training, and sales refund expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + 品牌及市场运营中心 + organization + 品牌及市场运营中心是对外捐赠支出的归口管理部门,负责审核捐赠申请。<SEP>品牌及市场运营中心is responsible for advertising expenses, business promotion expenses, and external donation expenses. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + 组织人事部 + organization + 组织人事部负责职工福利费的年度计划管理,并规定薪酬福利支事的审批程序标准。<SEP>组织人事部是公司负责人事管理的部门,确定调动工作人员的报销标准。<SEP>Personnel Department determines reimbursement standards for assigned personnel working at remote locations.<SEP>组织人事部is responsible for salary and benefits (excluding canteen and meal allowances), external labor, employee insurance, relocation allowances, and other benefits expenses. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011999 + + + + 人力资源服务部 + organization + 人力资源服务部is responsible for recruitment business and employee education training expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + 产业投资部 + organization + 产业投资部is responsible for equity investment expenses and merger business expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + 证券与法律事务部 + organization + 证券与法律事务部is responsible for legal affairs expenses, listing information disclosure expenses, and trademark registration expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + 产品规划设计部 + organization + 产品规划设计部is responsible for intellectual property expenses, information technology consulting expenses, and research and development expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + DAP研发中心 + organization + DAP研发中心is responsible for testing, evaluation, and analysis expenses paid to external units during research and development. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 信息管理部 + organization + 信息管理部is responsible for IT asset acquisition, leasing, operations, maintenance, repairs, and network usage expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 后勤服务部 + organization + 后勤服务部is responsible for non-IT asset management, canteen expenses, office expenses, and business travel settlement. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 审批权限 + concept + 审批权限refers to the approval authority levels for different management positions regarding expense approvals within their responsibilities. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 报销标准 + concept + 报销标准refers to reimbursement standards including travel expense transportation methods and allowances. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012002 + + + + 差旅费 + concept + Travel expenses is a category of expenditure managed under the measures.<SEP>差旅费是指因公出差发生的各项费用,包括交通费、住宿费、餐补等,公司制定了详细的报销规定和注意事项。<SEP>差旅费是指员工因公出差所发生的交通费、住宿费、出差补贴等费用。<SEP>Travel expenses are regulated under the key expense management provisions, with specific standards for business trips.<SEP>差旅费refers to travel expenses including transportation standards and allowances for company leadership. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012002 + + + + 全资子公司 + organization + 全资子公司is a subsidiary type wholly owned by远光软件股份有限公司, included in the scope of the expenditure management measures.<SEP>全资子公司refers to wholly-owned subsidiaries whose approval authority is managed by parent company personnel. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012003 + + + + 人事归口管理部门 + organization + 人事归口管理部门refers to the centralized HR management department responsible for salary, bonuses, and benefits approval. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012003 + + + + 母公司 + organization + 母公司refers to the parent company that oversees subsidiary management and authorization. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012003 + + + + 逐级审批规则 + concept + 逐级审批规则refers to the level-by-level approval rules that form the basis of business workflow execution, following organizational relationships. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 终审岗 + concept + 终审岗refers to the final approval position in the hierarchical approval process. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 财务入账条件 + concept + 财务入账条件refers to the conditions required for financial recording of settlement business expenses. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 薪酬福利支出分配计划 + concept + 薪酬福利支出分配计划refers to the salary and benefits expense distribution plan approved according to HR management department regulations. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 公司1号文 + content + 公司1号文refers to Company Document No. 1 which specifies job authorization standards for subsidiary department managers. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 总监 + concept + 总监refers to the director-level position with corresponding approval authority for subsidiary management. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 一级部门总经理 + concept + 一级部门总经理refers to the level-1 department general manager position with corresponding approval authority. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + P8 + concept + P8 refers to the position level of company leadership in the organizational hierarchy. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 报销标准变化情况 + concept + Reimbursement standard changes is a key topic in the regulations, adjusting reimbursement rules for official vehicle subsidies and other expenses.<SEP>报销标准变化情况refers to the changes in reimbursement standards, including travel expense transportation and allowance modifications. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 取消报销规定 + concept + 取消报销规定refers to the cancelled reimbursement regulations, such as the official vehicle allowance now included in salary. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 新增报销规定 + concept + 新增报销规定refers to new reimbursement regulations added to the company expense management policy. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 因公用车补贴 + concept + 因公用车补贴refers to the official vehicle allowance that was cancelled and integrated into salary payment. + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012006 + + + + 异地挂职锻炼补贴标准 + concept + Remote assignment training allowance standards define the reimbursement rules for transportation, meal allowances, and basic subsidies during temporary assignments.<SEP>异地挂职锻炼补贴标准refers to the subsidy standard for off-site training and development exercises arranged by the organization. + chunk-afc57a0e9548d1f484da6df6c182676b<SEP>chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + 组织安排 + concept + 组织安排refers to organizational arrangements for employee training, relocation, + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + Company + organization + The company is an organization that establishes expenditure management principles, approval authority, and departmental responsibilities for financial operations. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + Management Personnel At All Levels + person + Management personnel at all levels should fulfill expenditure reimbursement approval authority within authorized approval scope and responsibilities, bearing approval responsibilities. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + Centralized Management department + organization + The centralized management department is responsible for determining expenditure scope, standards, methods, and management processes for various expenditure businesses based on business management needs. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + Planning and Finance Department + organization + The Planning and Finance Department is responsible for clarifying expenditure reimbursement approval processes, audit points, and reimbursement documentation standards. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + Operating Department Individual + person + The operating department (individual) is responsible for conducting expenditure business activities within departmental job responsibilities and authorized business scope, adhering to budget-first, thrifty, and benefit-priority principles. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012008 + + + + Operator + person + The operator should timely fill out system documents, submit business original documents, obtain real, compliant, related, and complete business original documents, and verify invoice authenticity. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + First Approver + person + The first approver should comprehensively audit the authenticity, compliance, necessity, and rationality of expenditure business, and bear approval responsibility. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + Subsequent Approver + person + The subsequent approver should audit the necessity and rationality of expenditure business, and bear approval responsibility. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + Financial Information System + artifact + The financial information system is the platform through which expenditure reimbursement applications are submitted via system documents. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + Business Original Documents + data + Business original documents are the supporting materials for expenditure reimbursement, including invoices and related vouchers that must be complete, compliant, related, and complete. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + VAT Special Invoice + content + VAT special invoice is a tax document required for most expenditures, and if not obtained when required, the operator should explain the reason in the system document. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + Expenditure Reimbursement Application + concept + The expenditure reimbursement application is submitted through the financial information system, and financial原则上does not accept paper application documents. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + Expenditure Authorization Approval Scope + concept + Expenditure authorization approval scope refers to the authorized limits for expenditure approval at various management levels. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011997 + + + + Separation of Approval and Processing Principle + concept + The separation of approval and processing principle requires that the expenditure operator and approver cannot be the same person. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + Three Flows Consistency Principle + concept + The three flows consistency principle requires alignment of invoice flow, capital flow, and material flow during business operations. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + Employee Remuneration + concept + Employee remuneration is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011998 + + + + Personal Service Compensation + concept + Personal service compensation is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011999 + + + + Travel Allowance + concept + Travel allowance is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011999 + + + + Special Subsidy + concept + Special subsidy is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011999 + + + + Current Account Payment + concept + Current account payment is a type of expenditure that does not require tax authority-recognized invoices for reimbursement. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779011999 + + + + Trade Union Fund + concept + Trade union fund is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Employee Welfare + concept + Employee welfare is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Staff Activities + concept + Staff activities is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Business Entertainment + concept + Business entertainment is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Transportation Tickets + concept + Transportation tickets is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Government Fees + concept + Government fees is a type of expenditure exempt from VAT special invoice requirements. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Tax Control System Details + data + Tax control system details is a supplementary document required for aggregated VAT invoices. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + Three Working Days Deadline + concept + The three working days deadline refers to the timeframe within which the operator must supplement incomplete business original documents after financial review. + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + 广告费 + concept + 广告费是指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业务佐证材料。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012000 + + + + 业务宣传费 + concept + 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 培训费 + concept + Training expenses is a category of expenditure managed under the measures.<SEP>培训费是指公司安排、主办或承办培训发生的学费、书籍杂费、师资费、资料费、场地费、设备材料费、住宿费、交通费等,以及员工根据工作需要,经归口管理部门批准参加外部培训、考证、教育产生的相关费用。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012001 + + + + 通信费 + concept + Communication fees is a category of expenditure managed under the measures.<SEP>通信费是指为满足公司日常办公需要发生的电话、传真、集团网、网络连接等支出。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012002 + + + + 邮递费 + concept + 邮递费是指公司因业务需要邮递物品而支付的费用,包括邮件费、托运费、快递费等。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012004 + + + + 薪酬福利支出 + concept + 薪酬福利支出是指公司相关制度规定的职工薪资、奖励提成、福利费支出,以及临时的奖励及福利支出。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 对外捐赠支出 + concept + 对外捐赠支出是指公司对外捐赠的费用,由品牌及市场运营中心归口管理,需严格预算单控。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 涉外业务汇率标准 + concept + 涉外业务汇率标准规定以外币结算时按支付凭据所载汇率折算,未载明汇率的按中国银行外汇折算价执行。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012005 + + + + 公司员工教育培训管理办法 + content + 《公司员工教育培训管理办法》是规定培训费报销资格认定标准的制度文件。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012006 + + + + 公司员工因公通讯费用实施细则 + content + 《公司员工因公通讯费用实施细则》是规定员工通讯费执行标准的制度文件。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012006 + + + + 公司团建管理办法 + content + 《公司团建管理办法》是规定职工活动支出执行标准的制度文件。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012006 + + + + 工会经费管理办法 + content + 《工会经费管理办法》是规定职工活动支出执行标准的制度文件。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012006 + + + + 中国银行外汇折算价 + data + 中国银行外汇折算价是涉外业务汇率折算的标准依据。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012019 + + + + 中国外汇交易中心参考汇率 + data + 中国外汇交易中心参考汇率是中国银行无折算价币种的备选汇率依据。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012007 + + + + 第十七条 + concept + 第十七条规定培训费的定义、范围及报销标准。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012008 + + + + 第十八条 + concept + 第十八条规定通信费的定义及执行标准。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012008 + + + + 第十九条 + concept + 第十九条规定邮递费的定义及报销凭据要求。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012008 + + + + 第二十条 + concept + 第二十条规定薪酬福利支出的分类、执行标准及审批程序。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012008 + + + + 第二十一条 + concept + 第二十一条规定对外捐赠支出的归口管理、预算控制及审批流程。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + 第二十二条 + concept + 第二十二条规定涉外业务汇率标准及结算方式。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + 第二十三条 + concept + 第二十三条规定办法的归口管理部门及实施时间。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + 第二十四条 + concept + 第二十四条规定办法的附件内容。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + 附表1:员工支出报销审批权限表 + content + 附表1规定了员工支出报销的审批权限表,明确各部门经理、总监、副总裁等审批层级及权限。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012009 + + + + 附表2:岗位支出报销审批权限表 + content + 附表2规定了岗位支出报销的审批权限表。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012011 + + + + 附表3:支出归口管理部门与归口业务范围 + content + 附表3规定了支出归口管理部门及其对应的业务范围。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012011 + + + + 业务招待 + concept + 业务招待是员工支出类别之一,对应审批权限额度分别为0.5万元、1万元、2万元、3万元、15万元。<SEP>业务招待是报销审批权限表中的支出项目类别。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6<SEP>chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + 报销资格 + concept + 报销资格是指员工申请费用报销所需满足的条件和标准。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + 捐赠申请 + concept + 捐赠申请是指业务部门向公司提出对外捐赠申请的文件,需说明捐赠事由、对象、金额等内容。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + 预算调整决策程序 + concept + 预算调整决策程序是指针对未纳入预算的对外捐赠事项所履行的预算调整审批流程。 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + Departments And Units + organization + Departments and units are organizational entities within the company that must strictly enforce travel approval procedures and control the number and duration of business trips. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + Company Business Travel System + concept + The company business travel system is an internal approval platform through which employees below department deputy level must obtain pre-approval for business travel. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + Transportation Level Standards + concept + Transportation level standards is a table specifying the allowed transportation modes and classes for employees based on their rank for domestic and international travel. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012012 + + + + Hotel Accommodation Standards + concept + Hotel accommodation standards are tables specifying the maximum hotel accommodation expenses allowed for employees based on their rank and destination. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012013 + + + + Travel Allowance Standards + concept + Travel allowance standards are tables specifying the daily allowance amounts for employees, including meal allowances and basic travel allowances, categorized by travel destination. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012013 + + + + Company Property Rental Management + concept + Company property rental management refers to a policy document that governs the application and reimbursement standards for remote work housing. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012013 + + + + Business Trip Approval + concept + Business trip approval is a required procedural step that employees must follow before undertaking official travel. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012013 + + + + Company Leadership + person + Company leadership refers to the highest-ranking executives (P8 and above) who have the most generous travel expense standards. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012013 + + + + Senior Managers + person + Senior managers (P7 level) are executives with the second-highest travel expense standards after company leadership. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012014 + + + + Middle Managers + person + Middle managers (P5-P6 level) and external experts have moderate travel expense standards. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012014 + + + + Basic Level Managers + person + Basic level managers (P4) are supervisors with lower travel expense standards and additional approval requirements for air travel. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012015 + + + + Other Employees + person + Other employees (P1-P3 level) have the most restrictive travel expense standards and must fly economy class at 50% discount or below. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012015 + + + + Remote Work Housing + artifact + Remote work housing refers to accommodation provided to employees on long-term business travel assignments, governed by company rental management policies. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012015 + + + + Commercial Insurance + content + Commercial insurance including transportation insurance has been purchased by the company for employees, and insurance fees during business trips are non-reimbursable. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012015 + + + + Transportation Cost Reimbursement + concept + Transportation cost reimbursement covers expenses for air travel, train, ship, and other transportation modes based on employee rank and travel standards. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012016 + + + + Accommodation Cost Reimbursement + concept + Accommodation cost reimbursement covers hotel lodging expenses according to location category and employee rank, with approval required for excess amounts. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012016 + + + + Hong Kong, Macau, And Taiwan Region + location + Hong Kong, Macau, and Taiwan region is a specific travel destination category in the expense reimbursement tables, listed separately from domestic mainland destinations. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012016 + + + + Directly-Controlled Municipalities And Special Administrative Regions + location + Directly-controlled municipalities and special administrative regions refer to major cities like Beijing, Shanghai, Tianjin, Chongqing, and Shenzhen that have higher accommodation standards than provincial capitals. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012016 + + + + Provincial Capitals + location + Provincial capitals are cities designated as provincial-level administrative centers that have moderate accommodation standards in the policy. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012016 + + + + Other Areas + location + Other areas refer to locations outside directly-controlled municipalities, special administrative regions, and provincial capitals that have the lowest accommodation standards in the policy. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012017 + + + + Night High-Speed Rail Provision + concept + Night high-speed rail provision is a special allowance permitting employees to choose sleeper seats when traveling on high-speed trains for 6 hours or more at night. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012017 + + + + Taxi Usage Regulations + concept + Taxi usage regulations specify the limited circumstances under which taxi expenses can be reimbursed, including emergency official business, client接送, late night work after 22:00, and other special circumstances. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012017 + + + + Self-Driving Travel Provisions + concept + Self-driving travel provisions outline the reimbursement rules for road tolls, parking fees, fuel costs, and electricity expenses when employees use personal vehicles for business travel, with department head approval required. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012017 + + + + Remote Work Housing Rental Expenses + data + Remote work housing rental expenses include rent, initial housing configuration costs, property management fees, heating fees, utility and gas expenses including shared portions, and internet broadband fees. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012018 + + + + External Conference Accommodation + concept + External conference accommodation refers to hotel expenses when employees attend conferences or training sessions, which may be reimbursed at the conference organizer's stated standards if accommodations are uniformly arranged. + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012018 + + + + 取消报销规定内容 + concept + The content about canceling reimbursement regulations refers to the removal of official vehicle subsidy-related expressions since the subsidy has been included in wages. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012018 + + + + 新增规定内容 + concept + Newly added regulations content covers remote assignment allowance standards and standardized business travel booking procedures. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012018 + + + + 商旅订票规范 + concept + Business travel booking standards specify that official business trips should use the commercial travel system for unified booking. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012018 + + + + 审批权限变化情况 + concept + Changes in approval authority mainly involve adjusting bid security deposit approval limits and clarifying expense approval workflow procedures. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012019 + + + + 投标保证金 + data + Bid security deposit is a type of expense requiring approval, with different approval limits assigned to different management levels. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012019 + + + + 审批流转程序 + method + Approval workflow process is the multi-level approval rule based on organizational relationships, with provisions for special matters to bypass levels. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012019 + + + + 出差规定 + concept + Business travel regulations define the reimbursement standards for transportation, meal allowances, and basic subsidies during transit periods. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012019 + + + + 财务信息化系统 + method + Financial information system is the system used for submitting expense reimbursement applications electronically. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012020 + + + + 支出报销申请与审批 + concept + Expenditure reimbursement application and approval is a key process regulated by the measures.<SEP>Expense reimbursement application and approval is a chapter of the regulations covering application methods and approval processes. + chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012020 + + + + 重点支出管理规定 + concept + Key expense management regulations is a chapter covering petty cash loans, local transportation fees, and travel expenses. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012020 + + + + 备用金借款 + concept + Petty cash loans is a category of key expenditure regulated by the measures.<SEP>备用金借款是公司借支给正式员工用于支付与公司经济业务相关且必须预支的费用,遵循"前款不清、后款不借"原则。<SEP>Petty cash loan is one of the key expense items regulated under the management provisions. + chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012020 + + + + 市内交通费 + concept + Local transportation fees is a category of expenditure managed under the measures.<SEP>市内交通费指员工为公司生产经营活动在工作所在地发生的交通费用,不包括正常上下班交通费。<SEP>Local transportation fees are regulated under the key expense management provisions. + chunk-18d968b78afe916b419c1b5973421ebe<SEP>chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012022 + + + + 审批权限表 + content + Approval authority table is an attachment showing approval limits for different management positions regarding bid security deposits. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012022 + + + + 1 Yuan Per Person Per Kilometer Reimbursement + data + 1 yuan per person per kilometer reimbursement standard is the new rule for official vehicle subsidy calculation, with amounts exceeding the standard to be borne by the employee. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Official Vehicle Subsidy + content + Official vehicle subsidy has been included in wages, leading to the cancellation of related reimbursement regulations. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Department Manager + person + Department Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Director + person + Director is a management position with approval authority for bid security deposits up to 50,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + First-Level Department General Manager + person + First-Level Department General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Institution General Manager + person + Institution General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Business Division General Manager + person + Business Division General Manager is a management position with approval authority for bid security deposits up to 50,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Vice President + person + Vice President is a senior management position with approval authority for bid security deposits up to 100,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Chief Engineer + person + Chief Engineer is a senior management position with approval authority for bid security deposits up to 100,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012023 + + + + Senior Vice President + person + Senior Vice President is a senior management position with approval authority for bid security deposits up to 200,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012024 + + + + President + person + President is the highest executive position with approval authority for bid security deposits up to 500,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012024 + + + + Committee Chairpersons + person + Committee Chairpersons are senior management positions with approval authority for bid security deposits up to 200,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012024 + + + + Bid Security Deposit Approval Limits Table + content + The bid security deposit approval limits table specifies different approval authority limits for various management positions, ranging from 50,000 to 5,000,000 yuan. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012024 + + + + 50000 Yuan Approval Limit + data + 50,000 yuan is the bid security deposit approval limit for department managers, directors, and first-level department general managers. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012024 + + + + 100000 Yuan Approval Limit + data + 100,000 yuan is the bid security deposit approval limit for vice presidents and chief engineers. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012025 + + + + 200000 Yuan Approval Limit + data + 200,000 yuan is the bid security deposit approval limit for senior vice presidents and committee chairpersons. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012025 + + + + 5000000 Yuan Approval Limit + data + 5,000,000 yuan is the bid security deposit approval limit for the president. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012025 + + + + Commercial Travel System + method + Commercial travel system is the unified booking platform that should be used for official business travel arrangements. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012026 + + + + Multi-Level Approval Rule + method + Multi-level approval rule is based on organizational relationships, requiring expenses to be approved sequentially through management levels. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012026 + + + + Final Approval Position + person + Final approval position is the highest approval authority that can be bypassed for special matters after decision-making. + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012026 + + + + Company Hotel Accommodation Limit Standards + data + Company hotel accommodation limit + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012026 + + + + 公司支出管理办法(2024) + content + 公司支出管理办法(2024)is a regulation issued by远光软件股份有限公司to optimize expenditure and reimbursement standards, standardize approval processes, and prevent operational risks. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012026 + + + + 国家电网公司 + organization + 国家电网公司is a reference entity whose management regulations are consulted in the development of the expenditure management measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 国网数科公司 + organization + 国网数科公司is a reference entity whose management regulations are consulted in the development of the expenditure management measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 办法 + concept + The original "Company Expenditure Management Measures" that has been superseded by the 2024 version. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 2024年4月17日 + event + 2024年4月17日is the date when the Company Expenditure Management Measures (2024) were issued by远光软件股份有限公司. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012027 + + + + 预算先行 + concept + Budget-first is one of the core management principles in the expenditure management system, requiring expenditures to be within budget targets. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012028 + + + + 厉行节约 + concept + Rigorous economy is a management principle requiring departments and positions to adhere to cost-saving and efficiency-first principles. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012028 + + + + 分级授权 + concept + Hierarchical authorization is a management principle where management personnel exercise expenditure approval authority based on their positions. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012028 + + + + 分类控制 + concept + Classified control is a management principle for standardized expenditure business activities. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012028 + + + + 批办分离 + concept + Separation of approval and execution is a management principle in the expenditure management system. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 业务招待费 + concept + Business entertainment expenses is a category of expenditure managed under the measures.<SEP>业务招待费是指各级单位正常生产经营管理过程中需要发生的、用于必要招待的各项费用,包括用于接待客户和相关单位的餐饮等合理支出。 + chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 会议费 + concept + Meeting expenses is a category of expenditure managed under the measures.<SEP>会议费是指公司主办或承办会议发生的会场租金、文件资料、设备租金等支出,以及参加外部会议所发生的会务相关支出。 + chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 广告宣传费 + concept + Advertising expenses is a category of expenditure managed under the measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 控股子公司 + organization + 控股子公司is a controlled subsidiary of远光软件股份有限公司that should follow the expenditure management measures and report to计划财务部. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 分支机构 + organization + 分支机构refers to non-legal-person branches of远光软件股份有限公司included in the scope of the expenditure management measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 经办部门 + organization + 经办部门is a department or individual responsible for handling expenditure business activities under the measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012029 + + + + 各级管理人员 + concept + 各级管理人员refers to management personnel at all levels who exercise expenditure approval authority within their authorization scope. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012030 + + + + 归口管理部门 + organization + 归口管理部门is the central management department responsible for expenditure management under the measures. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012044 + + + + 远光制度〔2024〕14号 + content + 远光制度〔2024〕14号is the document number assigned to the Company Expenditure Management Measures (2024) issued by远光软件股份有限公司. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012044 + + + + 修订说明 + content + 修订说明is an appendix to the Company Expenditure Management Measures (2024) explaining the revisions made. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012032 + + + + 员工支出报销审批权限表 + content + 员工支出报销审批权限表is an appendix table defining employee-level expenditure reimbursement approval authority.<SEP>员工支出报销审批权限表是一份规定公司员工各项支出报销审批层级的制度文件,详细列示了不同支出项目的审批权限金额标准。 + chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012032 + + + + 岗位支出报销审批权限表 + content + 岗位支出报销审批权限表is an appendix table defining position-level expenditure reimbursement approval authority.<SEP>岗位支出报销审批权限表是一份规定公司各岗位支出报销审批层级的制度文件,涵盖资本性支出、收益性支出、财务专用等类别。 + chunk-dd87aa5bc62cc9587ecb4c26d35a5263<SEP>chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012032 + + + + 支出归口管理部门与归口业务范围 + content + 支出归口管理部门与归口业务范围is an appendix table defining the mapping between central management departments and their corresponding business scopes. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012033 + + + + 效益优先 + concept + 效益优先is a management principle requiring departments and positions to prioritize efficiency in expenditure activities. + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012033 + + + + 公司各部门 + organization + 公司各部门refers to all departments within远光软件股份有限公司, which are recipients of the Company Expenditure Management Measures (2024). + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012033 + + + + 因公借款 + concept + 因公借款是员工支出类别之一,对应审批权限额度分别为0.5万元、1万元、2万元、3万元、15万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012033 + + + + 其他支出(员工) + concept + 其他支出是员工支出类别之一,对应审批权限额度分别为1万元、2万元、3万元、5万元、50万元,并包含差旅费、市内交通、客服及商务等补充说明。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012034 + + + + 资产采购 + concept + 资产采购属于资本性支出类别,包括固定资产、无形资产、低值易耗品采购,对应审批权限为1万元、2万元、10万元、15万元、200万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012034 + + + + 基建工程 + concept + 基建工程属于资本性支出类别,对应审批权限为5万元、10万元、50万元,但前两级审批权限为空。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012034 + + + + 股权投资、兼并收购 + concept + 股权投资、兼并收购属于资本性支出类别,全部审批权限为空,需由董事长单独审批。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 材料采购 + concept + 材料采购属于收益性支出类别,包括生产采购(原材料、辅助材料、机物料)和项目采购(设备、软件、公有云资源),对应审批权限为10万元、20万元、30万元、500万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 分包外包(内部单位) + concept + 分包外包(内部单位)属于收益性支出类别,包括研发、实施、运维、服务等分包外包,对应审批权限为10万元、20万元、30万元、500万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 分包外包(外部单位) + concept + 分包外包(外部单位)属于收益性支出类别,包括委托加工、项目土建装修等,对应审批权限为10万元、20万元、30万元、200万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 保证金 + concept + 保证金属于收益性支出类别,包括投标保证金、质保金、履约保证金及招标相关保证金,对应审批权限为5万元、50万元、100万元、200万元、500万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 销售退款 + concept + 销售退款属于收益性支出类别,包括销售退款和代付款(代收后),对应审批权限为5万元、10万元、30万元、50万元、200万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 房屋租金 + concept + 房屋租金属于收益性支出类别,不含水电及杂费,需经业务归口部门审批,对应审批权限为5万元、10万元、20万元、30万元、200万元。 + chunk-9841d66d8fb8548aab40220663a51693 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 基本出差补贴 + concept + 基本出差补贴是指出差期间每天给予的基本生活补助,标准为35元,适用于所有出差地区。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012048 + + + + 商旅系统 + artifact + 商旅系统是公司统一的差旅预订和审批平台,出差人员原则上应通过该系统预定交通和住宿,特殊情况未通过系统下单的应邮件知会商旅客服并抄送部门负责人。<SEP>商旅系统是公司用于事前审批员工出差申请的系统平台。 + chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012049 + + + + 业务佐证材料 + data + 业务佐证材料是指用于证明出差真实性、费用合理性等的材料,包括登机牌、通行记录、支付记录、审批邮件、订单详情等。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012036 + + + + 探亲路费 + concept + 探亲路费是指员工因私回家探亲产生的交通费用,应遵循公司员工探亲管理办法规定,不得以因公差旅方式报销,不享受出差补贴。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012037 + + + + 调动工作 + event + 调动工作是指公司组织安排员工到异地挂职锻炼、培养锻炼、人才帮扶、借调支援等,在途期间按出差规定执行,在异地工作期间按组织人事部确定的标准执行。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012037 + + + + 直辖市 + location + 直辖市是中国行政区划的一种,指由中央政府直接管辖的城市,包括北京、上海、天津、重庆等。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012037 + + + + 特区 + location + 特区在这里指深圳等经济特区,是享有特殊经济政策的国家级区域。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012037 + + + + 西藏 + location + 西藏是中国的一个自治区,在差旅补贴标准中作为特殊地区标注,餐补标准为140元。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012037 + + + + 第十四条 + concept + 第十四条是公司差旅费报销规定中关于业务招待费的条款,定义了业务招待费的范围和报销要求。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 第十五条 + concept + 第十五条是公司差旅费报销规定中关于会议费的条款,规定了公司主办或承办会议的报销标准。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 第十六条 + concept + 第十六条是公司差旅费报销规定中关于广告宣传费的条款,包括广告费和业务宣传费的管理规定。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 公司总裁 + person + 公司总裁是公司最高管理人员,负责批准预算调整申请。<SEP>公司总裁是公司最高管理层人员,对经费预算30000元及以上的内部会议、研讨与集中培训具有审批权限。 + chunk-d26b288ed4001dc5c504dce0eb841362<SEP>chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 商旅客服 + person + 商旅客服是商旅系统的服务人员,负责处理差旅预订异常情况,未通过系统下单的需邮件知会。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012038 + + + + 公司员工探亲管理办法 + content + 《公司员工探亲管理办法》是公司制定的关于员工探亲路费报销的管理规定,探亲路费应严格遵循此办法。 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 增值税发票 + content + 增值税发票是指企业开具的税务发票,用于记录商品或服务的销售和税收信息。文中提到汇总开具的增值税发票应附税控系统明细清单。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 税控系统明细清单 + content + 税控系统明细清单是由税控系统生成的详细清单,汇总开具增值税发票时需要附上该清单。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 财务 + organization + 财务是指公司内部的财务部门,负责审核业务原始凭据、影像扫描、审核与支付等报销相关工作。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 经办人 + person + 经办人是指负责具体业务办理并提交报销申请的人员,需确保原始凭据完整并按时补充。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 业务原始凭据 + content + 业务原始凭据是指证明经济业务发生的原始文件,财务审核时需要检查其完整性。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 报销申请时限 + concept + 报销申请时限是指从业务完成日到附件影像资料挂接系统单据日的期间,公司各类支出报销结算申请时限为三个月。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 预付款项 + concept + 预付款项是指公司预先支付给员工或供应商的款项,原则上应在次月底前完成结算。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012039 + + + + 公司 + organization + 公司是指雇用员工并制定报销政策的企业主体,通过公对私或公对公方式进行支出结算。 + chunk-061324cc36078214691a6fc1cd0aaeea<SEP>chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012040 + + + + 员工 + person + 员工是公司的正式成员,可以申请备用金借款、差旅费报销等支出业务。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012040 + + + + 供应商 + organization + 供应商是公司采购商品或服务的外部合作方,岗位支出业务原则上采用公对公方式与其直接结算。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012040 + + + + 支出报销审批 + event + 支出报销审批是指按照审批权限对报销申请进行审核批准的过程,包括预算内和预算外支出的不同审批流程。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012041 + + + + 预算内支出 + concept + 预算内支出是指在已批准预算范围内的支出,按附表1、附表2执行审批权限。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 正式员工 + person + 正式员工是公司的正式成员,有资格申请备用金借款,非正式员工不得申请。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 分管领导 + person + 分管领导是负责特定业务领域的公司管理人员,需对季度不能及时报账核销的备用金借款进行延期审批。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 一万元 + data + 一万元是员工备用金借款额度的上限。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 出租车 + method + 出租车是市内交通出行方式之一,仅限紧急公务、接送客户、夜间工作至22:00后等特别情形使用。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 部门负责人 + person + 部门负责人负责从严管理出租车市内交通使用,确保符合使用规定。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 出差审批程序 + method + 出差审批程序是出差前必须完成的审批流程,各部门应严格执行以从严控制出差人数和天数。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 交通费 + concept + 交通费是差旅费的组成部分,包括出差发生的各种交通支出。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012042 + + + + 住宿费 + concept + 住宿费是差旅费的组成部分,指出差期间的住宿支出。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012043 + + + + 出差补贴 + concept + 出差补贴是差旅费的组成部分,用于补贴员工出差期间的相关开支。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012043 + + + + 成本中心归属 + concept + 成本中心归属基于责任原则与受益原则确定,特殊情况由业务部门协商确定。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012043 + + + + 责任原则 + concept + 责任原则是确定支出成本中心归属的原则之一。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012043 + + + + 受益原则 + concept + 受益原则是确定支出成本中心归属的原则之一。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012044 + + + + 结算起点 + data + 结算起点是1000元,低于此金额且无法直接结算的小额支出需附付款凭据截图。 + chunk-061324cc36078214691a6fc1cd0aaeea + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012044 + + + + Procurement Management Regulations + chunk-74c01decac4a10cd40a491786743b0ee + Operating departments must strictly follow company bidding, procurement, and material management regulations for procurement of materials and services. + UNKNOWN + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012081 + + + + Tax Authority Recognized Invoice + chunk-74c01decac4a10cd40a491786743b0ee + Expenditures other than employee remuneration, personal service compensation, travel allowance, special subsidy, and current account payment require tax authority-recognized invoices. + UNKNOWN + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012094 + + + + Financial Review + chunk-74c01decac4a10cd40a491786743b0ee + Financial review can reject applications if documents are incomplete, incorrectly filled, or non-compliant. + UNKNOWN + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012085 + + + + 1.0 + 远光软件股份有限公司is the issuer of the expense reimbursement regulations, with Chapter 1 General Provisions establishing the foundational framework. + organizational hierarchy,regulation issuer + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012088 + + + + 1.0 + 远光软件股份有限公司established the Planning and Finance Department to handle financial management and expense-related responsibilities. + financial authority,organizational structure + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012076 + + + + 1.0 + Yuan Guang Software Co., Ltd. has implemented changes in reimbursement standards as part of its internal policy adjustments. + company governance,policy adjustment + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012084 + + + + 1.0 + 远光软件股份有限公司issued the Company Expenditure Management Measures (2024) to regulate company expenditures. + document issuance,policy implementation + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012085 + + + + 1.0 + 远光软件股份有限公司assigned document number远光制度〔2024〕14号to the expenditure management measures. + document numbering,regulatory framework + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012086 + + + + 1.0 + Article 3 Management Principles provides the foundational principles for the entire expense reimbursement system. + chapter article relationship,regulatory foundation + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012076 + + + + 1.0 + The designated management department determines expenditure scope, standards, methods, and management processes in accordance with company financial, procurement, and human resources policies. + department responsibility,regulatory compliance + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012079 + + + + 1.0 + 控股子公司must report expenditure management measures to计划财务部for filing after approval. + compliance,reporting relationship + chunk-dd87aa5bc62cc9587ecb4c26d35a5263 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012099 + + + + 1.0 + Article 11 Petty Cash Loans is one of the key expense management regulations under Chapter + chapter article relationship,specific regulation + chunk-aa5435156b829944c173fa1d2d7a93d4 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012088 + + + + 1.0 + 办公室(党委办公室)manages party building and official vehicle expenses according to the company expense management policy. + expense management,policy execution + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012088 + + + + 1.0 + 工会委员会manages trade union expenses according to the company expense management policy. + expense management,policy execution + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012079 + + + + 1.0 + 营销中心manages bidding + expense management,policy execution + chunk-afc57a0e9548d1f484da6df6c182676b + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012079 + + + + 1.0 + 组织人事部负责确定调动工作人员的报销标准,对异地挂职锻炼等人员的费用报销进行规范管理。 + 标准制定、人事管理 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012090 + + + + 1.0 + 商旅系统是公司差旅费管理的核心平台,用于统一预定和审批差旅相关费用。 + 系统支持、费用管控 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012086 + + + + 1.0 + 业务招待费是公司费用管理体系的重要组成部分,与差旅费并列作为公司主要费用报销项目。 + 费用类别、公司运营 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012089 + + + + 1.0 + 会议费是公司费用管理体系的重要组成部分,与差旅费并列作为公司主要费用报销项目,需要事前审批并附业务佐证材料。 + 费用类别、公司运营 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012092 + + + + 1.0 + The reimbursement standard changes include the cancellation of official vehicle subsidy reimbursement since it has been incorporated into wages. + expense reduction,policy modification + chunk-18d968b78afe916b419c1b5973421ebe + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012085 + + + + 1.0 + Management personnel at all levels exercise approval authority within the company's expenditure authorization approval scope. + hierarchical authority,organizational structure + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012076 + + + + 1.0 + The centralized management department implements expenditure centralized management on behalf of the company. + centralized management,departmental responsibility + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012077 + + + + 1.0 + The Planning and Finance Department assists the centralized management department and is responsible for financial reimbursement auditing and payment settlement. + departmental responsibility,financial oversight + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012077 + + + + 1.0 + The operating department (individual) conducts expenditure business activities within the company's authorization. + business execution,departmental responsibility + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012078 + + + + 1.0 + Operating departments must strictly follow company bidding, procurement, and material management regulations for procurement of materials and services. + business execution,procurement compliance + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012083 + + + + 1.0 + The operator submits the expenditure reimbursement application through the financial information system and provides business original documents. + documentation responsibility,workflow initiation + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012079 + + + + 1.0 + The operator is responsible for obtaining real + authenticity responsibility,document preparation + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012094 + + + + 1.0 + The operator has three working days to supplement incomplete business original documents after financial review. + documentation compliance,workflow timeline + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012083 + + + + 1.0 + Financial review can reject applications if documents are incomplete, incorrectly filled, or non-compliant. + document verification,rejection handling + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012088 + + + + 1.0 + Aggregated VAT invoices must be accompanied by tax control system detail lists. + documentation requirement,tax compliance + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012079 + + + + 1.0 + Expenditures other than employee remuneration, personal service compensation, travel allowance, special subsidy, and current account payment require tax authority-recognized invoices. + compliance requirement,documentation standard + chunk-74c01decac4a10cd40a491786743b0ee + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012082 + + + + 1.0 + 公司通过第十七条对培训费进行规范管理。 + 合规管理,规章制定 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012084 + + + + 1.0 + 公司通过第十八条对通信费进行规范管理。 + 合规管理,规章制定 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012084 + + + + 1.0 + 公司通过第十九条对邮递费进行规范管理。 + 合规管理,规章制定 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012084 + + + + 1.0 + 公司通过第二十条对薪酬福利支出进行规范管理。 + 薪酬管理,规章制定 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012086 + + + + 1.0 + 公司通过第二十一条对对外捐赠支出进行规范管理。 + 捐赠管理,规章制定 + chunk-e9438f69c9e221d9f0f00a05ad84eac6 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012089 + + + + 1.0 + Departments and units must implement the night high-speed rail provision allowing employees to choose sleeper seats when high-speed train travel exceeds 6 hours at night. + policy implementation,travel standards + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012084 + + + + 1.0 + Departments and units are responsible for strictly managing taxi usage, limiting reimbursements to emergency situations, client接送, late night work, and other special circumstances. + cost control,policy enforcement + chunk-613d6dfd4c5e9c807229a3147f96b584 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012099 + + + + 1.0 + 第十四条专门定义了业务招待费的概念和报销要求,明确了接待客户和相关单位的餐饮等合理支出范围。 + 条款规定、费用管理 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012092 + + + + 1.0 + 第十五条专门规定了会议费的报销范围和报销要求,包括公司主办或承办会议以及参加外部会议的报销标准。 + 条款规定、费用管理 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012092 + + + + 1.0 + 公司总裁对经费预算30000元及以上的内部会议具有审批权限,体现了公司对大额会议支出的管控。 + 审批权限、预算管理 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012093 + + + + 1.0 + 第十六条专门规定了广告宣传费的定义和管理要求,包括广告费和业务宣传费两类。 + 条款规定、费用管理 + chunk-d26b288ed4001dc5c504dce0eb841362 + /app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf + 1779012092 + + + + diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_doc_status.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_doc_status.json index 7a678de..7a16619 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_doc_status.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_doc_status.json @@ -24,5 +24,28 @@ "processing_start_time": 1779011842, "processing_end_time": 1779012093 } + }, + "a8f8465df08e455ebe133351721d49f8": { + "status": "failed", + "error_msg": "Embedding func: Worker execution timeout after 60s", + "chunks_count": 6, + "chunks_list": [ + "chunk-07de6ea74f60535b689f977295770273", + "chunk-99c6f377dff2b9a37a7214b7b05ea9a8", + "chunk-1746bd83138e85e66a78e0cb9ad79272", + "chunk-ce44e4483e4119265b43eacb72e0326a", + "chunk-2187fa0609874bdda339c9850da45a26", + "chunk-2224d777c0b72d0b2dab622c79096c2c" + ], + "content_summary": "# 产品需求文档\n## 文档信息\n| 项目 | 内容 |\n|------|------|\n| 项目名称 |\n无单报销\n|\n| 版本 | V1.0 |\n| 日期 | 2026-05-06 |\n| 状态 | 正式版 |\n---\n## 1. 项目概述\n### 1.1 项目背景\n面向\n大型企业,\n从业务人员视角出发,解决现有ERP使用体验不佳的问题。\n在ERP的发展历程中,“单据化”曾是财务合规的一大进步,它确保了每笔支出都有据可查。但不可否认,传统的人工填单确实\n也制造了很多\n“枷锁”。在AI时代,解...", + "content_length": 9088, + "created_at": "2026-05-19T15:59:57.283110+00:00", + "updated_at": "2026-05-19T16:00:57.323299+00:00", + "file_path": "/app/server/storage/knowledge/报销制度/a8f8465df08e455ebe133351721d49f8__无单需求文档0506.docx", + "track_id": "insert_20260519_155957_88c49850", + "metadata": { + "processing_start_time": 1779206397, + "processing_end_time": 1779206457 + } } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_entity_chunks.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_entity_chunks.json index 3dbf2a6..d6b1b69 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_entity_chunks.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_entity_chunks.json @@ -1,2358 +1,2358 @@ -{ - "远光软件股份有限公司": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4", - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 3, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "远光软件股份有限公司" - }, - "第一章总则": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第一章总则" - }, - "第二条目的": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第二条目的" - }, - "第二条范围": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第二条范围" - }, - "第三条管理原则": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第三条管理原则" - }, - "第二章职责分工": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第二章职责分工" - }, - "第四条归口管理部门主要职责": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "第四条归口管理部门主要职责" - }, - "计划财务部": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4", - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 5, - "create_time": 1779011991, - "update_time": 1779011991, - "_id": "计划财务部" - }, - "第五条计划财务部主要职责": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011992, - "update_time": 1779011992, - "_id": "第五条计划财务部主要职责" - }, - "第六条经办部门(个人)主要职责": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011992, - "update_time": 1779011992, - "_id": "第六条经办部门(个人)主要职责" - }, - "第七条各级管理人员主要职责": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011992, - "update_time": 1779011992, - "_id": "第七条各级管理人员主要职责" - }, - "第三章支出报销申请与审批": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011992, - "update_time": 1779011992, - "_id": "第三章支出报销申请与审批" - }, - "第八条支出报销申请": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011992, - "update_time": 1779011992, - "_id": "第八条支出报销申请" - }, - "第九条支出报销审批": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011993, - "update_time": 1779011993, - "_id": "第九条支出报销审批" - }, - "第十条支出成本中心归属": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011993, - "update_time": 1779011993, - "_id": "第十条支出成本中心归属" - }, - "第四章重点支出管理规定": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第四章重点支出管理规定" - }, - "第十一条备用金借款": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第十一条备用金借款" - }, - "第十二条市内交通费": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第十二条市内交通费" - }, - "第十三条差旅费": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第十三条差旅费" - }, - "第十四条业务招待费": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第十四条业务招待费" - }, - "第五章附则": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第五章附则" - }, - "第二十三条本办法的归口与实施": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011994, - "update_time": 1779011994, - "_id": "第二十三条本办法的归口与实施" - }, - "第二十四条附件": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011995, - "update_time": 1779011995, - "_id": "第二十四条附件" - }, - "交通工具等级标准": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011995, - "update_time": 1779011995, - "_id": "交通工具等级标准" - }, - "出差补贴标准": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011996, - "update_time": 1779011996, - "_id": "出差补贴标准" - }, - "经济舱": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011996, - "update_time": 1779011996, - "_id": "经济舱" - }, - "火车硬席": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011996, - "update_time": 1779011996, - "_id": "火车硬席" - }, - "三等舱": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011996, - "update_time": 1779011996, - "_id": "三等舱" - }, - "凭据报销": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "凭据报销" - }, - "餐补": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "餐补" - }, - "基本补助": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "基本补助" - }, - "公司支出管理办法": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "公司支出管理办法" - }, - "办公室(党委办公室)": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "办公室(党委办公室)" - }, - "工会委员会": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "工会委员会" - }, - "营销中心": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "营销中心" - }, - "品牌及市场运营中心": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 2, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "品牌及市场运营中心" - }, - "组织人事部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 4, - "create_time": 1779011999, - "update_time": 1779011999, - "_id": "组织人事部" - }, - "人力资源服务部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "人力资源服务部" - }, - "产业投资部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "产业投资部" - }, - "证券与法律事务部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "证券与法律事务部" - }, - "产品规划设计部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "产品规划设计部" - }, - "DAP研发中心": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "DAP研发中心" - }, - "信息管理部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "信息管理部" - }, - "后勤服务部": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "后勤服务部" - }, - "审批权限": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "审批权限" - }, - "报销标准": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012002, - "update_time": 1779012002, - "_id": "报销标准" - }, - "差旅费": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-d26b288ed4001dc5c504dce0eb841362", - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 5, - "create_time": 1779012002, - "update_time": 1779012002, - "_id": "差旅费" - }, - "全资子公司": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 2, - "create_time": 1779012003, - "update_time": 1779012003, - "_id": "全资子公司" - }, - "人事归口管理部门": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012003, - "update_time": 1779012003, - "_id": "人事归口管理部门" - }, - "母公司": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012003, - "update_time": 1779012003, - "_id": "母公司" - }, - "逐级审批规则": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "逐级审批规则" - }, - "终审岗": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "终审岗" - }, - "财务入账条件": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "财务入账条件" - }, - "薪酬福利支出分配计划": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "薪酬福利支出分配计划" - }, - "公司1号文": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "公司1号文" - }, - "总监": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "总监" - }, - "一级部门总经理": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "一级部门总经理" - }, - "P8": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "P8" - }, - "报销标准变化情况": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 2, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "报销标准变化情况" - }, - "取消报销规定": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "取消报销规定" - }, - "新增报销规定": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "新增报销规定" - }, - "因公用车补贴": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012006, - "update_time": 1779012006, - "_id": "因公用车补贴" - }, - "异地挂职锻炼补贴标准": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b", - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 2, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "异地挂职锻炼补贴标准" - }, - "组织安排": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "组织安排" - }, - "Company": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "Company" - }, - "Management Personnel At All Levels": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "Management Personnel At All Levels" - }, - "Centralized Management department": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "Centralized Management department" - }, - "Planning and Finance Department": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "Planning and Finance Department" - }, - "Operating Department Individual": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012008, - "update_time": 1779012008, - "_id": "Operating Department Individual" - }, - "Operator": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "Operator" - }, - "First Approver": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "First Approver" - }, - "Subsequent Approver": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "Subsequent Approver" - }, - "Financial Information System": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "Financial Information System" - }, - "Business Original Documents": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "Business Original Documents" - }, - "VAT Special Invoice": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "VAT Special Invoice" - }, - "Expenditure Reimbursement Application": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "Expenditure Reimbursement Application" - }, - "Expenditure Authorization Approval Scope": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011997, - "update_time": 1779011997, - "_id": "Expenditure Authorization Approval Scope" - }, - "Separation of Approval and Processing Principle": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "Separation of Approval and Processing Principle" - }, - "Three Flows Consistency Principle": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "Three Flows Consistency Principle" - }, - "Employee Remuneration": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011998, - "update_time": 1779011998, - "_id": "Employee Remuneration" - }, - "Personal Service Compensation": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011999, - "update_time": 1779011999, - "_id": "Personal Service Compensation" - }, - "Travel Allowance": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011999, - "update_time": 1779011999, - "_id": "Travel Allowance" - }, - "Special Subsidy": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011999, - "update_time": 1779011999, - "_id": "Special Subsidy" - }, - "Current Account Payment": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779011999, - "update_time": 1779011999, - "_id": "Current Account Payment" - }, - "Trade Union Fund": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Trade Union Fund" - }, - "Employee Welfare": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Employee Welfare" - }, - "Staff Activities": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Staff Activities" - }, - "Business Entertainment": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Business Entertainment" - }, - "Transportation Tickets": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Transportation Tickets" - }, - "Government Fees": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Government Fees" - }, - "Tax Control System Details": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Tax Control System Details" - }, - "Three Working Days Deadline": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "Three Working Days Deadline" - }, - "广告费": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "create_time": 1779012000, - "update_time": 1779012000, - "_id": "广告费" - }, - "业务宣传费": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "业务宣传费" - }, - "培训费": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 2, - "create_time": 1779012001, - "update_time": 1779012001, - "_id": "培训费" - }, - "通信费": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 2, - "create_time": 1779012002, - "update_time": 1779012002, - "_id": "通信费" - }, - "邮递费": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012004, - "update_time": 1779012004, - "_id": "邮递费" - }, - "薪酬福利支出": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "薪酬福利支出" - }, - "对外捐赠支出": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "对外捐赠支出" - }, - "涉外业务汇率标准": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012005, - "update_time": 1779012005, - "_id": "涉外业务汇率标准" - }, - "公司员工教育培训管理办法": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012006, - "update_time": 1779012006, - "_id": "公司员工教育培训管理办法" - }, - "公司员工因公通讯费用实施细则": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012006, - "update_time": 1779012006, - "_id": "公司员工因公通讯费用实施细则" - }, - "公司团建管理办法": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012006, - "update_time": 1779012006, - "_id": "公司团建管理办法" - }, - "工会经费管理办法": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012006, - "update_time": 1779012006, - "_id": "工会经费管理办法" - }, - "中国银行外汇折算价": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012019, - "update_time": 1779012019, - "_id": "中国银行外汇折算价" - }, - "中国外汇交易中心参考汇率": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012007, - "update_time": 1779012007, - "_id": "中国外汇交易中心参考汇率" - }, - "第十七条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012008, - "update_time": 1779012008, - "_id": "第十七条" - }, - "第十八条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012008, - "update_time": 1779012008, - "_id": "第十八条" - }, - "第十九条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012008, - "update_time": 1779012008, - "_id": "第十九条" - }, - "第二十条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012008, - "update_time": 1779012008, - "_id": "第二十条" - }, - "第二十一条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "第二十一条" - }, - "第二十二条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "第二十二条" - }, - "第二十三条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "第二十三条" - }, - "第二十四条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "第二十四条" - }, - "附表1:员工支出报销审批权限表": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012009, - "update_time": 1779012009, - "_id": "附表1:员工支出报销审批权限表" - }, - "附表2:岗位支出报销审批权限表": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012011, - "update_time": 1779012011, - "_id": "附表2:岗位支出报销审批权限表" - }, - "附表3:支出归口管理部门与归口业务范围": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012011, - "update_time": 1779012011, - "_id": "附表3:支出归口管理部门与归口业务范围" - }, - "业务招待": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6", - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 2, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "业务招待" - }, - "报销资格": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "报销资格" - }, - "捐赠申请": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "捐赠申请" - }, - "预算调整决策程序": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "预算调整决策程序" - }, - "Departments And Units": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "Departments And Units" - }, - "Company Business Travel System": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "Company Business Travel System" - }, - "Transportation Level Standards": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012012, - "update_time": 1779012012, - "_id": "Transportation Level Standards" - }, - "Hotel Accommodation Standards": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012013, - "update_time": 1779012013, - "_id": "Hotel Accommodation Standards" - }, - "Travel Allowance Standards": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012013, - "update_time": 1779012013, - "_id": "Travel Allowance Standards" - }, - "Company Property Rental Management": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012013, - "update_time": 1779012013, - "_id": "Company Property Rental Management" - }, - "Business Trip Approval": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012013, - "update_time": 1779012013, - "_id": "Business Trip Approval" - }, - "Company Leadership": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012013, - "update_time": 1779012013, - "_id": "Company Leadership" - }, - "Senior Managers": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012014, - "update_time": 1779012014, - "_id": "Senior Managers" - }, - "Middle Managers": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012014, - "update_time": 1779012014, - "_id": "Middle Managers" - }, - "Basic Level Managers": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012015, - "update_time": 1779012015, - "_id": "Basic Level Managers" - }, - "Other Employees": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012015, - "update_time": 1779012015, - "_id": "Other Employees" - }, - "Remote Work Housing": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012015, - "update_time": 1779012015, - "_id": "Remote Work Housing" - }, - "Commercial Insurance": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012015, - "update_time": 1779012015, - "_id": "Commercial Insurance" - }, - "Transportation Cost Reimbursement": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012016, - "update_time": 1779012016, - "_id": "Transportation Cost Reimbursement" - }, - "Accommodation Cost Reimbursement": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012016, - "update_time": 1779012016, - "_id": "Accommodation Cost Reimbursement" - }, - "Hong Kong, Macau, And Taiwan Region": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012016, - "update_time": 1779012016, - "_id": "Hong Kong, Macau, And Taiwan Region" - }, - "Directly-Controlled Municipalities And Special Administrative Regions": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012016, - "update_time": 1779012016, - "_id": "Directly-Controlled Municipalities And Special Administrative Regions" - }, - "Provincial Capitals": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012016, - "update_time": 1779012016, - "_id": "Provincial Capitals" - }, - "Other Areas": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012017, - "update_time": 1779012017, - "_id": "Other Areas" - }, - "Night High-Speed Rail Provision": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012017, - "update_time": 1779012017, - "_id": "Night High-Speed Rail Provision" - }, - "Taxi Usage Regulations": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012017, - "update_time": 1779012017, - "_id": "Taxi Usage Regulations" - }, - "Self-Driving Travel Provisions": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012017, - "update_time": 1779012017, - "_id": "Self-Driving Travel Provisions" - }, - "Remote Work Housing Rental Expenses": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012018, - "update_time": 1779012018, - "_id": "Remote Work Housing Rental Expenses" - }, - "External Conference Accommodation": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012018, - "update_time": 1779012018, - "_id": "External Conference Accommodation" - }, - "取消报销规定内容": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012018, - "update_time": 1779012018, - "_id": "取消报销规定内容" - }, - "新增规定内容": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012018, - "update_time": 1779012018, - "_id": "新增规定内容" - }, - "商旅订票规范": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012018, - "update_time": 1779012018, - "_id": "商旅订票规范" - }, - "审批权限变化情况": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012019, - "update_time": 1779012019, - "_id": "审批权限变化情况" - }, - "投标保证金": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012019, - "update_time": 1779012019, - "_id": "投标保证金" - }, - "审批流转程序": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012019, - "update_time": 1779012019, - "_id": "审批流转程序" - }, - "出差规定": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012019, - "update_time": 1779012019, - "_id": "出差规定" - }, - "财务信息化系统": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012020, - "update_time": 1779012020, - "_id": "财务信息化系统" - }, - "支出报销申请与审批": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 2, - "create_time": 1779012020, - "update_time": 1779012020, - "_id": "支出报销申请与审批" - }, - "重点支出管理规定": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012020, - "update_time": 1779012020, - "_id": "重点支出管理规定" - }, - "备用金借款": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 3, - "create_time": 1779012020, - "update_time": 1779012020, - "_id": "备用金借款" - }, - "市内交通费": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe", - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 3, - "create_time": 1779012022, - "update_time": 1779012022, - "_id": "市内交通费" - }, - "审批权限表": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012022, - "update_time": 1779012022, - "_id": "审批权限表" - }, - "1 Yuan Per Person Per Kilometer Reimbursement": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "1 Yuan Per Person Per Kilometer Reimbursement" - }, - "Official Vehicle Subsidy": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Official Vehicle Subsidy" - }, - "Department Manager": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Department Manager" - }, - "Director": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Director" - }, - "First-Level Department General Manager": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "First-Level Department General Manager" - }, - "Institution General Manager": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Institution General Manager" - }, - "Business Division General Manager": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Business Division General Manager" - }, - "Vice President": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Vice President" - }, - "Chief Engineer": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012023, - "update_time": 1779012023, - "_id": "Chief Engineer" - }, - "Senior Vice President": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012024, - "update_time": 1779012024, - "_id": "Senior Vice President" - }, - "President": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012024, - "update_time": 1779012024, - "_id": "President" - }, - "Committee Chairpersons": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012024, - "update_time": 1779012024, - "_id": "Committee Chairpersons" - }, - "Bid Security Deposit Approval Limits Table": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012024, - "update_time": 1779012024, - "_id": "Bid Security Deposit Approval Limits Table" - }, - "50000 Yuan Approval Limit": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012024, - "update_time": 1779012024, - "_id": "50000 Yuan Approval Limit" - }, - "100000 Yuan Approval Limit": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012025, - "update_time": 1779012025, - "_id": "100000 Yuan Approval Limit" - }, - "200000 Yuan Approval Limit": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012025, - "update_time": 1779012025, - "_id": "200000 Yuan Approval Limit" - }, - "5000000 Yuan Approval Limit": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012025, - "update_time": 1779012025, - "_id": "5000000 Yuan Approval Limit" - }, - "Commercial Travel System": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012026, - "update_time": 1779012026, - "_id": "Commercial Travel System" - }, - "Multi-Level Approval Rule": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012026, - "update_time": 1779012026, - "_id": "Multi-Level Approval Rule" - }, - "Final Approval Position": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012026, - "update_time": 1779012026, - "_id": "Final Approval Position" - }, - "Company Hotel Accommodation Limit Standards": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012026, - "update_time": 1779012026, - "_id": "Company Hotel Accommodation Limit Standards" - }, - "公司支出管理办法(2024)": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012026, - "update_time": 1779012026, - "_id": "公司支出管理办法(2024)" - }, - "国家电网公司": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "国家电网公司" - }, - "国网数科公司": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "国网数科公司" - }, - "办法": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "办法" - }, - "2024年4月17日": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012027, - "update_time": 1779012027, - "_id": "2024年4月17日" - }, - "预算先行": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012028, - "update_time": 1779012028, - "_id": "预算先行" - }, - "厉行节约": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012028, - "update_time": 1779012028, - "_id": "厉行节约" - }, - "分级授权": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012028, - "update_time": 1779012028, - "_id": "分级授权" - }, - "分类控制": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012028, - "update_time": 1779012028, - "_id": "分类控制" - }, - "批办分离": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "批办分离" - }, - "业务招待费": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "业务招待费" - }, - "会议费": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "会议费" - }, - "广告宣传费": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 2, - "update_time": 1779012089, - "_id": "广告宣传费" - }, - "控股子公司": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "控股子公司" - }, - "分支机构": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "分支机构" - }, - "经办部门": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012029, - "update_time": 1779012029, - "_id": "经办部门" - }, - "各级管理人员": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012030, - "update_time": 1779012030, - "_id": "各级管理人员" - }, - "归口管理部门": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012044, - "update_time": 1779012044, - "_id": "归口管理部门" - }, - "远光制度〔2024〕14号": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012044, - "update_time": 1779012044, - "_id": "远光制度〔2024〕14号" - }, - "修订说明": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012032, - "update_time": 1779012032, - "_id": "修订说明" - }, - "员工支出报销审批权限表": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 2, - "create_time": 1779012032, - "update_time": 1779012032, - "_id": "员工支出报销审批权限表" - }, - "岗位支出报销审批权限表": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 2, - "create_time": 1779012032, - "update_time": 1779012032, - "_id": "岗位支出报销审批权限表" - }, - "支出归口管理部门与归口业务范围": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012033, - "update_time": 1779012033, - "_id": "支出归口管理部门与归口业务范围" - }, - "效益优先": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012033, - "update_time": 1779012033, - "_id": "效益优先" - }, - "公司各部门": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012033, - "update_time": 1779012033, - "_id": "公司各部门" - }, - "因公借款": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012033, - "update_time": 1779012033, - "_id": "因公借款" - }, - "其他支出(员工)": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012034, - "update_time": 1779012034, - "_id": "其他支出(员工)" - }, - "资产采购": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012034, - "update_time": 1779012034, - "_id": "资产采购" - }, - "基建工程": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012034, - "update_time": 1779012034, - "_id": "基建工程" - }, - "股权投资、兼并收购": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "股权投资、兼并收购" - }, - "材料采购": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "材料采购" - }, - "分包外包(内部单位)": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "分包外包(内部单位)" - }, - "分包外包(外部单位)": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "分包外包(外部单位)" - }, - "保证金": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "保证金" - }, - "销售退款": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "销售退款" - }, - "房屋租金": { - "chunk_ids": [ - "chunk-9841d66d8fb8548aab40220663a51693" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "房屋租金" - }, - "基本出差补贴": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012048, - "update_time": 1779012048, - "_id": "基本出差补贴" - }, - "商旅系统": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362", - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 2, - "create_time": 1779012049, - "update_time": 1779012049, - "_id": "商旅系统" - }, - "业务佐证材料": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012036, - "update_time": 1779012036, - "_id": "业务佐证材料" - }, - "探亲路费": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012037, - "update_time": 1779012037, - "_id": "探亲路费" - }, - "调动工作": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012037, - "update_time": 1779012037, - "_id": "调动工作" - }, - "直辖市": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012037, - "update_time": 1779012037, - "_id": "直辖市" - }, - "特区": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012037, - "update_time": 1779012037, - "_id": "特区" - }, - "西藏": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012037, - "update_time": 1779012037, - "_id": "西藏" - }, - "第十四条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "第十四条" - }, - "第十五条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "第十五条" - }, - "第十六条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "第十六条" - }, - "公司总裁": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362", - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 2, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "公司总裁" - }, - "商旅客服": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012038, - "update_time": 1779012038, - "_id": "商旅客服" - }, - "公司员工探亲管理办法": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "公司员工探亲管理办法" - }, - "增值税发票": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "增值税发票" - }, - "税控系统明细清单": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "税控系统明细清单" - }, - "财务": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "财务" - }, - "经办人": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "经办人" - }, - "业务原始凭据": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "业务原始凭据" - }, - "报销申请时限": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "报销申请时限" - }, - "预付款项": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012039, - "update_time": 1779012039, - "_id": "预付款项" - }, - "公司": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea", - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 2, - "update_time": 1779012094, - "_id": "公司" - }, - "员工": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012040, - "update_time": 1779012040, - "_id": "员工" - }, - "供应商": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012040, - "update_time": 1779012040, - "_id": "供应商" - }, - "支出报销审批": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012041, - "update_time": 1779012041, - "_id": "支出报销审批" - }, - "预算内支出": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "预算内支出" - }, - "正式员工": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "正式员工" - }, - "分管领导": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "分管领导" - }, - "一万元": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "一万元" - }, - "出租车": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "出租车" - }, - "部门负责人": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "部门负责人" - }, - "出差审批程序": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "出差审批程序" - }, - "交通费": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012042, - "update_time": 1779012042, - "_id": "交通费" - }, - "住宿费": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012043, - "update_time": 1779012043, - "_id": "住宿费" - }, - "出差补贴": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012043, - "update_time": 1779012043, - "_id": "出差补贴" - }, - "成本中心归属": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012043, - "update_time": 1779012043, - "_id": "成本中心归属" - }, - "责任原则": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012043, - "update_time": 1779012043, - "_id": "责任原则" - }, - "受益原则": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012044, - "update_time": 1779012044, - "_id": "受益原则" - }, - "结算起点": { - "chunk_ids": [ - "chunk-061324cc36078214691a6fc1cd0aaeea" - ], - "count": 1, - "create_time": 1779012044, - "update_time": 1779012044, - "_id": "结算起点" - }, - "Procurement Management Regulations": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012081, - "update_time": 1779012081, - "_id": "Procurement Management Regulations" - }, - "Tax Authority Recognized Invoice": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012094, - "update_time": 1779012094, - "_id": "Tax Authority Recognized Invoice" - }, - "Financial Review": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012085, - "update_time": 1779012085, - "_id": "Financial Review" - } +{ + "远光软件股份有限公司": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4", + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 3, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "远光软件股份有限公司" + }, + "第一章总则": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第一章总则" + }, + "第二条目的": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第二条目的" + }, + "第二条范围": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第二条范围" + }, + "第三条管理原则": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第三条管理原则" + }, + "第二章职责分工": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第二章职责分工" + }, + "第四条归口管理部门主要职责": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "第四条归口管理部门主要职责" + }, + "计划财务部": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4", + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 5, + "create_time": 1779011991, + "update_time": 1779011991, + "_id": "计划财务部" + }, + "第五条计划财务部主要职责": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011992, + "update_time": 1779011992, + "_id": "第五条计划财务部主要职责" + }, + "第六条经办部门(个人)主要职责": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011992, + "update_time": 1779011992, + "_id": "第六条经办部门(个人)主要职责" + }, + "第七条各级管理人员主要职责": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011992, + "update_time": 1779011992, + "_id": "第七条各级管理人员主要职责" + }, + "第三章支出报销申请与审批": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011992, + "update_time": 1779011992, + "_id": "第三章支出报销申请与审批" + }, + "第八条支出报销申请": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011992, + "update_time": 1779011992, + "_id": "第八条支出报销申请" + }, + "第九条支出报销审批": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011993, + "update_time": 1779011993, + "_id": "第九条支出报销审批" + }, + "第十条支出成本中心归属": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011993, + "update_time": 1779011993, + "_id": "第十条支出成本中心归属" + }, + "第四章重点支出管理规定": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第四章重点支出管理规定" + }, + "第十一条备用金借款": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第十一条备用金借款" + }, + "第十二条市内交通费": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第十二条市内交通费" + }, + "第十三条差旅费": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第十三条差旅费" + }, + "第十四条业务招待费": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第十四条业务招待费" + }, + "第五章附则": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第五章附则" + }, + "第二十三条本办法的归口与实施": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011994, + "update_time": 1779011994, + "_id": "第二十三条本办法的归口与实施" + }, + "第二十四条附件": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011995, + "update_time": 1779011995, + "_id": "第二十四条附件" + }, + "交通工具等级标准": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011995, + "update_time": 1779011995, + "_id": "交通工具等级标准" + }, + "出差补贴标准": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011996, + "update_time": 1779011996, + "_id": "出差补贴标准" + }, + "经济舱": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011996, + "update_time": 1779011996, + "_id": "经济舱" + }, + "火车硬席": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011996, + "update_time": 1779011996, + "_id": "火车硬席" + }, + "三等舱": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011996, + "update_time": 1779011996, + "_id": "三等舱" + }, + "凭据报销": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "凭据报销" + }, + "餐补": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "餐补" + }, + "基本补助": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "基本补助" + }, + "公司支出管理办法": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "公司支出管理办法" + }, + "办公室(党委办公室)": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "办公室(党委办公室)" + }, + "工会委员会": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "工会委员会" + }, + "营销中心": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "营销中心" + }, + "品牌及市场运营中心": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 2, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "品牌及市场运营中心" + }, + "组织人事部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 4, + "create_time": 1779011999, + "update_time": 1779011999, + "_id": "组织人事部" + }, + "人力资源服务部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "人力资源服务部" + }, + "产业投资部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "产业投资部" + }, + "证券与法律事务部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "证券与法律事务部" + }, + "产品规划设计部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "产品规划设计部" + }, + "DAP研发中心": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "DAP研发中心" + }, + "信息管理部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "信息管理部" + }, + "后勤服务部": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "后勤服务部" + }, + "审批权限": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "审批权限" + }, + "报销标准": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012002, + "update_time": 1779012002, + "_id": "报销标准" + }, + "差旅费": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-d26b288ed4001dc5c504dce0eb841362", + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 5, + "create_time": 1779012002, + "update_time": 1779012002, + "_id": "差旅费" + }, + "全资子公司": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 2, + "create_time": 1779012003, + "update_time": 1779012003, + "_id": "全资子公司" + }, + "人事归口管理部门": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012003, + "update_time": 1779012003, + "_id": "人事归口管理部门" + }, + "母公司": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012003, + "update_time": 1779012003, + "_id": "母公司" + }, + "逐级审批规则": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "逐级审批规则" + }, + "终审岗": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "终审岗" + }, + "财务入账条件": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "财务入账条件" + }, + "薪酬福利支出分配计划": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "薪酬福利支出分配计划" + }, + "公司1号文": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "公司1号文" + }, + "总监": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "总监" + }, + "一级部门总经理": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "一级部门总经理" + }, + "P8": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "P8" + }, + "报销标准变化情况": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 2, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "报销标准变化情况" + }, + "取消报销规定": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "取消报销规定" + }, + "新增报销规定": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "新增报销规定" + }, + "因公用车补贴": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012006, + "update_time": 1779012006, + "_id": "因公用车补贴" + }, + "异地挂职锻炼补贴标准": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b", + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 2, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "异地挂职锻炼补贴标准" + }, + "组织安排": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "组织安排" + }, + "Company": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "Company" + }, + "Management Personnel At All Levels": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "Management Personnel At All Levels" + }, + "Centralized Management department": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "Centralized Management department" + }, + "Planning and Finance Department": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "Planning and Finance Department" + }, + "Operating Department Individual": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012008, + "update_time": 1779012008, + "_id": "Operating Department Individual" + }, + "Operator": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "Operator" + }, + "First Approver": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "First Approver" + }, + "Subsequent Approver": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "Subsequent Approver" + }, + "Financial Information System": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "Financial Information System" + }, + "Business Original Documents": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "Business Original Documents" + }, + "VAT Special Invoice": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "VAT Special Invoice" + }, + "Expenditure Reimbursement Application": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "Expenditure Reimbursement Application" + }, + "Expenditure Authorization Approval Scope": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011997, + "update_time": 1779011997, + "_id": "Expenditure Authorization Approval Scope" + }, + "Separation of Approval and Processing Principle": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "Separation of Approval and Processing Principle" + }, + "Three Flows Consistency Principle": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "Three Flows Consistency Principle" + }, + "Employee Remuneration": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011998, + "update_time": 1779011998, + "_id": "Employee Remuneration" + }, + "Personal Service Compensation": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011999, + "update_time": 1779011999, + "_id": "Personal Service Compensation" + }, + "Travel Allowance": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011999, + "update_time": 1779011999, + "_id": "Travel Allowance" + }, + "Special Subsidy": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011999, + "update_time": 1779011999, + "_id": "Special Subsidy" + }, + "Current Account Payment": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779011999, + "update_time": 1779011999, + "_id": "Current Account Payment" + }, + "Trade Union Fund": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Trade Union Fund" + }, + "Employee Welfare": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Employee Welfare" + }, + "Staff Activities": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Staff Activities" + }, + "Business Entertainment": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Business Entertainment" + }, + "Transportation Tickets": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Transportation Tickets" + }, + "Government Fees": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Government Fees" + }, + "Tax Control System Details": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Tax Control System Details" + }, + "Three Working Days Deadline": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "Three Working Days Deadline" + }, + "广告费": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "create_time": 1779012000, + "update_time": 1779012000, + "_id": "广告费" + }, + "业务宣传费": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "业务宣传费" + }, + "培训费": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 2, + "create_time": 1779012001, + "update_time": 1779012001, + "_id": "培训费" + }, + "通信费": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 2, + "create_time": 1779012002, + "update_time": 1779012002, + "_id": "通信费" + }, + "邮递费": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012004, + "update_time": 1779012004, + "_id": "邮递费" + }, + "薪酬福利支出": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "薪酬福利支出" + }, + "对外捐赠支出": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "对外捐赠支出" + }, + "涉外业务汇率标准": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012005, + "update_time": 1779012005, + "_id": "涉外业务汇率标准" + }, + "公司员工教育培训管理办法": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012006, + "update_time": 1779012006, + "_id": "公司员工教育培训管理办法" + }, + "公司员工因公通讯费用实施细则": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012006, + "update_time": 1779012006, + "_id": "公司员工因公通讯费用实施细则" + }, + "公司团建管理办法": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012006, + "update_time": 1779012006, + "_id": "公司团建管理办法" + }, + "工会经费管理办法": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012006, + "update_time": 1779012006, + "_id": "工会经费管理办法" + }, + "中国银行外汇折算价": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012019, + "update_time": 1779012019, + "_id": "中国银行外汇折算价" + }, + "中国外汇交易中心参考汇率": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012007, + "update_time": 1779012007, + "_id": "中国外汇交易中心参考汇率" + }, + "第十七条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012008, + "update_time": 1779012008, + "_id": "第十七条" + }, + "第十八条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012008, + "update_time": 1779012008, + "_id": "第十八条" + }, + "第十九条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012008, + "update_time": 1779012008, + "_id": "第十九条" + }, + "第二十条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012008, + "update_time": 1779012008, + "_id": "第二十条" + }, + "第二十一条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "第二十一条" + }, + "第二十二条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "第二十二条" + }, + "第二十三条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "第二十三条" + }, + "第二十四条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "第二十四条" + }, + "附表1:员工支出报销审批权限表": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012009, + "update_time": 1779012009, + "_id": "附表1:员工支出报销审批权限表" + }, + "附表2:岗位支出报销审批权限表": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012011, + "update_time": 1779012011, + "_id": "附表2:岗位支出报销审批权限表" + }, + "附表3:支出归口管理部门与归口业务范围": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012011, + "update_time": 1779012011, + "_id": "附表3:支出归口管理部门与归口业务范围" + }, + "业务招待": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6", + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 2, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "业务招待" + }, + "报销资格": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "报销资格" + }, + "捐赠申请": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "捐赠申请" + }, + "预算调整决策程序": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "预算调整决策程序" + }, + "Departments And Units": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "Departments And Units" + }, + "Company Business Travel System": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "Company Business Travel System" + }, + "Transportation Level Standards": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012012, + "update_time": 1779012012, + "_id": "Transportation Level Standards" + }, + "Hotel Accommodation Standards": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012013, + "update_time": 1779012013, + "_id": "Hotel Accommodation Standards" + }, + "Travel Allowance Standards": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012013, + "update_time": 1779012013, + "_id": "Travel Allowance Standards" + }, + "Company Property Rental Management": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012013, + "update_time": 1779012013, + "_id": "Company Property Rental Management" + }, + "Business Trip Approval": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012013, + "update_time": 1779012013, + "_id": "Business Trip Approval" + }, + "Company Leadership": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012013, + "update_time": 1779012013, + "_id": "Company Leadership" + }, + "Senior Managers": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012014, + "update_time": 1779012014, + "_id": "Senior Managers" + }, + "Middle Managers": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012014, + "update_time": 1779012014, + "_id": "Middle Managers" + }, + "Basic Level Managers": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012015, + "update_time": 1779012015, + "_id": "Basic Level Managers" + }, + "Other Employees": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012015, + "update_time": 1779012015, + "_id": "Other Employees" + }, + "Remote Work Housing": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012015, + "update_time": 1779012015, + "_id": "Remote Work Housing" + }, + "Commercial Insurance": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012015, + "update_time": 1779012015, + "_id": "Commercial Insurance" + }, + "Transportation Cost Reimbursement": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012016, + "update_time": 1779012016, + "_id": "Transportation Cost Reimbursement" + }, + "Accommodation Cost Reimbursement": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012016, + "update_time": 1779012016, + "_id": "Accommodation Cost Reimbursement" + }, + "Hong Kong, Macau, And Taiwan Region": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012016, + "update_time": 1779012016, + "_id": "Hong Kong, Macau, And Taiwan Region" + }, + "Directly-Controlled Municipalities And Special Administrative Regions": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012016, + "update_time": 1779012016, + "_id": "Directly-Controlled Municipalities And Special Administrative Regions" + }, + "Provincial Capitals": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012016, + "update_time": 1779012016, + "_id": "Provincial Capitals" + }, + "Other Areas": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012017, + "update_time": 1779012017, + "_id": "Other Areas" + }, + "Night High-Speed Rail Provision": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012017, + "update_time": 1779012017, + "_id": "Night High-Speed Rail Provision" + }, + "Taxi Usage Regulations": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012017, + "update_time": 1779012017, + "_id": "Taxi Usage Regulations" + }, + "Self-Driving Travel Provisions": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012017, + "update_time": 1779012017, + "_id": "Self-Driving Travel Provisions" + }, + "Remote Work Housing Rental Expenses": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012018, + "update_time": 1779012018, + "_id": "Remote Work Housing Rental Expenses" + }, + "External Conference Accommodation": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012018, + "update_time": 1779012018, + "_id": "External Conference Accommodation" + }, + "取消报销规定内容": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012018, + "update_time": 1779012018, + "_id": "取消报销规定内容" + }, + "新增规定内容": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012018, + "update_time": 1779012018, + "_id": "新增规定内容" + }, + "商旅订票规范": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012018, + "update_time": 1779012018, + "_id": "商旅订票规范" + }, + "审批权限变化情况": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012019, + "update_time": 1779012019, + "_id": "审批权限变化情况" + }, + "投标保证金": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012019, + "update_time": 1779012019, + "_id": "投标保证金" + }, + "审批流转程序": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012019, + "update_time": 1779012019, + "_id": "审批流转程序" + }, + "出差规定": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012019, + "update_time": 1779012019, + "_id": "出差规定" + }, + "财务信息化系统": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012020, + "update_time": 1779012020, + "_id": "财务信息化系统" + }, + "支出报销申请与审批": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 2, + "create_time": 1779012020, + "update_time": 1779012020, + "_id": "支出报销申请与审批" + }, + "重点支出管理规定": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012020, + "update_time": 1779012020, + "_id": "重点支出管理规定" + }, + "备用金借款": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 3, + "create_time": 1779012020, + "update_time": 1779012020, + "_id": "备用金借款" + }, + "市内交通费": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe", + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 3, + "create_time": 1779012022, + "update_time": 1779012022, + "_id": "市内交通费" + }, + "审批权限表": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012022, + "update_time": 1779012022, + "_id": "审批权限表" + }, + "1 Yuan Per Person Per Kilometer Reimbursement": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "1 Yuan Per Person Per Kilometer Reimbursement" + }, + "Official Vehicle Subsidy": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Official Vehicle Subsidy" + }, + "Department Manager": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Department Manager" + }, + "Director": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Director" + }, + "First-Level Department General Manager": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "First-Level Department General Manager" + }, + "Institution General Manager": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Institution General Manager" + }, + "Business Division General Manager": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Business Division General Manager" + }, + "Vice President": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Vice President" + }, + "Chief Engineer": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012023, + "update_time": 1779012023, + "_id": "Chief Engineer" + }, + "Senior Vice President": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012024, + "update_time": 1779012024, + "_id": "Senior Vice President" + }, + "President": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012024, + "update_time": 1779012024, + "_id": "President" + }, + "Committee Chairpersons": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012024, + "update_time": 1779012024, + "_id": "Committee Chairpersons" + }, + "Bid Security Deposit Approval Limits Table": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012024, + "update_time": 1779012024, + "_id": "Bid Security Deposit Approval Limits Table" + }, + "50000 Yuan Approval Limit": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012024, + "update_time": 1779012024, + "_id": "50000 Yuan Approval Limit" + }, + "100000 Yuan Approval Limit": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012025, + "update_time": 1779012025, + "_id": "100000 Yuan Approval Limit" + }, + "200000 Yuan Approval Limit": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012025, + "update_time": 1779012025, + "_id": "200000 Yuan Approval Limit" + }, + "5000000 Yuan Approval Limit": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012025, + "update_time": 1779012025, + "_id": "5000000 Yuan Approval Limit" + }, + "Commercial Travel System": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012026, + "update_time": 1779012026, + "_id": "Commercial Travel System" + }, + "Multi-Level Approval Rule": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012026, + "update_time": 1779012026, + "_id": "Multi-Level Approval Rule" + }, + "Final Approval Position": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012026, + "update_time": 1779012026, + "_id": "Final Approval Position" + }, + "Company Hotel Accommodation Limit Standards": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012026, + "update_time": 1779012026, + "_id": "Company Hotel Accommodation Limit Standards" + }, + "公司支出管理办法(2024)": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012026, + "update_time": 1779012026, + "_id": "公司支出管理办法(2024)" + }, + "国家电网公司": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "国家电网公司" + }, + "国网数科公司": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "国网数科公司" + }, + "办法": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "办法" + }, + "2024年4月17日": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012027, + "update_time": 1779012027, + "_id": "2024年4月17日" + }, + "预算先行": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012028, + "update_time": 1779012028, + "_id": "预算先行" + }, + "厉行节约": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012028, + "update_time": 1779012028, + "_id": "厉行节约" + }, + "分级授权": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012028, + "update_time": 1779012028, + "_id": "分级授权" + }, + "分类控制": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012028, + "update_time": 1779012028, + "_id": "分类控制" + }, + "批办分离": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "批办分离" + }, + "业务招待费": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "业务招待费" + }, + "会议费": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "会议费" + }, + "广告宣传费": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 2, + "update_time": 1779012089, + "_id": "广告宣传费" + }, + "控股子公司": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "控股子公司" + }, + "分支机构": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "分支机构" + }, + "经办部门": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012029, + "update_time": 1779012029, + "_id": "经办部门" + }, + "各级管理人员": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012030, + "update_time": 1779012030, + "_id": "各级管理人员" + }, + "归口管理部门": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012044, + "update_time": 1779012044, + "_id": "归口管理部门" + }, + "远光制度〔2024〕14号": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012044, + "update_time": 1779012044, + "_id": "远光制度〔2024〕14号" + }, + "修订说明": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012032, + "update_time": 1779012032, + "_id": "修订说明" + }, + "员工支出报销审批权限表": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 2, + "create_time": 1779012032, + "update_time": 1779012032, + "_id": "员工支出报销审批权限表" + }, + "岗位支出报销审批权限表": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263", + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 2, + "create_time": 1779012032, + "update_time": 1779012032, + "_id": "岗位支出报销审批权限表" + }, + "支出归口管理部门与归口业务范围": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012033, + "update_time": 1779012033, + "_id": "支出归口管理部门与归口业务范围" + }, + "效益优先": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012033, + "update_time": 1779012033, + "_id": "效益优先" + }, + "公司各部门": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012033, + "update_time": 1779012033, + "_id": "公司各部门" + }, + "因公借款": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012033, + "update_time": 1779012033, + "_id": "因公借款" + }, + "其他支出(员工)": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012034, + "update_time": 1779012034, + "_id": "其他支出(员工)" + }, + "资产采购": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012034, + "update_time": 1779012034, + "_id": "资产采购" + }, + "基建工程": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012034, + "update_time": 1779012034, + "_id": "基建工程" + }, + "股权投资、兼并收购": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "股权投资、兼并收购" + }, + "材料采购": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "材料采购" + }, + "分包外包(内部单位)": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "分包外包(内部单位)" + }, + "分包外包(外部单位)": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "分包外包(外部单位)" + }, + "保证金": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "保证金" + }, + "销售退款": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "销售退款" + }, + "房屋租金": { + "chunk_ids": [ + "chunk-9841d66d8fb8548aab40220663a51693" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "房屋租金" + }, + "基本出差补贴": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012048, + "update_time": 1779012048, + "_id": "基本出差补贴" + }, + "商旅系统": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362", + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 2, + "create_time": 1779012049, + "update_time": 1779012049, + "_id": "商旅系统" + }, + "业务佐证材料": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012036, + "update_time": 1779012036, + "_id": "业务佐证材料" + }, + "探亲路费": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012037, + "update_time": 1779012037, + "_id": "探亲路费" + }, + "调动工作": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012037, + "update_time": 1779012037, + "_id": "调动工作" + }, + "直辖市": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012037, + "update_time": 1779012037, + "_id": "直辖市" + }, + "特区": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012037, + "update_time": 1779012037, + "_id": "特区" + }, + "西藏": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012037, + "update_time": 1779012037, + "_id": "西藏" + }, + "第十四条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "第十四条" + }, + "第十五条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "第十五条" + }, + "第十六条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "第十六条" + }, + "公司总裁": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362", + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 2, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "公司总裁" + }, + "商旅客服": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012038, + "update_time": 1779012038, + "_id": "商旅客服" + }, + "公司员工探亲管理办法": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "公司员工探亲管理办法" + }, + "增值税发票": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "增值税发票" + }, + "税控系统明细清单": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "税控系统明细清单" + }, + "财务": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "财务" + }, + "经办人": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "经办人" + }, + "业务原始凭据": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "业务原始凭据" + }, + "报销申请时限": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "报销申请时限" + }, + "预付款项": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012039, + "update_time": 1779012039, + "_id": "预付款项" + }, + "公司": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea", + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 2, + "update_time": 1779012094, + "_id": "公司" + }, + "员工": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012040, + "update_time": 1779012040, + "_id": "员工" + }, + "供应商": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012040, + "update_time": 1779012040, + "_id": "供应商" + }, + "支出报销审批": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012041, + "update_time": 1779012041, + "_id": "支出报销审批" + }, + "预算内支出": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "预算内支出" + }, + "正式员工": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "正式员工" + }, + "分管领导": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "分管领导" + }, + "一万元": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "一万元" + }, + "出租车": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "出租车" + }, + "部门负责人": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "部门负责人" + }, + "出差审批程序": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "出差审批程序" + }, + "交通费": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012042, + "update_time": 1779012042, + "_id": "交通费" + }, + "住宿费": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012043, + "update_time": 1779012043, + "_id": "住宿费" + }, + "出差补贴": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012043, + "update_time": 1779012043, + "_id": "出差补贴" + }, + "成本中心归属": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012043, + "update_time": 1779012043, + "_id": "成本中心归属" + }, + "责任原则": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012043, + "update_time": 1779012043, + "_id": "责任原则" + }, + "受益原则": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012044, + "update_time": 1779012044, + "_id": "受益原则" + }, + "结算起点": { + "chunk_ids": [ + "chunk-061324cc36078214691a6fc1cd0aaeea" + ], + "count": 1, + "create_time": 1779012044, + "update_time": 1779012044, + "_id": "结算起点" + }, + "Procurement Management Regulations": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012081, + "update_time": 1779012081, + "_id": "Procurement Management Regulations" + }, + "Tax Authority Recognized Invoice": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012094, + "update_time": 1779012094, + "_id": "Tax Authority Recognized Invoice" + }, + "Financial Review": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012085, + "update_time": 1779012085, + "_id": "Financial Review" + } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_docs.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_docs.json index f04eeaf..c1b40ba 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_docs.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_docs.json @@ -5,5 +5,12 @@ "create_time": 1779011842, "update_time": 1779011842, "_id": "2c1cb358f08d44ceb0e4d287133206ec" + }, + "a8f8465df08e455ebe133351721d49f8": { + "content": "# 产品需求文档\n## 文档信息\n| 项目 | 内容 |\n|------|------|\n| 项目名称 |\n无单报销\n|\n| 版本 | V1.0 |\n| 日期 | 2026-05-06 |\n| 状态 | 正式版 |\n---\n## 1. 项目概述\n### 1.1 项目背景\n面向\n大型企业,\n从业务人员视角出发,解决现有ERP使用体验不佳的问题。\n在ERP的发展历程中,“单据化”曾是财务合规的一大进步,它确保了每笔支出都有据可查。但不可否认,传统的人工填单确实\n也制造了很多\n“枷锁”。在AI时代,解决这一困境的核心逻辑是从“人适应系统”\n转向\n“系统适应人”。\n### 1.2 产品定位\n| 属性 | 说明 |\n|------|------|\n| 产品类型 |\n无单报销工具\n(Web端) |\n| 核心价值 |\n全自动感知、智能风控\n|\n| 目标用户 | 企业业务人员(非财务专业人员) |\n| 使用场景 | 费用报销 |\n### 1.3 产品别名\n-\n产品名称:总裁办\n-\n系统定位:\n影子ERP(Shadow Ledger):本工具将前置于erp系统,用户只需要在工具中操作即完成,将ERP作为后端的\"数据库\"和\"归档库\"。\n零报销(Zero-Expense):业务整理完成即报销、入账,不是\"让报销更快\",而是\"让报销消失\"。\n即效合规:严格遵循财务风控边界,沉淀开箱即用的财务业务专业能力。\n---\n## 2. 用户角色\n### 2.1 系统用户\n| 角色 | 说明 | 权限范围 |\n|------|------|----------|\n| 业务人员 | 发起报销的一线员工 | 发起报销、查看进度、上传凭证 |\n### 2.2 AI助手角色\n| 角色ID | 角色名称 | 职责说明 |\n|--------|----------|----------|\n| 行政秘书 | 行政秘书 | 负责行政事务管理,包括日程安排、会议组织、对外接待等,配合总裁办高效运转 |\n| 费控专责 | 费控专责 | 负责费用审核与控制,确保费用报销符合公司制度和预算要求 |\n| 财务BP | 财务BP | 作为财务业务伙伴,为业务部门提供财务支持和专业建议 |\n| 采购专员 | 采购专员 | 负责采购流程管理,供应商评估与采购执行 |\n---\n## 3. 系统架构\n### 3.1 整体布局\n```\n┌─────────────────────────────────────────────────────────┐\n│ 标题栏 (35px) │\n├────┬────────────┬──────────────────────────────────────┤\n│ 活动栏 │ 侧边栏 │ 主内容区 │\n│(50px)│ (250px) │ │\n│ │ │ ┌────────────────────────────────┐ │\n│ │ │ │ 内容区域 │ │\n│ │ │ │ │ │\n│ │ │ ├────────────────────────────────┤ │\n│ │ │ │ 底部对话栏 (可选展开) │ │\n│ │ │ └────────────────────────────────┘ │\n├────┴────────────┴──────────────────────────────────────┤\n│ 状态栏 (22px) │\n└─────────────────────────────────────────────────────────┘\n```\n### 3.2 配色方案\n| CSS变量 | 颜色值 | 用途 |\n|---------|-------|------|\n| --bg-dark | #ffffff | 主背景色 |\n| --bg-sidebar | #f3f3f3 | 侧边栏背景 |\n| --bg-activity | #f3f3f3 | 活动栏背景 |\n| --bg-title | #dddddd | 标题栏背景 |\n| --bg-hover | #e8e8e8 | 悬停背景 |\n| --bg-active | #d4d4d4 | 激活背景 |\n| --border-color | #e1e1e1 | 边框色 |\n| --text-primary | #333333 | 主文字色 |\n| --text-secondary | #6e6e6e | 辅文字色 |\n| --text-bright | #000000 | 高亮文字 |\n| --accent | #007acc | 强调色(主按钮) |\n| --accent-hover | #005a9e | 强调色(悬停) |\n| --danger | #d32f2f | 危险/错误色 |\n| --icon-color | #333333 | 图标色 |\n| --transition | all 0.15s ease | 过渡动画 |\n---\n## 4. 功能需求\n### 4.1 功能模块总览\n```\n┌─────────────────────────────────────────────────────────────┐\n│ 总裁办 │\n├─────────────────────────────────────────────────────────────┤\n│\n菜单2\n. 事项\n列表\n│\n│ -\n全部\n事项 │\n├─────────────────────────────────────────────────────────────┤\n│\n菜单1\n. 无单报销 │\n│ - 发起事项 │\n│ -\n附件\n上传\n/小程序码上传\n│\n│ - 时间轨迹 │\n│ - AI预审 │\n│ - 提交报销 │\n├─────────────────────────────────────────────────────────────┤\n│\n功能点3\n. AI助手 │\n│ - 全局对话 │\n├─────────────────────────────────────────────────────────────┤\n│\n功能点\n4. 用户中心 │\n│ - 个人中心 │\n│ - 登出 │\n└─────────────────────────────────────────────────────────────┘\n```\n---\n### 4.2\n菜单2\n:事项\n列表\n#### 4.2.1 侧边栏菜单\n| 菜单项 | 显示内容 | 提醒 |\n|--------|---------|------|\n| 无单报销 | 主菜单 |\n无\n|\n|\n事项列表\n| 待办事项数 | 红色\n图标\n显示\n待办\n数量 |\n#### 4.2.2 功能说明\n-\n用户可查看待处理和已处理的事项列表\n-\n每条事项显示:事项摘要、金额、状态、截止时间\n-\n\n双击每条\n事项可查看详情\n- 列字段可筛选\n序\n业务类型\n事项摘要\n金额\n状态\n发起时间\n最后修改时间\n1\n差旅\nQ1季度业务进展汇报\n1678.20\n未提交\n2026-4-6 12:33\n:\n00\n2026-4-\n22\n12:33\n:\n00\n2\n差旅\n参加\n2026全国AI\n技术峰会(合肥)\n3498.39\n未提交\n2026-4-\n8\n\n0\n2:33\n:\n00\n2026-4-\n15\n\n12\n:\n41:\n00\n3\n差旅\n智慧平台江苏省公司运营支持\n1345.87\n已提交\n2026-4-\n12\n\n09\n:\n5\n3\n:12\n2026-4-\n15\n\n10\n:\n5\n3\n:00\n---\n### 4.3 模块2:无单报销\n#### 4.3.1 发起事项\n用户可通过以下方式发起报销:\n| 发起方式 | 说明 |\n|----------|------|\n| 发起事项 | 点击\"发起事项\"按钮,上传本机附件,系统自动整理账单 |\n| 小程序码 | 点击\"小程序码\"按钮,通过小程序扫码上传手机中的文件 |\n#### 4.3.2 凭证上传\n点击后弹出文件夹上传,或直接拖拽文件。\n| 功能点 | 说明 |\n|--------|------|\n| 拖拽上传 | 支持将文件拖入上传区域 |\n| 点击上传 | 点击上传区域选择文件 |\n| 文件格式 | 支持PDF、JPG、JPEG、PNG\n、XLSX\n|\n| 多文件上传 | 支持同时上传多个文件 |\n| 文件预览 | 上传后显示文件缩略图列表 |\n| 重置 | 支持清空已上传文件重新选择 |\n| 确定 | 确认文件并进入下一步 |\n点击确认后开始生成时间轨迹图。\n#### 4.3.3 时间轨迹\n上传文件后显示时间轨迹,格式如下:\n#####\n时间轨迹\n| 字段 | 说明 |\n|------|------|\n|\n名称\n|\n自动命名为摘要+时间轨迹图,如:“Q1季度业务进展汇报差旅时间轨迹图”\n\n、后续如有其他时间类型如:“高校教师产学研项目调研业务招待时间轨迹图”\n|\n|\n时间\n|\n年/月/日-年/月/日\n|\n|\n地点\n|\n如:北京-上海-北京\n|\n|\n总支出\n|\n等于商旅预订+线下支付,如:7655.00\n|\n|\n商旅预订\n|\n如:5990.00\n|\n|\n线下自付\n|\n如:1665.00\n|\n#####\n详细\n字段\n| 字段 | 说明 |\n|------|------|\n|\n时间\n|\n日/月\n|\n| 图标 | 交通\n(出发/到达)\n/住宿 |\n|\n城市\n|\n所在城市名称\n|\n|\n描述\n|\n如:北京大兴机场-武汉天河机场 CZ3118\n|\n| 金额 | 费用金额 |\n| 凭证缩略图 | 已上传凭证的缩略预览 |\n| 问题标记 | ⚠缺失凭证等问题的提示 |\n##### 账单项目类型\n| 类型 | 图标颜色 | 示例 |\n|------|---------|------|\n| 交通 | 蓝色 | 高铁、出租车 |\n| 住宿 | 紫色 | 酒店住宿 |\n| 商旅预订 | 绿色 | 带\"商旅预订\"标签 |\n|\n线下自付\n|\n橙\n色 | 带\"\n线下自付\n\"标签 |\n##### 问题项目标识\n-\n显示问题图标(⚠)\n-\n显示问题描述文字(如\"缺少支付凭证截图\")\n-\n提供\"补充上传\"按钮\n#### 4.3.4 凭证识别\n点击凭证缩略图可查看自动识别的凭证信息:\n| 字段 | 说明 |\n|------|------|\n| 凭证标题 | 如\"高铁火车票\" |\n| 识别结果 | 各项识别内容(出发地、目的地、车次、座位、票价、日期等) |\n| 发票类型 | 凭证类型描述 |\n| 合规检查 | ✓ 通过 / ✗ 不通过 |\n##### 支持的凭证类型\n| 凭证类型 | 识别字段 |\n|----------|----------|\n| 火车票 | 出发地、目的地、车次、座位、票价、日期 |\n| 出租车发票\n及行程单\n| 车号、日期、时间、起点、终点、金额 |\n| 住宿发票\n及流水\n| 酒店名称、入住日期、退房日期、天数、房型、金额 |\n|\n公共交通\n发票\n及行程单\n| 日期、时间、起点、终点、金额 |\n|\n飞机\n票\n行程单\n| 出发地、目的地、\n航班号\n、座位、票价、日期\n、税额\n|\n#### 4.3.5 AI预审\n预审按钮在时间轨迹图的末尾行区域\n点击\"预审\"按钮执行AI预审:\n| 功能点 | 说明 |\n|--------|------|\n| 完整性检查 | 检查各项凭证是否完整 |\n| 合规性检查 | 检查是否符合费用标准 |\n| 问题标注 | 自动标注问题项 |\n附:审核点\n序\n检查项\n是/否\n类型\n1\n上传附件中是否有商旅附件信息?\n是(2)/否(提示:请补充商旅附件)\n完整性\n2\n检查商旅附件上是否有预订火车票信息?\n是(3)/否(✅)\n完整性\n3\n商旅附件预订火车票信息是否均有上传对应的报销凭证?\n是(✅)/否(提示:请补充xxx-xxx的火车票报销凭证)\n完整性\n4\n根据商旅附件上的出差起始地和到达地(如有途经地也要包含)检查出发和返回所到的城市轨迹是否为闭环,即有去有回?(远距离跨城:火车+火车、飞机+火车、飞机+飞机需要闭环;近距离跨城或市内:汽车+汽车、汽车+打车、打车+打车、打车+公交地铁、公共地铁+公交地铁、汽车+公交地铁需要闭环)\n是(✅)/否(5)\n完整性\n5\n如果商旅附件中有行程非闭环,需要检查此段行程是否有上传的对应的报销凭证,火车或飞机行程的,除了票据外,还需要检查是否有线下预订的审批邮件、线下预订的支付凭证截图。\n是(✅)/否(提示1:xxx-xxx行程断档,请提供相应的报销凭证,如是线下预订请提供审批邮件和支付截图。2:xxx-xxx行程火车/飞机票未通过商旅预订,请提供审批邮件和支付截图。)\n完整性\n6\n检查商旅附件上的酒店住宿城市与出差所在城市是否相符?\n是(✅)/否(提示:出差目的地为xxx,住宿城市为xxx,请解释说明)\n合规性\n7\n检查商旅附件上的酒店住宿天数(如有线下预订也要算上)与出差时间是否相符?(尤其是住宿天数大于出差天数时,并且出差天数根据交通轨迹来计算比商旅附件上的更准确)\n是(✅)/否(提示:出差天数\nx\n天,酒店住宿x天,请解释说明不符原因)\n合规性\n8\n检查上传附件中是否有酒店住宿凭证(发票、线下预订审批邮件、支付截图)?\n是(9)/否(✅)\n完整性\n9\n检查上传附件中的酒店住宿凭证,与商旅附件是否有重合?\n是(提示:线上线下酒店预订有重合,请检查)/否(✅)\n合规性\n10\n检查上传附件中的汽车、打车、公交地铁时间,与火车飞机的行程时间是否有重合?\n是(提示:xxx-xxx行程与xxx-xxx行程时间有重合,请检查)/否(✅)\n合规性\n11\n检查线下预订火车票是否超标?\n是(提示:火车票\nxxx\n超标,请检查)/否(✅)\n合规性\n12\n检查线下预订飞机票是否超标?\n是(提示:飞机票\nxxx\n超标,请检查)/否(✅)\n合规性\n13\n检查线下预订住宿酒店是否超标?\n是(提示:x月x日酒店住宿xxx元已超标,请检查)/否(✅)\n合规性\n14\n检查出租车行程时间是否在夜间22:00之后,如果不是需要补充说明\n是(提示:请补充打车说明)/否(✅)\n合规性\n15\n检查如果有出租车发票,需要同步提供行程单\n是(提示:请补充发票xxxx(金额xxx)对应的行程单)/否(✅)\n完整性\n16\n检查如果有机场大巴汽车票,需要同步提供车票或预订截图\n是(提示:请补充车票或预订截图/否(✅)\n完整性\n17\n检查如果有地铁发票,需要同步提提供地铁行程截图和支付截图\n是(提示:请补充行程截图和支付截图/否(✅)\n完整性\n18\n发票抬头检查,所有车票、发票抬头必须是\nxxxxxxx\n,否则为开错\n是(✅)/否(提示:票据xxxx抬头错误,请检查)\n合规性\n##### 预审结果\n-\n正常项目:标记为\"已修复\"/\"issue-fixed\"\n-\n问题项目:\n-\n显示\"⚠\"图标\n-\n显示问题描述\n-\n提供\"补充上传\"按钮\n-\n问题汇总:显示\"预审发现问题(N项)\"及具体描述\n##### 预审通过\n-\n提示\"预审通过!所有单据完整且合规。\"\n#### 4.3.6 提交报销\n点击\"去报销吧\"按钮:\n| 步骤 | 说明 |\n|------|------|\n| 确认提示 | \"请注意:总裁办去报销了,前期事务已完结且不可回退。\" |\n| 提交 | 确认后提交报销申请 |\n| 结果提示 | \"已提交报销申请,等待审批...\" |\n---\n### 4.4 模块3:AI助手\n#### 4.4.1 全局对话栏\n底部悬浮对话栏,支持快速交互:\n| 功能点 | 说明 |\n|--------|------|\n| 触发区域 | 点击展开对话栏 |\n| 展开/收起 | 点击按钮切换状态 |\n| 欢迎消息 | \"您好!有什么可以帮您?\" |\n| 快捷回复 | 显示可用的操作选项 |\n| 消息输入 | 支持输入文字消息 |\n| 发送 | 支持点击发送和回车发送 |\n##### 消息类型\n| 类型 | 说明 |\n|------|------|\n| 用户消息 | 右侧显示,蓝色背景 |\n| AI回复 | 左侧显示,灰色背景 |\n#### 4.4.2 召唤面板\n点击\"召唤\"入口显示召唤面板:\n| 功能 | 说明 |\n|------|------|\n| 模式切换 | 编辑指令 / 选择助理 |\n| 编辑指令 | 文本框输入自定义指令 |\n| 选择助理 | 预设助理卡片列表,单选 |\n##### 预设助理卡片\n| 卡片 | 内容 |\n|------|------|\n| 头像 | 渐变色背景 |\n| 名称 | 角色名称 |\n| 设置 | 点击可配置角色信息 |\n##### 角色配置弹窗\n| 字段 | 说明 |\n|------|------|\n| 角色名称 | 可编辑的助手名称 |\n| 角色设定 | 角色的职责描述 |\n| Prompt | AI提示词模板 |\n#### 4.4.3 角色Prompt模板\n| 角色 | Prompt格式 |\n|------|-----------|\n| 行政秘书 | 角色:{role}\\n\\n请帮我处理以下行政事务:\\n{task}\\n\\n要求:\\n1. 严格按照公司制度执行\\n2. 注意保密事项\\n3. 及时反馈进度 |\n| 费控专责 | 角色:{role}\\n\\n请帮我审核以下费用报销:\\n{expense}\\n\\n要求:\\n1. 核对发票真实性\\n2. 检查是否符合费用标准\\n3. 确认预算充足 |\n| 财务BP | 角色:{role}\\n\\n请帮我分析以下财务问题:\\n{question}\\n\\n要求:\\n1. 提供专业财务建议\\n2. 注意合规性\\n3. 兼顾业务发展 |\n| 采购专员 | 角色:{role}\\n\\n请帮我处理以下采购事项:\\n{purchase}\\n\\n要求:\\n1. 遵循采购流程\\n2. 比价择优\\n3. 记录台账 |\n---\n### 4.5 模块4:用户中心\n#### 4.5.1 用户菜单\n点击头像显示下拉菜单:\n| 项目 | 说明 |\n|------|------|\n| 用户名 | 当前登录用户 |\n| 组织 | 所属组织名称(如\"Ashy有限公司本部\") |\n| 设置 | 跳转设置页面 |\n| 登出 | 退出登录 |\n---\n### 4.6 模块5:扫码上传\n#### 4.6.1 扫码入口\n| 位置 | 形式 |\n|------|------|\n| 活动栏 | 扫码图标按钮 |\n| 主内容区 | 小程序码卡片(悬浮) |\n| 底部浮动 | 扫码图标(悬停提示) |\n#### 4.6.2 功能说明\n-\n点击提示:\"请使用小程序码扫码,在手机上选择文件上传\"\n---\n### 4.7 模块6:状态栏\n底部状态栏显示:\n| 显示内容 | 说明 |\n|----------|------|\n| 行列号 | 当前光标位置 |\n| 编码 | UTF-8 |\n| 工作台 | 总裁办 |\n---\n## 5. 界面要求\n### 5.1 布局规格\n| 区域 | 高度/宽度 | 说明 |\n|------|-----------|------|\n| 标题栏 | 35px | 顶部固定 |\n| 活动栏 | 50px | 左侧固定 |\n| 侧边栏 | 250px | 可折叠 |\n| 状态栏 | 22px | 底部固定 |\n| 底部对话栏 | 可变 | 展开高度280px,收起高度约44px |\n### 5.2 交互规范\n| 交互 | 效果 |\n|------|------|\n| 按钮悬停 | 背景色变化,transition 0.15s |\n| 卡片悬停 | 背景色变化 |\n| 模态框 | 点击外部关闭 |\n| 下拉菜单 | 点击外部关闭 |\n| 侧边栏折叠 | 宽度过渡0.2s |\n---\n## 6. 数据流程\n### 6.1 报销流程\n```\n1. 发起事项 → 2. 上传凭证 → 3. 生成时间轨迹 → 4. AI预审\n(循环)\n→ 5.\n修改(循环)\n→\n6\n.\n提\n交报销\n```\n### 6.2 凭证处理流程\n```\n上传文件 → 自动识别 → 生成时间轨迹 → 显示凭证缩略图 → 可查看详情\n```\n### 6.3 AI预审流程\n```\n点击预审 → 检查完整性 → 检查合规性 → 标注问题 → 显示结果\n```\n---\n\n# 问答线索补充\n\n以下内容由入库阶段根据章节标题、条款、列表、键值对与相邻正文提炼,供问答检索时优先命中更短、更直接的制度依据。\n\n- 正文:# 产品需求文档\n- 正文:## 1. 项目概述\n- 正文:### 1.1 项目背景\n- 正文:从业务人员视角出发,解决现有ERP使用体验不佳的问题\n- 正文:“枷锁”\n- 正文:在AI时代,解决这一困境的核心逻辑是从“人适应系统”\n- 正文:“系统适应人”\n- 正文:### 1.2 产品定位\n- 正文:无单报销工具\n- 正文:### 1.3 产品别名\n- 正文:产品名称:总裁办\n- 正文:影子ERP(Shadow Ledger):本工具将前置于erp系统,用户只需要在工具中操作即完成,将ERP作为后端的\"数据库\"和\"归档库\"\n- 正文:零报销(Zero-Expense):业务整理完成即报销、入账,不是\"让报销更快\",而是\"让报销消失\"\n- 正文:即效合规:严格遵循财务风控边界,沉淀开箱即用的财务业务专业能力\n- 正文:## 2. 用户角色\n- 正文:### 2.1 系统用户\n- 正文:### 2.2 AI助手角色\n- 正文:## 3. 系统架构\n- 正文:### 3.1 整体布局\n- 正文:│ 标题栏 (35px) │\n- 正文:│(50px)│ (250px) │ │\n- 正文:│ 状态栏 (22px) │\n- 正文:### 3.2 配色方案\n- 正文:## 4. 功能需求", + "file_path": "/app/server/storage/knowledge/报销制度/a8f8465df08e455ebe133351721d49f8__无单需求文档0506.docx", + "create_time": 1779206397, + "update_time": 1779206397, + "_id": "a8f8465df08e455ebe133351721d49f8" } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_entities.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_entities.json index 160de12..815b966 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_entities.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_entities.json @@ -1,268 +1,268 @@ -{ - "2c1cb358f08d44ceb0e4d287133206ec": { - "entity_names": [ - "工会委员会", - "Business Original Documents", - "First Approver", - "P8", - "一级部门总经理", - "组织人事部", - "业务原始凭据", - "营销中心", - "保证金", - "投标保证金", - "餐补", - "第十四条业务招待费", - "Chief Engineer", - "业务招待", - "Employee Welfare", - "经济舱", - "2024年4月17日", - "三等舱", - "财务信息化系统", - "分管领导", - "重点支出管理规定", - "备用金借款", - "Financial Review", - "第五章附则", - "Company Leadership", - "第十九条", - "经办人", - "预算内支出", - "Current Account Payment", - "Business Entertainment", - "Tax Control System Details", - "第二十一条", - "成本中心归属", - "岗位支出报销审批权限表", - "工会经费管理办法", - "商旅系统", - "Special Subsidy", - "中国银行外汇折算价", - "因公借款", - "资产采购", - "广告费", - "First-Level Department General Manager", - "正式员工", - "一万元", - "公司员工教育培训管理办法", - "责任原则", - "第二章职责分工", - "预算先行", - "Planning and Finance Department", - "Accommodation Cost Reimbursement", - "Official Vehicle Subsidy", - "第四条归口管理部门主要职责", - "Personal Service Compensation", - "邮递费", - "附表3:支出归口管理部门与归口业务范围", - "员工", - "第二条目的", - "Director", - "支出归口管理部门与归口业务范围", - "其他支出(员工)", - "报销标准", - "5000000 Yuan Approval Limit", - "第十一条备用金借款", - "会议费", - "第十七条", - "第七条各级管理人员主要职责", - "50000 Yuan Approval Limit", - "全资子公司", - "涉外业务汇率标准", - "总监", - "第十三条差旅费", - "审批权限表", - "商旅订票规范", - "Final Approval Position", - "报销资格", - "新增报销规定", - "公司支出管理办法", - "Institution General Manager", - "房屋租金", - "Staff Activities", - "分包外包(内部单位)", - "报销申请时限", - "Financial Information System", - "Expenditure Authorization Approval Scope", - "直辖市", - "培训费", - "第十二条市内交通费", - "第十五条", - "终审岗", - "Remote Work Housing", - "Centralized Management department", - "第二十条", - "办公室(党委办公室)", - "Three Flows Consistency Principle", - "审批权限", - "VAT Special Invoice", - "后勤服务部", - "员工支出报销审批权限表", - "公司总裁", - "出差补贴", - "Basic Level Managers", - "预付款项", - "附表1:员工支出报销审批权限表", - "经办部门", - "信息管理部", - "通信费", - "第十六条", - "增值税发票", - "财务入账条件", - "Hotel Accommodation Standards", - "审批流转程序", - "Self-Driving Travel Provisions", - "交通费", - "第九条支出报销审批", - "薪酬福利支出分配计划", - "产品规划设计部", - "因公用车补贴", - "Committee Chairpersons", - "Business Division General Manager", - "组织安排", - "1 Yuan Per Person Per Kilometer Reimbursement", - "Separation of Approval and Processing Principle", - "第五条计划财务部主要职责", - "200000 Yuan Approval Limit", - "公司各部门", - "第十四条", - "Other Areas", - "分支机构", - "Departments And Units", - "计划财务部", - "Other Employees", - "第二十三条", - "公司团建管理办法", - "火车硬席", - "税控系统明细清单", - "Trade Union Fund", - "报销标准变化情况", - "薪酬福利支出", - "Hong Kong, Macau, And Taiwan Region", - "对外捐赠支出", - "Multi-Level Approval Rule", - "Three Working Days Deadline", - "Employee Remuneration", - "销售退款", - "股权投资、兼并收购", - "控股子公司", - "取消报销规定", - "Procurement Management Regulations", - "Middle Managers", - "差旅费", - "批办分离", - "住宿费", - "Travel Allowance Standards", - "第二十三条本办法的归口与实施", - "Senior Vice President", - "供应商", - "人事归口管理部门", - "Management Personnel At All Levels", - "效益优先", - "Operating Department Individual", - "Remote Work Housing Rental Expenses", - "取消报销规定内容", - "Company", - "修订说明", - "国网数科公司", - "Vice President", - "分级授权", - "Expenditure Reimbursement Application", - "第二十四条附件", - "第二十二条", - "出租车", - "Night High-Speed Rail Provision", - "各级管理人员", - "受益原则", - "公司员工因公通讯费用实施细则", - "公司支出管理办法(2024)", - "出差补贴标准", - "Bid Security Deposit Approval Limits Table", - "第二条范围", - "Company Property Rental Management", - "调动工作", - "远光软件股份有限公司", - "市内交通费", - "交通工具等级标准", - "Operator", - "第八条支出报销申请", - "Directly-Controlled Municipalities And Special Administrative Regions", - "出差规定", - "业务招待费", - "Senior Managers", - "逐级审批规则", - "Company Business Travel System", - "广告宣传费", - "Transportation Cost Reimbursement", - "财务", - "第一章总则", - "材料采购", - "人力资源服务部", - "证券与法律事务部", - "Transportation Level Standards", - "归口管理部门", - "商旅客服", - "第四章重点支出管理规定", - "出差审批程序", - "Business Trip Approval", - "西藏", - "附表2:岗位支出报销审批权限表", - "第十八条", - "第二十四条", - "Company Hotel Accommodation Limit Standards", - "办法", - "DAP研发中心", - "新增规定内容", - "基本补助", - "Travel Allowance", - "异地挂职锻炼补贴标准", - "部门负责人", - "Provincial Capitals", - "特区", - "Transportation Tickets", - "第三章支出报销申请与审批", - "品牌及市场运营中心", - "分包外包(外部单位)", - "探亲路费", - "President", - "凭据报销", - "基本出差补贴", - "Taxi Usage Regulations", - "Government Fees", - "Commercial Travel System", - "远光制度〔2024〕14号", - "审批权限变化情况", - "基建工程", - "支出报销申请与审批", - "中国外汇交易中心参考汇率", - "Department Manager", - "支出报销审批", - "预算调整决策程序", - "公司1号文", - "External Conference Accommodation", - "厉行节约", - "Commercial Insurance", - "公司", - "第三条管理原则", - "捐赠申请", - "分类控制", - "业务宣传费", - "产业投资部", - "公司员工探亲管理办法", - "Subsequent Approver", - "100000 Yuan Approval Limit", - "Tax Authority Recognized Invoice", - "国家电网公司", - "业务佐证材料", - "第六条经办部门(个人)主要职责", - "结算起点", - "第十条支出成本中心归属", - "母公司" - ], - "count": 258, - "create_time": 1779012093, - "update_time": 1779012093, - "_id": "2c1cb358f08d44ceb0e4d287133206ec" - } +{ + "2c1cb358f08d44ceb0e4d287133206ec": { + "entity_names": [ + "工会委员会", + "Business Original Documents", + "First Approver", + "P8", + "一级部门总经理", + "组织人事部", + "业务原始凭据", + "营销中心", + "保证金", + "投标保证金", + "餐补", + "第十四条业务招待费", + "Chief Engineer", + "业务招待", + "Employee Welfare", + "经济舱", + "2024年4月17日", + "三等舱", + "财务信息化系统", + "分管领导", + "重点支出管理规定", + "备用金借款", + "Financial Review", + "第五章附则", + "Company Leadership", + "第十九条", + "经办人", + "预算内支出", + "Current Account Payment", + "Business Entertainment", + "Tax Control System Details", + "第二十一条", + "成本中心归属", + "岗位支出报销审批权限表", + "工会经费管理办法", + "商旅系统", + "Special Subsidy", + "中国银行外汇折算价", + "因公借款", + "资产采购", + "广告费", + "First-Level Department General Manager", + "正式员工", + "一万元", + "公司员工教育培训管理办法", + "责任原则", + "第二章职责分工", + "预算先行", + "Planning and Finance Department", + "Accommodation Cost Reimbursement", + "Official Vehicle Subsidy", + "第四条归口管理部门主要职责", + "Personal Service Compensation", + "邮递费", + "附表3:支出归口管理部门与归口业务范围", + "员工", + "第二条目的", + "Director", + "支出归口管理部门与归口业务范围", + "其他支出(员工)", + "报销标准", + "5000000 Yuan Approval Limit", + "第十一条备用金借款", + "会议费", + "第十七条", + "第七条各级管理人员主要职责", + "50000 Yuan Approval Limit", + "全资子公司", + "涉外业务汇率标准", + "总监", + "第十三条差旅费", + "审批权限表", + "商旅订票规范", + "Final Approval Position", + "报销资格", + "新增报销规定", + "公司支出管理办法", + "Institution General Manager", + "房屋租金", + "Staff Activities", + "分包外包(内部单位)", + "报销申请时限", + "Financial Information System", + "Expenditure Authorization Approval Scope", + "直辖市", + "培训费", + "第十二条市内交通费", + "第十五条", + "终审岗", + "Remote Work Housing", + "Centralized Management department", + "第二十条", + "办公室(党委办公室)", + "Three Flows Consistency Principle", + "审批权限", + "VAT Special Invoice", + "后勤服务部", + "员工支出报销审批权限表", + "公司总裁", + "出差补贴", + "Basic Level Managers", + "预付款项", + "附表1:员工支出报销审批权限表", + "经办部门", + "信息管理部", + "通信费", + "第十六条", + "增值税发票", + "财务入账条件", + "Hotel Accommodation Standards", + "审批流转程序", + "Self-Driving Travel Provisions", + "交通费", + "第九条支出报销审批", + "薪酬福利支出分配计划", + "产品规划设计部", + "因公用车补贴", + "Committee Chairpersons", + "Business Division General Manager", + "组织安排", + "1 Yuan Per Person Per Kilometer Reimbursement", + "Separation of Approval and Processing Principle", + "第五条计划财务部主要职责", + "200000 Yuan Approval Limit", + "公司各部门", + "第十四条", + "Other Areas", + "分支机构", + "Departments And Units", + "计划财务部", + "Other Employees", + "第二十三条", + "公司团建管理办法", + "火车硬席", + "税控系统明细清单", + "Trade Union Fund", + "报销标准变化情况", + "薪酬福利支出", + "Hong Kong, Macau, And Taiwan Region", + "对外捐赠支出", + "Multi-Level Approval Rule", + "Three Working Days Deadline", + "Employee Remuneration", + "销售退款", + "股权投资、兼并收购", + "控股子公司", + "取消报销规定", + "Procurement Management Regulations", + "Middle Managers", + "差旅费", + "批办分离", + "住宿费", + "Travel Allowance Standards", + "第二十三条本办法的归口与实施", + "Senior Vice President", + "供应商", + "人事归口管理部门", + "Management Personnel At All Levels", + "效益优先", + "Operating Department Individual", + "Remote Work Housing Rental Expenses", + "取消报销规定内容", + "Company", + "修订说明", + "国网数科公司", + "Vice President", + "分级授权", + "Expenditure Reimbursement Application", + "第二十四条附件", + "第二十二条", + "出租车", + "Night High-Speed Rail Provision", + "各级管理人员", + "受益原则", + "公司员工因公通讯费用实施细则", + "公司支出管理办法(2024)", + "出差补贴标准", + "Bid Security Deposit Approval Limits Table", + "第二条范围", + "Company Property Rental Management", + "调动工作", + "远光软件股份有限公司", + "市内交通费", + "交通工具等级标准", + "Operator", + "第八条支出报销申请", + "Directly-Controlled Municipalities And Special Administrative Regions", + "出差规定", + "业务招待费", + "Senior Managers", + "逐级审批规则", + "Company Business Travel System", + "广告宣传费", + "Transportation Cost Reimbursement", + "财务", + "第一章总则", + "材料采购", + "人力资源服务部", + "证券与法律事务部", + "Transportation Level Standards", + "归口管理部门", + "商旅客服", + "第四章重点支出管理规定", + "出差审批程序", + "Business Trip Approval", + "西藏", + "附表2:岗位支出报销审批权限表", + "第十八条", + "第二十四条", + "Company Hotel Accommodation Limit Standards", + "办法", + "DAP研发中心", + "新增规定内容", + "基本补助", + "Travel Allowance", + "异地挂职锻炼补贴标准", + "部门负责人", + "Provincial Capitals", + "特区", + "Transportation Tickets", + "第三章支出报销申请与审批", + "品牌及市场运营中心", + "分包外包(外部单位)", + "探亲路费", + "President", + "凭据报销", + "基本出差补贴", + "Taxi Usage Regulations", + "Government Fees", + "Commercial Travel System", + "远光制度〔2024〕14号", + "审批权限变化情况", + "基建工程", + "支出报销申请与审批", + "中国外汇交易中心参考汇率", + "Department Manager", + "支出报销审批", + "预算调整决策程序", + "公司1号文", + "External Conference Accommodation", + "厉行节约", + "Commercial Insurance", + "公司", + "第三条管理原则", + "捐赠申请", + "分类控制", + "业务宣传费", + "产业投资部", + "公司员工探亲管理办法", + "Subsequent Approver", + "100000 Yuan Approval Limit", + "Tax Authority Recognized Invoice", + "国家电网公司", + "业务佐证材料", + "第六条经办部门(个人)主要职责", + "结算起点", + "第十条支出成本中心归属", + "母公司" + ], + "count": 258, + "create_time": 1779012093, + "update_time": 1779012093, + "_id": "2c1cb358f08d44ceb0e4d287133206ec" + } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_relations.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_relations.json index 3801c65..6d1a2ff 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_relations.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_full_relations.json @@ -1,166 +1,166 @@ -{ - "2c1cb358f08d44ceb0e4d287133206ec": { - "relation_pairs": [ - [ - "Departments And Units", - "Taxi Usage Regulations" - ], - [ - "取消报销规定内容", - "报销标准变化情况" - ], - [ - "业务招待费", - "第十四条" - ], - [ - "控股子公司", - "计划财务部" - ], - [ - "公司支出管理办法", - "工会委员会" - ], - [ - "第一章总则", - "第三条管理原则" - ], - [ - "广告宣传费", - "第十六条" - ], - [ - "Tax Control System Details", - "VAT Special Invoice" - ], - [ - "Expenditure Reimbursement Application", - "Tax Authority Recognized Invoice" - ], - [ - "远光制度〔2024〕14号", - "远光软件股份有限公司" - ], - [ - "Financial Review", - "Operator" - ], - [ - "Operating Department Individual", - "Procurement Management Regulations" - ], - [ - "会议费", - "第十五条" - ], - [ - "Company", - "Management Personnel At All Levels" - ], - [ - "公司", - "第十七条" - ], - [ - "公司", - "第十八条" - ], - [ - "Operator", - "Three Working Days Deadline" - ], - [ - "第十一条备用金借款", - "第四章重点支出管理规定" - ], - [ - "Expenditure Reimbursement Application", - "Operator" - ], - [ - "业务招待费", - "差旅费" - ], - [ - "公司", - "第二十一条" - ], - [ - "公司支出管理办法(2024)", - "远光软件股份有限公司" - ], - [ - "第四条归口管理部门主要职责", - "计划财务部" - ], - [ - "会议费", - "差旅费" - ], - [ - "Company", - "Operating Department Individual" - ], - [ - "商旅系统", - "差旅费" - ], - [ - "会议费", - "公司总裁" - ], - [ - "计划财务部", - "远光软件股份有限公司" - ], - [ - "公司", - "第十九条" - ], - [ - "公司", - "第二十条" - ], - [ - "Company", - "Planning and Finance Department" - ], - [ - "公司支出管理办法", - "营销中心" - ], - [ - "Business Original Documents", - "Operator" - ], - [ - "公司支出管理办法", - "办公室(党委办公室)" - ], - [ - "Departments And Units", - "Night High-Speed Rail Provision" - ], - [ - "Centralized Management department", - "Company" - ], - [ - "组织人事部", - "调动工作" - ], - [ - "报销标准变化情况", - "远光软件股份有限公司" - ], - [ - "第一章总则", - "远光软件股份有限公司" - ] - ], - "count": 39, - "create_time": 1779012093, - "update_time": 1779012093, - "_id": "2c1cb358f08d44ceb0e4d287133206ec" - } +{ + "2c1cb358f08d44ceb0e4d287133206ec": { + "relation_pairs": [ + [ + "Departments And Units", + "Taxi Usage Regulations" + ], + [ + "取消报销规定内容", + "报销标准变化情况" + ], + [ + "业务招待费", + "第十四条" + ], + [ + "控股子公司", + "计划财务部" + ], + [ + "公司支出管理办法", + "工会委员会" + ], + [ + "第一章总则", + "第三条管理原则" + ], + [ + "广告宣传费", + "第十六条" + ], + [ + "Tax Control System Details", + "VAT Special Invoice" + ], + [ + "Expenditure Reimbursement Application", + "Tax Authority Recognized Invoice" + ], + [ + "远光制度〔2024〕14号", + "远光软件股份有限公司" + ], + [ + "Financial Review", + "Operator" + ], + [ + "Operating Department Individual", + "Procurement Management Regulations" + ], + [ + "会议费", + "第十五条" + ], + [ + "Company", + "Management Personnel At All Levels" + ], + [ + "公司", + "第十七条" + ], + [ + "公司", + "第十八条" + ], + [ + "Operator", + "Three Working Days Deadline" + ], + [ + "第十一条备用金借款", + "第四章重点支出管理规定" + ], + [ + "Expenditure Reimbursement Application", + "Operator" + ], + [ + "业务招待费", + "差旅费" + ], + [ + "公司", + "第二十一条" + ], + [ + "公司支出管理办法(2024)", + "远光软件股份有限公司" + ], + [ + "第四条归口管理部门主要职责", + "计划财务部" + ], + [ + "会议费", + "差旅费" + ], + [ + "Company", + "Operating Department Individual" + ], + [ + "商旅系统", + "差旅费" + ], + [ + "会议费", + "公司总裁" + ], + [ + "计划财务部", + "远光软件股份有限公司" + ], + [ + "公司", + "第十九条" + ], + [ + "公司", + "第二十条" + ], + [ + "Company", + "Planning and Finance Department" + ], + [ + "公司支出管理办法", + "营销中心" + ], + [ + "Business Original Documents", + "Operator" + ], + [ + "公司支出管理办法", + "办公室(党委办公室)" + ], + [ + "Departments And Units", + "Night High-Speed Rail Provision" + ], + [ + "Centralized Management department", + "Company" + ], + [ + "组织人事部", + "调动工作" + ], + [ + "报销标准变化情况", + "远光软件股份有限公司" + ], + [ + "第一章总则", + "远光软件股份有限公司" + ] + ], + "count": 39, + "create_time": 1779012093, + "update_time": 1779012093, + "_id": "2c1cb358f08d44ceb0e4d287133206ec" + } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_relation_chunks.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_relation_chunks.json index 7ad229d..9465773 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_relation_chunks.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_relation_chunks.json @@ -1,353 +1,353 @@ -{ - "第一章总则远光软件股份有限公司": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779012088, - "update_time": 1779012088, - "_id": "第一章总则远光软件股份有限公司" - }, - "第十一条备用金借款第四章重点支出管理规定": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779012088, - "update_time": 1779012088, - "_id": "第十一条备用金借款第四章重点支出管理规定" - }, - "公司支出管理办法办公室(党委办公室)": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012088, - "update_time": 1779012088, - "_id": "公司支出管理办法办公室(党委办公室)" - }, - "计划财务部远光软件股份有限公司": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779012076, - "update_time": 1779012076, - "_id": "计划财务部远光软件股份有限公司" - }, - "第一章总则第三条管理原则": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779012076, - "update_time": 1779012076, - "_id": "第一章总则第三条管理原则" - }, - "CompanyManagement Personnel At All Levels": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012076, - "update_time": 1779012076, - "_id": "CompanyManagement Personnel At All Levels" - }, - "Centralized Management departmentCompany": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012077, - "update_time": 1779012077, - "_id": "Centralized Management departmentCompany" - }, - "CompanyPlanning and Finance Department": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012077, - "update_time": 1779012077, - "_id": "CompanyPlanning and Finance Department" - }, - "CompanyOperating Department Individual": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012078, - "update_time": 1779012078, - "_id": "CompanyOperating Department Individual" - }, - "公司支出管理办法工会委员会": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012079, - "update_time": 1779012079, - "_id": "公司支出管理办法工会委员会" - }, - "Expenditure Reimbursement ApplicationOperator": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012079, - "update_time": 1779012079, - "_id": "Expenditure Reimbursement ApplicationOperator" - }, - "公司支出管理办法营销中心": { - "chunk_ids": [ - "chunk-afc57a0e9548d1f484da6df6c182676b" - ], - "count": 1, - "create_time": 1779012079, - "update_time": 1779012079, - "_id": "公司支出管理办法营销中心" - }, - "第四条归口管理部门主要职责计划财务部": { - "chunk_ids": [ - "chunk-aa5435156b829944c173fa1d2d7a93d4" - ], - "count": 1, - "create_time": 1779012079, - "update_time": 1779012079, - "_id": "第四条归口管理部门主要职责计划财务部" - }, - "Tax Control System DetailsVAT Special Invoice": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012079, - "update_time": 1779012079, - "_id": "Tax Control System DetailsVAT Special Invoice" - }, - "Operating Department IndividualProcurement Management Regulations": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012081, - "update_time": 1779012081, - "_id": "Operating Department IndividualProcurement Management Regulations" - }, - "Business Original DocumentsOperator": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012094, - "update_time": 1779012094, - "_id": "Business Original DocumentsOperator" - }, - "Expenditure Reimbursement ApplicationTax Authority Recognized Invoice": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012094, - "update_time": 1779012094, - "_id": "Expenditure Reimbursement ApplicationTax Authority Recognized Invoice" - }, - "公司第十七条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012094, - "update_time": 1779012094, - "_id": "公司第十七条" - }, - "OperatorThree Working Days Deadline": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012083, - "update_time": 1779012083, - "_id": "OperatorThree Working Days Deadline" - }, - "Departments And UnitsNight High-Speed Rail Provision": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012084, - "update_time": 1779012084, - "_id": "Departments And UnitsNight High-Speed Rail Provision" - }, - "公司第十八条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012084, - "update_time": 1779012084, - "_id": "公司第十八条" - }, - "公司第十九条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012084, - "update_time": 1779012084, - "_id": "公司第十九条" - }, - "报销标准变化情况远光软件股份有限公司": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012084, - "update_time": 1779012084, - "_id": "报销标准变化情况远光软件股份有限公司" - }, - "取消报销规定内容报销标准变化情况": { - "chunk_ids": [ - "chunk-18d968b78afe916b419c1b5973421ebe" - ], - "count": 1, - "create_time": 1779012085, - "update_time": 1779012085, - "_id": "取消报销规定内容报销标准变化情况" - }, - "Financial ReviewOperator": { - "chunk_ids": [ - "chunk-74c01decac4a10cd40a491786743b0ee" - ], - "count": 1, - "create_time": 1779012085, - "update_time": 1779012085, - "_id": "Financial ReviewOperator" - }, - "公司支出管理办法(2024)远光软件股份有限公司": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012085, - "update_time": 1779012085, - "_id": "公司支出管理办法(2024)远光软件股份有限公司" - }, - "远光制度〔2024〕14号远光软件股份有限公司": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012086, - "update_time": 1779012086, - "_id": "远光制度〔2024〕14号远光软件股份有限公司" - }, - "Departments And UnitsTaxi Usage Regulations": { - "chunk_ids": [ - "chunk-613d6dfd4c5e9c807229a3147f96b584" - ], - "count": 1, - "create_time": 1779012099, - "update_time": 1779012099, - "_id": "Departments And UnitsTaxi Usage Regulations" - }, - "控股子公司计划财务部": { - "chunk_ids": [ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - ], - "count": 1, - "create_time": 1779012099, - "update_time": 1779012099, - "_id": "控股子公司计划财务部" - }, - "公司第二十条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012086, - "update_time": 1779012086, - "_id": "公司第二十条" - }, - "商旅系统差旅费": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012086, - "update_time": 1779012086, - "_id": "商旅系统差旅费" - }, - "业务招待费差旅费": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012089, - "update_time": 1779012089, - "_id": "业务招待费差旅费" - }, - "公司第二十一条": { - "chunk_ids": [ - "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - ], - "count": 1, - "create_time": 1779012089, - "update_time": 1779012089, - "_id": "公司第二十一条" - }, - "广告宣传费第十六条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012089, - "update_time": 1779012089, - "_id": "广告宣传费第十六条" - }, - "组织人事部调动工作": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012090, - "update_time": 1779012090, - "_id": "组织人事部调动工作" - }, - "会议费差旅费": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012092, - "update_time": 1779012092, - "_id": "会议费差旅费" - }, - "业务招待费第十四条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012092, - "update_time": 1779012092, - "_id": "业务招待费第十四条" - }, - "会议费第十五条": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012092, - "update_time": 1779012092, - "_id": "会议费第十五条" - }, - "会议费公司总裁": { - "chunk_ids": [ - "chunk-d26b288ed4001dc5c504dce0eb841362" - ], - "count": 1, - "create_time": 1779012093, - "update_time": 1779012093, - "_id": "会议费公司总裁" - } +{ + "第一章总则远光软件股份有限公司": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779012088, + "update_time": 1779012088, + "_id": "第一章总则远光软件股份有限公司" + }, + "第十一条备用金借款第四章重点支出管理规定": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779012088, + "update_time": 1779012088, + "_id": "第十一条备用金借款第四章重点支出管理规定" + }, + "公司支出管理办法办公室(党委办公室)": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012088, + "update_time": 1779012088, + "_id": "公司支出管理办法办公室(党委办公室)" + }, + "计划财务部远光软件股份有限公司": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779012076, + "update_time": 1779012076, + "_id": "计划财务部远光软件股份有限公司" + }, + "第一章总则第三条管理原则": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779012076, + "update_time": 1779012076, + "_id": "第一章总则第三条管理原则" + }, + "CompanyManagement Personnel At All Levels": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012076, + "update_time": 1779012076, + "_id": "CompanyManagement Personnel At All Levels" + }, + "Centralized Management departmentCompany": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012077, + "update_time": 1779012077, + "_id": "Centralized Management departmentCompany" + }, + "CompanyPlanning and Finance Department": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012077, + "update_time": 1779012077, + "_id": "CompanyPlanning and Finance Department" + }, + "CompanyOperating Department Individual": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012078, + "update_time": 1779012078, + "_id": "CompanyOperating Department Individual" + }, + "公司支出管理办法工会委员会": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012079, + "update_time": 1779012079, + "_id": "公司支出管理办法工会委员会" + }, + "Expenditure Reimbursement ApplicationOperator": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012079, + "update_time": 1779012079, + "_id": "Expenditure Reimbursement ApplicationOperator" + }, + "公司支出管理办法营销中心": { + "chunk_ids": [ + "chunk-afc57a0e9548d1f484da6df6c182676b" + ], + "count": 1, + "create_time": 1779012079, + "update_time": 1779012079, + "_id": "公司支出管理办法营销中心" + }, + "第四条归口管理部门主要职责计划财务部": { + "chunk_ids": [ + "chunk-aa5435156b829944c173fa1d2d7a93d4" + ], + "count": 1, + "create_time": 1779012079, + "update_time": 1779012079, + "_id": "第四条归口管理部门主要职责计划财务部" + }, + "Tax Control System DetailsVAT Special Invoice": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012079, + "update_time": 1779012079, + "_id": "Tax Control System DetailsVAT Special Invoice" + }, + "Operating Department IndividualProcurement Management Regulations": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012081, + "update_time": 1779012081, + "_id": "Operating Department IndividualProcurement Management Regulations" + }, + "Business Original DocumentsOperator": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012094, + "update_time": 1779012094, + "_id": "Business Original DocumentsOperator" + }, + "Expenditure Reimbursement ApplicationTax Authority Recognized Invoice": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012094, + "update_time": 1779012094, + "_id": "Expenditure Reimbursement ApplicationTax Authority Recognized Invoice" + }, + "公司第十七条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012094, + "update_time": 1779012094, + "_id": "公司第十七条" + }, + "OperatorThree Working Days Deadline": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012083, + "update_time": 1779012083, + "_id": "OperatorThree Working Days Deadline" + }, + "Departments And UnitsNight High-Speed Rail Provision": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012084, + "update_time": 1779012084, + "_id": "Departments And UnitsNight High-Speed Rail Provision" + }, + "公司第十八条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012084, + "update_time": 1779012084, + "_id": "公司第十八条" + }, + "公司第十九条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012084, + "update_time": 1779012084, + "_id": "公司第十九条" + }, + "报销标准变化情况远光软件股份有限公司": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012084, + "update_time": 1779012084, + "_id": "报销标准变化情况远光软件股份有限公司" + }, + "取消报销规定内容报销标准变化情况": { + "chunk_ids": [ + "chunk-18d968b78afe916b419c1b5973421ebe" + ], + "count": 1, + "create_time": 1779012085, + "update_time": 1779012085, + "_id": "取消报销规定内容报销标准变化情况" + }, + "Financial ReviewOperator": { + "chunk_ids": [ + "chunk-74c01decac4a10cd40a491786743b0ee" + ], + "count": 1, + "create_time": 1779012085, + "update_time": 1779012085, + "_id": "Financial ReviewOperator" + }, + "公司支出管理办法(2024)远光软件股份有限公司": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012085, + "update_time": 1779012085, + "_id": "公司支出管理办法(2024)远光软件股份有限公司" + }, + "远光制度〔2024〕14号远光软件股份有限公司": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012086, + "update_time": 1779012086, + "_id": "远光制度〔2024〕14号远光软件股份有限公司" + }, + "Departments And UnitsTaxi Usage Regulations": { + "chunk_ids": [ + "chunk-613d6dfd4c5e9c807229a3147f96b584" + ], + "count": 1, + "create_time": 1779012099, + "update_time": 1779012099, + "_id": "Departments And UnitsTaxi Usage Regulations" + }, + "控股子公司计划财务部": { + "chunk_ids": [ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + ], + "count": 1, + "create_time": 1779012099, + "update_time": 1779012099, + "_id": "控股子公司计划财务部" + }, + "公司第二十条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012086, + "update_time": 1779012086, + "_id": "公司第二十条" + }, + "商旅系统差旅费": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012086, + "update_time": 1779012086, + "_id": "商旅系统差旅费" + }, + "业务招待费差旅费": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012089, + "update_time": 1779012089, + "_id": "业务招待费差旅费" + }, + "公司第二十一条": { + "chunk_ids": [ + "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + ], + "count": 1, + "create_time": 1779012089, + "update_time": 1779012089, + "_id": "公司第二十一条" + }, + "广告宣传费第十六条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012089, + "update_time": 1779012089, + "_id": "广告宣传费第十六条" + }, + "组织人事部调动工作": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012090, + "update_time": 1779012090, + "_id": "组织人事部调动工作" + }, + "会议费差旅费": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012092, + "update_time": 1779012092, + "_id": "会议费差旅费" + }, + "业务招待费第十四条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012092, + "update_time": 1779012092, + "_id": "业务招待费第十四条" + }, + "会议费第十五条": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012092, + "update_time": 1779012092, + "_id": "会议费第十五条" + }, + "会议费公司总裁": { + "chunk_ids": [ + "chunk-d26b288ed4001dc5c504dce0eb841362" + ], + "count": 1, + "create_time": 1779012093, + "update_time": 1779012093, + "_id": "会议费公司总裁" + } } \ No newline at end of file diff --git a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_text_chunks.json b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_text_chunks.json index f30207a..f3bc5b1 100644 --- a/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_text_chunks.json +++ b/server/storage/knowledge/.lightrag/x_financial_knowledge/kv_store_text_chunks.json @@ -1,112 +1,112 @@ -{ - "chunk-dd87aa5bc62cc9587ecb4c26d35a5263": { - "tokens": 1200, - "content": "商密【中】\n\n 远光软件股份有限公司文件\n\n 远光制度〔2024〕14 号\n\n关于颁布《公司支出管理办法(2024)》的\n 通知\n\n公司各部门、分支机构、子公司:\n 为适应公司业务发展需要,优化、完善支出和报销标准,规\n范支出业务审批和报销过程,防范经营风险,依据国家有关法\n律法规,参照国家电网公司和国网数科公司有关管理规定,结\n合市场经营环境和公司实际情况,在广泛征求意见的基础上,\n公司对《公司支出管理办法》进行了修订,现予颁布。本办法自\n颁布之日起施行,原办法同时废止。\n 特此通知。\n\n 附件:1.公司支出管理办法(2024)\n 2.修订说明\n\n 远光软件股份有限公司\n 2024 年 4 月 17 日\n\n远光软件股份有限公司 2024 年 4 月 17 日印发\n\n 第 1 页 共 20 页\n 商密【中】\n附件 1\n\n 公司支出管理办法(2024)\n 目录\n 第一章 总则.............................................................. 4\n\n 第一条 目的............................................................. 4\n\n 第二条 范围............................................................. 4\n\n 第三条 管理原则......................................................... 4\n\n 第二章 职责分工 .......................................................... 4\n\n 第四条 归口管理部门主要职责 ............................................. 4\n\n 第五条 计划财务部主要职责............................................... 5\n\n 第六条 经办部门(个人)主要职责 ......................................... 5\n\n 第七条 各级管理人员主要职责 ............................................. 5\n\n 第三章 支出报销申请与审批 ................................................ 6\n\n 第八条 支出报销申请..................................................... 6\n\n 第九条 支出报销审批..................................................... 7\n\n 第十条 支出成本中心归属................................................. 7\n\n 第四章 重点支出管理规定 .................................................. 7\n\n 第十一条 备用金借款....................................................... 7\n\n 第十二条 市内交通费....................................................... 8\n\n 第十三条 差旅费........................................................... 8\n\n 第十四条 业务招待费...................................................... 11\n\n 第十五条 会议费.......................................................... 11\n\n 第十六条 广告宣传费...................................................... 11\n\n 第十七条 培训费.......................................................... 12\n\n 第十八条 通信费.......................................................... 12\n\n 第 2 页 共 20 页\n 商密【中】\n 第十九条 邮递费.......................................................... 12\n\n 第二十条 薪酬福利支出.................................................... 12\n\n 第二十一条 对外捐赠支出 ................................................. 13\n\n 第二十二条 涉外业务汇率标准 ............................................. 13\n\n第五章 附则............................................................. 13\n\n 第二十三条 本办法的归口与实施 ........................................... 13\n\n 第二十四条 附件 ......................................................... 13\n\n 附表 1:员工支出报销审批权限表 ............................................ 14\n\n 附表 2:岗位支出报销审批权限表 ............................................ 15\n\n 附表 3:支出归口管理部门与归口业务范围 .................................... 18\n\n 第 3 页 共 20 页\n 商密【中】\n 第一章 总则\n\n第一条 目的\n\n 为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善\n\n支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n\n法规,参照国家电网公司和国网数科公司有关管理规定,结合市场经营环境和公司实\n\n际情况,在广泛征求意见的基础上,制定本办法。\n\n第二条 范围\n\n 本办法适用于公司各类成本费用和资本性支出。\n\n 本办法适用于全公司范围,包括:公司各部门、分支机构(非独立法人)、全资\n\n子公司。控股子公司应参照本办法制订支出管理办法,按子公司相关议事规则审批、\n\n报公司计划财务部备案后执行。\n\n第三条 管理原则\n\n 公司支出管理遵循“预算先行、厉行节约、效益优先,分级授权、分类控制、批\n\n办分离”的基本原则。\n\n 1 公司支出管理遵循预算先行的控制原则,应在预算目标范围内支出,并遵循\n\n 公司内控、预算、考核管理相关规定,预算外支出履行预算审批程序后执行。\n\n 2 各部门(分支机构)、各岗位应在部门(岗位)职责与授权业务范围内,秉\n\n 持厉行节约、效益优先原则,规范开展支出业务活动。\n\n 3 各级管理人员应根据其管理岗位,在公司支出授权审批范围和本人职责范\n\n 围内,履行支出审批职权,承担审批责任。岗位审批权限与职级待遇分离,\n\n 审", - "chunk_order_index": 0, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" - }, - "chunk-74c01decac4a10cd40a491786743b0ee": { - "tokens": 1200, - "content": "(分支机构)、各岗位应在部门(岗位)职责与授权业务范围内,秉\n\n 持厉行节约、效益优先原则,规范开展支出业务活动。\n\n 3 各级管理人员应根据其管理岗位,在公司支出授权审批范围和本人职责范\n\n 围内,履行支出审批职权,承担审批责任。岗位审批权限与职级待遇分离,\n\n 审批权限按管理岗位执行。\n\n 4 公司支出管理遵循批办分离的牵制原则,支出经办人和审批人不得为同一\n\n 人。\n\n 第二章 职责分工\n\n第四条 归口管理部门主要职责\n\n 公司实行支出归口管理,归口管理部门主要职责如下:\n\n 1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n\n 确定各项支出业务的开支范围、标准、方式和管理流程。\n\n 第 4 页 共 20 页\n 商密【中】\n 2 根据公司有关规定,对支出业务的合规性、预算(计划)执行控制情况等进\n\n 行检查、分析与监督。\n\n 支出归口管理部门与范围按“附表3”执行,部门职责变化时,相应调整归口分\n\n工。\n\n第五条 计划财务部主要职责\n\n 1 明确支出报销审批流程、审核要点和报销资料规范。\n\n 2 协助归口管理部门确定各项支出业务的开支范围和标准。\n\n 3 负责报销业务财务审核,对业务原始凭据的完整性、合规性进行审查,对报\n\n 销事项与业务原始凭据的业务关联性、内容一致性、金额准确性等进行复\n\n 核,可要求报销人提供不限于本办法规定的佐证资料。\n\n 4 办理报销业务的结算支付。\n\n 5 组织开展支出报销业务日常财务稽核与宣贯。\n\n第六条 经办部门(个人)主要职责\n\n 1 在部门(岗位)职责与授权业务范围内,秉持预算先行、厉行节约、效益优\n\n 先原则,规范开展支出业务活动。\n\n 2 业务经办过程中,应严格遵循开支范围和标准,切实履行业务流程与审批程\n\n 序,并遵循“发票、资金、物资”三流一致原则,及时取得真实、合规、关\n\n 联、完整的业务原始凭据,验证发票真伪。\n\n 3 各项物资(原材料、固定资产、无形资产、低值易耗品、办公用品)、服务、\n\n 外包分包业务的采购,应以经审核的需求计划(项目采购预算)为前提,并\n\n 严格遵循公司招标、采购与物资管理相关规定。\n\n 4 及时提交报销申请,履行报销审批流程,配合提供发票,以及不限于本办法\n\n 规定的业务佐证资料。\n\n 5 对支出业务的真实性、合规性、必要性、合理性以及业务原始凭据的真实性、\n\n 合规性、关联性、完整性,承担全部责任。\n\n第七条 各级管理人员主要职责\n\n 各级管理人员应在授权审批范围和职责范围内,履行支出报销审批职权,承担审\n\n批责任。\n\n 第 5 页 共 20 页\n 商密【中】\n 1 第一审批人:应对支出业务的真实性、合规性、必要性、合理性进行全面审\n\n 核,并承担审批责任。\n\n 2 后续审批人:应对支出业务的必要性、合理性进行审核,并承担审批责任。\n\n 第三章 支出报销申请与审批\n\n第八条 支出报销申请\n\n 1 申请方式\n\n 支出报销申请通过公司财务信息化系统实现(以下简称“系统单据”),财务原\n\n则上不接受“纸质申请单据”。\n\n (1)经办人应及时填写“系统单据”,并同步提交业务原始凭据。\n\n (2)除“员工薪酬、个人劳务报酬、出差补贴、专项补贴、往来款项支付”等支\n\n 出业务外,其他支出均需提交税务机关认可的票据。\n\n ① “工会经费、员工福利、职工活动、业务招待、车票、政府规费”以外的\n\n 支出,原则上均应取得增值税专用发票;应取得但未取得增值税专用发\n\n 票的,经办人应在“系统单据”中说明原因。\n\n ② 汇总开具的增值税发票,应附税控系统明细清单。\n\n (3)财务审核发现业务原始凭据不完整的,经办人应在三个工作日内补充并重新\n\n 提交至财务;经办人填单错误、业务原始凭据不合规,以及不能按时补充业\n\n 务原始凭据的,财务退单处理。", - "chunk_order_index": 1, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-74c01decac4a10cd40a491786743b0ee" - }, - "chunk-061324cc36078214691a6fc1cd0aaeea": { - "tokens": 1200, - "content": "说明原因。\n\n ② 汇总开具的增值税发票,应附税控系统明细清单。\n\n (3)财务审核发现业务原始凭据不完整的,经办人应在三个工作日内补充并重新\n\n 提交至财务;经办人填单错误、业务原始凭据不合规,以及不能按时补充业\n\n 务原始凭据的,财务退单处理。\n\n 2 申请时限\n\n 支出报销申请时限指“业务完成日”至“附件影像资料挂接系统单据日”的期间。\n\n (1)公司各类支出报销结算申请时限为三个月。\n\n ① 逾期需说明原因,经分管领导审批后方可报销。\n\n ② 按自然月度计算并定期发生的费用支出,需月度结束后方可报销。\n\n ③ 差旅费原则上需在行程结束三个月内提交报销申请(连续出差超过一个\n\n 月时,原则上应按月报销),逾期不予报销出差补贴。\n\n (2)预付款项,原则上应在次月底前完成结算,不得长期挂账。\n\n 3 结算方式\n\n (1)员工支出业务:原则上采用“公对私”结算方式,报销申请审批通过后,公\n\n 第 6 页 共 20 页\n 商密【中】\n 司支付给经办人。\n\n (2)岗位支出业务:原则上采用“公对公”结算方式,报销申请审批通过后,公\n\n 司与供应商直接结算。结算起点(1000 元)以下、且确实无法与供应商直接\n\n 结算的小额支出,报销时应附向供应商付款凭据的截图佐证。\n\n第九条 支出报销审批\n\n 1 审批权限\n\n (1)预算内支出,按“附表 1、附表 2”执行。\n\n (2)预算外支出,经办部门提交预算调整申请,经公司总裁批准后,再按“附表\n\n 1、附表 2”执行。\n\n 2 审批时限\n\n 各级管理人员原则上应在待批单据流转至本岗位后三个工作日内完成审批。\n\n 3 财务审核时限\n\n (1)影像扫描:“系统单据和纸质原始凭据”流转至影像岗后,原则上应在一个\n\n 工作日内处理完毕。\n\n (2)审核与支付:已完成审批的系统单据,原则上应在三个工作日内处理完毕。\n\n第十条 支出成本中心归属\n\n 支出成本中心归属原则上基于责任原则与受益原则确定,特殊情况由相关业务\n\n部门协商确定。\n\n 第四章 重点支出管理规定\n\n第十一条 备用金借款\n\n 备用金是公司借支给正式员工,用于支付与公司经济业务相关的、必须预支且尚\n\n不具备报销条件的费用支出款项。\n\n 1 备用金借款必须是真实合法的经济业务,应遵循“前款不清、后款不借”的\n 原则,严禁以各种名义挪用公司资金。\n 2 非正式员工不得申请备用金借款。\n 3 备用金借款按季定期清理。季度不能及时报账核销的,借款人应向其分管领\n 导申请延期审批,但不得跨年。\n 4 员工备用金借款额度原则上不得超过一万元。\n\n 第 7 页 共 20 页\n 商密【中】\n第十二条 市内交通费\n 市内交通费是指员工为公司生产经营活动,在工作所在地发生的市内交通费用,\n包括工作时间内外出办理公事、夜间工作或非工作日发生的市内交通费用,不包括员\n工正常上下班所发生的交通费用、从居住地公出或公出结束返回居住地发生的市内\n交通费。\n 1 市内交通费凭据报销,应与工作实际相符,严禁报销与工作无关的交通费,\n 不接受充值预付费方式的交通费。\n 2 基于厉行节约原则,鼓励员工选择公交、地铁等公共交通出行。出租车仅限\n “紧急公务、接送客户、夜间工作至 22:00 后、以及其他确有需要的特别情\n 形”等事项的市内交通使用,由部门负责人从严管理。\n第十三条 差旅费\n 差旅费是指员工因公出差所发生的交通费、住宿费、出差补贴等。\n 各部门(单位)应严格执行出差审批程序,从严控制出差人数和出差天数,严禁\n无实质内容、无明确公务目的的差旅活动。部门副职及以下员工出差需通过公司商旅\n系统事前审批。\n 1 交通费\n 表1 交通工具等级标准\n 国内(含港澳台)、国外", - "chunk_order_index": 2, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-061324cc36078214691a6fc1cd0aaeea" - }, - "chunk-613d6dfd4c5e9c807229a3147f96b584": { - "tokens": 1200, - "content": "费、住宿费、出差补贴等。\n 各部门(单位)应严格执行出差审批程序,从严控制出差人数和出差天数,严禁\n无实质内容、无明确公务目的的差旅活动。部门副职及以下员工出差需通过公司商旅\n系统事前审批。\n 1 交通费\n 表1 交通工具等级标准\n 国内(含港澳台)、国外\n 其他交通工具\n 员工职级\n 飞机 火车 轮船 (不含小汽\n 车)\n公司领导、高层经理、\n 中层经理 火车硬席(硬\n 经济舱 卧、硬座)、\n(P5 及以上、外聘专\n 家) 高铁/动车二\n 三等舱 凭据报销\n 等座,全列软\n基层经理、其他人员 经济舱 席列车二等软\n (P4 及以下) (注 3) 座\n\n 第 8 页 共 20 页\n 商密【中】\n 国内(含港澳台)、国外\n 其他交通工具\n 员工职级\n 飞机 火车 轮船 (不含小汽\n 车)\n注 1:交通工具选乘应遵循“性价比优先”原则。\n注 2:基层经理及以下人员(P4 及以下)乘坐飞机需事前报经部门负责人审批。基层经\n理(P4)应选乘 6 折及以下经济舱、其他人员(P1-P3)应选乘 5 折及以下经济舱。\n注 3:夜间乘坐高铁/动车 6 小时以上时,可选乘卧铺。\n注 4:出差人员应当严格执行交通工具等级标准,确因紧急公务、特别情形等事项导致\n交通工具超过规定标准时,超标 20%以内时由部门负责人审批,超标 20%以上时需分管\n领导审批。\n注 5:公司已为员工购买了商业保险(含交通险),出差期间发生的保险费不再报销。\n注 6:出租车仅限“紧急公务、接送客户、夜间工作至 22:00 后、以及其他确有需要的\n特别情形”等事项的市内交通使用,由部门负责人从严管理。往返机场、车站、港口,\n原则上应选择“公交车、轨道交通”。\n注 7:自驾车出差发生的路桥费、停车费、油费、电费等据实报销,由部门负责人从严\n管理。自驾发生事故或违章罚款等造成的损失,责任自负。\n 2 住宿费\n (1)酒店住宿\n\n 表2 酒店住宿限额标准\n\n 单位:人民币元\n 国内\n 员工职级 直辖市/特区/ 国外\n 省会城市 其他地区\n 港澳台\n 公司领导\n 500 450 400 800\n (P8及以上)\n 高层经理\n 450 400 350 700\n (P7)\n 中层经理、基层经理\n 400 350 300 600\n(P4~P6、外聘专家)\n\n 其他员工 350 300 250 500\n注1:出差人员应严格执行住宿限额标准,确因紧急公务、特别情形等事项导致住宿超\n过规定标准时,超标20%以内时由部门负责人审批,超标20%以上时需分管领导审批。\n注2:外出参加会议、培训,统一安排食宿的,会议期间的住宿费按外部会议组织方通\n知标准凭据报销。不统一安排食宿的,按照上表标准执行。\n\n (2)异地工作用房\n\n ① 因公长期出差人员申请异地工作用房的,按照《公司物业租赁管理办法》\n\n 第 9 页 共 20 页\n 商密【中】\n 执行。\n\n ② 异地工作用房产生的租金、房屋初始配置费、物业管理费、取暖费、水电\n\n 燃气支出(含公摊)、网络宽带费等费用,按照《公司物业租赁管理办法》确\n\n 定标准进行报销。\n\n 3 出差补贴\n\n 出差补贴按出差自然天数(日历天数)进行报销,具体标准如下:\n\n 表3 出差补贴标准\n\n 单位:人民币元/天\n\n 国内\n补助类型 项目 国外\n 港澳台 直辖市/特区/西藏 其他地区\n\n 餐补 自行解决餐食 75 65 55 140\n全额\n补助 基本\n 基本出差补贴 35 35 35 35\n 补助\n 合计 110 100 90 175\n注:因组织安排、销售、推广、调", - "chunk_order_index": 3, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-613d6dfd4c5e9c807229a3147f96b584" - }, - "chunk-d26b288ed4001dc5c504dce0eb841362": { - "tokens": 1200, - "content": "直辖市/特区/西藏 其他地区\n\n 餐补 自行解决餐食 75 65 55 140\n全额\n补助 基本\n 基本出差补贴 35 35 35 35\n 补助\n 合计 110 100 90 175\n注:因组织安排、销售、推广、调研、培训、会议、研讨、借调等出差,主办方统一安排\n餐食的,不再报销餐补。\n 4 差旅费其他注意事项\n\n (1)因公发生的订票、签转、退票等费用凭据报销,由部门负责人审核确认。\n\n (2)出差记录链条中断时,应提供业务佐证材料:\n\n ① 登机牌、高速道路通行记录、其他道路通行记录、租车记录等。\n\n ② 支付记录。\n\n ③ 出差审批邮件、短信、微信等。\n\n (3)出差或调动工作期间,经部门负责人批准就近回家省亲办事的,其绕道交通\n\n 费,扣除出差直线单程交通费,多开支的部分由个人自理。绕道和在家期间\n\n 不得报销住宿费、出差补贴。\n\n (4)出差交通费原始凭据丢失的,提供情况说明和订单详情、支付截图等佐证材\n\n 料,可按票面价值的 75%报销,未提供的,不予报销。通过商旅订票未取得\n\n 火车票的,从差旅补贴或其他差旅杂费中扣减。\n\n (5)探亲路费应严格遵循《公司员工探亲管理办法》相关规定,不得以因公差旅\n\n 第 10 页 共 20 页\n 商密【中】\n 方式报销,不享受出差补贴。探亲路费原始凭据丢失的,不允许报销。\n\n (6)员工工作地调动时,所发生的行李、家具等邮寄费,在每人每公里 1 元以内\n\n 凭据报销,超过部分自理。\n\n (7)经组织安排到异地挂职锻炼、培养锻炼、人才帮扶、借调支援等工作人员,\n\n 在途期间的交通费、餐补和基本补助按出差规定执行;在异地单位工作期间,\n\n 不适用出差补贴标准规定,按照组织人事部确定的挂职人员报销标准执行;\n\n 所在单位不安排住宿的,住宿标准按公司酒店住宿限额标准执行。\n\n (8)境外出差(港澳台与国外出差)应单列预算、事前履行经费申请与审批程序。\n\n (9)以上涉及事前审批的事项,以公司商旅系统审批截图或审批邮件为报销必备\n\n 附件。\n\n(10)因公出差原则上应使用商旅系统统一预定,特殊情况未通过商旅系统下单\n\n的,应邮件知会商旅客服并抄送部门负责人。\n\n第十四条 业务招待费\n\n 业务招待费是指各级单位正常生产经营管理过程中需要发生的、用于必要招待\n\n的各项费用,包括用于接待客户和相关单位的餐饮等合理支出。未能“公对公”结算\n\n的,应附“向供应商付款凭据”佐证。\n\n第十五条 会议费\n\n 会议费是指公司主办或承办会议发生的会场租金、文件资料、设备租金等支出,\n\n以及参加外部会议所发生的会务相关支出。\n\n 1 公司主办或承办的会议\n\n (1)会议费中不得列支与会议无关的旅游观光、宴请、礼品馈赠等支出。\n\n (2)经费预算 30,000 元及以上(包括不在会议费列支,但与会议直接相关的培训\n\n 费、差旅费等全部支出)的公司内部会议、研讨与集中培训,需事前报请公\n\n 司总裁审批。\n\n (3)报销时应附:经审批的会议申请与预算、会议通知、参会人员签到表、会议\n\n 开支明细等业务佐证材料,会务费发票应附服务方费用明细清单。\n\n 2 参加外部会议的,报销时应附会议通知、参会回执等业务佐证材料。\n\n第十六条 广告宣传费\n\n 第 11 页 共 20 页\n 商密【中】\n 广告宣传费是指展示企业形象、宣传企业文化及营销活动中推广企业产品、业务\n\n所发生的支出,包括广告费和业务宣传费。\n\n 1 广告费\n\n 广告费指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业\n\n务佐证材料。\n\n 2 业务宣传费\n\n 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体\n\n传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。业务宣传", - "chunk_order_index": 4, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-d26b288ed4001dc5c504dce0eb841362" - }, - "chunk-e9438f69c9e221d9f0f00a05ad84eac6": { - "tokens": 1200, - "content": "费。\n\n 1 广告费\n\n 广告费指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业\n\n务佐证材料。\n\n 2 业务宣传费\n\n 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体\n\n传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。业务宣传费报销时\n\n应附活动方案、费用预算等业务佐证材料,并需遵循公司采购与物资管理相关规定。\n\n第十七条 培训费\n\n 培训费是指公司安排、主办或承办培训发生的学费、书籍杂费、师资费、资料费、\n\n场地费、设备材料费、住宿费、交通费等,以及员工根据工作需要,经归口管理部门\n\n批准参加外部培训、考证、教育产生的相关费用。\n\n 1 报销资格按《公司员工教育培训管理办法》认定标准执行。\n\n 2 经归口管理部门认定符合办法标准的,培训期间的主要交通费(往返)、住\n\n 宿费按出差规定标准执行,其他费用自理。\n\n第十八条 通信费\n\n 通信费是指为满足公司日常办公需要发生的电话、传真、集团网、网络连接等支\n\n出,其中员工通讯费按《公司员工因公通讯费用实施细则》执行。\n\n第十九条 邮递费\n\n 邮递费是指公司因业务需要邮递物品而支付的费用,包括邮件费、托运费、快递\n\n费等,员工报销的应附快递底单,单位统一结算的应附寄件明细清单与支出分摊表。\n\n第二十条 薪酬福利支出\n\n 1 公司相关制度规定的职工薪资、奖励提成、福利费支出,按公司相关制度规\n\n 定执行。\n\n 2 临时的奖励及福利支出,需事先计划并报公司总裁审批,具体支出时由分管\n\n 领导根据已批准的计划审批。\n\n 3 职工福利费由组织人事部实行年度计划管理,对未纳入福利计划的福利项\n\n 第 12 页 共 20 页\n 商密【中】\n 目,不得列支和报销。\n\n 4 薪酬福利支出事前审批程序以组织人事部规定标准执行。\n\n 5 职工活动支出,按照《公司团建管理办法》或《工会经费管理办法》执行。\n\n第二十一条 对外捐赠支出\n\n 公司对外捐赠支出由品牌及市场运营中心归口管理,并严格预算单控,未纳入对\n\n外捐赠预算的,不得对外捐赠。\n\n 1 预算内对外捐赠事项发生时,实施捐赠的业务部门应提出捐赠申请,详细说\n\n 明捐赠事由、捐赠对象、捐赠金额等内容,经业务部门负责人审批,品牌及\n\n 市场运营中心归口审核,业务部门分管领导审批后执行。\n\n 2 未纳入预算的对外捐赠事项,实施捐赠的业务部门应提出捐赠申请,详细说\n\n 明捐赠必要性,履行对外捐赠的预算调整决策程序,并纳入预算范围后方可\n\n 实施。\n\n 3 捐赠事项完成后,实施捐赠的业务部门应将捐赠活动的凭据资料报品牌及\n\n 市场运营中心备案,凭据资料包括但不限于发票、收据、捐赠协议、接收函、\n\n 捐赠清单等。\n\n第二十二条 涉外业务汇率标准\n\n 以人民币结算的,凭据报销;以外币结算的,按支付凭据所载汇率折算,支付凭\n\n据未载明汇率的按业务发生月第一个交易日“中国银行外汇折算价”执行,中国银行\n\n无折算价的币种按“中国外汇交易中心参考汇率”执行。\n\n 第五章 附则\n\n第二十三条 本办法的归口与实施\n\n 1 本办法自颁布之日起施行,原办法同时废止。\n\n 2 本办法由公司计划财务部负责制定、修订、解释及实施协调工作。\n\n第二十四条 附件\n\n 附表1:员工支出报销审批权限表\n\n 附表2:岗位支出报销审批权限表\n\n 附表3:支出归口管理部门与归口业务范围\n\n 第 13 页 共 20 页\n 商密【中】\n 附表 1:员工支出报销审批权限表\n 单位:人民币万元\n 审批权限\n 部门经理/\n 支出项目 总监/ 副总裁/ 高级副总裁/ 补充说明\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n 业务招待 0", - "chunk_order_index": 5, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-e9438f69c9e221d9f0f00a05ad84eac6" - }, - "chunk-9841d66d8fb8548aab40220663a51693": { - "tokens": 1200, - "content": "附表 1:员工支出报销审批权限表\n 单位:人民币万元\n 审批权限\n 部门经理/\n 支出项目 总监/ 副总裁/ 高级副总裁/ 补充说明\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n 业务招待 0.5 1 2 3 15\n\n 因公借款 0.5 1 2 3 15\n员工支出\n\n 1.差旅费、市内交通。\n 其他支出 1 2 3 5 50 2.客服及商务、教育、邮递。\n 3.职工活动等其他临时性支出。\n\n 第 14 页 共 20 页\n 商密【中】\n 附表 2:岗位支出报销审批权限表\n 单位:人民币万元\n\n 审批权限\n\n 支出项目 部门经理/ 总监/ 补充说明\n 高级\n 机构总经理/ 一级部门 副总裁 总裁\n 副总裁\n 事业部总经理 总经理\n\n 资产采购 1 2 10 15 200 包括固定资产、无形资产、低值易耗品。\n资本性 基建工程 —— —— 5 10 50\n支出\n 股权投资、\n —— —— —— —— —— 由董事长审批。\n 兼并收购\n 1.生产采购:产品生产用原材料、辅助\n 材料、机物料等。\n 材料采购 —— 10 20 30 500\n 2.项目采购:项目外采的设备、软件、公\n 有云资源等。\n 分包外包\n —— 10 20 30 500\n (内部单位) 1.研发、实施、运维、服务等分包外包。\n 分包外包 2.委托加工、项目土建装修等。\n收益性 —— 10 20 30 200\n (外部单位)\n支出\n 1.投标相关保证金:投标保证金、质保\n 保证金 5 50 100 200 500 金、履约保证金。\n 2.招标相关保证金。\n 1.销售退款。\n 销售退款 5 10 30 50 200\n 2.代付款(代收后)。\n 不含水电及杂费,需经业务归口部门审\n 房屋租金 5 10 20 30 200\n 批。\n\n 第 15 页 共 20 页\n 商密【中】\n 公司统一结算或批量采购的:\n 1.食堂采购、通勤车。\n 统结支出\n 2.交通费、住宿费。\n 3.业务招待费、通讯费等。\n 代付子公司\n 代付子公司投标支出。\n 经营支出\n 1.广告费。\n 市场营销 2.业务宣传费:业务宣传、企业文化宣\n 传、市场活动、营销活动。\n\n 专项服务、 1 3 5 10 50 外部单位与个人提供的咨询、培训、劳务\n 外聘劳务 等服务(非专家意见、非鉴证报告)。\n\n 国家机关、事业单位、代行政府职能的\n 政府规费\n 社会团体收费。\n 慰问救济、对\n —— —— 1 2 10 公司制度外的慰问救济捐赠。\n 外捐赠\n 税费支出 50 100 200 300 500 增值税及各类附加费、所得税等。\n\n 其他支出 1 2 3 5 50 设备租赁、公务车支出等\n\n 资金转户/调拨 50 —— —— 2000 3000\n\n财务专用 银行手续费\n 5 10 30 50 200\n 错付退款 不含销售退款。\n\n 其他支付 —— —— —— 15 50 出纳提现等。\n\n 第 16 页 共 20 页\n 商密【中】\n注 1:上述权限额度均含本数,各级管理人员权限指各自职责范围内审批权限。\n注 2:上述权限中业务流转执行以组织关系为基准的逐级审批规则。特殊事项经决策后,可越级至终审岗审批。\n注 3:仅结算业务,由业务部门审批确定是否达到财务入账条件,并按照上述对应支出项目权限审批。\n注 4:代付", - "chunk_order_index": 6, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-9841d66d8fb8548aab40220663a51693" - }, - "chunk-afc57a0e9548d1f484da6df6c182676b": { - "tokens": 1200, - "content": "】\n注 1:上述权限额度均含本数,各级管理人员权限指各自职责范围内审批权限。\n注 2:上述权限中业务流转执行以组织关系为基准的逐级审批规则。特殊事项经决策后,可越级至终审岗审批。\n注 3:仅结算业务,由业务部门审批确定是否达到财务入账条件,并按照上述对应支出项目权限审批。\n注 4:代付子公司经营支出原则上与子公司按季度汇总结算。\n注 5:薪酬福利支出分配计划审批按照人事归口管理部门规定执行。\n注 6:上述权限不含协管、高级顾问、顾问及主持工作人员,公司如有其相关文件通知,从其规定授权。\n注 7:全资子公司总经理由母公司未明确层级管理人员担任的,审批权限按“总监/一级部门总经理”执行。\n注 8:全资子公司员工薪资、奖金提成、福利支出、临时的奖励及福利支出审批按照人事归口管理部门规定执行。\n注 9:全资子公司其他部门经理,按照公司 1 号文对应岗位授权,由平级人员分角色担任的,按最高岗位授权,逐级审批。\n\n 第 17 页 共 20 页\n 商密【中】\n 附表 3:支出归口管理部门与归口业务范围\n 归口管理部门 归口业务范围\n 1.党建支出。\n办公室(党委办公室)\n 2.公务车支出。\n 工会委员会 工会支出。\n 1.投标业务支出:标书费、投标保证金、中标服务费(或保险)等。\n 营销中心 2.机构营销业务支出:客服及商务支出、营销活动支出。\n 3.客户培训、销售退款等支出。\n 1.广告费支出。\n品牌及市场运营中心 2.业务宣传支出:业务宣传、文化宣传、市场活动。\n 3.对外捐赠支出。\n 1.薪酬福利(不含食堂、误餐)、外聘劳务、员工保险支出。\n 组织人事部 2.探亲差旅、条件艰苦及安全风险较高区域补助等支出。\n 3.其他福利支出。\n 1.招聘业务。\n 人力资源服务部\n 2.员工教育培训支出。\n 1.员工备用金、差旅费、市内交通费、出国经费、误餐费支出。\n 计划财务部 2.税金及附加、审计评估、财务费用支出。\n 3.客服及商务支出。\n 产业投资部 股权投资支出、并购业务支出。\n证券与法律事务部 法律事务类支出、上市信披类支出、商标注册类支出。\n 产品规划设计部 知识产权类支出、信息技术咨询类支出、研发活动类支出。\n DAP 研发中心 研究开发过程中支付给外单位的检测、评测、测试及化验等支出。\n 1.IT 类资产的购置(建造)、租入、运维、修理等支出。\n 信息管理部\n 2.网络使用费支出。\n 1.非 IT 类资产的购置、租入、运维、修理、装修等支出,财产保险支出。\n 2.食堂支出。\n 后勤服务部\n 3.办公费用支出。\n 4.商旅统付结算。\n\n 第 18 页 共 20 页\n 商密【中】\n附件 2\n\n 修订说明\n\n 《公司支出管理办法》修订涉及修改报销标准 2 项,取消报销规定 1 项,新增\n\n报销规定 2 项,审批权限变化 2 项,具体情况如下。\n\n 一、报销标准变化情况\n\n 只调整了差旅费、异地调动邮寄费两项内容。\n\n 1.差旅费。只调整了公司领导出差交通工具标准和出差补贴。\n\n 出差交通工具标准:公司领导(P8)乘坐火车标准(由“不限”标准调低为\n\n“二等座”)、轮船标准(由“二等舱”调低为“三等舱”)。\n\n 出差补贴:原制度描述出差补助为包干制,实际执行时在两项补助之外还存在\n\n报销打车费情况,故取消了原制度中“包干”表述。\n\n 2.异地调动邮寄费。将原制度“可凭据报销一次个人物品邮寄费用,金额超过\n\n1000 元的需事前审批”,调整为“1 元/人/公里内报销,超标自理”。\n\n 二、取消报销规定内容\n\n 因公用车补贴已纳入工资内发放,新制度中取消该费用相关表述。\n\n 三、新增规定内容\n\n 1.异地挂职锻炼补贴标准。经组织安排到异地挂职锻炼、培养锻炼、", - "chunk_order_index": 7, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-afc57a0e9548d1f484da6df6c182676b" - }, - "chunk-18d968b78afe916b419c1b5973421ebe": { - "tokens": 1200, - "content": "1000 元的需事前审批”,调整为“1 元/人/公里内报销,超标自理”。\n\n 二、取消报销规定内容\n\n 因公用车补贴已纳入工资内发放,新制度中取消该费用相关表述。\n\n 三、新增规定内容\n\n 1.异地挂职锻炼补贴标准。经组织安排到异地挂职锻炼、培养锻炼、人才帮\n\n扶、借调支援等工作人员,在途期间的交通费、餐补和基本补助按出差规定执行;\n\n在异地单位工作期间,不适用出差补贴标准规定,按照组织人事部确定的挂职人员\n\n报销标准执行;所在单位不安排住宿的,住宿标准按公司酒店住宿限额标准执行。\n\n 2.对使用商旅订票进行了规范。因公出差原则上应使用商旅系统统一预定。\n\n 四、审批权限变化情况\n\n 只调整了“投标保证金”“审批流转程序”两项内容:\n\n 1.调整投标保证金审批权限。因对投标缴纳保证金的时效性要求较高,为提高\n\n审批效率,调整投标保证金审批权限,具体如下(单位:万元):\n\n 第 19 页 共 20 页\n 商密【中】\n 审批权限\n\n 部门经理/\n支出项目 总监/ 副总裁/ 高级副总裁/\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n保证金 5 50 100 200 500\n\n 2.明确支出审批流转程序。业务流转执行以组织关系为基准的逐级审批规则。\n\n 特殊事项经决策后,可越级至终审岗审批。\n\n 第 20 页 共 20 页\n 商密【中】\n\n# 章节导航\n\n以下内容由入库阶段从制度原文中提取,供检索时优先理解制度层级、条目和标准所在章节。\n\n- 第一章 总则.............................................................. 4\n- 第二章 职责分工 .......................................................... 4\n- 第三章 支出报销申请与审批 ................................................ 6\n- 第四章 重点支出管理规定 .................................................. 7\n- 第五章 附则............................................................. 13\n- 第一章 总则\n- 第二章 职责分工\n- 第三章 支出报销申请与审批\n- 第四章 重点支出管理规定\n- 第五章 附则\n- 一、报销标准变化情况\n- 二、取消报销规定内容\n\n# 重点章节摘录\n\n## 第一章 总则.............................................................. 4\n\n第一条 目的............................................................. 4;第二条 范围............................................................. 4;第三条 管理原则......................................................... 4\n\n## 第二章 职责分工 .......................................................... 4\n\n第四条 归口管理部门主要职责 ............................................. 4;第五条 计划财务部主要职责............................................... 5;第六条 经办部门(个人)主要职责 ......................................... 5\n\n## 第三章 支出报销申请与审批 ................................................ 6\n\n第八条 支出报销申请..................................................... 6;第九条 支出报销审批..................................................... 7;第十条 支出成本中心归属................................................. 7\n\n## 第四章 重点支出管理规定 .................................................. 7\n\n第十一条 备用金借款....................................................... 7;第十二条 市内交通费....................................................... 8;第十三条 差旅费........................................................... 8\n\n## 第五章 附则............................................................. 13\n\n第二十三条 本办法的归口与实施 ........................................... 13;第二十四条 附件 ......................................................... 13;附表 1:员工支出报销审批权限表 ............................................ 14\n\n## 第一章 总则\n\n第一条 目的;为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善;支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n\n## 第二章 职责分工\n\n第四条 归口管理部门主要职责;公司实行支出归口管理,归口管理部门主要职责如下:;1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n\n## 第三章 支出报销申请与审批\n\n第八条 支出报销申请;1 申请方式;支出报销申请通过公司财务信息化系统实现(以下简称“系统单据”),财务原\n\n# 问答线索补充\n\n以下内容由入库阶段根据章节标题、条款、列表、键值对与相邻正文提炼,供问答检索时优先命中更短、更直接的制度依据。\n\n- 第一章 总则.............................................................. 4:第一条 目的............................................................. 4\n- 第一章 总则.............................................................. 4:第二条 范围............................................................. 4\n- 第一章 总则................................", - "chunk_order_index": 8, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-18d968b78afe916b419c1b5973421ebe" - }, - "chunk-aa5435156b829944c173fa1d2d7a93d4": { - "tokens": 1020, - "content": "问答线索补充\n\n以下内容由入库阶段根据章节标题、条款、列表、键值对与相邻正文提炼,供问答检索时优先命中更短、更直接的制度依据。\n\n- 第一章 总则.............................................................. 4:第一条 目的............................................................. 4\n- 第一章 总则.............................................................. 4:第二条 范围............................................................. 4\n- 第一章 总则.............................................................. 4:第三条 管理原则......................................................... 4\n- 第二章 职责分工 .......................................................... 4:第四条 归口管理部门主要职责 ............................................. 4\n- 第二章 职责分工 .......................................................... 4:第五条 计划财务部主要职责............................................... 5\n- 第二章 职责分工 .......................................................... 4:第六条 经办部门(个人)主要职责 ......................................... 5\n- 第二章 职责分工 .......................................................... 4:第七条 各级管理人员主要职责 ............................................. 5\n- 第三章 支出报销申请与审批 ................................................ 6:第八条 支出报销申请..................................................... 6\n- 第三章 支出报销申请与审批 ................................................ 6:第九条 支出报销审批..................................................... 7\n- 第三章 支出报销申请与审批 ................................................ 6:第十条 支出成本中心归属................................................. 7\n- 第四章 重点支出管理规定 .................................................. 7:第十一条 备用金借款....................................................... 7\n- 第四章 重点支出管理规定 .................................................. 7:第十二条 市内交通费....................................................... 8\n- 第四章 重点支出管理规定 .................................................. 7:第十三条 差旅费........................................................... 8\n- 第四章 重点支出管理规定 .................................................. 7:第十四条 业务招待费...................................................... 11\n- 第五章 附则............................................................. 13:第二十三条 本办法的归口与实施 ........................................... 13\n- 第五章 附则............................................................. 13:第二十四条 附件 ......................................................... 13\n- 第五章 附则............................................................. 13:第 3 页 共 20 页\n- 第一章 总则:第一条 目的\n- 第一章 总则:为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善\n- 第一章 总则:支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n- 第一章 总则:第二条 范围\n- 第二章 职责分工:第四条 归口管理部门主要职责\n- 第二章 职责分工:1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n- 第二章 职责分工:确定各项支出业务的开支范围、标准、方式和管理流程\n\n# 结构化表格补充\n\n以下表格由知识归纳阶段依据原文重新整理,供问答检索时优先理解行列关系。\n\n## 表1 交通工具等级标准\n\n| 员工职级 | 飞机 | 火车 | 轮船 | 其他交通工具(不含小汽车) |\n|---|:---:|:---:|:---:|:---:|\n| 公司领导、高层经理、中层经理(P5及以上、外聘专家) | 经济舱 | 火车硬席(硬卧、硬座)、高铁/动车二等座,全列软席列车二等软座 | 三等舱 | 凭据报销 |\n| 基层经理、其他人员(P4及以下) | 经济舱 | 火车硬席(硬卧、硬座)、高铁/动车二等座,全列软席列车二等软座 | 三等舱 | 凭据报销 |\n\n## 表3 出差补贴标准\n\n| 补助类型 | 项目 | 港澳台 | 直辖市/特区/西藏 | 其他地区 | 国外 |\n|---------|------|--------|------------------|---------|------|\n| 餐补 | 自行解决餐食 | 75 | 65 | 55 | 140 |\n| 基本补助 | 基本出差补贴 | 35 | 35 | 35 | 35 |\n| 合计 | - | 110 | 100 | 90 | 175 |", - "chunk_order_index": 9, - "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", - "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", - "llm_cache_list": [], - "create_time": 1779011842, - "update_time": 1779011842, - "_id": "chunk-aa5435156b829944c173fa1d2d7a93d4" - } +{ + "chunk-dd87aa5bc62cc9587ecb4c26d35a5263": { + "tokens": 1200, + "content": "商密【中】\n\n 远光软件股份有限公司文件\n\n 远光制度〔2024〕14 号\n\n关于颁布《公司支出管理办法(2024)》的\n 通知\n\n公司各部门、分支机构、子公司:\n 为适应公司业务发展需要,优化、完善支出和报销标准,规\n范支出业务审批和报销过程,防范经营风险,依据国家有关法\n律法规,参照国家电网公司和国网数科公司有关管理规定,结\n合市场经营环境和公司实际情况,在广泛征求意见的基础上,\n公司对《公司支出管理办法》进行了修订,现予颁布。本办法自\n颁布之日起施行,原办法同时废止。\n 特此通知。\n\n 附件:1.公司支出管理办法(2024)\n 2.修订说明\n\n 远光软件股份有限公司\n 2024 年 4 月 17 日\n\n远光软件股份有限公司 2024 年 4 月 17 日印发\n\n 第 1 页 共 20 页\n 商密【中】\n附件 1\n\n 公司支出管理办法(2024)\n 目录\n 第一章 总则.............................................................. 4\n\n 第一条 目的............................................................. 4\n\n 第二条 范围............................................................. 4\n\n 第三条 管理原则......................................................... 4\n\n 第二章 职责分工 .......................................................... 4\n\n 第四条 归口管理部门主要职责 ............................................. 4\n\n 第五条 计划财务部主要职责............................................... 5\n\n 第六条 经办部门(个人)主要职责 ......................................... 5\n\n 第七条 各级管理人员主要职责 ............................................. 5\n\n 第三章 支出报销申请与审批 ................................................ 6\n\n 第八条 支出报销申请..................................................... 6\n\n 第九条 支出报销审批..................................................... 7\n\n 第十条 支出成本中心归属................................................. 7\n\n 第四章 重点支出管理规定 .................................................. 7\n\n 第十一条 备用金借款....................................................... 7\n\n 第十二条 市内交通费....................................................... 8\n\n 第十三条 差旅费........................................................... 8\n\n 第十四条 业务招待费...................................................... 11\n\n 第十五条 会议费.......................................................... 11\n\n 第十六条 广告宣传费...................................................... 11\n\n 第十七条 培训费.......................................................... 12\n\n 第十八条 通信费.......................................................... 12\n\n 第 2 页 共 20 页\n 商密【中】\n 第十九条 邮递费.......................................................... 12\n\n 第二十条 薪酬福利支出.................................................... 12\n\n 第二十一条 对外捐赠支出 ................................................. 13\n\n 第二十二条 涉外业务汇率标准 ............................................. 13\n\n第五章 附则............................................................. 13\n\n 第二十三条 本办法的归口与实施 ........................................... 13\n\n 第二十四条 附件 ......................................................... 13\n\n 附表 1:员工支出报销审批权限表 ............................................ 14\n\n 附表 2:岗位支出报销审批权限表 ............................................ 15\n\n 附表 3:支出归口管理部门与归口业务范围 .................................... 18\n\n 第 3 页 共 20 页\n 商密【中】\n 第一章 总则\n\n第一条 目的\n\n 为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善\n\n支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n\n法规,参照国家电网公司和国网数科公司有关管理规定,结合市场经营环境和公司实\n\n际情况,在广泛征求意见的基础上,制定本办法。\n\n第二条 范围\n\n 本办法适用于公司各类成本费用和资本性支出。\n\n 本办法适用于全公司范围,包括:公司各部门、分支机构(非独立法人)、全资\n\n子公司。控股子公司应参照本办法制订支出管理办法,按子公司相关议事规则审批、\n\n报公司计划财务部备案后执行。\n\n第三条 管理原则\n\n 公司支出管理遵循“预算先行、厉行节约、效益优先,分级授权、分类控制、批\n\n办分离”的基本原则。\n\n 1 公司支出管理遵循预算先行的控制原则,应在预算目标范围内支出,并遵循\n\n 公司内控、预算、考核管理相关规定,预算外支出履行预算审批程序后执行。\n\n 2 各部门(分支机构)、各岗位应在部门(岗位)职责与授权业务范围内,秉\n\n 持厉行节约、效益优先原则,规范开展支出业务活动。\n\n 3 各级管理人员应根据其管理岗位,在公司支出授权审批范围和本人职责范\n\n 围内,履行支出审批职权,承担审批责任。岗位审批权限与职级待遇分离,\n\n 审", + "chunk_order_index": 0, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-dd87aa5bc62cc9587ecb4c26d35a5263" + }, + "chunk-74c01decac4a10cd40a491786743b0ee": { + "tokens": 1200, + "content": "(分支机构)、各岗位应在部门(岗位)职责与授权业务范围内,秉\n\n 持厉行节约、效益优先原则,规范开展支出业务活动。\n\n 3 各级管理人员应根据其管理岗位,在公司支出授权审批范围和本人职责范\n\n 围内,履行支出审批职权,承担审批责任。岗位审批权限与职级待遇分离,\n\n 审批权限按管理岗位执行。\n\n 4 公司支出管理遵循批办分离的牵制原则,支出经办人和审批人不得为同一\n\n 人。\n\n 第二章 职责分工\n\n第四条 归口管理部门主要职责\n\n 公司实行支出归口管理,归口管理部门主要职责如下:\n\n 1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n\n 确定各项支出业务的开支范围、标准、方式和管理流程。\n\n 第 4 页 共 20 页\n 商密【中】\n 2 根据公司有关规定,对支出业务的合规性、预算(计划)执行控制情况等进\n\n 行检查、分析与监督。\n\n 支出归口管理部门与范围按“附表3”执行,部门职责变化时,相应调整归口分\n\n工。\n\n第五条 计划财务部主要职责\n\n 1 明确支出报销审批流程、审核要点和报销资料规范。\n\n 2 协助归口管理部门确定各项支出业务的开支范围和标准。\n\n 3 负责报销业务财务审核,对业务原始凭据的完整性、合规性进行审查,对报\n\n 销事项与业务原始凭据的业务关联性、内容一致性、金额准确性等进行复\n\n 核,可要求报销人提供不限于本办法规定的佐证资料。\n\n 4 办理报销业务的结算支付。\n\n 5 组织开展支出报销业务日常财务稽核与宣贯。\n\n第六条 经办部门(个人)主要职责\n\n 1 在部门(岗位)职责与授权业务范围内,秉持预算先行、厉行节约、效益优\n\n 先原则,规范开展支出业务活动。\n\n 2 业务经办过程中,应严格遵循开支范围和标准,切实履行业务流程与审批程\n\n 序,并遵循“发票、资金、物资”三流一致原则,及时取得真实、合规、关\n\n 联、完整的业务原始凭据,验证发票真伪。\n\n 3 各项物资(原材料、固定资产、无形资产、低值易耗品、办公用品)、服务、\n\n 外包分包业务的采购,应以经审核的需求计划(项目采购预算)为前提,并\n\n 严格遵循公司招标、采购与物资管理相关规定。\n\n 4 及时提交报销申请,履行报销审批流程,配合提供发票,以及不限于本办法\n\n 规定的业务佐证资料。\n\n 5 对支出业务的真实性、合规性、必要性、合理性以及业务原始凭据的真实性、\n\n 合规性、关联性、完整性,承担全部责任。\n\n第七条 各级管理人员主要职责\n\n 各级管理人员应在授权审批范围和职责范围内,履行支出报销审批职权,承担审\n\n批责任。\n\n 第 5 页 共 20 页\n 商密【中】\n 1 第一审批人:应对支出业务的真实性、合规性、必要性、合理性进行全面审\n\n 核,并承担审批责任。\n\n 2 后续审批人:应对支出业务的必要性、合理性进行审核,并承担审批责任。\n\n 第三章 支出报销申请与审批\n\n第八条 支出报销申请\n\n 1 申请方式\n\n 支出报销申请通过公司财务信息化系统实现(以下简称“系统单据”),财务原\n\n则上不接受“纸质申请单据”。\n\n (1)经办人应及时填写“系统单据”,并同步提交业务原始凭据。\n\n (2)除“员工薪酬、个人劳务报酬、出差补贴、专项补贴、往来款项支付”等支\n\n 出业务外,其他支出均需提交税务机关认可的票据。\n\n ① “工会经费、员工福利、职工活动、业务招待、车票、政府规费”以外的\n\n 支出,原则上均应取得增值税专用发票;应取得但未取得增值税专用发\n\n 票的,经办人应在“系统单据”中说明原因。\n\n ② 汇总开具的增值税发票,应附税控系统明细清单。\n\n (3)财务审核发现业务原始凭据不完整的,经办人应在三个工作日内补充并重新\n\n 提交至财务;经办人填单错误、业务原始凭据不合规,以及不能按时补充业\n\n 务原始凭据的,财务退单处理。", + "chunk_order_index": 1, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-74c01decac4a10cd40a491786743b0ee" + }, + "chunk-061324cc36078214691a6fc1cd0aaeea": { + "tokens": 1200, + "content": "说明原因。\n\n ② 汇总开具的增值税发票,应附税控系统明细清单。\n\n (3)财务审核发现业务原始凭据不完整的,经办人应在三个工作日内补充并重新\n\n 提交至财务;经办人填单错误、业务原始凭据不合规,以及不能按时补充业\n\n 务原始凭据的,财务退单处理。\n\n 2 申请时限\n\n 支出报销申请时限指“业务完成日”至“附件影像资料挂接系统单据日”的期间。\n\n (1)公司各类支出报销结算申请时限为三个月。\n\n ① 逾期需说明原因,经分管领导审批后方可报销。\n\n ② 按自然月度计算并定期发生的费用支出,需月度结束后方可报销。\n\n ③ 差旅费原则上需在行程结束三个月内提交报销申请(连续出差超过一个\n\n 月时,原则上应按月报销),逾期不予报销出差补贴。\n\n (2)预付款项,原则上应在次月底前完成结算,不得长期挂账。\n\n 3 结算方式\n\n (1)员工支出业务:原则上采用“公对私”结算方式,报销申请审批通过后,公\n\n 第 6 页 共 20 页\n 商密【中】\n 司支付给经办人。\n\n (2)岗位支出业务:原则上采用“公对公”结算方式,报销申请审批通过后,公\n\n 司与供应商直接结算。结算起点(1000 元)以下、且确实无法与供应商直接\n\n 结算的小额支出,报销时应附向供应商付款凭据的截图佐证。\n\n第九条 支出报销审批\n\n 1 审批权限\n\n (1)预算内支出,按“附表 1、附表 2”执行。\n\n (2)预算外支出,经办部门提交预算调整申请,经公司总裁批准后,再按“附表\n\n 1、附表 2”执行。\n\n 2 审批时限\n\n 各级管理人员原则上应在待批单据流转至本岗位后三个工作日内完成审批。\n\n 3 财务审核时限\n\n (1)影像扫描:“系统单据和纸质原始凭据”流转至影像岗后,原则上应在一个\n\n 工作日内处理完毕。\n\n (2)审核与支付:已完成审批的系统单据,原则上应在三个工作日内处理完毕。\n\n第十条 支出成本中心归属\n\n 支出成本中心归属原则上基于责任原则与受益原则确定,特殊情况由相关业务\n\n部门协商确定。\n\n 第四章 重点支出管理规定\n\n第十一条 备用金借款\n\n 备用金是公司借支给正式员工,用于支付与公司经济业务相关的、必须预支且尚\n\n不具备报销条件的费用支出款项。\n\n 1 备用金借款必须是真实合法的经济业务,应遵循“前款不清、后款不借”的\n 原则,严禁以各种名义挪用公司资金。\n 2 非正式员工不得申请备用金借款。\n 3 备用金借款按季定期清理。季度不能及时报账核销的,借款人应向其分管领\n 导申请延期审批,但不得跨年。\n 4 员工备用金借款额度原则上不得超过一万元。\n\n 第 7 页 共 20 页\n 商密【中】\n第十二条 市内交通费\n 市内交通费是指员工为公司生产经营活动,在工作所在地发生的市内交通费用,\n包括工作时间内外出办理公事、夜间工作或非工作日发生的市内交通费用,不包括员\n工正常上下班所发生的交通费用、从居住地公出或公出结束返回居住地发生的市内\n交通费。\n 1 市内交通费凭据报销,应与工作实际相符,严禁报销与工作无关的交通费,\n 不接受充值预付费方式的交通费。\n 2 基于厉行节约原则,鼓励员工选择公交、地铁等公共交通出行。出租车仅限\n “紧急公务、接送客户、夜间工作至 22:00 后、以及其他确有需要的特别情\n 形”等事项的市内交通使用,由部门负责人从严管理。\n第十三条 差旅费\n 差旅费是指员工因公出差所发生的交通费、住宿费、出差补贴等。\n 各部门(单位)应严格执行出差审批程序,从严控制出差人数和出差天数,严禁\n无实质内容、无明确公务目的的差旅活动。部门副职及以下员工出差需通过公司商旅\n系统事前审批。\n 1 交通费\n 表1 交通工具等级标准\n 国内(含港澳台)、国外", + "chunk_order_index": 2, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-061324cc36078214691a6fc1cd0aaeea" + }, + "chunk-613d6dfd4c5e9c807229a3147f96b584": { + "tokens": 1200, + "content": "费、住宿费、出差补贴等。\n 各部门(单位)应严格执行出差审批程序,从严控制出差人数和出差天数,严禁\n无实质内容、无明确公务目的的差旅活动。部门副职及以下员工出差需通过公司商旅\n系统事前审批。\n 1 交通费\n 表1 交通工具等级标准\n 国内(含港澳台)、国外\n 其他交通工具\n 员工职级\n 飞机 火车 轮船 (不含小汽\n 车)\n公司领导、高层经理、\n 中层经理 火车硬席(硬\n 经济舱 卧、硬座)、\n(P5 及以上、外聘专\n 家) 高铁/动车二\n 三等舱 凭据报销\n 等座,全列软\n基层经理、其他人员 经济舱 席列车二等软\n (P4 及以下) (注 3) 座\n\n 第 8 页 共 20 页\n 商密【中】\n 国内(含港澳台)、国外\n 其他交通工具\n 员工职级\n 飞机 火车 轮船 (不含小汽\n 车)\n注 1:交通工具选乘应遵循“性价比优先”原则。\n注 2:基层经理及以下人员(P4 及以下)乘坐飞机需事前报经部门负责人审批。基层经\n理(P4)应选乘 6 折及以下经济舱、其他人员(P1-P3)应选乘 5 折及以下经济舱。\n注 3:夜间乘坐高铁/动车 6 小时以上时,可选乘卧铺。\n注 4:出差人员应当严格执行交通工具等级标准,确因紧急公务、特别情形等事项导致\n交通工具超过规定标准时,超标 20%以内时由部门负责人审批,超标 20%以上时需分管\n领导审批。\n注 5:公司已为员工购买了商业保险(含交通险),出差期间发生的保险费不再报销。\n注 6:出租车仅限“紧急公务、接送客户、夜间工作至 22:00 后、以及其他确有需要的\n特别情形”等事项的市内交通使用,由部门负责人从严管理。往返机场、车站、港口,\n原则上应选择“公交车、轨道交通”。\n注 7:自驾车出差发生的路桥费、停车费、油费、电费等据实报销,由部门负责人从严\n管理。自驾发生事故或违章罚款等造成的损失,责任自负。\n 2 住宿费\n (1)酒店住宿\n\n 表2 酒店住宿限额标准\n\n 单位:人民币元\n 国内\n 员工职级 直辖市/特区/ 国外\n 省会城市 其他地区\n 港澳台\n 公司领导\n 500 450 400 800\n (P8及以上)\n 高层经理\n 450 400 350 700\n (P7)\n 中层经理、基层经理\n 400 350 300 600\n(P4~P6、外聘专家)\n\n 其他员工 350 300 250 500\n注1:出差人员应严格执行住宿限额标准,确因紧急公务、特别情形等事项导致住宿超\n过规定标准时,超标20%以内时由部门负责人审批,超标20%以上时需分管领导审批。\n注2:外出参加会议、培训,统一安排食宿的,会议期间的住宿费按外部会议组织方通\n知标准凭据报销。不统一安排食宿的,按照上表标准执行。\n\n (2)异地工作用房\n\n ① 因公长期出差人员申请异地工作用房的,按照《公司物业租赁管理办法》\n\n 第 9 页 共 20 页\n 商密【中】\n 执行。\n\n ② 异地工作用房产生的租金、房屋初始配置费、物业管理费、取暖费、水电\n\n 燃气支出(含公摊)、网络宽带费等费用,按照《公司物业租赁管理办法》确\n\n 定标准进行报销。\n\n 3 出差补贴\n\n 出差补贴按出差自然天数(日历天数)进行报销,具体标准如下:\n\n 表3 出差补贴标准\n\n 单位:人民币元/天\n\n 国内\n补助类型 项目 国外\n 港澳台 直辖市/特区/西藏 其他地区\n\n 餐补 自行解决餐食 75 65 55 140\n全额\n补助 基本\n 基本出差补贴 35 35 35 35\n 补助\n 合计 110 100 90 175\n注:因组织安排、销售、推广、调", + "chunk_order_index": 3, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-613d6dfd4c5e9c807229a3147f96b584" + }, + "chunk-d26b288ed4001dc5c504dce0eb841362": { + "tokens": 1200, + "content": "直辖市/特区/西藏 其他地区\n\n 餐补 自行解决餐食 75 65 55 140\n全额\n补助 基本\n 基本出差补贴 35 35 35 35\n 补助\n 合计 110 100 90 175\n注:因组织安排、销售、推广、调研、培训、会议、研讨、借调等出差,主办方统一安排\n餐食的,不再报销餐补。\n 4 差旅费其他注意事项\n\n (1)因公发生的订票、签转、退票等费用凭据报销,由部门负责人审核确认。\n\n (2)出差记录链条中断时,应提供业务佐证材料:\n\n ① 登机牌、高速道路通行记录、其他道路通行记录、租车记录等。\n\n ② 支付记录。\n\n ③ 出差审批邮件、短信、微信等。\n\n (3)出差或调动工作期间,经部门负责人批准就近回家省亲办事的,其绕道交通\n\n 费,扣除出差直线单程交通费,多开支的部分由个人自理。绕道和在家期间\n\n 不得报销住宿费、出差补贴。\n\n (4)出差交通费原始凭据丢失的,提供情况说明和订单详情、支付截图等佐证材\n\n 料,可按票面价值的 75%报销,未提供的,不予报销。通过商旅订票未取得\n\n 火车票的,从差旅补贴或其他差旅杂费中扣减。\n\n (5)探亲路费应严格遵循《公司员工探亲管理办法》相关规定,不得以因公差旅\n\n 第 10 页 共 20 页\n 商密【中】\n 方式报销,不享受出差补贴。探亲路费原始凭据丢失的,不允许报销。\n\n (6)员工工作地调动时,所发生的行李、家具等邮寄费,在每人每公里 1 元以内\n\n 凭据报销,超过部分自理。\n\n (7)经组织安排到异地挂职锻炼、培养锻炼、人才帮扶、借调支援等工作人员,\n\n 在途期间的交通费、餐补和基本补助按出差规定执行;在异地单位工作期间,\n\n 不适用出差补贴标准规定,按照组织人事部确定的挂职人员报销标准执行;\n\n 所在单位不安排住宿的,住宿标准按公司酒店住宿限额标准执行。\n\n (8)境外出差(港澳台与国外出差)应单列预算、事前履行经费申请与审批程序。\n\n (9)以上涉及事前审批的事项,以公司商旅系统审批截图或审批邮件为报销必备\n\n 附件。\n\n(10)因公出差原则上应使用商旅系统统一预定,特殊情况未通过商旅系统下单\n\n的,应邮件知会商旅客服并抄送部门负责人。\n\n第十四条 业务招待费\n\n 业务招待费是指各级单位正常生产经营管理过程中需要发生的、用于必要招待\n\n的各项费用,包括用于接待客户和相关单位的餐饮等合理支出。未能“公对公”结算\n\n的,应附“向供应商付款凭据”佐证。\n\n第十五条 会议费\n\n 会议费是指公司主办或承办会议发生的会场租金、文件资料、设备租金等支出,\n\n以及参加外部会议所发生的会务相关支出。\n\n 1 公司主办或承办的会议\n\n (1)会议费中不得列支与会议无关的旅游观光、宴请、礼品馈赠等支出。\n\n (2)经费预算 30,000 元及以上(包括不在会议费列支,但与会议直接相关的培训\n\n 费、差旅费等全部支出)的公司内部会议、研讨与集中培训,需事前报请公\n\n 司总裁审批。\n\n (3)报销时应附:经审批的会议申请与预算、会议通知、参会人员签到表、会议\n\n 开支明细等业务佐证材料,会务费发票应附服务方费用明细清单。\n\n 2 参加外部会议的,报销时应附会议通知、参会回执等业务佐证材料。\n\n第十六条 广告宣传费\n\n 第 11 页 共 20 页\n 商密【中】\n 广告宣传费是指展示企业形象、宣传企业文化及营销活动中推广企业产品、业务\n\n所发生的支出,包括广告费和业务宣传费。\n\n 1 广告费\n\n 广告费指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业\n\n务佐证材料。\n\n 2 业务宣传费\n\n 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体\n\n传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。业务宣传", + "chunk_order_index": 4, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-d26b288ed4001dc5c504dce0eb841362" + }, + "chunk-e9438f69c9e221d9f0f00a05ad84eac6": { + "tokens": 1200, + "content": "费。\n\n 1 广告费\n\n 广告费指公司通过各种公开媒体宣传所发生的费用,报销时应附广告投放等业\n\n务佐证材料。\n\n 2 业务宣传费\n\n 业务宣传费是指公司自身开展宣传与营销活动所发生的费用,包括未通过媒体\n\n传播的广告性支出,以及发放的印有公司标志的礼品、纪念品等。业务宣传费报销时\n\n应附活动方案、费用预算等业务佐证材料,并需遵循公司采购与物资管理相关规定。\n\n第十七条 培训费\n\n 培训费是指公司安排、主办或承办培训发生的学费、书籍杂费、师资费、资料费、\n\n场地费、设备材料费、住宿费、交通费等,以及员工根据工作需要,经归口管理部门\n\n批准参加外部培训、考证、教育产生的相关费用。\n\n 1 报销资格按《公司员工教育培训管理办法》认定标准执行。\n\n 2 经归口管理部门认定符合办法标准的,培训期间的主要交通费(往返)、住\n\n 宿费按出差规定标准执行,其他费用自理。\n\n第十八条 通信费\n\n 通信费是指为满足公司日常办公需要发生的电话、传真、集团网、网络连接等支\n\n出,其中员工通讯费按《公司员工因公通讯费用实施细则》执行。\n\n第十九条 邮递费\n\n 邮递费是指公司因业务需要邮递物品而支付的费用,包括邮件费、托运费、快递\n\n费等,员工报销的应附快递底单,单位统一结算的应附寄件明细清单与支出分摊表。\n\n第二十条 薪酬福利支出\n\n 1 公司相关制度规定的职工薪资、奖励提成、福利费支出,按公司相关制度规\n\n 定执行。\n\n 2 临时的奖励及福利支出,需事先计划并报公司总裁审批,具体支出时由分管\n\n 领导根据已批准的计划审批。\n\n 3 职工福利费由组织人事部实行年度计划管理,对未纳入福利计划的福利项\n\n 第 12 页 共 20 页\n 商密【中】\n 目,不得列支和报销。\n\n 4 薪酬福利支出事前审批程序以组织人事部规定标准执行。\n\n 5 职工活动支出,按照《公司团建管理办法》或《工会经费管理办法》执行。\n\n第二十一条 对外捐赠支出\n\n 公司对外捐赠支出由品牌及市场运营中心归口管理,并严格预算单控,未纳入对\n\n外捐赠预算的,不得对外捐赠。\n\n 1 预算内对外捐赠事项发生时,实施捐赠的业务部门应提出捐赠申请,详细说\n\n 明捐赠事由、捐赠对象、捐赠金额等内容,经业务部门负责人审批,品牌及\n\n 市场运营中心归口审核,业务部门分管领导审批后执行。\n\n 2 未纳入预算的对外捐赠事项,实施捐赠的业务部门应提出捐赠申请,详细说\n\n 明捐赠必要性,履行对外捐赠的预算调整决策程序,并纳入预算范围后方可\n\n 实施。\n\n 3 捐赠事项完成后,实施捐赠的业务部门应将捐赠活动的凭据资料报品牌及\n\n 市场运营中心备案,凭据资料包括但不限于发票、收据、捐赠协议、接收函、\n\n 捐赠清单等。\n\n第二十二条 涉外业务汇率标准\n\n 以人民币结算的,凭据报销;以外币结算的,按支付凭据所载汇率折算,支付凭\n\n据未载明汇率的按业务发生月第一个交易日“中国银行外汇折算价”执行,中国银行\n\n无折算价的币种按“中国外汇交易中心参考汇率”执行。\n\n 第五章 附则\n\n第二十三条 本办法的归口与实施\n\n 1 本办法自颁布之日起施行,原办法同时废止。\n\n 2 本办法由公司计划财务部负责制定、修订、解释及实施协调工作。\n\n第二十四条 附件\n\n 附表1:员工支出报销审批权限表\n\n 附表2:岗位支出报销审批权限表\n\n 附表3:支出归口管理部门与归口业务范围\n\n 第 13 页 共 20 页\n 商密【中】\n 附表 1:员工支出报销审批权限表\n 单位:人民币万元\n 审批权限\n 部门经理/\n 支出项目 总监/ 副总裁/ 高级副总裁/ 补充说明\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n 业务招待 0", + "chunk_order_index": 5, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-e9438f69c9e221d9f0f00a05ad84eac6" + }, + "chunk-9841d66d8fb8548aab40220663a51693": { + "tokens": 1200, + "content": "附表 1:员工支出报销审批权限表\n 单位:人民币万元\n 审批权限\n 部门经理/\n 支出项目 总监/ 副总裁/ 高级副总裁/ 补充说明\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n 业务招待 0.5 1 2 3 15\n\n 因公借款 0.5 1 2 3 15\n员工支出\n\n 1.差旅费、市内交通。\n 其他支出 1 2 3 5 50 2.客服及商务、教育、邮递。\n 3.职工活动等其他临时性支出。\n\n 第 14 页 共 20 页\n 商密【中】\n 附表 2:岗位支出报销审批权限表\n 单位:人民币万元\n\n 审批权限\n\n 支出项目 部门经理/ 总监/ 补充说明\n 高级\n 机构总经理/ 一级部门 副总裁 总裁\n 副总裁\n 事业部总经理 总经理\n\n 资产采购 1 2 10 15 200 包括固定资产、无形资产、低值易耗品。\n资本性 基建工程 —— —— 5 10 50\n支出\n 股权投资、\n —— —— —— —— —— 由董事长审批。\n 兼并收购\n 1.生产采购:产品生产用原材料、辅助\n 材料、机物料等。\n 材料采购 —— 10 20 30 500\n 2.项目采购:项目外采的设备、软件、公\n 有云资源等。\n 分包外包\n —— 10 20 30 500\n (内部单位) 1.研发、实施、运维、服务等分包外包。\n 分包外包 2.委托加工、项目土建装修等。\n收益性 —— 10 20 30 200\n (外部单位)\n支出\n 1.投标相关保证金:投标保证金、质保\n 保证金 5 50 100 200 500 金、履约保证金。\n 2.招标相关保证金。\n 1.销售退款。\n 销售退款 5 10 30 50 200\n 2.代付款(代收后)。\n 不含水电及杂费,需经业务归口部门审\n 房屋租金 5 10 20 30 200\n 批。\n\n 第 15 页 共 20 页\n 商密【中】\n 公司统一结算或批量采购的:\n 1.食堂采购、通勤车。\n 统结支出\n 2.交通费、住宿费。\n 3.业务招待费、通讯费等。\n 代付子公司\n 代付子公司投标支出。\n 经营支出\n 1.广告费。\n 市场营销 2.业务宣传费:业务宣传、企业文化宣\n 传、市场活动、营销活动。\n\n 专项服务、 1 3 5 10 50 外部单位与个人提供的咨询、培训、劳务\n 外聘劳务 等服务(非专家意见、非鉴证报告)。\n\n 国家机关、事业单位、代行政府职能的\n 政府规费\n 社会团体收费。\n 慰问救济、对\n —— —— 1 2 10 公司制度外的慰问救济捐赠。\n 外捐赠\n 税费支出 50 100 200 300 500 增值税及各类附加费、所得税等。\n\n 其他支出 1 2 3 5 50 设备租赁、公务车支出等\n\n 资金转户/调拨 50 —— —— 2000 3000\n\n财务专用 银行手续费\n 5 10 30 50 200\n 错付退款 不含销售退款。\n\n 其他支付 —— —— —— 15 50 出纳提现等。\n\n 第 16 页 共 20 页\n 商密【中】\n注 1:上述权限额度均含本数,各级管理人员权限指各自职责范围内审批权限。\n注 2:上述权限中业务流转执行以组织关系为基准的逐级审批规则。特殊事项经决策后,可越级至终审岗审批。\n注 3:仅结算业务,由业务部门审批确定是否达到财务入账条件,并按照上述对应支出项目权限审批。\n注 4:代付", + "chunk_order_index": 6, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-9841d66d8fb8548aab40220663a51693" + }, + "chunk-afc57a0e9548d1f484da6df6c182676b": { + "tokens": 1200, + "content": "】\n注 1:上述权限额度均含本数,各级管理人员权限指各自职责范围内审批权限。\n注 2:上述权限中业务流转执行以组织关系为基准的逐级审批规则。特殊事项经决策后,可越级至终审岗审批。\n注 3:仅结算业务,由业务部门审批确定是否达到财务入账条件,并按照上述对应支出项目权限审批。\n注 4:代付子公司经营支出原则上与子公司按季度汇总结算。\n注 5:薪酬福利支出分配计划审批按照人事归口管理部门规定执行。\n注 6:上述权限不含协管、高级顾问、顾问及主持工作人员,公司如有其相关文件通知,从其规定授权。\n注 7:全资子公司总经理由母公司未明确层级管理人员担任的,审批权限按“总监/一级部门总经理”执行。\n注 8:全资子公司员工薪资、奖金提成、福利支出、临时的奖励及福利支出审批按照人事归口管理部门规定执行。\n注 9:全资子公司其他部门经理,按照公司 1 号文对应岗位授权,由平级人员分角色担任的,按最高岗位授权,逐级审批。\n\n 第 17 页 共 20 页\n 商密【中】\n 附表 3:支出归口管理部门与归口业务范围\n 归口管理部门 归口业务范围\n 1.党建支出。\n办公室(党委办公室)\n 2.公务车支出。\n 工会委员会 工会支出。\n 1.投标业务支出:标书费、投标保证金、中标服务费(或保险)等。\n 营销中心 2.机构营销业务支出:客服及商务支出、营销活动支出。\n 3.客户培训、销售退款等支出。\n 1.广告费支出。\n品牌及市场运营中心 2.业务宣传支出:业务宣传、文化宣传、市场活动。\n 3.对外捐赠支出。\n 1.薪酬福利(不含食堂、误餐)、外聘劳务、员工保险支出。\n 组织人事部 2.探亲差旅、条件艰苦及安全风险较高区域补助等支出。\n 3.其他福利支出。\n 1.招聘业务。\n 人力资源服务部\n 2.员工教育培训支出。\n 1.员工备用金、差旅费、市内交通费、出国经费、误餐费支出。\n 计划财务部 2.税金及附加、审计评估、财务费用支出。\n 3.客服及商务支出。\n 产业投资部 股权投资支出、并购业务支出。\n证券与法律事务部 法律事务类支出、上市信披类支出、商标注册类支出。\n 产品规划设计部 知识产权类支出、信息技术咨询类支出、研发活动类支出。\n DAP 研发中心 研究开发过程中支付给外单位的检测、评测、测试及化验等支出。\n 1.IT 类资产的购置(建造)、租入、运维、修理等支出。\n 信息管理部\n 2.网络使用费支出。\n 1.非 IT 类资产的购置、租入、运维、修理、装修等支出,财产保险支出。\n 2.食堂支出。\n 后勤服务部\n 3.办公费用支出。\n 4.商旅统付结算。\n\n 第 18 页 共 20 页\n 商密【中】\n附件 2\n\n 修订说明\n\n 《公司支出管理办法》修订涉及修改报销标准 2 项,取消报销规定 1 项,新增\n\n报销规定 2 项,审批权限变化 2 项,具体情况如下。\n\n 一、报销标准变化情况\n\n 只调整了差旅费、异地调动邮寄费两项内容。\n\n 1.差旅费。只调整了公司领导出差交通工具标准和出差补贴。\n\n 出差交通工具标准:公司领导(P8)乘坐火车标准(由“不限”标准调低为\n\n“二等座”)、轮船标准(由“二等舱”调低为“三等舱”)。\n\n 出差补贴:原制度描述出差补助为包干制,实际执行时在两项补助之外还存在\n\n报销打车费情况,故取消了原制度中“包干”表述。\n\n 2.异地调动邮寄费。将原制度“可凭据报销一次个人物品邮寄费用,金额超过\n\n1000 元的需事前审批”,调整为“1 元/人/公里内报销,超标自理”。\n\n 二、取消报销规定内容\n\n 因公用车补贴已纳入工资内发放,新制度中取消该费用相关表述。\n\n 三、新增规定内容\n\n 1.异地挂职锻炼补贴标准。经组织安排到异地挂职锻炼、培养锻炼、", + "chunk_order_index": 7, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-afc57a0e9548d1f484da6df6c182676b" + }, + "chunk-18d968b78afe916b419c1b5973421ebe": { + "tokens": 1200, + "content": "1000 元的需事前审批”,调整为“1 元/人/公里内报销,超标自理”。\n\n 二、取消报销规定内容\n\n 因公用车补贴已纳入工资内发放,新制度中取消该费用相关表述。\n\n 三、新增规定内容\n\n 1.异地挂职锻炼补贴标准。经组织安排到异地挂职锻炼、培养锻炼、人才帮\n\n扶、借调支援等工作人员,在途期间的交通费、餐补和基本补助按出差规定执行;\n\n在异地单位工作期间,不适用出差补贴标准规定,按照组织人事部确定的挂职人员\n\n报销标准执行;所在单位不安排住宿的,住宿标准按公司酒店住宿限额标准执行。\n\n 2.对使用商旅订票进行了规范。因公出差原则上应使用商旅系统统一预定。\n\n 四、审批权限变化情况\n\n 只调整了“投标保证金”“审批流转程序”两项内容:\n\n 1.调整投标保证金审批权限。因对投标缴纳保证金的时效性要求较高,为提高\n\n审批效率,调整投标保证金审批权限,具体如下(单位:万元):\n\n 第 19 页 共 20 页\n 商密【中】\n 审批权限\n\n 部门经理/\n支出项目 总监/ 副总裁/ 高级副总裁/\n 机构总经理/ 总裁\n 一级部门总经理 总工程师 各委员会主任\n 事业部总经理\n\n保证金 5 50 100 200 500\n\n 2.明确支出审批流转程序。业务流转执行以组织关系为基准的逐级审批规则。\n\n 特殊事项经决策后,可越级至终审岗审批。\n\n 第 20 页 共 20 页\n 商密【中】\n\n# 章节导航\n\n以下内容由入库阶段从制度原文中提取,供检索时优先理解制度层级、条目和标准所在章节。\n\n- 第一章 总则.............................................................. 4\n- 第二章 职责分工 .......................................................... 4\n- 第三章 支出报销申请与审批 ................................................ 6\n- 第四章 重点支出管理规定 .................................................. 7\n- 第五章 附则............................................................. 13\n- 第一章 总则\n- 第二章 职责分工\n- 第三章 支出报销申请与审批\n- 第四章 重点支出管理规定\n- 第五章 附则\n- 一、报销标准变化情况\n- 二、取消报销规定内容\n\n# 重点章节摘录\n\n## 第一章 总则.............................................................. 4\n\n第一条 目的............................................................. 4;第二条 范围............................................................. 4;第三条 管理原则......................................................... 4\n\n## 第二章 职责分工 .......................................................... 4\n\n第四条 归口管理部门主要职责 ............................................. 4;第五条 计划财务部主要职责............................................... 5;第六条 经办部门(个人)主要职责 ......................................... 5\n\n## 第三章 支出报销申请与审批 ................................................ 6\n\n第八条 支出报销申请..................................................... 6;第九条 支出报销审批..................................................... 7;第十条 支出成本中心归属................................................. 7\n\n## 第四章 重点支出管理规定 .................................................. 7\n\n第十一条 备用金借款....................................................... 7;第十二条 市内交通费....................................................... 8;第十三条 差旅费........................................................... 8\n\n## 第五章 附则............................................................. 13\n\n第二十三条 本办法的归口与实施 ........................................... 13;第二十四条 附件 ......................................................... 13;附表 1:员工支出报销审批权限表 ............................................ 14\n\n## 第一章 总则\n\n第一条 目的;为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善;支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n\n## 第二章 职责分工\n\n第四条 归口管理部门主要职责;公司实行支出归口管理,归口管理部门主要职责如下:;1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n\n## 第三章 支出报销申请与审批\n\n第八条 支出报销申请;1 申请方式;支出报销申请通过公司财务信息化系统实现(以下简称“系统单据”),财务原\n\n# 问答线索补充\n\n以下内容由入库阶段根据章节标题、条款、列表、键值对与相邻正文提炼,供问答检索时优先命中更短、更直接的制度依据。\n\n- 第一章 总则.............................................................. 4:第一条 目的............................................................. 4\n- 第一章 总则.............................................................. 4:第二条 范围............................................................. 4\n- 第一章 总则................................", + "chunk_order_index": 8, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-18d968b78afe916b419c1b5973421ebe" + }, + "chunk-aa5435156b829944c173fa1d2d7a93d4": { + "tokens": 1020, + "content": "问答线索补充\n\n以下内容由入库阶段根据章节标题、条款、列表、键值对与相邻正文提炼,供问答检索时优先命中更短、更直接的制度依据。\n\n- 第一章 总则.............................................................. 4:第一条 目的............................................................. 4\n- 第一章 总则.............................................................. 4:第二条 范围............................................................. 4\n- 第一章 总则.............................................................. 4:第三条 管理原则......................................................... 4\n- 第二章 职责分工 .......................................................... 4:第四条 归口管理部门主要职责 ............................................. 4\n- 第二章 职责分工 .......................................................... 4:第五条 计划财务部主要职责............................................... 5\n- 第二章 职责分工 .......................................................... 4:第六条 经办部门(个人)主要职责 ......................................... 5\n- 第二章 职责分工 .......................................................... 4:第七条 各级管理人员主要职责 ............................................. 5\n- 第三章 支出报销申请与审批 ................................................ 6:第八条 支出报销申请..................................................... 6\n- 第三章 支出报销申请与审批 ................................................ 6:第九条 支出报销审批..................................................... 7\n- 第三章 支出报销申请与审批 ................................................ 6:第十条 支出成本中心归属................................................. 7\n- 第四章 重点支出管理规定 .................................................. 7:第十一条 备用金借款....................................................... 7\n- 第四章 重点支出管理规定 .................................................. 7:第十二条 市内交通费....................................................... 8\n- 第四章 重点支出管理规定 .................................................. 7:第十三条 差旅费........................................................... 8\n- 第四章 重点支出管理规定 .................................................. 7:第十四条 业务招待费...................................................... 11\n- 第五章 附则............................................................. 13:第二十三条 本办法的归口与实施 ........................................... 13\n- 第五章 附则............................................................. 13:第二十四条 附件 ......................................................... 13\n- 第五章 附则............................................................. 13:第 3 页 共 20 页\n- 第一章 总则:第一条 目的\n- 第一章 总则:为适应远光软件股份有限公司(以下简称“公司”)业务发展需要,优化、完善\n- 第一章 总则:支出和报销标准,规范支出业务审批和报销过程,防范经营风险,依据国家有关法律\n- 第一章 总则:第二条 范围\n- 第二章 职责分工:第四条 归口管理部门主要职责\n- 第二章 职责分工:1 在遵循公司财务、采购、人资等相关制度规定的基础上,根据业务管理需要,\n- 第二章 职责分工:确定各项支出业务的开支范围、标准、方式和管理流程\n\n# 结构化表格补充\n\n以下表格由知识归纳阶段依据原文重新整理,供问答检索时优先理解行列关系。\n\n## 表1 交通工具等级标准\n\n| 员工职级 | 飞机 | 火车 | 轮船 | 其他交通工具(不含小汽车) |\n|---|:---:|:---:|:---:|:---:|\n| 公司领导、高层经理、中层经理(P5及以上、外聘专家) | 经济舱 | 火车硬席(硬卧、硬座)、高铁/动车二等座,全列软席列车二等软座 | 三等舱 | 凭据报销 |\n| 基层经理、其他人员(P4及以下) | 经济舱 | 火车硬席(硬卧、硬座)、高铁/动车二等座,全列软席列车二等软座 | 三等舱 | 凭据报销 |\n\n## 表3 出差补贴标准\n\n| 补助类型 | 项目 | 港澳台 | 直辖市/特区/西藏 | 其他地区 | 国外 |\n|---------|------|--------|------------------|---------|------|\n| 餐补 | 自行解决餐食 | 75 | 65 | 55 | 140 |\n| 基本补助 | 基本出差补贴 | 35 | 35 | 35 | 35 |\n| 合计 | - | 110 | 100 | 90 | 175 |", + "chunk_order_index": 9, + "full_doc_id": "2c1cb358f08d44ceb0e4d287133206ec", + "file_path": "/app/server/storage/knowledge/报销制度/2c1cb358f08d44ceb0e4d287133206ec__远光《公司支出管理办法(2024)》.pdf", + "llm_cache_list": [], + "create_time": 1779011842, + "update_time": 1779011842, + "_id": "chunk-aa5435156b829944c173fa1d2d7a93d4" + } } \ No newline at end of file diff --git a/server/tests/test_agent_asset_onlyoffice_key.py b/server/tests/test_agent_asset_onlyoffice_key.py index 30d9cb1..36f3d87 100644 --- a/server/tests/test_agent_asset_onlyoffice_key.py +++ b/server/tests/test_agent_asset_onlyoffice_key.py @@ -15,9 +15,8 @@ def test_rule_spreadsheet_onlyoffice_key_uses_safe_characters() -> None: key = AgentAssetService._build_onlyoffice_document_key( "asset:id", - "v1.0.0", metadata, ) - assert key == "asset_id-v1.0.0-abc123" + assert key == "asset_id-abc123" assert ":" not in key diff --git a/server/tests/test_agent_asset_service.py b/server/tests/test_agent_asset_service.py index 83d0869..774ac6b 100644 --- a/server/tests/test_agent_asset_service.py +++ b/server/tests/test_agent_asset_service.py @@ -310,7 +310,7 @@ def test_restore_version_creates_new_working_copy_without_rewriting_published_ve assert restored.current_version_change_note == "基于历史版本 v1.0.0 恢复生成工作稿" -def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None: +def test_spreadsheet_upload_records_sheet_and_cell_changes_without_versions() -> None: with build_session() as db: service = AgentAssetService(db) rule = next( @@ -322,34 +322,33 @@ def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None: service.upload_rule_spreadsheet( rule.id, filename="公司差旅费报销规则.xlsx", - content=build_workbook_bytes([["城市", "住宿"], ["北京", 500]]), + content=build_workbook_bytes([["城市", "住宿"], ["北京", 500]]), actor="finance_user", ) - base_version = service.get_asset(rule.id).working_version # type: ignore[union-attr] service.upload_rule_spreadsheet( rule.id, filename="公司差旅费报销规则.xlsx", content=build_workbook_bytes([["城市", "住宿"], ["北京", 550], ["武汉", 450]]), actor="finance_user", ) - target_version = service.get_asset(rule.id).working_version # type: ignore[union-attr] - diff = service.compare_spreadsheet_versions( - rule.id, - base_version=base_version or "", - target_version=target_version or "", - ) + records = service.list_spreadsheet_change_records(rule.id) + latest = records[0] - assert diff.changed_sheet_count == 1 - assert diff.changed_cell_count == 3 + assert latest.changed_sheet_count == 1 + assert latest.changed_cell_count == 3 assert any( item.cell == "B2" and item.change_type == "modified" - for item in diff.cell_changes + for item in latest.cell_changes ) - assert any(item.cell == "A3" and item.change_type == "added" for item in diff.cell_changes) + assert any( + item.cell == "A3" and item.change_type == "added" + for item in latest.cell_changes + ) + assert not hasattr(latest, "version") -def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_copy() -> None: +def test_spreadsheet_content_reads_current_rule_file_without_version_snapshot() -> None: with build_session() as db: service = AgentAssetService(db) rule = next( @@ -366,7 +365,6 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co ) detail = service.get_asset(rule.id) assert detail is not None - working_version = detail.working_version or "" current_asset = service.repository.get(rule.id) assert current_asset is not None @@ -375,23 +373,13 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co assert "agent_assets" not in live_storage_key live_path = service.spreadsheet_manager.resolve_storage_path(live_storage_key) assert not service.spreadsheet_manager.asset_root.exists() - original_live_bytes = live_path.read_bytes() - try: - live_path.write_bytes(build_workbook_bytes([["城市", "住宿"], ["北京", 999]])) - snapshot_path, _, _ = service.get_rule_spreadsheet_content( - rule.id, - version=working_version, - ) + current_path, _, _ = service.get_rule_spreadsheet_content(rule.id) - assert snapshot_path != live_path - assert FINANCE_RULES_LIBRARY in snapshot_path.parts - assert ".versions" in snapshot_path.parts - assert "agent_assets" not in snapshot_path.parts - workbook = load_workbook(snapshot_path, data_only=False) - assert workbook.active["B2"].value == 500 - finally: - live_path.write_bytes(original_live_bytes) + assert current_path == live_path + assert ".versions" not in current_path.parts + workbook = load_workbook(current_path, data_only=False) + assert workbook.active["B2"].value == 500 def test_spreadsheet_change_records_return_recent_edit_details() -> None: @@ -454,7 +442,6 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None: ) detail = service.get_asset(rule.id) assert detail is not None - first_version = detail.working_version service.upload_rule_spreadsheet( rule.id, @@ -473,7 +460,7 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None: changed_sheets = {item.sheet_name for item in latest.sheet_changes} changed_cell_sheets = {item.sheet_name for item in latest.cell_changes} - assert latest.version != first_version + assert not hasattr(latest, "version") assert latest.changed_sheet_count == 2 assert {"差旅标准", "填表说明"}.issubset(changed_sheets) assert {"差旅标准", "填表说明"}.issubset(changed_cell_sheets) @@ -513,6 +500,8 @@ def test_editable_spreadsheet_onlyoffice_config_enables_forcesave(monkeypatch) - customization = config.config["editorConfig"]["customization"] assert config.config["editorConfig"]["mode"] == "edit" assert customization["forcesave"] is True + assert "version=" not in config.config["document"]["url"] + assert "version=" not in config.config["editorConfig"]["callbackUrl"] def test_version_timeline_contains_created_review_and_publish_events() -> None: diff --git a/server/tests/test_agent_foundation_endpoints.py b/server/tests/test_agent_foundation_endpoints.py index 557207d..79e9e3b 100644 --- a/server/tests/test_agent_foundation_endpoints.py +++ b/server/tests/test_agent_foundation_endpoints.py @@ -1,98 +1,98 @@ -from __future__ import annotations - -from collections.abc import Generator - -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import Session, sessionmaker -from sqlalchemy.pool import StaticPool - -from app.api.deps import get_db -from app.core.agent_enums import AgentAssetStatus -from app.db.base import Base -from app.main import create_app -from app.services.agent_assets import AgentAssetService - - -def build_client() -> tuple[TestClient, sessionmaker[Session]]: - engine = create_engine( - "sqlite+pysqlite:///:memory:", - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - ) - Base.metadata.create_all(bind=engine) - session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) - - app = create_app() - - def override_db() -> Generator[Session, None, None]: - db = session_factory() - try: - yield db - finally: - db.close() - - app.dependency_overrides[get_db] = override_db - return TestClient(app), session_factory - - -def test_list_agent_assets_endpoint_returns_seeded_items() -> None: - client, _ = build_client() - - response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) - - assert response.status_code == 200 - payload = response.json() - assert payload - assert all(item["asset_type"] == "rule" for item in payload) - assert any(item["code"] == "rule.expense.travel_risk_control_standard" for item in payload) - - -def test_get_agent_asset_detail_endpoint_returns_version_history() -> None: - client, _ = build_client() - - list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) - asset_id = next( - item["id"] - for item in list_response.json() - if item["code"] == "rule.expense.travel_risk_control_standard" - ) - - response = client.get(f"/api/v1/agent-assets/{asset_id}") - - assert response.status_code == 200 - payload = response.json() - assert payload["recent_versions"] - assert payload["current_version_content_type"] == "markdown" - assert payload["current_version"] == "v1.1.0" - assert "行程闭环" in payload["current_version_content"] - - -def test_activate_pending_rule_endpoint_is_blocked() -> None: - client, session_factory = build_client() - - with session_factory() as db: - pending_rule = next( - item - for item in AgentAssetService(db).list_assets(asset_type="rule") - if item.status == AgentAssetStatus.REVIEW.value - ) - - response = client.post( - f"/api/v1/agent-assets/{pending_rule.id}/activate", - headers={"x-actor": "pytest"}, - ) - - assert response.status_code == 400 - assert "审核" in response.json()["detail"] - - -def test_list_audit_logs_endpoint_returns_seeded_logs() -> None: - client, _ = build_client() - - response = client.get("/api/v1/audit-logs") - - assert response.status_code == 200 - payload = response.json() - assert payload - assert any(item["action"] == "review_rule" for item in payload) +from __future__ import annotations + +from collections.abc import Generator + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.api.deps import get_db +from app.core.agent_enums import AgentAssetStatus +from app.db.base import Base +from app.main import create_app +from app.services.agent_assets import AgentAssetService + + +def build_client() -> tuple[TestClient, sessionmaker[Session]]: + engine = create_engine( + "sqlite+pysqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + app = create_app() + + def override_db() -> Generator[Session, None, None]: + db = session_factory() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_db + return TestClient(app), session_factory + + +def test_list_agent_assets_endpoint_returns_seeded_items() -> None: + client, _ = build_client() + + response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) + + assert response.status_code == 200 + payload = response.json() + assert payload + assert all(item["asset_type"] == "rule" for item in payload) + assert any(item["code"] == "rule.expense.travel_risk_control_standard" for item in payload) + + +def test_get_agent_asset_detail_endpoint_returns_version_history() -> None: + client, _ = build_client() + + list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) + asset_id = next( + item["id"] + for item in list_response.json() + if item["code"] == "rule.expense.travel_risk_control_standard" + ) + + response = client.get(f"/api/v1/agent-assets/{asset_id}") + + assert response.status_code == 200 + payload = response.json() + assert payload["recent_versions"] + assert payload["current_version_content_type"] == "markdown" + assert payload["current_version"] == "v1.1.0" + assert "行程闭环" in payload["current_version_content"] + + +def test_activate_pending_rule_endpoint_is_blocked() -> None: + client, session_factory = build_client() + + with session_factory() as db: + pending_rule = next( + item + for item in AgentAssetService(db).list_assets(asset_type="rule") + if item.status == AgentAssetStatus.REVIEW.value + ) + + response = client.post( + f"/api/v1/agent-assets/{pending_rule.id}/activate", + headers={"x-actor": "pytest"}, + ) + + assert response.status_code == 400 + assert "审核" in response.json()["detail"] + + +def test_list_audit_logs_endpoint_returns_seeded_logs() -> None: + client, _ = build_client() + + response = client.get("/api/v1/audit-logs") + + assert response.status_code == 200 + payload = response.json() + assert payload + assert any(item["action"] == "review_rule" for item in payload) diff --git a/server/tests/test_ontology_service.py b/server/tests/test_ontology_service.py index 9c80f4f..55a855b 100644 --- a/server/tests/test_ontology_service.py +++ b/server/tests/test_ontology_service.py @@ -1,529 +1,529 @@ -from __future__ import annotations - -from collections.abc import Generator - -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import Session, sessionmaker -from sqlalchemy.pool import StaticPool - -from app.api.deps import get_db -from app.db.base import Base -from app.main import create_app -from app.schemas.ontology import OntologyParseRequest -from app.services.ontology import LlmOntologyParseResult, SemanticOntologyService - - -def build_session_factory() -> sessionmaker[Session]: - engine = create_engine( - "sqlite+pysqlite:///:memory:", - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - ) - Base.metadata.create_all(bind=engine) - return sessionmaker(bind=engine, autoflush=False, autocommit=False) - - -def build_client() -> tuple[TestClient, sessionmaker[Session]]: - session_factory = build_session_factory() - app = create_app() - - def override_db() -> Generator[Session, None, None]: - db = session_factory() - try: - yield db - finally: - db.close() - - app.dependency_overrides[get_db] = override_db - return TestClient(app), session_factory - - -EVALUATION_CASES = [ - pytest.param( - "查一下本周报销超标风险", - "expense", - "risk_check", - "read", - {}, - id="expense-risk-check", - ), - pytest.param( - "张三 4 月差旅报销金额是多少", - "expense", - "query", - "read", - {}, - id="expense-query-employee-month", - ), - pytest.param( - "为什么酒店超标报销不能直接通过", - "expense", - "explain", - "read", - {}, - id="expense-explain-policy", - ), - pytest.param( - "列出金额最高的10笔报销", - "expense", - "query", - "read", - {}, - id="expense-topn-query", - ), - pytest.param( - "帮我生成张三4月差旅报销草稿", - "expense", - "draft", - "draft_write", - {}, - id="expense-draft", - ), - pytest.param( - "我今天去客户现场,招待了客户,花销了1000元", - "expense", - "draft", - "draft_write", - {}, - id="expense-narrative-draft", - ), - pytest.param( - "客户 A 这个月还有多少应收", - "accounts_receivable", - "query", - "read", - {}, - id="ar-query-customer-month", - ), - pytest.param( - "对比客户A和客户B本月应收差异", - "accounts_receivable", - "compare", - "read", - {}, - id="ar-compare-customers", - ), - pytest.param( - "检查客户B逾期应收风险", - "accounts_receivable", - "risk_check", - "read", - {}, - id="ar-risk-check", - ), - pytest.param( - "生成客户A回款跟进草稿", - "accounts_receivable", - "draft", - "draft_write", - {}, - id="ar-draft", - ), - pytest.param( - "查询客户B账龄明细", - "accounts_receivable", - "query", - "read", - {}, - id="ar-aging-query", - ), - pytest.param( - "供应商 B 明天要付多少钱", - "accounts_payable", - "query", - "read", - {}, - id="ap-query-vendor-tomorrow", - ), - pytest.param( - "对比供应商A和供应商B本月应付差异", - "accounts_payable", - "compare", - "read", - {}, - id="ap-compare-vendors", - ), - pytest.param( - "检查供应商B逾期付款风险", - "accounts_payable", - "risk_check", - "read", - {}, - id="ap-risk-check", - ), - pytest.param( - "生成供应商A付款沟通草稿", - "accounts_payable", - "draft", - "draft_write", - {}, - id="ap-draft", - ), - pytest.param( - "帮我安排付款给供应商B", - "accounts_payable", - "operate", - "approval_required", - {"role_codes": ["finance"]}, - id="ap-operate-approval-required", - ), - pytest.param( - "公司财务制度在哪里看", - "knowledge", - "query", - "read", - {}, - id="knowledge-query", - ), - pytest.param( - "规则中心的审核依据是什么", - "knowledge", - "explain", - "read", - {}, - id="knowledge-explain", - ), - pytest.param( - "知识库里有没有双人复核制度", - "knowledge", - "query", - "read", - {}, - id="knowledge-query-library", - ), - pytest.param( - "帮我直接付款给供应商B", - "accounts_payable", - "operate", - "forbidden", - {"role_codes": ["user"]}, - id="forbidden-direct-payment", - ), - pytest.param( - "帮我上线付款双人复核规则", - "accounts_payable", - "operate", - "forbidden", - {"role_codes": ["user"]}, - id="forbidden-activate-rule", - ), - pytest.param( - "帮我删除今天的报销记录", - "expense", - "operate", - "forbidden", - {"role_codes": ["user"]}, - id="forbidden-delete-expense", - ), -] - - -@pytest.mark.parametrize("query,scenario,intent,permission,context_json", EVALUATION_CASES) -def test_semantic_ontology_service_matches_day3_evaluation_set( - query: str, - scenario: str, - intent: str, - permission: str, - context_json: dict, -) -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query=query, - user_id="pytest", - context_json=context_json, - ) - ) - - assert result.scenario == scenario - assert result.intent == intent - assert result.permission.level == permission - assert result.run_id.startswith("run_") - - -def test_semantic_ontology_service_extracts_entities_time_and_constraints() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="张三 2026年4月差旅报销金额超过5000元的明细", - user_id="pytest", - ) - ) - - assert result.scenario == "expense" - assert result.intent == "query" - assert result.time_range.start_date == "2026-04-01" - assert result.time_range.end_date == "2026-04-30" - - -def test_semantic_ontology_service_treats_travel_amount_question_as_knowledge_query() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="我要去武汉出差3天,请问我一共可以报销多少费用?", - user_id="pytest", - context_json={ - "role_codes": ["employee"], - "name": "曹笑竹", - "grade": "P3", - "session_type": "knowledge", - }, - ) - ) - - assert result.scenario == "knowledge" - assert result.intent == "query" - assert result.clarification_required is False - assert result.clarification_question is None - assert result.missing_slots == [] - - -def test_semantic_ontology_service_keeps_travel_amount_follow_up_in_knowledge_query() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="那P4员工可以报销多少钱?", - user_id="pytest", - context_json={ - "role_codes": ["employee"], - "name": "曹笑竹", - "grade": "P3", - "session_type": "knowledge", - "conversation_history": [ - { - "role": "user", - "content": "我要去武汉出差3天,请问我一共可以报销多少费用?", - } - ], - }, - ) - ) - - assert result.scenario == "knowledge" - assert result.intent == "query" - assert result.clarification_required is False - - -def test_semantic_ontology_service_rejects_draft_intent_inside_knowledge_session(monkeypatch) -> None: - session_factory = build_session_factory() - with session_factory() as db: - service = SemanticOntologyService(db) - monkeypatch.setattr( - service, - "_parse_with_model", - lambda **kwargs: LlmOntologyParseResult( - scenario="expense", - intent="draft", - confidence=0.91, - clarification_required=True, - clarification_question="请补充招待对象和票据附件。", - missing_slots=["participants", "attachments"], - ambiguity=[], - entity_hints=[], - ), - ) - - result = service.parse( - OntologyParseRequest( - query="我要去北京出差3天,一共可以报销多少钱?", - user_id="pytest", - context_json={ - "role_codes": ["employee"], - "name": "曹笑竹", - "grade": "P3", - "session_type": "knowledge", - }, - ) - ) - - assert result.scenario == "knowledge" - assert result.intent == "query" - assert result.clarification_required is False - assert result.clarification_question is None - - -def test_semantic_ontology_service_prefers_expense_for_customer_entertainment_narrative() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="我今天去客户现场,招待了客户,花销了1000元", - user_id="pytest", - ) - ) - - assert result.scenario == "expense" - assert result.intent == "draft" - assert result.permission.level == "draft_write" - assert result.time_range.raw == "今天" - assert result.clarification_required is True - assert "customer_name" in result.missing_slots - assert "participants" in result.missing_slots - assert any( - item.type == "expense_type" and item.normalized_value == "entertainment" - for item in result.entities - ) - - -def test_semantic_ontology_service_uses_client_local_date_for_relative_time() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="我昨天请客户吃饭花了200元", - user_id="pytest", - context_json={ - "client_now_iso": "2026-05-12T16:30:00.000Z", - "client_timezone_offset_minutes": -480, - }, - ) - ) - - assert result.time_range.raw == "昨天" - assert result.time_range.start_date == "2026-05-12" - assert result.time_range.end_date == "2026-05-12" - - -def test_semantic_ontology_service_extracts_day_before_yesterday_from_client_local_date() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="我前天请客户吃饭花了200元", - user_id="pytest", - context_json={ - "client_now_iso": "2026-05-12T16:30:00.000Z", - "client_timezone_offset_minutes": -480, - }, - ) - ) - - assert result.time_range.raw == "前天" - assert result.time_range.start_date == "2026-05-11" - assert result.time_range.end_date == "2026-05-11" - - -def test_semantic_ontology_service_maps_office_supplies_to_office_expense_type() -> None: - session_factory = build_session_factory() - with session_factory() as db: - result = SemanticOntologyService(db).parse( - OntologyParseRequest( - query="我买了办公用品和文具,花了88元,帮我报销", - user_id="pytest", - ) - ) - - assert result.scenario == "expense" - assert result.intent == "draft" - assert any( - item.type == "expense_type" and item.normalized_value == "office" - for item in result.entities - ) - - -def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None: - session_factory = build_session_factory() - with session_factory() as db: - service = SemanticOntologyService(db) - monkeypatch.setattr( - service, - "_parse_with_model", - lambda **kwargs: LlmOntologyParseResult( - scenario="expense", - intent="draft", - confidence=0.91, - clarification_required=True, - clarification_question="请补充费用类型、金额和票据附件。", - missing_slots=["expense_type", "amount", "attachments"], - ambiguity=[], - entity_hints=[], - ), - ) - - result = service.parse( - OntologyParseRequest( - query="我要报销", - user_id="pytest", - ) - ) - - assert result.scenario == "expense" - assert result.intent == "draft" - assert result.parse_strategy == "llm_primary" - assert result.clarification_required is True - assert "expense_type" in result.missing_slots - assert result.clarification_question == "请补充费用类型、金额和票据附件。" - - -def test_parse_ontology_endpoint_returns_eight_fields_and_writes_trace() -> None: - client, _ = build_client() - - response = client.post( - "/api/v1/ontology/parse", - json={ - "query": "查一下本周报销超标风险", - "user_id": "pytest", - "context_json": {"role_codes": ["finance"]}, - }, - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["scenario"] == "expense" - assert payload["intent"] == "risk_check" - assert payload["permission"]["level"] == "read" - assert payload["run_id"].startswith("run_") - assert set(payload) >= { - "scenario", - "intent", - "entities", - "time_range", - "metrics", - "constraints", - "risk_flags", - "permission", - "confidence", - "missing_slots", - "ambiguity", - "parse_strategy", - "clarification_required", - "clarification_question", - "run_id", - "field_errors", - } - - run_response = client.get(f"/api/v1/agent-runs/{payload['run_id']}") - - assert run_response.status_code == 200 - run_payload = run_response.json() - assert run_payload["ontology_json"]["scenario"] == "expense" - assert run_payload["ontology_json"]["intent"] == "risk_check" - assert run_payload["semantic_parse"]["scenario"] == "expense" - assert run_payload["semantic_parse"]["intent"] == "risk_check" - - -def test_parse_ontology_endpoint_returns_forbidden_for_unprivileged_payment_request() -> None: - client, _ = build_client() - - response = client.post( - "/api/v1/ontology/parse", - json={ - "query": "帮我直接付款给供应商B", - "user_id": "pytest", - "context_json": {"role_codes": ["user"]}, - }, - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["scenario"] == "accounts_payable" - assert payload["intent"] == "operate" - assert payload["permission"]["level"] == "forbidden" - assert payload["clarification_required"] is True - assert payload["field_errors"] +from __future__ import annotations + +from collections.abc import Generator + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.api.deps import get_db +from app.db.base import Base +from app.main import create_app +from app.schemas.ontology import OntologyParseRequest +from app.services.ontology import LlmOntologyParseResult, SemanticOntologyService + + +def build_session_factory() -> sessionmaker[Session]: + engine = create_engine( + "sqlite+pysqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + return sessionmaker(bind=engine, autoflush=False, autocommit=False) + + +def build_client() -> tuple[TestClient, sessionmaker[Session]]: + session_factory = build_session_factory() + app = create_app() + + def override_db() -> Generator[Session, None, None]: + db = session_factory() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_db + return TestClient(app), session_factory + + +EVALUATION_CASES = [ + pytest.param( + "查一下本周报销超标风险", + "expense", + "risk_check", + "read", + {}, + id="expense-risk-check", + ), + pytest.param( + "张三 4 月差旅报销金额是多少", + "expense", + "query", + "read", + {}, + id="expense-query-employee-month", + ), + pytest.param( + "为什么酒店超标报销不能直接通过", + "expense", + "explain", + "read", + {}, + id="expense-explain-policy", + ), + pytest.param( + "列出金额最高的10笔报销", + "expense", + "query", + "read", + {}, + id="expense-topn-query", + ), + pytest.param( + "帮我生成张三4月差旅报销草稿", + "expense", + "draft", + "draft_write", + {}, + id="expense-draft", + ), + pytest.param( + "我今天去客户现场,招待了客户,花销了1000元", + "expense", + "draft", + "draft_write", + {}, + id="expense-narrative-draft", + ), + pytest.param( + "客户 A 这个月还有多少应收", + "accounts_receivable", + "query", + "read", + {}, + id="ar-query-customer-month", + ), + pytest.param( + "对比客户A和客户B本月应收差异", + "accounts_receivable", + "compare", + "read", + {}, + id="ar-compare-customers", + ), + pytest.param( + "检查客户B逾期应收风险", + "accounts_receivable", + "risk_check", + "read", + {}, + id="ar-risk-check", + ), + pytest.param( + "生成客户A回款跟进草稿", + "accounts_receivable", + "draft", + "draft_write", + {}, + id="ar-draft", + ), + pytest.param( + "查询客户B账龄明细", + "accounts_receivable", + "query", + "read", + {}, + id="ar-aging-query", + ), + pytest.param( + "供应商 B 明天要付多少钱", + "accounts_payable", + "query", + "read", + {}, + id="ap-query-vendor-tomorrow", + ), + pytest.param( + "对比供应商A和供应商B本月应付差异", + "accounts_payable", + "compare", + "read", + {}, + id="ap-compare-vendors", + ), + pytest.param( + "检查供应商B逾期付款风险", + "accounts_payable", + "risk_check", + "read", + {}, + id="ap-risk-check", + ), + pytest.param( + "生成供应商A付款沟通草稿", + "accounts_payable", + "draft", + "draft_write", + {}, + id="ap-draft", + ), + pytest.param( + "帮我安排付款给供应商B", + "accounts_payable", + "operate", + "approval_required", + {"role_codes": ["finance"]}, + id="ap-operate-approval-required", + ), + pytest.param( + "公司财务制度在哪里看", + "knowledge", + "query", + "read", + {}, + id="knowledge-query", + ), + pytest.param( + "规则中心的审核依据是什么", + "knowledge", + "explain", + "read", + {}, + id="knowledge-explain", + ), + pytest.param( + "知识库里有没有双人复核制度", + "knowledge", + "query", + "read", + {}, + id="knowledge-query-library", + ), + pytest.param( + "帮我直接付款给供应商B", + "accounts_payable", + "operate", + "forbidden", + {"role_codes": ["user"]}, + id="forbidden-direct-payment", + ), + pytest.param( + "帮我上线付款双人复核规则", + "accounts_payable", + "operate", + "forbidden", + {"role_codes": ["user"]}, + id="forbidden-activate-rule", + ), + pytest.param( + "帮我删除今天的报销记录", + "expense", + "operate", + "forbidden", + {"role_codes": ["user"]}, + id="forbidden-delete-expense", + ), +] + + +@pytest.mark.parametrize("query,scenario,intent,permission,context_json", EVALUATION_CASES) +def test_semantic_ontology_service_matches_day3_evaluation_set( + query: str, + scenario: str, + intent: str, + permission: str, + context_json: dict, +) -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query=query, + user_id="pytest", + context_json=context_json, + ) + ) + + assert result.scenario == scenario + assert result.intent == intent + assert result.permission.level == permission + assert result.run_id.startswith("run_") + + +def test_semantic_ontology_service_extracts_entities_time_and_constraints() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="张三 2026年4月差旅报销金额超过5000元的明细", + user_id="pytest", + ) + ) + + assert result.scenario == "expense" + assert result.intent == "query" + assert result.time_range.start_date == "2026-04-01" + assert result.time_range.end_date == "2026-04-30" + + +def test_semantic_ontology_service_treats_travel_amount_question_as_knowledge_query() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="我要去武汉出差3天,请问我一共可以报销多少费用?", + user_id="pytest", + context_json={ + "role_codes": ["employee"], + "name": "曹笑竹", + "grade": "P3", + "session_type": "knowledge", + }, + ) + ) + + assert result.scenario == "knowledge" + assert result.intent == "query" + assert result.clarification_required is False + assert result.clarification_question is None + assert result.missing_slots == [] + + +def test_semantic_ontology_service_keeps_travel_amount_follow_up_in_knowledge_query() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="那P4员工可以报销多少钱?", + user_id="pytest", + context_json={ + "role_codes": ["employee"], + "name": "曹笑竹", + "grade": "P3", + "session_type": "knowledge", + "conversation_history": [ + { + "role": "user", + "content": "我要去武汉出差3天,请问我一共可以报销多少费用?", + } + ], + }, + ) + ) + + assert result.scenario == "knowledge" + assert result.intent == "query" + assert result.clarification_required is False + + +def test_semantic_ontology_service_rejects_draft_intent_inside_knowledge_session(monkeypatch) -> None: + session_factory = build_session_factory() + with session_factory() as db: + service = SemanticOntologyService(db) + monkeypatch.setattr( + service, + "_parse_with_model", + lambda **kwargs: LlmOntologyParseResult( + scenario="expense", + intent="draft", + confidence=0.91, + clarification_required=True, + clarification_question="请补充招待对象和票据附件。", + missing_slots=["participants", "attachments"], + ambiguity=[], + entity_hints=[], + ), + ) + + result = service.parse( + OntologyParseRequest( + query="我要去北京出差3天,一共可以报销多少钱?", + user_id="pytest", + context_json={ + "role_codes": ["employee"], + "name": "曹笑竹", + "grade": "P3", + "session_type": "knowledge", + }, + ) + ) + + assert result.scenario == "knowledge" + assert result.intent == "query" + assert result.clarification_required is False + assert result.clarification_question is None + + +def test_semantic_ontology_service_prefers_expense_for_customer_entertainment_narrative() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="我今天去客户现场,招待了客户,花销了1000元", + user_id="pytest", + ) + ) + + assert result.scenario == "expense" + assert result.intent == "draft" + assert result.permission.level == "draft_write" + assert result.time_range.raw == "今天" + assert result.clarification_required is True + assert "customer_name" in result.missing_slots + assert "participants" in result.missing_slots + assert any( + item.type == "expense_type" and item.normalized_value == "entertainment" + for item in result.entities + ) + + +def test_semantic_ontology_service_uses_client_local_date_for_relative_time() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="我昨天请客户吃饭花了200元", + user_id="pytest", + context_json={ + "client_now_iso": "2026-05-12T16:30:00.000Z", + "client_timezone_offset_minutes": -480, + }, + ) + ) + + assert result.time_range.raw == "昨天" + assert result.time_range.start_date == "2026-05-12" + assert result.time_range.end_date == "2026-05-12" + + +def test_semantic_ontology_service_extracts_day_before_yesterday_from_client_local_date() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="我前天请客户吃饭花了200元", + user_id="pytest", + context_json={ + "client_now_iso": "2026-05-12T16:30:00.000Z", + "client_timezone_offset_minutes": -480, + }, + ) + ) + + assert result.time_range.raw == "前天" + assert result.time_range.start_date == "2026-05-11" + assert result.time_range.end_date == "2026-05-11" + + +def test_semantic_ontology_service_maps_office_supplies_to_office_expense_type() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query="我买了办公用品和文具,花了88元,帮我报销", + user_id="pytest", + ) + ) + + assert result.scenario == "expense" + assert result.intent == "draft" + assert any( + item.type == "expense_type" and item.normalized_value == "office" + for item in result.entities + ) + + +def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None: + session_factory = build_session_factory() + with session_factory() as db: + service = SemanticOntologyService(db) + monkeypatch.setattr( + service, + "_parse_with_model", + lambda **kwargs: LlmOntologyParseResult( + scenario="expense", + intent="draft", + confidence=0.91, + clarification_required=True, + clarification_question="请补充费用类型、金额和票据附件。", + missing_slots=["expense_type", "amount", "attachments"], + ambiguity=[], + entity_hints=[], + ), + ) + + result = service.parse( + OntologyParseRequest( + query="我要报销", + user_id="pytest", + ) + ) + + assert result.scenario == "expense" + assert result.intent == "draft" + assert result.parse_strategy == "llm_primary" + assert result.clarification_required is True + assert "expense_type" in result.missing_slots + assert result.clarification_question == "请补充费用类型、金额和票据附件。" + + +def test_parse_ontology_endpoint_returns_eight_fields_and_writes_trace() -> None: + client, _ = build_client() + + response = client.post( + "/api/v1/ontology/parse", + json={ + "query": "查一下本周报销超标风险", + "user_id": "pytest", + "context_json": {"role_codes": ["finance"]}, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["scenario"] == "expense" + assert payload["intent"] == "risk_check" + assert payload["permission"]["level"] == "read" + assert payload["run_id"].startswith("run_") + assert set(payload) >= { + "scenario", + "intent", + "entities", + "time_range", + "metrics", + "constraints", + "risk_flags", + "permission", + "confidence", + "missing_slots", + "ambiguity", + "parse_strategy", + "clarification_required", + "clarification_question", + "run_id", + "field_errors", + } + + run_response = client.get(f"/api/v1/agent-runs/{payload['run_id']}") + + assert run_response.status_code == 200 + run_payload = run_response.json() + assert run_payload["ontology_json"]["scenario"] == "expense" + assert run_payload["ontology_json"]["intent"] == "risk_check" + assert run_payload["semantic_parse"]["scenario"] == "expense" + assert run_payload["semantic_parse"]["intent"] == "risk_check" + + +def test_parse_ontology_endpoint_returns_forbidden_for_unprivileged_payment_request() -> None: + client, _ = build_client() + + response = client.post( + "/api/v1/ontology/parse", + json={ + "query": "帮我直接付款给供应商B", + "user_id": "pytest", + "context_json": {"role_codes": ["user"]}, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["scenario"] == "accounts_payable" + assert payload["intent"] == "operate" + assert payload["permission"]["level"] == "forbidden" + assert payload["clarification_required"] is True + assert payload["field_errors"] diff --git a/web/UI/流程输入.png b/web/UI/流程输入.png new file mode 100644 index 0000000..f480b9c Binary files /dev/null and b/web/UI/流程输入.png differ diff --git a/web/src/assets/styles/views/audit-view.css b/web/src/assets/styles/views/audit-view.css index ca64c7b..e28a9ab 100644 --- a/web/src/assets/styles/views/audit-view.css +++ b/web/src/assets/styles/views/audit-view.css @@ -1,2726 +1,2726 @@ -.skill-center { - height: 100%; - min-height: 0; -} - -.skill-view-enter-active, -.skill-view-leave-active { - transition: opacity 220ms ease, transform 300ms var(--ease); -} - -.skill-view-enter-from, -.skill-view-leave-to { - opacity: 0; - transform: translateY(14px); -} - -.skill-list, -.skill-detail { - height: 100%; - min-height: 0; -} - -.skill-detail { - display: grid; - grid-template-rows: minmax(0, 1fr) auto; - gap: 12px; -} - -.skill-detail.spreadsheet-skill-detail { - gap: 10px; -} - -.skill-list { - display: flex; - flex-direction: column; - min-height: 0; - padding: 18px 20px; - overflow: hidden; -} - -.status-tabs { - display: flex; - gap: 18px; - padding-bottom: 12px; - border-bottom: 1px solid #edf2f7; -} - -.status-tabs button { - position: relative; - border: 0; - background: transparent; - color: #64748b; - font-size: 14px; - font-weight: 760; -} - -.status-tabs button.active { - color: #0f172a; -} - -.status-tabs button.active::after { - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: -13px; - height: 3px; - border-radius: 999px; - background: #10b981; -} - -.list-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 14px 0 10px; -} - -.filter-set { - display: flex; - align-items: center; - gap: 10px; - flex: 1 1 auto; - flex-wrap: wrap; -} - -.search-filter { - width: 280px; - min-height: 38px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 11px; - border: 1px solid #d7e0ea; - border-radius: 8px; - background: #fff; - color: #64748b; -} - -.search-filter i { - color: #94a3b8; - font-size: 16px; -} - -.search-filter input { - width: 100%; - min-width: 0; - border: 0; - outline: 0; - background: transparent; - color: #0f172a; - font-size: 13px; -} - -.search-filter input::placeholder { - color: #94a3b8; -} - -.search-filter:focus-within { - border-color: rgba(16, 185, 129, 0.48); - box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); -} - -.picker-trigger, -.ghost-filter-btn, -.create-btn, -.row-action { - min-height: 38px; - border-radius: 8px; - font-size: 13px; - font-weight: 760; -} - -.picker-filter { - position: relative; -} - -.picker-trigger { - min-width: 124px; - display: inline-flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 0 34px 0 12px; - border: 1px solid #d7e0ea; - background: #fff; - color: #334155; - white-space: nowrap; -} - -.picker-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.picker-trigger .mdi { - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - color: #64748b; - pointer-events: none; -} - -.picker-trigger:hover, -.picker-filter.open .picker-trigger { - border-color: rgba(16, 185, 129, 0.34); - background: #f6fffb; - color: #0f9f78; -} - -.picker-popover { - position: absolute; - top: calc(100% + 8px); - left: 0; - width: 224px; - z-index: 40; - display: grid; - gap: 14px; - padding: 16px; - border: 1px solid #d7e0ea; - border-radius: 12px; - background: #fff; - box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16); -} - -.picker-popover header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.picker-popover header strong { - color: #0f172a; - font-size: 15px; -} - -.picker-popover header button { - width: 30px; - height: 30px; - display: grid; - place-items: center; - border: 0; - border-radius: 8px; - background: transparent; - color: #64748b; -} - -.picker-popover header button:hover { - background: #f1f5f9; - color: #0f172a; -} - -.picker-option-list { - display: grid; - gap: 8px; - max-height: 240px; - overflow-y: auto; -} - -.picker-option { - min-height: 36px; - display: inline-flex; - align-items: center; - padding: 0 12px; - border: 1px solid #d7e0ea; - border-radius: 8px; - background: #fff; - color: #334155; - font-size: 13px; - font-weight: 750; - text-align: left; -} - -.picker-option:hover, -.picker-option.active { - border-color: rgba(16, 185, 129, 0.32); - background: rgba(16, 185, 129, 0.08); - color: #059669; -} - -.toolbar-actions { - display: flex; - align-items: center; - gap: 10px; -} - -.ghost-filter-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 12px; - border: 1px solid #d7e0ea; - background: #fff; - color: #475569; -} - -.ghost-filter-btn:hover { - border-color: rgba(16, 185, 129, 0.28); - color: #059669; -} - -.create-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 14px; - border: 0; - background: #059669; - color: #fff; - box-shadow: 0 8px 18px rgba(5, 150, 105, 0.18); -} - -.create-btn:disabled { - background: #cbd5e1; - color: #f8fafc; - box-shadow: none; - cursor: not-allowed; -} - -.hint { - display: inline-flex; - align-items: center; - gap: 6px; - margin: 0 0 12px; - color: #64748b; - font-size: 12px; -} - -.hint .mdi { - color: #94a3b8; -} - -.active-filter-strip { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.active-filter-chip { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: rgba(16, 185, 129, 0.1); - color: #047857; - font-size: 12px; - font-weight: 800; -} - -.table-wrap { - flex: 1 1 auto; - position: relative; - min-height: 400px; - overflow: auto; - border: 1px solid #edf2f7; - border-radius: 12px; - background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%); - display: flex; - flex-direction: column; - align-items: stretch; - justify-content: flex-start; -} - -.table-wrap.is-empty { - align-items: center; - justify-content: center; -} - -.table-wrap table { - width: 100%; - align-self: flex-start; -} - -.table-state, -.detail-inline-state { - width: 100%; - min-height: 220px; - display: grid; - place-items: center; - gap: 12px; - padding: 28px 24px; - text-align: center; - color: #64748b; - align-self: center; -} - -.detail-inline-state { - min-height: 180px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - text-align: left; -} - -.table-state i, -.detail-inline-state i { - font-size: 28px; - color: #94a3b8; -} - -.table-state.error i, -.detail-inline-state.error i { - color: #dc2626; -} - -.table-state.empty i { - color: #0ea5e9; -} - -.table-state p, -.detail-inline-state p { - margin-top: 6px; - font-size: 13px; - line-height: 1.6; -} - -.detail-inline-state strong { - color: #0f172a; - font-size: 14px; - font-weight: 850; -} - -.detail-inline-state > div { - flex: 1 1 auto; -} - -.state-action { - height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 14px; - border: 1px solid rgba(16, 185, 129, 0.28); - border-radius: 8px; - background: #fff; - color: #059669; - font-size: 13px; - font-weight: 760; -} - -.list-foot { - display: flex; - align-items: center; - justify-content: flex-end; - padding-top: 12px; -} - -.page-summary { - color: #64748b; - font-size: 12px; - font-weight: 700; -} - -table { - width: 100%; - min-width: 1120px; - border-collapse: collapse; -} - -th, -td { - padding: 14px 12px; - border-bottom: 1px solid #edf2f7; - text-align: center; - vertical-align: middle; - color: #334155; - font-size: 12px; -} - -th { - background: #f8fafc; - color: #64748b; - font-weight: 800; - white-space: nowrap; -} - -tbody tr { - cursor: pointer; - transition: background 180ms ease; -} - -th:first-child, -td:first-child { - text-align: left; -} - -tbody tr:hover { - background: #f8fbff; -} - -tbody tr.spotlight { - background: linear-gradient(90deg, rgba(16, 185, 129, 0.05), rgba(59, 130, 246, 0.03)); -} - -.skill-name-cell { - display: grid; - grid-template-columns: 38px minmax(0, 1fr); - gap: 10px; - align-items: center; -} - -.skill-avatar { - width: 38px; - height: 38px; - display: grid; - place-items: center; - border-radius: 11px; - color: #fff; - font-size: 13px; - font-weight: 900; -} - -.skill-avatar.emerald { background: linear-gradient(135deg, #10b981, #059669); } -.skill-avatar.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); } -.skill-avatar.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } -.skill-avatar.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); } -.skill-avatar.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); } - -.skill-name-cell strong { - display: block; - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.skill-name-cell span:last-child { - display: block; - margin-top: 4px; - color: #64748b; - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.scope-pill, -.status-pill { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 26px; - padding: 0 9px; - border-radius: 999px; - font-size: 11px; - font-weight: 800; - white-space: nowrap; -} - -.scope-pill { - background: #f1f5f9; - color: #475569; -} - -.status-pill.success { - background: #dcfce7; - color: #059669; -} - -.status-pill.warning { - background: #fff7ed; - color: #ea580c; -} - -.status-pill.draft { - background: #eef2ff; - color: #6366f1; -} - -.status-pill.danger { - background: #fee2e2; - color: #dc2626; -} - -.status-pill.disabled { - background: #e2e8f0; - color: #475569; -} - -.row-action { - padding: 0 12px; - border: 1px solid rgba(16, 185, 129, 0.32); - background: #fff; - color: #059669; -} - -.row-action:hover { - background: rgba(16, 185, 129, 0.08); -} - -.detail-scroll { - height: 100%; - overflow: auto; - display: grid; - align-content: start; - gap: 16px; -} - -.spreadsheet-skill-detail .detail-scroll { - min-height: 0; - overflow: hidden; - align-content: stretch; - grid-template-rows: minmax(0, 1fr); -} - -.detail-hero { - display: grid; - grid-template-columns: minmax(0, 1fr) 320px; - align-items: start; - gap: 10px; - padding: 16px 20px; -} - -.hero-title { - min-width: 0; -} - -.skill-badge { - display: inline-flex; - align-items: center; - min-height: 24px; - padding: 0 8px; - border-radius: 999px; - color: #fff; - font-size: 11px; - font-weight: 800; -} - -.skill-badge.emerald { background: linear-gradient(135deg, #10b981, #059669); } -.skill-badge.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); } -.skill-badge.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } -.skill-badge.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); } -.skill-badge.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); } - -.hero-title h2 { - margin-top: 10px; - color: #0f172a; - font-size: 24px; - font-weight: 850; -} - -.hero-title p { - margin-top: 8px; - max-width: 820px; - color: #64748b; - font-size: 14px; - line-height: 1.6; -} - -.hero-review-meta { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - margin-top: 12px; -} - -.inline-review-meta { - margin-top: 10px; -} - -.review-note-block { - display: grid; - gap: 6px; - margin-top: 12px; - padding: 12px 14px; - border: 1px solid rgba(16, 185, 129, 0.16); - border-radius: 12px; - background: linear-gradient(180deg, #f8fffc, #ffffff); -} - -.review-note-block strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.review-note-block p, -.review-note-block span { - color: #64748b; - font-size: 12px; - line-height: 1.6; -} - -.review-note-block.muted { - border-color: #e2e8f0; - background: #f8fafc; -} - -.hero-review-meta span { - display: inline-flex; - align-items: center; - gap: 5px; - min-height: 26px; - padding: 0 9px; - border-radius: 999px; - background: #f8fafc; - color: #475569; - font-size: 12px; - font-weight: 800; -} - -.hero-review-meta i { - color: #059669; - font-size: 15px; -} - -.hero-stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.hero-stat { - min-height: 54px; - display: grid; - align-content: center; - gap: 4px; - padding: 9px 12px; - border-radius: 10px; - background: #f8fafc; - border: 1px solid #e5eaf0; -} - -.hero-stat span { - display: block; - color: #64748b; - font-size: 11px; - font-weight: 800; - line-height: 1.2; -} - -.hero-stat strong { - display: block; - color: #0f172a; - font-size: 15px; - font-weight: 850; - line-height: 1.25; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.hero-stat .status-pill { - width: fit-content; - min-height: 22px; - padding: 0 8px; -} - -.detail-grid { - display: grid; - grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.78fr); - gap: 16px; -} - -.detail-grid.skill-md-detail-grid { - grid-template-columns: minmax(0, 1fr) 372px; - align-items: stretch; -} - -.detail-grid.skill-md-detail-grid .detail-main { - height: 100%; -} - -.detail-main, -.detail-side { - display: grid; - gap: 16px; - align-content: start; -} - -.detail-card, -.side-card { - padding: 18px; -} - -.card-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - margin-bottom: 16px; -} - -.card-head h3 { - color: #0f172a; - font-size: 16px; - font-weight: 850; -} - -.card-head p { - margin-top: 6px; - color: #64748b; - font-size: 13px; - line-height: 1.5; -} - -.edit-badge { - display: inline-flex; - align-items: center; - min-height: 28px; - padding: 0 10px; - border-radius: 999px; - background: #ecfeff; - color: #0891b2; - font-size: 12px; - font-weight: 800; -} - -.form-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.field { - display: grid; - gap: 7px; -} - -.field.span-2 { - grid-column: span 2; -} - -.field span { - color: #64748b; - font-size: 12px; - font-weight: 800; -} - -.field input, -.field textarea, -.prompt-block textarea { - width: 100%; - border: 1px solid #d7e0ea; - border-radius: 10px; - background: #fff; - color: #0f172a; - font-size: 13px; - line-height: 1.55; - padding: 10px 12px; - resize: vertical; -} - -.field input[readonly], -.field textarea[readonly], -.prompt-block textarea[readonly] { - background: #f8fafc; - color: #334155; -} - -.markdown-card { - min-height: 620px; - display: grid; - grid-template-rows: auto minmax(0, 1fr); -} - -.json-editor-card { - display: grid; - gap: 12px; -} - -.spreadsheet-rule-card { - display: grid; - gap: 14px; -} - -.spreadsheet-editor-shell { - min-height: 0; - height: 100%; - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; -} - -.spreadsheet-editor-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.spreadsheet-editor-title { - min-width: 0; - display: flex; - align-items: flex-start; - gap: 12px; -} - -.spreadsheet-editor-title h2 { - color: #0f172a; - font-size: 18px; - font-weight: 850; -} - -.spreadsheet-editor-title p { - margin-top: 2px; - max-width: 760px; - color: #64748b; - font-size: 12px; - line-height: 1.4; -} - -.spreadsheet-editor-actions { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.spreadsheet-editor-meta { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.spreadsheet-editor-meta span { - min-height: 28px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 10px; - border-radius: 999px; - background: #f8fafc; - color: #475569; - font-size: 12px; - font-weight: 750; -} - -.spreadsheet-editor-meta strong { - color: #0f172a; - font-weight: 850; -} - -.spreadsheet-editor-body { - flex: 1 1 auto; - min-height: 0; - display: grid; - grid-template-columns: minmax(0, 1fr) 286px; - gap: 14px; - align-items: start; -} - -.spreadsheet-main-stage { - min-height: 0; - height: 100%; - display: flex; - flex-direction: column; - gap: 10px; -} - -.spreadsheet-workbench { - flex: 1 1 auto; - position: relative; - min-height: 0; - overflow: visible; - border: 1px solid #dbe4ee; - border-radius: 12px; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -.spreadsheet-workbench .rule-spreadsheet-host { - min-height: 0; - height: 100%; - overflow: visible; -} - -.spreadsheet-editor-foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - color: #64748b; - font-size: 12px; - line-height: 1.5; -} - -.spreadsheet-version-center { - min-height: 0; - height: 100%; - align-self: stretch; - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - gap: 12px; - padding: 14px; - border: 1px solid #dbe4ee; - border-radius: 14px; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - overflow: hidden; -} - -.version-center-head h3, -.version-center-head p, -.version-center-section header, -.version-center-section p { - margin: 0; -} - -.version-center-head h3 { - color: #0f172a; - font-size: 15px; - font-weight: 900; -} - -.version-center-head p { - margin-top: 3px; - color: #64748b; - font-size: 12px; -} - -.version-pair-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.version-pair-card { - display: grid; - gap: 4px; - min-height: 84px; - padding: 10px; - border-radius: 12px; -} - -.version-pair-card span { - color: #64748b; - font-size: 11px; - font-weight: 800; -} - -.version-pair-card strong { - color: #0f172a; - font-size: 16px; - font-weight: 900; -} - -.version-pair-card b { - width: fit-content; - min-height: 20px; - display: inline-flex; - align-items: center; - padding: 0 7px; - border-radius: 999px; - font-size: 11px; - font-weight: 850; -} - -.version-pair-card.published { - background: rgba(16, 185, 129, 0.1); -} - -.version-pair-card.published b { - background: #dcfce7; - color: #059669; -} - -.version-pair-card.working { - background: rgba(37, 99, 235, 0.08); -} - -.version-pair-card.working b { - background: #dbeafe; - color: #2563eb; -} - -.version-center-section { - display: grid; - gap: 8px; -} - -.version-history-section { - min-height: 0; - grid-template-rows: auto minmax(0, 1fr); -} - -.version-center-section > header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.version-center-section > header strong { - color: #0f172a; - font-size: 13px; - font-weight: 900; -} - -.version-center-section > header small, -.version-center-section > header button { - color: #64748b; - font-size: 11px; - font-weight: 800; -} - -.version-center-section > header button { - padding: 0; - border: 0; - background: transparent; - color: #2563eb; - cursor: pointer; -} - -.version-center-list { - display: grid; - align-content: start; - gap: 8px; - min-height: 0; - overflow-y: auto; - padding-right: 2px; -} - -.version-center-item { - display: grid; - gap: 8px; - padding: 10px; - border: 1px solid #e2e8f0; - border-radius: 12px; - background: #fff; -} - -.version-center-item.active { - border-color: rgba(16, 185, 129, 0.35); - background: rgba(16, 185, 129, 0.05); -} - -.version-center-item > button { - display: grid; - gap: 5px; - padding: 0; - border: 0; - background: transparent; - text-align: left; - cursor: pointer; -} - -.version-center-item > button:disabled { - cursor: default; -} - -.version-center-item > button div { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.version-center-item > button strong { - color: #0f172a; - font-size: 13px; - font-weight: 900; -} - -.version-center-item > button span, -.version-center-item > button p, -.version-center-item > button small { - color: #64748b; - font-size: 11px; -} - -.version-center-item > button p { - margin: 0; - line-height: 1.45; -} - -.change-record-summary-empty { - color: #64748b; - font-size: 11px; - line-height: 1.5; -} - -.change-record-item { - gap: 10px; - width: 100%; - border: 1px solid #e2e8f0; - background: #fff; - color: inherit; - text-align: left; - cursor: pointer; - transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; -} - -.change-record-item:hover { - border-color: rgba(16, 185, 129, 0.35); - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); - transform: translateY(-1px); -} - -.change-record-item:focus-visible { - outline: 3px solid rgba(16, 185, 129, 0.18); - outline-offset: 1px; -} - -.change-record-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 10px; -} - -.change-record-head > div { - display: grid; - gap: 3px; -} - -.change-record-head strong { - color: #0f172a; - font-size: 13px; - font-weight: 900; -} - -.change-record-head span, -.change-record-item p, -.change-record-more { - color: #64748b; - font-size: 11px; -} - -.change-record-head b { - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - background: #eef2ff; - color: #4f46e5; - font-size: 11px; - font-weight: 850; -} - -.change-record-item p { - margin: 0; - line-height: 1.5; -} - -.change-record-preview { - display: grid; - gap: 6px; -} - -.change-record-preview span { - display: block; - padding: 7px 8px; - border-radius: 10px; - background: #f8fafc; - color: #334155; - font-size: 11px; - line-height: 1.45; - overflow-wrap: anywhere; -} - -.change-record-more { - font-weight: 800; -} - -.change-detail-content { - gap: 14px; -} - -.change-detail-meta { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.change-detail-meta article { - display: grid; - gap: 4px; - min-height: 0; - padding: 10px 12px; - border-radius: 14px; - background: #f8fafc; -} - -.change-detail-meta span { - color: #64748b; - font-size: 12px; -} - -.change-detail-meta strong { - color: #0f172a; - font-size: 15px; - font-weight: 900; -} - -.version-flow-empty { - color: #64748b; - font-size: 11px; -} - -.rule-drawer-backdrop { - position: fixed; - inset: 0; - z-index: 80; - display: flex; - justify-content: flex-end; - background: rgba(15, 23, 42, 0.34); - backdrop-filter: blur(3px); -} - -.rule-drawer { - width: min(860px, 78vw); - height: 100%; - display: grid; - grid-template-rows: auto auto minmax(0, 1fr); - gap: 18px; - padding: 22px; - overflow: auto; - background: #fff; - box-shadow: -18px 0 42px rgba(15, 23, 42, 0.18); -} - -.timeline-drawer { - width: min(640px, 62vw); -} - -.rule-drawer-head { - display: flex; - align-items: start; - justify-content: space-between; - gap: 16px; -} - -.rule-drawer-head span { - color: #2563eb; - font-size: 12px; - font-weight: 850; -} - -.rule-drawer-head h3 { - margin: 4px 0 0; - color: #0f172a; - font-size: 20px; - font-weight: 950; -} - -.rule-drawer-head button { - width: 34px; - height: 34px; - border: 0; - border-radius: 999px; - background: #f1f5f9; - color: #475569; - cursor: pointer; -} - -.rule-drawer-state { - min-height: 160px; - display: grid; - place-items: center; - gap: 10px; - color: #64748b; -} - -.rule-drawer-state.error { - color: #dc2626; -} - -.rule-timeline-list { - display: grid; - align-content: start; -} - -.rule-timeline-item { - position: relative; - display: grid; - grid-template-columns: 30px minmax(0, 1fr); - gap: 12px; - padding-bottom: 20px; -} - -.rule-timeline-item:not(:last-child)::after { - content: ''; - position: absolute; - left: 14px; - top: 28px; - bottom: 0; - width: 2px; - background: #e2e8f0; -} - -.rule-timeline-item > i { - position: relative; - z-index: 1; - width: 30px; - height: 30px; - display: grid; - place-items: center; - border-radius: 999px; - background: #f1f5f9; - color: #475569; -} - -.rule-timeline-item > i.success { - background: #dcfce7; - color: #059669; -} - -.rule-timeline-item > i.warning { - background: #fef3c7; - color: #d97706; -} - -.rule-timeline-item > i.danger { - background: #fee2e2; - color: #dc2626; -} - -.rule-timeline-item > i.info { - background: #dbeafe; - color: #2563eb; -} - -.rule-timeline-item header { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.rule-timeline-item header strong { - color: #0f172a; - font-size: 14px; -} - -.rule-timeline-item header b { - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - background: #f1f5f9; - color: #475569; - font-size: 11px; -} - -.rule-timeline-item header span, -.rule-timeline-item p, -.rule-timeline-item small { - color: #64748b; -} - -.rule-timeline-item p { - margin: 8px 0 4px; - line-height: 1.6; -} - -.compare-toolbar { - display: flex; - align-items: end; - gap: 12px; -} - -.compare-toolbar label { - min-width: 0; - display: grid; - gap: 6px; - flex: 1; -} - -.compare-toolbar span { - color: #64748b; - font-size: 12px; - font-weight: 850; -} - -.compare-toolbar select { - width: 100%; - min-height: 40px; - padding: 0 12px; - border: 1px solid #cbd5e1; - border-radius: 12px; - background: #fff; -} - -.compare-toolbar i { - margin-bottom: 10px; - color: #94a3b8; -} - -.compare-content { - min-height: 0; - display: grid; - grid-template-rows: auto auto minmax(0, 1fr); - gap: 18px; -} - -.compare-summary-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.compare-summary-grid article { - display: grid; - gap: 4px; - align-content: center; - min-height: 0; - padding: 10px 12px; - border-radius: 14px; - background: #f8fafc; -} - -.compare-summary-grid span { - color: #64748b; - font-size: 12px; -} - -.compare-summary-grid strong { - color: #0f172a; - font-size: 22px; - font-weight: 950; - line-height: 1.1; -} - -.compare-panel { - display: grid; - gap: 10px; -} - -.compare-cell-panel { - min-height: 0; - grid-template-rows: auto minmax(0, 1fr); -} - -.compare-panel header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.compare-panel header strong { - color: #0f172a; - font-size: 14px; -} - -.compare-panel p, -.compare-panel small { - color: #64748b; -} - -.compare-sheet-list { - display: grid; - gap: 8px; -} - -.compare-sheet-list article { - min-height: 40px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 0 12px; - border: 1px solid #e2e8f0; - border-radius: 12px; - background: #fff; -} - -.compare-sheet-list strong { - min-width: 0; - color: #0f172a; - font-size: 13px; - font-weight: 850; - overflow-wrap: anywhere; -} - -.compare-sheet-list b { - min-height: 24px; - display: inline-flex; - align-items: center; - flex: 0 0 auto; - padding: 0 9px; - border-radius: 999px; - font-size: 12px; - font-weight: 850; -} - -.compare-sheet-list b.success { - background: #dcfce7; - color: #059669; -} - -.compare-sheet-list b.danger { - background: #fee2e2; - color: #dc2626; -} - -.compare-table-wrap { - min-height: 0; - overflow: auto; - border: 1px solid #e2e8f0; - border-radius: 14px; -} - -.compare-table-wrap table { - width: 100%; - border-collapse: collapse; -} - -.compare-table-wrap th, -.compare-table-wrap td { - padding: 10px 12px; - border-bottom: 1px solid #eef2f7; - text-align: left; - vertical-align: top; -} - -.compare-table-wrap th { - color: #475569; - font-size: 12px; - background: #f8fafc; -} - -.compare-table-wrap td { - color: #0f172a; - font-size: 13px; -} - -.compare-table-wrap td b { - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - font-size: 11px; -} - -.compare-table-wrap td b.success { - background: #dcfce7; - color: #059669; -} - -.compare-table-wrap td b.warning { - background: #fef3c7; - color: #d97706; -} - -.compare-table-wrap td b.danger { - background: #fee2e2; - color: #dc2626; -} - -.review-submit-form { - display: grid; - gap: 12px; -} - -.review-submit-form label { - display: grid; - gap: 7px; -} - -.review-submit-form label span { - color: #475569; - font-size: 12px; - font-weight: 850; -} - -.review-submit-form input, -.review-submit-form select { - width: 100%; - min-height: 42px; - padding: 0 12px; - border: 1px solid #cbd5e1; - border-radius: 12px; - background: #fff; - color: #0f172a; - font-size: 14px; -} - -.review-submit-form input:focus, -.review-submit-form select:focus { - outline: 0; - border-color: rgba(16, 185, 129, 0.5); - box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12); -} - -.review-submit-hint { - margin: 0; - padding: 10px 12px; - border-radius: 12px; - background: #fff7ed; - color: #c2410c; - font-size: 12px; - line-height: 1.6; -} - -@media (max-width: 1280px) { - .spreadsheet-editor-body { - grid-template-columns: 1fr; - } -} - -@media (max-width: 900px) { - .rule-drawer, - .timeline-drawer { - width: 100%; - } - - .compare-summary-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .change-detail-meta { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -.rule-spreadsheet-toolbar { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.spreadsheet-mode-pill { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: #eff6ff; - color: #1d4ed8; - font-size: 12px; - font-weight: 800; -} - -.spreadsheet-upload-input { - display: none; -} - -.spreadsheet-meta-strip { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.spreadsheet-meta-strip span { - min-height: 28px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 10px; - border-radius: 999px; - background: #f8fafc; - color: #475569; - font-size: 12px; - font-weight: 750; -} - -.spreadsheet-meta-strip strong { - color: #0f172a; - font-weight: 850; -} - -.rule-spreadsheet-stage { - position: relative; - min-height: 720px; - overflow: hidden; - border: 1px solid #dbe4ee; - border-radius: 14px; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -.rule-spreadsheet-host { - width: 100%; - height: 100%; - min-height: 720px; -} - -.rule-spreadsheet-host.hidden { - visibility: hidden; -} - -.rule-spreadsheet-state { - position: absolute; - inset: 0; - display: grid; - place-items: center; - gap: 8px; - padding: 24px; - background: rgba(248, 250, 252, 0.94); - color: #475569; - font-size: 13px; - font-weight: 800; - text-align: center; -} - -.rule-spreadsheet-state i { - font-size: 22px; -} - -.rule-spreadsheet-state.error { - color: #dc2626; -} - -.preview-mode-note { - display: inline-flex; - align-items: center; - gap: 8px; - margin-top: 14px; - padding: 10px 12px; - border: 1px solid rgba(14, 165, 233, 0.22); - border-radius: 12px; - background: linear-gradient(180deg, rgba(240, 249, 255, 0.96), rgba(224, 242, 254, 0.9)); - color: #075985; - font-size: 12px; - font-weight: 760; - line-height: 1.5; -} - -.preview-mode-note i { - font-size: 16px; -} - -.markdown-card .field { - min-height: 0; - grid-template-rows: auto minmax(0, 1fr); -} - -.markdown-editor { - min-height: 520px; - height: 100%; - font-family: "Cascadia Mono", "Consolas", monospace; - font-size: 13px; - line-height: 1.65; - white-space: pre; -} - -.markdown-editor.disabled { - background: #f8fafc; - color: #475569; -} - -.json-template-meta { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.json-template-meta span { - min-height: 28px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 10px; - border-radius: 999px; - background: #f8fafc; - color: #475569; - font-size: 12px; - font-weight: 750; -} - -.json-template-meta strong { - color: #0f172a; - font-weight: 850; -} - -.json-editor { - min-height: 320px; - font-family: "Cascadia Mono", "Consolas", monospace; - font-size: 13px; - line-height: 1.65; - white-space: pre; -} - -.json-editor.disabled { - background: #f8fafc; - color: #475569; -} - -.subtle-banner, -.editor-foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} - -.subtle-banner { - min-height: 38px; - margin-bottom: 10px; - padding: 0 12px; - border: 1px solid #e0f2fe; - border-radius: 10px; - background: #f0f9ff; - color: #0369a1; - font-size: 12px; - font-weight: 700; -} - -.editor-foot { - margin-top: 12px; - color: #64748b; - font-size: 12px; - line-height: 1.5; -} - -.skill-review-side { - align-content: start; - padding-right: 8px; -} - -.review-card { - position: sticky; - top: 0; -} - -.reviewer-card { - border-color: rgba(16, 185, 129, 0.24); - background: linear-gradient(180deg, #ffffff, #f8fffc); -} - -.review-list { - display: grid; - gap: 0; -} - -.review-row { - display: grid; - gap: 6px; - padding: 12px 0; - border-top: 1px solid #edf2f7; -} - -.review-row:first-child { - border-top: 0; - padding-top: 0; -} - -.review-row span { - color: #64748b; - font-size: 12px; - font-weight: 800; -} - -.review-row strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; - line-height: 1.45; -} - -.version-list { - display: grid; - gap: 0; -} - -.version-row { - display: grid; - gap: 6px; - width: 100%; - padding: 10px 12px; - border-top: 1px solid #edf2f7; - border-right: 0; - border-bottom: 0; - border-left: 0; - background: transparent; - text-align: left; - transition: background 180ms ease; -} - -.version-row:first-child { - border-top: 0; -} - -.version-row:hover { - border-radius: 10px; - background: #f8fafc; -} - -.version-row.active { - border-radius: 10px; - background: rgba(16, 185, 129, 0.08); -} - -.version-main { - display: grid; - gap: 6px; - width: 100%; - padding: 0; - border: 0; - background: transparent; - text-align: left; - cursor: pointer; -} - -.version-row-head { - display: grid; - grid-template-columns: minmax(52px, 1fr) 46px 82px; - align-items: center; - gap: 8px; -} - -.version-row strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.version-row span { - color: #94a3b8; - font-size: 11px; - font-weight: 800; - white-space: nowrap; - text-align: right; -} - -.version-current-slot { - min-width: 46px; - display: grid; - place-items: center; - text-align: center; -} - -.version-current-slot .current-version { - min-height: 20px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 7px; - border-radius: 999px; - background: #dcfce7; - color: #059669; - font-size: 11px; - font-weight: 850; -} - -.version-current-slot .current-version.working { - background: #dbeafe; - color: #2563eb; -} - -.version-row p { - color: #64748b; - font-size: 12px; - line-height: 1.5; -} - -.version-row-foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.version-state { - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 850; -} - -.version-state.success { - background: #dcfce7; - color: #059669; -} - -.version-state.warning { - background: #fef3c7; - color: #d97706; -} - -.version-state.danger { - background: #fee2e2; - color: #dc2626; -} - -.version-state.draft { - background: #e2e8f0; - color: #475569; -} - -.version-state.disabled { - background: #f1f5f9; - color: #64748b; -} - -.version-restore-btn { - min-height: 24px; - padding: 0 9px; - border: 0; - border-radius: 999px; - background: #fff7ed; - color: #ea580c; - font-size: 11px; - font-weight: 850; - cursor: pointer; -} - -.version-restore-btn:disabled { - cursor: not-allowed; - opacity: 0.55; -} - -.empty-side-note { - min-height: 120px; - display: grid; - place-items: center; - gap: 8px; - color: #64748b; - font-size: 13px; - text-align: center; -} - -.empty-side-note i { - font-size: 24px; - color: #94a3b8; -} - -.version-modal-summary { - display: grid; - grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr); - align-items: center; - gap: 10px; - margin-top: 4px; -} - -.version-modal-summary div { - padding: 12px; - border: 1px solid #edf2f7; - border-radius: 10px; - background: #f8fafc; -} - -.version-modal-summary span, -.version-modal-note span { - display: block; - color: #64748b; - font-size: 12px; - font-weight: 800; -} - -.version-modal-summary strong, -.version-modal-note strong { - display: block; - margin-top: 6px; - color: #0f172a; - font-size: 15px; - font-weight: 850; -} - -.version-modal-summary i { - color: #94a3b8; - text-align: center; -} - -.version-modal-note { - margin-top: 12px; - padding: 12px; - border: 1px solid #edf2f7; - border-radius: 10px; - background: #fff; -} - -.prompt-stack { - display: grid; - gap: 14px; -} - -.prompt-block { - padding: 14px; - border: 1px solid #edf2f7; - border-radius: 12px; - background: #fbfdff; -} - -.prompt-block header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: 10px; -} - -.prompt-block strong { - color: #0f172a; - font-size: 14px; - font-weight: 850; -} - -.prompt-block header span { - color: #64748b; - font-size: 12px; - font-weight: 700; -} - -.contract-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.contract-panel { - padding: 14px; - border: 1px solid #edf2f7; - border-radius: 12px; - background: #fbfdff; -} - -.contract-panel h4 { - margin: 0 0 10px; - color: #0f172a; - font-size: 14px; - font-weight: 850; -} - -.contract-panel ul { - display: grid; - gap: 8px; - margin: 0; - padding-left: 18px; - color: #475569; - font-size: 13px; - line-height: 1.6; -} - -.test-row, -.tool-row, -.history-row { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 10px; - padding: 12px 0; - border-top: 1px solid #edf2f7; -} - -.test-row:first-child, -.tool-row:first-child, -.history-row:first-child { - border-top: 0; - padding-top: 0; -} - -.test-row strong, -.tool-row strong, -.history-row strong { - display: block; - color: #0f172a; - font-size: 13px; - font-weight: 800; -} - -.test-row span, -.tool-row span, -.history-row span, -.history-row small { - display: block; - margin-top: 4px; - color: #64748b; - font-size: 12px; - line-height: 1.5; -} - -.test-state, -.tool-state { - display: inline-flex; - align-items: center; - min-height: 24px; - padding: 0 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 800; - white-space: nowrap; -} - -.test-state.success, -.tool-state.safe { - background: #dcfce7; - color: #059669; -} - -.test-state.warning, -.tool-state.active { - background: #fff7ed; - color: #ea580c; -} - -.test-state.danger, -.tool-state.danger { - background: #fee2e2; - color: #dc2626; -} - -.review-action-strip { - display: flex; - gap: 10px; - flex-wrap: wrap; - margin-top: 16px; -} - -.action-help { - margin-top: 12px; - color: #64748b; - font-size: 12px; - line-height: 1.6; -} - -.tag-list { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.tag-list span { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: #eff6ff; - color: #2563eb; - font-size: 12px; - font-weight: 800; -} - -.publish-card { - display: grid; - gap: 14px; -} - -.publish-card p, -.publish-summary span { - margin-top: 6px; - color: #64748b; - font-size: 13px; - line-height: 1.55; -} - -.publish-summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-height: 42px; - padding: 0 12px; - border-radius: 10px; - background: #f8fafc; -} - -.publish-summary strong { - color: #059669; - font-size: 14px; - font-weight: 850; -} - -.detail-actions { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px 0 0; - border-top: 1px solid #e5eaf0; -} - -.detail-action-group { - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.back-action, -.minor-action, -.major-action { - height: 38px; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 0 14px; - border-radius: 8px; - font-size: 13px; - font-weight: 760; -} - -.back-action { - border: 1px solid #d7e0ea; - background: #fff; - color: #475569; -} - -.minor-action { - border: 1px solid #d7e0ea; - background: #fff; - color: #334155; -} - -.minor-action.success-action { - border-color: rgba(5, 150, 105, 0.26); - color: #059669; -} - -.minor-action.danger-action { - border-color: rgba(220, 38, 38, 0.2); - color: #dc2626; -} - -.major-action { - border: 1px solid #059669; - background: #059669; - color: #fff; - box-shadow: 0 4px 12px rgba(5, 150, 105, .16); -} - -.back-action:hover, -.minor-action:hover, -.major-action:hover, -.mini-btn:hover { - transform: translateY(-1px); -} - -.back-action:disabled, -.minor-action:disabled, -.major-action:disabled, -.mini-btn:disabled { - opacity: 0.52; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -.detail-meta-actions { - align-items: center; -} - -.footer-note { - color: #64748b; - font-size: 12px; - font-weight: 700; -} - -.mini-btn { - min-height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 0 12px; - border: 1px solid #d7e0ea; - border-radius: 8px; - background: #fff; - color: #334155; - font-size: 13px; - font-weight: 760; -} - -.mini-btn.primary { - border-color: #059669; - background: #059669; - color: #fff; -} - -@media (max-width: 1320px) { - .detail-hero { - grid-template-columns: 1fr; - } - - .hero-stats, - .form-grid, - .contract-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .rule-spreadsheet-stage, - .rule-spreadsheet-host { - min-height: 620px; - } - - .detail-grid { - grid-template-columns: 1fr; - } - - .detail-grid.skill-md-detail-grid { - grid-template-columns: 1fr; - } - - .skill-review-side { - padding-right: 0; - } - - .review-card { - position: static; - } -} - -@media (max-width: 860px) { - .skill-list, - .detail-card, - .side-card, - .detail-hero { - padding: 16px; - } - - .list-toolbar, - .card-head, - .detail-actions, - .detail-action-group, - .toolbar-actions, - .detail-inline-state { - flex-direction: column; - align-items: stretch; - } - - .status-tabs, - .filter-set { - overflow-x: auto; - } - - .search-filter, - .picker-trigger, - .picker-filter, - .toolbar-actions > * { - width: 100%; - } - - .picker-popover { - width: min(100vw - 64px, 320px); - } - - .hero-stats, - .form-grid, - .contract-grid { - grid-template-columns: 1fr; - } - - .review-action-strip { - flex-direction: column; - } - - .version-modal-summary { - grid-template-columns: 1fr; - } - - .version-modal-summary i { - transform: rotate(90deg); - } - - .field.span-2 { - grid-column: span 1; - } -} - -.json-risk-skill-detail .detail-scroll { - display: grid; - grid-template-rows: auto minmax(0, 1fr); -} - -.json-risk-editor-shell { - min-height: 0; - height: 100%; - display: flex; - flex-direction: column; - gap: 10px; - padding: 10px; -} - -.json-risk-editor-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.json-risk-editor-title { - min-width: 0; - display: flex; - align-items: flex-start; - gap: 12px; -} - -.json-risk-editor-title h2 { - color: #0f172a; - font-size: 18px; - font-weight: 850; -} - -.json-risk-editor-title p { - margin-top: 2px; - max-width: 760px; - color: #64748b; - font-size: 12px; - line-height: 1.4; -} - -.json-risk-head-subtitle { - display: -webkit-box; - margin: 6px 0 0; - max-width: 760px; - overflow: hidden; - color: #64748b; - font-size: 13px; - line-height: 1.55; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -.json-risk-head-category { - margin: 6px 0 0; - color: #be123c; - font-size: 12px; - font-weight: 600; -} - -.skill-name-cell .skill-list-subtitle { - display: -webkit-box; - overflow: hidden; - color: #94a3b8; - font-size: 12px; - line-height: 1.45; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -.json-risk-editor-actions { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.json-risk-mode-pill { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: #fff1f2; - color: #be123c; - font-size: 12px; - font-weight: 800; -} - -.json-risk-editor-body { - flex: 1 1 auto; - min-height: 0; - display: block; -} - -.json-risk-main-stage { - min-height: 0; - display: grid; - gap: 12px; -} - -.json-risk-description-card { - border-color: #fecdd3; - background: linear-gradient(180deg, #fffafb 0%, #ffffff 100%); -} - -.json-risk-description-text { - margin: 0; - padding: 0 4px 8px; - color: #334155; - font-size: 14px; - line-height: 1.75; - white-space: pre-wrap; - word-break: break-word; -} - -.json-risk-description-source { - margin: 0; - padding: 8px 12px 4px; - border-top: 1px solid #ffe4e6; - color: #94a3b8; - font-size: 12px; - line-height: 1.5; -} - -.json-risk-summary-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.json-risk-summary-grid span { - min-height: 34px; - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border-radius: 10px; - background: #f8fafc; - color: #475569; - font-size: 12px; -} - -.json-risk-summary-grid strong { - color: #0f172a; - font-size: 11px; - font-weight: 800; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.json-risk-flow-diagram { - display: grid; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); - gap: 10px; - align-items: center; -} - -.json-risk-flow-column { - display: grid; - gap: 6px; - padding: 12px; - border-radius: 12px; - border: 1px solid #e2e8f0; - background: #f8fafc; -} - -.json-risk-flow-column.center { - text-align: center; - background: #fff1f2; - border-color: #fecdd3; -} - -.json-risk-flow-column code { - font-size: 11px; - color: #334155; -} - -.json-risk-flow-label { - font-size: 11px; - font-weight: 800; - color: #64748b; - text-transform: uppercase; -} - -.json-risk-flow-arrow { - color: #94a3b8; - font-size: 18px; - font-weight: 800; -} - -.json-risk-editor-toolbar { - display: flex; - align-items: center; - gap: 8px; -} - -.json-risk-editor { - min-height: 280px; -} - -.json-risk-version-center { - min-height: 0; - display: grid; - gap: 12px; - align-content: start; - padding: 12px; - border-radius: 12px; - border: 1px solid #e2e8f0; - background: #ffffff; -} +.skill-center { + height: 100%; + min-height: 0; +} + +.skill-view-enter-active, +.skill-view-leave-active { + transition: opacity 220ms ease, transform 300ms var(--ease); +} + +.skill-view-enter-from, +.skill-view-leave-to { + opacity: 0; + transform: translateY(14px); +} + +.skill-list, +.skill-detail { + height: 100%; + min-height: 0; +} + +.skill-detail { + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + gap: 12px; +} + +.skill-detail.spreadsheet-skill-detail { + gap: 10px; +} + +.skill-list { + display: flex; + flex-direction: column; + min-height: 0; + padding: 18px 20px; + overflow: hidden; +} + +.status-tabs { + display: flex; + gap: 18px; + padding-bottom: 12px; + border-bottom: 1px solid #edf2f7; +} + +.status-tabs button { + position: relative; + border: 0; + background: transparent; + color: #64748b; + font-size: 14px; + font-weight: 760; +} + +.status-tabs button.active { + color: #0f172a; +} + +.status-tabs button.active::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -13px; + height: 3px; + border-radius: 999px; + background: #10b981; +} + +.list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 0 10px; +} + +.filter-set { + display: flex; + align-items: center; + gap: 10px; + flex: 1 1 auto; + flex-wrap: wrap; +} + +.search-filter { + width: 280px; + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 11px; + border: 1px solid #d7e0ea; + border-radius: 8px; + background: #fff; + color: #64748b; +} + +.search-filter i { + color: #94a3b8; + font-size: 16px; +} + +.search-filter input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + color: #0f172a; + font-size: 13px; +} + +.search-filter input::placeholder { + color: #94a3b8; +} + +.search-filter:focus-within { + border-color: rgba(16, 185, 129, 0.48); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); +} + +.picker-trigger, +.ghost-filter-btn, +.create-btn, +.row-action { + min-height: 38px; + border-radius: 8px; + font-size: 13px; + font-weight: 760; +} + +.picker-filter { + position: relative; +} + +.picker-trigger { + min-width: 124px; + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 34px 0 12px; + border: 1px solid #d7e0ea; + background: #fff; + color: #334155; + white-space: nowrap; +} + +.picker-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.picker-trigger .mdi { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #64748b; + pointer-events: none; +} + +.picker-trigger:hover, +.picker-filter.open .picker-trigger { + border-color: rgba(16, 185, 129, 0.34); + background: #f6fffb; + color: #0f9f78; +} + +.picker-popover { + position: absolute; + top: calc(100% + 8px); + left: 0; + width: 224px; + z-index: 40; + display: grid; + gap: 14px; + padding: 16px; + border: 1px solid #d7e0ea; + border-radius: 12px; + background: #fff; + box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16); +} + +.picker-popover header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.picker-popover header strong { + color: #0f172a; + font-size: 15px; +} + +.picker-popover header button { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border: 0; + border-radius: 8px; + background: transparent; + color: #64748b; +} + +.picker-popover header button:hover { + background: #f1f5f9; + color: #0f172a; +} + +.picker-option-list { + display: grid; + gap: 8px; + max-height: 240px; + overflow-y: auto; +} + +.picker-option { + min-height: 36px; + display: inline-flex; + align-items: center; + padding: 0 12px; + border: 1px solid #d7e0ea; + border-radius: 8px; + background: #fff; + color: #334155; + font-size: 13px; + font-weight: 750; + text-align: left; +} + +.picker-option:hover, +.picker-option.active { + border-color: rgba(16, 185, 129, 0.32); + background: rgba(16, 185, 129, 0.08); + color: #059669; +} + +.toolbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.ghost-filter-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid #d7e0ea; + background: #fff; + color: #475569; +} + +.ghost-filter-btn:hover { + border-color: rgba(16, 185, 129, 0.28); + color: #059669; +} + +.create-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 14px; + border: 0; + background: #059669; + color: #fff; + box-shadow: 0 8px 18px rgba(5, 150, 105, 0.18); +} + +.create-btn:disabled { + background: #cbd5e1; + color: #f8fafc; + box-shadow: none; + cursor: not-allowed; +} + +.hint { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0 0 12px; + color: #64748b; + font-size: 12px; +} + +.hint .mdi { + color: #94a3b8; +} + +.active-filter-strip { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.active-filter-chip { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: rgba(16, 185, 129, 0.1); + color: #047857; + font-size: 12px; + font-weight: 800; +} + +.table-wrap { + flex: 1 1 auto; + position: relative; + min-height: 400px; + overflow: auto; + border: 1px solid #edf2f7; + border-radius: 12px; + background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%); + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; +} + +.table-wrap.is-empty { + align-items: center; + justify-content: center; +} + +.table-wrap table { + width: 100%; + align-self: flex-start; +} + +.table-state, +.detail-inline-state { + width: 100%; + min-height: 220px; + display: grid; + place-items: center; + gap: 12px; + padding: 28px 24px; + text-align: center; + color: #64748b; + align-self: center; +} + +.detail-inline-state { + min-height: 180px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + text-align: left; +} + +.table-state i, +.detail-inline-state i { + font-size: 28px; + color: #94a3b8; +} + +.table-state.error i, +.detail-inline-state.error i { + color: #dc2626; +} + +.table-state.empty i { + color: #0ea5e9; +} + +.table-state p, +.detail-inline-state p { + margin-top: 6px; + font-size: 13px; + line-height: 1.6; +} + +.detail-inline-state strong { + color: #0f172a; + font-size: 14px; + font-weight: 850; +} + +.detail-inline-state > div { + flex: 1 1 auto; +} + +.state-action { + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 14px; + border: 1px solid rgba(16, 185, 129, 0.28); + border-radius: 8px; + background: #fff; + color: #059669; + font-size: 13px; + font-weight: 760; +} + +.list-foot { + display: flex; + align-items: center; + justify-content: flex-end; + padding-top: 12px; +} + +.page-summary { + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +table { + width: 100%; + min-width: 1120px; + border-collapse: collapse; +} + +th, +td { + padding: 14px 12px; + border-bottom: 1px solid #edf2f7; + text-align: center; + vertical-align: middle; + color: #334155; + font-size: 12px; +} + +th { + background: #f8fafc; + color: #64748b; + font-weight: 800; + white-space: nowrap; +} + +tbody tr { + cursor: pointer; + transition: background 180ms ease; +} + +th:first-child, +td:first-child { + text-align: left; +} + +tbody tr:hover { + background: #f8fbff; +} + +tbody tr.spotlight { + background: linear-gradient(90deg, rgba(16, 185, 129, 0.05), rgba(59, 130, 246, 0.03)); +} + +.skill-name-cell { + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + gap: 10px; + align-items: center; +} + +.skill-avatar { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border-radius: 11px; + color: #fff; + font-size: 13px; + font-weight: 900; +} + +.skill-avatar.emerald { background: linear-gradient(135deg, #10b981, #059669); } +.skill-avatar.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); } +.skill-avatar.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } +.skill-avatar.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); } +.skill-avatar.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); } + +.skill-name-cell strong { + display: block; + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.skill-name-cell span:last-child { + display: block; + margin-top: 4px; + color: #64748b; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.scope-pill, +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 26px; + padding: 0 9px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + white-space: nowrap; +} + +.scope-pill { + background: #f1f5f9; + color: #475569; +} + +.status-pill.success { + background: #dcfce7; + color: #059669; +} + +.status-pill.warning { + background: #fff7ed; + color: #ea580c; +} + +.status-pill.draft { + background: #eef2ff; + color: #6366f1; +} + +.status-pill.danger { + background: #fee2e2; + color: #dc2626; +} + +.status-pill.disabled { + background: #e2e8f0; + color: #475569; +} + +.row-action { + padding: 0 12px; + border: 1px solid rgba(16, 185, 129, 0.32); + background: #fff; + color: #059669; +} + +.row-action:hover { + background: rgba(16, 185, 129, 0.08); +} + +.detail-scroll { + height: 100%; + overflow: auto; + display: grid; + align-content: start; + gap: 16px; +} + +.spreadsheet-skill-detail .detail-scroll { + min-height: 0; + overflow: hidden; + align-content: stretch; + grid-template-rows: minmax(0, 1fr); +} + +.detail-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + align-items: start; + gap: 10px; + padding: 16px 20px; +} + +.hero-title { + min-width: 0; +} + +.skill-badge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + color: #fff; + font-size: 11px; + font-weight: 800; +} + +.skill-badge.emerald { background: linear-gradient(135deg, #10b981, #059669); } +.skill-badge.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); } +.skill-badge.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } +.skill-badge.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); } +.skill-badge.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); } + +.hero-title h2 { + margin-top: 10px; + color: #0f172a; + font-size: 24px; + font-weight: 850; +} + +.hero-title p { + margin-top: 8px; + max-width: 820px; + color: #64748b; + font-size: 14px; + line-height: 1.6; +} + +.hero-review-meta { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-top: 12px; +} + +.inline-review-meta { + margin-top: 10px; +} + +.review-note-block { + display: grid; + gap: 6px; + margin-top: 12px; + padding: 12px 14px; + border: 1px solid rgba(16, 185, 129, 0.16); + border-radius: 12px; + background: linear-gradient(180deg, #f8fffc, #ffffff); +} + +.review-note-block strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.review-note-block p, +.review-note-block span { + color: #64748b; + font-size: 12px; + line-height: 1.6; +} + +.review-note-block.muted { + border-color: #e2e8f0; + background: #f8fafc; +} + +.hero-review-meta span { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 26px; + padding: 0 9px; + border-radius: 999px; + background: #f8fafc; + color: #475569; + font-size: 12px; + font-weight: 800; +} + +.hero-review-meta i { + color: #059669; + font-size: 15px; +} + +.hero-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.hero-stat { + min-height: 54px; + display: grid; + align-content: center; + gap: 4px; + padding: 9px 12px; + border-radius: 10px; + background: #f8fafc; + border: 1px solid #e5eaf0; +} + +.hero-stat span { + display: block; + color: #64748b; + font-size: 11px; + font-weight: 800; + line-height: 1.2; +} + +.hero-stat strong { + display: block; + color: #0f172a; + font-size: 15px; + font-weight: 850; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.hero-stat .status-pill { + width: fit-content; + min-height: 22px; + padding: 0 8px; +} + +.detail-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.78fr); + gap: 16px; +} + +.detail-grid.skill-md-detail-grid { + grid-template-columns: minmax(0, 1fr) 372px; + align-items: stretch; +} + +.detail-grid.skill-md-detail-grid .detail-main { + height: 100%; +} + +.detail-main, +.detail-side { + display: grid; + gap: 16px; + align-content: start; +} + +.detail-card, +.side-card { + padding: 18px; +} + +.card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.card-head h3 { + color: #0f172a; + font-size: 16px; + font-weight: 850; +} + +.card-head p { + margin-top: 6px; + color: #64748b; + font-size: 13px; + line-height: 1.5; +} + +.edit-badge { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + background: #ecfeff; + color: #0891b2; + font-size: 12px; + font-weight: 800; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.field { + display: grid; + gap: 7px; +} + +.field.span-2 { + grid-column: span 2; +} + +.field span { + color: #64748b; + font-size: 12px; + font-weight: 800; +} + +.field input, +.field textarea, +.prompt-block textarea { + width: 100%; + border: 1px solid #d7e0ea; + border-radius: 10px; + background: #fff; + color: #0f172a; + font-size: 13px; + line-height: 1.55; + padding: 10px 12px; + resize: vertical; +} + +.field input[readonly], +.field textarea[readonly], +.prompt-block textarea[readonly] { + background: #f8fafc; + color: #334155; +} + +.markdown-card { + min-height: 620px; + display: grid; + grid-template-rows: auto minmax(0, 1fr); +} + +.json-editor-card { + display: grid; + gap: 12px; +} + +.spreadsheet-rule-card { + display: grid; + gap: 14px; +} + +.spreadsheet-editor-shell { + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; +} + +.spreadsheet-editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.spreadsheet-editor-title { + min-width: 0; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.spreadsheet-editor-title h2 { + color: #0f172a; + font-size: 18px; + font-weight: 850; +} + +.spreadsheet-editor-title p { + margin-top: 2px; + max-width: 760px; + color: #64748b; + font-size: 12px; + line-height: 1.4; +} + +.spreadsheet-editor-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.spreadsheet-editor-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.spreadsheet-editor-meta span { + min-height: 28px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + border-radius: 999px; + background: #f8fafc; + color: #475569; + font-size: 12px; + font-weight: 750; +} + +.spreadsheet-editor-meta strong { + color: #0f172a; + font-weight: 850; +} + +.spreadsheet-editor-body { + flex: 1 1 auto; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) 286px; + gap: 14px; + align-items: start; +} + +.spreadsheet-main-stage { + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 10px; +} + +.spreadsheet-workbench { + flex: 1 1 auto; + position: relative; + min-height: 0; + overflow: visible; + border: 1px solid #dbe4ee; + border-radius: 12px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.spreadsheet-workbench .rule-spreadsheet-host { + min-height: 0; + height: 100%; + overflow: visible; +} + +.spreadsheet-editor-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + color: #64748b; + font-size: 12px; + line-height: 1.5; +} + +.spreadsheet-change-center { + min-height: 0; + height: 100%; + align-self: stretch; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 12px; + padding: 14px; + border: 1px solid #dbe4ee; + border-radius: 14px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + overflow: hidden; +} + +.change-center-head h3, +.change-center-head p, +.change-center-section header, +.change-center-section p { + margin: 0; +} + +.change-center-head h3 { + color: #0f172a; + font-size: 15px; + font-weight: 900; +} + +.change-center-head p { + margin-top: 3px; + color: #64748b; + font-size: 12px; +} + +.version-pair-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.version-pair-card { + display: grid; + gap: 4px; + min-height: 84px; + padding: 10px; + border-radius: 12px; +} + +.version-pair-card span { + color: #64748b; + font-size: 11px; + font-weight: 800; +} + +.version-pair-card strong { + color: #0f172a; + font-size: 16px; + font-weight: 900; +} + +.version-pair-card b { + width: fit-content; + min-height: 20px; + display: inline-flex; + align-items: center; + padding: 0 7px; + border-radius: 999px; + font-size: 11px; + font-weight: 850; +} + +.version-pair-card.published { + background: rgba(16, 185, 129, 0.1); +} + +.version-pair-card.published b { + background: #dcfce7; + color: #059669; +} + +.version-pair-card.working { + background: rgba(37, 99, 235, 0.08); +} + +.version-pair-card.working b { + background: #dbeafe; + color: #2563eb; +} + +.change-center-section { + display: grid; + gap: 8px; +} + +.change-history-section { + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); +} + +.change-center-section > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.change-center-section > header strong { + color: #0f172a; + font-size: 13px; + font-weight: 900; +} + +.change-center-section > header small, +.change-center-section > header button { + color: #64748b; + font-size: 11px; + font-weight: 800; +} + +.change-center-section > header button { + padding: 0; + border: 0; + background: transparent; + color: #2563eb; + cursor: pointer; +} + +.change-center-list { + display: grid; + align-content: start; + gap: 8px; + min-height: 0; + overflow-y: auto; + padding-right: 2px; +} + +.change-center-item { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; +} + +.change-center-item.active { + border-color: rgba(16, 185, 129, 0.35); + background: rgba(16, 185, 129, 0.05); +} + +.change-center-item > button { + display: grid; + gap: 5px; + padding: 0; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; +} + +.change-center-item > button:disabled { + cursor: default; +} + +.change-center-item > button div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.change-center-item > button strong { + color: #0f172a; + font-size: 13px; + font-weight: 900; +} + +.change-center-item > button span, +.change-center-item > button p, +.change-center-item > button small { + color: #64748b; + font-size: 11px; +} + +.change-center-item > button p { + margin: 0; + line-height: 1.45; +} + +.change-record-summary-empty { + color: #64748b; + font-size: 11px; + line-height: 1.5; +} + +.change-record-item { + gap: 10px; + width: 100%; + border: 1px solid #e2e8f0; + background: #fff; + color: inherit; + text-align: left; + cursor: pointer; + transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; +} + +.change-record-item:hover { + border-color: rgba(16, 185, 129, 0.35); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); + transform: translateY(-1px); +} + +.change-record-item:focus-visible { + outline: 3px solid rgba(16, 185, 129, 0.18); + outline-offset: 1px; +} + +.change-record-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.change-record-head > div { + display: grid; + gap: 3px; +} + +.change-record-head strong { + color: #0f172a; + font-size: 13px; + font-weight: 900; +} + +.change-record-head span, +.change-record-item p, +.change-record-more { + color: #64748b; + font-size: 11px; +} + +.change-record-head b { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + background: #eef2ff; + color: #4f46e5; + font-size: 11px; + font-weight: 850; +} + +.change-record-item p { + margin: 0; + line-height: 1.5; +} + +.change-record-preview { + display: grid; + gap: 6px; +} + +.change-record-preview span { + display: block; + padding: 7px 8px; + border-radius: 10px; + background: #f8fafc; + color: #334155; + font-size: 11px; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.change-record-more { + font-weight: 800; +} + +.change-detail-content { + gap: 14px; +} + +.change-detail-meta { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.change-detail-meta article { + display: grid; + gap: 4px; + min-height: 0; + padding: 10px 12px; + border-radius: 14px; + background: #f8fafc; +} + +.change-detail-meta span { + color: #64748b; + font-size: 12px; +} + +.change-detail-meta strong { + color: #0f172a; + font-size: 15px; + font-weight: 900; +} + +.change-flow-empty { + color: #64748b; + font-size: 11px; +} + +.rule-drawer-backdrop { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + justify-content: flex-end; + background: rgba(15, 23, 42, 0.34); + backdrop-filter: blur(3px); +} + +.rule-drawer { + width: min(860px, 78vw); + height: 100%; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + gap: 18px; + padding: 22px; + overflow: auto; + background: #fff; + box-shadow: -18px 0 42px rgba(15, 23, 42, 0.18); +} + +.timeline-drawer { + width: min(640px, 62vw); +} + +.rule-drawer-head { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; +} + +.rule-drawer-head span { + color: #2563eb; + font-size: 12px; + font-weight: 850; +} + +.rule-drawer-head h3 { + margin: 4px 0 0; + color: #0f172a; + font-size: 20px; + font-weight: 950; +} + +.rule-drawer-head button { + width: 34px; + height: 34px; + border: 0; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + cursor: pointer; +} + +.rule-drawer-state { + min-height: 160px; + display: grid; + place-items: center; + gap: 10px; + color: #64748b; +} + +.rule-drawer-state.error { + color: #dc2626; +} + +.rule-timeline-list { + display: grid; + align-content: start; +} + +.rule-timeline-item { + position: relative; + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 12px; + padding-bottom: 20px; +} + +.rule-timeline-item:not(:last-child)::after { + content: ''; + position: absolute; + left: 14px; + top: 28px; + bottom: 0; + width: 2px; + background: #e2e8f0; +} + +.rule-timeline-item > i { + position: relative; + z-index: 1; + width: 30px; + height: 30px; + display: grid; + place-items: center; + border-radius: 999px; + background: #f1f5f9; + color: #475569; +} + +.rule-timeline-item > i.success { + background: #dcfce7; + color: #059669; +} + +.rule-timeline-item > i.warning { + background: #fef3c7; + color: #d97706; +} + +.rule-timeline-item > i.danger { + background: #fee2e2; + color: #dc2626; +} + +.rule-timeline-item > i.info { + background: #dbeafe; + color: #2563eb; +} + +.rule-timeline-item header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.rule-timeline-item header strong { + color: #0f172a; + font-size: 14px; +} + +.rule-timeline-item header b { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + font-size: 11px; +} + +.rule-timeline-item header span, +.rule-timeline-item p, +.rule-timeline-item small { + color: #64748b; +} + +.rule-timeline-item p { + margin: 8px 0 4px; + line-height: 1.6; +} + +.compare-toolbar { + display: flex; + align-items: end; + gap: 12px; +} + +.compare-toolbar label { + min-width: 0; + display: grid; + gap: 6px; + flex: 1; +} + +.compare-toolbar span { + color: #64748b; + font-size: 12px; + font-weight: 850; +} + +.compare-toolbar select { + width: 100%; + min-height: 40px; + padding: 0 12px; + border: 1px solid #cbd5e1; + border-radius: 12px; + background: #fff; +} + +.compare-toolbar i { + margin-bottom: 10px; + color: #94a3b8; +} + +.compare-content { + min-height: 0; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + gap: 18px; +} + +.compare-summary-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.compare-summary-grid article { + display: grid; + gap: 4px; + align-content: center; + min-height: 0; + padding: 10px 12px; + border-radius: 14px; + background: #f8fafc; +} + +.compare-summary-grid span { + color: #64748b; + font-size: 12px; +} + +.compare-summary-grid strong { + color: #0f172a; + font-size: 22px; + font-weight: 950; + line-height: 1.1; +} + +.compare-panel { + display: grid; + gap: 10px; +} + +.compare-cell-panel { + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); +} + +.compare-panel header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.compare-panel header strong { + color: #0f172a; + font-size: 14px; +} + +.compare-panel p, +.compare-panel small { + color: #64748b; +} + +.compare-sheet-list { + display: grid; + gap: 8px; +} + +.compare-sheet-list article { + min-height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 12px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; +} + +.compare-sheet-list strong { + min-width: 0; + color: #0f172a; + font-size: 13px; + font-weight: 850; + overflow-wrap: anywhere; +} + +.compare-sheet-list b { + min-height: 24px; + display: inline-flex; + align-items: center; + flex: 0 0 auto; + padding: 0 9px; + border-radius: 999px; + font-size: 12px; + font-weight: 850; +} + +.compare-sheet-list b.success { + background: #dcfce7; + color: #059669; +} + +.compare-sheet-list b.danger { + background: #fee2e2; + color: #dc2626; +} + +.compare-table-wrap { + min-height: 0; + overflow: auto; + border: 1px solid #e2e8f0; + border-radius: 14px; +} + +.compare-table-wrap table { + width: 100%; + border-collapse: collapse; +} + +.compare-table-wrap th, +.compare-table-wrap td { + padding: 10px 12px; + border-bottom: 1px solid #eef2f7; + text-align: left; + vertical-align: top; +} + +.compare-table-wrap th { + color: #475569; + font-size: 12px; + background: #f8fafc; +} + +.compare-table-wrap td { + color: #0f172a; + font-size: 13px; +} + +.compare-table-wrap td b { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; +} + +.compare-table-wrap td b.success { + background: #dcfce7; + color: #059669; +} + +.compare-table-wrap td b.warning { + background: #fef3c7; + color: #d97706; +} + +.compare-table-wrap td b.danger { + background: #fee2e2; + color: #dc2626; +} + +.review-submit-form { + display: grid; + gap: 12px; +} + +.review-submit-form label { + display: grid; + gap: 7px; +} + +.review-submit-form label span { + color: #475569; + font-size: 12px; + font-weight: 850; +} + +.review-submit-form input, +.review-submit-form select { + width: 100%; + min-height: 42px; + padding: 0 12px; + border: 1px solid #cbd5e1; + border-radius: 12px; + background: #fff; + color: #0f172a; + font-size: 14px; +} + +.review-submit-form input:focus, +.review-submit-form select:focus { + outline: 0; + border-color: rgba(16, 185, 129, 0.5); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12); +} + +.review-submit-hint { + margin: 0; + padding: 10px 12px; + border-radius: 12px; + background: #fff7ed; + color: #c2410c; + font-size: 12px; + line-height: 1.6; +} + +@media (max-width: 1280px) { + .spreadsheet-editor-body { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .rule-drawer, + .timeline-drawer { + width: 100%; + } + + .compare-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .change-detail-meta { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.rule-spreadsheet-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.spreadsheet-mode-pill { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + font-size: 12px; + font-weight: 800; +} + +.spreadsheet-upload-input { + display: none; +} + +.spreadsheet-meta-strip { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.spreadsheet-meta-strip span { + min-height: 28px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + border-radius: 999px; + background: #f8fafc; + color: #475569; + font-size: 12px; + font-weight: 750; +} + +.spreadsheet-meta-strip strong { + color: #0f172a; + font-weight: 850; +} + +.rule-spreadsheet-stage { + position: relative; + min-height: 720px; + overflow: hidden; + border: 1px solid #dbe4ee; + border-radius: 14px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.rule-spreadsheet-host { + width: 100%; + height: 100%; + min-height: 720px; +} + +.rule-spreadsheet-host.hidden { + visibility: hidden; +} + +.rule-spreadsheet-state { + position: absolute; + inset: 0; + display: grid; + place-items: center; + gap: 8px; + padding: 24px; + background: rgba(248, 250, 252, 0.94); + color: #475569; + font-size: 13px; + font-weight: 800; + text-align: center; +} + +.rule-spreadsheet-state i { + font-size: 22px; +} + +.rule-spreadsheet-state.error { + color: #dc2626; +} + +.preview-mode-note { + display: inline-flex; + align-items: center; + gap: 8px; + margin-top: 14px; + padding: 10px 12px; + border: 1px solid rgba(14, 165, 233, 0.22); + border-radius: 12px; + background: linear-gradient(180deg, rgba(240, 249, 255, 0.96), rgba(224, 242, 254, 0.9)); + color: #075985; + font-size: 12px; + font-weight: 760; + line-height: 1.5; +} + +.preview-mode-note i { + font-size: 16px; +} + +.markdown-card .field { + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); +} + +.markdown-editor { + min-height: 520px; + height: 100%; + font-family: "Cascadia Mono", "Consolas", monospace; + font-size: 13px; + line-height: 1.65; + white-space: pre; +} + +.markdown-editor.disabled { + background: #f8fafc; + color: #475569; +} + +.json-template-meta { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.json-template-meta span { + min-height: 28px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + border-radius: 999px; + background: #f8fafc; + color: #475569; + font-size: 12px; + font-weight: 750; +} + +.json-template-meta strong { + color: #0f172a; + font-weight: 850; +} + +.json-editor { + min-height: 320px; + font-family: "Cascadia Mono", "Consolas", monospace; + font-size: 13px; + line-height: 1.65; + white-space: pre; +} + +.json-editor.disabled { + background: #f8fafc; + color: #475569; +} + +.subtle-banner, +.editor-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.subtle-banner { + min-height: 38px; + margin-bottom: 10px; + padding: 0 12px; + border: 1px solid #e0f2fe; + border-radius: 10px; + background: #f0f9ff; + color: #0369a1; + font-size: 12px; + font-weight: 700; +} + +.editor-foot { + margin-top: 12px; + color: #64748b; + font-size: 12px; + line-height: 1.5; +} + +.skill-review-side { + align-content: start; + padding-right: 8px; +} + +.review-card { + position: sticky; + top: 0; +} + +.reviewer-card { + border-color: rgba(16, 185, 129, 0.24); + background: linear-gradient(180deg, #ffffff, #f8fffc); +} + +.review-list { + display: grid; + gap: 0; +} + +.review-row { + display: grid; + gap: 6px; + padding: 12px 0; + border-top: 1px solid #edf2f7; +} + +.review-row:first-child { + border-top: 0; + padding-top: 0; +} + +.review-row span { + color: #64748b; + font-size: 12px; + font-weight: 800; +} + +.review-row strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; + line-height: 1.45; +} + +.version-list { + display: grid; + gap: 0; +} + +.version-row { + display: grid; + gap: 6px; + width: 100%; + padding: 10px 12px; + border-top: 1px solid #edf2f7; + border-right: 0; + border-bottom: 0; + border-left: 0; + background: transparent; + text-align: left; + transition: background 180ms ease; +} + +.version-row:first-child { + border-top: 0; +} + +.version-row:hover { + border-radius: 10px; + background: #f8fafc; +} + +.version-row.active { + border-radius: 10px; + background: rgba(16, 185, 129, 0.08); +} + +.version-main { + display: grid; + gap: 6px; + width: 100%; + padding: 0; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; +} + +.version-row-head { + display: grid; + grid-template-columns: minmax(52px, 1fr) 46px 82px; + align-items: center; + gap: 8px; +} + +.version-row strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.version-row span { + color: #94a3b8; + font-size: 11px; + font-weight: 800; + white-space: nowrap; + text-align: right; +} + +.version-current-slot { + min-width: 46px; + display: grid; + place-items: center; + text-align: center; +} + +.version-current-slot .current-version { + min-height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 7px; + border-radius: 999px; + background: #dcfce7; + color: #059669; + font-size: 11px; + font-weight: 850; +} + +.version-current-slot .current-version.working { + background: #dbeafe; + color: #2563eb; +} + +.version-row p { + color: #64748b; + font-size: 12px; + line-height: 1.5; +} + +.version-row-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.version-state { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 850; +} + +.version-state.success { + background: #dcfce7; + color: #059669; +} + +.version-state.warning { + background: #fef3c7; + color: #d97706; +} + +.version-state.danger { + background: #fee2e2; + color: #dc2626; +} + +.version-state.draft { + background: #e2e8f0; + color: #475569; +} + +.version-state.disabled { + background: #f1f5f9; + color: #64748b; +} + +.version-restore-btn { + min-height: 24px; + padding: 0 9px; + border: 0; + border-radius: 999px; + background: #fff7ed; + color: #ea580c; + font-size: 11px; + font-weight: 850; + cursor: pointer; +} + +.version-restore-btn:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.empty-side-note { + min-height: 120px; + display: grid; + place-items: center; + gap: 8px; + color: #64748b; + font-size: 13px; + text-align: center; +} + +.empty-side-note i { + font-size: 24px; + color: #94a3b8; +} + +.version-modal-summary { + display: grid; + grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr); + align-items: center; + gap: 10px; + margin-top: 4px; +} + +.version-modal-summary div { + padding: 12px; + border: 1px solid #edf2f7; + border-radius: 10px; + background: #f8fafc; +} + +.version-modal-summary span, +.version-modal-note span { + display: block; + color: #64748b; + font-size: 12px; + font-weight: 800; +} + +.version-modal-summary strong, +.version-modal-note strong { + display: block; + margin-top: 6px; + color: #0f172a; + font-size: 15px; + font-weight: 850; +} + +.version-modal-summary i { + color: #94a3b8; + text-align: center; +} + +.version-modal-note { + margin-top: 12px; + padding: 12px; + border: 1px solid #edf2f7; + border-radius: 10px; + background: #fff; +} + +.prompt-stack { + display: grid; + gap: 14px; +} + +.prompt-block { + padding: 14px; + border: 1px solid #edf2f7; + border-radius: 12px; + background: #fbfdff; +} + +.prompt-block header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; +} + +.prompt-block strong { + color: #0f172a; + font-size: 14px; + font-weight: 850; +} + +.prompt-block header span { + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.contract-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.contract-panel { + padding: 14px; + border: 1px solid #edf2f7; + border-radius: 12px; + background: #fbfdff; +} + +.contract-panel h4 { + margin: 0 0 10px; + color: #0f172a; + font-size: 14px; + font-weight: 850; +} + +.contract-panel ul { + display: grid; + gap: 8px; + margin: 0; + padding-left: 18px; + color: #475569; + font-size: 13px; + line-height: 1.6; +} + +.test-row, +.tool-row, +.history-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + padding: 12px 0; + border-top: 1px solid #edf2f7; +} + +.test-row:first-child, +.tool-row:first-child, +.history-row:first-child { + border-top: 0; + padding-top: 0; +} + +.test-row strong, +.tool-row strong, +.history-row strong { + display: block; + color: #0f172a; + font-size: 13px; + font-weight: 800; +} + +.test-row span, +.tool-row span, +.history-row span, +.history-row small { + display: block; + margin-top: 4px; + color: #64748b; + font-size: 12px; + line-height: 1.5; +} + +.test-state, +.tool-state { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + white-space: nowrap; +} + +.test-state.success, +.tool-state.safe { + background: #dcfce7; + color: #059669; +} + +.test-state.warning, +.tool-state.active { + background: #fff7ed; + color: #ea580c; +} + +.test-state.danger, +.tool-state.danger { + background: #fee2e2; + color: #dc2626; +} + +.review-action-strip { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 16px; +} + +.action-help { + margin-top: 12px; + color: #64748b; + font-size: 12px; + line-height: 1.6; +} + +.tag-list { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.tag-list span { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: #eff6ff; + color: #2563eb; + font-size: 12px; + font-weight: 800; +} + +.publish-card { + display: grid; + gap: 14px; +} + +.publish-card p, +.publish-summary span { + margin-top: 6px; + color: #64748b; + font-size: 13px; + line-height: 1.55; +} + +.publish-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 42px; + padding: 0 12px; + border-radius: 10px; + background: #f8fafc; +} + +.publish-summary strong { + color: #059669; + font-size: 14px; + font-weight: 850; +} + +.detail-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 0 0; + border-top: 1px solid #e5eaf0; +} + +.detail-action-group { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.back-action, +.minor-action, +.major-action { + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 0 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 760; +} + +.back-action { + border: 1px solid #d7e0ea; + background: #fff; + color: #475569; +} + +.minor-action { + border: 1px solid #d7e0ea; + background: #fff; + color: #334155; +} + +.minor-action.success-action { + border-color: rgba(5, 150, 105, 0.26); + color: #059669; +} + +.minor-action.danger-action { + border-color: rgba(220, 38, 38, 0.2); + color: #dc2626; +} + +.major-action { + border: 1px solid #059669; + background: #059669; + color: #fff; + box-shadow: 0 4px 12px rgba(5, 150, 105, .16); +} + +.back-action:hover, +.minor-action:hover, +.major-action:hover, +.mini-btn:hover { + transform: translateY(-1px); +} + +.back-action:disabled, +.minor-action:disabled, +.major-action:disabled, +.mini-btn:disabled { + opacity: 0.52; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.detail-meta-actions { + align-items: center; +} + +.footer-note { + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.mini-btn { + min-height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 0 12px; + border: 1px solid #d7e0ea; + border-radius: 8px; + background: #fff; + color: #334155; + font-size: 13px; + font-weight: 760; +} + +.mini-btn.primary { + border-color: #059669; + background: #059669; + color: #fff; +} + +@media (max-width: 1320px) { + .detail-hero { + grid-template-columns: 1fr; + } + + .hero-stats, + .form-grid, + .contract-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .rule-spreadsheet-stage, + .rule-spreadsheet-host { + min-height: 620px; + } + + .detail-grid { + grid-template-columns: 1fr; + } + + .detail-grid.skill-md-detail-grid { + grid-template-columns: 1fr; + } + + .skill-review-side { + padding-right: 0; + } + + .review-card { + position: static; + } +} + +@media (max-width: 860px) { + .skill-list, + .detail-card, + .side-card, + .detail-hero { + padding: 16px; + } + + .list-toolbar, + .card-head, + .detail-actions, + .detail-action-group, + .toolbar-actions, + .detail-inline-state { + flex-direction: column; + align-items: stretch; + } + + .status-tabs, + .filter-set { + overflow-x: auto; + } + + .search-filter, + .picker-trigger, + .picker-filter, + .toolbar-actions > * { + width: 100%; + } + + .picker-popover { + width: min(100vw - 64px, 320px); + } + + .hero-stats, + .form-grid, + .contract-grid { + grid-template-columns: 1fr; + } + + .review-action-strip { + flex-direction: column; + } + + .version-modal-summary { + grid-template-columns: 1fr; + } + + .version-modal-summary i { + transform: rotate(90deg); + } + + .field.span-2 { + grid-column: span 1; + } +} + +.json-risk-skill-detail .detail-scroll { + display: grid; + grid-template-rows: auto minmax(0, 1fr); +} + +.json-risk-editor-shell { + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; +} + +.json-risk-editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.json-risk-editor-title { + min-width: 0; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.json-risk-editor-title h2 { + color: #0f172a; + font-size: 18px; + font-weight: 850; +} + +.json-risk-editor-title p { + margin-top: 2px; + max-width: 760px; + color: #64748b; + font-size: 12px; + line-height: 1.4; +} + +.json-risk-head-subtitle { + display: -webkit-box; + margin: 6px 0 0; + max-width: 760px; + overflow: hidden; + color: #64748b; + font-size: 13px; + line-height: 1.55; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.json-risk-head-category { + margin: 6px 0 0; + color: #be123c; + font-size: 12px; + font-weight: 600; +} + +.skill-name-cell .skill-list-subtitle { + display: -webkit-box; + overflow: hidden; + color: #94a3b8; + font-size: 12px; + line-height: 1.45; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.json-risk-editor-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.json-risk-mode-pill { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: #fff1f2; + color: #be123c; + font-size: 12px; + font-weight: 800; +} + +.json-risk-editor-body { + flex: 1 1 auto; + min-height: 0; + display: block; +} + +.json-risk-main-stage { + min-height: 0; + display: grid; + gap: 12px; +} + +.json-risk-description-card { + border-color: #fecdd3; + background: linear-gradient(180deg, #fffafb 0%, #ffffff 100%); +} + +.json-risk-description-text { + margin: 0; + padding: 0 4px 8px; + color: #334155; + font-size: 14px; + line-height: 1.75; + white-space: pre-wrap; + word-break: break-word; +} + +.json-risk-description-source { + margin: 0; + padding: 8px 12px 4px; + border-top: 1px solid #ffe4e6; + color: #94a3b8; + font-size: 12px; + line-height: 1.5; +} + +.json-risk-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.json-risk-summary-grid span { + min-height: 34px; + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border-radius: 10px; + background: #f8fafc; + color: #475569; + font-size: 12px; +} + +.json-risk-summary-grid strong { + color: #0f172a; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.json-risk-flow-diagram { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); + gap: 10px; + align-items: center; +} + +.json-risk-flow-column { + display: grid; + gap: 6px; + padding: 12px; + border-radius: 12px; + border: 1px solid #e2e8f0; + background: #f8fafc; +} + +.json-risk-flow-column.center { + text-align: center; + background: #fff1f2; + border-color: #fecdd3; +} + +.json-risk-flow-column code { + font-size: 11px; + color: #334155; +} + +.json-risk-flow-label { + font-size: 11px; + font-weight: 800; + color: #64748b; + text-transform: uppercase; +} + +.json-risk-flow-arrow { + color: #94a3b8; + font-size: 18px; + font-weight: 800; +} + +.json-risk-editor-toolbar { + display: flex; + align-items: center; + gap: 8px; +} + +.json-risk-editor { + min-height: 280px; +} + +.json-risk-version-center { + min-height: 0; + display: grid; + gap: 12px; + align-content: start; + padding: 12px; + border-radius: 12px; + border: 1px solid #e2e8f0; + background: #ffffff; +} diff --git a/web/src/assets/styles/views/travel-reimbursement-create-view.css b/web/src/assets/styles/views/travel-reimbursement-create-view.css index f5d7ede..9056f85 100644 --- a/web/src/assets/styles/views/travel-reimbursement-create-view.css +++ b/web/src/assets/styles/views/travel-reimbursement-create-view.css @@ -1,3706 +1,4037 @@ -.assistant-overlay { - position: fixed; - inset: 0; - z-index: 9999; - display: grid; - place-items: center; - background: - radial-gradient(circle at 18% 14%, rgba(16, 185, 129, 0.18), transparent 24%), - radial-gradient(circle at 82% 12%, rgba(59, 130, 246, 0.12), transparent 28%), - rgba(97, 110, 131, 0.34); - backdrop-filter: blur(18px) saturate(1.02); - -webkit-backdrop-filter: blur(18px) saturate(1.02); -} - -.assistant-modal { - --assistant-base-width: 1430; - --assistant-base-height: 820; - --assistant-base-width-px: 1430px; - --assistant-base-height-px: 820px; - --assistant-safe-offset-x: 64; - --assistant-safe-offset-y: 48; - --assistant-fit-scale-width: calc((var(--desktop-viewport-width, 1440) - var(--assistant-safe-offset-x)) / var(--assistant-base-width)); - --assistant-fit-scale-height: calc((var(--desktop-viewport-height, 900) - var(--assistant-safe-offset-y)) / var(--assistant-base-height)); - --assistant-scale: min(1, var(--assistant-fit-scale-width), var(--assistant-fit-scale-height)); - width: calc(var(--assistant-base-width-px) * var(--assistant-scale)); - height: calc(var(--assistant-base-height-px) * var(--assistant-scale)); - position: relative; - display: block; - background: transparent; - box-shadow: none; - border: 0; - border-radius: 30px; - backdrop-filter: none; - -webkit-backdrop-filter: none; - overflow: hidden; - isolation: isolate; -} - -.assistant-modal-stage { - position: relative; - width: var(--assistant-base-width-px); - height: var(--assistant-base-height-px); - display: grid; - grid-template-rows: auto minmax(0, 1fr); - transform: scale(var(--assistant-scale)); - transform-origin: top left; - border-radius: 30px; - background: - radial-gradient(circle at top right, rgba(16, 185, 129, 0.14), transparent 26%), - radial-gradient(circle at top left, rgba(59, 130, 246, 0.10), transparent 24%), - linear-gradient(180deg, rgba(241, 246, 245, 0.92) 0%, rgba(230, 237, 235, 0.88) 100%); - box-shadow: - 0 28px 72px rgba(15, 23, 42, 0.22), - 0 10px 28px rgba(15, 23, 42, 0.09), - inset 0 1px 0 rgba(255, 255, 255, 0.42); - border: 1px solid rgba(255, 255, 255, 0.44); - background-clip: padding-box; - overflow: hidden; - isolation: isolate; -} - -.assistant-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 22px 172px 18px 26px; - border-bottom: 1px solid rgba(203, 213, 225, 0.78); - background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%); -} - -.assistant-header-main { - display: flex; - align-items: flex-start; - gap: 14px; - min-width: 0; -} - -.assistant-badge { - min-height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 14px; - border-radius: 999px; - background: linear-gradient(135deg, #22c55e, #10b981); - color: #fff; - font-size: 12px; - font-weight: 800; - box-shadow: 0 8px 16px rgba(16, 185, 129, 0.14); - white-space: nowrap; -} - -.assistant-badge.warning { - background: rgba(249, 115, 22, 0.12); - color: #c2410c; -} - -.assistant-header h2 { - color: #0f172a; - font-size: 22px; - font-weight: 900; - letter-spacing: 0.01em; -} - -.assistant-header p { - margin-top: 4px; - color: #64748b; - font-size: 13px; - line-height: 1.55; -} - -.assistant-header-actions { - position: absolute; - top: calc(22px * var(--assistant-scale)); - right: calc(26px * var(--assistant-scale)); - z-index: 40; - display: flex; - align-items: center; - gap: calc(10px * var(--assistant-scale)); - pointer-events: auto; -} - -.assistant-toggle-btn, -.session-trash-btn { - width: calc(38px * var(--assistant-scale)); - height: calc(38px * var(--assistant-scale)); - display: grid; - place-items: center; - padding: 0; - border: 1px solid rgba(248, 113, 113, 0.28); - border-radius: calc(14px * var(--assistant-scale)); - flex: none; -} - -.assistant-toggle-btn { - border-color: rgba(16, 185, 129, 0.18); - background: rgba(245, 252, 249, 0.96); - color: #166534; - font-size: calc(16px * var(--assistant-scale)); - box-shadow: 0 8px 18px rgba(16, 185, 129, 0.1); -} - -.assistant-toggle-btn:hover:not(:disabled) { - background: rgba(236, 253, 245, 0.98); - border-color: rgba(16, 185, 129, 0.28); -} - -.assistant-toggle-btn:disabled, -.assistant-toggle-btn.disabled { - opacity: 0.48; - cursor: not-allowed; - box-shadow: none; -} - -.session-trash-btn { - background: rgba(254, 242, 242, 0.96); - color: #dc2626; - font-size: calc(16px * var(--assistant-scale)); - box-shadow: 0 8px 18px rgba(239, 68, 68, 0.12); -} - -.session-trash-btn:hover:not(:disabled) { - background: rgba(254, 226, 226, 0.98); - border-color: rgba(239, 68, 68, 0.34); -} - -.session-trash-btn:disabled { - opacity: 0.42; - cursor: not-allowed; - box-shadow: none; -} - -.assistant-close-btn, -.close-btn { - position: relative; - width: calc(38px * var(--assistant-scale)); - height: calc(38px * var(--assistant-scale)); - display: grid; - place-items: center; - padding: 0; - flex: none; - border: 1px solid rgba(193, 204, 216, 0.92); - border-radius: calc(14px * var(--assistant-scale)); - background: rgba(248, 251, 251, 0.94); - color: #475569; - font-size: calc(16px * var(--assistant-scale)); - box-shadow: 0 8px 18px rgba(148, 163, 184, 0.18); - cursor: pointer; - pointer-events: auto; - user-select: none; - -webkit-user-select: none; -} - -.assistant-close-btn { - z-index: 30; - pointer-events: auto; -} - -.assistant-close-btn i { - pointer-events: none; -} - -.assistant-close-btn:hover, -.close-btn:hover { - background: rgba(241, 245, 249, 0.98); - border-color: rgba(148, 163, 184, 0.34); - color: #0f172a; -} - -.assistant-layout { - min-height: 0; - display: flex; - padding: 16px; - align-items: stretch; -} - -.dialog-panel, -.insight-panel { - min-width: 0; - min-height: 0; - border: 1px solid rgba(189, 201, 214, 0.74); - border-radius: 24px; - background: rgba(248, 251, 251, 0.84); - box-shadow: - 0 14px 32px rgba(148, 163, 184, 0.16), - 0 2px 6px rgba(15, 23, 42, 0.05); - backdrop-filter: blur(22px); - -webkit-backdrop-filter: blur(22px); -} - -.dialog-panel { - flex: 1 1 auto; - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - overflow: hidden; - background: - radial-gradient(circle at top right, rgba(59, 130, 246, 0.07), transparent 22%), - linear-gradient(180deg, rgba(252, 253, 253, 0.88) 0%, rgba(243, 247, 248, 0.84) 100%); - transition: - transform 320ms cubic-bezier(0.22, 1, 0.36, 1), - box-shadow 320ms cubic-bezier(0.22, 1, 0.36, 1); - will-change: transform; -} - -.insight-panel-shell { - flex: none; - width: clamp(360px, 31vw, 440px); - min-width: 0; - margin-left: 16px; - overflow: hidden; - transition: - width 360ms cubic-bezier(0.22, 1, 0.36, 1), - margin-left 360ms cubic-bezier(0.22, 1, 0.36, 1); -} - -.insight-panel-shell.collapsed { - width: 0; - margin-left: 0; -} - -.dialog-toolbar { - display: flex; - gap: 12px; - flex-wrap: wrap; - padding: 16px 18px 12px; - border-bottom: 1px solid rgba(238, 242, 247, 0.9); -} - -.shortcut-chip { - min-height: 36px; - display: inline-flex; - align-items: center; - gap: 7px; - padding: 0 14px; - border: 1px solid rgba(219, 230, 240, 0.9); - border-radius: 999px; - background: rgba(255, 255, 255, 0.95); - color: #334155; - font-size: 12px; - font-weight: 750; - box-shadow: 0 4px 12px rgba(241, 245, 249, 0.78); - white-space: nowrap; -} - -.shortcut-chip i { - color: #059669; -} - -.shortcut-chip:disabled { - opacity: 0.48; - cursor: not-allowed; - box-shadow: none; -} - -.message-list { - min-height: 0; - display: grid; - align-content: start; - gap: 14px; - padding: 18px; - overflow-y: auto; -} - -.message-row { - display: grid; - grid-template-columns: 38px minmax(0, 1fr); - align-items: start; - gap: 12px; -} - -.message-row.user { - grid-template-columns: minmax(0, 1fr) 38px; -} - -.message-row.user .message-avatar { - order: 2; - background: transparent; -} - -.message-row.user .message-bubble { - order: 1; - justify-self: end; - background: linear-gradient(135deg, rgba(226, 238, 255, 0.98), rgba(242, 247, 255, 0.9)); - border-color: rgba(96, 165, 250, 0.24); - box-shadow: 0 14px 30px rgba(59, 130, 246, 0.08); -} - -.message-avatar { - width: 38px; - height: 38px; - display: grid; - place-items: center; - border-radius: 999px; - overflow: hidden; - background: transparent; - box-shadow: 0 10px 20px rgba(148, 163, 184, 0.24); -} - -.message-avatar img { - width: 100%; - height: 100%; - display: block; - object-fit: cover; -} - -.message-bubble { - max-width: min(100%, 720px); - padding: 14px 16px; - border: 1px solid rgba(210, 220, 230, 0.94); - border-radius: 20px; - background: rgba(253, 254, 254, 0.94); - color: #24324a; - line-height: 1.65; - box-shadow: 0 10px 22px rgba(226, 232, 240, 0.48); -} - -.message-meta { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 8px; -} - -.message-meta strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.message-meta time { - color: #94a3b8; - font-size: 12px; -} - -.message-bubble p { - color: #334155; - font-size: 14px; -} - -.message-answer-content { - display: grid; - gap: 12px; -} - -.message-answer-content :deep(p), -.message-answer-content :deep(ul), -.message-answer-content :deep(ol), -.message-answer-content :deep(blockquote), -.message-answer-content :deep(pre) { - margin: 0; -} - -.message-answer-markdown :deep(h1), -.message-answer-markdown :deep(h2), -.message-answer-markdown :deep(h3), -.message-answer-markdown :deep(h4) { - margin: 0; - color: #0f172a; - line-height: 1.35; -} - -.message-answer-markdown :deep(h1) { - font-size: 18px; -} - -.message-answer-markdown :deep(h2) { - font-size: 16px; -} - -.message-answer-markdown :deep(h3), -.message-answer-markdown :deep(h4) { - font-size: 14px; -} - -.message-answer-markdown :deep(p), -.message-answer-markdown :deep(li) { - line-height: 1.7; -} - -.message-answer-markdown :deep(ul), -.message-answer-markdown :deep(ol) { - padding-left: 22px; -} - -.message-answer-markdown :deep(strong) { - color: #0f172a; -} - -.message-answer-markdown :deep(blockquote) { - padding: 10px 12px; - border-left: 4px solid #93c5fd; - border-radius: 0 12px 12px 0; - background: #eff6ff; - color: #475569; -} - -.message-answer-markdown :deep(code) { - padding: 2px 6px; - border-radius: 6px; - background: #e2e8f0; - font-size: 12px; -} - -.message-answer-markdown :deep(pre) { - overflow-x: auto; - padding: 12px; - border-radius: 14px; - background: #0f172a; - color: #e2e8f0; -} - -.message-answer-markdown :deep(pre code) { - padding: 0; - background: transparent; - color: inherit; -} - -.message-answer-markdown :deep(a) { - color: #2563eb; - text-decoration: underline; -} - -.message-answer-markdown { - overflow-x: auto; -} - -.message-answer-markdown :deep(table) { - width: auto; - max-width: 100%; - border: 1px solid #dbe4ee; - border-radius: 16px; - border-collapse: collapse; - background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); - font-size: 13px; -} - -.message-answer-markdown :deep(th), -.message-answer-markdown :deep(td) { - padding: 10px 12px; - border-bottom: 1px solid #e2e8f0; - text-align: left; - white-space: nowrap; -} - -.message-answer-markdown :deep(th) { - background: #eff6ff; - color: #0f172a; - font-weight: 850; -} - -.message-answer-markdown :deep(td) { - color: #334155; - font-weight: 650; -} - -.message-answer-markdown :deep(tbody tr:last-child td) { - border-bottom: 0; -} - -.message-meta-row { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.message-meta-chip, -.capability-chip, -.risk-chip, -.message-risk-chip, -.message-action-chip { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - font-size: 12px; - font-weight: 800; -} - -.message-meta-chip, -.capability-chip { - background: #eef6ff; - color: #1d4ed8; -} - -.risk-chip, -.message-risk-chip { - background: #fff1f2; - color: #be123c; -} - -.message-action-chip { - background: #ecfdf5; - color: #059669; -} - -.message-detail-block { - margin-top: 14px; - display: grid; - gap: 10px; -} - -.message-detail-block > strong { - color: #0f172a; - font-size: 12px; - font-weight: 850; -} - -.message-citation-disclosure { - overflow: hidden; - border: 1px solid #dbe4ee; - border-radius: 16px; - background: #fbfdff; -} - -.message-citation-disclosure summary { - min-height: 42px; - display: flex; - align-items: center; - gap: 8px; - padding: 0 14px; - color: #0f172a; - cursor: pointer; - list-style: none; -} - -.message-citation-disclosure summary::-webkit-details-marker { - display: none; -} - -.message-citation-disclosure summary strong { - font-size: 12px; - font-weight: 850; -} - -.message-citation-disclosure summary span { - color: #64748b; - font-size: 12px; - font-weight: 750; -} - -.message-citation-disclosure summary i { - margin-left: auto; - color: #64748b; - font-size: 16px; - transition: transform 0.18s ease; -} - -.message-citation-disclosure[open] summary { - border-bottom: 1px solid #e2e8f0; -} - -.message-citation-disclosure[open] summary i { - transform: rotate(180deg); -} - -.message-citation-disclosure .message-citation-list { - padding: 12px; -} - -.expense-query-block { - gap: 10px; -} - -.expense-query-window-label { - margin: -4px 0 0; - color: #64748b; - font-size: 11px; - line-height: 1.5; -} - -.expense-query-summary-row { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.expense-query-summary-chip { - min-height: 24px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - font-size: 11px; - font-weight: 800; - background: #eef2ff; - color: #3730a3; -} - -.expense-query-summary-chip.draft { - background: #fef3c7; - color: #b45309; -} - -.expense-query-summary-chip.in_progress { - background: #dbeafe; - color: #1d4ed8; -} - -.expense-query-summary-chip.completed { - background: #dcfce7; - color: #15803d; -} - -.expense-query-summary-chip.other { - background: #f1f5f9; - color: #475569; -} - -.expense-query-record-list { - display: grid; - gap: 8px; -} - -.expense-query-record-list.compact .expense-query-record-card { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 12px; - border: 1px solid #dbe4ee; - border-radius: 14px; - background: #fbfdff; - cursor: pointer; - font: inherit; - text-align: left; - transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; -} - -.expense-query-record-list.compact .expense-query-record-card:hover { - transform: translateY(-1px); - border-color: #bfdbfe; - box-shadow: 0 8px 18px rgba(148, 163, 184, 0.12); -} - -.expense-query-record-card > i { - color: #94a3b8; - font-size: 16px; -} - -.expense-query-record-main { - min-width: 0; - display: grid; - gap: 5px; - flex: 1; -} - -.expense-query-record-top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.expense-query-record-top strong { - min-width: 0; - color: #0f172a; - font-size: 12px; - font-weight: 850; -} - -.expense-query-record-top strong, -.expense-query-record-card p, -.expense-query-record-meta span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.expense-query-record-status { - flex-shrink: 0; - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - font-size: 10px; - font-weight: 800; - background: #f1f5f9; - color: #475569; -} - -.expense-query-record-status.draft { - background: #fef3c7; - color: #b45309; -} - -.expense-query-record-status.in_progress { - background: #dbeafe; - color: #1d4ed8; -} - -.expense-query-record-status.completed { - background: #dcfce7; - color: #15803d; -} - -.expense-query-record-card p { - margin: 0; - color: #334155; - font-size: 12px; -} - -.expense-query-record-meta { - display: flex; - flex-wrap: wrap; - gap: 4px 10px; - color: #64748b; - font-size: 11px; - font-weight: 700; -} - -.expense-query-pager { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - margin-top: 2px; -} - -.expense-query-pager-btn { - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid #dbe4ee; - border-radius: 999px; - background: #fff; - color: #475569; - transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease; -} - -.expense-query-pager-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.expense-query-pager-btn:not(:disabled):hover { - border-color: #bfdbfe; - color: #2563eb; - background: #f8fbff; -} - -.expense-query-pager-dots { - display: inline-flex; - align-items: center; - gap: 8px; -} - -.expense-query-pager-dot { - width: 8px; - height: 8px; - padding: 0; - border: 0; - border-radius: 999px; - background: #cbd5e1; - transition: transform 0.2s ease, background 0.2s ease; -} - -.expense-query-pager-dot.active { - background: #2563eb; - transform: scale(1.15); -} - -.expense-query-empty { - min-height: 52px; - display: flex; - align-items: center; - gap: 10px; - padding: 0 14px; - border: 1px dashed #dbe4ee; - border-radius: 16px; - color: #64748b; - font-size: 12px; - font-weight: 700; -} - -.expense-query-empty i { - font-size: 18px; - color: #94a3b8; -} - -.expense-query-hint { - margin: 0; - color: #64748b; - font-size: 11px; - line-height: 1.6; -} - -.message-detail-chip-row, -.capability-chip-row { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.message-citation-list, -.citation-stack, -.action-list { - display: grid; - gap: 10px; -} - -.message-citation-card, -.citation-card, -.action-card { - padding: 12px 14px; - border: 1px solid #e2e8f0; - border-radius: 16px; - background: #f8fbff; -} - -.message-citation-card header, -.citation-card header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 6px; -} - -.message-citation-card header span, -.citation-card header strong, -.action-card strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.message-citation-card header small, -.citation-card header span { - color: #64748b; - font-size: 11px; - font-weight: 700; -} - -.message-citation-card p, -.citation-card p, -.action-card p, -.draft-preview pre { - margin: 0; - color: #475569; - font-size: 12px; - line-height: 1.65; -} - -.draft-preview { - margin-top: 12px; - padding: 12px 14px; - border: 1px solid #dbe3ec; - border-radius: 16px; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -.draft-preview header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 8px; -} - -.draft-preview header strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.draft-preview header span { - color: #b45309; - font-size: 12px; - font-weight: 800; -} - -.draft-preview pre { - white-space: pre-wrap; - word-break: break-word; - font-family: inherit; -} - -.message-files { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-top: 10px; -} - -.file-chip { - min-height: 28px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 10px; - border: 0; - border-radius: 999px; - background: #f1f5f9; - color: #475569; - font-size: 12px; - font-weight: 700; - max-width: 100%; -} - -.file-chip.active { - background: #eef6ff; - color: #2563eb; -} - -.composer { - padding: 0 18px 18px; - display: grid; - gap: 12px; -} - -.hidden-file-input { - display: none; -} - -.composer-shell { - min-width: 0; - border: 1px solid rgba(214, 225, 234, 0.95); - border-radius: 20px; - background: rgba(255, 255, 255, 0.98); - box-shadow: - 0 10px 22px rgba(226, 232, 240, 0.24), - 0 1px 4px rgba(15, 23, 42, 0.03); -} - -.composer-files-panel { - display: grid; - gap: 10px; - padding: 14px; - border: 1px solid rgba(226, 232, 240, 0.9); - border-radius: 18px; - background: linear-gradient(180deg, rgba(248, 251, 255, 0.92) 0%, rgba(242, 247, 251, 0.78) 100%); -} - -.composer-files-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.composer-files-head strong { - color: #0f172a; - font-size: 12px; - font-weight: 850; -} - -.composer-files-actions { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - justify-content: flex-end; -} - -.composer-file-link { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 0; - border: 0; - background: transparent; - color: #2563eb; - font-size: 11px; - font-weight: 800; -} - -.composer-file-link.danger { - color: #dc2626; -} - -.composer-file-link:disabled { - opacity: 0.48; -} - -.composer-file-chip-row { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.composer-file-chip { - max-width: min(100%, 280px); -} - -.file-chip.summary { - border: 1px dashed rgba(96, 165, 250, 0.34); - background: rgba(239, 246, 255, 0.92); - cursor: pointer; -} - -.file-chip-label { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-chip-remove { - width: 18px; - height: 18px; - display: grid; - place-items: center; - padding: 0; - border: 0; - border-radius: 999px; - background: rgba(255, 255, 255, 0.82); - color: inherit; - flex: none; -} - -.file-chip-remove:disabled { - opacity: 0.48; -} - -.composer-files-expanded { - display: grid; - gap: 8px; - max-height: 176px; - overflow-y: auto; - padding-right: 2px; -} - -.composer-expanded-file { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 12px; - border-radius: 14px; - border: 1px solid rgba(219, 230, 240, 0.92); - background: rgba(255, 255, 255, 0.88); -} - -.composer-expanded-file-copy { - min-width: 0; - display: flex; - align-items: center; - gap: 8px; - color: #334155; -} - -.composer-expanded-file-copy span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: 700; -} - -.composer-expanded-file-remove { - width: 28px; - height: 28px; - display: grid; - place-items: center; - border: 0; - border-radius: 10px; - background: rgba(248, 250, 252, 0.92); - color: #64748b; -} - -.composer-expanded-file-remove:disabled { - opacity: 0.48; -} - -.composer-shell textarea { - width: 100%; - min-height: 20px; - resize: none; - border: 0; - padding: 11px 14px; - background: transparent; - color: #0f172a; - font-size: 14px; - line-height: 1.5; -} - -.composer-shell textarea::placeholder { - color: #94a3b8; -} - -.composer-shell textarea:focus { - outline: none; -} - -.composer-shell textarea:disabled { - color: #94a3b8; -} - -.composer-row { - display: flex; - align-items: flex-end; - gap: 10px; -} - -.composer-row .composer-shell { - flex: 1 1 auto; -} - -.composer-side-btn, -.tool-btn, -.send-btn { - width: 44px; - height: 44px; - display: grid; - place-items: center; - border: 0; - border-radius: 999px; - flex: none; -} - -.tool-btn { - background: #ffffff; - color: #475569; - font-size: 18px; - border: 1px solid #dbe6f0; - box-shadow: 0 4px 12px rgba(241, 245, 249, 0.76); -} - -.tool-btn:disabled { - opacity: 0.48; - cursor: not-allowed; -} - -.send-btn { - background: linear-gradient(135deg, #22c55e, #10b981); - color: #fff; - font-size: 16px; - box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18); -} - -.send-btn:disabled { - opacity: 0.48; - cursor: not-allowed; - box-shadow: none; -} - -.insight-panel { - position: relative; - display: grid; - grid-template-rows: auto minmax(0, 1fr); - width: clamp(360px, 31vw, 440px); - height: 100%; - overflow: hidden; - background: - linear-gradient(180deg, rgba(239, 245, 243, 0.9) 0%, rgba(231, 238, 236, 0.84) 100%); - opacity: 1; - transform: translateX(0) scale(1); - transform-origin: right center; - transition: - opacity 260ms cubic-bezier(0.22, 1, 0.36, 1), - transform 320ms cubic-bezier(0.22, 1, 0.36, 1), - box-shadow 320ms cubic-bezier(0.22, 1, 0.36, 1); - will-change: transform, opacity; -} - -.insight-panel-shell.collapsed .insight-panel { - opacity: 0; - transform: translateX(44px) scale(0.985); - pointer-events: none; -} - -.insight-panel::before { - content: ""; - position: absolute; - top: -18px; - right: -34px; - width: 240px; - height: 150px; - border-radius: 0 0 0 140px; - background: - radial-gradient(circle at 0 100%, rgba(16, 185, 129, 0.14), transparent 54%), - linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(96, 165, 250, 0.06)); - opacity: 0.9; - pointer-events: none; -} - -.insight-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; - padding: 18px 18px 14px; - border-bottom: 1px solid rgba(205, 215, 224, 0.82); - position: relative; - z-index: 1; -} - -.insight-head.review-mode { - justify-content: space-between; -} - -.insight-head-eyebrow { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.insight-head-badge { - min-height: 24px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: rgba(240, 253, 244, 0.95); - color: #059669; - font-size: 11px; - font-weight: 800; - border: 1px solid rgba(16, 185, 129, 0.12); -} - -.review-insight-title-row { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.review-insight-title-copy { - min-width: 0; -} - -.review-insight-title-copy h3 { - margin: 0; -} - -.review-insight-tools { - display: inline-flex; - align-items: center; - gap: 8px; - flex: 0 0 auto; - align-self: center; -} - -.review-insight-switch-icon-btn { - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - border: 1px solid rgba(203, 213, 225, 0.92); - background: rgba(248, 250, 252, 0.96); - color: #94a3b8; - font-size: 14px; - flex: 0 0 auto; - transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease, transform 0.18s ease; -} - -.review-insight-switch-icon-btn.available { - border-color: rgba(245, 158, 11, 0.28); - background: rgba(255, 247, 237, 0.94); - color: #d97706; -} - -.review-insight-switch-icon-btn.active { - border-color: rgba(217, 119, 6, 0.42); - background: rgba(254, 243, 199, 0.98); - color: #b45309; - box-shadow: 0 6px 14px rgba(245, 158, 11, 0.16); -} - -.review-insight-switch-icon-btn.risk.available { - border-color: rgba(239, 68, 68, 0.28); - background: rgba(254, 242, 242, 0.96); - color: #dc2626; -} - -.review-insight-switch-icon-btn.risk.active { - border-color: rgba(220, 38, 38, 0.42); - background: rgba(254, 226, 226, 0.98); - color: #b91c1c; - box-shadow: 0 6px 14px rgba(239, 68, 68, 0.16); -} - -.review-insight-switch-icon-btn:hover:not(:disabled) { - transform: translateY(-1px); -} - -.review-insight-switch-icon-btn:disabled { - cursor: not-allowed; - opacity: 1; - color: #cbd5e1; - background: rgba(248, 250, 252, 0.9); -} - -.intent-pill { - min-height: 30px; - display: inline-flex; - align-items: center; - padding: 0 13px; - border-radius: 999px; - font-size: 12px; - font-weight: 800; -} - -.intent-pill.welcome { - background: #eef2ff; - color: #4f46e5; -} - -.intent-pill.draft { - background: #ecfdf5; - color: #059669; -} - -.intent-pill.approval { - background: #fff7ed; - color: #ea580c; -} - -.intent-pill.recognition { - background: #eff6ff; - color: #2563eb; -} - -.intent-pill.note { - background: #fdf2f8; - color: #db2777; -} - -.insight-head h3 { - margin-top: 10px; - color: #0f172a; - font-size: 19px; - font-weight: 900; - line-height: 1.25; -} - -.insight-head p { - margin-top: 6px; - color: #64748b; - font-size: 12px; - line-height: 1.6; -} - -.confidence-card { - min-width: 92px; - padding: 10px 12px; - border-radius: 14px; - background: rgba(250, 252, 252, 0.9); - border: 1px solid rgba(202, 213, 223, 0.9); - box-shadow: 0 8px 18px rgba(203, 213, 225, 0.3); - text-align: right; -} - -.confidence-card span { - display: block; - color: #94a3b8; - font-size: 11px; - font-weight: 800; -} - -.confidence-card strong { - display: block; - margin-top: 4px; - color: #0f172a; - font-size: 19px; - font-weight: 900; -} - -.insight-body { - min-height: 0; - display: grid; - align-content: start; - gap: 12px; - padding: 14px 18px 18px; - overflow-y: auto; - position: relative; - z-index: 1; -} - -.review-side-card { - display: grid; - gap: 10px; - padding: 14px; - border-radius: 18px; - border: 1px solid rgba(197, 209, 221, 0.88); - background: rgba(249, 251, 251, 0.88); - box-shadow: 0 10px 20px rgba(226, 232, 240, 0.3); -} - -.review-side-overview-card { - gap: 12px; - background: linear-gradient(180deg, rgba(251, 252, 252, 0.92) 0%, rgba(240, 246, 244, 0.86) 100%); -} - -.review-side-intent-row { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - color: #475569; - font-size: 13px; -} - -.review-side-intent-row i { - color: #059669; - font-size: 16px; -} - -.review-side-intent-row strong { - color: #0f172a; - font-size: 14px; - font-weight: 850; -} - -.review-side-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.review-side-grid.compact { - gap: 8px; -} - -.review-side-metric-card { - display: grid; - grid-template-columns: 32px minmax(0, 1fr); - gap: 8px; - align-items: start; - padding: 12px; - border-radius: 14px; - border: 1px solid rgba(206, 216, 226, 0.88); - background: rgba(251, 252, 252, 0.82); - position: relative; - cursor: pointer; - transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; -} - -.review-side-metric-card.invalid { - border-color: rgba(239, 68, 68, 0.34); - background: rgba(254, 242, 242, 0.72); -} - -.review-side-metric-card.editable:hover, -.review-side-metric-card.editing { - border-color: rgba(16, 185, 129, 0.34); - background: rgba(248, 252, 250, 0.92); - transform: translateY(-1px); -} - -.review-side-metric-icon { - width: 32px; - height: 32px; - display: grid; - place-items: center; - border-radius: 10px; - background: rgba(240, 253, 244, 0.95); - color: #059669; - font-size: 15px; -} - -.review-side-metric-copy { - display: grid; - gap: 4px; -} - -.review-side-metric-copy small { - color: #64748b; - font-size: 11px; - font-weight: 800; -} - -.review-side-metric-copy strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; - line-height: 1.35; - word-break: break-word; -} - -.review-inline-input { - width: 100%; - min-height: 34px; - padding: 0 10px; - border: 1px solid rgba(16, 185, 129, 0.2); - border-radius: 10px; - background: rgba(255, 255, 255, 0.96); - color: #0f172a; - font-size: 12px; - font-weight: 700; -} - -.review-inline-input.invalid { - border-color: rgba(239, 68, 68, 0.4); - color: #b91c1c; - box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.08); -} - -.review-inline-input:focus { - outline: none; - border-color: rgba(16, 185, 129, 0.42); - box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.08); -} - -.review-inline-select-list { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.review-inline-select-option { - min-height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 10px; - border-radius: 999px; - border: 1px solid rgba(203, 213, 225, 0.92); - background: rgba(255, 255, 255, 0.96); - color: #475569; - font-size: 11px; - font-weight: 700; -} - -.review-inline-select-option.active { - border-color: rgba(16, 185, 129, 0.36); - background: rgba(240, 253, 244, 0.94); - color: #047857; -} - -.review-inline-error { - color: #dc2626; - font-size: 11px; - font-weight: 800; - line-height: 1.45; -} - -.review-side-edit-hint { - position: absolute; - top: 8px; - right: 8px; - min-height: 20px; - display: inline-flex; - align-items: center; - padding: 0 6px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.92); - border: 1px solid rgba(226, 232, 240, 0.92); - color: #94a3b8; - font-size: 10px; - font-weight: 800; - opacity: 0; - transition: opacity 0.18s ease; -} - -.review-side-edit-hint.upload { - color: #059669; -} - -.review-side-metric-card:hover .review-side-edit-hint, -.review-side-metric-card.editing .review-side-edit-hint { - opacity: 1; -} - -.review-side-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.review-side-head strong { - color: #0f172a; - font-size: 14px; - font-weight: 900; -} - -.review-side-head-copy { - display: grid; - gap: 4px; - min-width: 0; -} - -.review-side-head-copy p { - margin: 0; - color: #64748b; - font-size: 11px; - line-height: 1.55; -} - -.review-side-confidence { - color: #10b981; - font-size: 12px; - font-weight: 900; -} - -.review-side-category-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.review-side-category-card { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 8px; - padding: 12px; - min-height: 66px; - border-radius: 14px; - border: 1px solid rgba(226, 232, 240, 0.94); - background: rgba(255, 255, 255, 0.68); - cursor: pointer; - transition: border-color 0.18s ease, background 0.18s ease; -} - -.review-side-category-card.active { - border-color: rgba(52, 211, 153, 0.62); - background: rgba(240, 253, 244, 0.9); - box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08); -} - -.review-side-category-copy { - display: grid; - gap: 4px; - min-width: 0; -} - -.review-side-category-copy strong { - color: #0f172a; - font-size: 12px; - font-weight: 850; - line-height: 1.35; - white-space: nowrap; -} - -.review-side-category-copy p { - margin: 0; - color: #64748b; - font-size: 10px; - line-height: 1.4; -} - -.review-side-group-check { - color: #10b981; - font-size: 18px; -} - -.review-other-category-popover { - display: flex; - flex-wrap: wrap; - gap: 8px; - padding-top: 2px; -} - -.review-other-category-option { - min-height: 30px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 12px; - border-radius: 999px; - border: 1px solid rgba(203, 213, 225, 0.92); - background: rgba(255, 255, 255, 0.94); - color: #475569; - font-size: 11px; - font-weight: 750; -} - -.review-other-category-option.active { - border-color: rgba(16, 185, 129, 0.36); - background: rgba(240, 253, 244, 0.94); - color: #047857; -} - -.review-side-risk-card { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.84) 0%, rgba(255, 249, 238, 0.8) 100%); -} - -.review-side-risk-score { - color: #f97316; - font-size: 13px; - font-weight: 900; -} - -.review-side-risk-score.empty { - color: #94a3b8; -} - -.review-side-risk-summary { - margin: 0; - color: #334155; - font-size: 12px; - line-height: 1.6; -} - -.review-side-risk-list { - display: grid; - gap: 8px; - margin: 0; - padding-left: 16px; - color: #475569; - font-size: 12px; - line-height: 1.6; -} - -.review-side-link { - width: fit-content; - display: inline-flex; - align-items: center; - gap: 4px; - padding: 0; - border: 0; - background: transparent; - color: #059669; - font-size: 12px; - font-weight: 850; -} - -.review-side-link:disabled { - opacity: 0.5; -} - -.review-side-empty { - display: grid; - justify-items: start; - gap: 8px; - padding: 14px; - border: 1px dashed rgba(203, 213, 225, 0.92); - border-radius: 16px; - background: rgba(255, 255, 255, 0.52); -} - -.review-side-empty-icon { - width: 36px; - height: 36px; - display: grid; - place-items: center; - border-radius: 12px; - background: rgba(240, 244, 248, 0.96); - color: #94a3b8; - font-size: 18px; -} - -.review-side-empty strong { - color: #475569; - font-size: 13px; - font-weight: 850; -} - -.review-side-empty p { - margin: 0; - color: #94a3b8; - font-size: 12px; - line-height: 1.6; -} - -.review-side-save-pill { - position: sticky; - bottom: 0; - justify-self: end; - min-height: 36px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 14px; - border: 1px solid rgba(16, 185, 129, 0.22); - border-radius: 999px; - background: rgba(255, 255, 255, 0.94); - color: #059669; - font-size: 12px; - font-weight: 850; - box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); -} - -.review-side-save-pill:disabled { - opacity: 0.5; - box-shadow: none; -} - -.review-document-switch-card { - gap: 14px; -} - -.review-ticket-drawer { - min-height: 0; -} - -.review-document-switch-head { - align-items: flex-start; -} - -.review-document-nav { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 4px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.92); - border: 1px solid rgba(226, 232, 240, 0.92); - white-space: nowrap; -} - -.review-document-nav span { - color: #334155; - font-size: 11px; - font-weight: 850; -} - -.review-document-nav-btn { - width: 28px; - height: 28px; - display: grid; - place-items: center; - border: 0; - border-radius: 999px; - background: rgba(241, 245, 249, 0.96); - color: #334155; -} - -.review-document-nav-btn:disabled { - opacity: 0.4; -} - -.review-document-stage { - display: grid; - gap: 12px; - min-height: 0; -} - -.review-document-stage-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.review-document-stage-copy { - min-width: 0; - display: grid; - gap: 6px; -} - -.review-document-stage-copy strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; - line-height: 1.5; - word-break: break-word; -} - -.review-document-index-chip { - width: fit-content; - min-height: 24px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: rgba(236, 253, 245, 0.92); - color: #059669; - font-size: 11px; - font-weight: 850; -} - -.review-document-meta-chip-row { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.review-document-meta-chip { - min-height: 26px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: rgba(248, 250, 252, 0.94); - border: 1px solid rgba(226, 232, 240, 0.92); - color: #475569; - font-size: 11px; - font-weight: 800; -} - -.review-document-meta-chip.confidence { - background: rgba(236, 253, 245, 0.92); - color: #047857; - border-color: rgba(167, 243, 208, 0.92); -} - -.review-document-scroll { - display: grid; - gap: 12px; - max-height: 430px; - overflow-y: auto; - padding-right: 4px; -} - -.review-document-preview-card { - min-height: 168px; - overflow: hidden; - border-radius: 16px; - border: 1px solid rgba(226, 232, 240, 0.94); - background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); -} - -.review-document-preview-card.clickable { - cursor: zoom-in; -} - -.review-document-preview-card.clickable img { - transition: transform 0.18s ease; -} - -.review-document-preview-card.clickable:hover img { - transform: scale(1.02); -} - -.review-document-preview-card.image img { - display: block; - width: 100%; - height: 188px; - object-fit: cover; -} - -.review-document-preview-placeholder { - min-height: 168px; - display: grid; - place-items: center; - gap: 6px; - padding: 18px; - text-align: center; -} - -.review-document-preview-placeholder i { - color: #64748b; - font-size: 34px; -} - -.review-document-preview-placeholder strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.review-document-preview-placeholder p { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.65; -} - -.review-document-edit-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.review-document-edit-field { - display: grid; - gap: 8px; -} - -.review-document-edit-field span { - color: #334155; - font-size: 12px; - font-weight: 800; -} - -.review-document-edit-field input, -.review-document-edit-field textarea { - width: 100%; - border: 1px solid rgba(219, 230, 240, 0.96); - border-radius: 14px; - background: rgba(255, 255, 255, 0.96); - color: #0f172a; - font-size: 13px; - line-height: 1.6; - padding: 10px 12px; - resize: vertical; -} - -.review-document-edit-field input:focus, -.review-document-edit-field textarea:focus { - outline: none; - border-color: rgba(16, 185, 129, 0.36); - box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.08); -} - -.review-document-edit-field textarea { - min-height: 88px; -} - -.review-document-warning-list { - display: grid; - gap: 8px; -} - -.review-document-warning-item { - display: grid; - grid-template-columns: 18px minmax(0, 1fr); - gap: 8px; - align-items: start; - padding: 10px 12px; - border-radius: 14px; - background: rgba(255, 247, 237, 0.92); - border: 1px solid rgba(253, 186, 116, 0.6); - color: #c2410c; - font-size: 12px; - line-height: 1.6; -} - -.review-side-empty.compact { - padding: 12px; -} - -.insight-card { - padding: 16px; - border: 1px solid #e7eef6; - border-radius: 20px; - background: rgba(255, 255, 255, 0.95); - box-shadow: 0 14px 24px rgba(241, 245, 249, 0.86); -} - -.insight-card.primary { - background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%); -} - -.card-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 14px; -} - -.card-head h4 { - color: #0f172a; - font-size: 15px; - font-weight: 850; -} - -.knowledge-question-list { - display: grid; - gap: 10px; -} - -.knowledge-question-btn { - width: 100%; - display: grid; - grid-template-columns: 28px minmax(0, 1fr) 18px; - align-items: center; - gap: 10px; - padding: 12px 14px; - border: 1px solid rgba(226, 232, 240, 0.92); - border-radius: 16px; - background: rgba(248, 250, 252, 0.86); - color: #1e293b; - text-align: left; - transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; -} - -.knowledge-question-btn:hover:not(:disabled) { - border-color: rgba(16, 185, 129, 0.3); - background: rgba(240, 253, 244, 0.9); - transform: translateY(-1px); -} - -.knowledge-question-btn:disabled { - opacity: 0.48; - cursor: not-allowed; - transform: none; -} - -.knowledge-question-btn i { - justify-self: end; - color: #059669; - font-size: 16px; -} - -.knowledge-question-index { - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - background: rgba(226, 232, 240, 0.9); - color: #0f172a; - font-size: 12px; - font-weight: 850; -} - -.knowledge-question-index.gold { - background: linear-gradient(135deg, #fbbf24, #f59e0b); - color: #7c2d12; - box-shadow: 0 6px 14px rgba(245, 158, 11, 0.22); -} - -.knowledge-question-index.silver { - background: linear-gradient(135deg, #e2e8f0, #cbd5e1); - color: #334155; - box-shadow: 0 6px 14px rgba(148, 163, 184, 0.18); -} - -.knowledge-question-index.bronze { - background: linear-gradient(135deg, #fdba74, #ea580c); - color: #7c2d12; - box-shadow: 0 6px 14px rgba(234, 88, 12, 0.18); -} - -.knowledge-question-copy { - min-width: 0; - color: #334155; - font-size: 13px; - font-weight: 750; - line-height: 1.5; -} - -.status-pill { - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - font-size: 12px; - font-weight: 800; - white-space: nowrap; -} - -.status-pill.success { - background: #ecfdf5; - color: #059669; -} - -.status-pill.warning { - background: #fff7ed; - color: #ea580c; -} - -.status-pill.note { - background: #fdf2f8; - color: #db2777; -} - -.metric-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.metric-grid.single { - grid-template-columns: 1fr; -} - -.metric-item { - padding: 12px 14px; - border-radius: 16px; - background: #f8fafc; -} - -.metric-item span { - display: block; - color: #94a3b8; - font-size: 11px; - font-weight: 800; -} - -.metric-item strong { - display: block; - margin-top: 6px; - color: #0f172a; - font-size: 14px; - font-weight: 850; - line-height: 1.5; -} - -.timeline-list, -.bullet-list { - display: grid; - gap: 12px; - padding: 0; - margin: 0; - list-style: none; -} - -.timeline-list li { - display: grid; - grid-template-columns: 14px minmax(0, 1fr); - gap: 12px; - align-items: start; -} - -.timeline-dot { - width: 10px; - height: 10px; - margin-top: 5px; - border-radius: 999px; - background: #cbd5e1; -} - -.timeline-list li.done .timeline-dot, -.timeline-list li.current .timeline-dot { - background: #10b981; -} - -.timeline-list strong { - display: block; - color: #0f172a; - font-size: 13px; - font-weight: 800; -} - -.timeline-list p, -.bullet-list li, -.welcome-card p, -.note-block p { - color: #64748b; - font-size: 13px; - line-height: 1.6; -} - -.receipt-list { - display: grid; - gap: 10px; -} - -.receipt-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - align-items: center; - padding: 12px 14px; - border-radius: 16px; - background: #f8fafc; -} - -.receipt-row strong, -.welcome-card strong, -.note-block strong { - color: #0f172a; - font-size: 14px; - font-weight: 850; -} - -.action-card { - background: #fff; -} - -.receipt-row p, -.receipt-row span { - color: #64748b; - font-size: 12px; -} - -.receipt-side { - text-align: right; -} - -.receipt-side strong { - display: block; -} - -.review-message-block { - margin-top: 12px; -} - -.review-summary { - margin: 0; - color: #1f2937; - font-size: 13px; - line-height: 1.75; - white-space: pre-line; -} - -.review-card-shell { - display: grid; - gap: 12px; - padding: 15px; - border-radius: 20px; - border: 1px solid rgba(16, 185, 129, 0.14); - background: - radial-gradient(circle at top right, rgba(34, 197, 94, 0.08), transparent 28%), - linear-gradient(180deg, #fbfffd 0%, #f6fbf9 100%); - box-shadow: 0 8px 20px rgba(226, 232, 240, 0.28); -} - -.review-flow-card { - display: grid; - gap: 10px; - padding-top: 2px; - border-top: 1px solid rgba(226, 232, 240, 0.72); -} - -.review-disclosure-card { - display: grid; - gap: 0; - border-top: 1px solid rgba(226, 232, 240, 0.72); - padding-top: 6px; -} - -.review-disclosure-card summary { - list-style: none; -} - -.review-disclosure-card summary::-webkit-details-marker { - display: none; -} - -.review-disclosure-summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; - border-radius: 16px; - border: 1px solid rgba(226, 232, 240, 0.92); - background: rgba(255, 255, 255, 0.78); - cursor: pointer; - transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; -} - -.review-disclosure-summary:hover { - border-color: rgba(16, 185, 129, 0.2); - background: rgba(255, 255, 255, 0.92); - box-shadow: 0 6px 16px rgba(226, 232, 240, 0.24); -} - -.review-disclosure-copy { - min-width: 0; - display: grid; - gap: 4px; -} - -.review-disclosure-copy strong { - color: #0f172a; - font-size: 12px; - font-weight: 900; - line-height: 1.4; -} - -.review-disclosure-copy p { - margin: 0; - color: #64748b; - font-size: 11px; - line-height: 1.55; -} - -.review-disclosure-toggle { - width: 28px; - height: 28px; - flex: none; - display: grid; - place-items: center; - border-radius: 999px; - background: rgba(240, 253, 244, 0.86); - color: #059669; - font-size: 16px; - transition: transform 0.18s ease, background 0.18s ease; -} - -.review-disclosure-card[open] .review-disclosure-toggle { - transform: rotate(180deg); - background: rgba(220, 252, 231, 0.92); -} - -.review-disclosure-body { - display: grid; - gap: 10px; - padding: 12px 4px 0; -} - -.review-card-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.review-card-head-main { - min-width: 0; - display: flex; - align-items: flex-start; - gap: 10px; -} - -.review-card-icon { - width: 32px; - height: 32px; - display: grid; - place-items: center; - border-radius: 10px; - background: linear-gradient(135deg, #22c55e, #10b981); - color: #fff; - font-size: 16px; - box-shadow: 0 8px 16px rgba(16, 185, 129, 0.16); -} - -.review-card-head-copy { - display: grid; - gap: 4px; -} - -.review-card-head-copy strong { - color: #0f172a; - font-size: 14px; - font-weight: 900; - line-height: 1.35; -} - -.review-card-head-copy p { - margin: 0; - color: #64748b; - font-size: 11px; - line-height: 1.55; -} - -.review-card-state { - min-height: 26px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - font-size: 11px; - font-weight: 850; - white-space: nowrap; -} - -.review-card-state.ready { - background: rgba(240, 253, 244, 0.95); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.14); -} - -.review-card-state.pending { - background: rgba(255, 251, 235, 0.95); - color: #b45309; - border: 1px solid rgba(245, 158, 11, 0.16); -} - -.review-section-card { - display: grid; - gap: 10px; - padding: 12px 14px; - border-radius: 16px; - border: 1px solid rgba(226, 232, 240, 0.92); - background: rgba(255, 255, 255, 0.76); -} - -.review-section-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.review-section-head strong { - color: #0f172a; - font-size: 12px; - font-weight: 900; -} - -.review-section-head span { - min-height: 22px; - display: inline-flex; - align-items: center; - padding: 0 8px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.92); - border: 1px solid #e2e8f0; - color: #475569; - font-size: 10px; - font-weight: 800; -} - -.review-alert-card { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.82) 0%, rgba(251, 248, 243, 0.82) 100%); -} - -/* 已删除:review-alert-chip-row 相关样式(冗余气泡) */ -/* 已删除:主对话框中的风险提示(与右侧边栏重复,已移除) */ - -/* 风险提示样式已统一到 review-pending-item */ -.review-risk-brief-list { - display: none; /* 隐藏原有的独立风险提示列表 */ -} - -.review-risk-brief { - display: none; /* 隐藏原有的独立风险提示项 */ -} - -.review-pending-list { - display: grid; - gap: 8px; -} - -.review-pending-list.plain { - gap: 0; -} - -.review-pending-item { - display: grid; - grid-template-columns: 36px minmax(0, 1fr) auto; - gap: 10px; - align-items: center; - padding: 11px 12px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.88); - border: 1px solid rgba(226, 232, 240, 0.92); -} - -.review-pending-list.plain .review-pending-item { - padding: 10px 0; - border: 0; - border-radius: 0; - background: transparent; - border-bottom: 1px solid rgba(226, 232, 240, 0.7); -} - -.review-pending-list.plain .review-pending-item:last-child { - border-bottom: 0; - padding-bottom: 0; -} - -.review-pending-list.plain .review-pending-item:first-child { - padding-top: 2px; -} - -.review-pending-icon { - width: 36px; - height: 36px; - display: grid; - place-items: center; - border-radius: 10px; - background: rgba(236, 253, 245, 0.95); - color: #059669; - font-size: 16px; -} - -/* 风险级别的图标样式(已删除主对话框中的风险提示,保留样式备用) */ -.review-pending-icon.high { - background: rgba(254, 226, 226, 0.95); - color: #dc2626; -} - -.review-pending-icon.warning { - background: rgba(255, 237, 213, 0.95); - color: #ea580c; -} - -.review-pending-list.plain .review-pending-icon { - background: rgba(236, 253, 245, 0.62); -} - -.review-pending-list.plain .review-pending-icon.high { - background: rgba(254, 226, 226, 0.62); -} - -.review-pending-list.plain .review-pending-icon.warning { - background: rgba(255, 237, 213, 0.62); -} - -.review-pending-copy { - display: grid; - gap: 4px; -} - -.review-pending-copy strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.review-pending-copy p { - margin: 0; - color: #64748b; - font-size: 11px; - line-height: 1.5; -} - -.review-pending-status { - min-height: 24px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 10px; - border-radius: 999px; - font-size: 10px; - font-weight: 800; - white-space: nowrap; -} - -.review-pending-status.warning { - background: rgba(255, 241, 242, 0.96); - color: #e11d48; - border: 1px solid #fecdd3; -} - -.review-pending-status.danger { - background: rgba(254, 242, 242, 0.96); - color: #dc2626; - border: 1px solid #fca5a5; -} - -.review-pending-status.ready { - background: rgba(240, 253, 244, 0.96); - color: #059669; - border: 1px solid #86efac; -} - -.review-footer-actions { - display: grid; - gap: 8px; - padding-top: 6px; - border-top: 1px solid rgba(226, 232, 240, 0.72); -} - -.review-footer-btn-row { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.review-footer-btn { - min-height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 14px; - border-radius: 12px; - border: 1px solid #dbe6f0; - background: rgba(255, 255, 255, 0.92); - color: #334155; - font-size: 12px; - font-weight: 800; - box-shadow: 0 3px 10px rgba(241, 245, 249, 0.58); -} - -.review-footer-btn.primary { - border-color: rgba(16, 185, 129, 0.26); - background: linear-gradient(135deg, #10b981, #059669); - color: #fff; - box-shadow: 0 6px 14px rgba(16, 185, 129, 0.16); -} - -.review-footer-btn:disabled { - cursor: not-allowed; - opacity: 0.6; - box-shadow: none; -} - -.review-summary { - margin: 0; - color: #1f2937; - font-size: 14px; - line-height: 1.7; -} - -.review-inline-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: flex-start; -} - -.review-inline-btn, -.primary-dialog-btn, -.secondary-dialog-btn, -.danger-dialog-btn { - min-height: 38px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 16px; - border-radius: 999px; - font-size: 12px; - font-weight: 800; -} - -.review-inline-btn { - border: 1px solid #dbe6f0; - background: #fff; - color: #334155; -} - -.review-inline-btn.primary, -.primary-dialog-btn { - border: 1px solid rgba(16, 185, 129, 0.22); - background: linear-gradient(135deg, #10b981, #059669); - color: #fff; - box-shadow: 0 10px 22px rgba(16, 185, 129, 0.18); -} - -.review-inline-btn.secondary, -.secondary-dialog-btn { - border: 1px solid #dbe6f0; - background: #fff; - color: #334155; -} - -.danger-dialog-btn { - border: 1px solid rgba(239, 68, 68, 0.22); - background: linear-gradient(135deg, #ef4444, #dc2626); - color: #fff; - box-shadow: 0 10px 22px rgba(239, 68, 68, 0.18); -} - -.review-inline-btn:disabled, -.primary-dialog-btn:disabled, -.secondary-dialog-btn:disabled, -.danger-dialog-btn:disabled { - cursor: not-allowed; - opacity: 0.62; - box-shadow: none; -} - -.review-inline-note { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.6; -} - -.review-inline-guidance { - margin: 0; - color: #0f766e; - font-size: 12px; - line-height: 1.7; -} - -.review-status-banner { - display: grid; - gap: 8px; - padding: 14px 16px; - border-radius: 18px; - border: 1px solid #dbeafe; - background: linear-gradient(180deg, #f8fbff 0%, #f0f7ff 100%); -} - -.review-status-banner.ready { - border-color: #bbf7d0; - background: linear-gradient(180deg, #f5fffa 0%, #ecfdf5 100%); -} - -.review-status-banner.pending { - border-color: #fde68a; - background: linear-gradient(180deg, #fffdf7 0%, #fffbeb 100%); -} - -.review-status-tag { - width: fit-content; - min-height: 26px; - display: inline-flex; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.86); - color: #0f172a; - font-size: 12px; - font-weight: 850; - border: 1px solid rgba(148, 163, 184, 0.22); -} - -.review-inline-section { - display: grid; - gap: 10px; - padding: 14px 16px; - border-radius: 18px; - border: 1px solid #e2e8f0; - background: rgba(255, 255, 255, 0.88); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.66); -} - -.review-inline-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.review-inline-head > strong { - color: #0f172a; - font-size: 12px; - font-weight: 850; -} - -.review-inline-head > span { - min-height: 24px; - display: inline-flex; - align-items: center; - padding: 0 9px; - border-radius: 999px; - background: #fff; - color: #475569; - font-size: 11px; - font-weight: 800; - border: 1px solid #e2e8f0; -} - -.review-inline-caption { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.65; -} - -.review-inline-list { - display: grid; - gap: 8px; -} - -.review-missing-chip-row { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.review-missing-chip { - min-height: 30px; - display: inline-flex; - align-items: center; - padding: 0 12px; - border-radius: 999px; - background: #fff; - color: #0f172a; - font-size: 12px; - font-weight: 800; - border: 1px solid #fed7aa; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); -} - -.review-inline-item { - display: grid; - gap: 4px; - padding: 10px 12px; - border-radius: 14px; - border: 1px solid #e2e8f0; - background: #fff; -} - -.review-inline-item.warning { - background: #fff7ed; - border-color: #fed7aa; -} - -.review-inline-item.high { - background: #fff1f2; - border-color: #fecdd3; -} - -.review-inline-item span { - color: #0f172a; - font-size: 12px; - font-weight: 800; -} - -.review-inline-item p { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.65; -} - -.review-inline-footer { - display: grid; - gap: 10px; - padding-top: 2px; - border-top: 1px dashed rgba(203, 213, 225, 0.78); -} - -.review-mini-grid, -.review-slot-grid, -.review-doc-field-grid { - display: grid; - gap: 10px; -} - -.review-mini-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.review-slot-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.review-slot-card, -.review-doc-field-card, -.review-brief-card, -.review-claim-card, -.review-document-card { - border: 1px solid #e2e8f0; - border-radius: 16px; - background: #f8fbff; -} - -.review-slot-card { - display: grid; - gap: 8px; - padding: 12px 14px; -} - -.review-slot-card.compact { - gap: 4px; - padding: 10px 12px; -} - -.review-slot-card header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.review-slot-card span, -.review-doc-field-card span, -.review-brief-card strong, -.review-document-card header span { - color: #64748b; - font-size: 11px; - font-weight: 800; -} - -.review-slot-card strong, -.review-doc-field-card strong, -.review-claim-card strong, -.review-document-card header strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; -} - -.review-slot-card p, -.review-brief-card p, -.review-claim-card p, -.review-document-card p { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.6; -} - -.review-slot-card.missing { - border-color: #fecdd3; - background: #fff7f7; -} - -.review-slot-card.inferred { - border-color: #dbeafe; - background: #f8fbff; -} - -.review-slot-meta-list { - display: grid; - gap: 8px; -} - -.review-slot-meta-item { - padding: 9px 10px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.82); - border: 1px solid rgba(226, 232, 240, 0.9); -} - -.review-slot-meta-item span { - color: #94a3b8; - font-size: 11px; - font-weight: 800; -} - -.review-slot-meta-item strong { - display: block; - margin-top: 4px; - font-size: 12px; -} - -.review-brief-list, -.review-claim-list, -.review-document-list { - display: grid; - gap: 10px; -} - -.review-brief-card, -.review-claim-card, -.review-document-card { - padding: 12px 14px; -} - -.review-brief-card.warning { - background: #fff7ed; - border-color: #fed7aa; -} - -.review-brief-card.high { - background: #fff1f2; - border-color: #fecdd3; -} - -.review-claim-card header, -.review-document-card header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 8px; -} - -.review-document-card { - display: grid; - gap: 10px; -} - -.document-preview { - min-height: 124px; - overflow: hidden; - border-radius: 14px; - background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); - border: 1px dashed #dbe3ec; -} - -.document-preview.image img { - display: block; - width: 100%; - height: 180px; - object-fit: cover; -} - -.document-preview-placeholder { - min-height: 124px; - display: grid; - place-items: center; - gap: 6px; - color: #64748b; - text-align: center; -} - -.document-preview-placeholder i { - font-size: 28px; -} - -.review-doc-field-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.review-doc-field-card { - padding: 10px 12px; -} - -.action-list.compact { - grid-template-columns: 1fr; -} - -.action-card.primary { - border-color: #bbf7d0; - background: #f0fdf4; -} - -.action-card.secondary { - background: #fff; -} - -.action-card.warning { - border-color: #fed7aa; - background: #fff7ed; -} - -.note-block { - display: grid; - gap: 8px; - padding: 14px; - border-radius: 16px; - background: #f8fafc; -} - -.note-block span { - color: #94a3b8; - font-size: 11px; - font-weight: 800; -} - -.review-conclusion strong { - font-size: 15px; - line-height: 1.6; -} - -.insight-text-section { - display: grid; - gap: 12px; - padding: 2px 0 0; -} - -.insight-text-section h4 { - color: #0f172a; - font-size: 15px; - font-weight: 850; -} - -.insight-text-list, -.review-document-plain-list { - display: grid; - gap: 12px; -} - -.recognition-bubble { - display: grid; - gap: 10px; - padding: 16px 18px; - border-radius: 22px; - border: 1px solid rgba(191, 219, 254, 0.9); - background: linear-gradient(180deg, #ffffff 0%, #f5fbff 100%); - box-shadow: 0 16px 28px rgba(241, 245, 249, 0.9); -} - -.recognition-bubble.secondary { - border-color: #e2e8f0; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -.recognition-bubble-label { - color: #0f766e; - font-size: 11px; - font-weight: 850; - letter-spacing: 0.02em; -} - -.recognition-bubble.secondary .recognition-bubble-label { - color: #475569; -} - -.recognition-bubble-copy { - display: grid; - gap: 8px; -} - -.recognition-bubble-line, -.recognition-bubble-note { - margin: 0; - color: #334155; - font-size: 13px; - line-height: 1.75; -} - -.recognition-bubble-line { - font-weight: 700; - color: #0f172a; -} - -.recognition-bubble-note { - color: #64748b; -} - -.review-document-bubble { - display: grid; - grid-template-columns: minmax(0, 1fr) 140px; - gap: 14px; - align-items: start; - padding: 16px; - border-radius: 22px; - background: linear-gradient(180deg, #ffffff 0%, #f7fafc 100%); - border: 1px solid rgba(226, 232, 240, 0.95); - box-shadow: 0 16px 28px rgba(241, 245, 249, 0.92); -} - -.review-document-copy { - display: grid; - gap: 6px; -} - -.review-document-index { - color: #1d4ed8; - font-size: 11px; - font-weight: 850; -} - -.review-document-copy strong { - color: #0f172a; - font-size: 13px; - font-weight: 850; - line-height: 1.6; -} - -.review-document-copy p { - margin: 0; - color: #64748b; - font-size: 12px; - line-height: 1.7; -} - -.review-overlay { - z-index: 10001; -} - -.review-confirm-modal, -.review-edit-modal { - width: min(720px, calc(100vw - 40px)); - border-radius: 24px; - background: - radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 28%), - linear-gradient(180deg, #fbfdff 0%, #f6f9fc 100%); - box-shadow: - 0 24px 80px rgba(15, 23, 42, 0.22), - 0 2px 12px rgba(15, 23, 42, 0.08); - border: 1px solid #e7eef6; -} - -.review-confirm-modal { - padding: 24px; - display: grid; - gap: 18px; -} - -.review-confirm-modal h3, -.review-edit-head h3 { - margin-top: 12px; - color: #0f172a; - font-size: 22px; - font-weight: 900; - line-height: 1.35; -} - -.review-confirm-modal p, -.review-edit-head p { - margin-top: 8px; - color: #64748b; - font-size: 14px; - line-height: 1.7; -} - -.review-confirm-actions, -.review-edit-actions { - display: flex; - justify-content: flex-end; - gap: 12px; - flex-wrap: wrap; -} - -.review-upload-decision-modal { - display: grid; - gap: 18px; -} - -.review-upload-decision-copy { - display: grid; - gap: 10px; -} - -.review-upload-decision-actions { - justify-content: stretch; -} - -.review-upload-decision-actions .primary-dialog-btn, -.review-upload-decision-actions .secondary-dialog-btn { - flex: 1 1 168px; -} - -.review-edit-modal { - max-height: min(860px, calc(100vh - 48px)); - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - overflow: hidden; -} - -.review-edit-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - padding: 22px 24px 18px; - border-bottom: 1px solid #eef2f7; -} - -.review-edit-form { - min-height: 0; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; - padding: 20px 24px; - overflow-y: auto; -} - -.review-edit-field { - display: grid; - gap: 8px; -} - -.review-edit-field.attachments, -.review-edit-field.business, -.review-edit-field.basic { - min-width: 0; -} - -.review-edit-field span { - color: #334155; - font-size: 13px; - font-weight: 800; -} - -.review-edit-field span em { - margin-left: 4px; - color: #dc2626; - font-style: normal; -} - -.review-edit-field input, -.review-edit-field textarea { - width: 100%; - border: 1px solid #dbe6f0; - border-radius: 16px; - background: #fff; - color: #0f172a; - font-size: 14px; - line-height: 1.6; - padding: 12px 14px; - resize: vertical; -} - -.review-edit-field input:focus, -.review-edit-field textarea:focus { - outline: none; - border-color: #60a5fa; - box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.14); -} - -.review-edit-field textarea { - min-height: 96px; -} - -.review-edit-field.attachments, -.review-edit-field textarea, -.review-edit-field .textarea { - grid-column: span 2; -} - -.review-edit-actions { - padding: 0 24px 24px; -} - -.review-preview-modal { - width: min(980px, calc(100vw - 40px)); - max-height: min(92vh, calc(100vh - 32px)); - display: grid; - grid-template-rows: auto minmax(0, 1fr); - overflow: hidden; - border-radius: 24px; - background: - radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 28%), - linear-gradient(180deg, #fbfdff 0%, #f6f9fc 100%); - box-shadow: - 0 24px 80px rgba(15, 23, 42, 0.22), - 0 2px 12px rgba(15, 23, 42, 0.08); - border: 1px solid #e7eef6; -} - -.review-preview-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - padding: 22px 24px 18px; - border-bottom: 1px solid #eef2f7; -} - -.review-preview-head h3 { - margin-top: 12px; - color: #0f172a; - font-size: 22px; - font-weight: 900; - line-height: 1.35; -} - -.review-preview-body { - min-height: 0; - display: grid; - place-items: center; - padding: 18px; - background: rgba(248, 250, 252, 0.88); -} - -.review-preview-body.image img { - max-width: 100%; - max-height: calc(92vh - 170px); - display: block; - border-radius: 20px; - object-fit: contain; - box-shadow: 0 16px 34px rgba(148, 163, 184, 0.26); -} - -.review-preview-body.pdf iframe { - width: 100%; - height: min(78vh, 820px); - border: 0; - border-radius: 18px; - background: #fff; -} - -.welcome-grid { - display: grid; - gap: 12px; -} - -.welcome-card { - padding: 14px; - border-radius: 18px; - background: #f8fafc; -} - -.welcome-card i { - color: #10b981; - font-size: 20px; -} - -.welcome-card strong { - display: block; - margin-top: 10px; -} - -.assistant-modal-enter-active, -.assistant-modal-leave-active { - transition: opacity 220ms ease; -} - -.assistant-modal-enter-active .assistant-modal, -.assistant-modal-leave-active .assistant-modal { - transition: transform 260ms ease, opacity 220ms ease; -} - -.assistant-modal-enter-from, -.assistant-modal-leave-to { - opacity: 0; -} - -.assistant-modal-enter-from .assistant-modal, -.assistant-modal-leave-to .assistant-modal { - transform: translateY(10px) scale(0.985); - opacity: 0; -} - -.insight-switch-enter-active, -.insight-switch-leave-active { - transition: opacity 180ms ease, transform 180ms ease; -} - -.insight-switch-enter-from, -.insight-switch-leave-to { - opacity: 0; - transform: translateY(8px); -} - -@media (max-width: 1366px), (max-height: 780px) { - .insight-panel-shell { - width: 348px; - } - - .insight-panel { - width: 348px; - } - - .review-side-grid.compact { - grid-template-columns: 1fr; - } -} - -@media (max-width: 1280px) { - .assistant-layout { - flex-direction: column; - } - - .insight-panel-shell { - width: 100%; - margin-left: 0; - max-height: 720px; - transition: - max-height 320ms cubic-bezier(0.22, 1, 0.36, 1), - opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), - transform 280ms cubic-bezier(0.22, 1, 0.36, 1); - } - - .insight-panel-shell.collapsed { - width: 100%; - max-height: 0; - } - - .insight-panel { - width: 100%; - min-height: 320px; - } - - .insight-panel-shell.collapsed .insight-panel { - transform: translateY(-12px); - } -} - -@media (max-width: 760px) { - .assistant-modal { - width: 100vw; - height: 100vh; - } - - .assistant-modal-stage { - width: 100%; - height: 100%; - transform: none; - border-radius: 0; - } - - .assistant-header { - padding: 18px 18px 16px; - align-items: flex-start; - flex-direction: column; - } - - .assistant-header-actions { - top: 18px; - right: 18px; - gap: 10px; - width: auto; - justify-content: space-between; - } - - .assistant-toggle-btn, - .session-trash-btn, - .assistant-close-btn, - .close-btn { - width: 40px; - height: 40px; - border-radius: 14px; - font-size: 16px; - } - - .assistant-layout { - padding: 14px; - } - - .composer-row { - gap: 8px; - } - - .composer-side-btn, - .tool-btn, - .send-btn { - width: 40px; - height: 40px; - } - - .dialog-toolbar { - padding: 16px 16px 12px; - } - - .shortcut-chip { - width: 100%; - justify-content: center; - } - - .message-list { - padding: 16px; - } - - .message-row, - .message-row.user { - grid-template-columns: 34px minmax(0, 1fr); - } - - .message-row.user .message-avatar { - order: 0; - } - - .message-row.user .message-bubble { - order: 0; - justify-self: stretch; - } - - .composer { - padding: 0 16px 16px; - } - - .composer-files-head, - .review-insight-title-row, - .review-document-stage-head, - .review-document-switch-head { - align-items: flex-start; - flex-direction: column; - } - - .composer-files-actions, - .review-document-nav { - width: 100%; - justify-content: space-between; - } - - .review-card-head { - flex-direction: column; - } - - .metric-grid { - grid-template-columns: 1fr; - } - - .review-side-grid, - .review-side-category-grid, - .review-document-edit-grid { - grid-template-columns: 1fr; - } - - .review-pending-item { - grid-template-columns: 42px minmax(0, 1fr); - } - - .review-pending-status { - grid-column: 2; - justify-self: start; - } - - .review-footer-btn-row { - flex-direction: column; - } - - .review-footer-btn { - width: 100%; - } - - .review-slot-grid, - .review-doc-field-grid, - .review-mini-grid { - grid-template-columns: 1fr; - } - - .review-document-plain, - .review-document-bubble { - grid-template-columns: 1fr; - } - - .review-edit-modal { - width: calc(100vw - 24px); - } - - .review-preview-modal { - width: calc(100vw - 24px); - } - - .review-edit-form { - grid-template-columns: 1fr; - padding: 18px; - } - - .review-edit-field.attachments, - .review-edit-field textarea, - .review-edit-field .textarea { - grid-column: auto; - } - - .review-edit-actions, - .review-confirm-actions { - padding: 0 18px 18px; - justify-content: stretch; - } - - .review-upload-decision-actions { - width: 100%; - } - - .primary-dialog-btn, - .secondary-dialog-btn, - .danger-dialog-btn { - width: 100%; - } -} +.assistant-overlay { + /* 距屏幕边 10–18px,随视口微调;高度用 dvh 适配笔记本浏览器工具栏 */ + --assistant-viewport-inset: clamp(10px, 1.25vmin, 18px); + position: fixed; + inset: 0; + width: 100vw; + height: 100dvh; + max-height: 100dvh; + z-index: 9999; + display: flex; + align-items: stretch; + justify-content: stretch; + padding: var(--assistant-viewport-inset); + box-sizing: border-box; + background: + radial-gradient(circle at 18% 14%, rgba(16, 185, 129, 0.18), transparent 24%), + radial-gradient(circle at 82% 12%, rgba(59, 130, 246, 0.12), transparent 28%), + rgba(97, 110, 131, 0.34); + backdrop-filter: blur(18px) saturate(1.02); + -webkit-backdrop-filter: blur(18px) saturate(1.02); +} + +.assistant-modal { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + background: transparent; + box-shadow: none; + border: 0; + border-radius: 24px; + backdrop-filter: none; + -webkit-backdrop-filter: none; + overflow: hidden; + isolation: isolate; +} + +.assistant-modal-stage { + /* 工作台字号令牌:笔记本断点见文末 @media */ + --wb-fs-title: 22px; + --wb-fs-desc: 13px; + --wb-fs-badge: 12px; + --wb-fs-bubble: 14px; + --wb-fs-bubble-meta: 13px; + --wb-fs-bubble-time: 12px; + --wb-fs-chip: 12px; + --wb-fs-composer: 14px; + --wb-fs-tool-icon: 18px; + --wb-fs-md-h1: 18px; + --wb-fs-md-h2: 16px; + --wb-fs-md-h3: 14px; + --wb-fs-insight-title: 19px; + --wb-fs-insight-num: 19px; + --wb-fs-insight-body: 12px; + --wb-fs-insight-h4: 15px; + --wb-fs-metric: 13px; + --wb-fs-metric-strong: 13px; + --wb-fs-welcome: 20px; + position: relative; + flex: 1; + min-width: 0; + min-height: 0; + width: 100%; + height: 100%; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + transform: none; + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(16, 185, 129, 0.14), transparent 26%), + radial-gradient(circle at top left, rgba(59, 130, 246, 0.10), transparent 24%), + linear-gradient(180deg, rgba(241, 246, 245, 0.92) 0%, rgba(230, 237, 235, 0.88) 100%); + box-shadow: + 0 28px 72px rgba(15, 23, 42, 0.22), + 0 10px 28px rgba(15, 23, 42, 0.09), + inset 0 1px 0 rgba(255, 255, 255, 0.42); + border: 1px solid rgba(255, 255, 255, 0.44); + background-clip: padding-box; + overflow: hidden; + isolation: isolate; +} + +.assistant-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-shrink: 0; + padding: clamp(14px, 2vh, 22px) clamp(148px, 11vw, 172px) clamp(12px, 1.6vh, 18px) clamp(18px, 2vw, 26px); + border-bottom: 1px solid rgba(203, 213, 225, 0.78); + background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%); +} + +.assistant-header-main { + display: flex; + align-items: flex-start; + gap: 14px; + min-width: 0; +} + +.assistant-badge { + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 14px; + border-radius: 999px; + background: linear-gradient(135deg, #22c55e, #10b981); + color: #fff; + font-size: var(--wb-fs-badge); + font-weight: 800; + box-shadow: 0 8px 16px rgba(16, 185, 129, 0.14); + white-space: nowrap; +} + +.assistant-badge.warning { + background: rgba(249, 115, 22, 0.12); + color: #c2410c; +} + +.assistant-header h2 { + color: #0f172a; + font-size: clamp(17px, 1.1vw, var(--wb-fs-title)); + font-weight: 900; + letter-spacing: 0.01em; + line-height: 1.25; +} + +.assistant-header p { + margin-top: 4px; + color: #64748b; + font-size: clamp(11px, 0.85vw, var(--wb-fs-desc)); + line-height: 1.55; +} + +.assistant-header-actions { + position: absolute; + top: 16px; + right: 16px; + z-index: 60; + display: flex; + align-items: center; + gap: 10px; + pointer-events: auto; +} + +.assistant-toggle-btn, +.session-trash-btn { + width: 38px; + height: 38px; + display: grid; + place-items: center; + padding: 0; + border: 1px solid rgba(248, 113, 113, 0.28); + border-radius: 14px; + flex: none; +} + +.assistant-toggle-btn { + border-color: rgba(16, 185, 129, 0.18); + background: rgba(245, 252, 249, 0.96); + color: #166534; + font-size: 16px; + box-shadow: 0 8px 18px rgba(16, 185, 129, 0.1); +} + +.assistant-toggle-btn:hover:not(:disabled) { + background: rgba(236, 253, 245, 0.98); + border-color: rgba(16, 185, 129, 0.28); +} + +.assistant-toggle-btn:disabled, +.assistant-toggle-btn.disabled { + opacity: 0.48; + cursor: not-allowed; + box-shadow: none; +} + +.session-trash-btn { + background: rgba(254, 242, 242, 0.96); + color: #dc2626; + font-size: 16px; + box-shadow: 0 8px 18px rgba(239, 68, 68, 0.12); +} + +.session-trash-btn:hover:not(:disabled) { + background: rgba(254, 226, 226, 0.98); + border-color: rgba(239, 68, 68, 0.34); +} + +.session-trash-btn:disabled { + opacity: 0.42; + cursor: not-allowed; + box-shadow: none; +} + +.assistant-close-btn, +.close-btn { + position: relative; + width: 38px; + height: 38px; + display: grid; + place-items: center; + padding: 0; + flex: none; + border: 1px solid rgba(193, 204, 216, 0.92); + border-radius: 14px; + background: rgba(248, 251, 251, 0.94); + color: #475569; + font-size: 16px; + box-shadow: 0 8px 18px rgba(148, 163, 184, 0.18); + cursor: pointer; + pointer-events: auto; + user-select: none; + -webkit-user-select: none; +} + +.assistant-close-btn { + z-index: 61; + pointer-events: auto; +} + +.assistant-close-btn i { + pointer-events: none; +} + +.assistant-close-btn:hover, +.close-btn:hover { + background: rgba(241, 245, 249, 0.98); + border-color: rgba(148, 163, 184, 0.34); + color: #0f172a; +} + +.assistant-layout { + min-height: 0; + flex: 1; + display: flex; + padding: clamp(12px, 1.5vw, 16px); + align-items: stretch; + gap: clamp(12px, 1.5vw, 16px); +} + +.dialog-panel, +.insight-panel { + min-width: 0; + min-height: 0; + border: 1px solid rgba(189, 201, 214, 0.74); + border-radius: 24px; + background: rgba(248, 251, 251, 0.84); + box-shadow: + 0 14px 32px rgba(148, 163, 184, 0.16), + 0 2px 6px rgba(15, 23, 42, 0.05); + backdrop-filter: blur(22px); + -webkit-backdrop-filter: blur(22px); +} + +.dialog-panel { + flex: 1 1 auto; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; + background: + radial-gradient(circle at top right, rgba(59, 130, 246, 0.07), transparent 22%), + linear-gradient(180deg, rgba(252, 253, 253, 0.88) 0%, rgba(243, 247, 248, 0.84) 100%); + transition: + transform 320ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 320ms cubic-bezier(0.22, 1, 0.36, 1); + will-change: transform; +} + +.insight-panel-shell { + flex: none; + width: clamp(300px, 28vw, 420px); + min-width: 0; + max-width: 100%; + margin-left: 0; + overflow: hidden; + transition: + width 360ms cubic-bezier(0.22, 1, 0.36, 1), + margin-left 360ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.insight-panel-shell.collapsed { + width: 0; + margin-left: 0; +} + +.dialog-toolbar { + display: flex; + gap: 12px; + flex-wrap: wrap; + padding: 16px 18px 12px; + border-bottom: 1px solid rgba(238, 242, 247, 0.9); +} + +.shortcut-chip { + min-height: 36px; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 0 14px; + border: 1px solid rgba(219, 230, 240, 0.9); + border-radius: 999px; + background: rgba(255, 255, 255, 0.95); + color: #334155; + font-size: var(--wb-fs-chip); + font-weight: 750; + box-shadow: 0 4px 12px rgba(241, 245, 249, 0.78); + white-space: nowrap; +} + +.shortcut-chip i { + color: #059669; +} + +.shortcut-chip:disabled { + opacity: 0.48; + cursor: not-allowed; + box-shadow: none; +} + +.message-list { + min-height: 0; + display: grid; + align-content: start; + gap: 14px; + padding: 18px; + overflow-y: auto; +} + +.message-row { + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + align-items: start; + gap: 12px; +} + +.message-row.user { + grid-template-columns: minmax(0, 1fr) 38px; +} + +.message-row.user .message-avatar { + order: 2; + background: transparent; +} + +.message-row.user .message-bubble { + order: 1; + justify-self: end; + background: linear-gradient(135deg, rgba(226, 238, 255, 0.98), rgba(242, 247, 255, 0.9)); + border-color: rgba(96, 165, 250, 0.24); + box-shadow: 0 14px 30px rgba(59, 130, 246, 0.08); +} + +.message-avatar { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border-radius: 999px; + overflow: hidden; + background: transparent; + box-shadow: 0 10px 20px rgba(148, 163, 184, 0.24); +} + +.message-avatar img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.message-bubble { + max-width: min(100%, 720px); + padding: 14px 16px; + border: 1px solid rgba(210, 220, 230, 0.94); + border-radius: 20px; + background: rgba(253, 254, 254, 0.94); + color: #24324a; + line-height: 1.65; + box-shadow: 0 10px 22px rgba(226, 232, 240, 0.48); +} + +.message-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.message-meta strong { + color: #0f172a; + font-size: var(--wb-fs-bubble-meta); + font-weight: 850; +} + +.message-meta time { + color: #94a3b8; + font-size: var(--wb-fs-bubble-time); +} + +.message-bubble p { + color: #334155; + font-size: var(--wb-fs-bubble); +} + +.message-answer-content { + display: grid; + gap: 12px; +} + +.message-answer-content :deep(p), +.message-answer-content :deep(ul), +.message-answer-content :deep(ol), +.message-answer-content :deep(blockquote), +.message-answer-content :deep(pre) { + margin: 0; +} + +.message-answer-markdown :deep(h1), +.message-answer-markdown :deep(h2), +.message-answer-markdown :deep(h3), +.message-answer-markdown :deep(h4) { + margin: 0; + color: #0f172a; + line-height: 1.35; +} + +.message-answer-markdown :deep(h1) { + font-size: var(--wb-fs-md-h1); +} + +.message-answer-markdown :deep(h2) { + font-size: var(--wb-fs-md-h2); +} + +.message-answer-markdown :deep(h3), +.message-answer-markdown :deep(h4) { + font-size: var(--wb-fs-md-h3); +} + +.message-answer-markdown { + overflow-x: auto; + font-size: var(--wb-fs-bubble); + color: #334155; + line-height: 1.65; +} + +/* v-html 注入的 Markdown 节点无 scoped 标记,需用 :deep 与用户气泡 p 对齐字号 */ +.message-answer-markdown :deep(p), +.message-answer-markdown :deep(li), +.message-answer-markdown :deep(td), +.message-answer-markdown :deep(th), +.message-answer-markdown :deep(blockquote) { + font-size: inherit; + color: inherit; + line-height: 1.65; +} + +.message-answer-markdown :deep(ul), +.message-answer-markdown :deep(ol) { + padding-left: 22px; +} + +.message-answer-markdown :deep(strong) { + color: #0f172a; +} + +.message-answer-markdown :deep(blockquote) { + padding: 10px 12px; + border-left: 4px solid #93c5fd; + border-radius: 0 12px 12px 0; + background: #eff6ff; + color: #475569; +} + +.message-answer-markdown :deep(code) { + padding: 2px 6px; + border-radius: 6px; + background: #e2e8f0; + font-size: 12px; +} + +.message-answer-markdown :deep(pre) { + overflow-x: auto; + padding: 12px; + border-radius: 14px; + background: #0f172a; + color: #e2e8f0; +} + +.message-answer-markdown :deep(pre code) { + padding: 0; + background: transparent; + color: inherit; +} + +.message-answer-markdown :deep(a) { + color: #2563eb; + text-decoration: underline; +} + +.message-answer-markdown :deep(table) { + width: auto; + max-width: 100%; + border: 1px solid #dbe4ee; + border-radius: 16px; + border-collapse: collapse; + background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); + font-size: inherit; +} + +.message-answer-markdown :deep(th), +.message-answer-markdown :deep(td) { + padding: 10px 12px; + border-bottom: 1px solid #e2e8f0; + text-align: left; + white-space: nowrap; +} + +.message-answer-markdown :deep(th) { + background: #eff6ff; + color: #0f172a; + font-weight: 850; +} + +.message-answer-markdown :deep(td) { + color: #334155; + font-weight: 650; +} + +.message-answer-markdown :deep(tbody tr:last-child td) { + border-bottom: 0; +} + +.message-meta-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.message-meta-chip, +.capability-chip, +.risk-chip, +.message-risk-chip, +.message-action-chip { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + font-size: var(--wb-fs-chip); + font-weight: 800; +} + +.message-meta-chip, +.capability-chip { + background: #eef6ff; + color: #1d4ed8; +} + +.risk-chip, +.message-risk-chip { + background: #fff1f2; + color: #be123c; +} + +.message-action-chip { + background: #ecfdf5; + color: #059669; +} + +.message-detail-block { + margin-top: 14px; + display: grid; + gap: 10px; +} + +.message-detail-block > strong { + color: #0f172a; + font-size: 12px; + font-weight: 850; +} + +.message-citation-disclosure { + overflow: hidden; + border: 1px solid #dbe4ee; + border-radius: 16px; + background: #fbfdff; +} + +.message-citation-disclosure summary { + min-height: 42px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 14px; + color: #0f172a; + cursor: pointer; + list-style: none; +} + +.message-citation-disclosure summary::-webkit-details-marker { + display: none; +} + +.message-citation-disclosure summary strong { + font-size: 12px; + font-weight: 850; +} + +.message-citation-disclosure summary span { + color: #64748b; + font-size: 12px; + font-weight: 750; +} + +.message-citation-disclosure summary i { + margin-left: auto; + color: #64748b; + font-size: 16px; + transition: transform 0.18s ease; +} + +.message-citation-disclosure[open] summary { + border-bottom: 1px solid #e2e8f0; +} + +.message-citation-disclosure[open] summary i { + transform: rotate(180deg); +} + +.message-citation-disclosure .message-citation-list { + padding: 12px; +} + +.expense-query-block { + gap: 10px; +} + +.expense-query-window-label { + margin: -4px 0 0; + color: #64748b; + font-size: 11px; + line-height: 1.5; +} + +.expense-query-summary-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.expense-query-summary-chip { + min-height: 24px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + background: #eef2ff; + color: #3730a3; +} + +.expense-query-summary-chip.draft { + background: #fef3c7; + color: #b45309; +} + +.expense-query-summary-chip.in_progress { + background: #dbeafe; + color: #1d4ed8; +} + +.expense-query-summary-chip.completed { + background: #dcfce7; + color: #15803d; +} + +.expense-query-summary-chip.other { + background: #f1f5f9; + color: #475569; +} + +.expense-query-record-list { + display: grid; + gap: 8px; +} + +.expense-query-record-list.compact .expense-query-record-card { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border: 1px solid #dbe4ee; + border-radius: 14px; + background: #fbfdff; + cursor: pointer; + font: inherit; + text-align: left; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +.expense-query-record-list.compact .expense-query-record-card:hover { + transform: translateY(-1px); + border-color: #bfdbfe; + box-shadow: 0 8px 18px rgba(148, 163, 184, 0.12); +} + +.expense-query-record-card > i { + color: #94a3b8; + font-size: 16px; +} + +.expense-query-record-main { + min-width: 0; + display: grid; + gap: 5px; + flex: 1; +} + +.expense-query-record-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.expense-query-record-top strong { + min-width: 0; + color: #0f172a; + font-size: 12px; + font-weight: 850; +} + +.expense-query-record-top strong, +.expense-query-record-card p, +.expense-query-record-meta span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.expense-query-record-status { + flex-shrink: 0; + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 800; + background: #f1f5f9; + color: #475569; +} + +.expense-query-record-status.draft { + background: #fef3c7; + color: #b45309; +} + +.expense-query-record-status.in_progress { + background: #dbeafe; + color: #1d4ed8; +} + +.expense-query-record-status.completed { + background: #dcfce7; + color: #15803d; +} + +.expense-query-record-card p { + margin: 0; + color: #334155; + font-size: 12px; +} + +.expense-query-record-meta { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + color: #64748b; + font-size: 11px; + font-weight: 700; +} + +.expense-query-pager { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 2px; +} + +.expense-query-pager-btn { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #dbe4ee; + border-radius: 999px; + background: #fff; + color: #475569; + transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease; +} + +.expense-query-pager-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.expense-query-pager-btn:not(:disabled):hover { + border-color: #bfdbfe; + color: #2563eb; + background: #f8fbff; +} + +.expense-query-pager-dots { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.expense-query-pager-dot { + width: 8px; + height: 8px; + padding: 0; + border: 0; + border-radius: 999px; + background: #cbd5e1; + transition: transform 0.2s ease, background 0.2s ease; +} + +.expense-query-pager-dot.active { + background: #2563eb; + transform: scale(1.15); +} + +.expense-query-empty { + min-height: 52px; + display: flex; + align-items: center; + gap: 10px; + padding: 0 14px; + border: 1px dashed #dbe4ee; + border-radius: 16px; + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.expense-query-empty i { + font-size: 18px; + color: #94a3b8; +} + +.expense-query-hint { + margin: 0; + color: #64748b; + font-size: 11px; + line-height: 1.6; +} + +.message-detail-chip-row, +.capability-chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.message-citation-list, +.citation-stack, +.action-list { + display: grid; + gap: 10px; +} + +.message-citation-card, +.citation-card, +.action-card { + padding: 12px 14px; + border: 1px solid #e2e8f0; + border-radius: 16px; + background: #f8fbff; +} + +.message-citation-card header, +.citation-card header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 6px; +} + +.message-citation-card header span, +.citation-card header strong, +.action-card strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.message-citation-card header small, +.citation-card header span { + color: #64748b; + font-size: 11px; + font-weight: 700; +} + +.message-citation-card p, +.citation-card p, +.action-card p, +.draft-preview pre { + margin: 0; + color: #475569; + font-size: 12px; + line-height: 1.65; +} + +.draft-preview { + margin-top: 12px; + padding: 12px 14px; + border: 1px solid #dbe3ec; + border-radius: 16px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.draft-preview header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.draft-preview header strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.draft-preview header span { + color: #b45309; + font-size: 12px; + font-weight: 800; +} + +.draft-preview pre { + white-space: pre-wrap; + word-break: break-word; + font-family: inherit; +} + +.message-files { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 10px; +} + +.file-chip { + min-height: 28px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + border: 0; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + font-size: 12px; + font-weight: 700; + max-width: 100%; +} + +.file-chip.active { + background: #eef6ff; + color: #2563eb; +} + +.composer { + padding: 0 18px 18px; + display: grid; + gap: 12px; +} + +.hidden-file-input { + display: none; +} + +.composer-row { + --composer-control-size: 44px; +} + +.composer-leading-actions { + display: flex; + align-items: center; + gap: 8px; + flex: none; +} + +.composer-date-anchor { + position: relative; +} + +.tool-btn.composer-side-btn.active { + border-color: rgba(59, 130, 246, 0.42); + background: rgba(239, 246, 255, 0.96); + color: #2563eb; + box-shadow: 0 6px 14px rgba(59, 130, 246, 0.14); +} + +.composer-date-popover { + position: absolute; + bottom: calc(100% + 10px); + left: 0; + z-index: 30; + width: min(320px, calc(100vw - 48px)); + display: grid; + gap: 12px; + padding: 14px; + border: 1px solid rgba(203, 213, 225, 0.92); + border-radius: 16px; + background: rgba(255, 255, 255, 0.98); + box-shadow: + 0 18px 40px rgba(15, 23, 42, 0.16), + 0 4px 12px rgba(15, 23, 42, 0.06); +} + +.composer-date-mode-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + padding: 4px; + border-radius: 12px; + background: rgba(241, 245, 249, 0.92); +} + +.composer-date-mode-btn { + min-height: 34px; + border: 0; + border-radius: 10px; + background: transparent; + color: #64748b; + font-size: 12px; + font-weight: 800; +} + +.composer-date-mode-btn.active { + background: #fff; + color: #0f172a; + box-shadow: 0 4px 10px rgba(148, 163, 184, 0.18); +} + +.composer-date-fields { + display: grid; + gap: 8px; +} + +.composer-date-fields-range { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: end; + gap: 8px; +} + +.composer-date-field { + display: grid; + gap: 6px; + min-width: 0; +} + +.composer-date-field span { + color: #64748b; + font-size: 11px; + font-weight: 800; +} + +.composer-date-field input { + width: 100%; + min-height: 36px; + padding: 0 10px; + border: 1px solid rgba(203, 213, 225, 0.92); + border-radius: 10px; + background: #fff; + color: #0f172a; + font-size: 12px; + font-weight: 700; +} + +.composer-date-range-sep { + align-self: center; + color: #94a3b8; + font-size: 12px; + font-weight: 800; +} + +.composer-date-hint { + margin: 0; + color: #dc2626; + font-size: 11px; + line-height: 1.5; +} + +.composer-date-popover-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.composer-date-cancel-btn, +.composer-date-apply-btn { + min-height: 34px; + padding: 0 14px; + border-radius: 10px; + font-size: 12px; + font-weight: 800; +} + +.composer-date-cancel-btn { + border: 1px solid rgba(203, 213, 225, 0.92); + background: #fff; + color: #64748b; +} + +.composer-date-apply-btn { + border: 0; + background: linear-gradient(135deg, #22c55e, #10b981); + color: #fff; +} + +.composer-date-apply-btn:disabled { + opacity: 0.48; + cursor: not-allowed; +} + +.composer-shell { + min-width: 0; + min-height: var(--composer-control-size, 44px); + border: 1px solid rgba(214, 225, 234, 0.95); + border-radius: 999px; + background: rgba(255, 255, 255, 0.98); + box-shadow: + 0 10px 22px rgba(226, 232, 240, 0.24), + 0 1px 4px rgba(15, 23, 42, 0.03); +} + +.composer-shell-body { + min-height: var(--composer-control-size, 44px); + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 4px 12px; +} + +.composer-biz-time-tag { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: min(100%, 320px); + min-height: 28px; + padding: 0 8px 0 10px; + border-radius: 999px; + border: 1px solid rgba(59, 130, 246, 0.28); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(16, 185, 129, 0.12)); + color: #1d4ed8; + font-size: 11px; + font-weight: 800; + flex: none; +} + +.composer-biz-time-tag i { + font-size: 14px; + color: #2563eb; +} + +.composer-biz-time-tag-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.composer-biz-time-tag-remove { + width: 18px; + height: 18px; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + color: #3b82f6; + flex: none; +} + +.composer-biz-time-tag-remove:disabled { + opacity: 0.48; +} + +.composer-files-panel { + display: grid; + gap: 10px; + padding: 14px; + border: 1px solid rgba(226, 232, 240, 0.9); + border-radius: 18px; + background: linear-gradient(180deg, rgba(248, 251, 255, 0.92) 0%, rgba(242, 247, 251, 0.78) 100%); +} + +.composer-files-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.composer-files-head strong { + color: #0f172a; + font-size: 12px; + font-weight: 850; +} + +.composer-files-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.composer-file-link { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + border: 0; + background: transparent; + color: #2563eb; + font-size: 11px; + font-weight: 800; +} + +.composer-file-link.danger { + color: #dc2626; +} + +.composer-file-link:disabled { + opacity: 0.48; +} + +.composer-file-chip-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.composer-file-chip { + max-width: min(100%, 280px); +} + +.file-chip.summary { + border: 1px dashed rgba(96, 165, 250, 0.34); + background: rgba(239, 246, 255, 0.92); + cursor: pointer; +} + +.file-chip-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-chip-remove { + width: 18px; + height: 18px; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 999px; + background: rgba(255, 255, 255, 0.82); + color: inherit; + flex: none; +} + +.file-chip-remove:disabled { + opacity: 0.48; +} + +.composer-files-expanded { + display: grid; + gap: 8px; + max-height: 176px; + overflow-y: auto; + padding-right: 2px; +} + +.composer-expanded-file { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(219, 230, 240, 0.92); + background: rgba(255, 255, 255, 0.88); +} + +.composer-expanded-file-copy { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + color: #334155; +} + +.composer-expanded-file-copy span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; + font-weight: 700; +} + +.composer-expanded-file-remove { + width: 28px; + height: 28px; + display: grid; + place-items: center; + border: 0; + border-radius: 10px; + background: rgba(248, 250, 252, 0.92); + color: #64748b; +} + +.composer-expanded-file-remove:disabled { + opacity: 0.48; +} + +.composer-shell textarea { + flex: 1 1 120px; + width: auto; + min-width: 0; + min-height: 36px; + max-height: 120px; + resize: none; + border: 0; + padding: 8px 4px; + background: transparent; + color: #0f172a; + font-size: var(--wb-fs-composer); + line-height: 20px; +} + +.composer-shell textarea::placeholder { + color: #94a3b8; +} + +.composer-shell textarea:focus { + outline: none; +} + +.composer-shell textarea:disabled { + color: #94a3b8; +} + +.composer-row { + display: flex; + align-items: center; + gap: 10px; +} + +.composer-row .composer-shell { + flex: 1 1 auto; +} + +.composer-side-btn, +.composer-row .tool-btn, +.composer-row .send-btn { + width: var(--composer-control-size, 44px); + height: var(--composer-control-size, 44px); + display: grid; + place-items: center; + border: 0; + border-radius: 999px; + flex: none; +} + +.tool-btn { + background: #ffffff; + color: #475569; + font-size: var(--wb-fs-tool-icon); + border: 1px solid #dbe6f0; + box-shadow: 0 4px 12px rgba(241, 245, 249, 0.76); +} + +.tool-btn:disabled { + opacity: 0.48; + cursor: not-allowed; +} + +.send-btn { + background: linear-gradient(135deg, #22c55e, #10b981); + color: #fff; + font-size: var(--wb-fs-tool-icon); + box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18); +} + +.send-btn:disabled { + opacity: 0.48; + cursor: not-allowed; + box-shadow: none; +} + +.insight-panel { + position: relative; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + width: 100%; + height: 100%; + overflow: hidden; + background: + linear-gradient(180deg, rgba(239, 245, 243, 0.9) 0%, rgba(231, 238, 236, 0.84) 100%); + opacity: 1; + transform: translateX(0) scale(1); + transform-origin: right center; + transition: + opacity 260ms cubic-bezier(0.22, 1, 0.36, 1), + transform 320ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 320ms cubic-bezier(0.22, 1, 0.36, 1); + will-change: transform, opacity; +} + +.insight-panel-shell.collapsed .insight-panel { + opacity: 0; + transform: translateX(44px) scale(0.985); + pointer-events: none; +} + +.insight-panel::before { + content: ""; + position: absolute; + top: -18px; + right: -34px; + width: 240px; + height: 150px; + border-radius: 0 0 0 140px; + background: + radial-gradient(circle at 0 100%, rgba(16, 185, 129, 0.14), transparent 54%), + linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(96, 165, 250, 0.06)); + opacity: 0.9; + pointer-events: none; +} + +.insight-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + padding: 18px 18px 14px; + border-bottom: 1px solid rgba(205, 215, 224, 0.82); + position: relative; + z-index: 1; +} + +.insight-head.review-mode { + justify-content: space-between; +} + +.insight-head-eyebrow { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.insight-head-badge { + min-height: 24px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: rgba(240, 253, 244, 0.95); + color: #059669; + font-size: 11px; + font-weight: 800; + border: 1px solid rgba(16, 185, 129, 0.12); +} + +.review-insight-title-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.review-insight-title-copy { + min-width: 0; +} + +.review-insight-title-copy h3 { + margin: 0; +} + +.review-insight-tools { + display: inline-flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + align-self: center; +} + +.review-insight-switch-icon-btn { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + border: 1px solid rgba(203, 213, 225, 0.92); + background: rgba(248, 250, 252, 0.96); + color: #94a3b8; + font-size: 14px; + flex: 0 0 auto; + transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease, transform 0.18s ease; +} + +.review-insight-switch-icon-btn.available { + border-color: rgba(245, 158, 11, 0.28); + background: rgba(255, 247, 237, 0.94); + color: #d97706; +} + +.review-insight-switch-icon-btn.active { + border-color: rgba(217, 119, 6, 0.42); + background: rgba(254, 243, 199, 0.98); + color: #b45309; + box-shadow: 0 6px 14px rgba(245, 158, 11, 0.16); +} + +.review-insight-switch-icon-btn.risk.available { + border-color: rgba(239, 68, 68, 0.28); + background: rgba(254, 242, 242, 0.96); + color: #dc2626; +} + +.review-insight-switch-icon-btn.risk.active { + border-color: rgba(220, 38, 38, 0.42); + background: rgba(254, 226, 226, 0.98); + color: #b91c1c; + box-shadow: 0 6px 14px rgba(239, 68, 68, 0.16); +} + +.review-insight-switch-icon-btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.review-insight-switch-icon-btn:disabled { + cursor: not-allowed; + opacity: 1; + color: #cbd5e1; + background: rgba(248, 250, 252, 0.9); +} + +.intent-pill { + min-height: 30px; + display: inline-flex; + align-items: center; + padding: 0 13px; + border-radius: 999px; + font-size: var(--wb-fs-chip); + font-weight: 800; +} + +.intent-pill.welcome { + background: #eef2ff; + color: #4f46e5; +} + +.intent-pill.draft { + background: #ecfdf5; + color: #059669; +} + +.intent-pill.approval { + background: #fff7ed; + color: #ea580c; +} + +.intent-pill.recognition { + background: #eff6ff; + color: #2563eb; +} + +.intent-pill.note { + background: #fdf2f8; + color: #db2777; +} + +.insight-head h3 { + margin-top: 10px; + color: #0f172a; + font-size: var(--wb-fs-insight-title); + font-weight: 900; + line-height: 1.25; +} + +.insight-head p { + margin-top: 6px; + color: #64748b; + font-size: var(--wb-fs-insight-body); + line-height: 1.6; +} + +.confidence-card { + min-width: 92px; + padding: 10px 12px; + border-radius: 14px; + background: rgba(250, 252, 252, 0.9); + border: 1px solid rgba(202, 213, 223, 0.9); + box-shadow: 0 8px 18px rgba(203, 213, 225, 0.3); + text-align: right; +} + +.confidence-card span { + display: block; + color: #94a3b8; + font-size: 11px; + font-weight: 800; +} + +.confidence-card strong { + display: block; + margin-top: 4px; + color: #0f172a; + font-size: var(--wb-fs-insight-num); + font-weight: 900; +} + +.insight-body { + min-height: 0; + display: grid; + align-content: start; + gap: 12px; + padding: 14px 18px 18px; + overflow-y: auto; + position: relative; + z-index: 1; +} + +.review-side-card { + display: grid; + gap: 10px; + padding: 14px; + border-radius: 18px; + border: 1px solid rgba(197, 209, 221, 0.88); + background: rgba(249, 251, 251, 0.88); + box-shadow: 0 10px 20px rgba(226, 232, 240, 0.3); +} + +.review-side-overview-card { + gap: 12px; + background: linear-gradient(180deg, rgba(251, 252, 252, 0.92) 0%, rgba(240, 246, 244, 0.86) 100%); +} + +.review-side-intent-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + color: #475569; + font-size: var(--wb-fs-metric); +} + +.review-side-intent-row i { + color: #059669; + font-size: 16px; +} + +.review-side-intent-row strong { + color: #0f172a; + font-size: var(--wb-fs-bubble); + font-weight: 850; +} + +.review-side-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.review-side-grid.compact { + gap: 8px; +} + +.review-side-metric-card { + display: grid; + grid-template-columns: 32px minmax(0, 1fr); + gap: 8px; + align-items: start; + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(206, 216, 226, 0.88); + background: rgba(251, 252, 252, 0.82); + position: relative; + cursor: pointer; + transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; +} + +.review-side-metric-card.invalid { + border-color: rgba(239, 68, 68, 0.34); + background: rgba(254, 242, 242, 0.72); +} + +.review-side-metric-card.editable:hover, +.review-side-metric-card.editing { + border-color: rgba(16, 185, 129, 0.34); + background: rgba(248, 252, 250, 0.92); + transform: translateY(-1px); +} + +.review-side-metric-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 10px; + background: rgba(240, 253, 244, 0.95); + color: #059669; + font-size: 15px; +} + +.review-side-metric-copy { + display: grid; + gap: 4px; +} + +.review-side-metric-copy small { + color: #64748b; + font-size: 11px; + font-weight: 800; +} + +.review-side-metric-copy strong { + color: #0f172a; + font-size: var(--wb-fs-metric-strong); + font-weight: 850; + line-height: 1.35; + word-break: break-word; +} + +.review-inline-input { + width: 100%; + min-height: 34px; + padding: 0 10px; + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: 10px; + background: rgba(255, 255, 255, 0.96); + color: #0f172a; + font-size: 12px; + font-weight: 700; +} + +.review-inline-input.invalid { + border-color: rgba(239, 68, 68, 0.4); + color: #b91c1c; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.08); +} + +.review-inline-input:focus { + outline: none; + border-color: rgba(16, 185, 129, 0.42); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.08); +} + +.review-inline-select-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.review-inline-select-option { + min-height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 10px; + border-radius: 999px; + border: 1px solid rgba(203, 213, 225, 0.92); + background: rgba(255, 255, 255, 0.96); + color: #475569; + font-size: 11px; + font-weight: 700; +} + +.review-inline-select-option.active { + border-color: rgba(16, 185, 129, 0.36); + background: rgba(240, 253, 244, 0.94); + color: #047857; +} + +.review-inline-error { + color: #dc2626; + font-size: 11px; + font-weight: 800; + line-height: 1.45; +} + +.review-side-edit-hint { + position: absolute; + top: 8px; + right: 8px; + min-height: 20px; + display: inline-flex; + align-items: center; + padding: 0 6px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(226, 232, 240, 0.92); + color: #94a3b8; + font-size: 10px; + font-weight: 800; + opacity: 0; + transition: opacity 0.18s ease; +} + +.review-side-edit-hint.upload { + color: #059669; +} + +.review-side-metric-card:hover .review-side-edit-hint, +.review-side-metric-card.editing .review-side-edit-hint { + opacity: 1; +} + +.review-side-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.review-side-head strong { + color: #0f172a; + font-size: 14px; + font-weight: 900; +} + +.review-side-head-copy { + display: grid; + gap: 4px; + min-width: 0; +} + +.review-side-head-copy p { + margin: 0; + color: #64748b; + font-size: 11px; + line-height: 1.55; +} + +.review-side-confidence { + color: #10b981; + font-size: 12px; + font-weight: 900; +} + +.review-side-category-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.review-side-category-card { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + padding: 12px; + min-height: 66px; + border-radius: 14px; + border: 1px solid rgba(226, 232, 240, 0.94); + background: rgba(255, 255, 255, 0.68); + cursor: pointer; + transition: border-color 0.18s ease, background 0.18s ease; +} + +.review-side-category-card.active { + border-color: rgba(52, 211, 153, 0.62); + background: rgba(240, 253, 244, 0.9); + box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08); +} + +.review-side-category-copy { + display: grid; + gap: 4px; + min-width: 0; +} + +.review-side-category-copy strong { + color: #0f172a; + font-size: 12px; + font-weight: 850; + line-height: 1.35; + white-space: nowrap; +} + +.review-side-category-copy p { + margin: 0; + color: #64748b; + font-size: 10px; + line-height: 1.4; +} + +.review-side-group-check { + color: #10b981; + font-size: 18px; +} + +.review-other-category-popover { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-top: 2px; +} + +.review-other-category-option { + min-height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 12px; + border-radius: 999px; + border: 1px solid rgba(203, 213, 225, 0.92); + background: rgba(255, 255, 255, 0.94); + color: #475569; + font-size: 11px; + font-weight: 750; +} + +.review-other-category-option.active { + border-color: rgba(16, 185, 129, 0.36); + background: rgba(240, 253, 244, 0.94); + color: #047857; +} + +.review-side-risk-card { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.84) 0%, rgba(255, 249, 238, 0.8) 100%); +} + +.review-side-risk-score { + color: #f97316; + font-size: 13px; + font-weight: 900; +} + +.review-side-risk-score.empty { + color: #94a3b8; +} + +.review-side-risk-summary { + margin: 0; + color: #334155; + font-size: 12px; + line-height: 1.6; +} + +.review-side-risk-list { + display: grid; + gap: 8px; + margin: 0; + padding-left: 16px; + color: #475569; + font-size: 12px; + line-height: 1.6; +} + +.review-side-link { + width: fit-content; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + border: 0; + background: transparent; + color: #059669; + font-size: 12px; + font-weight: 850; +} + +.review-side-link:disabled { + opacity: 0.5; +} + +.review-side-empty { + display: grid; + justify-items: start; + gap: 8px; + padding: 14px; + border: 1px dashed rgba(203, 213, 225, 0.92); + border-radius: 16px; + background: rgba(255, 255, 255, 0.52); +} + +.review-side-empty-icon { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 12px; + background: rgba(240, 244, 248, 0.96); + color: #94a3b8; + font-size: 18px; +} + +.review-side-empty strong { + color: #475569; + font-size: 13px; + font-weight: 850; +} + +.review-side-empty p { + margin: 0; + color: #94a3b8; + font-size: 12px; + line-height: 1.6; +} + +.review-side-save-pill { + position: sticky; + bottom: 0; + justify-self: end; + min-height: 36px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 14px; + border: 1px solid rgba(16, 185, 129, 0.22); + border-radius: 999px; + background: rgba(255, 255, 255, 0.94); + color: #059669; + font-size: 12px; + font-weight: 850; + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); +} + +.review-side-save-pill:disabled { + opacity: 0.5; + box-shadow: none; +} + +.review-document-switch-card { + gap: 14px; +} + +.review-ticket-drawer { + min-height: 0; +} + +.review-document-switch-head { + align-items: flex-start; +} + +.review-document-nav { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(226, 232, 240, 0.92); + white-space: nowrap; +} + +.review-document-nav span { + color: #334155; + font-size: 11px; + font-weight: 850; +} + +.review-document-nav-btn { + width: 28px; + height: 28px; + display: grid; + place-items: center; + border: 0; + border-radius: 999px; + background: rgba(241, 245, 249, 0.96); + color: #334155; +} + +.review-document-nav-btn:disabled { + opacity: 0.4; +} + +.review-document-stage { + display: grid; + gap: 12px; + min-height: 0; +} + +.review-document-stage-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.review-document-stage-copy { + min-width: 0; + display: grid; + gap: 6px; +} + +.review-document-stage-copy strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; + line-height: 1.5; + word-break: break-word; +} + +.review-document-index-chip { + width: fit-content; + min-height: 24px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: rgba(236, 253, 245, 0.92); + color: #059669; + font-size: 11px; + font-weight: 850; +} + +.review-document-meta-chip-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.review-document-meta-chip { + min-height: 26px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: rgba(248, 250, 252, 0.94); + border: 1px solid rgba(226, 232, 240, 0.92); + color: #475569; + font-size: 11px; + font-weight: 800; +} + +.review-document-meta-chip.confidence { + background: rgba(236, 253, 245, 0.92); + color: #047857; + border-color: rgba(167, 243, 208, 0.92); +} + +.review-document-scroll { + display: grid; + gap: 12px; + max-height: 430px; + overflow-y: auto; + padding-right: 4px; +} + +.review-document-preview-card { + min-height: 168px; + overflow: hidden; + border-radius: 16px; + border: 1px solid rgba(226, 232, 240, 0.94); + background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); +} + +.review-document-preview-card.clickable { + cursor: zoom-in; +} + +.review-document-preview-card.clickable img { + transition: transform 0.18s ease; +} + +.review-document-preview-card.clickable:hover img { + transform: scale(1.02); +} + +.review-document-preview-card.image img { + display: block; + width: 100%; + height: 188px; + object-fit: cover; +} + +.review-document-preview-placeholder { + min-height: 168px; + display: grid; + place-items: center; + gap: 6px; + padding: 18px; + text-align: center; +} + +.review-document-preview-placeholder i { + color: #64748b; + font-size: 34px; +} + +.review-document-preview-placeholder strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.review-document-preview-placeholder p { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.65; +} + +.review-document-edit-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.review-document-edit-field { + display: grid; + gap: 8px; +} + +.review-document-edit-field span { + color: #334155; + font-size: 12px; + font-weight: 800; +} + +.review-document-edit-field input, +.review-document-edit-field textarea { + width: 100%; + border: 1px solid rgba(219, 230, 240, 0.96); + border-radius: 14px; + background: rgba(255, 255, 255, 0.96); + color: #0f172a; + font-size: 13px; + line-height: 1.6; + padding: 10px 12px; + resize: vertical; +} + +.review-document-edit-field input:focus, +.review-document-edit-field textarea:focus { + outline: none; + border-color: rgba(16, 185, 129, 0.36); + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.08); +} + +.review-document-edit-field textarea { + min-height: 88px; +} + +.review-document-warning-list { + display: grid; + gap: 8px; +} + +.review-document-warning-item { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 8px; + align-items: start; + padding: 10px 12px; + border-radius: 14px; + background: rgba(255, 247, 237, 0.92); + border: 1px solid rgba(253, 186, 116, 0.6); + color: #c2410c; + font-size: 12px; + line-height: 1.6; +} + +.review-side-empty.compact { + padding: 12px; +} + +.insight-card { + padding: 16px; + border: 1px solid #e7eef6; + border-radius: 20px; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 14px 24px rgba(241, 245, 249, 0.86); +} + +.insight-card.primary { + background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%); +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.card-head h4 { + color: #0f172a; + font-size: 15px; + font-weight: 850; +} + +.knowledge-question-list { + display: grid; + gap: 10px; +} + +.knowledge-question-btn { + width: 100%; + display: grid; + grid-template-columns: 28px minmax(0, 1fr) 18px; + align-items: center; + gap: 10px; + padding: 12px 14px; + border: 1px solid rgba(226, 232, 240, 0.92); + border-radius: 16px; + background: rgba(248, 250, 252, 0.86); + color: #1e293b; + text-align: left; + transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; +} + +.knowledge-question-btn:hover:not(:disabled) { + border-color: rgba(16, 185, 129, 0.3); + background: rgba(240, 253, 244, 0.9); + transform: translateY(-1px); +} + +.knowledge-question-btn:disabled { + opacity: 0.48; + cursor: not-allowed; + transform: none; +} + +.knowledge-question-btn i { + justify-self: end; + color: #059669; + font-size: 16px; +} + +.knowledge-question-index { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(226, 232, 240, 0.9); + color: #0f172a; + font-size: 12px; + font-weight: 850; +} + +.knowledge-question-index.gold { + background: linear-gradient(135deg, #fbbf24, #f59e0b); + color: #7c2d12; + box-shadow: 0 6px 14px rgba(245, 158, 11, 0.22); +} + +.knowledge-question-index.silver { + background: linear-gradient(135deg, #e2e8f0, #cbd5e1); + color: #334155; + box-shadow: 0 6px 14px rgba(148, 163, 184, 0.18); +} + +.knowledge-question-index.bronze { + background: linear-gradient(135deg, #fdba74, #ea580c); + color: #7c2d12; + box-shadow: 0 6px 14px rgba(234, 88, 12, 0.18); +} + +.knowledge-question-copy { + min-width: 0; + color: #334155; + font-size: 13px; + font-weight: 750; + line-height: 1.5; +} + +.status-pill { + min-height: 28px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.status-pill.success { + background: #ecfdf5; + color: #059669; +} + +.status-pill.warning { + background: #fff7ed; + color: #ea580c; +} + +.status-pill.note { + background: #fdf2f8; + color: #db2777; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.metric-grid.single { + grid-template-columns: 1fr; +} + +.metric-item { + padding: 12px 14px; + border-radius: 16px; + background: #f8fafc; +} + +.metric-item span { + display: block; + color: #94a3b8; + font-size: 11px; + font-weight: 800; +} + +.metric-item strong { + display: block; + margin-top: 6px; + color: #0f172a; + font-size: 14px; + font-weight: 850; + line-height: 1.5; +} + +.timeline-list, +.bullet-list { + display: grid; + gap: 12px; + padding: 0; + margin: 0; + list-style: none; +} + +.timeline-list li { + display: grid; + grid-template-columns: 14px minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +.timeline-dot { + width: 10px; + height: 10px; + margin-top: 5px; + border-radius: 999px; + background: #cbd5e1; +} + +.timeline-list li.done .timeline-dot, +.timeline-list li.current .timeline-dot { + background: #10b981; +} + +.timeline-list strong { + display: block; + color: #0f172a; + font-size: 13px; + font-weight: 800; +} + +.timeline-list p, +.bullet-list li, +.welcome-card p, +.note-block p { + color: #64748b; + font-size: var(--wb-fs-metric); + line-height: 1.6; +} + +.receipt-list { + display: grid; + gap: 10px; +} + +.receipt-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 12px 14px; + border-radius: 16px; + background: #f8fafc; +} + +.receipt-row strong, +.welcome-card strong, +.note-block strong { + color: #0f172a; + font-size: var(--wb-fs-bubble); + font-weight: 850; +} + +.action-card { + background: #fff; +} + +.receipt-row p, +.receipt-row span { + color: #64748b; + font-size: 12px; +} + +.receipt-side { + text-align: right; +} + +.receipt-side strong { + display: block; +} + +.review-message-block { + margin-top: 12px; +} + +.review-summary { + margin: 0; + color: #1f2937; + font-size: 13px; + line-height: 1.75; + white-space: pre-line; +} + +.review-card-shell { + display: grid; + gap: 12px; + padding: 15px; + border-radius: 20px; + border: 1px solid rgba(16, 185, 129, 0.14); + background: + radial-gradient(circle at top right, rgba(34, 197, 94, 0.08), transparent 28%), + linear-gradient(180deg, #fbfffd 0%, #f6fbf9 100%); + box-shadow: 0 8px 20px rgba(226, 232, 240, 0.28); +} + +.review-flow-card { + display: grid; + gap: 10px; + padding-top: 2px; + border-top: 1px solid rgba(226, 232, 240, 0.72); +} + +.review-disclosure-card { + display: grid; + gap: 0; + border-top: 1px solid rgba(226, 232, 240, 0.72); + padding-top: 6px; +} + +.review-disclosure-card summary { + list-style: none; +} + +.review-disclosure-card summary::-webkit-details-marker { + display: none; +} + +.review-disclosure-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 16px; + border: 1px solid rgba(226, 232, 240, 0.92); + background: rgba(255, 255, 255, 0.78); + cursor: pointer; + transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; +} + +.review-disclosure-summary:hover { + border-color: rgba(16, 185, 129, 0.2); + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 6px 16px rgba(226, 232, 240, 0.24); +} + +.review-disclosure-copy { + min-width: 0; + display: grid; + gap: 4px; +} + +.review-disclosure-copy strong { + color: #0f172a; + font-size: 12px; + font-weight: 900; + line-height: 1.4; +} + +.review-disclosure-copy p { + margin: 0; + color: #64748b; + font-size: 11px; + line-height: 1.55; +} + +.review-disclosure-toggle { + width: 28px; + height: 28px; + flex: none; + display: grid; + place-items: center; + border-radius: 999px; + background: rgba(240, 253, 244, 0.86); + color: #059669; + font-size: 16px; + transition: transform 0.18s ease, background 0.18s ease; +} + +.review-disclosure-card[open] .review-disclosure-toggle { + transform: rotate(180deg); + background: rgba(220, 252, 231, 0.92); +} + +.review-disclosure-body { + display: grid; + gap: 10px; + padding: 12px 4px 0; +} + +.review-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.review-card-head-main { + min-width: 0; + display: flex; + align-items: flex-start; + gap: 10px; +} + +.review-card-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 10px; + background: linear-gradient(135deg, #22c55e, #10b981); + color: #fff; + font-size: 16px; + box-shadow: 0 8px 16px rgba(16, 185, 129, 0.16); +} + +.review-card-head-copy { + display: grid; + gap: 4px; +} + +.review-card-head-copy strong { + color: #0f172a; + font-size: 14px; + font-weight: 900; + line-height: 1.35; +} + +.review-card-head-copy p { + margin: 0; + color: #64748b; + font-size: 11px; + line-height: 1.55; +} + +.review-card-state { + min-height: 26px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 850; + white-space: nowrap; +} + +.review-card-state.ready { + background: rgba(240, 253, 244, 0.95); + color: #059669; + border: 1px solid rgba(16, 185, 129, 0.14); +} + +.review-card-state.pending { + background: rgba(255, 251, 235, 0.95); + color: #b45309; + border: 1px solid rgba(245, 158, 11, 0.16); +} + +.review-section-card { + display: grid; + gap: 10px; + padding: 12px 14px; + border-radius: 16px; + border: 1px solid rgba(226, 232, 240, 0.92); + background: rgba(255, 255, 255, 0.76); +} + +.review-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.review-section-head strong { + color: #0f172a; + font-size: 12px; + font-weight: 900; +} + +.review-section-head span { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid #e2e8f0; + color: #475569; + font-size: 10px; + font-weight: 800; +} + +.review-alert-card { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.82) 0%, rgba(251, 248, 243, 0.82) 100%); +} + +/* 已删除:review-alert-chip-row 相关样式(冗余气泡) */ +/* 已删除:主对话框中的风险提示(与右侧边栏重复,已移除) */ + +/* 风险提示样式已统一到 review-pending-item */ +.review-risk-brief-list { + display: none; /* 隐藏原有的独立风险提示列表 */ +} + +.review-risk-brief { + display: none; /* 隐藏原有的独立风险提示项 */ +} + +.review-pending-list { + display: grid; + gap: 8px; +} + +.review-pending-list.plain { + gap: 0; +} + +.review-pending-item { + display: grid; + grid-template-columns: 36px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 11px 12px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(226, 232, 240, 0.92); +} + +.review-pending-list.plain .review-pending-item { + padding: 10px 0; + border: 0; + border-radius: 0; + background: transparent; + border-bottom: 1px solid rgba(226, 232, 240, 0.7); +} + +.review-pending-list.plain .review-pending-item:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.review-pending-list.plain .review-pending-item:first-child { + padding-top: 2px; +} + +.review-pending-icon { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 10px; + background: rgba(236, 253, 245, 0.95); + color: #059669; + font-size: 16px; +} + +/* 风险级别的图标样式(已删除主对话框中的风险提示,保留样式备用) */ +.review-pending-icon.high { + background: rgba(254, 226, 226, 0.95); + color: #dc2626; +} + +.review-pending-icon.warning { + background: rgba(255, 237, 213, 0.95); + color: #ea580c; +} + +.review-pending-list.plain .review-pending-icon { + background: rgba(236, 253, 245, 0.62); +} + +.review-pending-list.plain .review-pending-icon.high { + background: rgba(254, 226, 226, 0.62); +} + +.review-pending-list.plain .review-pending-icon.warning { + background: rgba(255, 237, 213, 0.62); +} + +.review-pending-copy { + display: grid; + gap: 4px; +} + +.review-pending-copy strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.review-pending-copy p { + margin: 0; + color: #64748b; + font-size: 11px; + line-height: 1.5; +} + +.review-pending-status { + min-height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 10px; + border-radius: 999px; + font-size: 10px; + font-weight: 800; + white-space: nowrap; +} + +.review-pending-status.warning { + background: rgba(255, 241, 242, 0.96); + color: #e11d48; + border: 1px solid #fecdd3; +} + +.review-pending-status.danger { + background: rgba(254, 242, 242, 0.96); + color: #dc2626; + border: 1px solid #fca5a5; +} + +.review-pending-status.ready { + background: rgba(240, 253, 244, 0.96); + color: #059669; + border: 1px solid #86efac; +} + +.review-footer-actions { + display: grid; + gap: 8px; + padding-top: 6px; + border-top: 1px solid rgba(226, 232, 240, 0.72); +} + +.review-footer-btn-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.review-footer-btn { + min-height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 14px; + border-radius: 12px; + border: 1px solid #dbe6f0; + background: rgba(255, 255, 255, 0.92); + color: #334155; + font-size: 12px; + font-weight: 800; + box-shadow: 0 3px 10px rgba(241, 245, 249, 0.58); +} + +.review-footer-btn.primary { + border-color: rgba(16, 185, 129, 0.26); + background: linear-gradient(135deg, #10b981, #059669); + color: #fff; + box-shadow: 0 6px 14px rgba(16, 185, 129, 0.16); +} + +.review-footer-btn:disabled { + cursor: not-allowed; + opacity: 0.6; + box-shadow: none; +} + +.review-summary { + margin: 0; + color: #1f2937; + font-size: 14px; + line-height: 1.7; +} + +.review-inline-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-start; +} + +.review-inline-btn, +.primary-dialog-btn, +.secondary-dialog-btn, +.danger-dialog-btn { + min-height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 16px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; +} + +.review-inline-btn { + border: 1px solid #dbe6f0; + background: #fff; + color: #334155; +} + +.review-inline-btn.primary, +.primary-dialog-btn { + border: 1px solid rgba(16, 185, 129, 0.22); + background: linear-gradient(135deg, #10b981, #059669); + color: #fff; + box-shadow: 0 10px 22px rgba(16, 185, 129, 0.18); +} + +.review-inline-btn.secondary, +.secondary-dialog-btn { + border: 1px solid #dbe6f0; + background: #fff; + color: #334155; +} + +.danger-dialog-btn { + border: 1px solid rgba(239, 68, 68, 0.22); + background: linear-gradient(135deg, #ef4444, #dc2626); + color: #fff; + box-shadow: 0 10px 22px rgba(239, 68, 68, 0.18); +} + +.review-inline-btn:disabled, +.primary-dialog-btn:disabled, +.secondary-dialog-btn:disabled, +.danger-dialog-btn:disabled { + cursor: not-allowed; + opacity: 0.62; + box-shadow: none; +} + +.review-inline-note { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.6; +} + +.review-inline-guidance { + margin: 0; + color: #0f766e; + font-size: 12px; + line-height: 1.7; +} + +.review-status-banner { + display: grid; + gap: 8px; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid #dbeafe; + background: linear-gradient(180deg, #f8fbff 0%, #f0f7ff 100%); +} + +.review-status-banner.ready { + border-color: #bbf7d0; + background: linear-gradient(180deg, #f5fffa 0%, #ecfdf5 100%); +} + +.review-status-banner.pending { + border-color: #fde68a; + background: linear-gradient(180deg, #fffdf7 0%, #fffbeb 100%); +} + +.review-status-tag { + width: fit-content; + min-height: 26px; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.86); + color: #0f172a; + font-size: 12px; + font-weight: 850; + border: 1px solid rgba(148, 163, 184, 0.22); +} + +.review-inline-section { + display: grid; + gap: 10px; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid #e2e8f0; + background: rgba(255, 255, 255, 0.88); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.66); +} + +.review-inline-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.review-inline-head > strong { + color: #0f172a; + font-size: 12px; + font-weight: 850; +} + +.review-inline-head > span { + min-height: 24px; + display: inline-flex; + align-items: center; + padding: 0 9px; + border-radius: 999px; + background: #fff; + color: #475569; + font-size: 11px; + font-weight: 800; + border: 1px solid #e2e8f0; +} + +.review-inline-caption { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.65; +} + +.review-inline-list { + display: grid; + gap: 8px; +} + +.review-missing-chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.review-missing-chip { + min-height: 30px; + display: inline-flex; + align-items: center; + padding: 0 12px; + border-radius: 999px; + background: #fff; + color: #0f172a; + font-size: 12px; + font-weight: 800; + border: 1px solid #fed7aa; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); +} + +.review-inline-item { + display: grid; + gap: 4px; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid #e2e8f0; + background: #fff; +} + +.review-inline-item.warning { + background: #fff7ed; + border-color: #fed7aa; +} + +.review-inline-item.high { + background: #fff1f2; + border-color: #fecdd3; +} + +.review-inline-item span { + color: #0f172a; + font-size: 12px; + font-weight: 800; +} + +.review-inline-item p { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.65; +} + +.review-inline-footer { + display: grid; + gap: 10px; + padding-top: 2px; + border-top: 1px dashed rgba(203, 213, 225, 0.78); +} + +.review-mini-grid, +.review-slot-grid, +.review-doc-field-grid { + display: grid; + gap: 10px; +} + +.review-mini-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.review-slot-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.review-slot-card, +.review-doc-field-card, +.review-brief-card, +.review-claim-card, +.review-document-card { + border: 1px solid #e2e8f0; + border-radius: 16px; + background: #f8fbff; +} + +.review-slot-card { + display: grid; + gap: 8px; + padding: 12px 14px; +} + +.review-slot-card.compact { + gap: 4px; + padding: 10px 12px; +} + +.review-slot-card header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.review-slot-card span, +.review-doc-field-card span, +.review-brief-card strong, +.review-document-card header span { + color: #64748b; + font-size: 11px; + font-weight: 800; +} + +.review-slot-card strong, +.review-doc-field-card strong, +.review-claim-card strong, +.review-document-card header strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; +} + +.review-slot-card p, +.review-brief-card p, +.review-claim-card p, +.review-document-card p { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.6; +} + +.review-slot-card.missing { + border-color: #fecdd3; + background: #fff7f7; +} + +.review-slot-card.inferred { + border-color: #dbeafe; + background: #f8fbff; +} + +.review-slot-meta-list { + display: grid; + gap: 8px; +} + +.review-slot-meta-item { + padding: 9px 10px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(226, 232, 240, 0.9); +} + +.review-slot-meta-item span { + color: #94a3b8; + font-size: 11px; + font-weight: 800; +} + +.review-slot-meta-item strong { + display: block; + margin-top: 4px; + font-size: 12px; +} + +.review-brief-list, +.review-claim-list, +.review-document-list { + display: grid; + gap: 10px; +} + +.review-brief-card, +.review-claim-card, +.review-document-card { + padding: 12px 14px; +} + +.review-brief-card.warning { + background: #fff7ed; + border-color: #fed7aa; +} + +.review-brief-card.high { + background: #fff1f2; + border-color: #fecdd3; +} + +.review-claim-card header, +.review-document-card header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.review-document-card { + display: grid; + gap: 10px; +} + +.document-preview { + min-height: 124px; + overflow: hidden; + border-radius: 14px; + background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); + border: 1px dashed #dbe3ec; +} + +.document-preview.image img { + display: block; + width: 100%; + height: 180px; + object-fit: cover; +} + +.document-preview-placeholder { + min-height: 124px; + display: grid; + place-items: center; + gap: 6px; + color: #64748b; + text-align: center; +} + +.document-preview-placeholder i { + font-size: 28px; +} + +.review-doc-field-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.review-doc-field-card { + padding: 10px 12px; +} + +.action-list.compact { + grid-template-columns: 1fr; +} + +.action-card.primary { + border-color: #bbf7d0; + background: #f0fdf4; +} + +.action-card.secondary { + background: #fff; +} + +.action-card.warning { + border-color: #fed7aa; + background: #fff7ed; +} + +.note-block { + display: grid; + gap: 8px; + padding: 14px; + border-radius: 16px; + background: #f8fafc; +} + +.note-block span { + color: #94a3b8; + font-size: 11px; + font-weight: 800; +} + +.review-conclusion strong { + font-size: var(--wb-fs-insight-h4); + line-height: 1.6; +} + +.insight-text-section { + display: grid; + gap: 12px; + padding: 2px 0 0; +} + +.insight-text-section h4 { + color: #0f172a; + font-size: var(--wb-fs-insight-h4); + font-weight: 850; +} + +.insight-text-list, +.review-document-plain-list { + display: grid; + gap: 12px; +} + +.recognition-bubble { + display: grid; + gap: 10px; + padding: 16px 18px; + border-radius: 22px; + border: 1px solid rgba(191, 219, 254, 0.9); + background: linear-gradient(180deg, #ffffff 0%, #f5fbff 100%); + box-shadow: 0 16px 28px rgba(241, 245, 249, 0.9); +} + +.recognition-bubble.secondary { + border-color: #e2e8f0; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.recognition-bubble-label { + color: #0f766e; + font-size: 11px; + font-weight: 850; + letter-spacing: 0.02em; +} + +.recognition-bubble.secondary .recognition-bubble-label { + color: #475569; +} + +.recognition-bubble-copy { + display: grid; + gap: 8px; +} + +.recognition-bubble-line, +.recognition-bubble-note { + margin: 0; + color: #334155; + font-size: 13px; + line-height: 1.75; +} + +.recognition-bubble-line { + font-weight: 700; + color: #0f172a; +} + +.recognition-bubble-note { + color: #64748b; +} + +.review-document-bubble { + display: grid; + grid-template-columns: minmax(0, 1fr) 140px; + gap: 14px; + align-items: start; + padding: 16px; + border-radius: 22px; + background: linear-gradient(180deg, #ffffff 0%, #f7fafc 100%); + border: 1px solid rgba(226, 232, 240, 0.95); + box-shadow: 0 16px 28px rgba(241, 245, 249, 0.92); +} + +.review-document-copy { + display: grid; + gap: 6px; +} + +.review-document-index { + color: #1d4ed8; + font-size: 11px; + font-weight: 850; +} + +.review-document-copy strong { + color: #0f172a; + font-size: 13px; + font-weight: 850; + line-height: 1.6; +} + +.review-document-copy p { + margin: 0; + color: #64748b; + font-size: 12px; + line-height: 1.7; +} + +.review-overlay { + z-index: 10001; +} + +.review-confirm-modal, +.review-edit-modal { + width: min(720px, calc(100vw - 40px)); + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 28%), + linear-gradient(180deg, #fbfdff 0%, #f6f9fc 100%); + box-shadow: + 0 24px 80px rgba(15, 23, 42, 0.22), + 0 2px 12px rgba(15, 23, 42, 0.08); + border: 1px solid #e7eef6; +} + +.review-confirm-modal { + padding: 24px; + display: grid; + gap: 18px; +} + +.review-confirm-modal h3, +.review-edit-head h3 { + margin-top: 12px; + color: #0f172a; + font-size: 22px; + font-weight: 900; + line-height: 1.35; +} + +.review-confirm-modal p, +.review-edit-head p { + margin-top: 8px; + color: #64748b; + font-size: 14px; + line-height: 1.7; +} + +.review-confirm-actions, +.review-edit-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + flex-wrap: wrap; +} + +.review-upload-decision-modal { + display: grid; + gap: 18px; +} + +.review-upload-decision-copy { + display: grid; + gap: 10px; +} + +.review-upload-decision-actions { + justify-content: stretch; +} + +.review-upload-decision-actions .primary-dialog-btn, +.review-upload-decision-actions .secondary-dialog-btn { + flex: 1 1 168px; +} + +.review-edit-modal { + max-height: min(860px, calc(100vh - 48px)); + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; +} + +.review-edit-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 22px 24px 18px; + border-bottom: 1px solid #eef2f7; +} + +.review-edit-form { + min-height: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + padding: 20px 24px; + overflow-y: auto; +} + +.review-edit-field { + display: grid; + gap: 8px; +} + +.review-edit-field.attachments, +.review-edit-field.business, +.review-edit-field.basic { + min-width: 0; +} + +.review-edit-field span { + color: #334155; + font-size: 13px; + font-weight: 800; +} + +.review-edit-field span em { + margin-left: 4px; + color: #dc2626; + font-style: normal; +} + +.review-edit-field input, +.review-edit-field textarea { + width: 100%; + border: 1px solid #dbe6f0; + border-radius: 16px; + background: #fff; + color: #0f172a; + font-size: 14px; + line-height: 1.6; + padding: 12px 14px; + resize: vertical; +} + +.review-edit-field input:focus, +.review-edit-field textarea:focus { + outline: none; + border-color: #60a5fa; + box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.14); +} + +.review-edit-field textarea { + min-height: 96px; +} + +.review-edit-field.attachments, +.review-edit-field textarea, +.review-edit-field .textarea { + grid-column: span 2; +} + +.review-edit-actions { + padding: 0 24px 24px; +} + +.review-preview-modal { + width: min(980px, calc(100vw - 40px)); + max-height: min(92vh, calc(100vh - 32px)); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + overflow: hidden; + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 28%), + linear-gradient(180deg, #fbfdff 0%, #f6f9fc 100%); + box-shadow: + 0 24px 80px rgba(15, 23, 42, 0.22), + 0 2px 12px rgba(15, 23, 42, 0.08); + border: 1px solid #e7eef6; +} + +.review-preview-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 22px 24px 18px; + border-bottom: 1px solid #eef2f7; +} + +.review-preview-head h3 { + margin-top: 12px; + color: #0f172a; + font-size: 22px; + font-weight: 900; + line-height: 1.35; +} + +.review-preview-body { + min-height: 0; + display: grid; + place-items: center; + padding: 18px; + background: rgba(248, 250, 252, 0.88); +} + +.review-preview-body.image img { + max-width: 100%; + max-height: calc(92vh - 170px); + display: block; + border-radius: 20px; + object-fit: contain; + box-shadow: 0 16px 34px rgba(148, 163, 184, 0.26); +} + +.review-preview-body.pdf iframe { + width: 100%; + height: min(78vh, 820px); + border: 0; + border-radius: 18px; + background: #fff; +} + +.welcome-grid { + display: grid; + gap: 12px; +} + +.welcome-card { + padding: 14px; + border-radius: 18px; + background: #f8fafc; +} + +.welcome-card i { + color: #10b981; + font-size: var(--wb-fs-welcome); +} + +.welcome-card strong { + display: block; + margin-top: 10px; +} + +.assistant-modal-enter-active, +.assistant-modal-leave-active { + transition: opacity 220ms ease; +} + +.assistant-modal-enter-active .assistant-modal, +.assistant-modal-leave-active .assistant-modal { + transition: transform 260ms ease, opacity 220ms ease; +} + +.assistant-modal-enter-from, +.assistant-modal-leave-to { + opacity: 0; +} + +.assistant-modal-enter-from .assistant-modal, +.assistant-modal-leave-to .assistant-modal { + transform: translateY(10px) scale(0.985); + opacity: 0; +} + +.insight-switch-enter-active, +.insight-switch-leave-active { + transition: opacity 180ms ease, transform 180ms ease; +} + +.insight-switch-enter-from, +.insight-switch-leave-to { + opacity: 0; + transform: translateY(8px); +} + +/* 笔记本 / 中等屏:工作台正文字号整体下调一档 */ +@media (max-width: 1680px) { + .assistant-modal-stage { + --wb-fs-title: 19px; + --wb-fs-desc: 12px; + --wb-fs-badge: 11px; + --wb-fs-bubble: 13px; + --wb-fs-bubble-meta: 12px; + --wb-fs-bubble-time: 11px; + --wb-fs-chip: 11px; + --wb-fs-composer: 13px; + --wb-fs-tool-icon: 16px; + --wb-fs-md-h1: 16px; + --wb-fs-md-h2: 15px; + --wb-fs-md-h3: 13px; + --wb-fs-insight-title: 17px; + --wb-fs-insight-num: 17px; + --wb-fs-insight-body: 11px; + --wb-fs-insight-h4: 14px; + --wb-fs-metric: 12px; + --wb-fs-metric-strong: 12px; + --wb-fs-welcome: 18px; + } + + .assistant-modal-stage .message-answer-markdown :deep(table) { + font-size: 12px; + } + + .assistant-modal-stage .intent-pill { + font-size: var(--wb-fs-chip); + } +} + +@media (max-width: 1440px) { + .assistant-modal-stage { + --wb-fs-title: 18px; + --wb-fs-bubble: 12px; + --wb-fs-bubble-meta: 11px; + --wb-fs-composer: 12px; + --wb-fs-insight-title: 16px; + --wb-fs-insight-num: 16px; + --wb-fs-md-h1: 15px; + --wb-fs-md-h2: 14px; + --wb-fs-insight-h4: 13px; + --wb-fs-welcome: 17px; + } +} + +/* 大屏:左右分栏;右侧详情区宽度随视口收缩 */ +@media (min-width: 1441px) and (max-width: 1680px) { + .insight-panel-shell { + width: clamp(280px, 26vw, 360px); + } +} + +/* 笔记本常见宽度:改为上下布局,对话区占满宽度,避免侧栏挤占 */ +@media (max-width: 1440px) { + .assistant-layout { + flex-direction: column; + } + + .dialog-panel { + flex: 1 1 auto; + min-height: 0; + } + + .insight-panel-shell { + width: 100%; + flex: 0 0 auto; + max-height: min(38dvh, 400px); + transition: + max-height 320ms cubic-bezier(0.22, 1, 0.36, 1), + opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), + transform 280ms cubic-bezier(0.22, 1, 0.36, 1); + } + + .insight-panel-shell.collapsed { + max-height: 0; + } + + .insight-panel { + width: 100%; + min-height: min(280px, 32dvh); + } + + .insight-panel-shell.collapsed .insight-panel { + transform: translateY(-12px); + } + + .review-side-grid.compact { + grid-template-columns: 1fr; + } +} + +/* 矮屏笔记本(如 1366×768):压缩顶栏与间距,把高度留给对话列表 */ +@media (max-height: 820px) { + .assistant-modal-stage { + --wb-fs-title: 17px; + --wb-fs-bubble: 12px; + --wb-fs-composer: 12px; + --wb-fs-insight-title: 15px; + --wb-fs-insight-num: 15px; + } + + .assistant-header { + padding-top: 12px; + padding-bottom: 10px; + } + + .assistant-header-actions { + top: 12px; + right: 12px; + } + + .assistant-layout { + padding: 10px; + gap: 10px; + } + + .dialog-toolbar { + padding: 12px 14px 10px; + } + + .message-list { + padding: 12px; + gap: 10px; + } + + .composer-shell-body { + padding: 4px 10px; + } +} + +@media (max-width: 1280px) { + .insight-panel-shell:not(.collapsed) { + max-height: min(34dvh, 360px); + } +} + +@media (max-width: 760px) { + .assistant-overlay { + --assistant-viewport-inset: 10px; + } + + .assistant-modal, + .assistant-modal-stage { + border-radius: 18px; + } + + .assistant-header { + padding: 18px 18px 16px; + align-items: flex-start; + flex-direction: column; + } + + .assistant-header-actions { + top: 18px; + right: 18px; + gap: 10px; + width: auto; + justify-content: space-between; + } + + .assistant-toggle-btn, + .session-trash-btn, + .assistant-close-btn, + .close-btn { + width: 40px; + height: 40px; + border-radius: 14px; + font-size: 16px; + } + + .assistant-layout { + padding: 14px; + } + + .composer-row { + gap: 8px; + --composer-control-size: 40px; + } + + .composer-shell textarea { + min-height: 32px; + } + + .dialog-toolbar { + padding: 16px 16px 12px; + } + + .shortcut-chip { + width: 100%; + justify-content: center; + } + + .message-list { + padding: 16px; + } + + .message-row, + .message-row.user { + grid-template-columns: 34px minmax(0, 1fr); + } + + .message-row.user .message-avatar { + order: 0; + } + + .message-row.user .message-bubble { + order: 0; + justify-self: stretch; + } + + .composer { + padding: 0 16px 16px; + } + + .composer-files-head, + .review-insight-title-row, + .review-document-stage-head, + .review-document-switch-head { + align-items: flex-start; + flex-direction: column; + } + + .composer-files-actions, + .review-document-nav { + width: 100%; + justify-content: space-between; + } + + .review-card-head { + flex-direction: column; + } + + .metric-grid { + grid-template-columns: 1fr; + } + + .review-side-grid, + .review-side-category-grid, + .review-document-edit-grid { + grid-template-columns: 1fr; + } + + .review-pending-item { + grid-template-columns: 42px minmax(0, 1fr); + } + + .review-pending-status { + grid-column: 2; + justify-self: start; + } + + .review-footer-btn-row { + flex-direction: column; + } + + .review-footer-btn { + width: 100%; + } + + .review-slot-grid, + .review-doc-field-grid, + .review-mini-grid { + grid-template-columns: 1fr; + } + + .review-document-plain, + .review-document-bubble { + grid-template-columns: 1fr; + } + + .review-edit-modal { + width: calc(100vw - 24px); + } + + .review-preview-modal { + width: calc(100vw - 24px); + } + + .review-edit-form { + grid-template-columns: 1fr; + padding: 18px; + } + + .review-edit-field.attachments, + .review-edit-field textarea, + .review-edit-field .textarea { + grid-column: auto; + } + + .review-edit-actions, + .review-confirm-actions { + padding: 0 18px 18px; + justify-content: stretch; + } + + .review-upload-decision-actions { + width: 100%; + } + + .primary-dialog-btn, + .secondary-dialog-btn, + .danger-dialog-btn { + width: 100%; + } +} diff --git a/web/src/components/shared/ConfirmDialog.vue b/web/src/components/shared/ConfirmDialog.vue index 1e07c1f..8cadeb5 100644 --- a/web/src/components/shared/ConfirmDialog.vue +++ b/web/src/components/shared/ConfirmDialog.vue @@ -1,261 +1,261 @@ - - - - - + + + + + diff --git a/web/src/composables/useAppShell.js b/web/src/composables/useAppShell.js index c191ad7..ae7ff5a 100644 --- a/web/src/composables/useAppShell.js +++ b/web/src/composables/useAppShell.js @@ -1,310 +1,310 @@ -import { computed, ref } from 'vue' -import { useRoute, useRouter } from 'vue-router' - -import { useNavigation, navItems } from './useNavigation.js' -import { useRequests } from './useRequests.js' -import { useSystemState } from './useSystemState.js' -import { useToast } from './useToast.js' -import { fetchLatestConversation } from '../services/orchestrator.js' -import { normalizeRequestForUi } from '../utils/requestViewModel.js' - -const SESSION_TYPE_EXPENSE = 'expense' - -function isPlaceholderValue(value) { - const text = String(value || '').trim() - if (!text) { - return true - } - - return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, '')) -} - -function hasMissingAttachment(request) { - const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : [] - - if (expenseItems.length) { - return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim()) - } - - const attachmentSummary = String(request?.attachmentSummary || '').trim() - const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim() - return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue) -} - -function hasPendingInfo(request) { - if (!request) { - return false - } - - if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') { - return true - } - - if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) { - return true - } - - return [ - request.profileDepartment, - request.profilePosition, - request.profileGrade, - request.profileManager, - request.reason, - request.occurredDisplay - ].some(isPlaceholderValue) -} - -function resolveDetailAlertTone(request) { - if (request?.approvalKey === 'completed') return 'success' - if (request?.approvalKey === 'rejected') return 'danger' - return 'warning' -} - -function buildDetailAlerts(request) { - if (!request) { - return [] - } - - const alerts = [] - const nodeLabel = String(request.node || request.approval || '').trim() - - if (nodeLabel) { - alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) }) - } - - if (hasMissingAttachment(request)) { - alerts.push({ label: '缺少票据', tone: 'warning' }) - } - - if (hasPendingInfo(request)) { - alerts.push({ label: '待补信息', tone: 'warning' }) - } - - return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3) -} - -export function useAppShell() { - const route = useRoute() - const router = useRouter() - - const smartEntryOpen = ref(false) - const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null }) - const smartEntrySessionId = ref(0) - - const { activeView, currentView, setView } = useNavigation() - const { - requests, - loading: requestsLoading, - error: requestsError, - search, - filters, - ranges, - activeRange, - filteredRequests, - approveRequest, - rejectRequest, - reload: reloadRequests - } = useRequests() - const { currentUser } = useSystemState() - const { toast } = useToast() - - const customRange = ref({ start: '2024-07-06', end: '2024-07-12' }) - - const selectedRequest = computed(() => { - const requestId = String(route.params.requestId || '') - - if (!requestId) { - return null - } - - const rawRequest = requests.value.find( - (item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId - ) - return normalizeRequestForUi(rawRequest) - }) - - const detailMode = computed(() => route.name === 'app-request-detail') - const logDetailMode = computed(() => route.name === 'app-log-detail') - const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : [])) - - const topBarView = computed(() => { - if (detailMode.value) { - return { - title: '报销单详情', - desc: '查看报销明细、票据材料、审批进度与风险提示。' - } - } - - if (logDetailMode.value) { - return { - title: '日志详情', - desc: '查看单条日志的解析结果、上下文信息与原始记录。' - } - } - - return currentView.value - }) - - const requestSummary = computed(() => - filteredRequests.value.reduce( - (summary, item) => { - const request = normalizeRequestForUi(item) - if (!request) { - return summary - } - - summary.total += 1 - - if (request.approvalKey === 'draft') { - summary.draft += 1 - } else if (request.approvalKey === 'in_progress') { - summary.inProgress += 1 - } else if (request.approvalKey === 'supplement') { - summary.supplement += 1 - } else if (request.approvalKey === 'completed') { - summary.completed += 1 - } - - return summary - }, - { total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 } - ) - ) - - function handleApprove(request) { - const message = approveRequest(request) - toast(message) - } - - function handleReject(request) { - const message = rejectRequest(request) - toast(message) - } - - function handleNavigate(view) { - smartEntryOpen.value = false - setView(view) - } - - function openTravelCreate() { - smartEntryOpen.value = true - smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null } - smartEntrySessionId.value += 1 - } - - function resolveCurrentUserId() { - const user = currentUser.value || {} - return String(user.username || user.name || 'anonymous').trim() || 'anonymous' - } - - async function resolveSmartEntryConversation(payload = {}) { - if (payload.conversation) { - return payload.conversation - } - - if (!payload.restoreLatestConversation) { - return null - } - - try { - const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, { - preferRecoverable: true - }) - return latestPayload?.found ? latestPayload.conversation || null : null - } catch (error) { - console.warn('Failed to restore latest expense conversation for smart entry:', error) - toast(error?.message || '恢复最近报销会话失败,请稍后重试。') - return null - } - } - - async function openSmartEntry(payload = {}) { - const conversation = await resolveSmartEntryConversation(payload) - smartEntryOpen.value = true - - smartEntryContext.value = { - prompt: payload.prompt ?? '', - source: payload.source ?? 'workbench', - request: payload.request ?? selectedRequest.value, - files: Array.isArray(payload.files) ? payload.files : [], - conversation - } - smartEntrySessionId.value += 1 - } - - function closeSmartEntry() { - smartEntryOpen.value = false - } - - async function handleDraftSaved(payload = {}) { - const claimNo = String(payload.claimNo || payload.claim_no || '').trim() - const status = String(payload.status || payload.claimStatus || '').trim() - const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim() - smartEntryOpen.value = false - await reloadRequests() - if (status === 'submitted') { - toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`) - } else { - toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`) - } - router.push({ name: 'app-requests' }) - } - - function openRequestDetail(request) { - router.push({ - name: 'app-request-detail', - params: { requestId: request.claimId || request.id } - }) - } - - function closeRequestDetail() { - router.push({ name: 'app-requests' }) - } - - async function handleRequestUpdated() { - await reloadRequests() - } - - async function handleRequestDeleted() { - await reloadRequests() - router.push({ name: 'app-requests' }) - } - - return { - activeRange, - activeView, - closeRequestDetail, - closeSmartEntry, - currentView, - customRange, - detailMode, - logDetailMode, - filteredRequests, - filters, - handleApprove, - handleDraftSaved, - handleNavigate, - handleReject, - handleRequestDeleted, - handleRequestUpdated, - navItems, - openRequestDetail, - openSmartEntry, - openTravelCreate, - ranges, - requestSummary, - requestsError, - requestsLoading, - reloadRequests, - requests, - search, - selectedRequest, - setView, - smartEntryContext, - smartEntryOpen, - smartEntrySessionId, - detailAlerts, - toast, - topBarView - } -} +import { computed, ref } from 'vue' +import { useRoute, useRouter } from 'vue-router' + +import { useNavigation, navItems } from './useNavigation.js' +import { useRequests } from './useRequests.js' +import { useSystemState } from './useSystemState.js' +import { useToast } from './useToast.js' +import { fetchLatestConversation } from '../services/orchestrator.js' +import { normalizeRequestForUi } from '../utils/requestViewModel.js' + +const SESSION_TYPE_EXPENSE = 'expense' + +function isPlaceholderValue(value) { + const text = String(value || '').trim() + if (!text) { + return true + } + + return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, '')) +} + +function hasMissingAttachment(request) { + const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : [] + + if (expenseItems.length) { + return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim()) + } + + const attachmentSummary = String(request?.attachmentSummary || '').trim() + const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim() + return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue) +} + +function hasPendingInfo(request) { + if (!request) { + return false + } + + if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') { + return true + } + + if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) { + return true + } + + return [ + request.profileDepartment, + request.profilePosition, + request.profileGrade, + request.profileManager, + request.reason, + request.occurredDisplay + ].some(isPlaceholderValue) +} + +function resolveDetailAlertTone(request) { + if (request?.approvalKey === 'completed') return 'success' + if (request?.approvalKey === 'rejected') return 'danger' + return 'warning' +} + +function buildDetailAlerts(request) { + if (!request) { + return [] + } + + const alerts = [] + const nodeLabel = String(request.node || request.approval || '').trim() + + if (nodeLabel) { + alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) }) + } + + if (hasMissingAttachment(request)) { + alerts.push({ label: '缺少票据', tone: 'warning' }) + } + + if (hasPendingInfo(request)) { + alerts.push({ label: '待补信息', tone: 'warning' }) + } + + return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3) +} + +export function useAppShell() { + const route = useRoute() + const router = useRouter() + + const smartEntryOpen = ref(false) + const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null }) + const smartEntrySessionId = ref(0) + + const { activeView, currentView, setView } = useNavigation() + const { + requests, + loading: requestsLoading, + error: requestsError, + search, + filters, + ranges, + activeRange, + filteredRequests, + approveRequest, + rejectRequest, + reload: reloadRequests + } = useRequests() + const { currentUser } = useSystemState() + const { toast } = useToast() + + const customRange = ref({ start: '2024-07-06', end: '2024-07-12' }) + + const selectedRequest = computed(() => { + const requestId = String(route.params.requestId || '') + + if (!requestId) { + return null + } + + const rawRequest = requests.value.find( + (item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId + ) + return normalizeRequestForUi(rawRequest) + }) + + const detailMode = computed(() => route.name === 'app-request-detail') + const logDetailMode = computed(() => route.name === 'app-log-detail') + const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : [])) + + const topBarView = computed(() => { + if (detailMode.value) { + return { + title: '报销单详情', + desc: '查看报销明细、票据材料、审批进度与风险提示。' + } + } + + if (logDetailMode.value) { + return { + title: '日志详情', + desc: '查看单条日志的解析结果、上下文信息与原始记录。' + } + } + + return currentView.value + }) + + const requestSummary = computed(() => + filteredRequests.value.reduce( + (summary, item) => { + const request = normalizeRequestForUi(item) + if (!request) { + return summary + } + + summary.total += 1 + + if (request.approvalKey === 'draft') { + summary.draft += 1 + } else if (request.approvalKey === 'in_progress') { + summary.inProgress += 1 + } else if (request.approvalKey === 'supplement') { + summary.supplement += 1 + } else if (request.approvalKey === 'completed') { + summary.completed += 1 + } + + return summary + }, + { total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 } + ) + ) + + function handleApprove(request) { + const message = approveRequest(request) + toast(message) + } + + function handleReject(request) { + const message = rejectRequest(request) + toast(message) + } + + function handleNavigate(view) { + smartEntryOpen.value = false + setView(view) + } + + function openTravelCreate() { + smartEntryOpen.value = true + smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null } + smartEntrySessionId.value += 1 + } + + function resolveCurrentUserId() { + const user = currentUser.value || {} + return String(user.username || user.name || 'anonymous').trim() || 'anonymous' + } + + async function resolveSmartEntryConversation(payload = {}) { + if (payload.conversation) { + return payload.conversation + } + + if (!payload.restoreLatestConversation) { + return null + } + + try { + const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, { + preferRecoverable: true + }) + return latestPayload?.found ? latestPayload.conversation || null : null + } catch (error) { + console.warn('Failed to restore latest expense conversation for smart entry:', error) + toast(error?.message || '恢复最近报销会话失败,请稍后重试。') + return null + } + } + + async function openSmartEntry(payload = {}) { + const conversation = await resolveSmartEntryConversation(payload) + smartEntryOpen.value = true + + smartEntryContext.value = { + prompt: payload.prompt ?? '', + source: payload.source ?? 'workbench', + request: payload.request ?? selectedRequest.value, + files: Array.isArray(payload.files) ? payload.files : [], + conversation + } + smartEntrySessionId.value += 1 + } + + function closeSmartEntry() { + smartEntryOpen.value = false + } + + async function handleDraftSaved(payload = {}) { + const claimNo = String(payload.claimNo || payload.claim_no || '').trim() + const status = String(payload.status || payload.claimStatus || '').trim() + const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim() + smartEntryOpen.value = false + await reloadRequests() + if (status === 'submitted') { + toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`) + } else { + toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`) + } + router.push({ name: 'app-requests' }) + } + + function openRequestDetail(request) { + router.push({ + name: 'app-request-detail', + params: { requestId: request.claimId || request.id } + }) + } + + function closeRequestDetail() { + router.push({ name: 'app-requests' }) + } + + async function handleRequestUpdated() { + await reloadRequests() + } + + async function handleRequestDeleted() { + await reloadRequests() + router.push({ name: 'app-requests' }) + } + + return { + activeRange, + activeView, + closeRequestDetail, + closeSmartEntry, + currentView, + customRange, + detailMode, + logDetailMode, + filteredRequests, + filters, + handleApprove, + handleDraftSaved, + handleNavigate, + handleReject, + handleRequestDeleted, + handleRequestUpdated, + navItems, + openRequestDetail, + openSmartEntry, + openTravelCreate, + ranges, + requestSummary, + requestsError, + requestsLoading, + reloadRequests, + requests, + search, + selectedRequest, + setView, + smartEntryContext, + smartEntryOpen, + smartEntrySessionId, + detailAlerts, + toast, + topBarView + } +} diff --git a/web/src/services/agentAssets.js b/web/src/services/agentAssets.js index e592553..65a661a 100644 --- a/web/src/services/agentAssets.js +++ b/web/src/services/agentAssets.js @@ -84,16 +84,12 @@ export function fetchAgentAssetDetail(assetId) { return apiRequest(`/agent-assets/${assetId}`) } -export function fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId, version = '') { - const query = buildQuery({ version }) - return apiRequest(`/agent-assets/${assetId}/spreadsheet/onlyoffice-config${query}`) +export function fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId) { + return apiRequest(`/agent-assets/${assetId}/spreadsheet/onlyoffice-config`) } -export function fetchAgentAssetSpreadsheetBlob(assetId, version = '', disposition = 'inline') { +export function fetchAgentAssetSpreadsheetBlob(assetId, disposition = 'inline') { const search = new URLSearchParams() - if (version) { - search.set('version', String(version).trim()) - } if (disposition) { search.set('disposition', String(disposition).trim()) } @@ -148,14 +144,6 @@ export function saveAgentAssetRuleJson(assetId, payload, options = {}) { }) } -export function compareAgentAssetSpreadsheetVersions(assetId, baseVersion, targetVersion) { - const query = new URLSearchParams({ - base_version: String(baseVersion || '').trim(), - target_version: String(targetVersion || '').trim() - }) - return apiRequest(`/agent-assets/${assetId}/versions/compare?${query.toString()}`) -} - export function fetchAgentAssetSpreadsheetChangeRecords(assetId, limit = 30) { return apiRequest( `/agent-assets/${assetId}/spreadsheet/change-records${buildQuery({ limit })}` diff --git a/web/src/views/AuditView.vue b/web/src/views/AuditView.vue index 93fe85e..1e624cd 100644 --- a/web/src/views/AuditView.vue +++ b/web/src/views/AuditView.vue @@ -103,7 +103,7 @@
- {{ selectedSpreadsheetVersionModeLabel }} + {{ selectedSpreadsheetModeLabel }}
@@ -153,35 +153,34 @@ - +

暂无修改记录

+ + @@ -1086,11 +1085,9 @@ {{ item.timeLabel }}

{{ item.description || item.note || '暂无补充说明' }}

- - 操作人:{{ item.actor }} - - - + + 操作人:{{ item.actor }} + @@ -1129,12 +1126,8 @@ 修改时间 {{ selectedSpreadsheetChangeRecord.time }} -
- 关联版本 - {{ selectedSpreadsheetChangeRecord.version }} -
-
- 修改工作表 +
+ 修改工作表 {{ selectedSpreadsheetChangeRecord.changed_sheet_count }}
@@ -1203,127 +1196,6 @@ - -
- -
-
diff --git a/web/src/views/TravelReimbursementCreateView.vue b/web/src/views/TravelReimbursementCreateView.vue index 1fe4df9..0f90910 100644 --- a/web/src/views/TravelReimbursementCreateView.vue +++ b/web/src/views/TravelReimbursementCreateView.vue @@ -1,7 +1,7 @@