184 行
6.9 KiB
Python
184 行
6.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Independently verify generated BIFF8 data and visual template contracts."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
import sys
|
||
|
||
try:
|
||
import xlrd
|
||
except ImportError:
|
||
sys.stderr.write("[payment-order-xls][FAIL] verifier dependency unavailable\n")
|
||
raise SystemExit(1)
|
||
|
||
|
||
TEMPLATE_PATH = Path(__file__).resolve().parents[1] / "src" / "assets" / "payment-order" / "ccb-payment-order-template.xls"
|
||
EXPECTED_HEADERS = [
|
||
"*序号",
|
||
"*付款方客户账号",
|
||
"*付款方账户名称",
|
||
"*收款方行别代码\n(01-本行 02-国内他行)",
|
||
"*收款方客户账号",
|
||
"*收款方账户名称",
|
||
"收款方开户行名称",
|
||
"收款方联行号",
|
||
"客户方流水号",
|
||
"*金额",
|
||
"*用途",
|
||
"备注",
|
||
"是否短信通知收款人(0-不通知,1-通知,默认为0-不通知。)",
|
||
"收款人手机号码",
|
||
"短信通知附加信息",
|
||
]
|
||
REQUIRED_HEADER_COLUMNS = (0, 1, 2, 3, 4, 5, 9, 10)
|
||
|
||
|
||
def font_signature(font: xlrd.formatting.Font) -> tuple:
|
||
return (
|
||
font.name,
|
||
font.height,
|
||
font.bold,
|
||
font.italic,
|
||
font.struck_out,
|
||
font.outline,
|
||
font.shadow,
|
||
font.colour_index,
|
||
font.weight,
|
||
font.escapement,
|
||
font.underline_type,
|
||
font.family,
|
||
font.character_set,
|
||
)
|
||
|
||
|
||
def rich_text_signature(book: xlrd.book.Book, sheet: xlrd.sheet.Sheet, row: int, column: int) -> list[tuple]:
|
||
text = sheet.cell_value(row, column)
|
||
runs = sheet.rich_text_runlist_map.get((row, column), [])
|
||
if not runs:
|
||
return []
|
||
signature = []
|
||
if runs[0][0] > 0:
|
||
base_font_index = book.xf_list[sheet.cell_xf_index(row, column)].font_index
|
||
signature.append((0, runs[0][0], text[:runs[0][0]], font_signature(book.font_list[base_font_index])))
|
||
for index, (offset, font_index) in enumerate(runs):
|
||
end = runs[index + 1][0] if index + 1 < len(runs) else len(text)
|
||
signature.append((offset, end, text[offset:end], font_signature(book.font_list[font_index])))
|
||
return signature
|
||
|
||
|
||
def visual_signature(book: xlrd.book.Book, sheet: xlrd.sheet.Sheet, row: int, column: int) -> tuple:
|
||
xf = book.xf_list[sheet.cell_xf_index(row, column)]
|
||
font = book.font_list[xf.font_index]
|
||
alignment = xf.alignment
|
||
background = xf.background
|
||
border = xf.border
|
||
return (
|
||
font.name,
|
||
font.height,
|
||
font.bold,
|
||
font.italic,
|
||
font.underline_type,
|
||
font.colour_index,
|
||
alignment.hor_align,
|
||
alignment.vert_align,
|
||
alignment.text_wrapped,
|
||
alignment.rotation,
|
||
alignment.shrink_to_fit,
|
||
background.fill_pattern,
|
||
background.pattern_colour_index,
|
||
background.background_colour_index,
|
||
border.left_line_style,
|
||
border.right_line_style,
|
||
border.top_line_style,
|
||
border.bottom_line_style,
|
||
border.left_colour_index,
|
||
border.right_colour_index,
|
||
border.top_colour_index,
|
||
border.bottom_colour_index,
|
||
)
|
||
|
||
|
||
def main() -> int:
|
||
stage = "read"
|
||
try:
|
||
generated_bytes = sys.stdin.buffer.read(16 * 1024 * 1024 + 1)
|
||
assert len(generated_bytes) <= 16 * 1024 * 1024
|
||
template = xlrd.open_workbook(str(TEMPLATE_PATH), formatting_info=True)
|
||
generated = xlrd.open_workbook(file_contents=generated_bytes, formatting_info=True)
|
||
stage = "structure"
|
||
assert generated.sheet_names() == ["Sheet1", "Sheet2", "Sheet3"]
|
||
assert [(sheet.nrows, sheet.ncols) for sheet in generated.sheets()] == [(5, 15), (0, 0), (0, 0)]
|
||
|
||
source_sheet = template.sheet_by_index(0)
|
||
sheet = generated.sheet_by_index(0)
|
||
stage = "headers"
|
||
assert sheet.row_values(0) == EXPECTED_HEADERS
|
||
stage = "header_rich_text"
|
||
assert set(sheet.rich_text_runlist_map) == set(source_sheet.rich_text_runlist_map)
|
||
for row_column in source_sheet.rich_text_runlist_map:
|
||
row, column = row_column
|
||
assert rich_text_signature(generated, sheet, row, column) == rich_text_signature(
|
||
template,
|
||
source_sheet,
|
||
row,
|
||
column,
|
||
)
|
||
for column in REQUIRED_HEADER_COLUMNS:
|
||
parts = rich_text_signature(generated, sheet, 0, column)
|
||
assert parts[0][2] == "*" and parts[0][3][7] == 10
|
||
assert "".join(part[2] for part in parts[1:]) == EXPECTED_HEADERS[column][1:]
|
||
assert all(part[3][7] == 8 for part in parts[1:])
|
||
stage = "column_widths"
|
||
stage = "explicit_column_widths"
|
||
assert {column: info.width for column, info in sheet.colinfo_map.items()} == {
|
||
column: info.width for column, info in source_sheet.colinfo_map.items()
|
||
}
|
||
stage = "header_height"
|
||
assert sheet.rowinfo_map[0].height == source_sheet.rowinfo_map[0].height
|
||
|
||
for row in range(1, 5):
|
||
stage = "data_row_height"
|
||
assert sheet.rowinfo_map[row].height == source_sheet.rowinfo_map[1].height
|
||
for column in range(15):
|
||
stage = "visual_style"
|
||
assert visual_signature(generated, sheet, row, column) == visual_signature(template, source_sheet, 1, column)
|
||
|
||
stage = "cell_types"
|
||
for row in range(1, 5):
|
||
for column in (0, 1, 2, 3, 4, 5, 6, 8, 10, 11, 12):
|
||
stage = f"text_cell_{column}"
|
||
assert sheet.cell_type(row, column) == xlrd.XL_CELL_TEXT
|
||
for column in (7, 13, 14):
|
||
stage = f"empty_cell_{column}"
|
||
assert sheet.cell_type(row, column) in (xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK)
|
||
stage = "payer_account_value"
|
||
assert sheet.cell_value(1, 1) == "6217000000000012345"
|
||
stage = "bank_type_value"
|
||
assert sheet.cell_value(1, 3) == "01"
|
||
stage = "payee_account_value"
|
||
assert sheet.cell_value(1, 4) == "6222000000000012345"
|
||
stage = "first_customer_serial_value"
|
||
assert sheet.cell_value(1, 8) == "PB-20260803-001-R001"
|
||
stage = "last_customer_serial_value"
|
||
assert sheet.cell_value(4, 8) == "PB-20260803-001-R004"
|
||
stage = "payee_name_value"
|
||
assert sheet.cell_value(1, 5) == "合成员工1"
|
||
stage = "purpose_value"
|
||
assert sheet.cell_value(1, 10) == "报销款"
|
||
stage = "sms_flag_value"
|
||
assert sheet.cell_value(1, 12) == "0"
|
||
for row in range(1, 5):
|
||
assert sheet.cell_type(row, 9) == xlrd.XL_CELL_NUMBER
|
||
stage = "amount_format"
|
||
assert generated.format_map[generated.xf_list[sheet.cell_xf_index(row, 9)].format_key].format_str == "0.00"
|
||
sys.stdout.write("[payment-order-xls][PASS] original BIFF8 template data and visual contracts verified\n")
|
||
return 0
|
||
except Exception:
|
||
sys.stderr.write(f"[payment-order-xls][FAIL] BIFF8 template contract mismatch: {stage}\n")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|