# Multi-Party Revision Reconciliation (多方修订对账)

## 场景

合同经多方修订（如WB、华诚-Z、Adon hase等不同修订人），需要：
1. 核对叠加修订后的最终效果是否符合协商一致的商业条件
2. 对比最终版与模板的差异
3. 评估差异对特定方权利义务的影响
4. 统一修订人署名

## 技术方法

### 1. 提取带修订标记的全文（含作者归属）

```python
from docx import Document
from lxml import etree

ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
doc = Document('contract.docx')
body = doc.element.body

# 统计修订作者
authors = set()
for elem in body.iter():
    author = elem.get(f'{W}author')
    if author:
        authors.add(author)

# 分作者统计插入/删除
ins_by_author = {}
del_by_author = {}
for ins in body.findall(f'.//{W}ins'):
    a = ins.get(f'{W}author', 'unknown')
    ins_by_author[a] = ins_by_author.get(a, 0) + 1
for d in body.findall(f'.//{W}del'):
    a = d.get(f'{W}author', 'unknown')
    del_by_author[a] = del_by_author.get(a, 0) + 1
```

### 2. 带修订标记的文本提取

格式：`[+作者: 插入文本]` / `[-作者: 删除文本]` / 普通文本

```python
def get_text_with_revisions(body):
    result = []
    for para in body.findall(f'.//{W}p'):
        para_text = []
        for elem in para.iter():
            if elem.tag == f'{W}ins':
                author = elem.get(f'{W}author', '?')
                texts = [t.text for t in elem.findall(f'.//{W}t') if t.text]
                if texts:
                    para_text.append(f"[+{author}: {''.join(texts)}]")
            elif elem.tag == f'{W}del':
                author = elem.get(f'{W}author', '?')
                texts = [t.text for t in elem.findall(f'.//{W}delText') if t.text]
                if texts:
                    para_text.append(f"[-{author}: {''.join(texts)}]")
            elif elem.tag == f'{W}t':
                in_revision = False
                p = elem
                while p is not None:
                    if p.tag in [f'{W}ins', f'{W}del']:
                        in_revision = True
                        break
                    p = p.getparent()
                if not in_revision and elem.text:
                    para_text.append(elem.text)
        if para_text:
            result.append(''.join(para_text))
    return result
```

### 3. 三版对比分析

对比维度：
- **模板** → 我方标准条款（基准线）
- **对方修订版** → 对方修改后接受所有修订的版本（ins=0, del=0 说明已全部接受）
- **当前叠加版** → 在WB修订基础上加入另一方修改

### 4. 协商一致核对表

按商业条件逐项核对：

| 协商条件 | 条款位置 | 当前版本内容 | 是否符合 |
|---|---|---|---|
| 费用40/60/60/80/80 | 3.3条 | 具体金额 | ✅/❌ |
| 违约责任按我方 | 第4条 | ... | ✅/⚠️ |

### 5. 模板偏差影响评估

对每处偏差评级：
- ✅ 对己方有利（扩大了己方权利/对方义务）
- ⚠️ 中等影响（条件调整但不改变核心权利义务）
- ❗ 重要（实质性削弱己方权利/扩大己方义务/给对方逃出合同的通道）

## 修订人统一规则

当需要将多个修订人统一为一个（如统一为WB）：

### 处理优先级
1. A修订了B的修订 → **以A为准**（接受A对B的修改）
2. A和B各自独立修订 → 两者都保留，统一署名

### 核心技术挑战：嵌套修订

**关键结构：B(华诚-Z)在A(WB)的 `w:ins` 内部做了 `w:del`**

XML表现为：
```xml
<w:ins w:author="WB">
  <w:r><w:t>个月未给乙方安排工作的，</w:t></w:r>
  <w:del w:author="华诚-Z">
    <w:r><w:delText>拍摄</w:delText></w:r>
  </w:del>
</w:ins>
```
含义：WB插入了"个月未给乙方安排拍摄工作的，"，华诚-Z在WB的插入中删除了"拍摄"，最终效果="个月未给乙方安排工作的，"。

### 三步统一流程（2026-07-03 MCN模特合同实证）

**Step 1: 接受嵌套删除**（B对A修订的修改）

```python
def accept_nested_deletions(body, inner_author='华诚-Z', outer_author='WB'):
    """接受inner_author对outer_author修订的修改（删除嵌套del元素）"""
    for ins_elem in body.findall(f'.//{W}ins'):
        if ins_elem.get(f'{W}author') != outer_author:
            continue
        # 找到outer_author的ins内部，inner_author做的del
        for del_elem in ins_elem.findall(f'.//{W}del'):
            if del_elem.get(f'{W}author') == inner_author:
                parent = del_elem.getparent()
                parent.remove(del_elem)

accept_nested_deletions(body)
```

**Step 2: 清除空壳元素**

接受嵌套删除后，某些WB的ins可能变空（内容全被华诚-Z删了，华诚-Z在旁边插入了替代文本）：

```python
def remove_empty_ins(body):
    """删除没有任何文本内容的ins元素"""
    for ins_elem in body.findall(f'.//{W}ins'):
        has_text = False
        for t in ins_elem.findall(f'.//{W}t'):
            if t.text and t.text.strip():
                has_text = True
                break
        if not has_text:
            parent = ins_elem.getparent()
            if parent is not None:
                parent.remove(ins_elem)

remove_empty_ins(body)
```

**Step 3: 统一作者名**

```python
def rename_author(body, old_author, new_author):
    """修改所有修订元素的author属性"""
    count = 0
    for elem in body.iter():
        if elem.get(f'{W}author') == old_author:
            elem.set(f'{W}author', new_author)
            count += 1
    return count

count = rename_author(body, '华诚-Z', 'WB')
```

### 在统一后的文件上追加模板修改

如果发现与模板有偏差需要修正（如恢复模板中的固定金额违约金选项、自动续约条件等），在已统一的文件上新增tracked changes：

```python
from copy import deepcopy

def get_rPr_from_run(run):
    """获取run的格式属性用于新增修订"""
    rpr = run.find(f'{W}rPr')
    return deepcopy(rpr) if rpr is not None else None

def make_ins(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'):
    """创建tracked insertion"""
    ins = etree.Element(f'{W}ins')
    ins.set(f'{W}id', str(abs(hash(text)) % 100000))
    ins.set(f'{W}author', author)
    ins.set(f'{W}date', date)
    r = etree.SubElement(ins, f'{W}r')
    if rPr is not None:
        r.append(deepcopy(rPr))
    t = etree.SubElement(r, f'{W}t')
    t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
    t.text = text
    return ins

def make_del(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'):
    """创建tracked deletion"""
    d = etree.Element(f'{W}del')
    d.set(f'{W}id', str(abs(hash(text + 'del')) % 100000))
    d.set(f'{W}author', author)
    d.set(f'{W}date', date)
    r = etree.SubElement(d, f'{W}r')
    if rPr is not None:
        r.append(deepcopy(rPr))
    dt = etree.SubElement(r, f'{W}delText')
    dt.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
    dt.text = text
    return d

# 修改模式：找到目标run → 替换为del(旧) + ins(新)
target_run = ...  # 找到包含目标文本的w:r元素
rPr = get_rPr_from_run(target_run)
old_text = target_run.find(f'.//{W}t').text

del_elem = make_del(old_text, rPr)
ins_elem = make_ins(new_text, rPr)

idx = list(para).index(target_run)
para.remove(target_run)
para.insert(idx, del_elem)
para.insert(idx + 1, ins_elem)
```

### 恢复已被删除的内容（撤回WB之前的del）

当需要把WB之前删除的内容恢复回来（因为模板中有这个内容）：

```python
# 找到WB的del元素
for child in para:
    if child.tag == f'{W}del' and child.get(f'{W}author') == 'WB':
        del_texts = ''.join(dt.text for dt in child.findall(f'.//{W}delText') if dt.text)
        if '目标文本' in del_texts:
            # 策略：删除这个del，插入一个ins代替（因为原文已经"被删"了）
            inner_r = child.find(f'.//{W}r')
            inner_rPr = get_rPr_from_run(inner_r)
            ins_restore = make_ins(del_texts, inner_rPr)
            idx = list(para).index(child)
            para.remove(child)
            para.insert(idx, ins_restore)
            break
```

## 验证步骤

统一修订人后必须验证：

```python
# 1. 确认只剩一个作者
authors = set()
for elem in body.iter():
    a = elem.get(f'{W}author')
    if a:
        authors.add(a)
assert authors == {'WB'}, f"Unexpected authors: {authors}"

# 2. 获取接受所有修订后的最终文本
def get_accepted_text(para):
    texts = []
    for elem in para.iter():
        if elem.tag == f'{W}t':
            in_del = False
            p = elem
            while p is not None:
                if p.tag == f'{W}del':
                    in_del = True
                    break
                p = p.getparent()
            if not in_del and elem.text:
                texts.append(elem.text)
    return ''.join(texts)

# 3. 逐条核对关键条款的最终文本
```

## 输出格式

报告分三部分：
1. **协商一致核对** — 逐项确认商业条件是否落实（✅/⚠️/❌）
2. **模板差异表** — 与标准模板的偏差点 + 对己方权利义务的影响评估
3. **结论与建议** — 哪些已符合、哪些需关注、是否需要进一步协商

## 注意事项

- 对方修订版如果修订已全部接受（ins=0, del=0），说明是"接受所有修订后"的干净版本
- 比较时需要分别提取：（1）当前版本接受所有修订后的最终文本；（2）带修订标记的过程文本
- 对方修改了措辞但实质不变的情况（如"整容"→"形象变化"），需评估措辞变化是否改变法律效果——措辞更宽泛时分析对哪方有利
- 费用条款的结构性改写（如合并年度/拆分年度）要验证数学正确性（年总额×期数=合同总额）
- 处理段落29-31连读场景：华诚-Z可能将原来的多个年度段落（第三、四年/第五年分开写）合并为一个（第四、第五年），导致中间段落被del清空，需要把前后段落连起来读才能看到完整的费用结构
- **修订人统一后的检查清单**：作者唯一性、ins/del总数合理、关键条款最终文本正确、文件可正常打开
