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*(?:晚|间夜)")