"""响应表覆盖必须使用Word网格,原要求单元格不可压平或移位。""" import os import tempfile import unittest import xml.etree.ElementTree as ET from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from models import Chapter, ExtractedTable, TableCell, TenderAnalysis from step4_writing import ( _fill_current_response_table, _overlay_table_xml_text, _save_chapter_docx, _w_tag, ) def text(element): return ''.join(t.text or '' for t in element.iter(_w_tag('t'))) def canonical_cell(cell): return ET.tostring(ET.fromstring(cell._tc.xml)) class ResponseTableTests(unittest.TestCase): def fixture(self): doc = Document() table = doc.add_table(rows=4, cols=5) headers = ['类别', '序号', '资格要求', '是否响应', '证明材料'] for i, value in enumerate(headers): table.cell(0, i).text = value table.cell(1, 0).merge(table.cell(3, 0)).text = '资格' cells = [[TableCell(text=value, col=i) for i, value in enumerate(headers)]] for r in range(1, 4): table.cell(r, 1).text = str(r) p = table.cell(r, 2).paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.add_run(f'{r}、提供').bold = True p.add_run('示例证明。').underline = True table.cell(r, 2).add_paragraph('原说明必须保留。') cells.append([ TableCell(text='资格' if r == 1 else '', row=r, col=0, rowspan=3 if r == 1 else 1, is_merged_origin=r == 1, is_merged_continuation=r > 1), TableCell(text=str(r), row=r, col=1), TableCell(text=f'{r}、提供示例证明。\n原说明必须保留。', row=r, col=2), TableCell(row=r, col=3), TableCell(row=r, col=4), ]) source = ExtractedTable(rows=4, cols=5, cells=cells, table_type='qualification_response', source_type='tender_pdf') return doc, table, source def test_vertical_merge_does_not_shift_requirements_or_flatten_runs(self): _, table, source = self.fixture() xml = ET.fromstring(table._tbl.xml) before = [[ET.tostring(tc) for tc in row.findall(_w_tag('tc'))[:3]] for row in xml.findall(_w_tag('tr'))] filled = _fill_current_response_table(source, '资格条件响应表') _overlay_table_xml_text(xml, filled) after = [[ET.tostring(tc) for tc in row.findall(_w_tag('tc'))[:3]] for row in xml.findall(_w_tag('tr'))] self.assertEqual(before, after, '原要求、合并续格及多段混合格式必须原样保留') for row in xml.findall(_w_tag('tr'))[1:]: self.assertEqual(text(row.findall(_w_tag('tc'))[3]), '是') def test_horizontal_and_vertical_merge_keep_response_origin_only(self): _, table, source = self.fixture() table.cell(1, 3).merge(table.cell(2, 3)) source.cells[1][3].rowspan = 2 source.cells[1][3].is_merged_origin = True source.cells[2][3].is_merged_continuation = True table.cell(3, 1).merge(table.cell(3, 2)) source.cells[3][1].text = '3\n3、提供示例证明。\n原说明必须保留。' source.cells[3][1].colspan = 2 source.cells[3][1].is_merged_origin = True source.cells[3][2].is_merged_continuation = True filled = _fill_current_response_table(source, '资格条件响应表') self.assertEqual(filled.cells[2][3].text, '', '合并续格不得被填充') xml = ET.fromstring(table._tbl.xml) _overlay_table_xml_text(xml, filled) rows = xml.findall(_w_tag('tr')) self.assertEqual(text(rows[2].findall(_w_tag('tc'))[3]), '') self.assertEqual(text(rows[3].findall(_w_tag('tc'))[2]), '是') def test_actual_chapter_write_preserves_step1_requirement_xml(self): doc, table, source = self.fixture() with tempfile.TemporaryDirectory() as directory: source.artifact_path = os.path.join(directory, 'source.docx') doc.save(source.artifact_path) template = Document() template.add_paragraph('第一章 资格', style='Heading 1') template.add_paragraph('%%资格条件响应表%%') template.add_paragraph('第二章 报价', style='Heading 1') template_path = os.path.join(directory, 'template.docx') template.save(template_path) path = _save_chapter_docx( Chapter(id='1', title='资格', template_chapter_id='1'), directory, template_path=template_path, analysis=TenderAnalysis(project_name='示例项目', tender_tables=[source]), ) result = Document(path).tables[0] for r in range(1, 4): self.assertEqual(result.cell(r, 2).text, table.cell(r, 2).text) self.assertEqual(canonical_cell(result.cell(r, 2)), canonical_cell(table.cell(r, 2))) self.assertEqual(result.cell(r, 3).text, '是') self.assertEqual(source.cells[1][3].text, '', '填表不得污染Step1元数据') def test_grid_before_and_existing_response_are_preserved(self): _, table, source = self.fixture() table.cell(1, 3).text = '已有响应' table.cell(2, 3).paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.RIGHT table.cell(2, 3).paragraphs[0].add_run(' ').underline = True xml = ET.fromstring(table._tbl.xml) rows = xml.findall(_w_tag('tr')) first_response = ET.tostring(rows[1].findall(_w_tag('tc'))[3]) rows[2].remove(rows[2].find(_w_tag('tc'))) pr = ET.Element(_w_tag('trPr')) ET.SubElement(pr, _w_tag('gridBefore')).set(_w_tag('val'), '1') rows[2].insert(0, pr) filled = _fill_current_response_table(source, '资格条件响应表') _overlay_table_xml_text(xml, filled) self.assertEqual(first_response, ET.tostring(rows[1].findall(_w_tag('tc'))[3])) response = rows[2].findall(_w_tag('tc'))[2] self.assertEqual(text(response), '是') self.assertEqual(response.find('.//' + _w_tag('jc')).get(_w_tag('val')), 'right') self.assertIsNotNone(response.find('.//' + _w_tag('u'))) self.assertEqual(len(response.findall(_w_tag('p'))), 1, '空单元格沿用原段落') once = ET.tostring(xml) _overlay_table_xml_text(xml, filled) self.assertEqual(once, ET.tostring(xml), '重复写出须幂等') if __name__ == '__main__': unittest.main()