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 decimal import Decimal, ROUND_HALF_UP
from pathlib import Path from pathlib import Path
import sys
from typing import Any, Dict, List from typing import Any, Dict, List
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
import pandas
from common import masterdata, rules_engine 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: def case_adjust(dossier: Dict[str, Any]) -> None:
""" """
@ -30,139 +31,134 @@ def case_adjust(dossier: Dict[str, Any]) -> None:
if conclusion == "拒付": if conclusion == "拒付":
return return
# 赔案理算记录 # 就票据层按照开票日期和票据号顺序排序
receipts_adjustments = ( dossier["receipts_layer"].sort(key=lambda x: (x["date"], x["number"]))
(
pandas.DataFrame(data=dossier["receipts_layer"]).assign( # 遍历票据层内所有票据,票据理算并添加理算记录
receipt_adjustments=lambda dataframe: dataframe.apply( for idx, receipt in enumerate(dossier["receipts_layer"]):
lambda row: receipt_adjust( # 添加理算记录
row=row, liabilities=dossier["liabilities_layer"] dossier["receipts_layer"][idx]["adjustments"] = receipt_adjust(
), receipt=receipt, liabilities=dossier["liabilities_layer"]
axis="columns",
) # 票据理算
)
) )
.explode(column="receipt_adjustments", ignore_index=True) # 票据理算金额
.pipe( dossier["receipts_layer"][idx]["adjustment_amount"] = Decimal(
lambda dataframe: pandas.concat( sum(
[ [
dataframe.drop( adjustment["adjustment_amount"]
[ for adjustment in dossier["receipts_layer"][idx]["adjustments"]
"receipt_adjustments", ]
],
axis="columns",
),
pandas.json_normalize(dataframe["receipt_adjustments"]),
],
axis="columns",
) )
).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( dossier["adjustment_layer"]["adjustment_amount"] = Decimal(
{ sum(
"adjustment_amount": ( [
receipts_adjustments["adjustment_amount"] adjustment["adjustment_amount"]
.sum() for receipt in dossier["receipts_layer"]
.quantize( for adjustment in receipt["adjustments"]
Decimal("0.00"), ]
rounding=ROUND_HALF_UP, )
) ).quantize(
), # 理算金额 exp=Decimal("0.00"),
}
)
# 实例化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"),
rounding=ROUND_HALF_UP, 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: for liability in liabilities:
if ( if (
row["payer"] in [liability["insured_person"] for liability in liabilities] receipt["payer"]
and row["accident"] == liability["accident"] in [liability["insured_person"] for liability in liabilities]
and row["verification"] in ["真票", "无法查验"] and receipt["accident"] == liability["accident"]
and receipt["verification"] in ["真票", "无法查验"]
and liability["commencement_date"] and liability["commencement_date"]
<= row["date"] <= receipt["date"]
<= liability["termination_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"], person_policy_guid=liability["person_policy_guid"],
) )
# 个人自费可理算金额 # 个人自费可理算金额
personal_self_adjustable_amount = ( personal_self_adjustable_amount = (
row["personal_self_payment"] receipt["personal_self_payment"] # 个人自费金额
* liability["personal_self_ratio"] * liability["personal_self_ratio"] # 个人自费理算比例
* Decimal("0.01") * Decimal("0.01")
).quantize(
exp=Decimal("0.00"),
rounding=ROUND_HALF_UP,
) )
# 个人自付可理算金额 # 个人自付可理算金额
non_medical_adjustable_amount = ( non_medical_adjustable_amount = (
row["non_medical_payment"] receipt["non_medical_payment"] # 个人自付金额
* liability["non_medical_ratio"] * liability["non_medical_ratio"] # 个人自付理算比例
* Decimal("0.01") * Decimal("0.01")
).quantize(
exp=Decimal("0.00"),
rounding=ROUND_HALF_UP,
) )
# 合理可理算金额 # 合理可理算金额
reasonable_adjustable_amount = ( reasonable_adjustable_amount = (
row["reasonable_amount"] receipt["reasonable_amount"] # 合理金额
* liability["reasonable_ratio"] * liability["reasonable_ratio"] # 合理理算比例
* Decimal("0.01") * Decimal("0.01")
).quantize(
exp=Decimal("0.00"),
rounding=ROUND_HALF_UP,
) )
# 理算金额 # 理算金额
adjustment_amount = max( adjustment_amount = max(
Decimal("0.00"), Decimal("0.00"),
min( min(
remaining_adjustable_amount, remaining_adjustment_amount, # 剩余理算金额
remaining_amount, remaining_coverage_amount, # 个单剩余保额
adjustable_amount := ( adjustable_amount := (
( (
personal_self_adjustable_amount personal_self_adjustable_amount # 个人自费可理算金额
+ non_medical_adjustable_amount + non_medical_adjustable_amount # 个人自付可理算金额
+ reasonable_adjustable_amount + reasonable_adjustable_amount # 合理可理算金额
).quantize( ).quantize(
Decimal("0.00"), Decimal("0.00"),
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
@ -175,35 +171,44 @@ def receipt_adjust(
if adjustment_amount > Decimal("0.00"): if adjustment_amount > Decimal("0.00"):
masterdata.add_coverage_change( masterdata.add_coverage_change(
person_policy_guid=liability["person_policy_guid"], person_policy_guid=liability["person_policy_guid"],
before_change_amount=remaining_amount, before_change_amount=remaining_coverage_amount,
change_amount=adjustment_amount, change_amount=adjustment_amount,
) )
receipt_adjustments.append( adjustments.append(
{ {
"group_policy": liability["group_policy"], # 团单号
"person_policy": liability["person_policy"], # 个单号 "person_policy": liability["person_policy"], # 个单号
"liability": liability["liability"], # 理赔责任名称 "liability": liability["liability"], # 理赔责任名称
"personal_self_payment": row[ "accident": liability["accident"], # 理赔类型
"personal_self_payment": receipt[
"personal_self_payment" "personal_self_payment"
], # 个人自费金额 ], # 个人自费金额
"personal_self_ratio": liability[ "personal_self_ratio": liability[
"personal_self_ratio" "personal_self_ratio"
], # 个人自费比例 ], # 个人自费理算比例
"personal_self_adjustable_amount": personal_self_adjustable_amount, # 个人自费可理算金额 "personal_self_adjustable_amount": personal_self_adjustable_amount, # 个人自费可理算金额
"non_medical_payment": row["non_medical_payment"], # 个人自付金额 "non_medical_payment": receipt[
"non_medical_ratio": liability["non_medical_ratio"], # 个人自付比例 "non_medical_payment"
], # 个人自付金额
"non_medical_ratio": liability[
"non_medical_ratio"
], # 个人自付理算比例
"non_medical_adjustable_amount": non_medical_adjustable_amount, # 个人自付可理算金额 "non_medical_adjustable_amount": non_medical_adjustable_amount, # 个人自付可理算金额
"reasonable_amount": row["reasonable_amount"], # 合理可理算金额 "reasonable_amount": receipt["reasonable_amount"], # 合理金额
"reasonable_ratio": liability["reasonable_ratio"], # 合理部分比例 "reasonable_ratio": liability["reasonable_ratio"], # 合理理算比例
"reasonable_adjustable_amount": reasonable_adjustable_amount, # 合理可理算金额 "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_amount": adjustment_amount, # 理算金额
"adjustment_explanation": f""" "adjustment_explanation": f"""
1应理算金额{remaining_adjustable_amount:.2f} 1剩余理算金额{remaining_adjustment_amount:.2f}
2个单余额{remaining_amount:.2f} 2个单{remaining_coverage_amount:.2f}
3可理算金额{adjustable_amount:.2f}其中 3可理算金额{adjustable_amount:.2f}其中
1个人自费可理算金额{personal_self_adjustable_amount:.2f}={row['personal_self_payment']:.2f}*{liability['personal_self_ratio']:.2f}% 1个人自费可理算金额{personal_self_adjustable_amount:.2f}={receipt['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}% 2个人自付可理算金额{non_medical_adjustable_amount:.2f}={receipt['non_medical_payment']:.2f}*{liability['non_medical_ratio']:.2f}%
3合理部分可理算金额{reasonable_adjustable_amount:.2f}={row['reasonable_amount']:.2f}*{liability['reasonable_ratio']:.2f}% 3合理部分可理算金额{reasonable_adjustable_amount:.2f}={receipt['reasonable_amount']:.2f}*{liability['reasonable_ratio']:.2f}%
4理算金额{adjustment_amount:.2f}即上述应理算金额个人余额和可理算金额的最小值 4理算金额{adjustment_amount:.2f}即上述应理算金额个人余额和可理算金额的最小值
""".replace( """.replace(
"\n", "" "\n", ""
@ -213,28 +218,41 @@ def receipt_adjust(
} }
) )
remaining_adjustable_amount -= adjustment_amount remaining_adjustment_amount -= adjustment_amount
# 若剩余理算金额小于等于0则跳出循环 # 若剩余理算金额小于等于0则跳出循环
if remaining_adjustable_amount <= Decimal("0.00"): if remaining_adjustment_amount <= Decimal("0.00"):
break break
if not receipt_adjustments: if not adjustments:
receipt_adjustments.append( adjustments.append(
{ {
"group_policy": None, # 团单号
"person_policy": None, # 个单号 "person_policy": None, # 个单号
"liability": None, # 理赔责任名称 "liability": None, # 理赔责任名称
"personal_self_payment": None, # 个人自费金额 "accident": None, # 理赔类型
"personal_self_ratio": None, # 个人自费比例 "personal_self_payment": receipt[
"personal_self_payment"
], # 个人自费金额
"personal_self_ratio": None, # 个人自费理算比例
"personal_self_adjustable_amount": None, # 个人自费可理算金额 "personal_self_adjustable_amount": None, # 个人自费可理算金额
"non_medical_payment": None, # 个人自付金额 "non_medical_payment": receipt["non_medical_payment"], # 个人自付金额
"non_medical_ratio": None, # 个人自付比例 "non_medical_ratio": None, # 个人自付理算比例
"non_medical_adjustable_amount": None, # 个人自付可理算金额 "non_medical_adjustable_amount": None, # 个人自付可理算金额
"reasonable_amount": None, # 合理可理算金额 "reasonable_amount": receipt["reasonable_amount"], # 合理金额
"reasonable_ratio": None, # 合理部分比例 "reasonable_ratio": None, # 合理理算比例
"reasonable_adjustable_amount": 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": "票据不予理算", # 理算说明 "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 image_format, image_ndarray, image_size_specified=2
) )
# 将影像件添加至影像件层 # 将已分类影像件添加至影像件层
dossier["images_layer"].append( dossier["images_layer"][f"{image_index:02d}"] = {
{ "image_path": image_path.as_posix(), # 影像件路径
"image_index": f"{image_index:02d}", # 影像件编号 "image_relative_path": image_path.relative_to(image_path.parent.parent).as_posix(), # 影像件相对路径
"image_path": image_path.as_posix(), # 影像件路径 "image_format": image_format, # 影像件格式
"image_name": image_path.stem, # 影像件名称 "image_guid": image_guid, # 影像件唯一标识
"image_format": image_format, # 影像件格式 "image_base64": image_base64, # 影像件BASE64编码
"image_guid": image_guid, # 影像件唯一标识 "image_type": image_type, # 影像件类型
"image_base64": image_base64, # 影像件BASE64编码 } # 影像件编号作为键名
"image_type": image_type, # 影像件类型
}
)
def image_read( def image_read(
@ -139,7 +136,7 @@ def image_read(
:return: 影像件图像数组 :return: 影像件图像数组
""" """
try: try:
# 先使用读取影像件,再解码为单通道灰度图数组对象 # 先使用读取影像件,再解码为单通道灰度图数组对象因在windows系统中cv2.imread就包含中文的影像件路径兼容较差估使用numpy.fromfile
image_ndarray = cv2.imdecode( image_ndarray = cv2.imdecode(
buf=numpy.fromfile(file=image_path, dtype=numpy.uint8), buf=numpy.fromfile(file=image_path, dtype=numpy.uint8),
flags=cv2.IMREAD_GRAYSCALE, flags=cv2.IMREAD_GRAYSCALE,
@ -218,13 +215,15 @@ def image_compress(
def image_recognize( def image_recognize(
image_index: str,
image: Dict[str, Any], image: Dict[str, Any],
insurer_company: str, insurer_company: str,
dossier: Dict[str, Any], dossier: Dict[str, Any],
) -> None: ) -> None:
""" """
识别影像件并整合至赔案档案 识别影像件并整合至赔案档案
:param image: 影像件 :param image_index: 影像件编号
:param image: 影像件数据字典
:param insurer_company: 保险分公司 :param insurer_company: 保险分公司
:param dossier: 赔案档案 :param dossier: 赔案档案
:return: :return:
@ -237,19 +236,19 @@ def image_recognize(
"image_type": image["image_type"], "image_type": image["image_type"],
}, },
)["recognize_enabled"]: )["recognize_enabled"]:
dossier["images_layer"][image_index]["image_recognized"] = "否,无需识别"
return return
# 根据影像件类型匹配影像件识别方法 # 根据影像件类型匹配影像件识别方法
match image["image_type"]: match image["image_type"]:
case "居民户口簿":
raise RuntimeError("暂不支持居民户口簿")
case "居民身份证(国徽、头像面)" | "居民身份证(国徽面)" | "居民身份证(头像面)": case "居民身份证(国徽、头像面)" | "居民身份证(国徽面)" | "居民身份证(头像面)":
# 居民身份证识别并整合至赔案档案 # 居民身份证识别并整合至赔案档案
identity_card_recognize( identity_card_recognize(
image=image, insurer_company=insurer_company, dossier=dossier image=image, insurer_company=insurer_company, dossier=dossier
) )
case "中国港澳台地区及境外护照": case "银行卡":
raise RuntimeError("暂不支持中国港澳台地区及境外护照") # 银行卡识别并整合至赔案档案
bank_card_recognize(image=image, dossier=dossier)
case "理赔申请书": case "理赔申请书":
application_recognize( application_recognize(
image=image, insurer_company=insurer_company, dossier=dossier image=image, insurer_company=insurer_company, dossier=dossier
@ -257,11 +256,15 @@ def image_recognize(
case "增值税发票" | "医疗门诊收费票据" | "医疗住院收费票据": case "增值税发票" | "医疗门诊收费票据" | "医疗住院收费票据":
# 票据识别并整合至赔案档案 # 票据识别并整合至赔案档案
receipt_recognize( receipt_recognize(
image=image, insurer_company=insurer_company, dossier=dossier image_index=image_index,
image=image,
insurer_company=insurer_company,
dossier=dossier,
) )
case "银行卡": case _:
# 银行卡识别并整合至赔案档案 raise RuntimeError(f"影像件类型未配置影像件识别方法")
bank_card_recognize(image=image, dossier=dossier)
dossier["images_layer"][image_index]["image_recognized"] = ""
def identity_card_recognize( def identity_card_recognize(
@ -327,7 +330,7 @@ def identity_card_recognize(
} }
) )
# 根据保险分公司、被保险人、证件类型、证件号码和出险时间查询个单 # 根据保险分公司名称、被保险人姓名、证件类型、证件号码和报案时间查询被保险人的理赔责任
dossier["liabilities_layer"] = masterdata.query_liabilities( dossier["liabilities_layer"] = masterdata.query_liabilities(
insurer_company=insurer_company, insurer_company=insurer_company,
insured_person=insured_person, insured_person=insured_person,
@ -577,17 +580,22 @@ def application_recognize(
def receipt_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: ) -> None:
""" """
识别票据并整合至赔案档案 识别票据并整合至赔案档案
:param image_index: 影像件编号
:param image: 影像件 :param image: 影像件
:param insurer_company: 保险分公司 :param insurer_company: 保险分公司
:param dossier: 赔案档案 :param dossier: 赔案档案
:return: :return:
""" """
# 初始化票据数据 # 初始化票据数据
receipt = {"image_index": image["image_index"]} receipt = {"image_index": image_index, "image_path": image["image_path"]}
# 请求深圳快瞳票据查验接口(兼容增值税发票、医疗门诊/住院收费票据) # 请求深圳快瞳票据查验接口(兼容增值税发票、医疗门诊/住院收费票据)
response = request.post( response = request.post(
url=(url := "https://ai.inspirvision.cn/s/api/ocr/invoiceCheckAll"), url=(url := "https://ai.inspirvision.cn/s/api/ocr/invoiceCheckAll"),
@ -613,25 +621,29 @@ def receipt_recognize(
"真票" "真票"
if response["data"]["details"]["invoiceTypeNo"] == "0" if response["data"]["details"]["invoiceTypeNo"] == "0"
else "红票" else "红票"
), # 红票为状态为失控、作废、已红冲、部分红冲和全额红冲的票据 ), # 查验状态,红票对应查验状态为失控、作废、已红冲、部分红冲和全额红冲
"number": response["data"]["details"]["number"], "number": response["data"]["details"]["number"], # 票据号
"code": ( "code": (
response["data"]["details"]["code"] response["data"]["details"]["code"]
if response["data"]["details"]["code"] if response["data"]["details"]["code"]
else None else None
), ), # 票据代码
"date": datetime.strptime( "date": datetime.strptime(
response["data"]["details"]["date"], "%Y年%m月%d" response["data"]["details"]["date"], "%Y年%m月%d"
), # 转为日期时间datetime对象 ), # 开票日期
"verification_code": response["data"]["details"]["check_code"], "check_code": response["data"]["details"][
"check_code"
], # 校验码
"amount": Decimal( "amount": Decimal(
response["data"]["details"]["total"] response["data"]["details"]["total"]
).quantize( ).quantize(
Decimal("0.00"), Decimal("0.00"),
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
), # 深圳快瞳票据查验接口中开票金额由字符串转为Decimal保留两位小数 ), # 开票金额
"payer": response["data"]["details"]["buyer"], "payer": response["data"]["details"]["buyer"], # 出险人
"institution": response["data"]["details"]["seller"], "institution": response["data"]["details"][
"seller"
], # 购药及就医机构
"items": [ "items": [
{ {
"item": item["name"], "item": item["name"],
@ -642,13 +654,13 @@ def receipt_recognize(
) )
if item["quantity"] if item["quantity"]
else Decimal("0.00") else Decimal("0.00")
), # 深圳快瞳票据查验接口中明细单位由空字符转为None若非空字符由字符串转为Decimal保留两位小数 ),
"amount": ( "amount": (
Decimal(item["total"]) + Decimal(item["tax"]) Decimal(item["total"]) + Decimal(item["tax"])
).quantize( ).quantize(
Decimal("0.00"), Decimal("0.00"),
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
), # 深圳快瞳票据查验接口中明细的金额和税额由字符串转为Decimal保留两位小数并求和 ),
} }
for item in response["data"]["details"]["items"] for item in response["data"]["details"]["items"]
], ],
@ -689,7 +701,7 @@ def receipt_recognize(
if response["data"]["hospitalizationDate"] if response["data"]["hospitalizationDate"]
else None else None
), ),
"verification_code": response["data"]["checkCode"], "check_code": response["data"]["checkCode"],
"amount": Decimal(response["data"]["amount"]).quantize( "amount": Decimal(response["data"]["amount"]).quantize(
Decimal("0.00"), Decimal("0.00"),
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
@ -781,9 +793,7 @@ def receipt_recognize(
fuzzy_match(response["data"], "开票日期"), fuzzy_match(response["data"], "开票日期"),
"%Y年%m月%d", "%Y年%m月%d",
), ),
"verification_code": fuzzy_match( "check_code": fuzzy_match(response["data"], "校验码"),
response["data"], "校验码"
),
"amount": Decimal( "amount": Decimal(
fuzzy_match(response["data"], "小写金额").replace( fuzzy_match(response["data"], "小写金额").replace(
"¥", "" "¥", ""
@ -857,9 +867,7 @@ def receipt_recognize(
fuzzy_match(response["data"], "开票日期"), fuzzy_match(response["data"], "开票日期"),
"%Y-%m-%d", "%Y-%m-%d",
), ),
"verification_code": fuzzy_match( "check_code": fuzzy_match(response["data"], "校验码"),
response["data"], "校验码"
),
"amount": Decimal( "amount": Decimal(
fuzzy_match( fuzzy_match(
response["data"], "合计金额(小写)" response["data"], "合计金额(小写)"
@ -882,7 +890,7 @@ def receipt_recognize(
"amount": Decimal(amount).quantize( "amount": Decimal(amount).quantize(
Decimal("0.00"), Decimal("0.00"),
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
), # 深圳快瞳票据识别接口中明细的金额和税额由字符串转为Decimal保留两位小数并求和 ),
} }
for name, quantity, amount in zip( for name, quantity, amount in zip(
[ [
@ -964,7 +972,7 @@ def receipt_recognize(
if isinstance(receipt["endtime"], dict) if isinstance(receipt["endtime"], dict)
else None else None
), ),
"verification_code": fuzzy_match( "check_code": fuzzy_match(
receipt["global_detail"]["region_specific"], receipt["global_detail"]["region_specific"],
"校验码", "校验码",
), ),
@ -1063,6 +1071,8 @@ def receipt_recognize(
) )
) )
.assign( .assign(
personal_self_payment=Decimal("0.00"), # 个人自费项
non_medical_payment=Decimal("0.00"), # 个人自付项
reasonable_amount=lambda dataframe: dataframe.apply( reasonable_amount=lambda dataframe: dataframe.apply(
lambda row: Decimal( lambda row: Decimal(
# 基于扣除明细项不合理费用决策规则评估 # 基于扣除明细项不合理费用决策规则评估
@ -1080,8 +1090,8 @@ def receipt_recognize(
rounding=ROUND_HALF_UP, rounding=ROUND_HALF_UP,
), ),
axis="columns", axis="columns",
) ), # 合理项
) # 扣除明细项不合理费用 )
) )
receipt.update( receipt.update(
@ -1092,7 +1102,7 @@ def receipt_recognize(
in receipt["payer"] in receipt["payer"]
else None else None
), # 出险人姓名 ), # 出险人姓名
"accident": "药店购药", # 出险事故 "accident": "药店购药", # 理赔类型
"diagnosis": "购药拟诊", # 医疗诊断 "diagnosis": "购药拟诊", # 医疗诊断
"personal_self_payment": Decimal("0.00"), # 个人自费金额 "personal_self_payment": Decimal("0.00"), # 个人自费金额
"non_medical_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 datetime import datetime
from pathlib import Path from pathlib import Path
import re
from case import case_adjust from case import case_adjust
from image import image_classify, image_recognize from image import image_classify, image_recognize
@ -24,15 +25,16 @@ if __name__ == "__main__":
# 初始化赔案档案推送至TPA时保险公司会提保险分公司名称、报案时间和影像件等TPA签收后生成赔案号 # 初始化赔案档案推送至TPA时保险公司会提保险分公司名称、报案时间和影像件等TPA签收后生成赔案号
dossier = { dossier = {
"report_layer": { "report_layer": {
"case_number": case_path.stem, # 默认为赔案文件夹名称
"insurer_company": ( "insurer_company": (
insurer_company := "中银保险有限公司苏州分公司" insurer_company := "中银保险有限公司苏州分公司"
), # 默认为中银保险有限公司苏州分公司 ), # 保险分公司名称默认为中银保险有限公司苏州分公司
"report_time": datetime( "report_time": datetime(
2025, 7, 25, 12, 0, 0 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": {}, # 出险人层 "insured_person_layer": {}, # 出险人层
"liabilities_layer": [], # 理赔责任层 "liabilities_layer": [], # 理赔责任层
"receipts_layer": [], # 票据层 "receipts_layer": [], # 票据层
@ -43,11 +45,11 @@ if __name__ == "__main__":
for image_index, image_path in enumerate( for image_index, image_path in enumerate(
sorted( sorted(
[ [
i x
for i in case_path.glob(pattern="*") for x in case_path.glob(pattern="*")
if i.is_file() and i.suffix.lower() in [".jpg", ".jpeg", ".png"] 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, 1,
): ):
@ -57,34 +59,47 @@ if __name__ == "__main__":
) )
# 就影像件层按照影像件类型指定排序 # 就影像件层按照影像件类型指定排序
dossier["images_layer"].sort( dossier["images_layer"] = dict(
key=lambda i: [ sorted(
"居民户口簿", dossier["images_layer"].items(),
"居民身份证(国徽面)", key=lambda x: [
"居民身份证(头像面)", "居民户口簿",
"居民身份证(国徽、头像面)", "居民身份证(国徽面)",
"中国港澳台地区及境外护照", "居民身份证(头像面)",
"理赔申请书", "居民身份证(国徽、头像面)",
"增值税发票", "中国港澳台地区及境外护照",
"医疗门诊收费票据", "银行卡",
"医疗住院收费票据", "理赔申请书",
"医疗费用清单", "其它",
"银行卡", "增值税发票",
"其它", "医疗门诊收费票据",
].index(i["image_type"]) "医疗住院收费票据",
"医疗费用清单",
].index(x[1]["image_type"]),
),
) )
# 统计已分类影像件数
dossier["classified_images_counts"] = len(dossier["images_layer"])
# 遍历影像件层内影像件 # 遍历影像件层内所有影像件
for image in dossier["images_layer"]: for image_index, image in dossier["images_layer"].items():
# 识别影像件并整合至赔案档案 if re.match(pattern=r"^\d{2}$", string=image_index):
image_recognize( # 识别影像件并整合至赔案档案
image=image, image_recognize(
insurer_company=insurer_company, image_index=image_index,
dossier=dossier, 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) case_adjust(dossier=dossier)

View File

@ -8,7 +8,7 @@ from decimal import Decimal, ROUND_HALF_UP
from hashlib import md5 from hashlib import md5
from pathlib import Path from pathlib import Path
import sys import sys
from typing import Any, Dict, List from typing import Any, Dict, List, Optional
sys.path.append(Path(__file__).parent.parent.as_posix()) sys.path.append(Path(__file__).parent.parent.as_posix())
from utils.sqlite import SQLite from utils.sqlite import SQLite
@ -97,7 +97,7 @@ class MasterData(SQLite):
guid TEXT PRIMARY KEY, guid TEXT PRIMARY KEY,
--理赔责任名称 --理赔责任名称
liability TEXT NOT NULL, liability TEXT NOT NULL,
--出险事故 --理赔类型
accident TEXT NOT NULL, accident TEXT NOT NULL,
--个人自费理算比例 --个人自费理算比例
personal_self_ratio TEXT NOT NULL, personal_self_ratio TEXT NOT NULL,
@ -130,7 +130,7 @@ class MasterData(SQLite):
--个单唯一标识 --个单唯一标识
person_policy_guid TEXT NOT NULL person_policy_guid TEXT NOT NULL
) )
""" """
) )
# 初始化购药及就医机构表 # 初始化购药及就医机构表
self.execute( 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: except Exception as exception:
raise RuntimeError(f"初始化主数据发生异常:{str(exception)}") from exception raise RuntimeError(f"初始化主数据发生异常:{str(exception)}") from exception
@ -309,7 +323,72 @@ class MasterData(SQLite):
except Exception as exception: except Exception as exception:
raise RuntimeError(f"{str(exception)}") from 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, self,
person_policy_guid: str, person_policy_guid: str,
) -> Decimal: ) -> Decimal:
@ -354,11 +433,6 @@ class MasterData(SQLite):
:param change_amount: 变动金额 :param change_amount: 变动金额
:return: :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( after_change_amount = (before_change_amount - change_amount).quantize(
Decimal("0.00"), Decimal("0.00"),

File diff suppressed because it is too large Load Diff