test_bx_reimburse_import.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # backend/tests/test_bx_reimburse_import.py
  2. from datetime import date
  3. from io import BytesIO
  4. from openpyxl import Workbook, load_workbook
  5. from app.services.bx.reimburse_import import (
  6. HEADERS,
  7. build_applicant_label,
  8. build_expense_path_labels,
  9. build_import_template_bytes,
  10. is_example_row,
  11. iter_data_rows,
  12. partition_import_groups,
  13. validate_group_headers,
  14. )
  15. from app.utils.excel_list_validation import add_list_validation, write_option_sheet
  16. def test_build_expense_path_labels_unique_and_enabled_only():
  17. nodes = [
  18. {"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
  19. {"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
  20. {"id": "3", "parent_id": "1", "item_name": "住宿费", "status": 0, "item_code": "ZS"},
  21. {"id": "4", "parent_id": "0", "item_name": "办公费", "status": 1, "item_code": "BG"},
  22. ]
  23. labels = build_expense_path_labels(nodes)
  24. assert labels["2"] == "差旅费 / 市内交通"
  25. assert labels["1"] == "差旅费"
  26. assert labels["4"] == "办公费"
  27. assert "3" not in labels
  28. def test_build_expense_path_labels_exclude_orphan_when_parent_missing():
  29. """父级未传入(等同已删除)时,子节点不应进入下拉——与 Web 树一致。"""
  30. nodes = [
  31. {"id": "29", "parent_id": "28", "item_name": "办公用品", "status": 1, "item_code": "BGYP"},
  32. {"id": "30", "parent_id": "0", "item_name": "物流运输费用", "status": 1, "item_code": "YSF"},
  33. ]
  34. labels = build_expense_path_labels(nodes)
  35. assert "29" not in labels
  36. assert labels["30"] == "物流运输费用"
  37. def test_build_expense_path_labels_disambiguate_collision():
  38. nodes = [
  39. {"id": "a", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "A1"},
  40. {"id": "b", "parent_id": "0", "item_name": "同名", "status": 1, "item_code": "B1"},
  41. ]
  42. labels = build_expense_path_labels(nodes)
  43. assert labels["a"] != labels["b"]
  44. assert "编码" in labels["a"] or "编码" in labels["b"] or labels["a"].endswith(")")
  45. def test_cell_str_normalizes_excel_float_ids():
  46. from app.services.bx.reimburse_import import cell_str
  47. assert cell_str(29.0) == "29"
  48. assert cell_str(29) == "29"
  49. def test_build_applicant_label():
  50. assert build_applicant_label("张三", "z001", "uid-abcdef") == "张三(z001)"
  51. assert build_applicant_label("李四", "", "abcdefghij") == "李四(efghij)"
  52. def test_is_example_row():
  53. assert is_example_row({"报销事由": "【示例】地铁"}) is True
  54. assert is_example_row({"报销事由": "正常事由"}) is False
  55. def test_partition_empty_group_is_solo():
  56. rows = [
  57. {"_row": 2, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "x", "金额(元)": 1, "报销事由": "r1", "发生时间": ""},
  58. {"_row": 3, "分组号": "G1", "申请人": "A", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "y", "金额(元)": 2, "报销事由": "r2", "发生时间": ""},
  59. {"_row": 4, "分组号": "", "申请人": "B", "报销日期": "2026-08-18", "报销标题": "", "费用项目": "z", "金额(元)": 3, "报销事由": "r3", "发生时间": ""},
  60. ]
  61. groups = partition_import_groups(rows)
  62. assert len(groups) == 2
  63. g1 = next(g for g in groups if g["group_no"] == "G1")
  64. assert len(g1["rows"]) == 2
  65. solo = next(g for g in groups if g["group_no"] == "")
  66. assert len(solo["rows"]) == 1
  67. def test_validate_group_headers_mismatch():
  68. rows = [
  69. {"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
  70. {"_row": 3, "申请人": "B", "报销日期": "2026-08-18", "报销标题": ""},
  71. ]
  72. err = validate_group_headers(rows)
  73. assert err and "申请人" in err
  74. def test_validate_group_headers_title_partial_empty():
  75. rows = [
  76. {"_row": 2, "申请人": "A", "报销日期": "2026-08-18", "报销标题": "有标题"},
  77. {"_row": 3, "申请人": "A", "报销日期": "2026-08-18", "报销标题": ""},
  78. ]
  79. err = validate_group_headers(rows)
  80. assert err and "标题" in err
  81. def test_write_option_sheet_and_validation():
  82. wb = Workbook()
  83. ws = wb.active
  84. ws.title = "报销导入"
  85. ws.append(["申请人"])
  86. write_option_sheet(wb, "_选项_申请人", ["张三(z001)"], ["u1"], hidden=True)
  87. add_list_validation(
  88. ws,
  89. option_sheet="_选项_申请人",
  90. option_rows=1,
  91. target_col=1,
  92. start_row=2,
  93. end_row=100,
  94. )
  95. assert wb["_选项_申请人"].sheet_state == "hidden"
  96. assert len(ws.data_validations.dataValidation) == 1
  97. def test_build_import_template_has_guide_examples_and_dv():
  98. raw = build_import_template_bytes(
  99. expense_nodes=[
  100. {"id": "1", "parent_id": "0", "item_name": "差旅费", "status": 1, "item_code": "CL"},
  101. {"id": "2", "parent_id": "1", "item_name": "市内交通", "status": 1, "item_code": "JT"},
  102. ],
  103. applicants=[{"id": "u1", "nick_name": "张三", "user_no": "z001"}],
  104. current_applicant_label="张三(z001)",
  105. today=date(2026, 8, 18),
  106. )
  107. wb = load_workbook(BytesIO(raw))
  108. assert "填写说明" in wb.sheetnames
  109. assert "报销导入" in wb.sheetnames
  110. assert wb["_选项_费用项目"].sheet_state == "hidden"
  111. ws = wb["报销导入"]
  112. assert [c.value for c in ws[1]][:8] == list(HEADERS)
  113. assert str(ws["G2"].value).startswith("【示例】")
  114. assert len(ws.data_validations.dataValidation) >= 1
  115. def test_iter_data_rows_skips_examples():
  116. wb = Workbook()
  117. ws = wb.active
  118. ws.title = "报销导入"
  119. ws.append(list(HEADERS))
  120. ws.append(
  121. ["G1", "张三(z001)", "2026-08-18", "", "差旅费", 50, "【示例】跳过", ""]
  122. )
  123. ws.append(
  124. ["G1", "张三(z001)", "2026-08-18", "", "差旅费", 80, "正式事由", ""]
  125. )
  126. rows = iter_data_rows(ws)
  127. assert len(rows) == 1
  128. assert rows[0]["报销事由"] == "正式事由"
  129. assert rows[0]["_row"] == 3
  130. def test_build_annotated_result_bytes_appends_status_columns():
  131. from app.services.bx.reimburse_import import build_annotated_result_bytes
  132. wb = Workbook()
  133. ws = wb.active
  134. ws.title = "报销导入"
  135. ws.append(list(HEADERS))
  136. ws.append(["G1", "张三(z001)", "2026-08-18", "", "差旅费", 80, "正式事由", ""])
  137. buf = BytesIO()
  138. wb.save(buf)
  139. raw = build_annotated_result_bytes(
  140. buf.getvalue(),
  141. {2: {"status": "成功", "message": "成功,单号 BX001"}},
  142. )
  143. out = load_workbook(BytesIO(raw))["报销导入"]
  144. headers = [c.value for c in out[1]]
  145. assert "导入结果" in headers
  146. assert "失败原因" in headers
  147. status_i = headers.index("导入结果") + 1
  148. assert out.cell(row=2, column=status_i).value == "成功"