Python/票据理赔自动化/main.py

203 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
票据理赔自动化最小化实现
功能清单
https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink
"""
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
import pandas
from jinja2 import Environment, FileSystemLoader
from common import dossier, rule_engine
from image import image_classify
from image import image_recognize
# -------------------------
# 主逻辑
# -------------------------
if __name__ == "__main__":
# 初始化工作目录路径
workplace_path = Path("directory")
workplace_path.mkdir(parents=True, exist_ok=True) # 若工作目录不存在则创建
# 实例化JINJA2环境
environment = Environment(loader=FileSystemLoader("."))
# 添加DATE过滤器
environment.filters["date"] = lambda date: (
date.strftime("%Y-%m-%d") if date else "长期"
)
# 加载赔案档案模版
template = environment.get_template("template.html")
# -------------------------
# 自定义方法
# -------------------------
# noinspection PyShadowingNames
def case_adjust() -> None:
"""
理算赔案并整合至赔案档案
:return: 无
"""
def receipt_adjust(row: pandas.Series) -> List[Dict[str, Any]]:
"""
票据理算
:param row: 票据
:return: 理算记录
"""
date = row["date"]
current_type = row["就诊类型"]
current_amount = row["合理金额"]
remaining_claim = current_amount
claim_details = []
if current_amount <= 0:
return []
# 筛选有效保单并排序
valid_rules = sorted(
[
r
for r in policy_rules
if current_type in r["就诊类型"]
and r["生效日期"] <= current_date <= r["失效日期"]
and r["剩余额度"] > 0.0
],
key=lambda x: x["剩余额度"],
reverse=True,
)
# 循环分摊赔付,生成分明细列表
for rule in valid_rules:
if remaining_claim <= 0.0:
break
pay_ratio = rule["赔付比例"]
rule_name = rule["责任名称"]
remaining_quota = rule["剩余额度"]
max_payable = remaining_claim * pay_ratio
actual_pay = min(remaining_quota, max_payable)
if actual_pay > 0.0:
corresponding_actual_amount = actual_pay / pay_ratio
# 构建明细字典字段与后续DataFrame列对应
detail = {
"就诊类型": current_type,
"就诊合理金额": current_amount,
"保单责任名称": rule_name,
"保单赔付比例": pay_ratio,
"保单本次赔付金额": round(actual_pay, 2),
"本次对应合理金额部分": round(corresponding_actual_amount, 2),
"保单赔付后剩余额度": round(remaining_quota - actual_pay, 2),
}
claim_details.append(detail)
# 更新保单额度和剩余待赔付金额
rule["剩余额度"] -= actual_pay
remaining_claim -= corresponding_actual_amount
return claim_details
# 基于据拒付规则评估
if not (result := rule_engine.evaluate(decision="拒付", inputs=dossier)):
# TODO: 若评估结果为空值(保险分公司未配置拒付规则)则流转至人工处理
raise
dossier["adjustment_layer"].update(
{
"conclusion": result["conclusion"], # 理赔结论
"explanation": result["explanation"], # 结论说明
}
)
if result["conclusion"] == "拒付":
return
adjustments = (
pandas.DataFrame(dossier["receipts_layer"]).assing(
adjustments=lambda dataframe: dataframe.apply(
receipt_adjust, axis="columns"
)
)
).explode("adjustments", ignore_index=True)
print(adjustments)
# 遍历工作目录中赔案目录并创建赔案档案(模拟自动化域就待自动化任务创建理赔档案)
for case_path in [x for x in workplace_path.iterdir() if x.is_dir()]:
# 初始化赔案档案保险公司将提供投保公司、保险分公司和报案时间等TPA作业系统签收后生成赔案号
dossier["report_layer"].update(
{
"report_time": datetime(2025, 7, 25, 12, 0, 0), # 指定报案时间
"case_number": case_path.stem, # 设定:赔案目录名称为赔案号
"insurer_company": (
insurer_company := "中银保险有限公司苏州分公司"
), # 指定保险分公司
}
)
# 遍历赔案目录中影像件
for image_index, image_path in enumerate(
sorted(
[
x
for x in case_path.glob(pattern="*")
if x.is_file() and x.suffix.lower() in [".jpg", ".jpeg", ".png"]
], # 实际作业亦仅支持JPG、JPEG或PNG
key=lambda x: x.stat().st_ctime, # 根据影像件创建时间顺序排序
),
1,
):
# 分类影像件并旋正(较初审自动化无使能检查)
image_classify(image_index, image_path)
# 就影像件层按照影像件类型指定排序
dossier["images_layer"].sort(
key=lambda x: [
"居民户口簿",
"居民身份证(国徽面)",
"居民身份证(头像面)",
"居民身份证(国徽、头像面)",
"中国港澳台地区及境外护照",
"理赔申请书",
"增值税发票",
"医疗门诊收费票据",
"医疗住院收费票据",
"医疗费用清单",
"银行卡",
"其它",
].index(x["image_type"])
)
# 遍历影像件层中影像件
for image in dossier["images_layer"]:
# 识别影像件并整合至赔案档案
image_recognize(
image,
insurer_company,
)
# 就票据层按照开票日期和票据号顺序排序
dossier["receipts_layer"].sort(key=lambda x: (x["date"], x["number"]))
print(dossier["insured_persons_layer"])
exit()
# 理算
case_adjust()
print(dossier["adjustment_layer"])
for receipt in dossier["receipts_layer"]:
print(receipt)
print(dossier["report_layer"])
print(dossier["insured_person_layer"])
print(dossier["insured_persons_layer"])
dossier.pop("images_layer")
dossier.pop("receipts_layer")