Files
X-Financial/server/src/app/services/expense_claim_constants.py

377 lines
10 KiB
Python
Raw Normal View History

from __future__ import annotations
import re
from decimal import Decimal
from app.services.expense_type_keywords import iter_expense_keywords
EXPENSE_TYPE_LABELS = {
"travel": "差旅",
"train_ticket": "火车票",
"flight_ticket": "机票",
"hotel_ticket": "住宿票",
"ride_ticket": "乘车",
"travel_allowance": "出差补贴",
"hotel": "住宿",
"transport": "交通",
"meal": "业务招待",
"meeting": "会务",
"entertainment": "招待",
"marketing": "市场推广",
"office": "办公用品",
"training": "培训",
"software": "软件服务",
"communication": "通讯",
"welfare": "福利",
}
MAX_DRAFT_CLAIMS_PER_USER = 3
EDITABLE_CLAIM_STATUSES = ("draft", "supplement", "returned")
SYSTEM_GENERATED_ITEM_TYPES = {"travel_allowance"}
TRAVEL_DETAIL_ITEM_TYPES = {
"train_ticket",
"flight_ticket",
"hotel_ticket",
"ride_ticket",
"travel_allowance",
}
TRAVEL_ALLOWANCE_TRIGGER_ITEM_TYPES = {"train_ticket", "flight_ticket"}
DOCUMENT_TYPE_ITEM_TYPE_MAP = {
"train_ticket": "train_ticket",
"flight_itinerary": "flight_ticket",
"hotel_invoice": "hotel_ticket",
"taxi_receipt": "ride_ticket",
"transport_receipt": "ride_ticket",
}
DOCUMENT_TYPE_SCENE_MAP = {
"train_ticket": "travel",
"flight_itinerary": "travel",
"hotel_invoice": "hotel",
"taxi_receipt": "transport",
"transport_receipt": "transport",
"parking_toll_receipt": "transport",
"meal_receipt": "meal",
"office_invoice": "office",
"meeting_invoice": "meeting",
"training_invoice": "training",
}
DOCUMENT_FACT_ITEM_TYPES = {
"train_ticket",
"flight_ticket",
"hotel_ticket",
"ride_ticket",
"ship_ticket",
"ferry_ticket",
}
ROUTE_DESCRIPTION_ITEM_TYPES = {
"train_ticket",
"flight_ticket",
"ship_ticket",
"ferry_ticket",
"ride_ticket",
}
DOCUMENT_TRIP_DATE_LABELS = {
"train_ticket": "列车出发时间",
"flight_itinerary": "起飞日期",
"taxi_receipt": "乘车时间",
"transport_receipt": "乘车时间",
"parking_toll_receipt": "通行日期",
}
DOCUMENT_TRIP_DATE_REQUIREMENT_LABELS = {
"train_ticket": "列车出发时间或乘车日期",
"flight_itinerary": "起飞日期或航班日期",
"taxi_receipt": "乘车时间",
"transport_receipt": "乘车时间",
"parking_toll_receipt": "通行日期",
"hotel_invoice": "入住或离店日期",
}
DOCUMENT_TRIP_DATE_KEYS = {
"traveldate",
"tripdate",
"journeydate",
"departuredate",
"departuretime",
"departdate",
"departtime",
"boardingdate",
"boardingtime",
"traindate",
"traintime",
"traindeparturetime",
"scheduleddeparturetime",
"flightdate",
"flighttime",
"ridedate",
"ridetime",
"pickuptime",
"starttime",
}
DOCUMENT_GENERIC_DATE_KEYS = {"date", "time", "occurredat", "occurreddate", "businessdate"}
DOCUMENT_INVOICE_DATE_KEYS = {"issuedat", "issuedate", "invoicedate", "billingdate"}
DOCUMENT_TRIP_DATE_LABEL_TOKENS = (
"出发日期",
"出发时间",
"列车出发时间",
"发车日期",
"发车时间",
"开车时间",
"乘车日期",
"乘车时间",
"起飞日期",
"航班日期",
"行程日期",
"上车时间",
"用车时间",
"通行日期",
)
DOCUMENT_GENERIC_DATE_LABEL_TOKENS = ("日期", "时间", "发生时间", "业务发生日期")
DOCUMENT_INVOICE_DATE_LABEL_TOKENS = ("开票日期", "发票日期")
DOCUMENT_ROUTE_FORMAT_PATTERN = re.compile(
r"^[A-Za-z0-9\u4e00-\u9fa5()·]{2,40}\s*-\s*"
r"[A-Za-z0-9\u4e00-\u9fa5()·]{2,40}$"
)
DOCUMENT_ROUTE_TEXT_PATTERN = re.compile(
r"([A-Za-z0-9\u4e00-\u9fa5()·]{2,40})\s*(?:至|到|→|->|—||-)\s*"
r"([A-Za-z0-9\u4e00-\u9fa5()·]{2,40})"
)
DOCUMENT_ROUTE_ORIGIN_LABELS = {
"起点",
"上车",
"上车地点",
"上车地址",
"出发",
"出发地",
"出发站",
"始发站",
"乘车起点",
}
DOCUMENT_ROUTE_DESTINATION_LABELS = {
"终点",
"下车",
"下车地点",
"下车地址",
"到达",
"到达地",
"到达站",
"目的地",
"乘车终点",
}
GENERIC_ATTACHMENT_BACKFILL_ITEM_TYPES = {"", "other", "travel", "transport", "hotel"}
LOCATION_REQUIRED_EXPENSE_TYPES = {"travel", "meeting", "entertainment"}
EXPENSE_SCENE_KEYWORDS = {
code: tuple(iter_expense_keywords(code))
for code in (
"travel",
"hotel",
"transport",
"meal",
"entertainment",
"marketing",
"office",
"meeting",
"training",
"software",
"communication",
"welfare",
)
}
EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES = {
"travel": {"travel", "hotel", "transport", "meal"},
"train_ticket": {"travel"},
"flight_ticket": {"travel"},
"hotel_ticket": {"hotel"},
"ride_ticket": {"transport"},
"travel_allowance": set(),
"hotel": {"hotel"},
"transport": {"transport", "travel"},
"meal": {"meal", "entertainment"},
"entertainment": {"entertainment", "meal"},
"marketing": {"marketing"},
"office": {"office"},
"meeting": {"meeting"},
"training": {"training"},
"software": {"software"},
}
DOCUMENT_SCENE_LABELS = {
"travel": "差旅",
"hotel": "住宿",
"transport": "交通",
"meal": "业务招待",
"entertainment": "业务招待",
"marketing": "市场推广",
"office": "办公用品",
"meeting": "会务",
"training": "培训",
"software": "软件服务",
"other": "其他票据",
}
DOCUMENT_ASSOCIATION_REVIEW_ACTIONS = {
"link_to_existing_draft",
"create_new_claim_from_documents",
}
PERSISTENT_EXPENSE_REVIEW_ACTIONS = {
"save_draft",
"next_step",
*DOCUMENT_ASSOCIATION_REVIEW_ACTIONS,
}
RETURN_REASON_OPTIONS = {
"missing_attachment": "附件缺失或不清晰",
"invoice_mismatch": "票据类型/金额与明细不一致",
"over_policy": "超出制度标准或缺少超标说明",
"business_explanation": "业务事由/地点/人员信息不完整",
"duplicate_or_abnormal": "疑似重复或异常票据",
"approval_question": "审批人需要补充说明",
}
MAX_CLAIM_NO_RETRY_ATTEMPTS = 3
DOCUMENT_DATE_PATTERN = re.compile(
r"((?:20\d{2}|19\d{2})[-/年.](?:1[0-2]|0?[1-9])[-/月.]"
r"(?:3[01]|[12]\d|0?[1-9])日?)"
)
SYSTEM_GENERATED_REASON_PREFIXES = (
"我上传了",
"请按当前已识别信息",
"请把当前上传的票据",
"请基于当前上传的多张票据",
"我已核对右侧识别结果",
"请同步修正逐票据识别结果",
"我已修改识别信息",
"查看报销草稿",
"请解释一下当前这笔报销的合规风险和待补充项",
)
LEADING_REASON_TIME_PATTERNS = (
re.compile(
r"^\s*(?:识别事项(?:有)?[:]\s*)?"
r"(?:业务发生(?:时间|日期)|费用发生(?:时间|日期)|发生(?:时间|日期)|报销(?:时间|日期)|时间)[:]?\s*"
r"(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?"
r"(?:\s*(?:至|到|~||—|-)\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?)?"
r"\s*[,。;;、]?\s*"
),
re.compile(
r"^\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?"
r"(?:\s*(?:至|到|~||—|-)\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?)?"
r"\s*[,。;;、]\s*"
),
)
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*(?:晚|间夜)")