| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- """响应表覆盖必须使用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()
|