| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753 |
- # -*- coding: utf-8 -*-
- """S0–S8 交付 Word:按 A/B 统一建议另存新版本(旧文件保留)。"""
- from __future__ import annotations
- import re
- import shutil
- import sys
- import tempfile
- from pathlib import Path
- from docx import Document
- from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
- from docx.oxml import OxmlElement
- from docx.oxml.ns import qn
- from docx.shared import Pt, RGBColor
- from docx.table import Table
- from docx.text.paragraph import Paragraph
- HERE = Path(__file__).resolve().parent
- sys.path.insert(0, str(HERE))
- from _add_req_func_codes import ( # noqa: E402
- S0_ALIASES,
- append_code,
- extract_heading_title,
- set_paragraph_text,
- )
- from _adjust_front_matter import ( # noqa: E402
- clear_page_breaks,
- ensure_page_break_before,
- remove_paragraph,
- )
- BASE = Path(r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档")
- TMP = Path(tempfile.gettempdir()) / "aidop_unify_ab"
- REPORT = HERE / "_tmp_unify_ab_report.txt"
- TODAY = "2026-09-04"
- SYS = "Ai-DOP智慧运营管理系统"
- AUTHOR = "智造易项目组"
- NOTE = (
- "统一系统名与版本记录;需求标题补 REQ;B 类需求补范围;"
- "S1 详设补部署/非功能与需求明细核验;数据中台改为数据对接服务;手册指引总纲"
- )
- SYS_REPL = [
- ("AI-DOP 智慧运营决策平台", SYS),
- ("Ai-DOP 智慧运营决策平台", SYS),
- ("Ai-DOP智慧运营决策平台", SYS),
- ("Ai-DOP 企业智慧运营管理系统", SYS),
- ("Ai-DOP企业智慧运营管理系统", SYS),
- ("Ai-DOP 智慧运营管理系统", SYS),
- ]
- DATA_REPL = [
- ("数据中台平台(Middle Data Platform)", "数据对接服务(Middle Data Platform)"),
- ("数据中台平台", "数据对接服务"),
- ("DOP 数据中台", "DOP 数据对接服务"),
- ("底层数据中台链路", "底层数据对接服务链路"),
- ("数据中台三层", "数据对接服务三层"),
- ("数据中台链路", "数据对接服务链路"),
- ("数据中台分层", "数据对接服务分层"),
- ("经数据中台", "经数据对接服务"),
- ("数据中台 T8", "数据对接服务 T8"),
- ("数据中台", "数据对接服务"),
- ]
- S0_CANON_REQ = {
- "标准 BOM 管理": "REQ-S0-001",
- "标准工艺路线管理": "REQ-S0-002",
- "标准工序管理": "REQ-S0-003",
- "工作中心管理": "REQ-S0-004",
- "产线管理": "REQ-S0-005",
- "产线物料管理": "REQ-S0-006",
- "产线工位管理": "REQ-S0-007",
- "人员技能管理": "REQ-S0-008",
- "人员技能分配": "REQ-S0-009",
- "前置工序要素管理": "REQ-S0-010",
- "前置工序要素参数管理": "REQ-S0-011",
- "生产要素参数管理": "REQ-S0-012",
- "工艺流程卡管理": "REQ-S0-013",
- "物料代替方案管理": "REQ-S0-014",
- "物料替代明细管理(制造域)": "REQ-S0-015",
- "物料替代明细管理": "REQ-S0-015",
- "SOP 文件类型管理": "REQ-S0-016",
- "SOP 维护管理": "REQ-S0-017",
- "工单控制参数管理": "REQ-S0-018",
- "订单排程周期管理": "REQ-S0-019",
- "物料管理": "REQ-S0-020",
- "客户管理": "REQ-S0-021",
- "订单评审周期规则管理": "REQ-S0-023",
- "产品设计周期规则管理": "REQ-S0-024",
- "合同评审周期规则管理": "REQ-S0-025",
- "订单优先级规则管理": "REQ-S0-026",
- "供应商管理": "REQ-S0-027",
- "采购分类与货源管理": "REQ-S0-028",
- "品类交货周期管理": "REQ-S0-029",
- "物料计划周期管理": "REQ-S0-030",
- "部门管理": "REQ-S0-031",
- "员工管理": "REQ-S0-032",
- "员工工作职责管理": "REQ-S0-033",
- "库位管理": "REQ-S0-034",
- "库位货架管理": "REQ-S0-035",
- "物料包装规格管理": "REQ-S0-036",
- "批次号控制管理": "REQ-S0-037",
- "批次类型管理": "REQ-S0-038",
- "标签类型管理": "REQ-S0-039",
- "成本中心管理": "REQ-S0-040",
- "任务分配管理": "REQ-S0-041",
- "检验基准管理": "REQ-S0-042",
- "检验频次管理": "REQ-S0-043",
- "检验仪器管理": "REQ-S0-044",
- "检验项目管理": "REQ-S0-045",
- "检验方法管理": "REQ-S0-046",
- "检验标准管理": "REQ-S0-047",
- "抽样方案管理": "REQ-S0-048",
- "检验计划管理": "REQ-S0-049",
- "来料检验规范(IQC)": "REQ-S0-050",
- "过程检验规范(IPQC)": "REQ-S0-051",
- "成品检验规范(OQC/FQC)": "REQ-S0-052",
- "来料白名单管理": "REQ-S0-053",
- "质量字典管理": "REQ-S0-054",
- "指标目录管理(KpiMaster)": "REQ-S0-058",
- "条码规则管理": "REQ-S0-060",
- "计量器具管理": "REQ-S0-061",
- "业务事实字典": "REQ-S0-062",
- "物料工艺要素管理": "REQ-S0-063",
- "详情看板配置": "REQ-S0-064",
- }
- S0_EXTRA_ALIAS = {
- "运营指标主数据": "指标目录管理(KpiMaster)",
- "指标关系图谱": "详情看板配置",
- "检验方案": "检验计划管理",
- "条码规则": "条码规则管理",
- "物料工艺要素": "物料工艺要素管理",
- }
- S5_REQ = {
- "来料检验申请查询": "REQ-S5-019",
- "采购收货单查询": "REQ-S5-004",
- "生产退料单查询": "REQ-S5-006",
- "库存查询": "REQ-S5-010",
- "进出存查询": "REQ-S5-011",
- "标签查询": "REQ-S5-008",
- "盘点结果查询与盘点标签确认": "REQ-S5-012",
- }
- S6_REQ = {
- "过程检验单查询与详情": "REQ-S6-002",
- "IPQC 检验流转(检验员提交 / 主管审核)": "REQ-S6-002",
- "生产设备台账查询": "REQ-S6-003",
- "模工具台账查询": "REQ-S6-004",
- }
- S7_REQ = {
- "生产入库单查询": "REQ-S7-003",
- "销售发货通知查询": "REQ-S7-004",
- "FQC 检验任务查询": "REQ-S7-001",
- "FQC 检验结果查询与详情": "REQ-S7-002",
- }
- S8_REQ = {
- "异常监控看板": "REQ-S8-001",
- "监控大屏(交付 / 生产 / 供应 / 订单执行)": "REQ-S8-002",
- "异常列表与详情": "REQ-S8-008",
- "异常处理流转": "REQ-S8-019",
- "主动提报": "REQ-S8-009",
- "监视规则配置与运行": "REQ-S8-016",
- "数据源配置": "REQ-S8-015",
- "订单执行链路下钻": "REQ-S8-007",
- "配置中心": "REQ-S8-010",
- }
- SCOPE = {
- "S1": (
- "本模块覆盖销售订单评审与交期确认、交付跟踪、发货、工单下达、计划联动、需求明细核验及产销协同看板。",
- "不维护客户/物料等主数据(见 S0);不编制车间作业计划与报工(见 S2/S6);"
- "不处理采购到货与来料检验(见 S4/S5);不处理成品出库实物确认(见 S7)。",
- ),
- "S2": (
- "本模块覆盖生产排程、作业计划及制造协同看板,承接 S1 已下达工单并形成可执行计划。",
- "不维护工艺/产线主数据(见 S0);不评审销售订单(见 S1);"
- "不负责车间报工与过程检验判定(见 S6);不处理采购与仓储事务(见 S3–S5)。",
- ),
- "S3": (
- "本模块覆盖物料需求与交货计划、采购申请、要货令、采购/委外/外协订单及供应协同、齐套看板。",
- "不维护供应商主数据(见 S0);不处理供应商交货回执与到货入库(见 S4/S5);不排产(见 S2)。",
- ),
- "S4": (
- "本模块覆盖供应商交货、发货单、采购退货、IQC 退货查询及采购执行看板。",
- "不编制采购订单与要货令(见 S3);不做到货入库记账与 IQC 判定(见 S5);不维护供应商主数据(见 S0)。",
- ),
- }
- JOBS = [
- ("S0", "brd", "S0-运营建模-业务需求描述-V0.6.docx", "V0.6", "V0.7"),
- ("S0", "bbp", "S0-运营建模-业务蓝图设计方案-V0.6.docx", "V0.6", "V0.7"),
- ("S0", "dds", "S0运营建模模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S0", "um", "S0-运营建模-用户操作手册.docx", "V1.0", "V1.1"),
- ("S1", "brd", "S1产销协同业务需求描述.docx", "V0.2", "V0.3"),
- ("S1", "bbp", "S1产销协同模块蓝图设计方案.docx", "V2.0", "V2.1"),
- ("S1", "dds", "S1产销协同模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S1", "um", "S1产销协同模块用户操作手册.docx", "V1.0", "V1.1"),
- ("S2", "brd", "S2制造协同业务需求描述.docx", "V0.2", "V0.3"),
- ("S2", "bbp", "S2制造协同模块蓝图设计方案.docx", "V0.2", "V0.3"),
- ("S2", "dds", "S2制造协同模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S2", "um", "S2制造协同模块用户操作手册.docx", "V1.0", "V1.1"),
- ("S3", "brd", "S3供应协同业务需求描述.docx", "V0.2", "V0.3"),
- ("S3", "bbp", "S3供应协同模块蓝图设计方案.docx", "V0.2", "V0.3"),
- ("S3", "dds", "S3供应协同模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S3", "um", "S3供应协同模块用户操作手册.docx", "V1.0", "V1.1"),
- ("S4", "brd", "S4采购执行业务需求描述.docx", "V0.2", "V0.3"),
- ("S4", "bbp", "S4采购执行模块蓝图设计方案.docx", "V0.2", "V0.3"),
- ("S4", "dds", "S4采购执行模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S4", "um", "S4采购执行模块用户操作手册.docx", "V1.0", "V1.1"),
- ("S5", "brd", "S5-物料仓储-业务需求描述.docx", "V1.0", "V1.1"),
- ("S5", "bbp", "S5-物料仓储-业务蓝图设计方案.docx", "V0.1", "V0.2"),
- ("S5", "dds", "S5物料仓储模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S5", "um", "S5-物料仓储-用户操作手册.docx", "V1.0", "V1.1"),
- ("S6", "brd", "S6-生产执行-业务需求描述.docx", "V1.0", "V1.1"),
- ("S6", "bbp", "S6-生产执行-业务蓝图设计方案.docx", "V0.1", "V0.2"),
- ("S6", "dds", "S6生产执行模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S6", "um", "S6-生产执行-用户操作手册.docx", "V1.0", "V1.1"),
- ("S7", "brd", "S7-成品仓储-业务需求描述.docx", "V1.0", "V1.1"),
- ("S7", "bbp", "S7-成品仓储-业务蓝图设计方案.docx", "V0.1", "V0.2"),
- ("S7", "dds", "S7成品仓储模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S7", "um", "S7-成品仓储-用户操作手册.docx", "V1.0", "V1.1"),
- ("S8", "brd", "S8-异常监控-业务需求描述.docx", "V1.0", "V1.1"),
- ("S8", "bbp", "S8-异常监控-业务蓝图设计方案.docx", "V0.1", "V0.2"),
- ("S8", "dds", "S8异常监控模块详细设计说明书.docx", "V1.0", "V1.1"),
- ("S8", "um", "S8-异常监控-用户操作手册.docx", "V1.0", "V1.1"),
- ]
- def dest_name(src_name: str, new_ver: str) -> str:
- stem = Path(src_name).stem
- stem = re.sub(r"-V\d+(?:\.\d+)+$", "", stem)
- return f"{stem}-{new_ver}.docx"
- def apply_repl(text: str, pairs) -> str:
- for a, b in pairs:
- if a in text:
- text = text.replace(a, b)
- return text
- def iter_paras(doc: Document):
- for p in doc.paragraphs:
- yield p
- for table in doc.tables:
- for row in table.rows:
- for cell in row.cells:
- for p in cell.paragraphs:
- yield p
- def replace_everywhere(doc: Document) -> int:
- n = 0
- for p in iter_paras(doc):
- old = p.text
- new = apply_repl(apply_repl(old, SYS_REPL), DATA_REPL)
- if new != old:
- set_paragraph_text(p, new)
- n += 1
- return n
- def body_blocks(doc: Document):
- for child in doc.element.body:
- tag = child.tag.split("}")[-1]
- if tag == "p":
- yield "p", Paragraph(child, doc)
- elif tag == "tbl":
- yield "t", Table(child, doc)
- def find_p(doc: Document, *names: str) -> Paragraph | None:
- want = {n.strip() for n in names}
- for p in doc.paragraphs:
- if p.text.strip() in want:
- return p
- return None
- def set_cell(cell, text: str, *, bold=False, size=10.5, color=None, fill=None, center=False):
- cell.text = ""
- p = cell.paragraphs[0]
- if center:
- p.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = p.add_run(text)
- run.font.size = Pt(size)
- run.bold = bold
- run.font.name = "微软雅黑"
- r_pr = run._element.get_or_add_rPr()
- r_fonts = r_pr.rFonts
- if r_fonts is None:
- r_fonts = OxmlElement("w:rFonts")
- r_pr.insert(0, r_fonts)
- r_fonts.set(qn("w:ascii"), "微软雅黑")
- r_fonts.set(qn("w:hAnsi"), "微软雅黑")
- r_fonts.set(qn("w:eastAsia"), "微软雅黑")
- if color is not None:
- run.font.color.rgb = color
- if fill:
- tc_pr = cell._element.get_or_add_tcPr()
- old = tc_pr.find(qn("w:shd"))
- if old is not None:
- tc_pr.remove(old)
- shd = OxmlElement("w:shd")
- shd.set(qn("w:val"), "clear")
- shd.set(qn("w:color"), "auto")
- shd.set(qn("w:fill"), fill)
- tc_pr.append(shd)
- def strip_document_control(doc: Document) -> int:
- removed = 0
- drop_next_table = False
- for kind, obj in list(body_blocks(doc)):
- if kind == "p":
- t = obj.text.strip()
- if t == "文档控制":
- remove_paragraph(obj)
- removed += 1
- continue
- if t in {"审核", "发布"}:
- remove_paragraph(obj)
- drop_next_table = True
- removed += 1
- continue
- if t == "更改记录":
- set_paragraph_text(obj, "版本记录")
- removed += 1
- elif kind == "t" and drop_next_table:
- obj._tbl.getparent().remove(obj._tbl)
- drop_next_table = False
- removed += 1
- return removed
- def update_cover_version(doc: Document, old_ver: str, new_ver: str) -> int:
- n = 0
- for table in doc.tables[:4]:
- for row in table.rows:
- cells = [c.text.strip().replace("\n", "") for c in row.cells]
- if not cells:
- continue
- key = cells[0]
- if key in {"版本", "文档版本", "当前版本"} and len(row.cells) >= 2:
- set_cell(row.cells[1], new_ver, center=True)
- n += 1
- for i, c in enumerate(row.cells):
- raw = c.text.strip()
- if raw == old_ver or raw == f"{old_ver}(草稿)":
- set_cell(c, new_ver, center=True)
- n += 1
- elif raw.startswith(old_ver) and "日期" not in raw and len(raw) < 20:
- set_cell(c, raw.replace(old_ver, new_ver, 1), center=True)
- n += 1
- for p in doc.paragraphs[:20]:
- t = p.text
- if "当前版本" in t and old_ver in t:
- set_paragraph_text(p, t.replace(old_ver, new_ver))
- n += 1
- elif t.startswith("版本:") and old_ver in t:
- set_paragraph_text(p, t.replace(old_ver, new_ver))
- n += 1
- elif "编制日期" in t or t.startswith("更新日期"):
- set_paragraph_text(p, re.sub(r"\d{4}-\d{2}-\d{2}", TODAY, t, count=1))
- n += 1
- return n
- def version_table_after(heading: Paragraph) -> Table | None:
- el = heading._element.getnext()
- while el is not None:
- tag = el.tag.split("}")[-1]
- if tag == "tbl":
- return Table(el, heading._parent)
- if tag == "p":
- txt = "".join(el.itertext()).strip()
- if txt:
- return None
- el = el.getnext()
- return None
- def append_version_row(table: Table, new_ver: str) -> None:
- headers = [c.text.strip().replace("\n", "") for c in table.rows[0].cells]
- values = {
- "日期": TODAY,
- "版本": new_ver,
- "修订人": AUTHOR,
- "姓名": AUTHOR,
- "变更说明": NOTE,
- }
- # reuse last empty data row if present
- target = None
- if len(table.rows) > 1:
- last = table.rows[-1]
- if not any(c.text.strip() for c in last.cells):
- target = last
- if target is None:
- target = table.add_row()
- for i, h in enumerate(headers):
- if i < len(target.cells) and h in values:
- set_cell(target.cells[i], values[h], size=10.5)
- def _ensure_tbl_borders(table: Table) -> None:
- tbl = table._tbl
- tbl_pr = tbl.tblPr
- if tbl_pr is None:
- tbl_pr = OxmlElement("w:tblPr")
- tbl.insert(0, tbl_pr)
- borders = tbl_pr.find(qn("w:tblBorders"))
- if borders is None:
- borders = OxmlElement("w:tblBorders")
- tbl_pr.append(borders)
- for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
- el = borders.find(qn(f"w:{edge}"))
- if el is None:
- el = OxmlElement(f"w:{edge}")
- borders.append(el)
- el.set(qn("w:val"), "single")
- el.set(qn("w:sz"), "4")
- el.set(qn("w:space"), "0")
- el.set(qn("w:color"), "000000")
- def make_version_table(doc: Document, heading: Paragraph, old_ver: str, new_ver: str) -> None:
- table = doc.add_table(rows=3, cols=4)
- try:
- table.style = "Table Grid"
- except (KeyError, ValueError):
- _ensure_tbl_borders(table)
- headers = ["日期", "版本", "修订人", "变更说明"]
- for i, h in enumerate(headers):
- set_cell(table.rows[0].cells[i], h, bold=True, color=RGBColor(255, 255, 255), fill="1F3864", center=True)
- set_cell(table.rows[1].cells[0], "2026-06-10")
- set_cell(table.rows[1].cells[1], old_ver)
- set_cell(table.rows[1].cells[2], AUTHOR)
- set_cell(table.rows[1].cells[3], "既有版本")
- set_cell(table.rows[2].cells[0], TODAY)
- set_cell(table.rows[2].cells[1], new_ver)
- set_cell(table.rows[2].cells[2], AUTHOR)
- set_cell(table.rows[2].cells[3], NOTE)
- heading._element.addnext(table._tbl)
- def ensure_version_page(doc: Document, old_ver: str, new_ver: str) -> str:
- heading = find_p(doc, "版本记录", "更改记录")
- if heading is not None:
- if heading.text.strip() == "更改记录":
- set_paragraph_text(heading, "版本记录")
- tbl = version_table_after(heading)
- if tbl is not None:
- append_version_row(tbl, new_ver)
- ensure_page_break_before(heading)
- toc = find_p(doc, "目录")
- if toc is not None:
- ensure_page_break_before(toc)
- return "append"
- toc = find_p(doc, "目录")
- if toc is None:
- return "skip"
- heading = toc.insert_paragraph_before("版本记录")
- heading.style = toc.style if toc.style and toc.style.name.startswith("Heading") else "Heading 1"
- make_version_table(doc, heading, old_ver, new_ver)
- ensure_page_break_before(heading)
- ensure_page_break_before(toc)
- return "insert"
- def s0_req_code(title: str) -> str | None:
- if title in S0_CANON_REQ:
- return S0_CANON_REQ[title]
- if title in S0_EXTRA_ALIAS:
- return S0_CANON_REQ.get(S0_EXTRA_ALIAS[title])
- if title in S0_ALIASES:
- return S0_CANON_REQ.get(S0_ALIASES[title])
- return None
- def add_req_codes(doc: Document, module: str) -> int:
- mapping = {
- "S0": None,
- "S5": S5_REQ,
- "S6": S6_REQ,
- "S7": S7_REQ,
- "S8": S8_REQ,
- }.get(module)
- n = 0
- for p in doc.paragraphs:
- raw = p.text.strip()
- if not raw or "REQ-S" in raw:
- continue
- title = extract_heading_title(raw)
- if module == "S8" and re.match(r"^[一二三四五六七八九十]+、", raw):
- title = re.sub(r"^[一二三四五六七八九十]+、", "", raw).strip()
- if not title:
- continue
- code = s0_req_code(title) if module == "S0" else (mapping or {}).get(title)
- if code and append_code(p, code):
- n += 1
- return n
- def insert_scope(doc: Document, module: str) -> int:
- if module not in SCOPE:
- return 0
- if any("本模块覆盖" in p.text for p in doc.paragraphs[:40]):
- return 0
- target = None
- for p in doc.paragraphs:
- if re.match(r"^(一)", p.text.strip()):
- target = p
- break
- if target is None:
- return 0
- do, dont = SCOPE[module]
- p3 = target.insert_paragraph_before(f"本模块不做什么:{dont}")
- p2 = target.insert_paragraph_before(f"本模块做什么:{do}")
- p1 = target.insert_paragraph_before("范围说明")
- try:
- p1.style = "Heading 2"
- except Exception:
- pass
- return 3
- def insert_login_pointer(doc: Document) -> int:
- marker = None
- for p in doc.paragraphs:
- t = p.text.strip()
- if t in {"1 登录与首页", "1. 系统登录与首页"}:
- marker = p
- break
- if marker is None:
- return 0
- el = marker._element.getnext()
- while el is not None and el.tag.split("}")[-1] != "p":
- el = el.getnext()
- if el is not None and "SYS-用户操作手册-总纲" in "".join(el.itertext()):
- return 0
- text = (
- "登录、修改密码、退出等通用操作见《SYS-用户操作手册-总纲》。"
- "本章仅说明进入本模块后的页面位置;详细登录步骤以总纲为准。"
- )
- if el is not None:
- Paragraph(el, marker._parent).insert_paragraph_before(text)
- else:
- marker.insert_paragraph_before(text)
- return 1
- def insert_um_common(doc: Document) -> int:
- if any("通用操作说明" in p.text for p in doc.paragraphs[:80]):
- return 0
- target = None
- for p in doc.paragraphs:
- t = p.text.strip()
- if t in {"2. 模块操作说明", "2.模块操作说明"} or re.match(r"^2\.1\s", t):
- target = p
- break
- if target is None:
- return 0
- lines = [
- "在查询区填写条件后点查询筛选数据,点重置清空条件并回到默认结果。",
- "列表底部显示总条数,可翻页或切换每页条数;操作列按行提供查看或编辑入口。",
- "录入窗口中红色星号为必填项,未填时保存会提示,不会生成不完整单据。",
- ]
- for line in reversed(lines):
- target.insert_paragraph_before(line)
- h = target.insert_paragraph_before("2.0 通用操作说明")
- try:
- h.style = "Heading 2"
- except Exception:
- pass
- return 4
- def patch_s1_dds(doc: Document) -> int:
- n = 0
- # rename appendix 7 -> 9 first
- for p in doc.paragraphs:
- t = p.text
- nt = t
- nt = re.sub(r"^7\.3\s", "9.3 ", nt)
- nt = re.sub(r"^7\.2\s", "9.2 ", nt)
- nt = re.sub(r"^7\.1\s", "9.1 ", nt)
- nt = re.sub(r"^7\.\s*附录", "9. 附录", nt)
- if nt != t:
- set_paragraph_text(p, nt)
- n += 1
- # renumber 3.2.x via placeholders
- mapping = [
- ("3.2.8", "@@A@@"),
- ("3.2.7", "@@B@@"),
- ("3.2.5", "@@C@@"),
- ("3.2.4", "@@D@@"),
- ("3.2.3", "@@E@@"),
- ("3.2.2", "@@F@@"),
- ]
- final = {
- "@@A@@": "3.2.7",
- "@@B@@": "3.2.5",
- "@@C@@": "3.2.4",
- "@@D@@": "3.2.3",
- "@@E@@": "3.2.2",
- "@@F@@": "3.2.1",
- }
- for p in iter_paras(doc):
- t = p.text
- nt = t
- for a, b in mapping:
- nt = nt.replace(a, b)
- for a, b in final.items():
- nt = nt.replace(a, b)
- if nt != t:
- set_paragraph_text(p, nt)
- n += 1
- # insert 3.2.6 before 3.2.7 产销协同看板
- target = None
- for p in doc.paragraphs:
- if p.text.strip().startswith("3.2.7") and "产销协同看板" in p.text:
- target = p
- break
- if target is not None and not any("3.2.6" in x.text and "需求明细核验" in x.text for x in doc.paragraphs):
- blocks = [
- ("Heading 3", "3.2.6 需求明细核验(S1-M08)"),
- ("Heading 3", "3.2.6.1 业务流程"),
- ("Normal", "计划员在交期确认前核验订单需求明细与成品库存、在制及物料齐套是否一致;有差异时先解释差异再确认交期。"),
- ("Heading 3", "3.2.6.2 功能处理"),
- ("Normal", "核验须绑定订单号与需求行;数量、交期、物料编码为空不得提交。核验结论与系统计算不一致时必须填写原因。无授权不得改写需求数量。"),
- ("Heading 3", "3.2.6.3 页面设计"),
- ("Normal", "列表按订单号、客户、物料筛选;详情展示需求行、齐套结论与差异说明。"),
- ("Heading 3", "3.2.6.4 设计验证要点"),
- ("Normal", "缺必填不可保存;差异无原因不可确认;核验结果可在订单评审与计划联动中追溯。"),
- ]
- for style, text in blocks:
- np = target.insert_paragraph_before(text)
- try:
- np.style = style
- except Exception:
- pass
- n += 1
- app = None
- for p in doc.paragraphs:
- if p.text.strip().startswith("9.") and "附录" in p.text:
- app = p
- break
- if app is not None and not any("SaaS" in x.text for x in doc.paragraphs):
- blocks = [
- ("Heading 1", "7. SaaS部署与运维设计"),
- ("Heading 2", "7.1 部署模式和环境"),
- ("Normal", "S1 随 Ai-DOP智慧运营管理系统统一 SaaS 多租户部署,不单独建库;开发、预发、生产环境配置隔离。"),
- ("Heading 2", "7.2 部署架构"),
- ("Normal", "采用容器化部署:反向代理 + 前端静态资源 + .NET API + MySQL + Redis。S1 业务服务随平台一同发布。"),
- ("Heading 2", "7.3 租户开通和初始化"),
- ("Normal", "新租户开通后初始化组织、角色与 S1 菜单权限;订单评审等业务数据按租户隔离,禁止跨租户可见。"),
- ("Heading 2", "7.4 发布、升级和回滚"),
- ("Normal", "发版随平台版本号发布;升级失败按平台回滚策略回退至上一可用版本,保留订单与评审记录。"),
- ("Heading 2", "7.5 监控、备份和恢复"),
- ("Normal", "监控接口时延、排程/评审任务失败率与同步延迟;数据库按平台策略备份,恢复后核对订单与交期结论。"),
- ("Heading 1", "8. 非功能设计"),
- ("Normal", "性能:列表默认分页,常用查询应在可接受时间内返回。安全:接口鉴权、操作审计、租户隔离。可用性:关键操作失败须给出业务原因,不得静默丢单。可维护性:需求、蓝图、详设编号与 FUNC/REQ 对齐。"),
- ]
- for style, text in blocks:
- np = app.insert_paragraph_before(text)
- try:
- np.style = style
- except Exception:
- pass
- n += 1
- return n
- def ensure_sys_cover(doc: Document) -> int:
- early = [p.text for p in doc.paragraphs[:15]]
- if any(SYS in t for t in early):
- return 0
- first = None
- for p in doc.paragraphs[:8]:
- if p.text.strip():
- first = p
- break
- if first is None:
- return 0
- el = first._element.getnext()
- while el is not None and el.tag.split("}")[-1] != "p":
- el = el.getnext()
- if el is None:
- return 0
- np = Paragraph(el, first._parent).insert_paragraph_before(SYS)
- np.alignment = WD_ALIGN_PARAGRAPH.CENTER
- return 1
- def open_via_temp(src: Path) -> tuple[Document, Path]:
- TMP.mkdir(parents=True, exist_ok=True)
- work = TMP / f"in_{src.stem[:40]}.docx"
- shutil.copy2(src, work)
- return Document(str(work)), work
- def process_job(module: str, kind: str, src_name: str, old_ver: str, new_ver: str) -> str:
- src = BASE / module / src_name
- if not src.exists():
- return f"MISSING {src}"
- dest = BASE / module / dest_name(src_name, new_ver)
- doc, work = open_via_temp(src)
- notes = []
- notes.append(f"repl={replace_everywhere(doc)}")
- notes.append(f"syscover={ensure_sys_cover(doc)}")
- notes.append(f"ctrl={strip_document_control(doc)}")
- notes.append(f"cover={update_cover_version(doc, old_ver, new_ver)}")
- notes.append(f"verpage={ensure_version_page(doc, old_ver, new_ver)}")
- if kind == "brd" and module in {"S0", "S5", "S6", "S7", "S8"}:
- notes.append(f"req={add_req_codes(doc, module)}")
- if kind == "brd" and module in SCOPE:
- notes.append(f"scope={insert_scope(doc, module)}")
- if kind == "dds" and module == "S1":
- notes.append(f"s1dds={patch_s1_dds(doc)}")
- if kind == "um":
- notes.append(f"login={insert_login_pointer(doc)}")
- if module in {"S1", "S2", "S3", "S4"}:
- notes.append(f"common={insert_um_common(doc)}")
- out = TMP / f"out_{dest.stem[:40]}.docx"
- doc.save(str(out))
- shutil.copy2(out, dest)
- return f"OK {dest.name} ({'; '.join(notes)}) {dest.stat().st_size // 1024}KB"
- def main() -> None:
- TMP.mkdir(parents=True, exist_ok=True)
- lines = [f"TODAY={TODAY}", ""]
- only = None
- for arg in sys.argv[1:]:
- if arg.startswith("--only="):
- only = {x.strip().upper() for x in arg.split("=", 1)[1].split(",") if x.strip()}
- for module, kind, src_name, old_ver, new_ver in JOBS:
- if only and module not in only:
- continue
- print(f"processing {module} {kind} {src_name} -> {new_ver}", flush=True)
- try:
- msg = process_job(module, kind, src_name, old_ver, new_ver)
- except Exception as exc:
- msg = f"FAIL {module} {src_name}: {exc}"
- print(msg, flush=True)
- lines.append(msg)
- REPORT.write_text("\n".join(lines), encoding="utf-8")
- print("report", REPORT)
- if __name__ == "__main__":
- main()
|