chore(skill): 清理全部 23 条历史校验警告
- 11 个超 20KB 的 SKILL.md 拆分,大章节移入 references/details.md - 7 处非标准字段(arguments/user_invocable)移入 metadata - 修 req-dev 4 条相对链接;validate 跳过行内代码避免  误报 - validate-skills.py: 23 warns -> 0 warns,0 errors
This commit is contained in:
@@ -5,9 +5,6 @@ metadata:
|
||||
invocation: user-invoked
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 财务对账技能
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **银行对账**: 银行流水 vs 系统收付款记录匹配
|
||||
@@ -481,457 +478,6 @@ def export_reconciliation_to_excel(
|
||||
|
||||
---
|
||||
|
||||
## 常见对账场景
|
||||
|
||||
### 场景1: 月末银行对账
|
||||
|
||||
```python
|
||||
# 1. 加载数据
|
||||
bank_df = load_bank_statement('银行流水_202601.xlsx')
|
||||
system_df = load_system_records('收付款记录_202601.xlsx')
|
||||
|
||||
# 2. 执行对账
|
||||
result = reconcile(bank_df, system_df)
|
||||
|
||||
# 3. 查看汇总
|
||||
print(f"已匹配: {result['summary']['matched_count']} 笔")
|
||||
print(f"银行未达: {result['summary']['bank_only_count']} 笔, 金额 {result['summary']['bank_only_amount']:,.2f}")
|
||||
print(f"企业未达: {result['summary']['system_only_count']} 笔, 金额 {result['summary']['system_only_amount']:,.2f}")
|
||||
|
||||
# 4. 生成调节表
|
||||
report = generate_bank_reconciliation_report(
|
||||
bank_balance=150000.00, # 银行对账单余额
|
||||
book_balance=145000.00, # 企业账面余额
|
||||
bank_only=result['bank_only'],
|
||||
system_only=result['system_only'],
|
||||
as_of_date='2026-01-31'
|
||||
)
|
||||
print(report)
|
||||
|
||||
# 5. 导出 Excel
|
||||
export_reconciliation_to_excel(result, '银行对账_202601.xlsx', '2026-01-31')
|
||||
```
|
||||
|
||||
### 场景2: 客户对账
|
||||
|
||||
```python
|
||||
# 加载客户交易明细
|
||||
ar_transactions = pd.read_excel('应收明细.xlsx')
|
||||
|
||||
# 筛选特定客户
|
||||
customer_data = ar_transactions[ar_transactions['customer'] == '深圳XX公司']
|
||||
|
||||
# 生成对账单
|
||||
statement = generate_ar_statement(
|
||||
customer_name='深圳XX公司',
|
||||
transactions=customer_data,
|
||||
as_of_date='2026-01-31'
|
||||
)
|
||||
print(statement)
|
||||
```
|
||||
|
||||
### 场景3: 账龄分析
|
||||
|
||||
```python
|
||||
# 加载未收款数据
|
||||
receivables = pd.read_excel('应收账款.xlsx')
|
||||
|
||||
# 账龄分析
|
||||
aging = aging_analysis(
|
||||
receivables=receivables,
|
||||
as_of_date='2026-01-31',
|
||||
aging_buckets=[30, 60, 90, 180, 365]
|
||||
)
|
||||
print(aging)
|
||||
|
||||
# 导出
|
||||
aging.to_excel('账龄分析_202601.xlsx')
|
||||
```
|
||||
|
||||
### 场景4: 扫描单据OCR识别与对账匹配(诉讼证据核对)
|
||||
|
||||
适用于:承运商/供应商提供扫描件作为诉讼证据,需要与我司对账单进行匹配核对。
|
||||
|
||||
```python
|
||||
from pdf2image import convert_from_path
|
||||
import pytesseract
|
||||
import pandas as pd
|
||||
import re
|
||||
|
||||
def ocr_pdf_extract_orders(pdf_path: str, dpi: int = 150) -> pd.DataFrame:
|
||||
"""
|
||||
从 PDF 扫描件中 OCR 提取订单信息
|
||||
|
||||
Args:
|
||||
pdf_path: PDF 文件路径
|
||||
dpi: 图像分辨率,越高识别越准但速度越慢
|
||||
|
||||
Returns:
|
||||
DataFrame 包含 order_no, page, tc_no 等字段
|
||||
"""
|
||||
print(f"正在转换 PDF...")
|
||||
images = convert_from_path(pdf_path, dpi=dpi)
|
||||
print(f"共 {len(images)} 页,开始 OCR 识别...")
|
||||
|
||||
all_records = []
|
||||
for i, img in enumerate(images, 1):
|
||||
if i % 10 == 0:
|
||||
print(f" 处理进度: {i}/{len(images)}")
|
||||
|
||||
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
|
||||
|
||||
# 提取多种订单号格式(根据实际业务调整)
|
||||
hm_orders = re.findall(r'(HM\d{12,15})(?:-\d+)?', text) # 华住订单
|
||||
hpo_orders = re.findall(r'(HPO-\d{8}-\d+)', text) # 华住采购订单
|
||||
fy_orders = re.findall(r'(FY\d{9,})', text) # 凤悦订单
|
||||
phgco_orders = re.findall(r'(PHGCO\d{9,})', text) # 锦江订单
|
||||
tc_orders = re.findall(r'(TC\d{8,})', text) # 运单号
|
||||
|
||||
# 提取日期
|
||||
dates = re.findall(r'(\d{4}-\d{2}-\d{2})', text)
|
||||
|
||||
all_orders = hm_orders + hpo_orders + fy_orders + phgco_orders
|
||||
for order in all_orders:
|
||||
all_records.append({
|
||||
'order_no': order,
|
||||
'page': i,
|
||||
'tc_no': tc_orders[0] if tc_orders else None,
|
||||
'date': dates[0] if dates else None,
|
||||
'raw_text': text[:500] # 保留部分原文用于核查
|
||||
})
|
||||
|
||||
df = pd.DataFrame(all_records)
|
||||
df = df.drop_duplicates(subset=['order_no'])
|
||||
print(f"提取完成,共 {len(df)} 条唯一订单")
|
||||
return df
|
||||
|
||||
|
||||
def extract_base_order_no(order_no: str) -> str:
|
||||
"""
|
||||
提取基础订单号(移除后缀如 -1, -2)
|
||||
|
||||
例如: HM202505227320-1 -> HM202505227320
|
||||
"""
|
||||
if pd.isna(order_no):
|
||||
return None
|
||||
order_str = str(order_no)
|
||||
match = re.match(r'(HM\d{12,15}|HPO-\d{8}-\d+|FY\d{9,}|PHGCO\d{9,})', order_str)
|
||||
return match.group(1) if match else order_str
|
||||
|
||||
|
||||
def reconcile_scanned_with_excel(
|
||||
scanned_df: pd.DataFrame,
|
||||
excel_path: str,
|
||||
main_sheet: str,
|
||||
pending_sheet: str = None,
|
||||
order_column: str = '华住/锦江订单号'
|
||||
) -> dict:
|
||||
"""
|
||||
扫描单据与 Excel 对账单匹配
|
||||
|
||||
Args:
|
||||
scanned_df: OCR 提取的订单 DataFrame
|
||||
excel_path: Excel 对账单路径
|
||||
main_sheet: 主数据 Sheet 名称
|
||||
pending_sheet: 待结算/次月结 Sheet 名称(可选)
|
||||
order_column: 订单号列名
|
||||
|
||||
Returns:
|
||||
{
|
||||
'matched_main': 在主表中匹配的订单,
|
||||
'matched_pending': 在待结算表中匹配的订单,
|
||||
'not_found': 未找到的订单,
|
||||
'summary': 汇总统计
|
||||
}
|
||||
"""
|
||||
# 加载主表
|
||||
df_main = pd.read_excel(excel_path, sheet_name=main_sheet)
|
||||
# 处理表头(如果第一行是表头)
|
||||
if df_main.iloc[0].astype(str).str.contains(order_column).any():
|
||||
df_main.columns = df_main.iloc[0]
|
||||
df_main = df_main.iloc[1:].reset_index(drop=True)
|
||||
|
||||
# 提取基础订单号
|
||||
main_orders = set(df_main[order_column].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 加载待结算表(如有)
|
||||
pending_orders = set()
|
||||
df_pending = None
|
||||
if pending_sheet:
|
||||
try:
|
||||
df_pending = pd.read_excel(excel_path, sheet_name=pending_sheet)
|
||||
pending_orders = set(df_pending[order_column].apply(extract_base_order_no).dropna())
|
||||
except:
|
||||
pass
|
||||
|
||||
all_excel_orders = main_orders | pending_orders
|
||||
|
||||
# 扫描件订单号
|
||||
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 匹配分析
|
||||
matched_main = scan_orders & main_orders
|
||||
matched_pending = scan_orders & pending_orders
|
||||
not_found = scan_orders - all_excel_orders
|
||||
|
||||
# 汇总
|
||||
summary = {
|
||||
'scan_total': len(scan_orders),
|
||||
'matched_main_count': len(matched_main),
|
||||
'matched_pending_count': len(matched_pending),
|
||||
'not_found_count': len(not_found),
|
||||
'match_rate': (len(matched_main) + len(matched_pending)) / len(scan_orders) * 100 if scan_orders else 0
|
||||
}
|
||||
|
||||
return {
|
||||
'matched_main': matched_main,
|
||||
'matched_pending': matched_pending,
|
||||
'not_found': not_found,
|
||||
'summary': summary,
|
||||
'df_main': df_main,
|
||||
'df_pending': df_pending
|
||||
}
|
||||
|
||||
|
||||
def generate_litigation_reconciliation_report(
|
||||
result: dict,
|
||||
scanned_df: pd.DataFrame,
|
||||
output_path: str
|
||||
):
|
||||
"""
|
||||
生成诉讼对账分析报告
|
||||
"""
|
||||
summary = result['summary']
|
||||
|
||||
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
||||
# Sheet1: 汇总
|
||||
summary_df = pd.DataFrame({
|
||||
'项目': [
|
||||
'扫描件订单数',
|
||||
'在主对账单中找到',
|
||||
'在待结算表中找到',
|
||||
'完全未找到',
|
||||
'匹配率'
|
||||
],
|
||||
'数值': [
|
||||
summary['scan_total'],
|
||||
summary['matched_main_count'],
|
||||
summary['matched_pending_count'],
|
||||
summary['not_found_count'],
|
||||
f"{summary['match_rate']:.1f}%"
|
||||
]
|
||||
})
|
||||
summary_df.to_excel(writer, sheet_name='对账汇总', index=False)
|
||||
|
||||
# Sheet2: 未找到的订单
|
||||
if result['not_found']:
|
||||
not_found_df = scanned_df[
|
||||
scanned_df['order_no'].apply(extract_base_order_no).isin(result['not_found'])
|
||||
][['order_no', 'page', 'tc_no', 'date']]
|
||||
not_found_df.columns = ['订单号', 'PDF页码', '运单号', '日期']
|
||||
not_found_df.to_excel(writer, sheet_name='未找到订单', index=False)
|
||||
|
||||
# Sheet3: 待结算订单详情
|
||||
if result['matched_pending'] and result['df_pending'] is not None:
|
||||
pending_detail = result['df_pending'][
|
||||
result['df_pending']['华住/锦江订单号'].apply(extract_base_order_no).isin(result['matched_pending'])
|
||||
]
|
||||
pending_detail.to_excel(writer, sheet_name='待结算订单', index=False)
|
||||
|
||||
print(f"报告已保存: {output_path}")
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```python
|
||||
# 1. 从 PDF 扫描件提取订单
|
||||
pdf_path = "承运商证据_6月业务.pdf"
|
||||
scanned_df = ocr_pdf_extract_orders(pdf_path, dpi=150)
|
||||
|
||||
# 2. 与 Excel 对账单匹配
|
||||
result = reconcile_scanned_with_excel(
|
||||
scanned_df=scanned_df,
|
||||
excel_path="对账单_2025年6月.xlsx",
|
||||
main_sheet="2025.6",
|
||||
pending_sheet="次月结",
|
||||
order_column="华住/锦江订单号"
|
||||
)
|
||||
|
||||
# 3. 查看匹配结果
|
||||
print(f"匹配率: {result['summary']['match_rate']:.1f}%")
|
||||
print(f"未找到: {result['summary']['not_found_count']} 条")
|
||||
|
||||
if result['not_found']:
|
||||
print("\n未找到的订单:")
|
||||
for order in result['not_found']:
|
||||
print(f" {order}")
|
||||
|
||||
# 4. 生成报告
|
||||
generate_litigation_reconciliation_report(
|
||||
result=result,
|
||||
scanned_df=scanned_df,
|
||||
output_path="诉讼对账分析报告.xlsx"
|
||||
)
|
||||
```
|
||||
|
||||
#### OCR 识别注意事项
|
||||
|
||||
1. **环境依赖**:
|
||||
```bash
|
||||
# macOS
|
||||
brew install tesseract tesseract-lang poppler
|
||||
pip install pdf2image pytesseract pandas openpyxl
|
||||
```
|
||||
|
||||
2. **识别精度**:
|
||||
- DPI 150 适合快速扫描,DPI 200-250 适合精确识别
|
||||
- 中文识别需要 `chi_sim` 语言包
|
||||
|
||||
3. **订单号格式差异**:
|
||||
- PDF 中可能是 `HM202505227320`
|
||||
- Excel 中可能是 `HM202505227320-1`、`HM202505227320-2`(带后缀)
|
||||
- 匹配时应使用基础订单号
|
||||
|
||||
4. **常见问题**:
|
||||
- 空白页(签收单背面)会被自动跳过
|
||||
- OCR 可能误识别字符(如 5→S, 0→O),建议人工抽查
|
||||
- 大 PDF 处理较慢,可先用低 DPI 预览
|
||||
|
||||
5. **"次月结"/"待结算" 订单**:
|
||||
- 这类订单通常标记为"没有报价"、"未建单"等
|
||||
- 虽然在系统中有记录,但可能未计入正式结算金额
|
||||
- 诉讼中需特别关注这类订单的状态
|
||||
|
||||
6. **多承运商订单归属校验(重要风险点)**:
|
||||
- 承运商可能将其他公司的订单混入证据,企图多收运费
|
||||
- 必须检查PDF订单是否出现在其他承运商的对账单中
|
||||
- 如果订单同时出现在多个承运商对账单中,则为异常订单
|
||||
|
||||
#### 多承运商订单归属校验
|
||||
|
||||
```python
|
||||
def check_order_ownership(
|
||||
scanned_df: pd.DataFrame,
|
||||
my_company_excel: str,
|
||||
other_carrier_excels: list[str],
|
||||
order_column: str = '华住/锦江订单号'
|
||||
) -> dict:
|
||||
"""
|
||||
检查订单归属,识别是否有其他承运商的订单被混入
|
||||
|
||||
Args:
|
||||
scanned_df: OCR 提取的订单 DataFrame
|
||||
my_company_excel: 我司对账单路径
|
||||
other_carrier_excels: 其他承运商对账单路径列表
|
||||
order_column: 订单号列名
|
||||
|
||||
Returns:
|
||||
{
|
||||
'my_orders': 属于我司的订单,
|
||||
'other_carrier_orders': 属于其他承运商的订单(异常),
|
||||
'disputed_orders': 同时出现在多处的争议订单,
|
||||
'summary': 汇总
|
||||
}
|
||||
"""
|
||||
# 提取PDF订单
|
||||
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 加载我司对账单订单
|
||||
my_orders = set()
|
||||
try:
|
||||
xl = pd.ExcelFile(my_company_excel)
|
||||
for sheet in xl.sheet_names:
|
||||
df = pd.read_excel(my_company_excel, sheet_name=sheet)
|
||||
for col in df.columns:
|
||||
if '订单' in str(col):
|
||||
orders = df[col].apply(extract_base_order_no).dropna()
|
||||
my_orders.update(orders)
|
||||
except Exception as e:
|
||||
print(f"加载我司对账单失败: {e}")
|
||||
|
||||
# 加载其他承运商对账单订单
|
||||
other_orders = {} # {carrier_name: set of orders}
|
||||
for excel_path in other_carrier_excels:
|
||||
carrier_name = excel_path.split('/')[-1]
|
||||
carrier_orders = set()
|
||||
try:
|
||||
xl = pd.ExcelFile(excel_path)
|
||||
for sheet in xl.sheet_names:
|
||||
df = pd.read_excel(excel_path, sheet_name=sheet)
|
||||
for col in df.columns:
|
||||
if '订单' in str(col):
|
||||
orders = df[col].apply(extract_base_order_no).dropna()
|
||||
carrier_orders.update(orders)
|
||||
other_orders[carrier_name] = carrier_orders
|
||||
except:
|
||||
pass
|
||||
|
||||
# 分析订单归属
|
||||
all_other_orders = set()
|
||||
for orders in other_orders.values():
|
||||
all_other_orders.update(orders)
|
||||
|
||||
# 分类
|
||||
only_mine = scan_orders & my_orders - all_other_orders # 仅在我司
|
||||
only_others = scan_orders & all_other_orders - my_orders # 仅在其他承运商(异常!)
|
||||
in_both = scan_orders & my_orders & all_other_orders # 同时出现(争议)
|
||||
nowhere = scan_orders - my_orders - all_other_orders # 哪里都没有
|
||||
|
||||
# 识别异常订单属于哪个承运商
|
||||
abnormal_details = []
|
||||
for order in only_others:
|
||||
for carrier, orders in other_orders.items():
|
||||
if order in orders:
|
||||
page = scanned_df[scanned_df['order_no'].apply(extract_base_order_no) == order]['page'].values
|
||||
abnormal_details.append({
|
||||
'order_no': order,
|
||||
'belongs_to': carrier,
|
||||
'pdf_page': page[0] if len(page) > 0 else None
|
||||
})
|
||||
|
||||
summary = {
|
||||
'scan_total': len(scan_orders),
|
||||
'my_orders_count': len(only_mine),
|
||||
'other_carrier_count': len(only_others), # 异常订单数
|
||||
'disputed_count': len(in_both),
|
||||
'not_found_count': len(nowhere)
|
||||
}
|
||||
|
||||
return {
|
||||
'my_orders': only_mine,
|
||||
'other_carrier_orders': only_others,
|
||||
'abnormal_details': abnormal_details,
|
||||
'disputed_orders': in_both,
|
||||
'not_found': nowhere,
|
||||
'summary': summary
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用示例(多承运商校验)
|
||||
|
||||
```python
|
||||
# 检查订单归属
|
||||
result = check_order_ownership(
|
||||
scanned_df=scanned_df,
|
||||
my_company_excel="智慧云彩对账单_2025年6月.xlsx",
|
||||
other_carrier_excels=[
|
||||
"友利速运对账单_2025年6月.xlsx",
|
||||
"其他承运商对账单.xlsx"
|
||||
]
|
||||
)
|
||||
|
||||
# 检查是否有异常订单
|
||||
if result['summary']['other_carrier_count'] > 0:
|
||||
print(f"⚠️ 发现 {result['summary']['other_carrier_count']} 个异常订单!")
|
||||
print("这些订单属于其他承运商,不应由我司支付:")
|
||||
for item in result['abnormal_details']:
|
||||
print(f" {item['order_no']} -> 属于 {item['belongs_to']} (PDF第{item['pdf_page']}页)")
|
||||
else:
|
||||
print("✓ 未发现异常订单,所有订单归属正确")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据格式统一**: 确保日期、金额格式一致后再对账
|
||||
@@ -949,3 +495,9 @@ else:
|
||||
- **data-excel**: 处理复杂的 Excel 数据转换
|
||||
- **feishu**: 将对账报告保存到飞书云文档
|
||||
- **siyuan**: 记录对账工作日志到思源笔记
|
||||
|
||||
## 详细内容
|
||||
|
||||
以下章节已移到 [references/details.md](references/details.md):
|
||||
|
||||
- 常见对账场景
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
# skills 详细内容
|
||||
|
||||
## 常见对账场景
|
||||
|
||||
### 场景1: 月末银行对账
|
||||
|
||||
```python
|
||||
# 1. 加载数据
|
||||
bank_df = load_bank_statement('银行流水_202601.xlsx')
|
||||
system_df = load_system_records('收付款记录_202601.xlsx')
|
||||
|
||||
# 2. 执行对账
|
||||
result = reconcile(bank_df, system_df)
|
||||
|
||||
# 3. 查看汇总
|
||||
print(f"已匹配: {result['summary']['matched_count']} 笔")
|
||||
print(f"银行未达: {result['summary']['bank_only_count']} 笔, 金额 {result['summary']['bank_only_amount']:,.2f}")
|
||||
print(f"企业未达: {result['summary']['system_only_count']} 笔, 金额 {result['summary']['system_only_amount']:,.2f}")
|
||||
|
||||
# 4. 生成调节表
|
||||
report = generate_bank_reconciliation_report(
|
||||
bank_balance=150000.00, # 银行对账单余额
|
||||
book_balance=145000.00, # 企业账面余额
|
||||
bank_only=result['bank_only'],
|
||||
system_only=result['system_only'],
|
||||
as_of_date='2026-01-31'
|
||||
)
|
||||
print(report)
|
||||
|
||||
# 5. 导出 Excel
|
||||
export_reconciliation_to_excel(result, '银行对账_202601.xlsx', '2026-01-31')
|
||||
```
|
||||
|
||||
### 场景2: 客户对账
|
||||
|
||||
```python
|
||||
# 加载客户交易明细
|
||||
ar_transactions = pd.read_excel('应收明细.xlsx')
|
||||
|
||||
# 筛选特定客户
|
||||
customer_data = ar_transactions[ar_transactions['customer'] == '深圳XX公司']
|
||||
|
||||
# 生成对账单
|
||||
statement = generate_ar_statement(
|
||||
customer_name='深圳XX公司',
|
||||
transactions=customer_data,
|
||||
as_of_date='2026-01-31'
|
||||
)
|
||||
print(statement)
|
||||
```
|
||||
|
||||
### 场景3: 账龄分析
|
||||
|
||||
```python
|
||||
# 加载未收款数据
|
||||
receivables = pd.read_excel('应收账款.xlsx')
|
||||
|
||||
# 账龄分析
|
||||
aging = aging_analysis(
|
||||
receivables=receivables,
|
||||
as_of_date='2026-01-31',
|
||||
aging_buckets=[30, 60, 90, 180, 365]
|
||||
)
|
||||
print(aging)
|
||||
|
||||
# 导出
|
||||
aging.to_excel('账龄分析_202601.xlsx')
|
||||
```
|
||||
|
||||
### 场景4: 扫描单据OCR识别与对账匹配(诉讼证据核对)
|
||||
|
||||
适用于:承运商/供应商提供扫描件作为诉讼证据,需要与我司对账单进行匹配核对。
|
||||
|
||||
```python
|
||||
from pdf2image import convert_from_path
|
||||
import pytesseract
|
||||
import pandas as pd
|
||||
import re
|
||||
|
||||
def ocr_pdf_extract_orders(pdf_path: str, dpi: int = 150) -> pd.DataFrame:
|
||||
"""
|
||||
从 PDF 扫描件中 OCR 提取订单信息
|
||||
|
||||
Args:
|
||||
pdf_path: PDF 文件路径
|
||||
dpi: 图像分辨率,越高识别越准但速度越慢
|
||||
|
||||
Returns:
|
||||
DataFrame 包含 order_no, page, tc_no 等字段
|
||||
"""
|
||||
print(f"正在转换 PDF...")
|
||||
images = convert_from_path(pdf_path, dpi=dpi)
|
||||
print(f"共 {len(images)} 页,开始 OCR 识别...")
|
||||
|
||||
all_records = []
|
||||
for i, img in enumerate(images, 1):
|
||||
if i % 10 == 0:
|
||||
print(f" 处理进度: {i}/{len(images)}")
|
||||
|
||||
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
|
||||
|
||||
# 提取多种订单号格式(根据实际业务调整)
|
||||
hm_orders = re.findall(r'(HM\d{12,15})(?:-\d+)?', text) # 华住订单
|
||||
hpo_orders = re.findall(r'(HPO-\d{8}-\d+)', text) # 华住采购订单
|
||||
fy_orders = re.findall(r'(FY\d{9,})', text) # 凤悦订单
|
||||
phgco_orders = re.findall(r'(PHGCO\d{9,})', text) # 锦江订单
|
||||
tc_orders = re.findall(r'(TC\d{8,})', text) # 运单号
|
||||
|
||||
# 提取日期
|
||||
dates = re.findall(r'(\d{4}-\d{2}-\d{2})', text)
|
||||
|
||||
all_orders = hm_orders + hpo_orders + fy_orders + phgco_orders
|
||||
for order in all_orders:
|
||||
all_records.append({
|
||||
'order_no': order,
|
||||
'page': i,
|
||||
'tc_no': tc_orders[0] if tc_orders else None,
|
||||
'date': dates[0] if dates else None,
|
||||
'raw_text': text[:500] # 保留部分原文用于核查
|
||||
})
|
||||
|
||||
df = pd.DataFrame(all_records)
|
||||
df = df.drop_duplicates(subset=['order_no'])
|
||||
print(f"提取完成,共 {len(df)} 条唯一订单")
|
||||
return df
|
||||
|
||||
|
||||
def extract_base_order_no(order_no: str) -> str:
|
||||
"""
|
||||
提取基础订单号(移除后缀如 -1, -2)
|
||||
|
||||
例如: HM202505227320-1 -> HM202505227320
|
||||
"""
|
||||
if pd.isna(order_no):
|
||||
return None
|
||||
order_str = str(order_no)
|
||||
match = re.match(r'(HM\d{12,15}|HPO-\d{8}-\d+|FY\d{9,}|PHGCO\d{9,})', order_str)
|
||||
return match.group(1) if match else order_str
|
||||
|
||||
|
||||
def reconcile_scanned_with_excel(
|
||||
scanned_df: pd.DataFrame,
|
||||
excel_path: str,
|
||||
main_sheet: str,
|
||||
pending_sheet: str = None,
|
||||
order_column: str = '华住/锦江订单号'
|
||||
) -> dict:
|
||||
"""
|
||||
扫描单据与 Excel 对账单匹配
|
||||
|
||||
Args:
|
||||
scanned_df: OCR 提取的订单 DataFrame
|
||||
excel_path: Excel 对账单路径
|
||||
main_sheet: 主数据 Sheet 名称
|
||||
pending_sheet: 待结算/次月结 Sheet 名称(可选)
|
||||
order_column: 订单号列名
|
||||
|
||||
Returns:
|
||||
{
|
||||
'matched_main': 在主表中匹配的订单,
|
||||
'matched_pending': 在待结算表中匹配的订单,
|
||||
'not_found': 未找到的订单,
|
||||
'summary': 汇总统计
|
||||
}
|
||||
"""
|
||||
# 加载主表
|
||||
df_main = pd.read_excel(excel_path, sheet_name=main_sheet)
|
||||
# 处理表头(如果第一行是表头)
|
||||
if df_main.iloc[0].astype(str).str.contains(order_column).any():
|
||||
df_main.columns = df_main.iloc[0]
|
||||
df_main = df_main.iloc[1:].reset_index(drop=True)
|
||||
|
||||
# 提取基础订单号
|
||||
main_orders = set(df_main[order_column].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 加载待结算表(如有)
|
||||
pending_orders = set()
|
||||
df_pending = None
|
||||
if pending_sheet:
|
||||
try:
|
||||
df_pending = pd.read_excel(excel_path, sheet_name=pending_sheet)
|
||||
pending_orders = set(df_pending[order_column].apply(extract_base_order_no).dropna())
|
||||
except:
|
||||
pass
|
||||
|
||||
all_excel_orders = main_orders | pending_orders
|
||||
|
||||
# 扫描件订单号
|
||||
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 匹配分析
|
||||
matched_main = scan_orders & main_orders
|
||||
matched_pending = scan_orders & pending_orders
|
||||
not_found = scan_orders - all_excel_orders
|
||||
|
||||
# 汇总
|
||||
summary = {
|
||||
'scan_total': len(scan_orders),
|
||||
'matched_main_count': len(matched_main),
|
||||
'matched_pending_count': len(matched_pending),
|
||||
'not_found_count': len(not_found),
|
||||
'match_rate': (len(matched_main) + len(matched_pending)) / len(scan_orders) * 100 if scan_orders else 0
|
||||
}
|
||||
|
||||
return {
|
||||
'matched_main': matched_main,
|
||||
'matched_pending': matched_pending,
|
||||
'not_found': not_found,
|
||||
'summary': summary,
|
||||
'df_main': df_main,
|
||||
'df_pending': df_pending
|
||||
}
|
||||
|
||||
|
||||
def generate_litigation_reconciliation_report(
|
||||
result: dict,
|
||||
scanned_df: pd.DataFrame,
|
||||
output_path: str
|
||||
):
|
||||
"""
|
||||
生成诉讼对账分析报告
|
||||
"""
|
||||
summary = result['summary']
|
||||
|
||||
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
||||
# Sheet1: 汇总
|
||||
summary_df = pd.DataFrame({
|
||||
'项目': [
|
||||
'扫描件订单数',
|
||||
'在主对账单中找到',
|
||||
'在待结算表中找到',
|
||||
'完全未找到',
|
||||
'匹配率'
|
||||
],
|
||||
'数值': [
|
||||
summary['scan_total'],
|
||||
summary['matched_main_count'],
|
||||
summary['matched_pending_count'],
|
||||
summary['not_found_count'],
|
||||
f"{summary['match_rate']:.1f}%"
|
||||
]
|
||||
})
|
||||
summary_df.to_excel(writer, sheet_name='对账汇总', index=False)
|
||||
|
||||
# Sheet2: 未找到的订单
|
||||
if result['not_found']:
|
||||
not_found_df = scanned_df[
|
||||
scanned_df['order_no'].apply(extract_base_order_no).isin(result['not_found'])
|
||||
][['order_no', 'page', 'tc_no', 'date']]
|
||||
not_found_df.columns = ['订单号', 'PDF页码', '运单号', '日期']
|
||||
not_found_df.to_excel(writer, sheet_name='未找到订单', index=False)
|
||||
|
||||
# Sheet3: 待结算订单详情
|
||||
if result['matched_pending'] and result['df_pending'] is not None:
|
||||
pending_detail = result['df_pending'][
|
||||
result['df_pending']['华住/锦江订单号'].apply(extract_base_order_no).isin(result['matched_pending'])
|
||||
]
|
||||
pending_detail.to_excel(writer, sheet_name='待结算订单', index=False)
|
||||
|
||||
print(f"报告已保存: {output_path}")
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```python
|
||||
# 1. 从 PDF 扫描件提取订单
|
||||
pdf_path = "承运商证据_6月业务.pdf"
|
||||
scanned_df = ocr_pdf_extract_orders(pdf_path, dpi=150)
|
||||
|
||||
# 2. 与 Excel 对账单匹配
|
||||
result = reconcile_scanned_with_excel(
|
||||
scanned_df=scanned_df,
|
||||
excel_path="对账单_2025年6月.xlsx",
|
||||
main_sheet="2025.6",
|
||||
pending_sheet="次月结",
|
||||
order_column="华住/锦江订单号"
|
||||
)
|
||||
|
||||
# 3. 查看匹配结果
|
||||
print(f"匹配率: {result['summary']['match_rate']:.1f}%")
|
||||
print(f"未找到: {result['summary']['not_found_count']} 条")
|
||||
|
||||
if result['not_found']:
|
||||
print("\n未找到的订单:")
|
||||
for order in result['not_found']:
|
||||
print(f" {order}")
|
||||
|
||||
# 4. 生成报告
|
||||
generate_litigation_reconciliation_report(
|
||||
result=result,
|
||||
scanned_df=scanned_df,
|
||||
output_path="诉讼对账分析报告.xlsx"
|
||||
)
|
||||
```
|
||||
|
||||
#### OCR 识别注意事项
|
||||
|
||||
1. **环境依赖**:
|
||||
```bash
|
||||
# macOS
|
||||
brew install tesseract tesseract-lang poppler
|
||||
pip install pdf2image pytesseract pandas openpyxl
|
||||
```
|
||||
|
||||
2. **识别精度**:
|
||||
- DPI 150 适合快速扫描,DPI 200-250 适合精确识别
|
||||
- 中文识别需要 `chi_sim` 语言包
|
||||
|
||||
3. **订单号格式差异**:
|
||||
- PDF 中可能是 `HM202505227320`
|
||||
- Excel 中可能是 `HM202505227320-1`、`HM202505227320-2`(带后缀)
|
||||
- 匹配时应使用基础订单号
|
||||
|
||||
4. **常见问题**:
|
||||
- 空白页(签收单背面)会被自动跳过
|
||||
- OCR 可能误识别字符(如 5→S, 0→O),建议人工抽查
|
||||
- 大 PDF 处理较慢,可先用低 DPI 预览
|
||||
|
||||
5. **"次月结"/"待结算" 订单**:
|
||||
- 这类订单通常标记为"没有报价"、"未建单"等
|
||||
- 虽然在系统中有记录,但可能未计入正式结算金额
|
||||
- 诉讼中需特别关注这类订单的状态
|
||||
|
||||
6. **多承运商订单归属校验(重要风险点)**:
|
||||
- 承运商可能将其他公司的订单混入证据,企图多收运费
|
||||
- 必须检查PDF订单是否出现在其他承运商的对账单中
|
||||
- 如果订单同时出现在多个承运商对账单中,则为异常订单
|
||||
|
||||
#### 多承运商订单归属校验
|
||||
|
||||
```python
|
||||
def check_order_ownership(
|
||||
scanned_df: pd.DataFrame,
|
||||
my_company_excel: str,
|
||||
other_carrier_excels: list[str],
|
||||
order_column: str = '华住/锦江订单号'
|
||||
) -> dict:
|
||||
"""
|
||||
检查订单归属,识别是否有其他承运商的订单被混入
|
||||
|
||||
Args:
|
||||
scanned_df: OCR 提取的订单 DataFrame
|
||||
my_company_excel: 我司对账单路径
|
||||
other_carrier_excels: 其他承运商对账单路径列表
|
||||
order_column: 订单号列名
|
||||
|
||||
Returns:
|
||||
{
|
||||
'my_orders': 属于我司的订单,
|
||||
'other_carrier_orders': 属于其他承运商的订单(异常),
|
||||
'disputed_orders': 同时出现在多处的争议订单,
|
||||
'summary': 汇总
|
||||
}
|
||||
"""
|
||||
# 提取PDF订单
|
||||
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
|
||||
|
||||
# 加载我司对账单订单
|
||||
my_orders = set()
|
||||
try:
|
||||
xl = pd.ExcelFile(my_company_excel)
|
||||
for sheet in xl.sheet_names:
|
||||
df = pd.read_excel(my_company_excel, sheet_name=sheet)
|
||||
for col in df.columns:
|
||||
if '订单' in str(col):
|
||||
orders = df[col].apply(extract_base_order_no).dropna()
|
||||
my_orders.update(orders)
|
||||
except Exception as e:
|
||||
print(f"加载我司对账单失败: {e}")
|
||||
|
||||
# 加载其他承运商对账单订单
|
||||
other_orders = {} # {carrier_name: set of orders}
|
||||
for excel_path in other_carrier_excels:
|
||||
carrier_name = excel_path.split('/')[-1]
|
||||
carrier_orders = set()
|
||||
try:
|
||||
xl = pd.ExcelFile(excel_path)
|
||||
for sheet in xl.sheet_names:
|
||||
df = pd.read_excel(excel_path, sheet_name=sheet)
|
||||
for col in df.columns:
|
||||
if '订单' in str(col):
|
||||
orders = df[col].apply(extract_base_order_no).dropna()
|
||||
carrier_orders.update(orders)
|
||||
other_orders[carrier_name] = carrier_orders
|
||||
except:
|
||||
pass
|
||||
|
||||
# 分析订单归属
|
||||
all_other_orders = set()
|
||||
for orders in other_orders.values():
|
||||
all_other_orders.update(orders)
|
||||
|
||||
# 分类
|
||||
only_mine = scan_orders & my_orders - all_other_orders # 仅在我司
|
||||
only_others = scan_orders & all_other_orders - my_orders # 仅在其他承运商(异常!)
|
||||
in_both = scan_orders & my_orders & all_other_orders # 同时出现(争议)
|
||||
nowhere = scan_orders - my_orders - all_other_orders # 哪里都没有
|
||||
|
||||
# 识别异常订单属于哪个承运商
|
||||
abnormal_details = []
|
||||
for order in only_others:
|
||||
for carrier, orders in other_orders.items():
|
||||
if order in orders:
|
||||
page = scanned_df[scanned_df['order_no'].apply(extract_base_order_no) == order]['page'].values
|
||||
abnormal_details.append({
|
||||
'order_no': order,
|
||||
'belongs_to': carrier,
|
||||
'pdf_page': page[0] if len(page) > 0 else None
|
||||
})
|
||||
|
||||
summary = {
|
||||
'scan_total': len(scan_orders),
|
||||
'my_orders_count': len(only_mine),
|
||||
'other_carrier_count': len(only_others), # 异常订单数
|
||||
'disputed_count': len(in_both),
|
||||
'not_found_count': len(nowhere)
|
||||
}
|
||||
|
||||
return {
|
||||
'my_orders': only_mine,
|
||||
'other_carrier_orders': only_others,
|
||||
'abnormal_details': abnormal_details,
|
||||
'disputed_orders': in_both,
|
||||
'not_found': nowhere,
|
||||
'summary': summary
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用示例(多承运商校验)
|
||||
|
||||
```python
|
||||
# 检查订单归属
|
||||
result = check_order_ownership(
|
||||
scanned_df=scanned_df,
|
||||
my_company_excel="智慧云彩对账单_2025年6月.xlsx",
|
||||
other_carrier_excels=[
|
||||
"友利速运对账单_2025年6月.xlsx",
|
||||
"其他承运商对账单.xlsx"
|
||||
]
|
||||
)
|
||||
|
||||
# 检查是否有异常订单
|
||||
if result['summary']['other_carrier_count'] > 0:
|
||||
print(f"⚠️ 发现 {result['summary']['other_carrier_count']} 个异常订单!")
|
||||
print("这些订单属于其他承运商,不应由我司支付:")
|
||||
for item in result['abnormal_details']:
|
||||
print(f" {item['order_no']} -> 属于 {item['belongs_to']} (PDF第{item['pdf_page']}页)")
|
||||
else:
|
||||
print("✓ 未发现异常订单,所有订单归属正确")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user