This commit is contained in:
liubiren 2026-01-11 22:04:57 +08:00
parent 0110be4b16
commit f30b837212
11 changed files with 922 additions and 2158 deletions

View File

@ -1,4 +0,0 @@
[settings]
order_by_type = true
multi_line_output = 3
indent = " "

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.languageServer": "None"
}

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
认证器
认证器模块
"""
import hashlib

View File

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
"""通用模块"""
from typing import Any, Dict, List
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Any, Dict, List
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
import pandas
from common import masterdata, rules_engine
@ -28,17 +31,75 @@ def case_adjust(dossier: Dict[str, Any]) -> None:
return
# 赔案理算记录
adjustments = (
pandas.DataFrame(data=dossier["receipts_layer"]).assing(
adjustments=lambda dataframe: dataframe.apply(
lambda row: receipt_adjust(
row=row, liabilities=dossier["liabilities_layer"]
),
axis="columns",
) # 票据理算
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",
) # 票据理算
)
)
.explode(column="receipt_adjustments", ignore_index=True)
.pipe(
lambda dataframe: pandas.concat(
[
dataframe.drop(
[
"receipt_adjustments",
],
axis="columns",
),
pandas.json_normalize(dataframe["receipt_adjustments"]),
],
axis="columns",
)
)
)
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,
}
)
)
).explode("adjustments", ignore_index=True)
print(adjustments)
def receipt_adjust(
@ -46,72 +107,140 @@ def receipt_adjust(
) -> List[Dict[str, Any]]:
"""
理算票据
:param row: 一张票据数据
:param row: 票据数据
:param liabilities: 理算责任
:return: 理算记录
"""
# 初始化票据理算记录
adjustments = []
# 初始化剩余个人自费金额
remaining_personal_self_payment = row["personal_self_payment"]
# 初始化剩余个人自付金额
remaining_non_medical_payment = row["non_medical_payment"]
# 初始化剩余合理金额
remaining_reasonable_amount = row["reasonable_amount"]
receipt_adjustments = []
# 出险事故
accident = row["accident"]
# 出险人
accident_person = row["payer"]
# 出险日期
accident_date = row["date"]
# 初始化票据剩余可理算金额
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,
)
# 查验状态
verification = row["verification"]
# 初始化理赔责任理算金额
adjustment_amount = Decimal("0.00")
# 初始化理赔责任理赔金额
claim_amount = Decimal("0.00")
# 遍历所有理赔责任,根据出险事故、出险人、出险日期和查验状态匹配责任
# 遍历所有理赔责任,根据出险人、出险事故、查验状态和出险日期匹配理赔责任
for liability in liabilities:
if (
accident == liability["accident"]
and accident_person == liability["insured_person"]
row["payer"] in [liability["insured_person"] for liability in liabilities]
and row["accident"] == liability["accident"]
and row["verification"] in ["真票", "无法查验"]
and liability["commencement_date"]
<= accident_date
<= row["date"]
<= liability["termination_date"]
and verification == "真票"
):
# 理赔责任理算金额
adjustment_amount = (
row["personal_self_payment"]
* liability["personal_self_ratio"] # 个人自费金额
+ row["non_medical_payment"]
* liability["non_medical_ratio"] # 个人自付金额
+ row["reasonable_amount"] * liability["reasonable_ratio"] # 合理金额
).quantize(
Decimal("0.00"),
rounding=ROUND_HALF_UP,
)
# 据变动保单唯一标识查询最新一条保额变动记录的变动后金额(理赔责任的理赔保单余额)
# 个单余额
remaining_amount = masterdata.query_remaining_amount(
policy_guid=liability["policy_guid"],
)
# 理赔责任理赔金额
claim_amount = min(
remaining_amount,
adjustment_amount,
person_policy_guid=liability["person_policy_guid"],
)
# 初始化票据理算记录
adjustment = {
"liability": liability["liability"], # 理赔责任名称
"type": row["就诊类型"],
"amount": row["合理金额"],
"payable": 0.0,
# 个人自费可理算金额
personal_self_adjustable_amount = (
row["personal_self_payment"]
* liability["personal_self_ratio"]
* Decimal("0.01")
)
# 个人自付可理算金额
non_medical_adjustable_amount = (
row["non_medical_payment"]
* liability["non_medical_ratio"]
* Decimal("0.01")
)
# 合理可理算金额
reasonable_adjustable_amount = (
row["reasonable_amount"]
* liability["reasonable_ratio"]
* Decimal("0.01")
)
# 理算金额
adjustment_amount = max(
Decimal("0.00"),
min(
remaining_adjustable_amount,
remaining_amount,
adjustable_amount := (
(
personal_self_adjustable_amount
+ non_medical_adjustable_amount
+ reasonable_adjustable_amount
).quantize(
Decimal("0.00"),
rounding=ROUND_HALF_UP,
)
), # 可理算金额
),
)
# 若理算金额大于0则新增保额扣减记录
if adjustment_amount > Decimal("0.00"):
masterdata.add_coverage_change(
person_policy_guid=liability["person_policy_guid"],
before_change_amount=remaining_amount,
change_amount=adjustment_amount,
)
receipt_adjustments.append(
{
"person_policy": liability["person_policy"], # 个单号
"liability": liability["liability"], # 理赔责任名称
"personal_self_payment": row[
"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_adjustable_amount": non_medical_adjustable_amount, # 个人自付可理算金额
"reasonable_amount": row["reasonable_amount"], # 合理可理算金额
"reasonable_ratio": liability["reasonable_ratio"], # 合理部分比例
"reasonable_adjustable_amount": reasonable_adjustable_amount, # 合理可理算金额
"adjustment_amount": adjustment_amount, # 理算金额
"adjustment_explanation": f"""
1应理算金额{remaining_adjustable_amount:.2f}
2个单余额{remaining_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}%
4理算金额{adjustment_amount:.2f}即上述应理算金额个人余额和可理算金额的最小值
""".replace(
"\n", ""
).replace(
" ", ""
), # 理算说明
}
)
remaining_adjustable_amount -= adjustment_amount
# 若剩余可理算金额小于等于0则跳出循环
if remaining_adjustable_amount <= Decimal("0.00"):
break
if not receipt_adjustments:
receipt_adjustments.append(
{
"person_policy": None, # 个单号
"liability": None, # 理赔责任名称
"personal_self_payment": None, # 个人自费金额
"personal_self_ratio": None, # 个人自费比例
"personal_self_adjustable_amount": None, # 个人自费可理算金额
"non_medical_payment": None, # 个人自付金额
"non_medical_ratio": None, # 个人自付比例
"non_medical_adjustable_amount": None, # 个人自付可理算金额
"reasonable_amount": None, # 合理可理算金额
"reasonable_ratio": None, # 合理部分比例
"reasonable_adjustable_amount": None, # 合理可理算金额
"adjustment_amount": Decimal("0.00"), # 理赔责任理算金额
"adjustment_explanation": "票据不予理算", # 理算说明
}
)
return adjustments
return receipt_adjustments

View File

@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
"""通用模块"""
import sys
from pathlib import Path
import sys
from masterdata import MasterData
sys.path.append(Path(__file__).parent.parent.as_posix())
from utils.rules_engine import RulesEngine
# 实例化主数据
masterdata = MasterData(database=Path(__file__).parent / "database.db")

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -3,27 +3,27 @@
影像件处理模块
"""
import json
import re
import sys
from base64 import b64encode
from datetime import datetime
from decimal import ROUND_HALF_UP, Decimal
from decimal import Decimal, ROUND_HALF_UP
from hashlib import md5
import json
from pathlib import Path
import re
import sys
from typing import Any, Dict, List, Optional, Tuple
import cv2
import numpy
import pandas
from common import masterdata, rules_engine
from fuzzywuzzy import fuzz
from jionlp import parse_location
import numpy
import pandas
from utils.authenticator import Authenticator
from utils.request import Request
from common import masterdata, rules_engine
sys.path.append(Path(__file__).parent.parent.as_posix())
from utils.authenticator import Authenticator
from utils.request import Request
# 实例化认证器
@ -139,12 +139,10 @@ def image_read(
:return: 影像件图像数组
"""
try:
with open(image_path, "rb") as file:
image_bytes = file.read() # 读取影像件字节流
# 先将影像件字节流转为 numpy.ndarray 对象,再解码为单通道灰度图数组对象
# 先使用读取影像件,再解码为单通道灰度图数组对象
image_ndarray = cv2.imdecode(
buf=numpy.frombuffer(image_bytes, numpy.uint8), flags=cv2.IMREAD_GRAYSCALE
buf=numpy.fromfile(file=image_path, dtype=numpy.uint8),
flags=cv2.IMREAD_GRAYSCALE,
)
if image_ndarray is None:
raise RuntimeError(f"影像件不存在")
@ -600,7 +598,9 @@ def receipt_recognize(
"token": authenticator.get_token(servicer="szkt"), # 获取深圳快瞳访问令牌
"imgBase64": f"data:image/{image["image_format"].lstrip(".")};base64,{image["image_base64"]}", # 将影像件格式和BASE64编码嵌入数据统一资源标识符
},
guid=md5(string=(url + image["image_guid"]).encode("utf-8")).hexdigest().upper(),
guid=md5(string=(url + image["image_guid"]).encode("utf-8"))
.hexdigest()
.upper(),
)
# 若查验状态为真票或红票则直接整合至赔案档案
if response.get("status") == 200 and response.get("code") == 10000:

View File

@ -10,7 +10,6 @@ from pathlib import Path
from case import case_adjust
from image import image_classify, image_recognize
from jinja2 import Environment, FileSystemLoader
if __name__ == "__main__":
# 初始化文件路径
@ -20,27 +19,18 @@ if __name__ == "__main__":
folder_path = file_path / "directory"
folder_path.mkdir(parents=True, exist_ok=True) # 若文件夹路径不存在则创建
# 实例化JINJA2环境
environment = Environment(loader=FileSystemLoader(file_path))
# 添加DATE过滤器
environment.filters["date"] = lambda date: (
date.strftime("%Y-%m-%d") if date else "长期"
)
# 加载赔案档案模版
template = environment.get_template("template.html")
# 遍历文件夹中赔案文件夹并创建赔案档案
for case_path in [x for x in folder_path.iterdir() if x.is_dir()]:
# 初始化赔案档案推送至TPA时保险公司会提保险分公司名称、报案时间和影像件等TPA签收后生成赔案号
dossier = {
"report_layer": {
"report_time": datetime(
2025, 7, 25, 12, 0, 0
), # 指定报案时间,默认为 datetime对象
"case_number": case_path.stem, # 默认为赔案文件夹名称
"insurer_company": (
insurer_company := "中银保险有限公司苏州分公司"
), # 默认为中银保险有限公司苏州分公司
"report_time": datetime(
2025, 7, 25, 12, 0, 0
), # 指定报案时间,默认为 datetime对象
}, # 报案层
"images_layer": [], # 影像件层
"insured_person_layer": {}, # 出险人层

View File

@ -3,11 +3,12 @@
主数据模块
"""
import sys
from datetime import datetime
from decimal import ROUND_HALF_UP, Decimal
from decimal import Decimal, ROUND_HALF_UP
from hashlib import md5
from pathlib import Path
from typing import Any, Dict, List, Optional
import sys
from typing import Any, Dict, List
sys.path.append(Path(__file__).parent.parent.as_posix())
from utils.sqlite import SQLite
@ -104,8 +105,6 @@ class MasterData(SQLite):
non_medical_ratio TEXT NOT NULL,
--合理理算比例
reasonable_ratio TEXT NOT NULL,
--理赔保单唯一标识
claim_policy_guid TEXT NOT NULL,
--个单唯一标识
person_policy_guid TEXT NOT NULL
)
@ -128,8 +127,8 @@ class MasterData(SQLite):
after_change_amount TEXT NOT NULL,
--变动时间
change_time TEXT NOT NULL,
--变动保单唯一标识
change_policy_guid TEXT NOT NULL
--单唯一标识
person_policy_guid TEXT NOT NULL
)
"""
)
@ -200,7 +199,7 @@ class MasterData(SQLite):
liabilities.personal_self_ratio,
liabilities.non_medical_ratio,
liabilities.reasonable_ratio,
liabilities.claim_policy_guid
liabilities.person_policy_guid
FROM insured_persons
INNER JOIN person_policies
ON insured_persons.person_policy_guid = person_policies.guid
@ -212,10 +211,10 @@ class MasterData(SQLite):
INNER JOIN liabilities
ON person_policies.guid = liabilities.person_policy_guid
INNER JOIN coverage_changes
ON liabilities.claim_policy_guid = coverage_changes.change_policy_guid
ON liabilities.person_policy_guid = coverage_changes.person_policy_guid
AND coverage_changes.change_time = (SELECT MAX(change_time)
FROM coverage_changes
WHERE liabilities.claim_policy_guid = change_policy_guid)
WHERE liabilities.person_policy_guid = person_policy_guid)
WHERE group_policies.insurer_company = ?
AND insured_persons.insured_person = ?
AND insured_persons.identity_type = ?
@ -312,11 +311,11 @@ class MasterData(SQLite):
def query_remaining_amount(
self,
policy_guid: str,
person_policy_guid: str,
) -> Decimal:
"""
根据变动保单唯一标识查询最新一条保额变动记录的变动后金额
:param policy_guid: 变动保单唯一标识
根据单唯一标识查询最新一条保额变动记录的变动后金额
:param person_policy_guid: 单唯一标识
:return: 变动后金额
"""
try:
@ -325,11 +324,11 @@ class MasterData(SQLite):
sql="""
SELECT after_change_amount
FROM coverage_changes
WHERE change_policy_guid = ?
WHERE person_policy_guid = ?
ORDER BY change_time DESC
LIMIT 1;
""",
parameters=(policy_guid,),
parameters=(person_policy_guid,),
)
if not result:
raise RuntimeError("查无数据")
@ -341,3 +340,63 @@ class MasterData(SQLite):
except Exception as exception:
raise RuntimeError(f"{str(exception)}") from exception
def add_coverage_change(
self,
person_policy_guid: str,
before_change_amount: Decimal,
change_amount: Decimal,
) -> None:
"""
新增保额扣减记录
:param person_policy_guid: 个单唯一标识
:param before_change_amount: 变动前金额
: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"),
rounding=ROUND_HALF_UP,
)
if after_change_amount < Decimal("0.00"):
raise ValueError("变动后金额小于0")
# 当前时间
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
# 构建保额变动唯一标识
guid = (
md5(
string=f"{person_policy_guid} 保额扣减 {before_change_amount:.2f} {change_amount:.2f} {after_change_amount:.2f} {current_time}".encode(
"utf-8"
)
)
.hexdigest()
.upper()
)
with self:
if not self.execute(
sql="""
INSERT INTO coverage_changes
(guid, change_type, before_change_amount, change_amount, after_change_amount, change_time, person_policy_guid)
VALUES
(?, ?, ?, ?, ?, ?, ?)
""",
parameters=(
guid,
"保额扣减",
f"{before_change_amount:.2f}",
f"{change_amount:.2f}",
f"{after_change_amount:.2f}",
current_time,
person_policy_guid,
),
):
raise RuntimeError("新增保额扣减记录发生异常")

File diff suppressed because it is too large Load Diff