594 lines
16 KiB
Python
594 lines
16 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
from decimal import Decimal
|
|||
|
|
|
|||
|
|
from sqlalchemy import func, or_, select
|
|||
|
|
from sqlalchemy.orm import Session
|
|||
|
|
|
|||
|
|
from app.api.deps import CurrentUserContext
|
|||
|
|
from app.core.agent_enums import AgentAssetType
|
|||
|
|
from app.models.employee import Employee
|
|||
|
|
from app.schemas.reimbursement import (
|
|||
|
|
TravelReimbursementCalculatorRequest,
|
|||
|
|
TravelReimbursementCalculatorResponse,
|
|||
|
|
)
|
|||
|
|
from app.services.agent_assets import AgentAssetService
|
|||
|
|
from app.services.expense_claims import ExpenseClaimService
|
|||
|
|
from app.services.expense_rule_runtime import RuntimeTravelPolicy, ExpenseRuleRuntimeService
|
|||
|
|
|
|||
|
|
OTHER_REGION_LOCATION_KEYWORDS = {
|
|||
|
|
"河北",
|
|||
|
|
"石家庄",
|
|||
|
|
"唐山",
|
|||
|
|
"秦皇岛",
|
|||
|
|
"邯郸",
|
|||
|
|
"邢台",
|
|||
|
|
"保定",
|
|||
|
|
"张家口",
|
|||
|
|
"承德",
|
|||
|
|
"沧州",
|
|||
|
|
"廊坊",
|
|||
|
|
"衡水",
|
|||
|
|
"山西",
|
|||
|
|
"太原",
|
|||
|
|
"大同",
|
|||
|
|
"长治",
|
|||
|
|
"晋城",
|
|||
|
|
"晋中",
|
|||
|
|
"运城",
|
|||
|
|
"临汾",
|
|||
|
|
"吕梁",
|
|||
|
|
"内蒙古",
|
|||
|
|
"呼和浩特",
|
|||
|
|
"包头",
|
|||
|
|
"赤峰",
|
|||
|
|
"通辽",
|
|||
|
|
"鄂尔多斯",
|
|||
|
|
"辽宁",
|
|||
|
|
"鞍山",
|
|||
|
|
"抚顺",
|
|||
|
|
"本溪",
|
|||
|
|
"丹东",
|
|||
|
|
"锦州",
|
|||
|
|
"营口",
|
|||
|
|
"盘锦",
|
|||
|
|
"吉林",
|
|||
|
|
"长春",
|
|||
|
|
"吉林市",
|
|||
|
|
"四平",
|
|||
|
|
"通化",
|
|||
|
|
"白山",
|
|||
|
|
"松原",
|
|||
|
|
"延边",
|
|||
|
|
"黑龙江",
|
|||
|
|
"哈尔滨",
|
|||
|
|
"齐齐哈尔",
|
|||
|
|
"牡丹江",
|
|||
|
|
"佳木斯",
|
|||
|
|
"大庆",
|
|||
|
|
"江苏",
|
|||
|
|
"常州",
|
|||
|
|
"南通",
|
|||
|
|
"连云港",
|
|||
|
|
"淮安",
|
|||
|
|
"盐城",
|
|||
|
|
"扬州",
|
|||
|
|
"镇江",
|
|||
|
|
"泰州",
|
|||
|
|
"宿迁",
|
|||
|
|
"浙江",
|
|||
|
|
"温州",
|
|||
|
|
"嘉兴",
|
|||
|
|
"湖州",
|
|||
|
|
"绍兴",
|
|||
|
|
"金华",
|
|||
|
|
"衢州",
|
|||
|
|
"舟山",
|
|||
|
|
"台州",
|
|||
|
|
"丽水",
|
|||
|
|
"安徽",
|
|||
|
|
"芜湖",
|
|||
|
|
"蚌埠",
|
|||
|
|
"淮南",
|
|||
|
|
"马鞍山",
|
|||
|
|
"淮北",
|
|||
|
|
"铜陵",
|
|||
|
|
"安庆",
|
|||
|
|
"黄山",
|
|||
|
|
"滁州",
|
|||
|
|
"阜阳",
|
|||
|
|
"宿州",
|
|||
|
|
"六安",
|
|||
|
|
"亳州",
|
|||
|
|
"池州",
|
|||
|
|
"宣城",
|
|||
|
|
"福建",
|
|||
|
|
"泉州",
|
|||
|
|
"漳州",
|
|||
|
|
"莆田",
|
|||
|
|
"三明",
|
|||
|
|
"南平",
|
|||
|
|
"龙岩",
|
|||
|
|
"宁德",
|
|||
|
|
"江西",
|
|||
|
|
"南昌",
|
|||
|
|
"景德镇",
|
|||
|
|
"萍乡",
|
|||
|
|
"九江",
|
|||
|
|
"新余",
|
|||
|
|
"鹰潭",
|
|||
|
|
"赣州",
|
|||
|
|
"吉安",
|
|||
|
|
"宜春",
|
|||
|
|
"抚州",
|
|||
|
|
"上饶",
|
|||
|
|
"山东",
|
|||
|
|
"淄博",
|
|||
|
|
"枣庄",
|
|||
|
|
"东营",
|
|||
|
|
"烟台",
|
|||
|
|
"潍坊",
|
|||
|
|
"济宁",
|
|||
|
|
"泰安",
|
|||
|
|
"威海",
|
|||
|
|
"日照",
|
|||
|
|
"临沂",
|
|||
|
|
"德州",
|
|||
|
|
"聊城",
|
|||
|
|
"滨州",
|
|||
|
|
"菏泽",
|
|||
|
|
"河南",
|
|||
|
|
"洛阳",
|
|||
|
|
"开封",
|
|||
|
|
"平顶山",
|
|||
|
|
"安阳",
|
|||
|
|
"鹤壁",
|
|||
|
|
"新乡",
|
|||
|
|
"焦作",
|
|||
|
|
"濮阳",
|
|||
|
|
"许昌",
|
|||
|
|
"漯河",
|
|||
|
|
"三门峡",
|
|||
|
|
"南阳",
|
|||
|
|
"商丘",
|
|||
|
|
"信阳",
|
|||
|
|
"周口",
|
|||
|
|
"驻马店",
|
|||
|
|
"湖北",
|
|||
|
|
"黄石",
|
|||
|
|
"十堰",
|
|||
|
|
"宜昌",
|
|||
|
|
"襄阳",
|
|||
|
|
"鄂州",
|
|||
|
|
"荆门",
|
|||
|
|
"孝感",
|
|||
|
|
"荆州",
|
|||
|
|
"黄冈",
|
|||
|
|
"咸宁",
|
|||
|
|
"随州",
|
|||
|
|
"恩施",
|
|||
|
|
"湖南",
|
|||
|
|
"株洲",
|
|||
|
|
"湘潭",
|
|||
|
|
"衡阳",
|
|||
|
|
"邵阳",
|
|||
|
|
"岳阳",
|
|||
|
|
"常德",
|
|||
|
|
"张家界",
|
|||
|
|
"益阳",
|
|||
|
|
"郴州",
|
|||
|
|
"永州",
|
|||
|
|
"怀化",
|
|||
|
|
"娄底",
|
|||
|
|
"湘西",
|
|||
|
|
"广东",
|
|||
|
|
"惠州",
|
|||
|
|
"江门",
|
|||
|
|
"湛江",
|
|||
|
|
"茂名",
|
|||
|
|
"肇庆",
|
|||
|
|
"梅州",
|
|||
|
|
"汕尾",
|
|||
|
|
"河源",
|
|||
|
|
"阳江",
|
|||
|
|
"清远",
|
|||
|
|
"潮州",
|
|||
|
|
"揭阳",
|
|||
|
|
"云浮",
|
|||
|
|
"广西",
|
|||
|
|
"南宁",
|
|||
|
|
"柳州",
|
|||
|
|
"桂林",
|
|||
|
|
"梧州",
|
|||
|
|
"北海",
|
|||
|
|
"防城港",
|
|||
|
|
"钦州",
|
|||
|
|
"贵港",
|
|||
|
|
"玉林",
|
|||
|
|
"百色",
|
|||
|
|
"贺州",
|
|||
|
|
"河池",
|
|||
|
|
"来宾",
|
|||
|
|
"崇左",
|
|||
|
|
"海南",
|
|||
|
|
"儋州",
|
|||
|
|
"四川",
|
|||
|
|
"自贡",
|
|||
|
|
"攀枝花",
|
|||
|
|
"泸州",
|
|||
|
|
"德阳",
|
|||
|
|
"绵阳",
|
|||
|
|
"广元",
|
|||
|
|
"遂宁",
|
|||
|
|
"内江",
|
|||
|
|
"乐山",
|
|||
|
|
"南充",
|
|||
|
|
"眉山",
|
|||
|
|
"宜宾",
|
|||
|
|
"广安",
|
|||
|
|
"达州",
|
|||
|
|
"雅安",
|
|||
|
|
"巴中",
|
|||
|
|
"资阳",
|
|||
|
|
"阿坝",
|
|||
|
|
"甘孜",
|
|||
|
|
"凉山",
|
|||
|
|
"贵州",
|
|||
|
|
"贵阳",
|
|||
|
|
"遵义",
|
|||
|
|
"六盘水",
|
|||
|
|
"安顺",
|
|||
|
|
"毕节",
|
|||
|
|
"铜仁",
|
|||
|
|
"黔东南",
|
|||
|
|
"黔南",
|
|||
|
|
"黔西南",
|
|||
|
|
"云南",
|
|||
|
|
"曲靖",
|
|||
|
|
"玉溪",
|
|||
|
|
"保山",
|
|||
|
|
"昭通",
|
|||
|
|
"丽江",
|
|||
|
|
"普洱",
|
|||
|
|
"临沧",
|
|||
|
|
"楚雄",
|
|||
|
|
"红河",
|
|||
|
|
"文山",
|
|||
|
|
"西双版纳",
|
|||
|
|
"大理",
|
|||
|
|
"德宏",
|
|||
|
|
"怒江",
|
|||
|
|
"迪庆",
|
|||
|
|
"陕西",
|
|||
|
|
"宝鸡",
|
|||
|
|
"咸阳",
|
|||
|
|
"铜川",
|
|||
|
|
"渭南",
|
|||
|
|
"延安",
|
|||
|
|
"汉中",
|
|||
|
|
"榆林",
|
|||
|
|
"安康",
|
|||
|
|
"商洛",
|
|||
|
|
"甘肃",
|
|||
|
|
"兰州",
|
|||
|
|
"嘉峪关",
|
|||
|
|
"金昌",
|
|||
|
|
"白银",
|
|||
|
|
"天水",
|
|||
|
|
"武威",
|
|||
|
|
"张掖",
|
|||
|
|
"平凉",
|
|||
|
|
"酒泉",
|
|||
|
|
"庆阳",
|
|||
|
|
"定西",
|
|||
|
|
"陇南",
|
|||
|
|
"临夏",
|
|||
|
|
"甘南",
|
|||
|
|
"青海",
|
|||
|
|
"西宁",
|
|||
|
|
"海东",
|
|||
|
|
"海北",
|
|||
|
|
"黄南",
|
|||
|
|
"海南州",
|
|||
|
|
"果洛",
|
|||
|
|
"玉树",
|
|||
|
|
"海西",
|
|||
|
|
"宁夏",
|
|||
|
|
"银川",
|
|||
|
|
"石嘴山",
|
|||
|
|
"吴忠",
|
|||
|
|
"固原",
|
|||
|
|
"中卫",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
OTHER_REGION_PROVINCE_KEYWORDS = {
|
|||
|
|
"河北",
|
|||
|
|
"山西",
|
|||
|
|
"内蒙古",
|
|||
|
|
"辽宁",
|
|||
|
|
"吉林",
|
|||
|
|
"黑龙江",
|
|||
|
|
"江苏",
|
|||
|
|
"浙江",
|
|||
|
|
"安徽",
|
|||
|
|
"福建",
|
|||
|
|
"江西",
|
|||
|
|
"山东",
|
|||
|
|
"河南",
|
|||
|
|
"湖北",
|
|||
|
|
"湖南",
|
|||
|
|
"广东",
|
|||
|
|
"广西",
|
|||
|
|
"海南",
|
|||
|
|
"四川",
|
|||
|
|
"贵州",
|
|||
|
|
"云南",
|
|||
|
|
"陕西",
|
|||
|
|
"甘肃",
|
|||
|
|
"青海",
|
|||
|
|
"宁夏",
|
|||
|
|
"新疆",
|
|||
|
|
"西藏",
|
|||
|
|
"台湾",
|
|||
|
|
"香港",
|
|||
|
|
"澳门",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AMBIGUOUS_PROVINCE_CITY_NAMES = {"吉林"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TravelReimbursementCalculatorService:
|
|||
|
|
def __init__(self, db: Session) -> None:
|
|||
|
|
self.db = db
|
|||
|
|
|
|||
|
|
def calculate(
|
|||
|
|
self,
|
|||
|
|
payload: TravelReimbursementCalculatorRequest,
|
|||
|
|
current_user: CurrentUserContext,
|
|||
|
|
) -> TravelReimbursementCalculatorResponse:
|
|||
|
|
days = max(1, int(payload.days))
|
|||
|
|
location = str(payload.location or "").strip()
|
|||
|
|
if not location:
|
|||
|
|
raise ValueError("请先填写出差地点。")
|
|||
|
|
|
|||
|
|
policy = self._load_travel_policy()
|
|||
|
|
grade = self._resolve_grade(payload.grade, current_user)
|
|||
|
|
if not grade:
|
|||
|
|
raise ValueError("未识别到当前员工职级,请在个人信息中维护职级后再计算。")
|
|||
|
|
|
|||
|
|
grade_band = ExpenseClaimService._resolve_travel_policy_band(grade)
|
|||
|
|
if not grade_band:
|
|||
|
|
raise ValueError(f"当前职级 {grade} 暂未匹配到差旅报销档位。")
|
|||
|
|
|
|||
|
|
matched_city = self._resolve_city(location, policy)
|
|||
|
|
matched_other_region = "" if matched_city else self._resolve_other_region(location)
|
|||
|
|
if not matched_city and not matched_other_region:
|
|||
|
|
raise ValueError(f"出差地点“{location}”未识别为有效出差地区,请按真实省市或规则表地点重新填写。")
|
|||
|
|
city_tier = policy.city_tiers.get(matched_city, "tier_3") if matched_city else "tier_3"
|
|||
|
|
hotel_rate = self._resolve_hotel_rate(policy, grade_band, matched_city, city_tier)
|
|||
|
|
allowance_region = self._resolve_allowance_region(location, matched_city or matched_other_region)
|
|||
|
|
meal_rate = self._resolve_allowance_rate(policy, "meal", allowance_region)
|
|||
|
|
basic_rate = self._resolve_allowance_rate(policy, "basic", allowance_region)
|
|||
|
|
total_allowance_rate = self._resolve_total_allowance_rate(policy, allowance_region, meal_rate, basic_rate)
|
|||
|
|
|
|||
|
|
hotel_amount = hotel_rate * Decimal(days)
|
|||
|
|
allowance_amount = total_allowance_rate * Decimal(days)
|
|||
|
|
total_amount = hotel_amount + allowance_amount
|
|||
|
|
band_label = policy.band_labels.get(grade_band, grade_band)
|
|||
|
|
rule_name = policy.standard_rule_name or policy.rule_name or "公司差旅费报销规则"
|
|||
|
|
rule_version = policy.standard_rule_version or policy.rule_version or ""
|
|||
|
|
display_city = matched_city or self._format_other_region_display(matched_other_region)
|
|||
|
|
formula_text = (
|
|||
|
|
f"住宿 {self._format_money(hotel_rate)} × {days} 天 + "
|
|||
|
|
f"补贴 {self._format_money(total_allowance_rate)} × {days} 天 = "
|
|||
|
|
f"{self._format_money(total_amount)}"
|
|||
|
|
)
|
|||
|
|
summary_text = (
|
|||
|
|
f"按《{rule_name}》{f'({rule_version})' if rule_version else ''}测算:"
|
|||
|
|
f"当前职级 {grade} 对应 {band_label} 档,出差地点“{location}”匹配为“{display_city}”,"
|
|||
|
|
f"住宿标准 {self._format_money(hotel_rate)} 元/天,补贴区域为“{allowance_region}”,"
|
|||
|
|
f"补贴标准 {self._format_money(total_allowance_rate)} 元/天"
|
|||
|
|
f"(伙食 {self._format_money(meal_rate)} + 基本 {self._format_money(basic_rate)})。"
|
|||
|
|
f"按 {days} 天计算,住宿合计 {self._format_money(hotel_amount)} 元,"
|
|||
|
|
f"补贴合计 {self._format_money(allowance_amount)} 元,"
|
|||
|
|
f"参考可报销总金额为 {self._format_money(total_amount)} 元。"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return TravelReimbursementCalculatorResponse(
|
|||
|
|
days=days,
|
|||
|
|
location=location,
|
|||
|
|
matched_city=display_city,
|
|||
|
|
city_tier=city_tier,
|
|||
|
|
grade=grade,
|
|||
|
|
grade_band=grade_band,
|
|||
|
|
grade_band_label=band_label,
|
|||
|
|
hotel_rate=hotel_rate,
|
|||
|
|
hotel_amount=hotel_amount,
|
|||
|
|
allowance_region=allowance_region,
|
|||
|
|
meal_allowance_rate=meal_rate,
|
|||
|
|
basic_allowance_rate=basic_rate,
|
|||
|
|
total_allowance_rate=total_allowance_rate,
|
|||
|
|
allowance_amount=allowance_amount,
|
|||
|
|
total_amount=total_amount,
|
|||
|
|
rule_name=rule_name,
|
|||
|
|
rule_version=rule_version,
|
|||
|
|
formula_text=formula_text,
|
|||
|
|
summary_text=summary_text,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _load_travel_policy(self) -> RuntimeTravelPolicy:
|
|||
|
|
AgentAssetService(self.db).list_assets(asset_type=AgentAssetType.RULE.value)
|
|||
|
|
policy = ExpenseRuleRuntimeService(self.db).load_catalog().travel_policy
|
|||
|
|
if policy is None:
|
|||
|
|
raise ValueError("规则中心暂未配置差旅报销规则。")
|
|||
|
|
return policy
|
|||
|
|
|
|||
|
|
def _resolve_grade(
|
|||
|
|
self,
|
|||
|
|
grade: str | None,
|
|||
|
|
current_user: CurrentUserContext,
|
|||
|
|
) -> str:
|
|||
|
|
normalized_grade = str(grade or "").strip()
|
|||
|
|
if normalized_grade:
|
|||
|
|
return normalized_grade
|
|||
|
|
|
|||
|
|
employee = self._resolve_current_employee(current_user)
|
|||
|
|
if employee is not None and str(employee.grade or "").strip():
|
|||
|
|
return str(employee.grade).strip()
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_other_region(location: str) -> str:
|
|||
|
|
normalized = re.sub(r"\s+", "", str(location or "").strip())
|
|||
|
|
if not normalized:
|
|||
|
|
return ""
|
|||
|
|
if any(keyword in normalized for keyword in ("国外", "境外", "海外")):
|
|||
|
|
return "国外"
|
|||
|
|
for keyword in ("香港", "澳门", "台湾", "港澳台", "西藏", "拉萨", "新疆", "乌鲁木齐"):
|
|||
|
|
if keyword in normalized:
|
|||
|
|
return keyword
|
|||
|
|
city_matches = []
|
|||
|
|
province_matches = []
|
|||
|
|
for keyword in OTHER_REGION_LOCATION_KEYWORDS:
|
|||
|
|
if not keyword or keyword not in normalized:
|
|||
|
|
continue
|
|||
|
|
if keyword in OTHER_REGION_PROVINCE_KEYWORDS:
|
|||
|
|
province_matches.append(keyword)
|
|||
|
|
else:
|
|||
|
|
city_matches.append(keyword)
|
|||
|
|
candidates = city_matches or province_matches
|
|||
|
|
if candidates:
|
|||
|
|
return sorted(candidates, key=len, reverse=True)[0]
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _format_other_region_display(region: str) -> str:
|
|||
|
|
normalized = str(region or "").strip()
|
|||
|
|
if not normalized:
|
|||
|
|
return ""
|
|||
|
|
if normalized in {"国外", "香港", "澳门", "台湾", "港澳台", "西藏", "拉萨", "新疆", "乌鲁木齐"}:
|
|||
|
|
return normalized
|
|||
|
|
return f"{normalized}(其他地区)"
|
|||
|
|
|
|||
|
|
def _resolve_current_employee(self, current_user: CurrentUserContext) -> Employee | None:
|
|||
|
|
candidates = [
|
|||
|
|
str(current_user.username or "").strip(),
|
|||
|
|
str(current_user.name or "").strip(),
|
|||
|
|
]
|
|||
|
|
normalized_candidates = [
|
|||
|
|
item
|
|||
|
|
for item in dict.fromkeys(candidate for candidate in candidates if candidate)
|
|||
|
|
if item
|
|||
|
|
]
|
|||
|
|
if not normalized_candidates:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
for candidate in normalized_candidates:
|
|||
|
|
employee = self.db.scalar(
|
|||
|
|
select(Employee)
|
|||
|
|
.where(
|
|||
|
|
or_(
|
|||
|
|
func.lower(Employee.email) == candidate.lower(),
|
|||
|
|
func.lower(Employee.employee_no) == candidate.lower(),
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.limit(1)
|
|||
|
|
)
|
|||
|
|
if employee is not None:
|
|||
|
|
return employee
|
|||
|
|
|
|||
|
|
for candidate in normalized_candidates:
|
|||
|
|
matches = list(
|
|||
|
|
self.db.scalars(
|
|||
|
|
select(Employee)
|
|||
|
|
.where(Employee.name == candidate)
|
|||
|
|
.limit(2)
|
|||
|
|
).all()
|
|||
|
|
)
|
|||
|
|
if len(matches) == 1:
|
|||
|
|
return matches[0]
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_city(location: str, policy: RuntimeTravelPolicy) -> str:
|
|||
|
|
normalized = str(location or "").strip()
|
|||
|
|
if not normalized:
|
|||
|
|
return ""
|
|||
|
|
city_names = set(policy.city_tiers.keys())
|
|||
|
|
city_names.update(policy.hotel_city_limits.keys())
|
|||
|
|
for city in sorted(city_names, key=lambda item: len(item), reverse=True):
|
|||
|
|
if city in AMBIGUOUS_PROVINCE_CITY_NAMES and normalized != city and f"{city}市" not in normalized:
|
|||
|
|
continue
|
|||
|
|
if city and city in normalized:
|
|||
|
|
return city
|
|||
|
|
compact = re.sub(r"(省|市|区|县|自治州|特别行政区)$", "", normalized)
|
|||
|
|
for city in sorted(city_names, key=lambda item: len(item), reverse=True):
|
|||
|
|
if city in AMBIGUOUS_PROVINCE_CITY_NAMES and compact != city and f"{city}市" not in normalized:
|
|||
|
|
continue
|
|||
|
|
if city and city in compact:
|
|||
|
|
return city
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_hotel_rate(
|
|||
|
|
policy: RuntimeTravelPolicy,
|
|||
|
|
grade_band: str,
|
|||
|
|
matched_city: str,
|
|||
|
|
city_tier: str,
|
|||
|
|
) -> Decimal:
|
|||
|
|
city_limits = policy.hotel_city_limits.get(matched_city, {}) if matched_city else {}
|
|||
|
|
if city_limits.get(grade_band) is not None:
|
|||
|
|
return Decimal(city_limits[grade_band])
|
|||
|
|
|
|||
|
|
band_limits = policy.hotel_limits.get(grade_band, {})
|
|||
|
|
if band_limits.get(city_tier) is not None:
|
|||
|
|
return Decimal(band_limits[city_tier])
|
|||
|
|
if band_limits.get("tier_3") is not None:
|
|||
|
|
return Decimal(band_limits["tier_3"])
|
|||
|
|
return Decimal("0")
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_allowance_region(location: str, matched_city: str) -> str:
|
|||
|
|
text = f"{location} {matched_city}".strip()
|
|||
|
|
if any(keyword in text for keyword in ("国外", "境外", "海外")):
|
|||
|
|
return "国外"
|
|||
|
|
if any(keyword in text for keyword in ("香港", "澳门", "台湾", "港澳台")):
|
|||
|
|
return "港澳台"
|
|||
|
|
if "乌鲁木齐" in text:
|
|||
|
|
return "新疆-乌鲁木齐"
|
|||
|
|
if "新疆" in text:
|
|||
|
|
return "新疆-其他"
|
|||
|
|
if "西藏" in text or "拉萨" in text:
|
|||
|
|
return "西藏"
|
|||
|
|
if any(keyword in text for keyword in ("北京", "上海", "天津", "重庆", "深圳", "珠海", "汕头", "厦门")):
|
|||
|
|
return "直辖市/特区"
|
|||
|
|
return "其他地区"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_allowance_rate(policy: RuntimeTravelPolicy, allowance_key: str, region: str) -> Decimal:
|
|||
|
|
limits = policy.allowance_limits.get(allowance_key, {})
|
|||
|
|
if limits.get(region) is not None:
|
|||
|
|
return Decimal(limits[region])
|
|||
|
|
if limits.get("其他地区") is not None:
|
|||
|
|
return Decimal(limits["其他地区"])
|
|||
|
|
return Decimal("0")
|
|||
|
|
|
|||
|
|
def _resolve_total_allowance_rate(
|
|||
|
|
self,
|
|||
|
|
policy: RuntimeTravelPolicy,
|
|||
|
|
region: str,
|
|||
|
|
meal_rate: Decimal,
|
|||
|
|
basic_rate: Decimal,
|
|||
|
|
) -> Decimal:
|
|||
|
|
total_limits = policy.allowance_limits.get("total", {})
|
|||
|
|
if total_limits.get(region) is not None:
|
|||
|
|
return Decimal(total_limits[region])
|
|||
|
|
if total_limits.get("其他地区") is not None:
|
|||
|
|
return Decimal(total_limits["其他地区"])
|
|||
|
|
return meal_rate + basic_rate
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _format_money(value: Decimal | int | float | str) -> str:
|
|||
|
|
return f"{Decimal(str(value)).quantize(Decimal('0.01'))}"
|