| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557 |
- import base64
- import json
- import os
- import tempfile
- import unittest
- from types import SimpleNamespace
- from unittest.mock import patch
- from docx import Document
- from docx.enum.section import WD_SECTION
- from docx.oxml import OxmlElement
- from models import (
- BidOutline,
- Chapter,
- ChapterType,
- CompanyInfo,
- ExtractedTable,
- ParsedDocument,
- ProjectData,
- RejectionItem,
- ScoringCriterion,
- FormatReconciliation,
- ReconciliationDecision,
- ReconciliationReport,
- TableCell,
- TenderAnalysis,
- )
- from step4_writing import (
- _defer_evaluation_index_chapter,
- _partition_step4_chapters,
- _persist_all_chapter_docs,
- _resolve_chapter_text_placeholders,
- _sanitize_bound_block,
- _save_chapter_docx,
- _select_placeholder_table,
- _write_single_chapter,
- )
- from step4_writing.content_injector import _ContentInjector
- from step4_writing.placeholder_engine import build_placeholder_map
- from step6_exporting.docx_builder import _insert_locked_scoring_sections
- class FakeBlockLlm:
- def __init__(self, blocks=None):
- self.blocks = blocks or []
- self.calls = []
- def extract_json(self, **kwargs):
- self.calls.append(kwargs)
- return {"blocks": self.blocks}
- class FakeWriter:
- def __init__(self, blocks=None):
- self.llm = FakeBlockLlm(blocks)
- def write_chapter(self, **_kwargs):
- raise AssertionError("模板章不得进入整章 LLM 攄写路径")
- def analysis():
- return TenderAnalysis(
- project_name="群众艺术馆项目",
- buyer_name="采购人甲",
- tender_project_id="XM-001",
- )
- class Step4BlockGenerationTests(unittest.TestCase):
- def test_table_source_priority_rejects_reconciliation_type_mismatch(self):
- wrong_scoring = ExtractedTable(
- table_id="tender-scoring",
- source_file="tender.pdf",
- source_type="tender_pdf",
- table_type="scoring",
- rows=8,
- cols=5,
- title_hint="客观分评审因素响应情况表",
- )
- tender_qualification = ExtractedTable(
- table_id="tender-qualification",
- source_file="tender.pdf",
- source_type="tender_pdf",
- table_type="qualification_response",
- rows=5,
- cols=5,
- title_hint="资格条件响应表",
- )
- reference_qualification = ExtractedTable(
- table_id="reference-qualification",
- source_file="reference.docx",
- source_type="reference_docx",
- table_type="qualification_response",
- rows=5,
- cols=5,
- title_hint="资格条件响应表",
- )
- report = ReconciliationReport(reconciliations=[FormatReconciliation(
- section_name="资格条件响应表",
- template_table=ExtractedTable(table_type="qualification_response"),
- best_match=wrong_scoring,
- replace_with_tender_table=wrong_scoring,
- decision=ReconciliationDecision.REPLACE_WITH_TENDER,
- )])
- project_data = ProjectData(
- project_id="test",
- project_name="测试项目",
- tender_docs=[ParsedDocument(
- file_path="tender.pdf",
- file_name="tender.pdf",
- content="",
- doc_type="tender",
- )],
- reference_tables=[reference_qualification],
- metadata={"reconciliation_report": report},
- )
- analysis = TenderAnalysis(project_name="测试项目", tender_tables=[
- wrong_scoring,
- tender_qualification,
- ])
- selected = _select_placeholder_table(
- "资格条件响应表", analysis, project_data
- )
- self.assertIs(selected, tender_qualification)
- def test_qualification_placeholder_uses_current_tender_rows_not_wrong_match(self):
- def cells(rows):
- return [
- [TableCell(text=value, row=ri, col=ci) for ci, value in enumerate(row)]
- for ri, row in enumerate(rows)
- ]
- wrong_scoring = ExtractedTable(
- table_id="scoring",
- source_file="tender.pdf",
- source_type="tender_pdf",
- table_type="scoring",
- rows=2,
- cols=5,
- cells=cells([
- ["序号", "名称", "是否响应", "响应情况", "页码"],
- ["1", "历史错表", "", "", ""],
- ]),
- )
- current_qualification = ExtractedTable(
- table_id="qualification",
- source_file="tender.pdf",
- source_type="tender_pdf",
- table_type="qualification_response",
- rows=2,
- cols=5,
- title_hint="资格条件响应表",
- cells=cells([
- ["项目内容", "具备的条件说明(要求)", "投标检查项(响应内容说明(是/否))", "详细内容所对应电子投标文件名称", "备注"],
- ["当前资格", "当前招标资格要求", "", "", ""],
- ]),
- )
- report = ReconciliationReport(reconciliations=[FormatReconciliation(
- section_name="资格条件响应表",
- template_table=ExtractedTable(table_type="qualification_response"),
- best_match=wrong_scoring,
- replace_with_tender_table=wrong_scoring,
- decision=ReconciliationDecision.REPLACE_WITH_TENDER,
- )])
- project_data = ProjectData(
- project_id="test",
- project_name="当前项目",
- tender_docs=[ParsedDocument(
- file_path="tender.pdf", file_name="tender.pdf", content="",
- doc_type="tender",
- )],
- metadata={"reconciliation_report": report},
- )
- analysis = TenderAnalysis(
- project_name="当前项目",
- tender_tables=[wrong_scoring, current_qualification],
- )
- chapter = Chapter(id="1", title="投标人资格、资信证明", level=1)
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第一章:投标人资格、资信证明", style="Heading 1")
- template.add_paragraph("%%资格条件响应表%%")
- template.add_paragraph("第二章:投标报价", style="Heading 1")
- template.save(template_path)
- output_path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- analysis=analysis,
- project_data=project_data,
- )
- artifact = Document(output_path)
- values = [
- cell.text
- for table in artifact.tables
- for row in table.rows
- for cell in row.cells
- ]
- self.assertIn("当前招标资格要求", values)
- self.assertIn("是", values)
- self.assertIn("投标文件对应章节", values)
- self.assertNotIn("历史错表", values)
- def test_table_source_priority_is_tender_then_procurement_then_reference(self):
- def table(table_id, path, source_type):
- return ExtractedTable(
- table_id=table_id,
- source_file=path,
- source_type=source_type,
- table_type="qualification_response",
- title_hint="资格条件响应表",
- )
- tender = table("tender", "tender.pdf", "tender_pdf")
- procurement = table("procurement", "procurement.docx", "procurement_docx")
- reference = table("reference", "reference.docx", "reference_docx")
- project_data = ProjectData(
- project_id="test",
- project_name="测试项目",
- tender_docs=[ParsedDocument(
- file_path="tender.pdf", file_name="tender.pdf", content="",
- doc_type="tender"
- )],
- procurement_docs=[ParsedDocument(
- file_path="procurement.docx",
- file_name="procurement.docx",
- content="",
- doc_type="procurement",
- )],
- reference_tables=[reference],
- )
- selected = _select_placeholder_table(
- "资格条件响应表",
- TenderAnalysis(
- project_name="测试项目",
- tender_tables=[reference, procurement, tender],
- ),
- project_data,
- )
- self.assertIs(selected, tender)
- selected_without_tender = _select_placeholder_table(
- "资格条件响应表",
- TenderAnalysis(
- project_name="测试项目",
- tender_tables=[reference, procurement],
- ),
- project_data,
- )
- self.assertIs(selected_without_tender, procurement)
- selected_reference_only = _select_placeholder_table(
- "资格条件响应表",
- TenderAnalysis(project_name="测试项目", tender_tables=[]),
- project_data,
- )
- self.assertIs(selected_reference_only, reference)
- def test_generic_table_name_uses_same_priority_without_known_type(self):
- def table(table_id, path, source_type, title):
- return ExtractedTable(
- table_id=table_id,
- source_file=path,
- source_type=source_type,
- table_type="other",
- title_hint=title,
- )
- tender = table("tender", "tender.pdf", "tender_pdf", "拟投入设备清单表")
- procurement = table(
- "procurement", "procurement.docx", "procurement_docx", "拟投入设备清单表"
- )
- reference = table(
- "reference", "reference.docx", "reference_docx", "拟投入设备清单表"
- )
- wrong = table("wrong", "tender.pdf", "tender_pdf", "人员配置表")
- project_data = ProjectData(
- project_id="test",
- project_name="测试项目",
- tender_docs=[ParsedDocument(
- file_path="tender.pdf", file_name="tender.pdf", content="",
- doc_type="tender",
- )],
- procurement_docs=[ParsedDocument(
- file_path="procurement.docx", file_name="procurement.docx", content="",
- doc_type="procurement",
- )],
- reference_tables=[reference],
- metadata={"reconciliation_report": ReconciliationReport(
- reconciliations=[FormatReconciliation(
- section_name="拟投入设备清单表",
- template_table=ExtractedTable(title_hint="拟投入设备清单表"),
- best_match=wrong,
- replace_with_tender_table=wrong,
- decision=ReconciliationDecision.REPLACE_WITH_TENDER,
- )],
- )},
- )
- selected = _select_placeholder_table(
- "拟投入设备清单表",
- TenderAnalysis(
- project_name="测试项目",
- tender_tables=[wrong, reference, procurement, tender],
- ),
- project_data,
- )
- self.assertIs(selected, tender)
- def test_template_fill_precedes_scoring_supplement_block(self):
- child = Chapter(
- id="4.1", title="一、需求分析", level=2,
- direct_scoring_criteria=["SC-01"],
- direct_scoring_bindings=["SC-01#1"],
- content_generation_mode="template_project_scoring",
- )
- chapter = Chapter(
- id="4", title="服务方案", level=1,
- chapter_type=ChapterType.TECHNICAL,
- children=[child],
- )
- outline = BidOutline(
- project_name="群众艺术馆项目",
- chapters=[chapter],
- evaluation_index_entries=[{
- "entry_type": "scoring",
- "source_id": "SC-01#1",
- "criterion_id": "SC-01",
- "display_name": "需求分析",
- "requirement": "结合项目特点分析需求",
- "final_heading_id": "4.1",
- }],
- )
- writer = FakeWriter([{
- "node_id": "4.1",
- "content": "围绕群众艺术馆开放特点形成针对性服务安排。",
- }])
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="项目名称:%%项目名称%%\n一、需求分析\n模板原文\n[图片块保持原位]",
- ):
- result = _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, [], ["招标要求"]),
- analysis=analysis(),
- company_info=CompanyInfo(full_name="测试公司"),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={"项目名称": "群众艺术馆项目"},
- table_fill_planned=True,
- )
- self.assertTrue(result.preserve_template_layout)
- self.assertTrue(result.template_fill_completed)
- self.assertIn("项目名称:群众艺术馆项目", result.generated_content)
- self.assertIn("[图片块保持原位]", result.generated_content)
- self.assertNotIn("针对性服务安排", result.generated_content)
- self.assertIn("针对性服务安排", child.supplement_content)
- self.assertEqual(
- [block["block_type"] for block in result.content_blocks],
- ["template_base", "native_table_plan", "scoring_supplement"],
- )
- self.assertEqual(child.content_blocks[0]["order"], 2)
- def test_missing_scoring_blocks_are_retried_for_full_coverage(self):
- child1 = Chapter(
- id="3.1",
- title="一、服务定位及预期目标",
- level=2,
- direct_scoring_bindings=["SC-02#1"],
- )
- child2 = Chapter(
- id="3.4",
- title="四、重点难点分析及应对措施",
- level=2,
- direct_scoring_bindings=["SC-02#2"],
- )
- child3 = Chapter(
- id="3.5",
- title="五、服务保障措施",
- level=2,
- direct_scoring_bindings=["SC-02#3"],
- )
- chapter = Chapter(
- id="3",
- title="需求理解",
- children=[child1, child2, child3],
- )
- outline = BidOutline(
- project_name="测试项目",
- chapters=[chapter],
- evaluation_index_entries=[
- {
- "entry_type": "scoring",
- "source_id": "SC-02#1",
- "criterion_id": "SC-02",
- "display_name": "服务定位及预期目标",
- "requirement": "服务定位分析及目标合理性",
- "final_heading_id": "3.1",
- },
- {
- "entry_type": "scoring",
- "source_id": "SC-02#2",
- "criterion_id": "SC-02",
- "display_name": "重点难点分析及应对措施",
- "requirement": "重点难点分析深度及应对措施",
- "final_heading_id": "3.4",
- },
- {
- "entry_type": "scoring",
- "source_id": "SC-02#3",
- "criterion_id": "SC-02",
- "display_name": "服务保障措施",
- "requirement": "服务保障措施要求",
- "final_heading_id": "3.5",
- },
- ],
- )
- class PartialLlm:
- def __init__(self, responses):
- self.responses = list(responses)
- self.calls = []
- def extract_json(self, **kwargs):
- self.calls.append(kwargs)
- return self.responses.pop(0)
- class PartialWriter:
- def __init__(self, responses):
- self.llm = PartialLlm(responses)
- writer = PartialWriter([
- {"blocks": [{"node_id": "3.1", "content": "服务定位正文"}]},
- {"blocks": [{"node_id": "3.1", "content": "服务定位正文"}]},
- {"blocks": [{"node_id": "3.1", "content": "服务定位正文"}]},
- {"blocks": [{"node_id": "3.1", "content": "服务定位正文"}]},
- {"blocks": [
- {"node_id": "3.4", "content": "重点难点正文"},
- {"node_id": "3.5", "content": "服务保障正文"},
- ]},
- ])
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="第三章模板原文",
- ):
- result = _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, [], ["招标要求"]),
- analysis=analysis(),
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertEqual(len(writer.llm.calls), 5)
- self.assertEqual(
- [call["max_tokens"] for call in writer.llm.calls],
- [32768, 65536, 65536, 65536, 65536],
- )
- self.assertEqual(child1.supplement_content, "服务定位正文")
- self.assertEqual(child2.supplement_content, "重点难点正文")
- self.assertEqual(child3.supplement_content, "服务保障正文")
- self.assertTrue(result.preserve_template_layout)
- def test_scoring_supplement_prompt_includes_existing_template_content(self):
- child = Chapter(
- id="1.18",
- title="十八、保险购买承诺",
- level=2,
- template_original_title="十八、保险购买承诺",
- direct_scoring_bindings=["SC-14#9"],
- )
- chapter = Chapter(
- id="1",
- title="投标人资格、资信证明",
- children=[child],
- )
- outline = BidOutline(
- project_name="测试项目",
- chapters=[chapter],
- evaluation_index_entries=[{
- "entry_type": "scoring",
- "source_id": "SC-14#9",
- "criterion_id": "SC-14",
- "display_name": "保险购买承诺",
- "requirement": "承诺购买足额保险",
- "final_heading_id": "1.18",
- }],
- )
- writer = FakeWriter([{
- "node_id": "1.18",
- "content": "补充正文",
- }])
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value=(
- "十八、保险购买承诺\n"
- "我方已承诺购买足额雇主责任险和公众责任险。\n"
- "模板其他原文"
- ),
- ):
- _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, []),
- analysis=analysis(),
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertEqual(len(writer.llm.calls), 1)
- self.assertIn(
- "我方已承诺购买足额雇主责任险和公众责任险。",
- writer.llm.calls[0]["user_prompt"],
- )
- self.assertEqual(child.supplement_content, "补充正文")
- def test_invalid_chapter_reference_is_sanitized(self):
- child = Chapter(
- id="8.2",
- title="二、服务人员专业素质与职业资格证书",
- level=2,
- direct_scoring_bindings=["SC-10#3"],
- )
- chapter = Chapter(
- id="8",
- title="服务人员配置承诺",
- children=[child],
- )
- outline = BidOutline(
- project_name="测试项目",
- chapters=[chapter],
- evaluation_index_entries=[{
- "entry_type": "scoring",
- "source_id": "SC-10#3",
- "criterion_id": "SC-10",
- "display_name": "服务人员专业素质与职业资格证书",
- "requirement": "提供人员专业素质与资格证书材料",
- "final_heading_id": "8.2",
- }],
- )
- writer = FakeWriter([{
- "node_id": "8.2",
- "content": (
- "具体证明材料及证书复印件,详见投标文件"
- "第十二章“项目服务人员配置”相应表单及附件。"
- ),
- }])
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="八、服务人员配置承诺\n模板原文",
- ):
- _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, []),
- analysis=analysis(),
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertNotIn("第十二章", child.supplement_content)
- self.assertIn("本投标文件相应章节及附件", child.supplement_content)
- def test_placeholder_replacement_collapses_duplicate_periods(self):
- filled = _resolve_chapter_text_placeholders(
- "服务内容:%%服务内容%%。",
- {"服务内容": "包括建筑物管理、设施设备管理、保安、保洁等内容。"},
- )
- self.assertNotIn("。。", filled)
- self.assertTrue(filled.endswith("。"))
- self.assertIn("包括建筑物管理", filled)
- filled_without_template_period = _resolve_chapter_text_placeholders(
- "服务内容:%%服务内容%%服务要求:%%服务要求%%",
- {"服务内容": "内容", "服务要求": "要求"},
- )
- self.assertIn("内容。服务要求:要求。", filled_without_template_period)
- def test_direct_h1_supplement_does_not_replace_template_block_plan(self):
- chapter = Chapter(
- id="1", title="资格证明", chapter_type=ChapterType.BUSINESS,
- direct_scoring_bindings=["SC-01#1"],
- )
- outline = BidOutline(
- project_name="测试",
- chapters=[chapter],
- evaluation_index_entries=[{
- "entry_type": "scoring",
- "source_id": "SC-01#1",
- "display_name": "资格证明",
- "requirement": "提供有效证明",
- "final_heading_id": "1",
- }],
- )
- writer = FakeWriter([{"node_id": "1", "content": "证明材料真实有效。"}])
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="资格证明模板原文",
- ):
- _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, []),
- analysis=analysis(),
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertEqual(
- [block["block_type"] for block in chapter.content_blocks],
- ["template_base", "native_table_plan", "scoring_supplement"],
- )
- def test_template_without_direct_binding_uses_no_llm(self):
- chapter = Chapter(id="2", title="投标报价", chapter_type=ChapterType.BUSINESS)
- outline = BidOutline(project_name="测试", chapters=[chapter])
- writer = FakeWriter()
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="投标报价模板原文",
- ):
- _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, []),
- analysis=analysis(),
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertEqual(chapter.generated_content, "投标报价模板原文")
- self.assertEqual(writer.llm.calls, [])
- def test_business_and_technical_are_ordinary_but_index_is_deferred(self):
- business = Chapter(id="1", title="资格证明", chapter_type=ChapterType.BUSINESS)
- technical = Chapter(id="3", title="服务方案", chapter_type=ChapterType.TECHNICAL)
- index = Chapter(
- id="index", title="与评标有关的投标文件主要内容索引表",
- chapter_type=ChapterType.BUSINESS,
- )
- ordinary, deferred = _partition_step4_chapters(
- BidOutline(project_name="测试", chapters=[index, business, technical])
- )
- self.assertEqual({node.id for node in ordinary}, {"1", "3"})
- self.assertEqual([node.id for node in deferred], ["index"])
- def test_evaluation_index_keeps_template_without_generating_rows_or_pages(self):
- chapter = Chapter(
- id="index",
- title="与评标有关的投标文件主要内容索引表",
- chapter_type=ChapterType.BUSINESS,
- content_blocks=[{"block_type": "deferred_evaluation_index"}],
- )
- result = _defer_evaluation_index_chapter(
- chapter=chapter,
- template_text="索引表模板原文\n详细说明见投标文件页码:",
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- self.assertEqual(result.generated_content, "索引表模板原文\n详细说明见投标文件页码:")
- self.assertTrue(result.preserve_template_layout)
- self.assertEqual(
- [block["block_type"] for block in result.content_blocks],
- ["template_base", "deferred_evaluation_index"],
- )
- self.assertEqual(
- result.content_blocks[1]["action"],
- "build_rows_and_pages_after_final_merge_and_pagination",
- )
- def test_deferred_empty_index_passes_artifact_gate_with_deferred_status(self):
- ordinary = Chapter(
- id="1", title="资格证明", chapter_type=ChapterType.BUSINESS,
- generated_content="资格证明正文",
- )
- index = _defer_evaluation_index_chapter(
- chapter=Chapter(
- id="index",
- title="与评标有关的投标文件主要内容索引表",
- chapter_type=ChapterType.BUSINESS,
- ),
- template_text="",
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- outline = BidOutline(project_name="测试项目", chapters=[ordinary, index])
- with tempfile.TemporaryDirectory() as directory:
- paths = _persist_all_chapter_docs(outline, directory)
- manifest_path = os.path.join(
- directory, "chapters", "测试项目", "manifest.json"
- )
- with open(manifest_path, encoding="utf-8") as file:
- manifest = json.load(file)
- self.assertEqual(len(paths), 2)
- self.assertTrue(all(os.path.isfile(path) for path in paths))
- self.assertEqual(
- manifest["chapters"][1]["status"],
- "deferred_until_final_pagination",
- )
- def test_chapter_artifact_uses_final_outline_titles_and_order(self):
- reused = Chapter(
- id="3.1",
- title="一、服务定位与预期目标分析",
- level=2,
- template_original_title="一、服务目标定位",
- supplement_content="评分补充正文一",
- )
- inserted = Chapter(
- id="3.2",
- title="二、重点难点分析与应对措施",
- level=2,
- supplement_content="评分补充正文二",
- )
- chapter = Chapter(
- id="3",
- title="需求理解",
- children=[reused, inserted],
- generated_content=(
- "第三章:需求理解\n"
- "一、服务目标定位\n"
- "模板原有定位正文"
- ),
- )
- with tempfile.TemporaryDirectory() as directory:
- path = _save_chapter_docx(chapter, directory)
- artifact = Document(path)
- headings = [
- paragraph.text for paragraph in artifact.paragraphs
- if paragraph.style.name.startswith("Heading")
- ]
- all_text = "\n".join(paragraph.text for paragraph in artifact.paragraphs)
- self.assertEqual(
- headings,
- [
- "第三章 需求理解",
- "一、服务定位与预期目标分析",
- "二、重点难点分析与应对措施",
- ],
- )
- self.assertNotIn("一、服务目标定位", all_text)
- self.assertIn("模板原有定位正文", all_text)
- self.assertIn("评分补充正文一", all_text)
- self.assertIn("评分补充正文二", all_text)
- def test_native_chapter_excludes_next_part_marker_and_inserts_heading_before_section_break(self):
- import zipfile
- import xml.etree.ElementTree as ET
- child = Chapter(
- id="2.5",
- title="五、报价得分",
- level=2,
- supplement_content="报价得分补充正文",
- )
- chapter = Chapter(
- id="2",
- title="投标报价",
- template_chapter_id="2",
- children=[child],
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第二章:投标报价", style="Heading 1")
- template.add_paragraph("四、商务报价说明", style="Heading 2")
- template.add_paragraph("模板报价正文")
- template.add_paragraph("技术部分")
- template.add_paragraph("第三章:需求理解", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- word_ns = (
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
- )
- with zipfile.ZipFile(path) as package:
- root = ET.fromstring(package.read("word/document.xml"))
- body = root.find(f"{{{word_ns}}}body")
- children = list(body)
- sect_indexes = [
- index for index, element in enumerate(children)
- if element.tag == f"{{{word_ns}}}sectPr"
- ]
- headings = [
- paragraph.text for paragraph in artifact.paragraphs
- if paragraph.style.name.startswith("Heading")
- ]
- all_text = "\n".join(paragraph.text for paragraph in artifact.paragraphs)
- self.assertEqual(
- headings,
- ["第二章 投标报价", "四、商务报价说明", "五、报价得分"],
- )
- self.assertNotIn("技术部分", all_text)
- self.assertIn("报价得分补充正文", all_text)
- self.assertTrue(sect_indexes)
- self.assertEqual(sect_indexes[-1], len(children) - 1)
- scoring_index = next(
- index for index, element in enumerate(children)
- if element.tag == f"{{{word_ns}}}p"
- and "五、报价得分" in "".join(
- node.text or "" for node in element.iter(f"{{{word_ns}}}t")
- )
- )
- self.assertLess(scoring_index, sect_indexes[-1])
- def test_duplicate_sibling_titles_anchor_to_own_parent_not_reinserted(self):
- section23 = Chapter(
- id="1.23",
- title="二十三、近三年类似项目业绩",
- level=2,
- template_original_title="二十三、近三年类似项目业绩",
- children=[Chapter(
- id="1.23.1",
- title="(一)国家税务总局上海市青浦区税务局",
- level=3,
- template_original_title="(一)国家税务总局上海市青浦区税务局",
- )],
- )
- section24 = Chapter(
- id="1.24",
- title="二十四、类似业绩业主评价",
- level=2,
- template_original_title="二十四、类似业绩业主评价",
- children=[
- Chapter(
- id="1.24.1",
- title="(一)国家税务总局上海市青浦区税务局",
- level=3,
- template_original_title="(一)国家税务总局上海市青浦区税务局",
- ),
- Chapter(
- id="1.24.10",
- title="(十)嘉定司法中心",
- level=3,
- template_original_title="(十)嘉定司法中心",
- ),
- ],
- )
- chapter = Chapter(
- id="1",
- title="投标人资格、资信证明",
- template_chapter_id="1",
- children=[section23, section24],
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第一章:投标人资格、资信证明", style="Heading 1")
- template.add_paragraph(
- "二十三、近三年类似项目业绩", style="Heading 2"
- )
- template.add_paragraph(
- "(一)国家税务总局上海市青浦区税务局", style="Heading 3"
- )
- template.add_paragraph(
- "二十四、类似业绩业主评价", style="Heading 2"
- )
- template.add_paragraph(
- "(一)国家税务总局上海市青浦区税务局", style="Heading 3"
- )
- template.add_paragraph("(十)嘉定司法中心", style="Heading 3")
- template.add_paragraph("第二章:其他章节", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- headings = [
- paragraph.text for paragraph in artifact.paragraphs
- if paragraph.style.name.startswith("Heading")
- ]
- self.assertEqual(
- headings,
- [
- "第一章 投标人资格、资信证明",
- "二十三、近三年类似项目业绩",
- "(一)国家税务总局上海市青浦区税务局",
- "二十四、类似业绩业主评价",
- "(一)国家税务总局上海市青浦区税务局",
- "(十)嘉定司法中心",
- ],
- )
- self.assertEqual(headings.count("二十四、类似业绩业主评价"), 1)
- self.assertEqual(
- headings.count("(一)国家税务总局上海市青浦区税务局"),
- 2,
- )
- def test_rejection_supplements_are_visible_in_bound_artifact_section(self):
- commitment = Chapter(
- id="1.17",
- title="十七、要求承诺函",
- level=2,
- template_original_title="十七、要求承诺函",
- direct_rejection_bindings=["RI-01", "RI-02"],
- )
- chapter = Chapter(
- id="1",
- title="投标人资格、资信证明",
- chapter_type=ChapterType.BUSINESS,
- children=[commitment],
- )
- outline = BidOutline(
- project_name="测试",
- chapters=[chapter],
- evaluation_index_entries=[
- {
- "entry_id": "RI-01", "entry_type": "rejection",
- "source_id": "RI-01", "requirement": "投标文件必须签署",
- "final_heading_id": "1.17",
- },
- {
- "entry_id": "RI-02", "entry_type": "rejection",
- "source_id": "RI-02", "requirement": "证明材料必须有效",
- "final_heading_id": "1.17",
- },
- ],
- )
- tender_analysis = analysis()
- tender_analysis.rejection_items = [
- RejectionItem(
- id="RI-01", category="投标文件编制",
- description="投标文件必须签署,否则投标无效。",
- ),
- RejectionItem(
- id="RI-02", category="证明材料",
- description="证明材料必须有效,否则投标无效。",
- ),
- ]
- writer = FakeWriter()
- with patch(
- "step4_writing._get_template_text_for_chapter",
- return_value="十七、要求承诺函\n模板承诺原文",
- ):
- _write_single_chapter(
- chapter=chapter,
- writer=writer,
- injector=_ContentInjector({}, []),
- analysis=tender_analysis,
- company_info=CompanyInfo(),
- template_structure=object(),
- cfg=SimpleNamespace(enforce_word_limit=False),
- outline=outline,
- placeholder_map={},
- )
- self.assertEqual(writer.llm.calls, [])
- self.assertIn("投标文件必须签署", commitment.supplement_content)
- self.assertIn("证明材料必须有效", commitment.supplement_content)
- self.assertNotIn("我方已充分理解并严格响应该项", commitment.supplement_content)
- self.assertEqual(len(commitment.supplement_content.splitlines()), 3)
- with tempfile.TemporaryDirectory() as directory:
- path = _save_chapter_docx(chapter, directory)
- artifact = Document(path)
- all_text = "\n".join(paragraph.text for paragraph in artifact.paragraphs)
- self.assertIn("十七、要求承诺函", all_text)
- self.assertIn("模板承诺原文", all_text)
- self.assertIn("投标文件必须签署", all_text)
- self.assertIn("证明材料必须有效", all_text)
- def test_native_commitment_placeholder_receives_compact_rejection_block_once(self):
- commitment = Chapter(
- id="1.17",
- title="十七、要求承诺函",
- level=2,
- template_original_title="十七、要求承诺函",
- direct_rejection_bindings=["RI-01", "RI-02"],
- supplement_content=(
- "我方郑重承诺:严格遵守全部实质性要求。\n"
- "投标文件编制方面:投标文件完整签署。\n"
- "证明材料方面:证明材料真实有效。"
- ),
- )
- chapter = Chapter(
- id="1",
- title="投标人资格、资信证明",
- template_chapter_id="1",
- children=[commitment],
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph(
- "第一章:投标人资格、资信证明", style="Heading 1"
- )
- template.add_paragraph("十七、要求承诺函", style="Heading 2")
- template.add_paragraph("模板承诺原文")
- template.add_paragraph("%%承诺函%%")
- template.add_paragraph("如有违反,愿承担相应责任。")
- template.add_paragraph("投标人授权代表签字:")
- template.add_paragraph("第二章:投标报价", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- lines = [paragraph.text for paragraph in artifact.paragraphs]
- all_text = "\n".join(lines)
- self.assertNotIn("%%承诺函%%", all_text)
- self.assertEqual(all_text.count("我方郑重承诺"), 1)
- self.assertLess(
- lines.index("证明材料方面:证明材料真实有效。"),
- lines.index("如有违反,愿承担相应责任。"),
- )
- self.assertLess(
- lines.index("如有违反,愿承担相应责任。"),
- lines.index("投标人授权代表签字:"),
- )
- def test_native_template_numeric_conflict_uses_current_scoring_requirement(self):
- child = Chapter(
- id="4.2.6",
- title="(六)人员流动率承诺",
- level=3,
- template_original_title="(六)人员稳定保障",
- direct_scoring_criteria=["SC-11"],
- supplement_content="我方承诺一年内项目人员流动率不超过20%。",
- )
- chapter = Chapter(
- id="4",
- title="基本服务方案",
- template_chapter_id="4",
- children=[child],
- )
- tender_analysis = analysis()
- tender_analysis.scoring_criteria = [ScoringCriterion(
- id="SC-11",
- category="技术方案",
- name="服务承诺",
- description="承诺一年内项目人员流动率不超过20%的得分。",
- max_score=1,
- )]
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第四章:基本服务方案", style="Heading 1")
- template.add_paragraph("(六)人员稳定保障", style="Heading 3")
- template.add_paragraph(
- "公司制定人员稳定方案,确保一年内人员流动率低于15%。"
- )
- template.add_paragraph("第五章:其他", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=tender_analysis,
- )
- artifact = Document(path)
- all_text = "\n".join(paragraph.text for paragraph in artifact.paragraphs)
- self.assertNotIn("15%", all_text)
- self.assertIn("人员流动率不超过20%", all_text)
- def test_native_template_table_and_image_survive_step4_artifact(self):
- chapter = Chapter(
- id="3",
- title="需求理解",
- template_chapter_id="3",
- children=[Chapter(
- id="3.1",
- title="一、服务定位与预期目标分析",
- level=2,
- template_original_title="一、服务目标定位",
- supplement_content="评分补充正文",
- )],
- )
- with tempfile.TemporaryDirectory() as directory:
- image_path = os.path.join(directory, "quality.png")
- with open(image_path, "wb") as file:
- file.write(base64.b64decode(
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwC"
- "AAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- ))
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第三章:需求理解", style="Heading 1")
- template.add_paragraph("一、服务目标定位", style="Heading 2")
- template.add_paragraph("品质督导管理体系")
- template.add_picture(image_path)
- table = template.add_table(rows=1, cols=2)
- table.cell(0, 0).text = "项目名称"
- table.cell(0, 1).text = "%%项目名称%%"
- template.add_paragraph("第四章:其他章节", style="Heading 1")
- template.add_paragraph("不应进入第三章产物")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={"项目名称": "群众艺术馆项目"},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- headings = [
- paragraph.text for paragraph in artifact.paragraphs
- if paragraph.style.name.startswith("Heading")
- ]
- all_text = "\n".join(paragraph.text for paragraph in artifact.paragraphs)
- self.assertEqual(
- headings,
- ["第三章 需求理解", "一、服务定位与预期目标分析"],
- )
- self.assertEqual(len(artifact.tables), 1)
- self.assertEqual(len(artifact.inline_shapes), 1)
- self.assertEqual(
- artifact.tables[0].cell(0, 1).text,
- "群众艺术馆项目",
- )
- self.assertIn("品质督导管理体系", all_text)
- self.assertIn("评分补充正文", all_text)
- self.assertNotIn("不应进入第三章产物", all_text)
- def test_step2_project_fields_fill_native_table_cells_and_table_placeholder(self):
- chapter = Chapter(id="2", title="投标报价", template_chapter_id="2")
- summary_table = ExtractedTable(
- table_id="summary",
- source_type="tender_pdf",
- rows=2,
- cols=2,
- table_type="bid_summary_table",
- col_widths=[0.35, 0.65],
- cells=[
- [
- TableCell(text="项目", row=0, col=0, bold=True, alignment="center"),
- TableCell(text="报价说明", row=0, col=1, bold=True, alignment="center"),
- ],
- [
- TableCell(text="本项目", row=1, col=0),
- TableCell(text="按招标文件报价", row=1, col=1),
- ],
- ],
- )
- detail_table = ExtractedTable(
- table_id="reference-detail",
- source_type="reference_docx",
- rows=2,
- cols=2,
- table_type="bid_detail_table",
- title_hint="投标报价分项明细表",
- header_fields=["项目名称:历史物业项目", "项目编号:OLD-001"],
- cells=[
- [
- TableCell(text="费用项目", row=0, col=0, bold=True),
- TableCell(text="金额(元)", row=0, col=1, bold=True),
- ],
- [
- TableCell(text="人员费用", row=1, col=0),
- TableCell(text="12345", row=1, col=1),
- ],
- ],
- )
- tender_analysis = TenderAnalysis(
- project_name="群众艺术馆项目",
- agency_name="采购中心",
- service_requirements=(
- "### 服务范围与内容\n- 建筑物管理\n- 设施设备管理\n"
- "### 服务标准\n- 按采购需求质量标准执行\n"
- "- 接受采购人考核\n"
- "- **服务期限**:自合同签订之日起三年"
- ),
- tender_tables=[summary_table],
- )
- project_data = ProjectData(
- project_id="XM-001",
- project_name="群众艺术馆项目",
- tender_docs=[ParsedDocument(
- file_path="tender.pdf",
- file_name="tender.pdf",
- content="服务期限:自合同签订之日起三年",
- doc_type="tender",
- )],
- reference_tables=[detail_table],
- )
- placeholder_map = build_placeholder_map(
- tender_analysis, CompanyInfo(), project_data
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- template.add_paragraph("第二章:投标报价", style="Heading 1")
- template.add_paragraph("%%项目名称%%")
- template.add_paragraph("采购机构:%%采购机构名称%%")
- opening = template.add_table(rows=1, cols=3)
- opening.cell(0, 0).text = "%%服务内容%%"
- opening.cell(0, 1).text = "%%服务要求%%"
- opening.cell(0, 2).text = "%%服务期限%%"
- template.add_paragraph("%%报价汇总表%%")
- template.add_paragraph("%%报价明细表%%")
- template.add_paragraph("第三章:其他", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map=placeholder_map,
- company_info=CompanyInfo(),
- analysis=tender_analysis,
- project_data=project_data,
- )
- artifact = Document(path)
- all_text = "\n".join(
- [paragraph.text for paragraph in artifact.paragraphs]
- + [cell.text for table in artifact.tables for row in table.rows for cell in row.cells]
- )
- self.assertEqual(len(artifact.tables), 3)
- self.assertNotIn("%%", all_text)
- self.assertIn("采购中心", all_text)
- self.assertIn("建筑物管理", all_text)
- self.assertIn("按采购需求质量标准执行", all_text)
- self.assertIn("自合同签订之日起三年", all_text)
- self.assertIn("按招标文件报价", all_text)
- self.assertIn("群众艺术馆项目", all_text)
- self.assertIn("待投标报价确认", all_text)
- self.assertNotIn("历史物业项目", all_text)
- self.assertNotIn("12345", all_text)
- def test_supplement_sanitizer_removes_tables_and_heading_numbering(self):
- content = _sanitize_bound_block(
- "## 分析\n一、项目特点\n正文\n| 列1 | 列2 |\n|---|---|"
- )
- self.assertNotIn("|", content)
- self.assertNotIn("一、", content)
- self.assertIn("项目特点:", content)
- def test_scoring_block_is_inserted_after_image_before_next_heading(self):
- doc = Document()
- doc.add_paragraph("第四章:服务方案", style="Heading 1")
- heading = doc.add_paragraph("一、需求分析", style="Heading 2")
- doc.add_paragraph("模板正文")
- image_para = doc.add_paragraph()
- image_para.add_run()._element.append(OxmlElement("w:drawing"))
- next_heading = doc.add_paragraph("二、下一模板节", style="Heading 2")
- child = Chapter(
- id="4.1", title="一、需求分析", level=2,
- direct_scoring_criteria=["SC-01"],
- supplement_content="评分补充正文",
- structure_locked=True,
- )
- chapter = Chapter(id="4", title="服务方案", children=[child])
- body = doc.element.body
- region = list(body)[:-1]
- inserted = _insert_locked_scoring_sections(
- doc, body, region, chapter, {}, augment_existing=True
- )
- elements = list(body)
- supplement = next(
- paragraph._element for paragraph in doc.paragraphs
- if paragraph.text == "评分补充正文"
- )
- self.assertEqual(inserted, 1)
- self.assertLess(elements.index(heading._element), elements.index(image_para._element))
- self.assertLess(elements.index(image_para._element), elements.index(supplement))
- self.assertLess(elements.index(supplement), elements.index(next_heading._element))
- class Step4HeaderFooterTests(unittest.TestCase):
- def test_native_chapter_keeps_header_footer_and_replaces_project_name(self):
- chapter = Chapter(
- id="3",
- title="需求理解",
- template_chapter_id="3",
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- header = template.sections[0].header
- placeholder_para = header.paragraphs[0]
- placeholder_para.add_run("%%项目名称%%项目投标文件")
- legacy_para = header.add_paragraph()
- legacy_para.add_run("上海市建筑工程学校")
- legacy_para.add_run("物业管理服务项目投标文件")
- footer = template.sections[0].footer
- footer.paragraphs[0].text = "第 1 页"
- template.add_paragraph("第三章:需求理解", style="Heading 1")
- template.add_paragraph("模板正文")
- template.add_paragraph("第四章:其他章节", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={
- "项目名称": "上海市群众艺术馆物业管理服务",
- },
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- header_text = "\n".join(
- paragraph.text for paragraph in artifact.sections[0].header.paragraphs
- )
- self.assertIn(
- "上海市群众艺术馆物业管理服务项目投标文件",
- header_text,
- )
- self.assertNotIn("%%", header_text)
- self.assertNotIn("上海市建筑工程学校", header_text)
- footer_text = artifact.sections[0].footer.paragraphs[0].text
- self.assertIn("第 1 页", footer_text)
- body_text = "\n".join(
- paragraph.text for paragraph in artifact.paragraphs
- )
- self.assertIn("模板正文", body_text)
- self.assertNotIn("其他章节", body_text)
- def test_fallback_index_chapter_has_header_and_footer(self):
- chapter = Chapter(
- id="index",
- title="与评标有关的投标文件主要内容索引表",
- chapter_type=ChapterType.BUSINESS,
- )
- with tempfile.TemporaryDirectory() as directory:
- path = _save_chapter_docx(
- chapter,
- directory,
- placeholder_map={"项目名称": "群众艺术馆项目"},
- analysis=analysis(),
- )
- artifact = Document(path)
- header_text = "\n".join(
- paragraph.text for paragraph in artifact.sections[0].header.paragraphs
- )
- self.assertIn("群众艺术馆项目投标文件", header_text)
- self.assertTrue(artifact.sections[0].footer.paragraphs)
- def test_native_chapters_use_unified_first_section_header(self):
- chapter1 = Chapter(
- id="1",
- title="投标人资格、资信证明",
- template_chapter_id="1",
- )
- chapter4 = Chapter(
- id="4",
- title="基本服务方案",
- template_chapter_id="4",
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- document = Document()
- first_section = document.sections[0]
- first_section.header.is_linked_to_previous = False
- first_section.header.paragraphs[0].text = "第一页眉"
- document.add_paragraph("第一章:投标人资格、资信证明", style="Heading 1")
- document.add_paragraph("第一章正文")
- second_section = document.add_section(WD_SECTION.NEW_PAGE)
- second_section.header.is_linked_to_previous = False
- second_section.header.paragraphs[0].text = "第二页眉"
- document.add_paragraph("第四章:基本服务方案", style="Heading 1")
- document.add_paragraph("第四章正文")
- document.save(template_path)
- path1 = _save_chapter_docx(
- chapter1,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- path4 = _save_chapter_docx(
- chapter4,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact1 = Document(path1)
- artifact4 = Document(path4)
- header1 = "\n".join(
- paragraph.text for paragraph in artifact1.sections[0].header.paragraphs
- )
- header4 = "\n".join(
- paragraph.text for paragraph in artifact4.sections[0].header.paragraphs
- )
- self.assertIn("第一页眉", header1)
- self.assertIn("第一页眉", header4)
- self.assertNotIn("第二页眉", header4)
- def test_native_chapter_applies_section_header_to_internal_section_breaks(self):
- chapter4 = Chapter(
- id="4",
- title="基本服务方案",
- template_chapter_id="4",
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- document = Document()
- first_section = document.sections[0]
- first_section.header.is_linked_to_previous = False
- first_section.header.paragraphs[0].text = "第一页眉"
- document.add_paragraph("第一章:投标人资格、资信证明", style="Heading 1")
- document.add_paragraph("第一章正文")
- second_section = document.add_section(WD_SECTION.NEW_PAGE)
- second_section.header.is_linked_to_previous = False
- second_section.header.paragraphs[0].text = "第二页眉"
- document.add_paragraph("第四章:基本服务方案", style="Heading 1")
- document.add_paragraph("第四章正文")
- internal_section = document.add_section(WD_SECTION.NEW_PAGE)
- internal_section.header.is_linked_to_previous = False
- internal_section.header.paragraphs[0].text = ""
- document.add_paragraph("内部分节正文")
- document.add_paragraph("第五章:其他章节", style="Heading 1")
- document.save(template_path)
- path = _save_chapter_docx(
- chapter4,
- directory,
- template_path=template_path,
- placeholder_map={},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- headers = [
- "\n".join(paragraph.text for paragraph in section.header.paragraphs)
- for section in artifact.sections
- ]
- self.assertTrue(headers)
- for header_text in headers:
- self.assertIn("第一页眉", header_text)
- self.assertNotIn("第二页眉", header_text)
- def test_native_index_chapter_uses_template_header(self):
- chapter = Chapter(
- id="index",
- title="与评标有关的投标文件主要内容索引表",
- chapter_type=ChapterType.BUSINESS,
- )
- with tempfile.TemporaryDirectory() as directory:
- template_path = os.path.join(directory, "template.docx")
- template = Document()
- header = template.sections[0].header
- header.is_linked_to_previous = False
- header.paragraphs[0].text = "%%项目名称%%项目投标文件"
- footer = template.sections[0].footer
- footer.is_linked_to_previous = False
- footer.paragraphs[0].text = "第 1 页"
- template.add_paragraph("第一章:投标人资格、资信证明", style="Heading 1")
- template.save(template_path)
- path = _save_chapter_docx(
- chapter,
- directory,
- template_path=template_path,
- placeholder_map={"项目名称": "群众艺术馆项目"},
- company_info=CompanyInfo(),
- analysis=analysis(),
- )
- artifact = Document(path)
- header_text = "\n".join(
- paragraph.text for paragraph in artifact.sections[0].header.paragraphs
- )
- self.assertIn("群众艺术馆项目项目投标文件", header_text)
- self.assertNotIn("%%", header_text)
- footer_text = artifact.sections[0].footer.paragraphs[0].text
- self.assertIn("第 1 页", footer_text)
- self.assertTrue(artifact.paragraphs)
- if __name__ == "__main__":
- unittest.main()
|