test_step4_response_table.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """响应表覆盖必须使用Word网格,原要求单元格不可压平或移位。"""
  2. import os
  3. import tempfile
  4. import unittest
  5. import xml.etree.ElementTree as ET
  6. from docx import Document
  7. from docx.enum.text import WD_ALIGN_PARAGRAPH
  8. from models import Chapter, ExtractedTable, TableCell, TenderAnalysis
  9. from step4_writing import (
  10. _fill_current_response_table, _overlay_table_xml_text,
  11. _save_chapter_docx, _w_tag,
  12. )
  13. def text(element):
  14. return ''.join(t.text or '' for t in element.iter(_w_tag('t')))
  15. def canonical_cell(cell):
  16. return ET.tostring(ET.fromstring(cell._tc.xml))
  17. class ResponseTableTests(unittest.TestCase):
  18. def fixture(self):
  19. doc = Document()
  20. table = doc.add_table(rows=4, cols=5)
  21. headers = ['类别', '序号', '资格要求', '是否响应', '证明材料']
  22. for i, value in enumerate(headers):
  23. table.cell(0, i).text = value
  24. table.cell(1, 0).merge(table.cell(3, 0)).text = '资格'
  25. cells = [[TableCell(text=value, col=i) for i, value in enumerate(headers)]]
  26. for r in range(1, 4):
  27. table.cell(r, 1).text = str(r)
  28. p = table.cell(r, 2).paragraphs[0]
  29. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  30. p.add_run(f'{r}、提供').bold = True
  31. p.add_run('示例证明。').underline = True
  32. table.cell(r, 2).add_paragraph('原说明必须保留。')
  33. cells.append([
  34. TableCell(text='资格' if r == 1 else '', row=r, col=0,
  35. rowspan=3 if r == 1 else 1,
  36. is_merged_origin=r == 1, is_merged_continuation=r > 1),
  37. TableCell(text=str(r), row=r, col=1),
  38. TableCell(text=f'{r}、提供示例证明。\n原说明必须保留。', row=r, col=2),
  39. TableCell(row=r, col=3), TableCell(row=r, col=4),
  40. ])
  41. source = ExtractedTable(rows=4, cols=5, cells=cells,
  42. table_type='qualification_response', source_type='tender_pdf')
  43. return doc, table, source
  44. def test_vertical_merge_does_not_shift_requirements_or_flatten_runs(self):
  45. _, table, source = self.fixture()
  46. xml = ET.fromstring(table._tbl.xml)
  47. before = [[ET.tostring(tc) for tc in row.findall(_w_tag('tc'))[:3]]
  48. for row in xml.findall(_w_tag('tr'))]
  49. filled = _fill_current_response_table(source, '资格条件响应表')
  50. _overlay_table_xml_text(xml, filled)
  51. after = [[ET.tostring(tc) for tc in row.findall(_w_tag('tc'))[:3]]
  52. for row in xml.findall(_w_tag('tr'))]
  53. self.assertEqual(before, after, '原要求、合并续格及多段混合格式必须原样保留')
  54. for row in xml.findall(_w_tag('tr'))[1:]:
  55. self.assertEqual(text(row.findall(_w_tag('tc'))[3]), '是')
  56. def test_horizontal_and_vertical_merge_keep_response_origin_only(self):
  57. _, table, source = self.fixture()
  58. table.cell(1, 3).merge(table.cell(2, 3))
  59. source.cells[1][3].rowspan = 2
  60. source.cells[1][3].is_merged_origin = True
  61. source.cells[2][3].is_merged_continuation = True
  62. table.cell(3, 1).merge(table.cell(3, 2))
  63. source.cells[3][1].text = '3\n3、提供示例证明。\n原说明必须保留。'
  64. source.cells[3][1].colspan = 2
  65. source.cells[3][1].is_merged_origin = True
  66. source.cells[3][2].is_merged_continuation = True
  67. filled = _fill_current_response_table(source, '资格条件响应表')
  68. self.assertEqual(filled.cells[2][3].text, '', '合并续格不得被填充')
  69. xml = ET.fromstring(table._tbl.xml)
  70. _overlay_table_xml_text(xml, filled)
  71. rows = xml.findall(_w_tag('tr'))
  72. self.assertEqual(text(rows[2].findall(_w_tag('tc'))[3]), '')
  73. self.assertEqual(text(rows[3].findall(_w_tag('tc'))[2]), '是')
  74. def test_actual_chapter_write_preserves_step1_requirement_xml(self):
  75. doc, table, source = self.fixture()
  76. with tempfile.TemporaryDirectory() as directory:
  77. source.artifact_path = os.path.join(directory, 'source.docx')
  78. doc.save(source.artifact_path)
  79. template = Document()
  80. template.add_paragraph('第一章 资格', style='Heading 1')
  81. template.add_paragraph('%%资格条件响应表%%')
  82. template.add_paragraph('第二章 报价', style='Heading 1')
  83. template_path = os.path.join(directory, 'template.docx')
  84. template.save(template_path)
  85. path = _save_chapter_docx(
  86. Chapter(id='1', title='资格', template_chapter_id='1'), directory,
  87. template_path=template_path,
  88. analysis=TenderAnalysis(project_name='示例项目', tender_tables=[source]),
  89. )
  90. result = Document(path).tables[0]
  91. for r in range(1, 4):
  92. self.assertEqual(result.cell(r, 2).text, table.cell(r, 2).text)
  93. self.assertEqual(canonical_cell(result.cell(r, 2)), canonical_cell(table.cell(r, 2)))
  94. self.assertEqual(result.cell(r, 3).text, '是')
  95. self.assertEqual(source.cells[1][3].text, '', '填表不得污染Step1元数据')
  96. def test_grid_before_and_existing_response_are_preserved(self):
  97. _, table, source = self.fixture()
  98. table.cell(1, 3).text = '已有响应'
  99. table.cell(2, 3).paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.RIGHT
  100. table.cell(2, 3).paragraphs[0].add_run(' ').underline = True
  101. xml = ET.fromstring(table._tbl.xml)
  102. rows = xml.findall(_w_tag('tr'))
  103. first_response = ET.tostring(rows[1].findall(_w_tag('tc'))[3])
  104. rows[2].remove(rows[2].find(_w_tag('tc')))
  105. pr = ET.Element(_w_tag('trPr'))
  106. ET.SubElement(pr, _w_tag('gridBefore')).set(_w_tag('val'), '1')
  107. rows[2].insert(0, pr)
  108. filled = _fill_current_response_table(source, '资格条件响应表')
  109. _overlay_table_xml_text(xml, filled)
  110. self.assertEqual(first_response, ET.tostring(rows[1].findall(_w_tag('tc'))[3]))
  111. response = rows[2].findall(_w_tag('tc'))[2]
  112. self.assertEqual(text(response), '是')
  113. self.assertEqual(response.find('.//' + _w_tag('jc')).get(_w_tag('val')), 'right')
  114. self.assertIsNotNone(response.find('.//' + _w_tag('u')))
  115. self.assertEqual(len(response.findall(_w_tag('p'))), 1, '空单元格沿用原段落')
  116. once = ET.tostring(xml)
  117. _overlay_table_xml_text(xml, filled)
  118. self.assertEqual(once, ET.tostring(xml), '重复写出须幂等')
  119. if __name__ == '__main__':
  120. unittest.main()