This commit is contained in:
liubiren 2026-01-12 21:41:46 +08:00
parent b3060d111d
commit 87ac2da670
8 changed files with 3533 additions and 876 deletions

81
utils/html_render.py Normal file
View File

@ -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)}")

View File

@ -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

View File

@ -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"), # 个人自付金额

View File

@ -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)

View File

@ -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