705 lines
24 KiB
Python
705 lines
24 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Build expense-control demo risk rule JSON manifests."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import UTC, datetime
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
SERVER_DIR = Path(__file__).resolve().parents[1]
|
||
|
|
RISK_RULE_DIR = SERVER_DIR / "rules" / "risk-rules"
|
||
|
|
|
||
|
|
|
||
|
|
BUDGET_EXPENSE_TYPES = (
|
||
|
|
"travel",
|
||
|
|
"hotel",
|
||
|
|
"transport",
|
||
|
|
"meal",
|
||
|
|
"meeting",
|
||
|
|
"marketing",
|
||
|
|
"office",
|
||
|
|
"training",
|
||
|
|
"software",
|
||
|
|
"communication",
|
||
|
|
"welfare",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
FIELD_LABELS = {
|
||
|
|
"claim.amount": ("报销金额", "number", "claim"),
|
||
|
|
"claim.expense_type": ("费用类型", "enum", "claim"),
|
||
|
|
"claim.department_name": ("部门", "text", "claim"),
|
||
|
|
"claim.reason": ("事由", "text", "claim"),
|
||
|
|
"item.item_reason": ("明细说明", "text", "item"),
|
||
|
|
"application.id": ("申请单", "text", "application"),
|
||
|
|
"application.status": ("申请状态", "enum", "application"),
|
||
|
|
"application.approved_amount": ("申请审批金额", "number", "application"),
|
||
|
|
"application.expense_type": ("申请费用类型", "enum", "application"),
|
||
|
|
"application.department_name": ("申请部门", "text", "application"),
|
||
|
|
"application.start_date": ("申请开始日期", "date", "application"),
|
||
|
|
"application.end_date": ("申请结束日期", "date", "application"),
|
||
|
|
"budget.line_id": ("预算行", "text", "budget"),
|
||
|
|
"budget.available_amount": ("预算可用金额", "number", "budget"),
|
||
|
|
"budget.used_rate": ("预算使用率", "number", "budget"),
|
||
|
|
"budget.status": ("预算状态", "enum", "budget"),
|
||
|
|
"budget.department_name": ("预算部门", "text", "budget"),
|
||
|
|
"budget.quarter": ("预算季度", "text", "budget"),
|
||
|
|
"budget.project_code": ("预算项目", "text", "budget"),
|
||
|
|
"material.attachment_count": ("附件数量", "number", "material"),
|
||
|
|
"material.contract_uploaded": ("合同已上传", "boolean", "material"),
|
||
|
|
"material.acceptance_uploaded": ("验收材料已上传", "boolean", "material"),
|
||
|
|
"material.plan_uploaded": ("方案已上传", "boolean", "material"),
|
||
|
|
"material.attendee_list_uploaded": ("参与人清单已上传", "boolean", "material"),
|
||
|
|
"material.invoice_uploaded": ("发票已上传", "boolean", "material"),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
BASE_FIELDS = (
|
||
|
|
"claim.amount",
|
||
|
|
"claim.expense_type",
|
||
|
|
"claim.department_name",
|
||
|
|
"claim.reason",
|
||
|
|
"item.item_reason",
|
||
|
|
)
|
||
|
|
BUDGET_FIELDS = BASE_FIELDS + (
|
||
|
|
"budget.line_id",
|
||
|
|
"budget.available_amount",
|
||
|
|
"budget.used_rate",
|
||
|
|
"budget.status",
|
||
|
|
"budget.department_name",
|
||
|
|
"budget.quarter",
|
||
|
|
"budget.project_code",
|
||
|
|
)
|
||
|
|
APPLICATION_FIELDS = BASE_FIELDS + (
|
||
|
|
"application.id",
|
||
|
|
"application.status",
|
||
|
|
"application.approved_amount",
|
||
|
|
"application.expense_type",
|
||
|
|
"application.department_name",
|
||
|
|
)
|
||
|
|
MATERIAL_FIELDS = BASE_FIELDS + (
|
||
|
|
"material.attachment_count",
|
||
|
|
"material.contract_uploaded",
|
||
|
|
"material.acceptance_uploaded",
|
||
|
|
"material.plan_uploaded",
|
||
|
|
"material.attendee_list_uploaded",
|
||
|
|
"material.invoice_uploaded",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True, slots=True)
|
||
|
|
class DemoRiskRule:
|
||
|
|
code: str
|
||
|
|
name: str
|
||
|
|
description: str
|
||
|
|
category: str
|
||
|
|
ontology_signal: str
|
||
|
|
finance_rule_code: str
|
||
|
|
finance_rule_sheet: str
|
||
|
|
business_stage: tuple[str, ...]
|
||
|
|
expense_types: tuple[str, ...]
|
||
|
|
condition_summary: str
|
||
|
|
keywords: tuple[str, ...]
|
||
|
|
severity: str
|
||
|
|
action: str
|
||
|
|
risk_score: int
|
||
|
|
risk_level: str
|
||
|
|
budget_required: bool = True
|
||
|
|
requires_attachment: bool = False
|
||
|
|
field_keys: tuple[str, ...] = field(default_factory=lambda: BASE_FIELDS)
|
||
|
|
|
||
|
|
|
||
|
|
def _budget_rule(
|
||
|
|
code: str,
|
||
|
|
name: str,
|
||
|
|
description: str,
|
||
|
|
condition_summary: str,
|
||
|
|
keywords: tuple[str, ...],
|
||
|
|
severity: str,
|
||
|
|
action: str,
|
||
|
|
risk_score: int,
|
||
|
|
) -> DemoRiskRule:
|
||
|
|
return DemoRiskRule(
|
||
|
|
code=code,
|
||
|
|
name=name,
|
||
|
|
description=description,
|
||
|
|
category="预算管控",
|
||
|
|
ontology_signal="budget_over_limit",
|
||
|
|
finance_rule_code="budget.execution.policy",
|
||
|
|
finance_rule_sheet="预算执行规则",
|
||
|
|
business_stage=("expense_application", "reimbursement", "budget_execution"),
|
||
|
|
expense_types=BUDGET_EXPENSE_TYPES,
|
||
|
|
condition_summary=condition_summary,
|
||
|
|
keywords=keywords,
|
||
|
|
severity=severity,
|
||
|
|
action=action,
|
||
|
|
risk_score=risk_score,
|
||
|
|
risk_level="high" if risk_score >= 80 else "medium",
|
||
|
|
field_keys=BUDGET_FIELDS,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _rule_payload(rule: DemoRiskRule) -> dict[str, object]:
|
||
|
|
now = datetime.now(UTC).isoformat()
|
||
|
|
fields = [
|
||
|
|
{
|
||
|
|
"key": key,
|
||
|
|
"label": FIELD_LABELS[key][0],
|
||
|
|
"type": FIELD_LABELS[key][1],
|
||
|
|
"source": FIELD_LABELS[key][2],
|
||
|
|
}
|
||
|
|
for key in rule.field_keys
|
||
|
|
]
|
||
|
|
return {
|
||
|
|
"schema_version": "2.0",
|
||
|
|
"rule_code": rule.code,
|
||
|
|
"name": rule.name,
|
||
|
|
"description": rule.description,
|
||
|
|
"enabled": True,
|
||
|
|
"requires_attachment": rule.requires_attachment,
|
||
|
|
"risk_dimension": "expense_control_demo",
|
||
|
|
"risk_category": rule.category,
|
||
|
|
"ontology_signal": rule.ontology_signal,
|
||
|
|
"evaluator": "template_rule",
|
||
|
|
"template_key": "keyword_match_v1",
|
||
|
|
"finance_rule_code": rule.finance_rule_code,
|
||
|
|
"finance_rule_sheet": rule.finance_rule_sheet,
|
||
|
|
"business_stage": list(rule.business_stage),
|
||
|
|
"expense_types": list(rule.expense_types),
|
||
|
|
"budget_required": rule.budget_required,
|
||
|
|
"applies_to": {
|
||
|
|
"domains": ["expense"],
|
||
|
|
"expense_types": list(rule.expense_types),
|
||
|
|
"business_stages": list(rule.business_stage),
|
||
|
|
},
|
||
|
|
"inputs": {"fields": fields},
|
||
|
|
"params": {
|
||
|
|
"template_key": "keyword_match_v1",
|
||
|
|
"field_keys": list(rule.field_keys),
|
||
|
|
"search_fields": [
|
||
|
|
"claim.reason",
|
||
|
|
"item.item_reason",
|
||
|
|
"claim.expense_type",
|
||
|
|
],
|
||
|
|
"keywords": list(rule.keywords),
|
||
|
|
"condition_summary": rule.condition_summary,
|
||
|
|
"finance_rule_code": rule.finance_rule_code,
|
||
|
|
"finance_rule_sheet": rule.finance_rule_sheet,
|
||
|
|
"business_stage": list(rule.business_stage),
|
||
|
|
"expense_types": list(rule.expense_types),
|
||
|
|
"budget_required": rule.budget_required,
|
||
|
|
},
|
||
|
|
"outcomes": {
|
||
|
|
"pass": {"severity": "none", "action": "continue"},
|
||
|
|
"fail": {
|
||
|
|
"severity": rule.severity,
|
||
|
|
"action": rule.action,
|
||
|
|
"risk_score": rule.risk_score,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"metadata": {
|
||
|
|
"owner": "风控与审计部",
|
||
|
|
"stability": "platform",
|
||
|
|
"source_ref": "费用管控 Demo 风险规则库",
|
||
|
|
"created_at": now,
|
||
|
|
"created_by": "system",
|
||
|
|
"risk_score": rule.risk_score,
|
||
|
|
"risk_level": rule.risk_level,
|
||
|
|
"rule_title": rule.name,
|
||
|
|
"finance_rule_code": rule.finance_rule_code,
|
||
|
|
"finance_rule_sheet": rule.finance_rule_sheet,
|
||
|
|
"business_stage": list(rule.business_stage),
|
||
|
|
"expense_types": list(rule.expense_types),
|
||
|
|
"budget_required": rule.budget_required,
|
||
|
|
},
|
||
|
|
"severity": rule.severity,
|
||
|
|
"risk_score": rule.risk_score,
|
||
|
|
"risk_level": rule.risk_level,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
RULES: tuple[DemoRiskRule, ...] = (
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.available_balance_insufficient",
|
||
|
|
"预算可用余额不足",
|
||
|
|
"提交后预算余额为负,或当前可用预算不足以覆盖本次申请/报销金额。",
|
||
|
|
"预算可用金额小于本次金额时触发。",
|
||
|
|
("预算不足", "可用余额不足", "超预算"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
88,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.usage_warning_80",
|
||
|
|
"预算使用率达到 80% 预警",
|
||
|
|
"报销或申请通过后,部门/项目/费用类型预算使用率达到 80% 以上。",
|
||
|
|
"预算使用率大于等于 80% 且低于 100% 时触发。",
|
||
|
|
("预算预警", "80%", "使用率过高"),
|
||
|
|
"medium",
|
||
|
|
"warning",
|
||
|
|
70,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.usage_over_100",
|
||
|
|
"预算使用率超过 100% 管控",
|
||
|
|
"报销或申请通过后,预算使用率超过 100%,需要阻断或升级审批。",
|
||
|
|
"预算使用率超过 100% 时触发。",
|
||
|
|
("预算超支", "超过100%", "禁止提交"),
|
||
|
|
"critical",
|
||
|
|
"block_submit",
|
||
|
|
96,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.frozen_or_closed_used",
|
||
|
|
"使用冻结或关闭预算",
|
||
|
|
"单据引用了已冻结、已关闭或已作废的预算行。",
|
||
|
|
"预算状态不是启用时触发。",
|
||
|
|
("冻结预算", "关闭预算", "预算作废"),
|
||
|
|
"high",
|
||
|
|
"block_submit",
|
||
|
|
90,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.missing_budget_line",
|
||
|
|
"缺少预算口径",
|
||
|
|
"需要预算管控的费用未关联年度、季度、部门、项目或费用类型预算。",
|
||
|
|
"费用类型要求预算管控但预算行为空时触发。",
|
||
|
|
("无预算", "预算口径缺失", "未关联预算"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
82,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.cross_department_without_authorization",
|
||
|
|
"跨部门预算未授权",
|
||
|
|
"报销部门与预算归属部门不一致,且没有跨部门预算授权。",
|
||
|
|
"单据部门与预算部门不一致且无授权说明时触发。",
|
||
|
|
("跨部门预算", "部门不一致", "未授权"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
86,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.cross_quarter_without_explanation",
|
||
|
|
"跨季度预算未说明",
|
||
|
|
"单据发生期间与预算季度不一致,且缺少跨季度使用说明。",
|
||
|
|
"发生季度与预算季度不一致且未说明时触发。",
|
||
|
|
("跨季度预算", "季度不一致", "未说明"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
76,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.project_department_mismatch",
|
||
|
|
"项目预算与部门不匹配",
|
||
|
|
"单据引用的项目预算不属于当前部门或当前成本中心。",
|
||
|
|
"项目预算归属与报销部门不一致时触发。",
|
||
|
|
("项目预算", "成本中心不匹配", "部门不匹配"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
84,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.duplicate_reserve",
|
||
|
|
"重复占用预算",
|
||
|
|
"同一申请、项目或合同已占用预算,本次单据再次占用同一预算口径。",
|
||
|
|
"相同业务标识存在未释放预算占用时触发。",
|
||
|
|
("重复占用", "预算锁定", "重复申请"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
74,
|
||
|
|
),
|
||
|
|
_budget_rule(
|
||
|
|
"risk.budget.consume_without_release",
|
||
|
|
"预算占用未释放",
|
||
|
|
"申请取消、退回或驳回后,预算占用未释放导致后续可用预算失真。",
|
||
|
|
"申请非有效状态但仍存在预算占用时触发。",
|
||
|
|
("占用未释放", "退回未释放", "预算释放"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
72,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.large_expense_without_preapproval",
|
||
|
|
"大额费用未事前申请",
|
||
|
|
"达到财务制度中大额标准的费用,未找到有效事前申请即进入报销。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"finance.preapproval.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"金额达到大额阈值且缺少已通过申请单时触发。",
|
||
|
|
("大额费用", "未申请", "先申请后报销"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
86,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.marketing_without_campaign",
|
||
|
|
"市场推广费无活动申请",
|
||
|
|
"市场活动、投放、展会等推广费用,缺少已审批的活动申请或投放方案。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("marketing",),
|
||
|
|
"市场推广费报销缺少活动申请或方案时触发。",
|
||
|
|
("市场推广", "活动申请", "投放方案"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
84,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.training_without_plan",
|
||
|
|
"培训费无培训计划",
|
||
|
|
"培训费报销缺少年度培训计划、专项培训申请或审批记录。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("training",),
|
||
|
|
"培训费用没有匹配到培训计划或事前申请时触发。",
|
||
|
|
("培训计划", "培训申请", "未申请"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
83,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.meeting_without_application",
|
||
|
|
"会务费无会议申请",
|
||
|
|
"会务场地、物料、会务服务等费用缺少会议申请或会议预算审批。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("meeting",),
|
||
|
|
"会务费报销缺少会议申请、会议预算或会议通知时触发。",
|
||
|
|
("会议申请", "会议预算", "会务费"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
82,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.software_without_purchase",
|
||
|
|
"软件服务费无采购申请",
|
||
|
|
"软件订阅、SaaS 服务或系统实施费用缺少采购申请或审批链。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("software",),
|
||
|
|
"软件服务费缺少采购申请或合同审批时触发。",
|
||
|
|
("软件采购", "SaaS", "采购申请"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
85,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("material.contract_uploaded",),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.office_bulk_without_purchase",
|
||
|
|
"办公用品大额采购未申请",
|
||
|
|
"批量办公用品或设备采购达到阈值但未走采购申请。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("office",),
|
||
|
|
"办公用品单次金额达到采购阈值且缺少采购申请时触发。",
|
||
|
|
("办公采购", "大额办公用品", "采购申请"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
78,
|
||
|
|
"medium",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.meal_high_value_without_preapproval",
|
||
|
|
"大额业务招待未申请",
|
||
|
|
"业务招待金额或人均金额超过制度阈值但未事前审批。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"expense.application.policy",
|
||
|
|
"费用申请前置规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("meal",),
|
||
|
|
"业务招待金额超过申请阈值且没有通过申请时触发。",
|
||
|
|
("业务招待", "人均超标", "未申请"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
84,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("material.attendee_list_uploaded",),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.application.travel_large_without_preapproval",
|
||
|
|
"大额差旅未申请",
|
||
|
|
"多人出差、长周期出差或高金额差旅报销缺少出差申请。",
|
||
|
|
"申请前置",
|
||
|
|
"application_required",
|
||
|
|
"rule.expense.company_travel_expense_reimbursement",
|
||
|
|
"差旅住宿费标准",
|
||
|
|
("reimbursement",),
|
||
|
|
("travel", "hotel", "transport"),
|
||
|
|
"差旅金额达到大额阈值且缺少有效出差申请时触发。",
|
||
|
|
("差旅申请", "大额差旅", "未申请"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
82,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.amount_over_application",
|
||
|
|
"报销金额超过申请金额",
|
||
|
|
"报销总金额超过已审批申请金额,需要按偏差规则复核。",
|
||
|
|
"报销偏差",
|
||
|
|
"amount_over_application",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"报销金额大于申请审批金额时触发。",
|
||
|
|
("超过申请金额", "报销偏差", "申请金额"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
76,
|
||
|
|
"medium",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.amount_over_application_10pct",
|
||
|
|
"报销金额超过申请金额 10%",
|
||
|
|
"报销金额比申请审批金额高出 10% 以上,需要升级审批或禁止提交。",
|
||
|
|
"报销偏差",
|
||
|
|
"amount_over_application",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"报销金额超过申请审批金额 10% 以上时触发。",
|
||
|
|
("超过申请10%", "金额偏差", "升级审批"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
88,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.expense_type_mismatch_application",
|
||
|
|
"报销费用类型与申请不一致",
|
||
|
|
"报销单费用类型与关联申请单费用类型不一致。",
|
||
|
|
"报销偏差",
|
||
|
|
"expense_type_mismatch",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"报销费用类型与申请费用类型不一致时触发。",
|
||
|
|
("费用类型不一致", "申请报销不匹配", "类型偏差"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
74,
|
||
|
|
"medium",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.department_mismatch_application",
|
||
|
|
"报销部门与申请部门不一致",
|
||
|
|
"报销部门、成本中心与关联申请单不一致,且缺少调整说明。",
|
||
|
|
"报销偏差",
|
||
|
|
"department_mismatch",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"报销部门与申请部门不一致时触发。",
|
||
|
|
("部门不一致", "成本中心偏差", "申请部门"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
72,
|
||
|
|
"medium",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.period_outside_application",
|
||
|
|
"报销发生期间超出申请期间",
|
||
|
|
"费用发生日期不在已审批申请的起止日期范围内。",
|
||
|
|
"报销偏差",
|
||
|
|
"period_outside_application",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"发生日期超出申请有效期间时触发。",
|
||
|
|
("期间不一致", "超出申请期间", "日期偏差"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
70,
|
||
|
|
"medium",
|
||
|
|
field_keys=APPLICATION_FIELDS + ("application.start_date", "application.end_date"),
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.rejected_application_claimed",
|
||
|
|
"已驳回申请被用于报销",
|
||
|
|
"报销单关联的申请单为驳回、撤回或已取消状态。",
|
||
|
|
"报销偏差",
|
||
|
|
"invalid_application_status",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"关联申请状态不是已通过或已完成时触发。",
|
||
|
|
("申请驳回", "申请撤回", "无效申请"),
|
||
|
|
"high",
|
||
|
|
"block_submit",
|
||
|
|
92,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.reimbursement.duplicate_against_application",
|
||
|
|
"同一申请重复报销",
|
||
|
|
"同一申请单或同一合同/项目存在多笔疑似重复报销。",
|
||
|
|
"报销偏差",
|
||
|
|
"duplicate_reimbursement",
|
||
|
|
"application.reimbursement.linkage.policy",
|
||
|
|
"申请报销关联规则",
|
||
|
|
("reimbursement",),
|
||
|
|
BUDGET_EXPENSE_TYPES,
|
||
|
|
"同一申请的已报销金额与本次金额超过申请金额时触发。",
|
||
|
|
("重复报销", "同一申请", "重复占用"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
86,
|
||
|
|
"high",
|
||
|
|
field_keys=APPLICATION_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.standard.meal_participants_missing",
|
||
|
|
"业务招待缺少参与人清单",
|
||
|
|
"业务招待费要求提供客户名称、参与人清单和招待说明。",
|
||
|
|
"材料完整性",
|
||
|
|
"material_missing",
|
||
|
|
"expense.material.policy",
|
||
|
|
"材料完整性规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("meal",),
|
||
|
|
"业务招待费缺少参与人清单或客户信息时触发。",
|
||
|
|
("参与人清单", "客户信息", "业务招待"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
72,
|
||
|
|
"medium",
|
||
|
|
requires_attachment=True,
|
||
|
|
field_keys=MATERIAL_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.standard.software_contract_missing",
|
||
|
|
"软件服务费缺少合同",
|
||
|
|
"软件服务费、SaaS 订阅或系统实施费用缺少合同、订单或验收材料。",
|
||
|
|
"材料完整性",
|
||
|
|
"contract_missing",
|
||
|
|
"expense.material.policy",
|
||
|
|
"材料完整性规则",
|
||
|
|
("expense_application", "reimbursement"),
|
||
|
|
("software",),
|
||
|
|
"软件服务费要求合同或订单但未提供时触发。",
|
||
|
|
("合同缺失", "软件服务", "验收材料"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
84,
|
||
|
|
"high",
|
||
|
|
requires_attachment=True,
|
||
|
|
field_keys=MATERIAL_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.standard.marketing_acceptance_missing",
|
||
|
|
"市场推广费缺少验收材料",
|
||
|
|
"市场推广活动缺少投放截图、活动复盘、验收单或结案材料。",
|
||
|
|
"材料完整性",
|
||
|
|
"acceptance_missing",
|
||
|
|
"expense.material.policy",
|
||
|
|
"材料完整性规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("marketing",),
|
||
|
|
"市场推广费要求验收或结案材料但未提供时触发。",
|
||
|
|
("验收材料", "活动结案", "投放截图"),
|
||
|
|
"high",
|
||
|
|
"manual_review",
|
||
|
|
82,
|
||
|
|
"high",
|
||
|
|
requires_attachment=True,
|
||
|
|
field_keys=MATERIAL_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.standard.office_fixed_asset_as_office",
|
||
|
|
"固定资产伪装为办公用品费",
|
||
|
|
"办公用品费明细疑似包含固定资产、电子设备或应走采购入库的物品。",
|
||
|
|
"费用标准",
|
||
|
|
"expense_type_mismatch",
|
||
|
|
"expense.classification.policy",
|
||
|
|
"费用类型归类规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("office",),
|
||
|
|
"办公用品费包含固定资产关键词或超过采购阈值时触发。",
|
||
|
|
("固定资产", "电脑", "显示器", "办公设备"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
78,
|
||
|
|
"medium",
|
||
|
|
field_keys=BASE_FIELDS,
|
||
|
|
),
|
||
|
|
DemoRiskRule(
|
||
|
|
"risk.standard.meeting_attendee_list_missing",
|
||
|
|
"会务费缺少参会名单",
|
||
|
|
"会务费报销缺少参会名单、会议通知、会议照片或会议纪要。",
|
||
|
|
"材料完整性",
|
||
|
|
"material_missing",
|
||
|
|
"expense.material.policy",
|
||
|
|
"材料完整性规则",
|
||
|
|
("reimbursement",),
|
||
|
|
("meeting",),
|
||
|
|
"会务费要求参会名单或会议材料但未提供时触发。",
|
||
|
|
("参会名单", "会议通知", "会议纪要"),
|
||
|
|
"medium",
|
||
|
|
"manual_review",
|
||
|
|
74,
|
||
|
|
"medium",
|
||
|
|
requires_attachment=True,
|
||
|
|
field_keys=MATERIAL_FIELDS,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
RISK_RULE_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
for rule in RULES:
|
||
|
|
path = RISK_RULE_DIR / f"{rule.code}.json"
|
||
|
|
payload = _rule_payload(rule)
|
||
|
|
path.write_text(
|
||
|
|
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
print(f"Generated {len(RULES)} expense-control demo risk rule manifest(s).")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|