This commit is contained in:
parent
b3060d111d
commit
87ac2da670
|
|
@ -0,0 +1,81 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
HTML渲染器
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
def datetime_to_str(field):
|
||||
"""
|
||||
渲染模板时,若字段为datetime对象则转为字符串
|
||||
:param field: 字段
|
||||
:return: 字符串
|
||||
"""
|
||||
if isinstance(field, datetime):
|
||||
if field == datetime(9999, 12, 31):
|
||||
return "长期"
|
||||
if field.hour == 0 and field.minute == 0 and field.second == 0:
|
||||
return field.strftime("%Y-%m-%d")
|
||||
return field.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
return field
|
||||
|
||||
|
||||
def str_to_str(field):
|
||||
"""
|
||||
渲染模板时,若字段为字符串则转空字符串
|
||||
:param field: 字段
|
||||
:return: 字符串
|
||||
"""
|
||||
if isinstance(field, str):
|
||||
return field
|
||||
return ""
|
||||
|
||||
|
||||
class HTMLRenderer:
|
||||
"""
|
||||
HTML渲染器,支持:
|
||||
基于模板,根据数据字典渲染HTML文档
|
||||
"""
|
||||
|
||||
def __init__(self, template_path: Path):
|
||||
"""
|
||||
初始化HTML渲染器
|
||||
:param template_path: 模板路径
|
||||
"""
|
||||
# 实例化jinja2环境
|
||||
self.environment = Environment(
|
||||
loader=FileSystemLoader(searchpath=template_path.parent)
|
||||
)
|
||||
# 设置过滤器
|
||||
self.environment.filters.update(
|
||||
{
|
||||
"datetime_to_str": datetime_to_str,
|
||||
"str_to_str": str_to_str,
|
||||
}
|
||||
)
|
||||
# 加载指定模板
|
||||
self.template = self.environment.get_template(template_path.name)
|
||||
|
||||
def render(self, obj: Dict[str, Any], output_path: Path) -> None:
|
||||
"""
|
||||
根据数据字典渲染HTML文档
|
||||
:param obj: 数据字典
|
||||
:param output_path: HTML文档输出路径
|
||||
:return: 无
|
||||
"""
|
||||
try:
|
||||
with open(
|
||||
file=output_path,
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
) as file:
|
||||
file.write(self.template.render(obj=obj)) # 在模板中需以obj获取键值
|
||||
|
||||
except Exception as exception:
|
||||
print(f"根据数据字典渲染HTML文档发生异常:{str(exception)}")
|
||||
270
票据理赔自动化/case.py
270
票据理赔自动化/case.py
|
|
@ -3,13 +3,14 @@
|
|||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
from datetime import datetime
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import pandas
|
||||
|
||||
from common import masterdata, rules_engine
|
||||
|
||||
sys.path.append(Path(__file__).parent.parent.as_posix())
|
||||
from utils.html_render import HTMLRenderer
|
||||
|
||||
|
||||
def case_adjust(dossier: Dict[str, Any]) -> None:
|
||||
"""
|
||||
|
|
@ -30,139 +31,134 @@ def case_adjust(dossier: Dict[str, Any]) -> None:
|
|||
if conclusion == "拒付":
|
||||
return
|
||||
|
||||
# 赔案理算记录
|
||||
receipts_adjustments = (
|
||||
(
|
||||
pandas.DataFrame(data=dossier["receipts_layer"]).assign(
|
||||
receipt_adjustments=lambda dataframe: dataframe.apply(
|
||||
lambda row: receipt_adjust(
|
||||
row=row, liabilities=dossier["liabilities_layer"]
|
||||
),
|
||||
axis="columns",
|
||||
) # 票据理算
|
||||
)
|
||||
# 就票据层按照开票日期和票据号顺序排序
|
||||
dossier["receipts_layer"].sort(key=lambda x: (x["date"], x["number"]))
|
||||
|
||||
# 遍历票据层内所有票据,票据理算并添加理算记录
|
||||
for idx, receipt in enumerate(dossier["receipts_layer"]):
|
||||
# 添加理算记录
|
||||
dossier["receipts_layer"][idx]["adjustments"] = receipt_adjust(
|
||||
receipt=receipt, liabilities=dossier["liabilities_layer"]
|
||||
)
|
||||
.explode(column="receipt_adjustments", ignore_index=True)
|
||||
.pipe(
|
||||
lambda dataframe: pandas.concat(
|
||||
# 票据理算金额
|
||||
dossier["receipts_layer"][idx]["adjustment_amount"] = Decimal(
|
||||
sum(
|
||||
[
|
||||
dataframe.drop(
|
||||
[
|
||||
"receipt_adjustments",
|
||||
],
|
||||
axis="columns",
|
||||
),
|
||||
pandas.json_normalize(dataframe["receipt_adjustments"]),
|
||||
],
|
||||
axis="columns",
|
||||
adjustment["adjustment_amount"]
|
||||
for adjustment in dossier["receipts_layer"][idx]["adjustments"]
|
||||
]
|
||||
)
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
)
|
||||
|
||||
dossier["receipts_layer"] = receipts_adjustments.to_dict(orient="records")
|
||||
|
||||
print(dossier["receipts_layer"])
|
||||
|
||||
# 赔案理算金额
|
||||
dossier["adjustment_layer"].update(
|
||||
{
|
||||
"adjustment_amount": (
|
||||
receipts_adjustments["adjustment_amount"]
|
||||
.sum()
|
||||
.quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
), # 理算金额
|
||||
}
|
||||
)
|
||||
|
||||
# 实例化JINJA2
|
||||
environment = Environment(
|
||||
loader=FileSystemLoader(file_path := Path(__file__).parent)
|
||||
)
|
||||
# 添加过滤器
|
||||
environment.filters["DateTime"] = lambda i: (
|
||||
i.strftime("%Y-%m-%d") if i != datetime(9999, 12, 31) else "长期"
|
||||
)
|
||||
# 加载赔案档案模版
|
||||
template = environment.get_template("template.html")
|
||||
|
||||
with open(
|
||||
file_path / f"dossiers/{dossier["report_layer"]["case_number"]}.html",
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as file:
|
||||
file.write(template.render(dossier=dossier))
|
||||
|
||||
|
||||
def receipt_adjust(
|
||||
row: pandas.Series, liabilities: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
理算票据
|
||||
:param row: 票据数据
|
||||
:param liabilities: 理算责任
|
||||
:return: 理算记录
|
||||
"""
|
||||
# 初始化票据理算记录
|
||||
receipt_adjustments = []
|
||||
|
||||
# 初始化票据剩余可理算金额
|
||||
remaining_adjustable_amount = (
|
||||
row["personal_self_payment"]
|
||||
+ row["non_medical_payment"]
|
||||
+ row["reasonable_amount"]
|
||||
).quantize( # type: ignore[reportAttributeAccessIssue]
|
||||
Decimal("0.00"),
|
||||
dossier["adjustment_layer"]["adjustment_amount"] = Decimal(
|
||||
sum(
|
||||
[
|
||||
adjustment["adjustment_amount"]
|
||||
for receipt in dossier["receipts_layer"]
|
||||
for adjustment in receipt["adjustments"]
|
||||
]
|
||||
)
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
|
||||
# 遍历所有理赔责任,根据出险人、出险事故、查验状态和出险日期匹配理赔责任
|
||||
# 实例化HTML渲染器
|
||||
html_renderer = HTMLRenderer(template_path=Path(__file__).parent / "template.html")
|
||||
# 根据赔案档案渲染HTML文档
|
||||
html_renderer.render(
|
||||
obj=dossier,
|
||||
output_path=Path(__file__).parent
|
||||
/ "dossiers"
|
||||
/ f"{dossier["report_layer"]["case_number"]}.html",
|
||||
)
|
||||
|
||||
|
||||
def receipt_adjust(
|
||||
receipt: Dict[str, Any], liabilities: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
理算票据
|
||||
:param receipt: 票据数据字典
|
||||
:param liabilities: 理算责任
|
||||
:return: 理算记录
|
||||
"""
|
||||
# 初始化理算记录
|
||||
adjustments = []
|
||||
|
||||
# 初始化票据剩余理算金额
|
||||
remaining_adjustment_amount = masterdata.query_remaining_adjustment_amount(
|
||||
receipt_number=receipt["number"],
|
||||
)
|
||||
if remaining_adjustment_amount is None:
|
||||
remaining_adjustment_amount = (
|
||||
receipt["personal_self_payment"] # 个人自费金额
|
||||
+ receipt["non_medical_payment"] # 个人自付金额
|
||||
+ receipt["reasonable_amount"] # 合理金额
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
|
||||
# 遍历所有理赔责任,根据出险人、理赔类型、查验状态和出险日期匹配理赔责任
|
||||
for liability in liabilities:
|
||||
if (
|
||||
row["payer"] in [liability["insured_person"] for liability in liabilities]
|
||||
and row["accident"] == liability["accident"]
|
||||
and row["verification"] in ["真票", "无法查验"]
|
||||
receipt["payer"]
|
||||
in [liability["insured_person"] for liability in liabilities]
|
||||
and receipt["accident"] == liability["accident"]
|
||||
and receipt["verification"] in ["真票", "无法查验"]
|
||||
and liability["commencement_date"]
|
||||
<= row["date"]
|
||||
<= receipt["date"]
|
||||
<= liability["termination_date"]
|
||||
):
|
||||
# 个单余额
|
||||
remaining_amount = masterdata.query_remaining_amount(
|
||||
# 个单剩余保额
|
||||
remaining_coverage_amount = masterdata.query_after_change_amount(
|
||||
person_policy_guid=liability["person_policy_guid"],
|
||||
)
|
||||
|
||||
# 个人自费可理算金额
|
||||
personal_self_adjustable_amount = (
|
||||
row["personal_self_payment"]
|
||||
* liability["personal_self_ratio"]
|
||||
receipt["personal_self_payment"] # 个人自费金额
|
||||
* liability["personal_self_ratio"] # 个人自费理算比例
|
||||
* Decimal("0.01")
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
# 个人自付可理算金额
|
||||
non_medical_adjustable_amount = (
|
||||
row["non_medical_payment"]
|
||||
* liability["non_medical_ratio"]
|
||||
receipt["non_medical_payment"] # 个人自付金额
|
||||
* liability["non_medical_ratio"] # 个人自付理算比例
|
||||
* Decimal("0.01")
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
# 合理可理算金额
|
||||
reasonable_adjustable_amount = (
|
||||
row["reasonable_amount"]
|
||||
* liability["reasonable_ratio"]
|
||||
receipt["reasonable_amount"] # 合理金额
|
||||
* liability["reasonable_ratio"] # 合理理算比例
|
||||
* Decimal("0.01")
|
||||
).quantize(
|
||||
exp=Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
|
||||
# 理算金额
|
||||
adjustment_amount = max(
|
||||
Decimal("0.00"),
|
||||
min(
|
||||
remaining_adjustable_amount,
|
||||
remaining_amount,
|
||||
remaining_adjustment_amount, # 剩余理算金额
|
||||
remaining_coverage_amount, # 个单剩余保额
|
||||
adjustable_amount := (
|
||||
(
|
||||
personal_self_adjustable_amount
|
||||
+ non_medical_adjustable_amount
|
||||
+ reasonable_adjustable_amount
|
||||
personal_self_adjustable_amount # 个人自费可理算金额
|
||||
+ non_medical_adjustable_amount # 个人自付可理算金额
|
||||
+ reasonable_adjustable_amount # 合理可理算金额
|
||||
).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
|
|
@ -175,35 +171,44 @@ def receipt_adjust(
|
|||
if adjustment_amount > Decimal("0.00"):
|
||||
masterdata.add_coverage_change(
|
||||
person_policy_guid=liability["person_policy_guid"],
|
||||
before_change_amount=remaining_amount,
|
||||
before_change_amount=remaining_coverage_amount,
|
||||
change_amount=adjustment_amount,
|
||||
)
|
||||
|
||||
receipt_adjustments.append(
|
||||
adjustments.append(
|
||||
{
|
||||
"group_policy": liability["group_policy"], # 团单号
|
||||
"person_policy": liability["person_policy"], # 个单号
|
||||
"liability": liability["liability"], # 理赔责任名称
|
||||
"personal_self_payment": row[
|
||||
"accident": liability["accident"], # 理赔类型
|
||||
"personal_self_payment": receipt[
|
||||
"personal_self_payment"
|
||||
], # 个人自费金额
|
||||
"personal_self_ratio": liability[
|
||||
"personal_self_ratio"
|
||||
], # 个人自费比例
|
||||
], # 个人自费理算比例
|
||||
"personal_self_adjustable_amount": personal_self_adjustable_amount, # 个人自费可理算金额
|
||||
"non_medical_payment": row["non_medical_payment"], # 个人自付金额
|
||||
"non_medical_ratio": liability["non_medical_ratio"], # 个人自付比例
|
||||
"non_medical_payment": receipt[
|
||||
"non_medical_payment"
|
||||
], # 个人自付金额
|
||||
"non_medical_ratio": liability[
|
||||
"non_medical_ratio"
|
||||
], # 个人自付理算比例
|
||||
"non_medical_adjustable_amount": non_medical_adjustable_amount, # 个人自付可理算金额
|
||||
"reasonable_amount": row["reasonable_amount"], # 合理可理算金额
|
||||
"reasonable_ratio": liability["reasonable_ratio"], # 合理部分比例
|
||||
"reasonable_amount": receipt["reasonable_amount"], # 合理金额
|
||||
"reasonable_ratio": liability["reasonable_ratio"], # 合理理算比例
|
||||
"reasonable_adjustable_amount": reasonable_adjustable_amount, # 合理可理算金额
|
||||
"remaining_adjustment_amount": remaining_adjustment_amount, # 剩余理算金额
|
||||
"remaining_coverage_amount": remaining_coverage_amount, # 个单剩余保额
|
||||
"adjustable_amount": adjustable_amount, # 可理算金额
|
||||
"adjustment_amount": adjustment_amount, # 理算金额
|
||||
"adjustment_explanation": f"""
|
||||
1、应理算金额:{remaining_adjustable_amount:.2f};
|
||||
2、个单余额:{remaining_amount:.2f};
|
||||
1、剩余理算金额:{remaining_adjustment_amount:.2f};
|
||||
2、个单剩余保额:{remaining_coverage_amount:.2f};
|
||||
3、可理算金额:{adjustable_amount:.2f},其中:
|
||||
1)个人自费可理算金额:{personal_self_adjustable_amount:.2f}={row['personal_self_payment']:.2f}*{liability['personal_self_ratio']:.2f}%;
|
||||
2)个人自付可理算金额:{non_medical_adjustable_amount:.2f}={row['non_medical_payment']:.2f}*{liability['non_medical_ratio']:.2f}%;
|
||||
3)合理部分可理算金额:{reasonable_adjustable_amount:.2f}={row['reasonable_amount']:.2f}*{liability['reasonable_ratio']:.2f}%;
|
||||
1)个人自费可理算金额:{personal_self_adjustable_amount:.2f}={receipt['personal_self_payment']:.2f}*{liability['personal_self_ratio']:.2f}%;
|
||||
2)个人自付可理算金额:{non_medical_adjustable_amount:.2f}={receipt['non_medical_payment']:.2f}*{liability['non_medical_ratio']:.2f}%;
|
||||
3)合理部分可理算金额:{reasonable_adjustable_amount:.2f}={receipt['reasonable_amount']:.2f}*{liability['reasonable_ratio']:.2f}%;
|
||||
4、理算金额:{adjustment_amount:.2f},即上述应理算金额、个人余额和可理算金额的最小值。
|
||||
""".replace(
|
||||
"\n", ""
|
||||
|
|
@ -213,28 +218,41 @@ def receipt_adjust(
|
|||
}
|
||||
)
|
||||
|
||||
remaining_adjustable_amount -= adjustment_amount
|
||||
# 若剩余可理算金额小于等于0则跳出循环
|
||||
if remaining_adjustable_amount <= Decimal("0.00"):
|
||||
remaining_adjustment_amount -= adjustment_amount
|
||||
# 若剩余理算金额小于等于0则跳出循环
|
||||
if remaining_adjustment_amount <= Decimal("0.00"):
|
||||
break
|
||||
|
||||
if not receipt_adjustments:
|
||||
receipt_adjustments.append(
|
||||
if not adjustments:
|
||||
adjustments.append(
|
||||
{
|
||||
"group_policy": None, # 团单号
|
||||
"person_policy": None, # 个单号
|
||||
"liability": None, # 理赔责任名称
|
||||
"personal_self_payment": None, # 个人自费金额
|
||||
"personal_self_ratio": None, # 个人自费比例
|
||||
"accident": None, # 理赔类型
|
||||
"personal_self_payment": receipt[
|
||||
"personal_self_payment"
|
||||
], # 个人自费金额
|
||||
"personal_self_ratio": None, # 个人自费理算比例
|
||||
"personal_self_adjustable_amount": None, # 个人自费可理算金额
|
||||
"non_medical_payment": None, # 个人自付金额
|
||||
"non_medical_ratio": None, # 个人自付比例
|
||||
"non_medical_payment": receipt["non_medical_payment"], # 个人自付金额
|
||||
"non_medical_ratio": None, # 个人自付理算比例
|
||||
"non_medical_adjustable_amount": None, # 个人自付可理算金额
|
||||
"reasonable_amount": None, # 合理可理算金额
|
||||
"reasonable_ratio": None, # 合理部分比例
|
||||
"reasonable_amount": receipt["reasonable_amount"], # 合理金额
|
||||
"reasonable_ratio": None, # 合理理算比例
|
||||
"reasonable_adjustable_amount": None, # 合理可理算金额
|
||||
"adjustment_amount": Decimal("0.00"), # 理赔责任理算金额
|
||||
"remaining_adjustment_amount": remaining_adjustment_amount, # 剩余理算金额
|
||||
"remaining_coverage_amount": None, # 个单剩余保额
|
||||
"adjustable_amount": None, # 可理算金额
|
||||
"adjustment_amount": Decimal("0.00"), # 理算金额
|
||||
"adjustment_explanation": "票据不予理算", # 理算说明
|
||||
}
|
||||
)
|
||||
|
||||
return receipt_adjustments
|
||||
# 新增理算记录
|
||||
masterdata.add_ajustment(
|
||||
receipt_number=receipt["number"],
|
||||
remaining_adjustment_amount=remaining_adjustment_amount,
|
||||
)
|
||||
|
||||
return adjustments
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
104
票据理赔自动化/image.py
104
票据理赔自动化/image.py
|
|
@ -115,18 +115,15 @@ def image_classify(image_index: int, image_path: Path, dossier: Dict[str, Any])
|
|||
image_format, image_ndarray, image_size_specified=2
|
||||
)
|
||||
|
||||
# 将影像件添加至影像件层
|
||||
dossier["images_layer"].append(
|
||||
{
|
||||
"image_index": f"{image_index:02d}", # 影像件编号
|
||||
"image_path": image_path.as_posix(), # 影像件路径
|
||||
"image_name": image_path.stem, # 影像件名称
|
||||
"image_format": image_format, # 影像件格式
|
||||
"image_guid": image_guid, # 影像件唯一标识
|
||||
"image_base64": image_base64, # 影像件BASE64编码
|
||||
"image_type": image_type, # 影像件类型
|
||||
}
|
||||
)
|
||||
# 将已分类影像件添加至影像件层
|
||||
dossier["images_layer"][f"{image_index:02d}"] = {
|
||||
"image_path": image_path.as_posix(), # 影像件路径
|
||||
"image_relative_path": image_path.relative_to(image_path.parent.parent).as_posix(), # 影像件相对路径
|
||||
"image_format": image_format, # 影像件格式
|
||||
"image_guid": image_guid, # 影像件唯一标识
|
||||
"image_base64": image_base64, # 影像件BASE64编码
|
||||
"image_type": image_type, # 影像件类型
|
||||
} # 影像件编号作为键名
|
||||
|
||||
|
||||
def image_read(
|
||||
|
|
@ -139,7 +136,7 @@ def image_read(
|
|||
:return: 影像件图像数组
|
||||
"""
|
||||
try:
|
||||
# 先使用读取影像件,再解码为单通道灰度图数组对象
|
||||
# 先使用读取影像件,再解码为单通道灰度图数组对象(因在windows系统中,cv2.imread就包含中文的影像件路径兼容较差,估使用numpy.fromfile)
|
||||
image_ndarray = cv2.imdecode(
|
||||
buf=numpy.fromfile(file=image_path, dtype=numpy.uint8),
|
||||
flags=cv2.IMREAD_GRAYSCALE,
|
||||
|
|
@ -218,13 +215,15 @@ def image_compress(
|
|||
|
||||
|
||||
def image_recognize(
|
||||
image_index: str,
|
||||
image: Dict[str, Any],
|
||||
insurer_company: str,
|
||||
dossier: Dict[str, Any],
|
||||
) -> None:
|
||||
"""
|
||||
识别影像件并整合至赔案档案
|
||||
:param image: 影像件
|
||||
:param image_index: 影像件编号
|
||||
:param image: 影像件数据字典
|
||||
:param insurer_company: 保险分公司
|
||||
:param dossier: 赔案档案
|
||||
:return: 无
|
||||
|
|
@ -237,19 +236,19 @@ def image_recognize(
|
|||
"image_type": image["image_type"],
|
||||
},
|
||||
)["recognize_enabled"]:
|
||||
dossier["images_layer"][image_index]["image_recognized"] = "否,无需识别"
|
||||
return
|
||||
|
||||
# 根据影像件类型匹配影像件识别方法
|
||||
match image["image_type"]:
|
||||
case "居民户口簿":
|
||||
raise RuntimeError("暂不支持居民户口簿")
|
||||
case "居民身份证(国徽、头像面)" | "居民身份证(国徽面)" | "居民身份证(头像面)":
|
||||
# 居民身份证识别并整合至赔案档案
|
||||
identity_card_recognize(
|
||||
image=image, insurer_company=insurer_company, dossier=dossier
|
||||
)
|
||||
case "中国港澳台地区及境外护照":
|
||||
raise RuntimeError("暂不支持中国港澳台地区及境外护照")
|
||||
case "银行卡":
|
||||
# 银行卡识别并整合至赔案档案
|
||||
bank_card_recognize(image=image, dossier=dossier)
|
||||
case "理赔申请书":
|
||||
application_recognize(
|
||||
image=image, insurer_company=insurer_company, dossier=dossier
|
||||
|
|
@ -257,11 +256,15 @@ def image_recognize(
|
|||
case "增值税发票" | "医疗门诊收费票据" | "医疗住院收费票据":
|
||||
# 票据识别并整合至赔案档案
|
||||
receipt_recognize(
|
||||
image=image, insurer_company=insurer_company, dossier=dossier
|
||||
image_index=image_index,
|
||||
image=image,
|
||||
insurer_company=insurer_company,
|
||||
dossier=dossier,
|
||||
)
|
||||
case "银行卡":
|
||||
# 银行卡识别并整合至赔案档案
|
||||
bank_card_recognize(image=image, dossier=dossier)
|
||||
case _:
|
||||
raise RuntimeError(f"影像件类型未配置影像件识别方法")
|
||||
|
||||
dossier["images_layer"][image_index]["image_recognized"] = "是"
|
||||
|
||||
|
||||
def identity_card_recognize(
|
||||
|
|
@ -327,7 +330,7 @@ def identity_card_recognize(
|
|||
}
|
||||
)
|
||||
|
||||
# 根据保险分公司、被保险人、证件类型、证件号码和出险时间查询个单
|
||||
# 根据保险分公司名称、被保险人姓名、证件类型、证件号码和报案时间查询被保险人的理赔责任
|
||||
dossier["liabilities_layer"] = masterdata.query_liabilities(
|
||||
insurer_company=insurer_company,
|
||||
insured_person=insured_person,
|
||||
|
|
@ -577,17 +580,22 @@ def application_recognize(
|
|||
|
||||
|
||||
def receipt_recognize(
|
||||
image: Dict[str, Any], insurer_company: str, dossier: Dict[str, Any]
|
||||
image_index: str,
|
||||
image: Dict[str, Any],
|
||||
insurer_company: str,
|
||||
dossier: Dict[str, Any],
|
||||
) -> None:
|
||||
"""
|
||||
识别票据并整合至赔案档案
|
||||
:param image_index: 影像件编号
|
||||
:param image: 影像件
|
||||
:param insurer_company: 保险分公司
|
||||
:param dossier: 赔案档案
|
||||
:return: 空
|
||||
"""
|
||||
# 初始化票据数据
|
||||
receipt = {"image_index": image["image_index"]}
|
||||
receipt = {"image_index": image_index, "image_path": image["image_path"]}
|
||||
|
||||
# 请求深圳快瞳票据查验接口(兼容增值税发票、医疗门诊/住院收费票据)
|
||||
response = request.post(
|
||||
url=(url := "https://ai.inspirvision.cn/s/api/ocr/invoiceCheckAll"),
|
||||
|
|
@ -613,25 +621,29 @@ def receipt_recognize(
|
|||
"真票"
|
||||
if response["data"]["details"]["invoiceTypeNo"] == "0"
|
||||
else "红票"
|
||||
), # 红票为状态为失控、作废、已红冲、部分红冲和全额红冲的票据
|
||||
"number": response["data"]["details"]["number"],
|
||||
), # 查验状态,红票对应查验状态为失控、作废、已红冲、部分红冲和全额红冲
|
||||
"number": response["data"]["details"]["number"], # 票据号
|
||||
"code": (
|
||||
response["data"]["details"]["code"]
|
||||
if response["data"]["details"]["code"]
|
||||
else None
|
||||
),
|
||||
), # 票据代码
|
||||
"date": datetime.strptime(
|
||||
response["data"]["details"]["date"], "%Y年%m月%d日"
|
||||
), # 转为日期时间(datetime对象)
|
||||
"verification_code": response["data"]["details"]["check_code"],
|
||||
), # 开票日期
|
||||
"check_code": response["data"]["details"][
|
||||
"check_code"
|
||||
], # 校验码
|
||||
"amount": Decimal(
|
||||
response["data"]["details"]["total"]
|
||||
).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
), # 深圳快瞳票据查验接口中开票金额由字符串转为Decimal,保留两位小数
|
||||
"payer": response["data"]["details"]["buyer"],
|
||||
"institution": response["data"]["details"]["seller"],
|
||||
), # 开票金额
|
||||
"payer": response["data"]["details"]["buyer"], # 出险人
|
||||
"institution": response["data"]["details"][
|
||||
"seller"
|
||||
], # 购药及就医机构
|
||||
"items": [
|
||||
{
|
||||
"item": item["name"],
|
||||
|
|
@ -642,13 +654,13 @@ def receipt_recognize(
|
|||
)
|
||||
if item["quantity"]
|
||||
else Decimal("0.00")
|
||||
), # 深圳快瞳票据查验接口中明细单位由空字符转为None,若非空字符由字符串转为Decimal,保留两位小数
|
||||
),
|
||||
"amount": (
|
||||
Decimal(item["total"]) + Decimal(item["tax"])
|
||||
).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
), # 深圳快瞳票据查验接口中明细的金额和税额由字符串转为Decimal,保留两位小数,并求和
|
||||
),
|
||||
}
|
||||
for item in response["data"]["details"]["items"]
|
||||
],
|
||||
|
|
@ -689,7 +701,7 @@ def receipt_recognize(
|
|||
if response["data"]["hospitalizationDate"]
|
||||
else None
|
||||
),
|
||||
"verification_code": response["data"]["checkCode"],
|
||||
"check_code": response["data"]["checkCode"],
|
||||
"amount": Decimal(response["data"]["amount"]).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
|
|
@ -781,9 +793,7 @@ def receipt_recognize(
|
|||
fuzzy_match(response["data"], "开票日期"),
|
||||
"%Y年%m月%d日",
|
||||
),
|
||||
"verification_code": fuzzy_match(
|
||||
response["data"], "校验码"
|
||||
),
|
||||
"check_code": fuzzy_match(response["data"], "校验码"),
|
||||
"amount": Decimal(
|
||||
fuzzy_match(response["data"], "小写金额").replace(
|
||||
"¥", ""
|
||||
|
|
@ -857,9 +867,7 @@ def receipt_recognize(
|
|||
fuzzy_match(response["data"], "开票日期"),
|
||||
"%Y-%m-%d",
|
||||
),
|
||||
"verification_code": fuzzy_match(
|
||||
response["data"], "校验码"
|
||||
),
|
||||
"check_code": fuzzy_match(response["data"], "校验码"),
|
||||
"amount": Decimal(
|
||||
fuzzy_match(
|
||||
response["data"], "合计金额(小写)"
|
||||
|
|
@ -882,7 +890,7 @@ def receipt_recognize(
|
|||
"amount": Decimal(amount).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
), # 深圳快瞳票据识别接口中明细的金额和税额由字符串转为Decimal,保留两位小数,并求和
|
||||
),
|
||||
}
|
||||
for name, quantity, amount in zip(
|
||||
[
|
||||
|
|
@ -964,7 +972,7 @@ def receipt_recognize(
|
|||
if isinstance(receipt["endtime"], dict)
|
||||
else None
|
||||
),
|
||||
"verification_code": fuzzy_match(
|
||||
"check_code": fuzzy_match(
|
||||
receipt["global_detail"]["region_specific"],
|
||||
"校验码",
|
||||
),
|
||||
|
|
@ -1063,6 +1071,8 @@ def receipt_recognize(
|
|||
)
|
||||
)
|
||||
.assign(
|
||||
personal_self_payment=Decimal("0.00"), # 个人自费项
|
||||
non_medical_payment=Decimal("0.00"), # 个人自付项
|
||||
reasonable_amount=lambda dataframe: dataframe.apply(
|
||||
lambda row: Decimal(
|
||||
# 基于扣除明细项不合理费用决策规则评估
|
||||
|
|
@ -1080,8 +1090,8 @@ def receipt_recognize(
|
|||
rounding=ROUND_HALF_UP,
|
||||
),
|
||||
axis="columns",
|
||||
)
|
||||
) # 扣除明细项不合理费用
|
||||
), # 合理项
|
||||
)
|
||||
)
|
||||
|
||||
receipt.update(
|
||||
|
|
@ -1092,7 +1102,7 @@ def receipt_recognize(
|
|||
in receipt["payer"]
|
||||
else None
|
||||
), # 出险人姓名
|
||||
"accident": "药店购药", # 出险事故
|
||||
"accident": "药店购药", # 理赔类型
|
||||
"diagnosis": "购药拟诊", # 医疗诊断
|
||||
"personal_self_payment": Decimal("0.00"), # 个人自费金额
|
||||
"non_medical_payment": Decimal("0.00"), # 个人自付金额
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink
|
|||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from case import case_adjust
|
||||
from image import image_classify, image_recognize
|
||||
|
|
@ -24,15 +25,16 @@ if __name__ == "__main__":
|
|||
# 初始化赔案档案(推送至TPA时,保险公司会提保险分公司名称、报案时间和影像件等,TPA签收后生成赔案号)
|
||||
dossier = {
|
||||
"report_layer": {
|
||||
"case_number": case_path.stem, # 默认为赔案文件夹名称
|
||||
"insurer_company": (
|
||||
insurer_company := "中银保险有限公司苏州分公司"
|
||||
), # 默认为中银保险有限公司苏州分公司
|
||||
), # 保险分公司名称默认为中银保险有限公司苏州分公司
|
||||
"report_time": datetime(
|
||||
2025, 7, 25, 12, 0, 0
|
||||
), # 指定报案时间,默认为 datetime对象
|
||||
), # 报案时间默认为2025-07-25 12:00:00
|
||||
"images_counts": 15, # 影像件数默认为15
|
||||
"case_number": case_path.stem, # 赔案号默认为赔案文件夹名称
|
||||
}, # 报案层
|
||||
"images_layer": [], # 影像件层
|
||||
"images_layer": {}, # 影像件层
|
||||
"insured_person_layer": {}, # 出险人层
|
||||
"liabilities_layer": [], # 理赔责任层
|
||||
"receipts_layer": [], # 票据层
|
||||
|
|
@ -43,11 +45,11 @@ if __name__ == "__main__":
|
|||
for image_index, image_path in enumerate(
|
||||
sorted(
|
||||
[
|
||||
i
|
||||
for i in case_path.glob(pattern="*")
|
||||
if i.is_file() and i.suffix.lower() in [".jpg", ".jpeg", ".png"]
|
||||
x
|
||||
for x in case_path.glob(pattern="*")
|
||||
if x.is_file() and x.suffix.lower() in [".jpg", ".jpeg", ".png"]
|
||||
],
|
||||
key=lambda i: i.stat().st_birthtime, # 根据影像件创建时间顺序排序
|
||||
key=lambda x: x.stat().st_birthtime, # 根据影像件创建时间顺序排序
|
||||
),
|
||||
1,
|
||||
):
|
||||
|
|
@ -57,34 +59,47 @@ if __name__ == "__main__":
|
|||
)
|
||||
|
||||
# 就影像件层按照影像件类型指定排序
|
||||
dossier["images_layer"].sort(
|
||||
key=lambda i: [
|
||||
"居民户口簿",
|
||||
"居民身份证(国徽面)",
|
||||
"居民身份证(头像面)",
|
||||
"居民身份证(国徽、头像面)",
|
||||
"中国港澳台地区及境外护照",
|
||||
"理赔申请书",
|
||||
"增值税发票",
|
||||
"医疗门诊收费票据",
|
||||
"医疗住院收费票据",
|
||||
"医疗费用清单",
|
||||
"银行卡",
|
||||
"其它",
|
||||
].index(i["image_type"])
|
||||
dossier["images_layer"] = dict(
|
||||
sorted(
|
||||
dossier["images_layer"].items(),
|
||||
key=lambda x: [
|
||||
"居民户口簿",
|
||||
"居民身份证(国徽面)",
|
||||
"居民身份证(头像面)",
|
||||
"居民身份证(国徽、头像面)",
|
||||
"中国港澳台地区及境外护照",
|
||||
"银行卡",
|
||||
"理赔申请书",
|
||||
"其它",
|
||||
"增值税发票",
|
||||
"医疗门诊收费票据",
|
||||
"医疗住院收费票据",
|
||||
"医疗费用清单",
|
||||
].index(x[1]["image_type"]),
|
||||
),
|
||||
)
|
||||
# 统计已分类影像件数
|
||||
dossier["classified_images_counts"] = len(dossier["images_layer"])
|
||||
|
||||
# 遍历影像件层内影像件
|
||||
for image in dossier["images_layer"]:
|
||||
# 识别影像件并整合至赔案档案
|
||||
image_recognize(
|
||||
image=image,
|
||||
insurer_company=insurer_company,
|
||||
dossier=dossier,
|
||||
)
|
||||
# 遍历影像件层内所有影像件
|
||||
for image_index, image in dossier["images_layer"].items():
|
||||
if re.match(pattern=r"^\d{2}$", string=image_index):
|
||||
# 识别影像件并整合至赔案档案
|
||||
image_recognize(
|
||||
image_index=image_index,
|
||||
image=image,
|
||||
insurer_company=insurer_company,
|
||||
dossier=dossier,
|
||||
)
|
||||
|
||||
# 就票据层按照开票日期和票据号顺序排序
|
||||
dossier["receipts_layer"].sort(key=lambda x: (x["date"], x["number"]))
|
||||
# 统计已识别影像件数
|
||||
dossier["recognized_images_counts"] = len(
|
||||
[
|
||||
image
|
||||
for image in dossier["images_layer"].values()
|
||||
if image["image_recognized"] == "是"
|
||||
]
|
||||
)
|
||||
|
||||
# 理算赔案并整合至赔案档案
|
||||
case_adjust(dossier=dossier)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from decimal import Decimal, ROUND_HALF_UP
|
|||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
sys.path.append(Path(__file__).parent.parent.as_posix())
|
||||
from utils.sqlite import SQLite
|
||||
|
|
@ -97,7 +97,7 @@ class MasterData(SQLite):
|
|||
guid TEXT PRIMARY KEY,
|
||||
--理赔责任名称
|
||||
liability TEXT NOT NULL,
|
||||
--出险事故
|
||||
--理赔类型
|
||||
accident TEXT NOT NULL,
|
||||
--个人自费理算比例
|
||||
personal_self_ratio TEXT NOT NULL,
|
||||
|
|
@ -130,7 +130,7 @@ class MasterData(SQLite):
|
|||
--个单唯一标识
|
||||
person_policy_guid TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
"""
|
||||
)
|
||||
# 初始化购药及就医机构表
|
||||
self.execute(
|
||||
|
|
@ -158,6 +158,20 @@ class MasterData(SQLite):
|
|||
)
|
||||
"""
|
||||
)
|
||||
# 初始化票据理算表
|
||||
self.execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS adjustments
|
||||
(
|
||||
--票据号
|
||||
receipt_number TEXT PRIMARY KEY,
|
||||
--剩余理算金额
|
||||
remaining_adjustment_amount TEXT NOT NULL,
|
||||
--理算时间
|
||||
adjust_time TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"初始化主数据发生异常:{str(exception)}") from exception
|
||||
|
||||
|
|
@ -309,7 +323,72 @@ class MasterData(SQLite):
|
|||
except Exception as exception:
|
||||
raise RuntimeError(f"{str(exception)}") from exception
|
||||
|
||||
def query_remaining_amount(
|
||||
def query_remaining_adjustment_amount(
|
||||
self,
|
||||
receipt_number: str,
|
||||
) -> Optional[Decimal]:
|
||||
"""
|
||||
根据票据号查询剩余理算金额
|
||||
:param receipt_number: 票据号
|
||||
:return: 剩余理算金额
|
||||
"""
|
||||
try:
|
||||
with self:
|
||||
result = self.query_one(
|
||||
sql="""
|
||||
SELECT remaining_adjustment_amount
|
||||
FROM adjustments
|
||||
WHERE receipt_number = ?
|
||||
ORDER BY adjust_time DESC
|
||||
LIMIT 1;
|
||||
""",
|
||||
parameters=(receipt_number,),
|
||||
)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return Decimal(result["remaining_adjustment_amount"]).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"{str(exception)}") from exception
|
||||
|
||||
def add_ajustment(
|
||||
self,
|
||||
receipt_number: str,
|
||||
remaining_adjustment_amount: Decimal,
|
||||
) -> None:
|
||||
"""
|
||||
新增理算记录
|
||||
:param receipt_number: 票据号
|
||||
:param remaining_adjustment_amount: 剩余理算金额
|
||||
:return: 无
|
||||
"""
|
||||
if remaining_adjustment_amount < Decimal("0.00"):
|
||||
raise ValueError("剩余理算金额小于0")
|
||||
|
||||
# 当前时间
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
with self:
|
||||
if not self.execute(
|
||||
sql="""
|
||||
INSERT INTO adjustments
|
||||
(receipt_number, remaining_adjustment_amount, adjust_time)
|
||||
VALUES
|
||||
(?, ?, ?)
|
||||
""",
|
||||
parameters=(
|
||||
receipt_number,
|
||||
f"{remaining_adjustment_amount:.2f}",
|
||||
current_time,
|
||||
),
|
||||
):
|
||||
raise RuntimeError("新增理算记录发生异常")
|
||||
|
||||
def query_after_change_amount(
|
||||
self,
|
||||
person_policy_guid: str,
|
||||
) -> Decimal:
|
||||
|
|
@ -354,11 +433,6 @@ class MasterData(SQLite):
|
|||
:param change_amount: 变动金额
|
||||
:return: 无
|
||||
"""
|
||||
if before_change_amount != self.query_remaining_amount(
|
||||
person_policy_guid=person_policy_guid,
|
||||
):
|
||||
raise ValueError("变动前金额不等于最新一条保额变动记录的变动后金额")
|
||||
|
||||
# 变动后金额
|
||||
after_change_amount = (before_change_amount - change_amount).quantize(
|
||||
Decimal("0.00"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue