Files
2026-09-17 11:12:57 +08:00

122 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from domain import common, date_distance, merchant_similarity
def check(title, points, maximum, detail, issue=''):
return dict(title=title, points=points, maximum=maximum, detail=detail,
issue=issue if points < maximum else '')
def values(items, field):
return '、'.join(dict.fromkeys(value for item in items for value in item['ocr'][field])) or '未识别'
def missing_fields(invoices, payments, field):
missing = []
if not any(item['ocr'][field] for item in invoices):
missing.append('发票')
if not any(item['ocr'][field] for item in payments):
missing.append('付款截图')
return '和'.join(missing)
def date_check(invoices, payments, maximum=15, tiered=True):
distance = date_distance(invoices, payments)
missing = missing_fields(invoices, payments, 'dates')
detail = f"发票日期:{values(invoices, 'dates')};付款日期:{values(payments, 'dates')}。"
if missing:
return check('日期接近', 0, maximum, detail + f'{missing}未识别到日期,无法比较;请打开原图核对。', '日期未识别')
partial = 8 if maximum == 15 else 5
points = (maximum if distance <= 3 else partial if distance <= 7 else 0) if tiered else (maximum if distance <= 7 else 0)
rule = f'相差 03 天得 {maximum} 分,47 天得 {partial} 分,超过 7 天不得分。' if tiered else f'相差不超过 7 天得 {maximum} 分,否则不得分。'
return check('日期接近', points, maximum, detail + f'最近相差 {distance} 天。{rule}开票和付款时间可能不同,请核对实际业务日期。',
f'日期相差 {distance} 天')
def single_checks(match, hundred_point=False):
invoice, payment = match['invoices'][0], match['payments'][0]
invoices, payments = [invoice], [payment]
shared = common(invoice, payment, 'amounts')
amount_max, merchant_max, date_max, order_max, unique_max = (40, 20, 10, 15, 15) if hundred_point else (55, 25, 15, 20, 20)
checks = [check('共同金额', amount_max if shared else 0, amount_max,
'识别到共同金额:' + '、'.join('¥' + amount for amount in shared) +
'。此项比较 OCR 提取的金额,不保证它就是价税合计或实付总额,请以原图为准。', '未识别到共同金额')]
similarity = merchant_similarity(invoice, payment)
merchant_points = int(merchant_max * similarity + .5) if similarity >= .35 else 0
missing = missing_fields(invoices, payments, 'merchants')
detail = f"发票商户:{values(invoices, 'merchants')};付款商户:{values(payments, 'merchants')}。"
if missing:
detail += f'{missing}未识别到商户,无法比较。请核对原图中的销售方和收款方。'
issue = '商户未识别'
else:
detail += f'最高名称相似度 {int(similarity * 100 + .5)}%;达到 35% 才按相似度 × {merchant_max} 分四舍五入计分。名称简称、收款平台或 OCR 误识别可能影响结果,请核对原图。'
issue = '商户未充分印证'
checks.append(check('商户相似', merchant_points, merchant_max, detail, issue))
checks.append(date_check(invoices, payments, date_max))
orders = common(invoice, payment, 'orderNumbers')
missing = missing_fields(invoices, payments, 'orderNumbers')
detail = f"发票单号:{values(invoices, 'orderNumbers')};付款单号:{values(payments, 'orderNumbers')}。"
if orders:
detail += '识别到相同交易单号。'
elif missing:
detail += f'{missing}未识别到交易单号,无法交叉验证;不表示单据错误。'
else:
detail += '未找到相同交易单号,可能是不同平台的编号,请人工核对。'
checks.append(check('交易单号', order_max if orders else 0, order_max, detail, '单号未识别' if missing else '单号不一致'))
unique_reason = next((reason for reason in match['reasons'] if '在本批材料中唯一' in reason), '')
checks.append(check('批次金额唯一', unique_max if unique_reason else 0, unique_max,
unique_reason + ',获得唯一性加分。' if unique_reason else
'自动匹配时,同一金额还出现在其他发票或付款材料中,未获得唯一性加分;请核对商户、日期或单号,避免同金额串单。',
'同金额存在其他材料'))
return checks
def explain_match(match):
if match.get('matchType') != 'auto':
return None
invoices, payments = match['invoices'], match['payments']
reasons = match.get('reasons', [])
title = '单张发票与付款截图'
single_pair = False
if any(reason.startswith('同一行程多人发票合计一致') for reason in reasons):
title = '同一行程多人发票'
checks = [
check('行程及合计金额', 95, 95, '同一行程的多张发票合计与铁路付款材料一致,存在付款凭证,最近日期相差不超过 14 天。此规则基础分为 95 分。'),
check('补充凭证', 5 if len(payments) >= 2 else 0, 5,
'已归入至少两份相关付款/订单材料。' if len(payments) >= 2 else
'目前仅有一份相关付款材料,缺少第二份订单/付款材料的交叉印证,因此未获得额外 5 分。', '缺少补充凭证')
]
elif any(reason.startswith('多张发票合计一致') for reason in reasons):
title = '多张发票合并付款'
same_route = '车次、路线及行程日期一致' in reasons
checks = [
check('唯一合计组合', 70, 70, '找到唯一的多张发票组合,其合计与付款金额一致,基础得 70 分。'),
check('补充凭证', 10 if len(payments) >= 2 else 0, 10,
'至少两份相关付款/订单材料相互印证。' if len(payments) >= 2 else
'只有一份付款材料,缺少其他相关订单/付款材料印证,未获得 10 分。', '缺少补充凭证'),
date_check(invoices, payments, 10, tiered=False),
check('同一行程', 10 if same_route else 0, 10,
'已识别为出行材料,提取的车次、路线及行程日期相同。' if same_route else
'未满足“所有发票均识别为出行材料且路线、时间、车次相同”的条件,未获得 10 分;非出行类报销不适用此加分项,并不代表配对错误。',
'未获得同一行程加分')
]
elif any(reason.startswith('多笔付款合计一致') for reason in reasons):
title = '一张发票分多笔付款'
checks = [
check('唯一分笔付款组合', 95, 95, '多笔付款合计与发票中提取的某一金额一致,且组合唯一;原匹配规则固定为 95 分。'),
check('规则保留分', 0, 5, '该规则没有逐笔验证商户、日期和单号,固定保留 5 分供人工复核;不是少匹配了 5% 的金额或材料。', '分笔付款需人工复核')
]
elif len(invoices) == 1 and len(payments) == 1 and any(reason.startswith('金额一致 ¥') for reason in reasons):
single_pair = True
checks = single_checks(match)
else:
return dict(version=2, title='历史匹配记录', rawScore=match['score'], checks=[],
note='这条记录没有足够的原始评分依据,无法准确还原各项得分;保留原评分,不推测扣分原因。可查看下方原匹配依据并人工核对。')
raw_score = sum(item['points'] for item in checks)
if min(100, raw_score) != match['score']:
return dict(version=2, title=title, rawScore=match['score'], checks=[],
note='当前识别信息与历史评分不一致,无法准确还原各项得分;保留原评分及配对结果,请结合下方原匹配依据核对。')
if single_pair:
checks = single_checks(match, hundred_point=True)
return dict(version=2, title=title, rawScore=sum(item['points'] for item in checks), checks=checks,
note='每项满分相加恰好为 100 分,实际得分相加就是下方百分比;各项未得分相加就是距满分的差额。这是证据评分,不是金额或材料的匹配比例,也不是匹配成功概率;即使满分也请核对原始单据。')