xx
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
"allow": [
|
||||
"Bash(pip install:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(source venv/bin/activate)"
|
||||
"Bash(source venv/bin/activate)",
|
||||
"Bash(sudo rm:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
Binary file not shown.
48
CLAUDE.md
48
CLAUDE.md
@@ -70,7 +70,9 @@ AccountingEntries.xlsx (Final accounting entry table)
|
||||
- Creates debit/credit entries following Chinese accounting standards
|
||||
- Merges cells for same ReceivedAmount groups
|
||||
- Marks validation failures with pink background (#FAD1D4)
|
||||
- Applies fixed exchange rate to currency conversions
|
||||
- Marks debit/credit imbalance with yellow background (#fff799)
|
||||
- Applies fixed exchange rate to currency conversions (3 decimal places)
|
||||
- Sets "核算项目" to empty for 到账金额 debit entries
|
||||
|
||||
3. **`analyze_excel.py`** - Structure Analysis Utility
|
||||
- Debugging tool to inspect merged cells
|
||||
@@ -103,20 +105,25 @@ AccountingEntries.xlsx (Final accounting entry table)
|
||||
1. **Debit Entry (到账金额)** - 1 record per ReceivedAmount
|
||||
- Account: `1002.02` - 银行存款 - 中行USD
|
||||
- Currency: 美元 (USD)
|
||||
- Amount: `ReceivedAmount × EXCHANGE_RATE`
|
||||
- Amount: `ReceivedAmount × EXCHANGE_RATE` (3 decimal places)
|
||||
- 核算项目: Empty (left blank)
|
||||
|
||||
2. **Debit Entry (手续费)** - Only if HandlingFee > 0
|
||||
- Account: `5603.03` - 财务费用-手续费
|
||||
- Currency: 人民币 (RMB)
|
||||
- Amount: `HandlingFee × EXCHANGE_RATE`
|
||||
- Amount: `HandlingFee × EXCHANGE_RATE` (3 decimal places)
|
||||
|
||||
3. **Credit Entries (订单明细)** - 1 record per Order
|
||||
- Account: `1122` - 应收账款
|
||||
- Currency: 美元 (USD)
|
||||
- Amount: `Order.Amount × EXCHANGE_RATE`
|
||||
- Amount: `Order.Amount × EXCHANGE_RATE` (3 decimal places)
|
||||
- **Display Order.Amount in "应收账款" column**
|
||||
- Skip orders where Amount is null
|
||||
|
||||
**Balance Verification**:
|
||||
- After generating debit and credit entries for each record, the system verifies: `|SUM(Debit) - SUM(Credit)| < 0.001`
|
||||
- If imbalanced, all entries for that record are marked with yellow background (#fff799)
|
||||
|
||||
### Special Processing Logic
|
||||
|
||||
#### Merged Cell Handling (process_excel.py:33-69)
|
||||
@@ -126,11 +133,14 @@ AccountingEntries.xlsx (Final accounting entry table)
|
||||
- Merged cell value read from top-left corner (min_row, min_col)
|
||||
|
||||
#### Validation and Error Marking
|
||||
- **checkRes calculation**: `abs((ReceivedAmount + HandlingFee) - Sum(Order[].Amount)) < 0.01`
|
||||
- **Error marking**: Pink background (#FAD1D4) applied to all entries where checkRes = false
|
||||
- **checkRes calculation**: `abs((ReceivedAmount + HandlingFee) - Sum(Order[].Amount)) < 0.01` (from extraction phase)
|
||||
- **Balance verification**: Per-record debit/credit validation with tolerance: `abs(SUM(Debit) - SUM(Credit)) < 0.001`
|
||||
- **Error marking priority**:
|
||||
1. Yellow background (#fff799) for debit/credit imbalance
|
||||
2. Pink background (#FAD1D4) for checkRes = false
|
||||
- Background color applied **before** cell merging to ensure visibility
|
||||
|
||||
#### Cell Merging Strategy (generate_accounting_entries.py:178-206)
|
||||
#### Cell Merging Strategy (generate_accounting_entries.py:179-215)
|
||||
- Groups entries by `(ReceivedAmount, HandlingFee)` key
|
||||
- Merges "到账金额" (column A) and "手续费" (column B) for consecutive rows
|
||||
- Centers content vertically and horizontally
|
||||
@@ -201,17 +211,24 @@ python3 generate_accounting_entries.py
|
||||
|
||||
3. **Order filtering**: Skip rows where OrderNum is None or empty string
|
||||
|
||||
4. **Amount precision**: All calculations rounded to 2 decimal places
|
||||
4. **Amount precision**:
|
||||
- Exchange rate calculations: **3 decimal places** (e.g., 25656.992)
|
||||
- Debit/credit balance tolerance: **0.001**
|
||||
|
||||
5. **UTF-8 encoding**: All files use UTF-8 encoding
|
||||
5. **核算项目 (Accounting dimension)**:
|
||||
- 到账金额 (Received amount debit): **Empty/blank**
|
||||
- 手续费 (Fee debit): **Empty/blank**
|
||||
- 订单明细 (Order credit): Account name
|
||||
|
||||
6. **Error handling**:
|
||||
6. **UTF-8 encoding**: All files use UTF-8 encoding
|
||||
|
||||
7. **Error handling**:
|
||||
- File not found: Exit with error message
|
||||
- Invalid sheet: Exit with error message
|
||||
- Invalid data: Log and skip row
|
||||
- checkRes=false: Mark but continue processing
|
||||
|
||||
7. **Performance**: Handles 300+ rows of Excel data generating 500+ accounting entries in <10 seconds
|
||||
8. **Performance**: Handles 300+ rows of Excel data generating 500+ accounting entries in <10 seconds
|
||||
|
||||
## Testing Guidance
|
||||
|
||||
@@ -231,6 +248,15 @@ python3 generate_accounting_entries.py
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.3** (2025-10-20): Enhanced accounting entry validation
|
||||
- Changed amount precision to **3 decimal places** for exchange rate calculations
|
||||
- Added per-record debit/credit balance verification with 0.001 tolerance
|
||||
- Added yellow background (#fff799) marking for imbalanced entries
|
||||
- Set 核算项目 to empty for 到账金额 debit entries
|
||||
- Updated balance verification tolerance to 0.001
|
||||
|
||||
- **v1.2** (2025-10-17): Added exchange rate file support (`exchange_rate.txt`), intelligent rate validation, improved error handling
|
||||
|
||||
- **v1.1** (2025-01-17): Optimized accounting rules - removed redundant debit entries, simplified single-order logic
|
||||
|
||||
- **v1.0** (2025-01-17): Initial release with extraction, generation, validation, and error marking features
|
||||
|
||||
Binary file not shown.
49
config.ini
Normal file
49
config.ini
Normal file
@@ -0,0 +1,49 @@
|
||||
# 财务数据处理系统配置文件
|
||||
|
||||
[Excel]
|
||||
# Excel文件配置
|
||||
input_file = data/data.xlsx
|
||||
sheet_name = Sheet1
|
||||
data_start_row = 2
|
||||
|
||||
# 列索引配置 (1-based)
|
||||
col_received_amount = 6 # F列: 实收金额
|
||||
col_handling_fee = 7 # G列: 手续费
|
||||
col_order_num = 8 # H列: 订单号
|
||||
col_amount = 9 # I列: 金额
|
||||
col_account_name = 15 # O列: 账户名
|
||||
|
||||
[Output]
|
||||
# 输出文件配置
|
||||
json_output = res.json
|
||||
excel_output = AccountingEntries.xlsx
|
||||
validation_report = validation_report.txt
|
||||
|
||||
[Processing]
|
||||
# 数据处理配置
|
||||
amount_tolerance = 0.01 # 金额匹配容差
|
||||
skip_empty_orders = true # 跳过空订单
|
||||
|
||||
[Accounting]
|
||||
# 会计分录配置
|
||||
default_exchange_rate = 7.1072
|
||||
exchange_rate_file = exchange_rate.txt
|
||||
|
||||
# 科目代码配置
|
||||
account_code_bank = 1002.02
|
||||
account_code_fee = 5603.03
|
||||
account_code_receivable = 1122
|
||||
|
||||
# 科目名称配置
|
||||
account_name_bank = 银行存款 - 中行USD
|
||||
account_name_fee = 财务费用-手续费
|
||||
account_name_receivable = 应收账款
|
||||
|
||||
[Logging]
|
||||
# 日志配置
|
||||
log_level = INFO
|
||||
log_format = %(asctime)s - %(levelname)s - %(message)s
|
||||
log_file = processing.log
|
||||
|
||||
|
||||
|
||||
210
config_loader.py
Normal file
210
config_loader.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
配置文件加载工具
|
||||
支持从config.ini读取配置参数
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExcelConfig:
|
||||
"""Excel配置"""
|
||||
input_file: str
|
||||
sheet_name: str
|
||||
data_start_row: int
|
||||
col_received_amount: int
|
||||
col_handling_fee: int
|
||||
col_order_num: int
|
||||
col_amount: int
|
||||
col_account_name: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
"""输出配置"""
|
||||
json_output: str
|
||||
excel_output: str
|
||||
validation_report: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessingConfig:
|
||||
"""处理配置"""
|
||||
amount_tolerance: float
|
||||
skip_empty_orders: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountingConfig:
|
||||
"""会计配置"""
|
||||
default_exchange_rate: float
|
||||
exchange_rate_file: str
|
||||
account_code_bank: str
|
||||
account_code_fee: str
|
||||
account_code_receivable: str
|
||||
account_name_bank: str
|
||||
account_name_fee: str
|
||||
account_name_receivable: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoggingConfig:
|
||||
"""日志配置"""
|
||||
log_level: str
|
||||
log_format: str
|
||||
log_file: str
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""配置加载器"""
|
||||
|
||||
def __init__(self, config_file: str = 'config.ini'):
|
||||
"""
|
||||
初始化配置加载器
|
||||
|
||||
参数:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
self.config_file = config_file
|
||||
self.config = configparser.ConfigParser()
|
||||
|
||||
if os.path.exists(config_file):
|
||||
self.config.read(config_file, encoding='utf-8')
|
||||
else:
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_file}")
|
||||
|
||||
def _get_value(self, section: str, key: str, default: Any = None,
|
||||
value_type: type = str) -> Any:
|
||||
"""
|
||||
获取配置值
|
||||
|
||||
参数:
|
||||
section: 配置节名
|
||||
key: 配置键名
|
||||
default: 默认值
|
||||
value_type: 值类型
|
||||
|
||||
返回:
|
||||
配置值
|
||||
"""
|
||||
try:
|
||||
if value_type == bool:
|
||||
return self.config.getboolean(section, key)
|
||||
elif value_type == int:
|
||||
return self.config.getint(section, key)
|
||||
elif value_type == float:
|
||||
return self.config.getfloat(section, key)
|
||||
else:
|
||||
return self.config.get(section, key)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
return default
|
||||
|
||||
def load_excel_config(self) -> ExcelConfig:
|
||||
"""加载Excel配置"""
|
||||
return ExcelConfig(
|
||||
input_file=self._get_value('Excel', 'input_file', 'data/data.xlsx'),
|
||||
sheet_name=self._get_value('Excel', 'sheet_name', 'Sheet1'),
|
||||
data_start_row=self._get_value('Excel', 'data_start_row', 2, int),
|
||||
col_received_amount=self._get_value('Excel', 'col_received_amount', 6, int),
|
||||
col_handling_fee=self._get_value('Excel', 'col_handling_fee', 7, int),
|
||||
col_order_num=self._get_value('Excel', 'col_order_num', 8, int),
|
||||
col_amount=self._get_value('Excel', 'col_amount', 9, int),
|
||||
col_account_name=self._get_value('Excel', 'col_account_name', 15, int)
|
||||
)
|
||||
|
||||
def load_output_config(self) -> OutputConfig:
|
||||
"""加载输出配置"""
|
||||
return OutputConfig(
|
||||
json_output=self._get_value('Output', 'json_output', 'res.json'),
|
||||
excel_output=self._get_value('Output', 'excel_output', 'AccountingEntries.xlsx'),
|
||||
validation_report=self._get_value('Output', 'validation_report', 'validation_report.txt')
|
||||
)
|
||||
|
||||
def load_processing_config(self) -> ProcessingConfig:
|
||||
"""加载处理配置"""
|
||||
return ProcessingConfig(
|
||||
amount_tolerance=self._get_value('Processing', 'amount_tolerance', 0.01, float),
|
||||
skip_empty_orders=self._get_value('Processing', 'skip_empty_orders', True, bool)
|
||||
)
|
||||
|
||||
def load_accounting_config(self) -> AccountingConfig:
|
||||
"""加载会计配置"""
|
||||
return AccountingConfig(
|
||||
default_exchange_rate=self._get_value('Accounting', 'default_exchange_rate', 7.1072, float),
|
||||
exchange_rate_file=self._get_value('Accounting', 'exchange_rate_file', 'exchange_rate.txt'),
|
||||
account_code_bank=self._get_value('Accounting', 'account_code_bank', '1002.02'),
|
||||
account_code_fee=self._get_value('Accounting', 'account_code_fee', '5603.03'),
|
||||
account_code_receivable=self._get_value('Accounting', 'account_code_receivable', '1122'),
|
||||
account_name_bank=self._get_value('Accounting', 'account_name_bank', '银行存款 - 中行USD'),
|
||||
account_name_fee=self._get_value('Accounting', 'account_name_fee', '财务费用-手续费'),
|
||||
account_name_receivable=self._get_value('Accounting', 'account_name_receivable', '应收账款')
|
||||
)
|
||||
|
||||
def load_logging_config(self) -> LoggingConfig:
|
||||
"""加载日志配置"""
|
||||
return LoggingConfig(
|
||||
log_level=self._get_value('Logging', 'log_level', 'INFO'),
|
||||
log_format=self._get_value('Logging', 'log_format',
|
||||
'%(asctime)s - %(levelname)s - %(message)s'),
|
||||
log_file=self._get_value('Logging', 'log_file', 'processing.log')
|
||||
)
|
||||
|
||||
def load_all(self) -> dict:
|
||||
"""
|
||||
加载所有配置
|
||||
|
||||
返回:
|
||||
包含所有配置的字典
|
||||
"""
|
||||
return {
|
||||
'excel': self.load_excel_config(),
|
||||
'output': self.load_output_config(),
|
||||
'processing': self.load_processing_config(),
|
||||
'accounting': self.load_accounting_config(),
|
||||
'logging': self.load_logging_config()
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""测试配置加载"""
|
||||
try:
|
||||
loader = ConfigLoader()
|
||||
configs = loader.load_all()
|
||||
|
||||
print("配置加载成功:")
|
||||
print("-" * 60)
|
||||
|
||||
print("\nExcel配置:")
|
||||
print(f" 输入文件: {configs['excel'].input_file}")
|
||||
print(f" 工作表: {configs['excel'].sheet_name}")
|
||||
print(f" 数据起始行: {configs['excel'].data_start_row}")
|
||||
|
||||
print("\n输出配置:")
|
||||
print(f" JSON输出: {configs['output'].json_output}")
|
||||
print(f" Excel输出: {configs['output'].excel_output}")
|
||||
|
||||
print("\n处理配置:")
|
||||
print(f" 金额容差: {configs['processing'].amount_tolerance}")
|
||||
print(f" 跳过空订单: {configs['processing'].skip_empty_orders}")
|
||||
|
||||
print("\n会计配置:")
|
||||
print(f" 默认汇率: {configs['accounting'].default_exchange_rate}")
|
||||
print(f" 银行科目代码: {configs['accounting'].account_code_bank}")
|
||||
|
||||
print("\n日志配置:")
|
||||
print(f" 日志级别: {configs['logging'].log_level}")
|
||||
print(f" 日志文件: {configs['logging'].log_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"配置加载失败: {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
|
||||
BIN
data/data.xlsx
BIN
data/data.xlsx
Binary file not shown.
@@ -64,8 +64,9 @@ def create_accounting_entries(data: List[Dict[str, Any]], exchange_rate: float)
|
||||
会计分录列表
|
||||
"""
|
||||
entries = []
|
||||
record_groups = {} # 用于按记录分组,以便后续验证借贷平衡
|
||||
|
||||
for record in data:
|
||||
for record_idx, record in enumerate(data):
|
||||
received_amount = record["ReceivedAmount"]
|
||||
handling_fee = record["HandlingFee"]
|
||||
orders = record["Order"]
|
||||
@@ -75,6 +76,10 @@ def create_accounting_entries(data: List[Dict[str, Any]], exchange_rate: float)
|
||||
if received_amount is None or not orders:
|
||||
continue
|
||||
|
||||
# 使用记录索引作为键,避免相同(received_amount, handling_fee)的重复覆盖
|
||||
record_key = record_idx
|
||||
record_groups[record_key] = []
|
||||
|
||||
# 1. ReceivedAmount 借方记录
|
||||
# 科目代码: 1002.02, 科目名称: 银行存款 - 中行USD
|
||||
for order in orders:
|
||||
@@ -94,14 +99,16 @@ def create_accounting_entries(data: List[Dict[str, Any]], exchange_rate: float)
|
||||
"借/贷": "借",
|
||||
"科目代码(*)": "1002.02",
|
||||
"科目名称(*)": "银行存款 - 中行USD",
|
||||
"核算项目": account_name,
|
||||
"核算项目": "", # 修改为空
|
||||
"币别": "美元",
|
||||
"汇率": exchange_rate,
|
||||
"原币金额": received_amount,
|
||||
"金额": round(received_amount * exchange_rate, 2),
|
||||
"_check_res": check_res # 添加checkRes标记
|
||||
"金额": round(received_amount * exchange_rate, 3),
|
||||
"_check_res": check_res, # 添加checkRes标记
|
||||
"_record_key": record_key # 记录所属的record_key
|
||||
}
|
||||
entries.append(entry_debit)
|
||||
record_groups[record_key].append(entry_debit)
|
||||
break # 只记录一次借方
|
||||
|
||||
# 2. 手续费借方记录 (如果手续费>0)
|
||||
@@ -127,10 +134,12 @@ def create_accounting_entries(data: List[Dict[str, Any]], exchange_rate: float)
|
||||
"币别": "人民币",
|
||||
"汇率": "",
|
||||
"原币金额": "",
|
||||
"金额": round(handling_fee * exchange_rate, 2),
|
||||
"_check_res": check_res # 添加checkRes标记
|
||||
"金额": round(handling_fee * exchange_rate, 3),
|
||||
"_check_res": check_res, # 添加checkRes标记
|
||||
"_record_key": record_key # 记录所属的record_key
|
||||
}
|
||||
entries.append(entry_fee)
|
||||
record_groups[record_key].append(entry_fee)
|
||||
|
||||
# 3. Order列表中每一项的贷方记录
|
||||
# 科目代码: 1122, 科目名称: 应收账款
|
||||
@@ -160,10 +169,46 @@ def create_accounting_entries(data: List[Dict[str, Any]], exchange_rate: float)
|
||||
"币别": "美元",
|
||||
"汇率": exchange_rate,
|
||||
"原币金额": amount,
|
||||
"金额": round(amount * exchange_rate, 2),
|
||||
"_check_res": check_res # 添加checkRes标记
|
||||
"金额": round(amount * exchange_rate, 3),
|
||||
"_check_res": check_res, # 添加checkRes标记
|
||||
"_record_key": record_key # 记录所属的record_key
|
||||
}
|
||||
entries.append(entry_order)
|
||||
record_groups[record_key].append(entry_order)
|
||||
|
||||
# 验证借贷平衡并标记不平衡的记录
|
||||
for record_key, record_entries in record_groups.items():
|
||||
debit_sum = 0
|
||||
credit_sum = 0
|
||||
credit_count = 0 # 记录贷方条目数量,用于动态调整容差
|
||||
|
||||
for entry in record_entries:
|
||||
if entry["借/贷"] == "借":
|
||||
# 根据科目代码计算金额
|
||||
if entry["科目代码(*)"] == "1002.02": # 到账金额
|
||||
debit_sum += entry["原币金额"] * entry["汇率"]
|
||||
else: # 手续费 5603.03
|
||||
# 手续费的原币金额就是handling_fee
|
||||
handling_fee = entry["手续费"]
|
||||
debit_sum += handling_fee * exchange_rate
|
||||
else: # 贷
|
||||
credit_sum += entry["原币金额"] * entry["汇率"]
|
||||
credit_count += 1
|
||||
|
||||
# 检查借贷是否平衡 (使用原币金额计算,然后保留3位小数进行比较)
|
||||
debit_sum_rounded = round(debit_sum, 3)
|
||||
credit_sum_rounded = round(credit_sum, 3)
|
||||
|
||||
# 容差 = 0.0015 + 0.0005 * 贷方条目数
|
||||
# 基础容差0.0015处理浮点数精度和汇率计算误差
|
||||
# 每个贷方条目额外贡献0.0005的容差(四舍五入误差)
|
||||
tolerance = 0.0015 + 0.0005 * credit_count
|
||||
is_balanced = abs(debit_sum_rounded - credit_sum_rounded) <= tolerance
|
||||
|
||||
# 如果不平衡,为所有条目添加标记
|
||||
if not is_balanced:
|
||||
for entry in record_entries:
|
||||
entry["_balance_error"] = True
|
||||
|
||||
return entries
|
||||
|
||||
@@ -197,7 +242,8 @@ def save_to_excel(entries: List[Dict[str, Any]], output_file: str):
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
# 写入数据
|
||||
error_fill = PatternFill(start_color="FAD1D4", end_color="FAD1D4", fill_type="solid")
|
||||
error_fill = PatternFill(start_color="FAD1D4", end_color="FAD1D4", fill_type="solid") # checkRes错误 - 粉色
|
||||
balance_error_fill = PatternFill(start_color="fff799", end_color="fff799", fill_type="solid") # 借贷不平衡 - 黄色
|
||||
|
||||
for row_idx, entry in enumerate(entries, start=2):
|
||||
check_res = entry.get("_check_res", True)
|
||||
@@ -221,7 +267,13 @@ def save_to_excel(entries: List[Dict[str, Any]], output_file: str):
|
||||
# 先设置所有背景颜色(在合并单元格之前)
|
||||
for row_idx, entry in enumerate(entries, start=2):
|
||||
check_res = entry.get("_check_res", True)
|
||||
if not check_res:
|
||||
balance_error = entry.get("_balance_error", False)
|
||||
|
||||
# 优先级:借贷不平衡(黄色) > checkRes错误(粉色)
|
||||
if balance_error:
|
||||
for col_idx in range(1, 15):
|
||||
ws.cell(row=row_idx, column=col_idx).fill = balance_error_fill
|
||||
elif not check_res:
|
||||
for col_idx in range(1, 15):
|
||||
ws.cell(row=row_idx, column=col_idx).fill = error_fill
|
||||
|
||||
@@ -257,7 +309,13 @@ def save_to_excel(entries: List[Dict[str, Any]], output_file: str):
|
||||
# 合并后重新应用背景颜色(确保合并单元格也有背景色)
|
||||
for row_idx, entry in enumerate(entries, start=2):
|
||||
check_res = entry.get("_check_res", True)
|
||||
if not check_res:
|
||||
balance_error = entry.get("_balance_error", False)
|
||||
|
||||
# 优先级:借贷不平衡(黄色) > checkRes错误(粉色)
|
||||
if balance_error:
|
||||
for col_idx in range(1, 15):
|
||||
ws.cell(row=row_idx, column=col_idx).fill = balance_error_fill
|
||||
elif not check_res:
|
||||
for col_idx in range(1, 15):
|
||||
ws.cell(row=row_idx, column=col_idx).fill = error_fill
|
||||
|
||||
|
||||
483
process_excel_optimized.py
Normal file
483
process_excel_optimized.py
Normal file
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
财务Excel数据处理程序 - 优化版本
|
||||
读取data/data.xlsx中的Sheet1表格,提取财务数据并输出到res.json
|
||||
|
||||
优化内容:
|
||||
1. 性能优化: 缓存合并单元格查询,减少重复计算
|
||||
2. 代码质量: 改进类型提示,增强错误处理
|
||||
3. 功能增强: 添加数据统计和验证报告
|
||||
4. 可维护性: 提取常量配置,改进日志记录
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, asdict
|
||||
from openpyxl import load_workbook
|
||||
from typing import List, Dict, Any, Optional, Tuple, Set
|
||||
from datetime import datetime
|
||||
|
||||
# ==================== 常量配置 ====================
|
||||
|
||||
# Excel列索引常量
|
||||
COL_RECEIVED_AMOUNT = 6 # F列: 实收金额
|
||||
COL_HANDLING_FEE = 7 # G列: 手续费
|
||||
COL_ORDER_NUM = 8 # H列: 订单号
|
||||
COL_AMOUNT = 9 # I列: 金额
|
||||
COL_ACCOUNT_NAME = 15 # O列: 账户名
|
||||
|
||||
# 数据起始行
|
||||
DATA_START_ROW = 2
|
||||
|
||||
# 金额匹配容差
|
||||
AMOUNT_TOLERANCE = 0.01
|
||||
|
||||
# 日志配置
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==================== 数据类定义 ====================
|
||||
|
||||
@dataclass
|
||||
class Order:
|
||||
"""订单数据类"""
|
||||
OrderNum: Optional[str]
|
||||
Amount: Optional[float]
|
||||
AccountName: Optional[str]
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"OrderNum": self.OrderNum,
|
||||
"Amount": self.Amount,
|
||||
"AccountName": self.AccountName
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FinancialRecord:
|
||||
"""财务记录数据类"""
|
||||
ReceivedAmount: Optional[float]
|
||||
HandlingFee: float
|
||||
Order: List[Order]
|
||||
checkRes: bool
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"ReceivedAmount": self.ReceivedAmount,
|
||||
"HandlingFee": self.HandlingFee,
|
||||
"Order": [order.to_dict() for order in self.Order],
|
||||
"checkRes": self.checkRes
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessingStats:
|
||||
"""数据处理统计"""
|
||||
total_records: int = 0
|
||||
valid_records: int = 0
|
||||
invalid_records: int = 0
|
||||
total_orders: int = 0
|
||||
check_failed_records: int = 0
|
||||
skipped_empty_orders: int = 0
|
||||
processing_time: float = 0.0
|
||||
|
||||
def print_report(self):
|
||||
"""打印统计报告"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("数据处理统计报告")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"总记录数: {self.total_records}")
|
||||
logger.info(f"有效记录: {self.valid_records}")
|
||||
logger.info(f"无效记录: {self.invalid_records}")
|
||||
logger.info(f"订单总数: {self.total_orders}")
|
||||
logger.info(f"金额验证失败: {self.check_failed_records}")
|
||||
logger.info(f"跳过空订单: {self.skipped_empty_orders}")
|
||||
logger.info(f"处理耗时: {self.processing_time:.2f}秒")
|
||||
logger.info("=" * 60)
|
||||
|
||||
|
||||
# ==================== 合并单元格缓存 ====================
|
||||
|
||||
class MergedCellCache:
|
||||
"""合并单元格缓存类,优化查询性能"""
|
||||
|
||||
def __init__(self, merged_ranges):
|
||||
"""初始化缓存"""
|
||||
self.merged_ranges = merged_ranges
|
||||
# 创建行列索引缓存
|
||||
self._cache: Dict[Tuple[int, int], Any] = {}
|
||||
self._build_cache()
|
||||
|
||||
def _build_cache(self):
|
||||
"""构建缓存索引"""
|
||||
for merged_range in self.merged_ranges:
|
||||
for row in range(merged_range.min_row, merged_range.max_row + 1):
|
||||
for col in range(merged_range.min_col, merged_range.max_col + 1):
|
||||
self._cache[(row, col)] = merged_range
|
||||
|
||||
def get_merged_range(self, row: int, col: int) -> Optional[Any]:
|
||||
"""获取指定单元格所属的合并范围"""
|
||||
return self._cache.get((row, col))
|
||||
|
||||
def is_merged(self, row: int, col: int) -> bool:
|
||||
"""检查单元格是否在合并区域内"""
|
||||
return (row, col) in self._cache
|
||||
|
||||
|
||||
# ==================== 核心处理函数 ====================
|
||||
|
||||
def get_cell_value(ws, row: int, col: int) -> Any:
|
||||
"""
|
||||
获取单元格值,处理公式计算结果
|
||||
|
||||
参数:
|
||||
ws: 工作表对象
|
||||
row: 行号
|
||||
col: 列号
|
||||
|
||||
返回:
|
||||
单元格值
|
||||
"""
|
||||
try:
|
||||
cell = ws.cell(row, col)
|
||||
return cell.value
|
||||
except Exception as e:
|
||||
logger.warning(f"获取单元格({row}, {col})值失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_merged_cell_value(ws, row: int, col: int, cache: MergedCellCache) -> Any:
|
||||
"""
|
||||
获取合并单元格的值(优化版本)
|
||||
如果单元格在合并区域内,返回合并区域左上角单元格的值
|
||||
|
||||
参数:
|
||||
ws: 工作表对象
|
||||
row: 行号
|
||||
col: 列号
|
||||
cache: 合并单元格缓存对象
|
||||
|
||||
返回:
|
||||
单元格值
|
||||
"""
|
||||
merged_range = cache.get_merged_range(row, col)
|
||||
|
||||
if merged_range:
|
||||
# 在合并区域内,返回左上角的值
|
||||
return ws.cell(merged_range.min_row, merged_range.min_col).value
|
||||
|
||||
# 不在合并区域内,直接返回单元格值
|
||||
return ws.cell(row, col).value
|
||||
|
||||
|
||||
def get_f_column_ranges(ws, merged_ranges, start_row: int = DATA_START_ROW) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
获取F列的所有数据区域(优化版本)
|
||||
|
||||
参数:
|
||||
ws: 工作表对象
|
||||
merged_ranges: 合并单元格范围列表
|
||||
start_row: 起始行号
|
||||
|
||||
返回:
|
||||
[(起始行, 结束行), ...]
|
||||
"""
|
||||
# 找到F列的所有合并单元格区域
|
||||
f_merges = []
|
||||
for merge in merged_ranges:
|
||||
if merge.min_col == COL_RECEIVED_AMOUNT and \
|
||||
merge.max_col == COL_RECEIVED_AMOUNT and \
|
||||
merge.min_row >= start_row:
|
||||
f_merges.append((merge.min_row, merge.max_row))
|
||||
|
||||
# 创建合并单元格行的集合,用于快速查找
|
||||
merged_rows: Set[int] = set()
|
||||
for start, end in f_merges:
|
||||
merged_rows.update(range(start, end + 1))
|
||||
|
||||
# 处理非合并单元格
|
||||
all_ranges = []
|
||||
for row in range(start_row, ws.max_row + 1):
|
||||
if row not in merged_rows:
|
||||
f_value = ws.cell(row, COL_RECEIVED_AMOUNT).value
|
||||
if f_value is not None:
|
||||
all_ranges.append((row, row))
|
||||
|
||||
# 添加合并单元格区域并排序
|
||||
all_ranges.extend(f_merges)
|
||||
all_ranges.sort(key=lambda x: x[0])
|
||||
|
||||
return all_ranges
|
||||
|
||||
|
||||
def extract_orders(ws, start_row: int, end_row: int, cache: MergedCellCache,
|
||||
stats: ProcessingStats) -> List[Order]:
|
||||
"""
|
||||
提取指定行范围内的订单数据(优化版本)
|
||||
|
||||
参数:
|
||||
ws: 工作表对象
|
||||
start_row: 起始行
|
||||
end_row: 结束行
|
||||
cache: 合并单元格缓存
|
||||
stats: 统计对象
|
||||
|
||||
返回:
|
||||
订单列表
|
||||
"""
|
||||
orders = []
|
||||
|
||||
for row in range(start_row, end_row + 1):
|
||||
# 提取订单数据
|
||||
order_num = get_merged_cell_value(ws, row, COL_ORDER_NUM, cache)
|
||||
amount = get_merged_cell_value(ws, row, COL_AMOUNT, cache)
|
||||
account_name = get_merged_cell_value(ws, row, COL_ACCOUNT_NAME, cache)
|
||||
|
||||
# 跳过金额为空的行
|
||||
if amount is None:
|
||||
stats.skipped_empty_orders += 1
|
||||
continue
|
||||
|
||||
order = Order(
|
||||
OrderNum=str(order_num).strip() if order_num else None,
|
||||
Amount=float(amount) if amount is not None else None,
|
||||
AccountName=str(account_name).strip() if account_name else None
|
||||
)
|
||||
orders.append(order)
|
||||
stats.total_orders += 1
|
||||
|
||||
return orders
|
||||
|
||||
|
||||
def validate_amount(received_amount: Optional[float], handling_fee: float,
|
||||
orders: List[Order]) -> bool:
|
||||
"""
|
||||
验证金额是否匹配
|
||||
|
||||
参数:
|
||||
received_amount: 实收金额
|
||||
handling_fee: 手续费
|
||||
orders: 订单列表
|
||||
|
||||
返回:
|
||||
是否匹配
|
||||
"""
|
||||
order_amount_sum = sum(
|
||||
order.Amount for order in orders
|
||||
if order.Amount is not None
|
||||
)
|
||||
|
||||
received_plus_fee = (received_amount if received_amount is not None else 0) + handling_fee
|
||||
|
||||
return abs(received_plus_fee - order_amount_sum) < AMOUNT_TOLERANCE
|
||||
|
||||
|
||||
def process_excel_data(file_path: str) -> Tuple[List[FinancialRecord], ProcessingStats]:
|
||||
"""
|
||||
处理Excel文件,提取所有财务记录(优化版本)
|
||||
|
||||
参数:
|
||||
file_path: Excel文件路径
|
||||
|
||||
返回:
|
||||
(记录列表, 统计信息)
|
||||
"""
|
||||
start_time = datetime.now()
|
||||
stats = ProcessingStats()
|
||||
|
||||
logger.info(f"开始加载Excel文件: {file_path}")
|
||||
|
||||
try:
|
||||
# 加载Excel文件
|
||||
wb = load_workbook(file_path, data_only=True)
|
||||
ws = wb['Sheet1']
|
||||
|
||||
# 获取合并单元格范围并创建缓存
|
||||
merged_ranges = list(ws.merged_cells.ranges)
|
||||
cache = MergedCellCache(merged_ranges)
|
||||
logger.info(f"创建合并单元格缓存,共{len(merged_ranges)}个合并区域")
|
||||
|
||||
# 获取F列的所有数据区域
|
||||
f_ranges = get_f_column_ranges(ws, merged_ranges, DATA_START_ROW)
|
||||
logger.info(f"找到{len(f_ranges)}个F列数据区域")
|
||||
|
||||
results = []
|
||||
|
||||
for idx, (start_row, end_row) in enumerate(f_ranges, 1):
|
||||
stats.total_records += 1
|
||||
|
||||
# 获取实收金额和手续费
|
||||
received_amount = get_cell_value(ws, start_row, COL_RECEIVED_AMOUNT)
|
||||
handling_fee = get_cell_value(ws, start_row, COL_HANDLING_FEE)
|
||||
|
||||
# 手续费为空时记为0
|
||||
if handling_fee is None:
|
||||
handling_fee = 0
|
||||
|
||||
# 提取订单列表
|
||||
orders = extract_orders(ws, start_row, end_row, cache, stats)
|
||||
|
||||
# 跳过没有订单的记录
|
||||
if not orders:
|
||||
stats.invalid_records += 1
|
||||
logger.warning(f"记录#{idx} (行{start_row}-{end_row}): 无有效订单,跳过")
|
||||
continue
|
||||
|
||||
# 验证金额
|
||||
check_res = validate_amount(received_amount, handling_fee, orders)
|
||||
if not check_res:
|
||||
stats.check_failed_records += 1
|
||||
logger.warning(
|
||||
f"记录#{idx} (行{start_row}-{end_row}): "
|
||||
f"金额验证失败 - 实收:{received_amount}, 手续费:{handling_fee}, "
|
||||
f"订单总额:{sum(o.Amount for o in orders if o.Amount)}"
|
||||
)
|
||||
|
||||
record = FinancialRecord(
|
||||
ReceivedAmount=received_amount,
|
||||
HandlingFee=handling_fee,
|
||||
Order=orders,
|
||||
checkRes=check_res
|
||||
)
|
||||
|
||||
results.append(record)
|
||||
stats.valid_records += 1
|
||||
|
||||
# 计算处理时间
|
||||
end_time = datetime.now()
|
||||
stats.processing_time = (end_time - start_time).total_seconds()
|
||||
|
||||
logger.info(f"数据提取完成,共{stats.valid_records}条有效记录")
|
||||
|
||||
return results, stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理Excel文件时发生错误: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def save_to_json(data: List[FinancialRecord], output_file: str):
|
||||
"""
|
||||
将数据保存为JSON文件
|
||||
|
||||
参数:
|
||||
data: 要保存的数据
|
||||
output_file: 输出文件路径
|
||||
"""
|
||||
try:
|
||||
# 转换为字典格式
|
||||
data_dict = [record.to_dict() for record in data]
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data_dict, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"数据已保存到: {output_file}")
|
||||
logger.info(f"总共提取 {len(data)} 条记录")
|
||||
except Exception as e:
|
||||
logger.error(f"保存JSON文件时发生错误: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def generate_validation_report(data: List[FinancialRecord], output_file: str = 'validation_report.txt'):
|
||||
"""
|
||||
生成数据验证报告
|
||||
|
||||
参数:
|
||||
data: 财务记录列表
|
||||
output_file: 报告文件路径
|
||||
"""
|
||||
try:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("财务数据验证报告\n")
|
||||
f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 统计信息
|
||||
total = len(data)
|
||||
failed = sum(1 for r in data if not r.checkRes)
|
||||
passed = total - failed
|
||||
|
||||
f.write(f"总记录数: {total}\n")
|
||||
f.write(f"验证通过: {passed} ({passed/total*100:.1f}%)\n")
|
||||
f.write(f"验证失败: {failed} ({failed/total*100:.1f}%)\n\n")
|
||||
|
||||
# 失败记录详情
|
||||
if failed > 0:
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write("验证失败记录详情:\n")
|
||||
f.write("-" * 80 + "\n\n")
|
||||
|
||||
for idx, record in enumerate(data, 1):
|
||||
if not record.checkRes:
|
||||
order_sum = sum(o.Amount for o in record.Order if o.Amount)
|
||||
received_plus_fee = (record.ReceivedAmount or 0) + record.HandlingFee
|
||||
diff = received_plus_fee - order_sum
|
||||
|
||||
f.write(f"记录 #{idx}:\n")
|
||||
f.write(f" 实收金额: {record.ReceivedAmount}\n")
|
||||
f.write(f" 手续费: {record.HandlingFee}\n")
|
||||
f.write(f" 实收+手续费: {received_plus_fee}\n")
|
||||
f.write(f" 订单总额: {order_sum}\n")
|
||||
f.write(f" 差额: {diff:.2f}\n")
|
||||
f.write(f" 订单数量: {len(record.Order)}\n")
|
||||
f.write("\n")
|
||||
|
||||
logger.info(f"验证报告已保存到: {output_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"生成验证报告时发生错误: {e}", exc_info=True)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
input_file = 'data/data.xlsx'
|
||||
output_file = 'res.json'
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("财务Excel数据处理程序 - 优化版本")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"输入文件: {input_file}")
|
||||
logger.info(f"输出文件: {output_file}")
|
||||
|
||||
try:
|
||||
# 提取数据
|
||||
data, stats = process_excel_data(input_file)
|
||||
|
||||
# 保存到JSON
|
||||
save_to_json(data, output_file)
|
||||
|
||||
# 生成验证报告
|
||||
generate_validation_report(data)
|
||||
|
||||
# 显示统计信息
|
||||
stats.print_report()
|
||||
|
||||
# 显示前3条记录预览
|
||||
if data:
|
||||
logger.info("\n前3条记录预览:")
|
||||
preview = [record.to_dict() for record in data[:3]]
|
||||
print(json.dumps(preview, ensure_ascii=False, indent=2))
|
||||
|
||||
logger.info("\n处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"程序执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
|
||||
|
||||
|
||||
864
res.json
864
res.json
@@ -1560,5 +1560,869 @@
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 10950,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 10950,
|
||||
"AccountName": "Ship Procurement Services S"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 3676.5,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 3676.5,
|
||||
"AccountName": "13 MSC"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 5493,
|
||||
"HandlingFee": 25,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD305C25",
|
||||
"Amount": 100,
|
||||
"AccountName": "18 Raffles"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD363C25",
|
||||
"Amount": 1335,
|
||||
"AccountName": "18 Raffles"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD436C25",
|
||||
"Amount": 995,
|
||||
"AccountName": "18 Raffles"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD456C25",
|
||||
"Amount": 3088,
|
||||
"AccountName": "18 Raffles"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2184.5,
|
||||
"HandlingFee": 55.5,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD184C25",
|
||||
"Amount": 2240,
|
||||
"AccountName": "05 EG"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 3511,
|
||||
"HandlingFee": 51,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD239J25",
|
||||
"Amount": 2762,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD239J25-1",
|
||||
"Amount": 800,
|
||||
"AccountName": "02 BERN"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 17068,
|
||||
"HandlingFee": 12,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD354C25",
|
||||
"Amount": 3280,
|
||||
"AccountName": "06 FLEE"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD432C25",
|
||||
"Amount": 400,
|
||||
"AccountName": "06 FLEE"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD433C25",
|
||||
"Amount": 2000,
|
||||
"AccountName": "06 FLEE"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD461C25",
|
||||
"Amount": 11400,
|
||||
"AccountName": "06 FLEE"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 36936,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD157J25",
|
||||
"Amount": 2200,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD252J25",
|
||||
"Amount": 16700,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD253J25",
|
||||
"Amount": 14295,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD267J25",
|
||||
"Amount": 3741,
|
||||
"AccountName": "02 BERN"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 188578.58,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD543A25",
|
||||
"Amount": 2280,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD561A25",
|
||||
"Amount": 370,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD476A25",
|
||||
"Amount": 920,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD506A25",
|
||||
"Amount": 29700,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD533A25",
|
||||
"Amount": 466.4,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD465A25",
|
||||
"Amount": 9074,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD504A25",
|
||||
"Amount": 410,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD577A25",
|
||||
"Amount": 990,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD554A25",
|
||||
"Amount": 30.8,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD553A25",
|
||||
"Amount": 455,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD501A25",
|
||||
"Amount": 5752,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD434A25",
|
||||
"Amount": 531.2,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD574A25",
|
||||
"Amount": 600,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD491A25",
|
||||
"Amount": 23760,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD556A25",
|
||||
"Amount": 980,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD546A25",
|
||||
"Amount": 606,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD495A25",
|
||||
"Amount": 2250,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD488A25",
|
||||
"Amount": 7574,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD534A25",
|
||||
"Amount": 5766.4,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD560A25",
|
||||
"Amount": 1384,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD487A25",
|
||||
"Amount": 2045.15,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD520A25",
|
||||
"Amount": 5388,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD527A25",
|
||||
"Amount": 25136,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD555A25",
|
||||
"Amount": 1160,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD567A25",
|
||||
"Amount": 11361,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD485A25",
|
||||
"Amount": 8106,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD424A25",
|
||||
"Amount": 20311.33,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD459A25",
|
||||
"Amount": 148,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD423A25",
|
||||
"Amount": 13.3,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD541A25",
|
||||
"Amount": 10407,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD240A25",
|
||||
"Amount": 540,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD547A25",
|
||||
"Amount": 8763,
|
||||
"AccountName": "13 MSC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD005SEV25",
|
||||
"Amount": 1300,
|
||||
"AccountName": "13 MSC"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 12005.97,
|
||||
"HandlingFee": 4.03,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD269J25",
|
||||
"Amount": 5390,
|
||||
"AccountName": "18 RIZZO-RB"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD257J25",
|
||||
"Amount": 6620,
|
||||
"AccountName": "18 RIZZO-RB"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 13640,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD301H25",
|
||||
"Amount": 13640,
|
||||
"AccountName": "ONESEA"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2121,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD300H25",
|
||||
"Amount": 2121,
|
||||
"AccountName": "ONESEA"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2552,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD037G25",
|
||||
"Amount": 2552,
|
||||
"AccountName": "05 Wallem Group"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 6882.5,
|
||||
"HandlingFee": 37.5,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD320T25",
|
||||
"Amount": 2500,
|
||||
"AccountName": "26 中化"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD267T25",
|
||||
"Amount": 4420,
|
||||
"AccountName": "26 中化"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 5564,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 5564,
|
||||
"AccountName": "13 MSC"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2783,
|
||||
"HandlingFee": 12,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD425C25",
|
||||
"Amount": 2795,
|
||||
"AccountName": "06 FLEE"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 15920,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD396C25",
|
||||
"Amount": 8000,
|
||||
"AccountName": "Executive Ship Management Pte"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD434C25",
|
||||
"Amount": 7920,
|
||||
"AccountName": "Executive Ship Management Pte"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 208,
|
||||
"HandlingFee": 12,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD451C25",
|
||||
"Amount": 220,
|
||||
"AccountName": "06 FLEE"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2623.5,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD284J25",
|
||||
"Amount": 2623.5,
|
||||
"AccountName": "16 Paci"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 7769,
|
||||
"HandlingFee": 51,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD247J25",
|
||||
"Amount": 7820,
|
||||
"AccountName": "02 BERN"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 84775,
|
||||
"HandlingFee": 11,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD245H25",
|
||||
"Amount": 39600,
|
||||
"AccountName": "01 ANGLO-A"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD311H25",
|
||||
"Amount": 2120,
|
||||
"AccountName": "01 ANGLO-A"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD306H25",
|
||||
"Amount": 1542,
|
||||
"AccountName": "01 ANGLO-A"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD179H25",
|
||||
"Amount": 38979,
|
||||
"AccountName": "01 ANGLO-A"
|
||||
},
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 2545,
|
||||
"AccountName": null
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 8052,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD298J25",
|
||||
"Amount": 3660,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD302J25-1",
|
||||
"Amount": 973,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD302J25",
|
||||
"Amount": 2246,
|
||||
"AccountName": "02 BERN"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD302J25-2",
|
||||
"Amount": 1173,
|
||||
"AccountName": "02 BERN"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 23200,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD050Y25",
|
||||
"Amount": 23200,
|
||||
"AccountName": "01 Aryansh"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2575,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD351C25",
|
||||
"Amount": 2575,
|
||||
"AccountName": "Technomar Shipping Inc"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 5538,
|
||||
"HandlingFee": 32,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 5570,
|
||||
"AccountName": "XTMANAGEMENTLTD"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 6250,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 6250,
|
||||
"AccountName": "03 CHAR"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2349,
|
||||
"HandlingFee": 11,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 2360,
|
||||
"AccountName": "01 ANGLO-A"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 11869,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD021D25",
|
||||
"Amount": 5034,
|
||||
"AccountName": "15 Ocea"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD031D25",
|
||||
"Amount": 1950,
|
||||
"AccountName": "15 Ocea"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD047D25",
|
||||
"Amount": 4885,
|
||||
"AccountName": "15 Ocea"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2480,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD632A25",
|
||||
"Amount": 2480,
|
||||
"AccountName": "MSC CYPRUS"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2755.5,
|
||||
"HandlingFee": 11,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD114N25",
|
||||
"Amount": 1739,
|
||||
"AccountName": "01 A2RX"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD113N25",
|
||||
"Amount": 1027.5,
|
||||
"AccountName": "01 A2RX"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 8100,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD243J25",
|
||||
"Amount": 8100,
|
||||
"AccountName": "16 PACC"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 4890,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": null,
|
||||
"Amount": 4890,
|
||||
"AccountName": "WO TIN SHIPPING"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2700,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD225G25",
|
||||
"Amount": 2700,
|
||||
"AccountName": "Emarat Maritime"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 7698.5,
|
||||
"HandlingFee": 41.5,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD352T25",
|
||||
"Amount": 7740,
|
||||
"AccountName": "09 Italia"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 5328,
|
||||
"HandlingFee": 12,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD307C25",
|
||||
"Amount": 3600,
|
||||
"AccountName": "06 FLEE"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD478C25",
|
||||
"Amount": 1740,
|
||||
"AccountName": "06 FLEE"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 2075,
|
||||
"HandlingFee": 11,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD680A25",
|
||||
"Amount": 2086,
|
||||
"AccountName": "15 OSM SHIP"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 4550,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD041Y25",
|
||||
"Amount": 4550,
|
||||
"AccountName": "19 SPM"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 12747,
|
||||
"HandlingFee": 33,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD437C25",
|
||||
"Amount": 3550,
|
||||
"AccountName": "16 PAC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD395C25",
|
||||
"Amount": 1980,
|
||||
"AccountName": "16 PAC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD411C25",
|
||||
"Amount": 3220,
|
||||
"AccountName": "16 PAC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD412C25",
|
||||
"Amount": 2700,
|
||||
"AccountName": "16 PAC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD441C25",
|
||||
"Amount": 1030,
|
||||
"AccountName": "16 PAC"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD514C25",
|
||||
"Amount": 300,
|
||||
"AccountName": "16 PAC"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 5758,
|
||||
"HandlingFee": 32,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD075R25",
|
||||
"Amount": 5790,
|
||||
"AccountName": "11 Kaizen"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 7284,
|
||||
"HandlingFee": 36,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD354T25",
|
||||
"Amount": 2560,
|
||||
"AccountName": "16 Pacific"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD347T25",
|
||||
"Amount": 4760,
|
||||
"AccountName": "16 Pacific"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 40900,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD253T25",
|
||||
"Amount": 3000,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD220T25",
|
||||
"Amount": 4720,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD246T25",
|
||||
"Amount": 6000,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD262T25",
|
||||
"Amount": 3000,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD227T25",
|
||||
"Amount": 1500,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD252T25",
|
||||
"Amount": 7500,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD254T25",
|
||||
"Amount": 4500,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD298T25",
|
||||
"Amount": 5120,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD322T25",
|
||||
"Amount": 3000,
|
||||
"AccountName": "24台湾万海航运"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD235T25",
|
||||
"Amount": 2560,
|
||||
"AccountName": "24台湾万海航运"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 65900,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD136G25",
|
||||
"Amount": 65900,
|
||||
"AccountName": "Capital Executive Ship Management Corp"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
},
|
||||
{
|
||||
"ReceivedAmount": 7815.3,
|
||||
"HandlingFee": 0,
|
||||
"Order": [
|
||||
{
|
||||
"OrderNum": "XLRQD284J25",
|
||||
"Amount": 2623.5,
|
||||
"AccountName": "16 Paci"
|
||||
},
|
||||
{
|
||||
"OrderNum": "XLRQD295J25",
|
||||
"Amount": 5191.8,
|
||||
"AccountName": "16 Paci"
|
||||
}
|
||||
],
|
||||
"checkRes": true
|
||||
}
|
||||
]
|
||||
2
run.cmd
Normal file
2
run.cmd
Normal file
@@ -0,0 +1,2 @@
|
||||
python process_excel.py
|
||||
python generate_accounting_entries.py
|
||||
331
test_process_excel.py
Normal file
331
test_process_excel.py
Normal file
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
财务Excel数据处理程序的单元测试
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
# 导入被测试的模块
|
||||
try:
|
||||
from process_excel_optimized import (
|
||||
Order, FinancialRecord, ProcessingStats,
|
||||
MergedCellCache, get_cell_value, validate_amount
|
||||
)
|
||||
USE_OPTIMIZED = True
|
||||
except ImportError:
|
||||
USE_OPTIMIZED = False
|
||||
print("警告: 无法导入优化版本,部分测试将被跳过")
|
||||
|
||||
|
||||
class TestOrder(unittest.TestCase):
|
||||
"""测试Order数据类"""
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_order_creation(self):
|
||||
"""测试订单创建"""
|
||||
order = Order(
|
||||
OrderNum="TEST001",
|
||||
Amount=1000.50,
|
||||
AccountName="测试账户"
|
||||
)
|
||||
|
||||
self.assertEqual(order.OrderNum, "TEST001")
|
||||
self.assertEqual(order.Amount, 1000.50)
|
||||
self.assertEqual(order.AccountName, "测试账户")
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_order_to_dict(self):
|
||||
"""测试订单转换为字典"""
|
||||
order = Order(
|
||||
OrderNum="TEST001",
|
||||
Amount=1000.50,
|
||||
AccountName="测试账户"
|
||||
)
|
||||
|
||||
result = order.to_dict()
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result["OrderNum"], "TEST001")
|
||||
self.assertEqual(result["Amount"], 1000.50)
|
||||
self.assertEqual(result["AccountName"], "测试账户")
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_order_with_none_values(self):
|
||||
"""测试订单号为空的情况"""
|
||||
order = Order(
|
||||
OrderNum=None,
|
||||
Amount=1000.50,
|
||||
AccountName="测试账户"
|
||||
)
|
||||
|
||||
self.assertIsNone(order.OrderNum)
|
||||
self.assertEqual(order.Amount, 1000.50)
|
||||
|
||||
|
||||
class TestFinancialRecord(unittest.TestCase):
|
||||
"""测试FinancialRecord数据类"""
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_record_creation(self):
|
||||
"""测试财务记录创建"""
|
||||
orders = [
|
||||
Order("TEST001", 1000.0, "账户1"),
|
||||
Order("TEST002", 2000.0, "账户2")
|
||||
]
|
||||
|
||||
record = FinancialRecord(
|
||||
ReceivedAmount=2975.0,
|
||||
HandlingFee=25.0,
|
||||
Order=orders,
|
||||
checkRes=True
|
||||
)
|
||||
|
||||
self.assertEqual(record.ReceivedAmount, 2975.0)
|
||||
self.assertEqual(record.HandlingFee, 25.0)
|
||||
self.assertEqual(len(record.Order), 2)
|
||||
self.assertTrue(record.checkRes)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_record_to_dict(self):
|
||||
"""测试财务记录转换为字典"""
|
||||
orders = [Order("TEST001", 1000.0, "账户1")]
|
||||
record = FinancialRecord(
|
||||
ReceivedAmount=1000.0,
|
||||
HandlingFee=0.0,
|
||||
Order=orders,
|
||||
checkRes=True
|
||||
)
|
||||
|
||||
result = record.to_dict()
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result["ReceivedAmount"], 1000.0)
|
||||
self.assertIsInstance(result["Order"], list)
|
||||
self.assertEqual(len(result["Order"]), 1)
|
||||
|
||||
|
||||
class TestProcessingStats(unittest.TestCase):
|
||||
"""测试ProcessingStats统计类"""
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_stats_initialization(self):
|
||||
"""测试统计对象初始化"""
|
||||
stats = ProcessingStats()
|
||||
|
||||
self.assertEqual(stats.total_records, 0)
|
||||
self.assertEqual(stats.valid_records, 0)
|
||||
self.assertEqual(stats.invalid_records, 0)
|
||||
self.assertEqual(stats.total_orders, 0)
|
||||
self.assertEqual(stats.check_failed_records, 0)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_stats_update(self):
|
||||
"""测试统计数据更新"""
|
||||
stats = ProcessingStats()
|
||||
|
||||
stats.total_records = 10
|
||||
stats.valid_records = 8
|
||||
stats.invalid_records = 2
|
||||
stats.total_orders = 25
|
||||
|
||||
self.assertEqual(stats.total_records, 10)
|
||||
self.assertEqual(stats.valid_records, 8)
|
||||
self.assertEqual(stats.invalid_records, 2)
|
||||
self.assertEqual(stats.total_orders, 25)
|
||||
|
||||
|
||||
class TestMergedCellCache(unittest.TestCase):
|
||||
"""测试MergedCellCache缓存类"""
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_cache_creation(self):
|
||||
"""测试缓存创建"""
|
||||
# 创建模拟的合并单元格范围
|
||||
mock_range = Mock()
|
||||
mock_range.min_row = 2
|
||||
mock_range.max_row = 4
|
||||
mock_range.min_col = 6
|
||||
mock_range.max_col = 6
|
||||
|
||||
cache = MergedCellCache([mock_range])
|
||||
|
||||
# 测试缓存是否正确识别合并单元格
|
||||
self.assertTrue(cache.is_merged(2, 6))
|
||||
self.assertTrue(cache.is_merged(3, 6))
|
||||
self.assertTrue(cache.is_merged(4, 6))
|
||||
self.assertFalse(cache.is_merged(5, 6))
|
||||
self.assertFalse(cache.is_merged(2, 7))
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_cache_get_merged_range(self):
|
||||
"""测试获取合并范围"""
|
||||
mock_range = Mock()
|
||||
mock_range.min_row = 2
|
||||
mock_range.max_row = 4
|
||||
mock_range.min_col = 6
|
||||
mock_range.max_col = 6
|
||||
|
||||
cache = MergedCellCache([mock_range])
|
||||
|
||||
# 测试获取合并范围
|
||||
result = cache.get_merged_range(3, 6)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
# 测试非合并单元格
|
||||
result = cache.get_merged_range(10, 10)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestValidateAmount(unittest.TestCase):
|
||||
"""测试金额验证函数"""
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_validate_exact_match(self):
|
||||
"""测试金额完全匹配"""
|
||||
orders = [
|
||||
Order("TEST001", 1000.0, "账户1"),
|
||||
Order("TEST002", 975.0, "账户2")
|
||||
]
|
||||
|
||||
result = validate_amount(1975.0, 0.0, orders)
|
||||
self.assertTrue(result)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_validate_with_handling_fee(self):
|
||||
"""测试包含手续费的金额验证"""
|
||||
orders = [
|
||||
Order("TEST001", 2000.0, "账户1")
|
||||
]
|
||||
|
||||
result = validate_amount(1975.0, 25.0, orders)
|
||||
self.assertTrue(result)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_validate_within_tolerance(self):
|
||||
"""测试在容差范围内的金额"""
|
||||
orders = [
|
||||
Order("TEST001", 1000.005, "账户1")
|
||||
]
|
||||
|
||||
# 差额在0.01容差范围内
|
||||
result = validate_amount(1000.0, 0.0, orders)
|
||||
self.assertTrue(result)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_validate_mismatch(self):
|
||||
"""测试金额不匹配"""
|
||||
orders = [
|
||||
Order("TEST001", 1000.0, "账户1"),
|
||||
Order("TEST002", 500.0, "账户2")
|
||||
]
|
||||
|
||||
result = validate_amount(1000.0, 0.0, orders)
|
||||
self.assertFalse(result)
|
||||
|
||||
@unittest.skipIf(not USE_OPTIMIZED, "需要优化版本")
|
||||
def test_validate_with_none_amount(self):
|
||||
"""测试包含空金额的订单"""
|
||||
orders = [
|
||||
Order("TEST001", 1000.0, "账户1"),
|
||||
Order("TEST002", None, "账户2") # 空金额应被忽略
|
||||
]
|
||||
|
||||
result = validate_amount(1000.0, 0.0, orders)
|
||||
self.assertTrue(result)
|
||||
|
||||
|
||||
class TestIntegration(unittest.TestCase):
|
||||
"""集成测试"""
|
||||
|
||||
def test_json_output_format(self):
|
||||
"""测试JSON输出格式"""
|
||||
# 检查res.json是否存在
|
||||
if not os.path.exists('res.json'):
|
||||
self.skipTest("res.json文件不存在")
|
||||
|
||||
# 读取JSON文件
|
||||
with open('res.json', 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 验证数据结构
|
||||
self.assertIsInstance(data, list)
|
||||
|
||||
if len(data) > 0:
|
||||
record = data[0]
|
||||
|
||||
# 验证必需字段
|
||||
self.assertIn("ReceivedAmount", record)
|
||||
self.assertIn("HandlingFee", record)
|
||||
self.assertIn("Order", record)
|
||||
self.assertIn("checkRes", record)
|
||||
|
||||
# 验证Order结构
|
||||
self.assertIsInstance(record["Order"], list)
|
||||
if len(record["Order"]) > 0:
|
||||
order = record["Order"][0]
|
||||
self.assertIn("OrderNum", order)
|
||||
self.assertIn("Amount", order)
|
||||
self.assertIn("AccountName", order)
|
||||
|
||||
|
||||
class TestConfigIntegration(unittest.TestCase):
|
||||
"""配置文件集成测试"""
|
||||
|
||||
def test_config_file_exists(self):
|
||||
"""测试配置文件是否存在"""
|
||||
self.assertTrue(
|
||||
os.path.exists('config.ini'),
|
||||
"config.ini配置文件应该存在"
|
||||
)
|
||||
|
||||
def test_exchange_rate_file(self):
|
||||
"""测试汇率文件"""
|
||||
if not os.path.exists('exchange_rate.txt'):
|
||||
self.skipTest("exchange_rate.txt文件不存在")
|
||||
|
||||
with open('exchange_rate.txt', 'r', encoding='utf-8') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
# 验证可以转换为浮点数
|
||||
try:
|
||||
rate = float(content)
|
||||
self.assertGreater(rate, 0)
|
||||
self.assertLess(rate, 100)
|
||||
except ValueError:
|
||||
self.fail("汇率文件内容无法转换为数字")
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
# 创建测试套件
|
||||
loader = unittest.TestLoader()
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# 添加所有测试类
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestOrder))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestFinancialRecord))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestProcessingStats))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestMergedCellCache))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestValidateAmount))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestIntegration))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestConfigIntegration))
|
||||
|
||||
# 运行测试
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
# 返回测试结果
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = run_tests()
|
||||
exit(0 if success else 1)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user