kyle 1 týždeň pred
commit
909d110c53

+ 119 - 0
SKILL.md

@@ -0,0 +1,119 @@
+---
+name: auto-generate-export-contracts
+description: 根据订单合同信息自动生成车辆出口销售合同(Word)和Proforma Invoice(Excel)。用于处理海外车辆出口业务,支持从订单文本解析买方信息、车型列表、贸易条款等,自动匹配车型价格配置表,生成标准格式的中英双语销售合同和PI表格。当用户提供订单信息、要求生成销售合同/PI/Proforma Invoice时触发。
+---
+
+# 海外合同自动生成 Skill
+
+## 功能
+
+根据用户提供的订单合同信息,自动生成:
+1. **车辆销售合同** (Vehicle Sales Contract) - `.docx` 格式
+2. **Proforma Invoice** - `.xlsx` 格式
+
+## 触发条件
+
+当用户:
+- 提供订单信息并要求生成销售合同/PI/Proforma Invoice
+- 提供包含车型、买方公司、贸易条款等信息的订单文本
+
+## 工作流程
+
+### Step 1: 接收订单信息
+
+支持两种输入格式:
+- **结构化编号列表**(标准格式):
+  ```
+  1. 姓名:xxx;
+  2.任职:xxx;
+  3.意向车型:
+  (1) 车型中文名  英文名 - 型号:LZWxxxx
+  排放标准  代码:xxx  X台颜色
+  4.合同签约公司中文名称:xxx;
+  5.合同签约公司英文名称:xxx
+  ...
+  ```
+- **自然语言描述**:用户自由描述订单内容
+
+解析提取核心字段:
+- 买方公司中英文名称和地址
+- 合同联系电话
+- 车型列表(型号、发动机代码、数量、颜色)
+- 贸易条款(FCA/FOB/EXW)
+- 出发港口、目的地国家
+
+### Step 2: 车型价格匹配
+
+遍历 `assets/vehicle-price-config.xlsx` 中的所有sheet,在**G列(型号)**精确匹配ModelCode:
+- 匹配成功 → 根据贸易条款获取对应USD价格(FCA→O列, FOB→N列, EXW→P列)
+- 匹配失败 → **提示用户提供该车型的单价USD**
+
+**用户提供的价格优先于价格表。**
+
+### Step 3: 计算汇总
+
+- 总数量 = Σ 各车型数量
+- 总金额 = Σ (单价 × 数量)
+- 30%金额 = `ceil(总金额 × 0.30)`(向上取整)
+- 70%金额 = 总金额 - 30%金额
+- 英文大写 = `num2words(总金额).upper()`
+
+### Step 4: 生成合同编号
+
+格式:`HLXYWPA{年月日}`,如 `HLXYWPA20260530`
+
+### Step 5: 生成文件
+
+**Proforma Invoice** (`{合同编号}-Proforma Invoice.xlsx`):
+- 复制模板 `assets/proforma-invoice-template.xlsx`
+- 替换买方信息、合同编号、日期、车型明细、价格、汇总信息
+- 保留Excel公式(SUM、乘法公式)
+
+**销售合同** (`{合同编号}-车辆销售合同.docx`):
+- 复制模板 `assets/vehicle-sales-contract-template.docx`
+- 替换段落文本(合同编号、日期、买方信息、贸易条款等)
+- 替换表格(车辆明细表、汇总信息)
+- 银行账户信息保持模板固定值
+
+### Step 6: 输出
+
+向用户输出两个文件,并附加固定提示语:
+> 销售合同、Proforma Invoice已生成,报价员核对无误后再发送给客户。
+
+## 脚本使用
+
+```bash
+python scripts/generate_contracts.py <订单信息文件.txt> -o <输出目录> -p '{"LZW1028SPY": 6176}'
+```
+
+参数:
+- `order_file`: 订单信息文本文件路径
+- `-o, --output`: 输出目录(默认当前目录)
+- `-p, --prices`: JSON字符串,手动提供缺失的车型价格
+
+Python API:
+```python
+from scripts.generate_contracts import generate_contracts
+
+result = generate_contracts(order_text, output_dir='.', user_prices={"LZW1028SPY": 6176})
+# result['success'] = True/False
+# result['pi_path'] = PI文件路径
+# result['contract_path'] = 销售合同文件路径
+# result['missing_prices'] = 未匹配到价格的车型列表
+```
+
+## 依赖
+
+- `python-docx`
+- `openpyxl`
+- `num2words`
+
+## 模板文件
+
+- `assets/proforma-invoice-template.xlsx`: Proforma Invoice模板
+- `assets/vehicle-sales-contract-template.docx`: 销售合同模板
+- `assets/vehicle-price-config.xlsx`: 车型价格配置表(多sheet)
+
+## 字段映射详情
+
+详见 [references/field-mapping.md](references/field-mapping.md)

BIN
assets/proforma-invoice-template.xlsx


BIN
assets/vehicle-price-config.xlsx


BIN
assets/vehicle-sales-contract-template.docx


+ 21 - 0
assets/订单合同信息案例.txt

@@ -0,0 +1,21 @@
+1. 姓名:陈凯武;
+2.任职:经理;
+3.意向车型:                 
+(1) 荣光新双排货车  N350 Double Cab Pickup,- 型号:LZW1028SPY
+国V China Ⅴ  代码:G33N    1台银色
+(2)五菱荣光新卡双后轮 2.0L 5MT 单排 型号:LZW5030XXYLGHUG
+国六B(RDE)  代码:AGMC  1台银色
+4.合同签约公司中文名称:财货通(香港)国际供应链有限公司;
+5.合同签约公司英文名称:Finmo (HongKong)International supply chain Co.,Ltd.
+6.合同签约公司中文地址:香港德輔道西 93-97 號聯威商業大廈 13 樓 B-C 室
+7.合同签约公司英文地址:FLAT B&C,13/F.,LUEN WAI COMMERCIAL BUILDING,93-97 DES VOEUX ROAD WEST,HONG KONG
+8.合同联系电话:13640613242
+9.国内出发港口:广州南沙;
+10.目的地国家:巴拿马Panama
+11.目的地港口:巴尔博亚港 Balboa
+12.结算方式:FCA
+二、付款公司:
+1.开户行名称:创兴银行
+2.开户账户:256801951225
+3.公司联系 电话:+8613640613242
+4.美元结算。

+ 45 - 0
references/field-mapping.md

@@ -0,0 +1,45 @@
+# 字段映射参考
+
+## 订单信息 → 合同字段映射
+
+### 买方信息
+| 订单字段 | PI位置 | 销售合同位置 |
+|---|---|---|
+| 合同签约公司英文名称 | B7 (TO:) | P11 (Buyer:) |
+| 合同签约公司中文名称 | — | P12 (买方:) |
+| 合同签约公司英文地址 | B8 (ADD:) | P13 (ADD:) |
+| 合同签约公司中文地址 | — | P14 (地址:) |
+| 合同联系电话 | B9 (Tel:) | P15 (Tel:) |
+
+### 物流信息
+| 订单字段 | PI位置 | 销售合同位置 |
+|---|---|---|
+| 国内出发港口 | E23 FCA港口 / B27 Delivery terms | P28 Incoterms |
+| 目的地国家 | — | P22 Export Destination |
+| 贸易条款 | E23 / B27 前缀 | P28 前缀 |
+| 运输标记 | G12 (固定N/M) | — |
+
+### 车辆信息
+| 订单字段 | PI位置 | 销售合同位置 |
+|---|---|---|
+| 型号 (ModelCode) | B17-B22 描述 | Table0 Row1+ 车型描述 |
+| 发动机代码 | B17-B22 描述 | Table0 Row1+ 车型描述 |
+| 数量 | C17-C22 | Table0 Row1+ 数量列 |
+| 单价USD | F17-F22 (查表或用户提供) | Table0 Row1+ 单价列 |
+| 颜色 | — | Table0 Color字段 |
+
+### 汇总信息
+| 计算项 | PI位置 | 销售合同位置 |
+|---|---|---|
+| 总数量 | H23 SUM公式 | Table0 汇总行 |
+| 总金额 | H23 SUM公式 | Table0 汇总行 |
+| 英文大写 | B24 | Table0 汇总行 |
+| 30%金额 | B29 Payment terms | — |
+| 70%金额 | B29 Payment terms | — |
+
+### 固定信息(不替换)
+| 信息 | 位置 |
+|---|---|
+| 卖方信息 | PI B1-B3 / 合同 P5-P9 |
+| 收款银行账户 | PI B30+ / 合同 Table1 |
+| 商标列表 | 合同 Table2 |

+ 811 - 0
scripts/generate_contracts.py

@@ -0,0 +1,811 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+主生成脚本:根据订单信息生成销售合同和Proforma Invoice
+"""
+
+import os
+import sys
+import shutil
+import re
+from datetime import datetime
+
+# 添加脚本目录到路径
+script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, script_dir)
+
+from parse_order_info import parse_order_info
+from match_vehicle import match_vehicle, build_vehicle_description
+from number_to_words import format_say_us_only, calculate_payment_split
+
+# 模板正文默认字号:12pt
+from docx.shared import Pt
+DEFAULT_FONT_SIZE = Pt(12)
+
+
+def copy_run_format(src_run, dst_run):
+    """复制源run的格式到目标run,字号默认12pt"""
+    dst_run.bold = src_run.bold
+    dst_run.italic = src_run.italic
+    dst_run.font.size = src_run.font.size or DEFAULT_FONT_SIZE
+    if src_run.font.name:
+        dst_run.font.name = src_run.font.name
+
+
+def set_run_format(run, bold=None, size=None):
+    """设置run格式,字号默认12pt"""
+    run.bold = bold
+    run.font.size = size or DEFAULT_FONT_SIZE
+
+
+def get_asset_path(filename: str) -> str:
+    """获取assets目录下的文件路径"""
+    return os.path.join(script_dir, '..', 'assets', filename)
+
+
+def generate_contract_no() -> str:
+    """生成合同编号: HLXYWPA{年月日}"""
+    return f"HLXYWPA{datetime.now().strftime('%Y%m%d')}"
+
+
+def format_date() -> str:
+    """格式化当前日期"""
+    return datetime.now().strftime('%Y-%m-%d')
+
+
+def format_date_for_cell():
+    """Excel单元格日期格式(仅日期,无时间)"""
+    from datetime import datetime
+    now = datetime.now()
+    return datetime(now.year, now.month, now.day)
+
+
+def get_port_names(port_input: str) -> tuple:
+    """获取港口的中英文名称,返回 (cn_name, en_name)"""
+    port_map = {
+        '广州南沙': ('中国广州南沙港口', 'Guangzhou nansha Port'),
+        '广州': ('中国广州港口', 'Guangzhou Port'),
+        '深圳': ('中国深圳港口', 'Shenzhen Port'),
+        '上海': ('中国上海港口', 'Shanghai Port'),
+        '天津': ('中国天津港口', 'Tianjin Port'),
+        '青岛': ('中国青岛港口', 'Qingdao Port'),
+        '宁波': ('中国宁波港口', 'Ningbo Port'),
+        '厦门': ('中国厦门港口', 'Xiamen Port'),
+    }
+    port_input = port_input.strip()
+    # 先尝试中文匹配
+    for cn, (cn_name, en_name) in port_map.items():
+        if cn in port_input:
+            return cn_name, en_name
+    # 再尝试英文匹配
+    for cn, (cn_name, en_name) in port_map.items():
+        if en_name.lower() in port_input.lower():
+            return cn_name, en_name
+    # 默认返回输入值
+    return port_input, port_input
+
+
+def number_to_chinese(num: int) -> str:
+    """将整数金额转换为中文大写(壹贰叁肆伍陆柒捌玖拾佰仟万)"""
+    if num == 0:
+        return '零'
+    if num < 0:
+        return '负' + number_to_chinese(-num)
+
+    units = ['', '拾', '佰', '仟']
+    big_units = ['', '万', '亿']
+    nums = '零壹贰叁肆伍陆柒捌玖'
+
+    def convert_group(n):
+        if n == 0:
+            return ''
+        s = ''
+        zero = False
+        for i in range(3, -1, -1):
+            d = (n // (10 ** i)) % 10
+            if d == 0:
+                if s and not zero:
+                    zero = True
+            else:
+                if zero:
+                    s += '零'
+                    zero = False
+                s += nums[d] + units[i]
+        return s
+
+    groups = []
+    idx = 0
+    while num > 0:
+        g = num % 10000
+        num //= 10000
+        if g > 0:
+            gs = convert_group(g)
+            if gs:
+                groups.append(gs + big_units[idx])
+            else:
+                groups.append(big_units[idx])
+        elif groups:
+            groups.append('零')
+        idx += 1
+
+    result = ''.join(reversed(groups))
+    result = result.replace('零零', '零')
+    result = result.replace('零万', '万')
+    result = result.replace('零亿', '亿')
+    result = result.rstrip('零')
+    return result
+
+
+def fill_paragraph_value(para, marker: str, value: str) -> bool:
+    """
+    在段落中 marker 标记后填入 value,保留原有格式。
+    不直接设置 para.text(这会破坏所有 run 格式)。
+    """
+    text = para.text
+    if marker not in text:
+        return False
+
+    parts = text.split(marker, 1)
+    if len(parts) < 2:
+        return False
+
+    after = parts[1].strip()
+    if after and after != value.strip():
+        # 已有内容且不是我们要填的值,视为已填充
+        return False
+
+    # 找到最后一个 run
+    if not para.runs:
+        run = para.add_run(value)
+        run.font.size = DEFAULT_FONT_SIZE
+        return True
+
+    # 从后往前找:优先修改空白占位符 run,否则在末尾追加
+    for i in range(len(para.runs) - 1, -1, -1):
+        r = para.runs[i]
+        if not r.text.strip():
+            # 空白 run,填入值,并复制前一个非空 run 的格式
+            r.text = value
+            src = None
+            for j in range(i - 1, -1, -1):
+                if para.runs[j].text:
+                    src = para.runs[j]
+                    break
+            if src:
+                copy_run_format(src, r)
+                if src.font.color and src.font.color.rgb:
+                    r.font.color.rgb = src.font.color.rgb
+            else:
+                r.font.size = DEFAULT_FONT_SIZE
+            return True
+        elif r.text.strip():
+            # 最后一个非空 run,在其后追加新 run,复制格式
+            new_run = para.add_run(value)
+            copy_run_format(r, new_run)
+            if r.font.color and r.font.color.rgb:
+                new_run.font.color.rgb = r.font.color.rgb
+            return True
+
+    return False
+
+
+def delete_table_row(table, row):
+    """从表格中真正删除一行(使用XML操作)"""
+    tbl = table._tbl
+    tr = row._tr
+    tbl.remove(tr)
+
+
+def set_cell_text(cell, text):
+    """设置单元格文本:删除除第一个段落外的所有段落,清空第一个段落并填入新文本"""
+    tc = cell._tc
+    # 删除除第一个外的所有段落(从后往前删)
+    while len(cell.paragraphs) > 1:
+        p = cell.paragraphs[-1]._p
+        tc.remove(p)
+    
+    # 清空并填充第一个段落
+    para = cell.paragraphs[0]
+    style = para.style
+    para.clear()
+    run = para.add_run(text)
+    para.style = style
+
+
+def set_cell_multiline(cell, text):
+    """设置单元格多段落文本:按换行符拆分为多个段落,保留模板格式。
+    
+    模板中 Description 单元格的格式为:
+      Para 0: Model: ...
+      Para 1: (配置描述)
+      Para 2: Color: ...
+      Para 3: 型号:...
+      Para 4: (配置描述)
+      Para 5: 颜色:...
+    """
+    from docx.oxml.ns import qn
+    from copy import deepcopy
+    tc = cell._tc
+    
+    # 保存第一个段落的完整 XML 作为模板
+    first_p = cell.paragraphs[0]._p
+    template_pPr = first_p.find(qn('w:pPr'))
+    
+    # 删除除第一个外的所有段落(从后往前删)
+    while len(cell.paragraphs) > 1:
+        p = cell.paragraphs[-1]._p
+        tc.remove(p)
+    
+    # 清空第一个段落
+    first_para = cell.paragraphs[0]
+    first_para.clear()
+    
+    # 按换行符拆分
+    lines = text.split('\n')
+    
+    # 第一行填入第一个段落
+    if lines:
+        first_para.add_run(lines[0])
+    
+    # 后续行追加为新段落
+    for line in lines[1:]:
+        new_p = tc.makeelement(qn('w:p'), {})
+        # 复制段落属性(对齐、缩进等)
+        if template_pPr is not None:
+            new_p.append(deepcopy(template_pPr))
+        # 添加 run
+        new_r = new_p.makeelement(qn('w:r'), {})
+        new_t = new_r.makeelement(qn('w:t'), {})
+        new_t.text = line
+        new_t.set(qn('xml:space'), 'preserve')
+        new_r.append(new_t)
+        new_p.append(new_r)
+        tc.append(new_p)
+
+
+def build_full_description(vehicle: dict, match_result: dict = None) -> str:
+    """
+    构建销售合同中 Commodity 表格的完整货物描述。
+    格式参考模板:
+      Model: {model_code}, {engine_code} {中文车型名}
+      {英文车型名}
+      {中文配置}
+      {英文配置}
+      Color: {color}
+      型号:{model_code}, {engine_code} {英文车型名}
+    """
+    model_code = vehicle.get('model_code', '')
+    engine_code = vehicle.get('engine_code', '')
+    color = vehicle.get('color', '1')
+    name_cn = vehicle.get('name_cn', '')
+    name_en = vehicle.get('name_en', '')
+
+    lines = []
+
+    if match_result:
+        # 有价格表匹配,使用价格表信息
+        desc_cn = match_result.get('description_cn', '')
+        series_en = match_result.get('series_en', '')
+        config_desc = match_result.get('config_desc', '')
+
+        # 分离中英文配置描述
+        config_cn = ''
+        config_en = ''
+        if config_desc:
+            config_parts = [p.strip() for p in config_desc.split('\n') if p.strip()]
+            for part in config_parts:
+                if re.search(r'[\u4e00-\u9fff]', part):
+                    config_cn = part
+                else:
+                    config_en = part
+
+        # Model 行(英文)
+        model_line = f"Model: {model_code}, {engine_code}"
+        if series_en:
+            model_line += f" {series_en}"
+        lines.append(model_line)
+
+        # 英文配置描述
+        if config_en:
+            lines.append(config_en)
+    else:
+        # 无价格表匹配,使用订单信息
+        model_line = f"Model: {model_code}, {engine_code}"
+        if name_en:
+            model_line += f" {name_en}"
+        elif name_cn:
+            model_line += f" {name_cn}"
+        lines.append(model_line)
+
+    # Color (English)
+    if color and color != '1':
+        lines.append(f"Color: {color}")
+    else:
+        lines.append("Color: ")
+
+    # 型号行(中文标签)- 带中文描述
+    model_cn_line = f"型号:{model_code}, {engine_code}"
+    if match_result and match_result.get('description_cn'):
+        model_cn_line += f" {match_result['description_cn']}"
+    elif name_cn:
+        model_cn_line += f" {name_cn}"
+    elif name_en:
+        model_cn_line += f" {name_en}"
+    lines.append(model_cn_line)
+
+    # 中文配置描述(仅提取包含中文的配置行)
+    if match_result and match_result.get('config_desc'):
+        config_parts = [p.strip() for p in match_result['config_desc'].split('\n') if p.strip()]
+        for part in config_parts:
+            if re.search(r'[\u4e00-\u9fff]', part):
+                lines.append(part)
+
+    # 颜色行(中文)
+    if color and color != '1':
+        lines.append(f"颜色:{color}")
+    else:
+        lines.append("颜色:")
+
+    return '\n'.join(lines)
+
+
+def process_vehicles(vehicles: list, trade_term: str) -> list:
+    """
+    处理车型列表,匹配价格表或提示用户提供价格
+
+    Returns:
+        (processed_vehicles, missing_prices)
+    """
+    processed = []
+    missing = []
+
+    for v in vehicles:
+        model_code = v.get('model_code', '')
+
+        # 如果用户已提供单价,直接使用
+        if v.get('unit_price_usd') is not None:
+            processed.append(v)
+            continue
+
+        # 从价格表匹配
+        match_result = match_vehicle(model_code, trade_term)
+        if match_result and match_result.get('unit_price_usd'):
+            v['unit_price_usd'] = match_result['unit_price_usd']
+            v['config_desc'] = build_vehicle_description(v, match_result)
+            v['_match_result'] = match_result
+            processed.append(v)
+        else:
+            # 未匹配到价格
+            missing.append(v)
+
+    return processed, missing
+
+
+def calculate_summary(vehicles: list) -> dict:
+    """计算汇总信息"""
+    total_qty = sum(v['quantity'] for v in vehicles)
+    total_amount = sum(v['unit_price_usd'] * v['quantity'] for v in vehicles)
+
+    deposit_30, balance_70 = calculate_payment_split(total_amount)
+
+    return {
+        'total_qty': total_qty,
+        'total_amount': round(total_amount, 2),
+        'total_amount_int': round(total_amount),
+        'deposit_30': deposit_30,
+        'balance_70': balance_70,
+        'amount_in_words': format_say_us_only(total_amount),
+        'amount_in_chinese': number_to_chinese(round(total_amount)),
+    }
+
+
+def generate_proforma_invoice(order: dict, vehicles: list, summary: dict,
+                               contract_no: str, output_dir: str) -> str:
+    """生成Proforma Invoice"""
+    import openpyxl
+    from openpyxl.utils import get_column_letter
+
+    # 复制模板
+    template_path = get_asset_path('proforma-invoice-template.xlsx')
+    output_filename = f"{contract_no}-Proforma Invoice.xlsx"
+    output_path = os.path.join(output_dir, output_filename)
+    shutil.copy(template_path, output_path)
+
+    # 打开并修改
+    wb = openpyxl.load_workbook(output_path)
+    ws = wb['INVOICE']
+
+    # 买方信息
+    ws['B7'] = f"TO:  {order.get('buyer_en', '')}"
+    ws['G7'] = f"Contract NO.: {contract_no}"
+    ws['H8'] = format_date_for_cell()
+    ws['B8'] = f"ADD: {order.get('address_en', '')}"
+    ws['B9'] = f"Tel: {order.get('tel', '')}"
+
+    # 港口信息
+    port_cn, port_en = get_port_names(order.get('departure_port', '广州南沙'))
+    trade_term = order.get('trade_term', 'FCA')
+
+    # 商品明细 (B17-H22)
+    start_row = 17
+    for i, v in enumerate(vehicles):
+        row = start_row + i
+        if row > 22:
+            break  # 模板最多支持到22行
+
+        desc = v.get('config_desc') or build_vehicle_description(v, v.get('_match_result'))
+        if not desc:
+            desc = f"Wuling {v['model_code']}, {v['engine_code']}"
+
+        ws.cell(row=row, column=2, value=desc)  # B列: DESCRIPTION
+        ws.cell(row=row, column=3, value=v['quantity'])  # C列: QTY
+        ws.cell(row=row, column=4, value='UNIT')  # D列
+        ws.cell(row=row, column=5, value='USD')  # E列
+        ws.cell(row=row, column=6, value=v['unit_price_usd'])  # F列: U.PRICE
+        ws.cell(row=row, column=7, value='USD')  # G列
+        # H列保留公式 =C*F
+
+    # 清除未使用的行(如果车辆少于模板默认行数)
+    # 注意: cell.value = None 在 openpyxl 中不能真正清除单元格,必须用 _value = None
+    for row in range(start_row + len(vehicles), 23):
+        for col in range(2, 9):
+            ws.cell(row=row, column=col)._value = None
+
+    # 汇总信息
+    ws['E23'] = f"  {trade_term}   {port_en} "
+    ws['B24'] = summary['amount_in_words']
+
+    # Delivery terms
+    ws['B27'] = f"1. Delivery terms:  {trade_term} {port_en}  "
+
+    # Payment terms
+    payment_text = (
+        f"3. Payment terms:\n"
+        f"1. 30% of the Total Amount (USD {summary['deposit_30']:,}) shall be paid to Seller within 5 working days from order confirmation.\n"
+        f"2. 70% of the Total Amount (USD {summary['balance_70']:,}) shall be paid to Seller within 10 working days before delivery.     "
+    )
+    ws['B29'] = payment_text
+
+    wb.save(output_path)
+    wb.close()
+
+    return output_path
+
+
+def accept_all_revisions(doc):
+    """
+    清除文档中的所有修订标记(Track Changes)和批注。
+    解决WPS/Word模板中未接受修订导致内容重复显示的问题。
+    策略:仅处理正文段落(不遍历表格),避免误删表格中的图片。
+    """
+    body = doc.element.body
+    ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
+    from docx.oxml.ns import qn
+    
+    # 仅处理 body 直接子元素中的段落(跳过表格内的内容,保护图片)
+    for p in body.findall(qn('w:p')):
+        # 删除段落内的 <w:del> 元素
+        for del_elem in list(p.iter(f'{{{ns}}}del')):
+            parent = del_elem.getparent()
+            if parent is not None:
+                parent.remove(del_elem)
+        # 删除段落内的 <w:ins> 元素
+        for ins_elem in list(p.iter(f'{{{ns}}}ins')):
+            parent = ins_elem.getparent()
+            if parent is not None:
+                parent.remove(ins_elem)
+        # 删除批注标记
+        for tag in ['commentRangeStart', 'commentRangeEnd', 'commentReference']:
+            for elem in list(p.iter(f'{{{ns}}}{tag}')):
+                parent = elem.getparent()
+                if parent is not None:
+                    parent.remove(elem)
+
+
+def generate_sales_contract(order: dict, vehicles: list, summary: dict,
+                            contract_no: str, output_dir: str) -> str:
+    """生成销售合同 (Word)"""
+    from docx import Document
+
+    # 复制模板
+    template_path = get_asset_path('vehicle-sales-contract-template.docx')
+    output_filename = f"{contract_no}-车辆销售合同.docx"
+    output_path = os.path.join(output_dir, output_filename)
+    shutil.copy(template_path, output_path)
+
+    # 打开并修改
+    doc = Document(output_path)
+    
+    # 先接受所有修订、删除批注,避免WPS显示重复内容
+    accept_all_revisions(doc)
+
+    port_cn, port_en = get_port_names(order.get('departure_port', '广州南沙'))
+    trade_term = order.get('trade_term', 'FCA')
+
+    # 1. 替换段落文本(保留原有格式)
+    for para in doc.paragraphs:
+        text = para.text
+        if not text:
+            continue
+
+        # 合同编号: No.: → No.: HLXYWPA...
+        if 'No.:' in text and not re.search(r'No\.:\s*\S', text):
+            fill_paragraph_value(para, 'No.:', f' {contract_no}')
+
+        # 签署日期
+        if 'Signature Date:' in text and not re.search(r'Signature Date:\s*\d', text):
+            fill_paragraph_value(para, 'Signature Date:', f' {format_date()}')
+
+        # 买方英文
+        if text.strip() == 'Buyer:':
+            fill_paragraph_value(para, 'Buyer:', f' {order.get("buyer_en", "")}')
+
+        # 买方中文
+        if text.strip() == '买方:':
+            fill_paragraph_value(para, '买方:', f'{order.get("buyer_cn", "")}')
+
+        # 英文地址(匹配 ADD: 或 ADD : 等变体,且内容较短说明是占位符)
+        if re.search(r'ADD\s*[::]', text) and len(text.strip()) < 10:
+            # 查找实际标记(支持空格和不间断空格)
+            m = re.search(r'(ADD\s*[::])', text)
+            if m:
+                fill_paragraph_value(para, m.group(1), f' {order.get("address_en", "")}')
+
+        # 中文地址
+        if re.search(r'地址\s*[::]', text) and len(text.strip()) < 10:
+            m = re.search(r'(地址\s*[::])', text)
+            if m:
+                fill_paragraph_value(para, m.group(1), f'{order.get("address_cn", "")}')
+
+        # 电话
+        if text.strip() == 'Tel电话:':
+            fill_paragraph_value(para, 'Tel电话:', f' {order.get("tel", "")}')
+
+        # 出口目的地(英文)
+        if 'Export Destination:' in text:
+            if para.runs:
+                for run in para.runs:
+                    if run.text == ' ' and run.bold is None:
+                        run.text = f' {order.get("destination_country", "")} '
+                        break
+
+        # 出口目的地(中文)
+        if '出口目的地:' in text:
+            dest_cn = order.get('destination_country', '')
+            if dest_cn and para.runs:
+                # 在":"后插入国家名,保留后面的(下称"区域")
+                for run in para.runs:
+                    if '(下称' in run.text:
+                        run.text = run.text.replace('(下称', f'{dest_cn}(下称')
+                        break
+
+        # 贸易条款(中英文同段落)
+        if 'Incoterms:' in text:
+            if para.runs:
+                src = para.runs[0]  # 保留第一个run的格式(bold=True)
+                para.clear()
+                # 英文部分
+                run_en = para.add_run(f"Incoterms: {trade_term} {port_en}")
+                copy_run_format(src, run_en)
+                # 中文部分
+                run_cn = para.add_run(f"贸易条款:{trade_term} {port_cn}")
+                set_run_format(run_cn, bold=None)
+
+        # 付款条款英文
+        if '30% of the Total Amount' in text and 'shall be paid' in text:
+            if para.runs:
+                src = para.runs[-1]
+                para.clear()
+                run = para.add_run(
+                    f"30% of the Total Amount shall be paid to the Seller by T/T before the production. "
+                    f"70% of the Total Amount shall be paid to the Seller by the Buyer by means of T/T before the delivery of vehicles."
+                )
+                copy_run_format(src, run)
+
+        # 付款条款中文
+        if '生产前,买方向卖方通过电汇支付' in text:
+            if para.runs:
+                src = para.runs[-1]
+                para.clear()
+                run = para.add_run(
+                    f"生产前,买方向卖方通过电汇支付本合同项下30% 货款。"
+                    f"发运前,买方向卖方通过电汇支付本合同项下70% 货款。"
+                )
+                copy_run_format(src, run)
+
+    # 2. 插入买方信息(签字区域,卖方信息之后)
+    buyer_en = order.get('buyer_en', '')
+    buyer_cn = order.get('buyer_cn', '')
+    if buyer_en or buyer_cn:
+        # 找到第二个 "Signature 签字:" (买方签字区) 前的空白段落
+        sig_count = 0
+        buyer_sig_idx = -1
+        for i, para in enumerate(doc.paragraphs):
+            if 'Signature' in para.text and '签字' in para.text:
+                sig_count += 1
+                if sig_count == 2:
+                    buyer_sig_idx = i
+                    break
+        if buyer_sig_idx > 0:
+            # 从卖方Title之后向前找空段落,按顺序填入Buyer(EN)和买方(CN)
+            empty_slots = []
+            for i in range(buyer_sig_idx - 1, max(buyer_sig_idx - 6, 0), -1):
+                if not doc.paragraphs[i].text.strip():
+                    empty_slots.append(i)
+                else:
+                    break  # 遇到非空段落就停止
+            empty_slots.reverse()  # 正序(从上到下)
+            if len(empty_slots) >= 2 and buyer_en:
+                run = doc.paragraphs[empty_slots[0]].add_run(f'Buyer: {buyer_en}')
+                set_run_format(run, bold=True)
+            if len(empty_slots) >= 2 and buyer_cn:
+                run = doc.paragraphs[empty_slots[1]].add_run(f'买方:{buyer_cn}')
+                set_run_format(run, bold=True)
+            elif len(empty_slots) >= 1 and buyer_cn:
+                run = doc.paragraphs[empty_slots[0]].add_run(f'买方:{buyer_cn}')
+                set_run_format(run, bold=True)
+
+    # 3. 替换表格内容
+    for table in doc.tables:
+        first_cell_text = table.rows[0].cells[0].text.strip() if table.rows else ''
+
+        if 'DESCRIPTION' in first_cell_text or '货物描述' in first_cell_text:
+            _fill_vehicle_table(table, vehicles, summary, trade_term, port_cn, port_en)
+        # 银行账户表和商标表保持原样
+
+    doc.save(output_path)
+    return output_path
+
+
+def _fill_vehicle_table(table, vehicles, summary, trade_term, port_cn, port_en):
+    """填充销售合同中的车辆明细表(保留格式 + 删除多余行)"""
+    data_start_row = 1  # 表头后第1行开始数据
+
+    # 先找到汇总行位置
+    summary_row_idx = None
+    for idx, row in enumerate(table.rows):
+        if idx == 0:
+            continue
+        row_text = ' '.join(cell.text for cell in row.cells)
+        if 'Total' in row_text or '总数量' in row_text or 'SAY USD' in row_text:
+            summary_row_idx = idx
+            break
+
+    # 确定可用的数据行(汇总行之前的行)
+    available_data_rows = []
+    for idx in range(data_start_row, len(table.rows)):
+        if idx == summary_row_idx:
+            break
+        available_data_rows.append(idx)
+
+    # 1. 填充车辆数据
+    for i, v in enumerate(vehicles):
+        if i >= len(available_data_rows):
+            break
+        row = table.rows[available_data_rows[i]]
+        desc = build_full_description(v, v.get('_match_result'))
+
+        if len(row.cells) > 0:
+            set_cell_multiline(row.cells[0], desc)
+        if len(row.cells) > 1:
+            set_cell_text(row.cells[1], str(v['quantity']))
+        if len(row.cells) > 2:
+            set_cell_text(row.cells[2], str(v['unit_price_usd']))
+        if len(row.cells) > 3:
+            set_cell_text(row.cells[3], str(round(v['unit_price_usd'] * v['quantity'], 2)))
+
+    # 2. 先填充汇总行(在删除操作之前,避免索引失效)
+    if summary_row_idx is not None and summary_row_idx < len(table.rows):
+        summary_row = table.rows[summary_row_idx]
+        amount_en = summary['amount_in_words'].replace('SAY US ', '').replace(' only', '')
+        amount_cn = summary['amount_in_chinese']
+
+        total_text = (
+            f"Total: {summary['total_qty']} units  总数量: {summary['total_qty']} 辆    \n"
+            f"Total Amount:  USD {summary['total_amount']:.2f}  总金额: 美元 {summary['total_amount']:.2f}\n"
+            f"(Say USD: {amount_en}  ONLY)\n"
+            f"(合计美元:{amount_cn}美元整)\n"
+            f"{trade_term}  {port_en}    {trade_term}  {port_cn}"
+        )
+
+        for cell in summary_row.cells:
+            set_cell_multiline(cell, total_text)
+
+    # 3. 删除多余空白行(从后往前删,跳过汇总行)
+    filled_count = min(len(vehicles), len(available_data_rows))
+    rows_to_delete = []
+    for idx in range(data_start_row + filled_count, len(table.rows)):
+        if idx == summary_row_idx:
+            break
+        rows_to_delete.append(table.rows[idx])
+    for row in reversed(rows_to_delete):
+        delete_table_row(table, row)
+
+
+def generate_contracts(order_text: str, output_dir: str = '.',
+                        user_prices: dict = None) -> dict:
+    """
+    主函数:根据订单文本生成合同
+
+    Args:
+        order_text: 订单信息文本
+        output_dir: 输出目录
+        user_prices: 用户手动提供的价格 {model_code: price}
+
+    Returns:
+        {
+            'pi_path': str,
+            'contract_path': str,
+            'contract_no': str,
+            'summary': dict,
+            'missing_prices': list,
+        }
+    """
+    # 1. 解析订单
+    order = parse_order_info(order_text)
+
+    # 2. 生成合同编号
+    contract_no = generate_contract_no()
+
+    # 3. 处理车型价格
+    vehicles = order.get('vehicles', [])
+    trade_term = order.get('trade_term', 'FCA')
+
+    # 应用用户提供的价格
+    if user_prices:
+        for v in vehicles:
+            if v['model_code'] in user_prices:
+                v['unit_price_usd'] = user_prices[v['model_code']]
+
+    processed, missing = process_vehicles(vehicles, trade_term)
+
+    # 如果有缺失价格,返回提示信息
+    if missing:
+        return {
+            'success': False,
+            'missing_prices': missing,
+            'message': f"以下车型在价格表中未找到,请提供单价USD: {[v['model_code'] for v in missing]}",
+        }
+
+    # 4. 计算汇总
+    summary = calculate_summary(processed)
+
+    # 5. 生成文件
+    os.makedirs(output_dir, exist_ok=True)
+
+    pi_path = generate_proforma_invoice(order, processed, summary, contract_no, output_dir)
+    contract_path = generate_sales_contract(order, processed, summary, contract_no, output_dir)
+
+    return {
+        'success': True,
+        'contract_no': contract_no,
+        'pi_path': pi_path,
+        'contract_path': contract_path,
+        'summary': summary,
+        'missing_prices': [],
+    }
+
+
+if __name__ == '__main__':
+    import argparse
+
+    parser = argparse.ArgumentParser(description='Generate export contracts from order info')
+    parser.add_argument('order_file', help='Path to order info text file')
+    parser.add_argument('-o', '--output', default='.', help='Output directory')
+    parser.add_argument('-p', '--prices', help='JSON string of manual prices {"LZW1028SPY": 6176}')
+
+    args = parser.parse_args()
+
+    with open(args.order_file, 'r', encoding='utf-8') as f:
+        order_text = f.read()
+
+    user_prices = None
+    if args.prices:
+        import json
+        user_prices = json.loads(args.prices)
+
+    result = generate_contracts(order_text, args.output, user_prices)
+
+    import json as json_mod
+    sys.stdout.reconfigure(encoding='utf-8')
+    print(json_mod.dumps(result, ensure_ascii=False, indent=2))

+ 164 - 0
scripts/match_vehicle.py

@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+车型匹配脚本:在价格配置表中按ModelCode匹配车型信息
+"""
+
+import os
+import openpyxl
+
+# 价格列映射
+PRICE_COLUMN_MAP = {
+    'FOB': 'N',
+    'FCA': 'O',
+    'EXW': 'P',
+}
+
+PRICE_COL_INDEX = {
+    'FOB': 13,  # N列 (0-based index)
+    'FCA': 14,  # O列
+    'EXW': 15,  # P列
+}
+
+
+def get_price_config_path():
+    """获取价格配置表路径"""
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    return os.path.join(script_dir, '..', 'assets', 'vehicle-price-config.xlsx')
+
+
+def match_vehicle(model_code: str, trade_term: str = 'FCA'):
+    """
+    在价格配置表中匹配车型
+    
+    Args:
+        model_code: 车型型号,如 LZW5030XXYLGHUG
+        trade_term: 贸易条款,如 FCA/FOB/EXW
+    
+    Returns:
+        dict or None: {
+            'unit_price_usd': float,
+            'description_cn': str,
+            'description_en': str,
+            'model_code': str,
+            'engine_code': str,
+            'version': str,
+            'config_desc': str,
+            'sheet_name': str,
+        }
+    """
+    if not model_code:
+        return None
+    
+    model_code = model_code.strip().upper()
+    trade_term = trade_term.upper()
+    
+    # 默认使用FCA价格列
+    price_col_idx = PRICE_COL_INDEX.get(trade_term, 14)
+    
+    config_path = get_price_config_path()
+    if not os.path.exists(config_path):
+        print(f"[WARN] Price config not found: {config_path}")
+        return None
+    
+    wb = openpyxl.load_workbook(config_path, data_only=True)
+    
+    for sheet_name in wb.sheetnames:
+        ws = wb[sheet_name]
+        for row in ws.iter_rows(min_row=2, max_row=ws.max_row, values_only=False):
+            # G列是型号 (index 6)
+            cell_g = row[6] if len(row) > 6 else None
+            if cell_g and cell_g.value:
+                # ModelCode可能包含换行符,如 "LZW5030XXYLGHUG\n国六B(RDE)"
+                cell_val = str(cell_g.value).strip().upper()
+                # 分割换行符取第一行作为ModelCode
+                cell_model = cell_val.split('\n')[0].strip()
+                if cell_model == model_code:
+                    # 找到匹配
+                    result = {
+                        'model_code': model_code,
+                        'sheet_name': sheet_name,
+                    }
+                    
+                    # B列: 市场名称
+                    result['description_cn'] = str(row[1].value).strip() if row[1].value else ''
+                    # C列: 版本
+                    result['version'] = str(row[2].value).strip() if row[2].value else ''
+                    # F列: 系别 (中英文)
+                    series_val = str(row[5].value).strip() if row[5].value else ''
+                    result['description_en'] = series_val
+                    # 分离F列中英文
+                    series_lines = [l.strip() for l in series_val.split('\n') if l.strip()]
+                    result['series_cn'] = series_lines[0] if series_lines else ''
+                    result['series_en'] = series_lines[1] if len(series_lines) > 1 else ''
+                    # I列: 发动机代码
+                    result['engine_code'] = str(row[8].value).strip() if row[8].value else ''
+                    # J列: 主要配置描述
+                    result['config_desc'] = str(row[9].value).strip() if row[9].value else ''
+                    
+                    # 价格列
+                    price_cell = row[price_col_idx] if len(row) > price_col_idx else None
+                    if price_cell and price_cell.value:
+                        try:
+                            result['unit_price_usd'] = round(float(price_cell.value), 2)
+                        except (ValueError, TypeError):
+                            result['unit_price_usd'] = None
+                    else:
+                        result['unit_price_usd'] = None
+                    
+                    wb.close()
+                    return result
+    
+    wb.close()
+    return None
+
+
+def build_vehicle_description(vehicle: dict, match_result: dict = None) -> str:
+    """
+    构建PI中的商品描述文本
+    
+    格式示例: Wuling LZW5030XXYLGHUG, AGMC,1.999L
+    """
+    import re
+    
+    model_code = vehicle.get('model_code', '')
+    engine_code = vehicle.get('engine_code', '')
+    
+    # 尝试从匹配结果获取排量信息
+    displacement = ''
+    
+    # 1. 从配置描述中提取排量,如 "(1.999L)"
+    if match_result and match_result.get('config_desc'):
+        m = re.search(r'\(([\d.]+L)\)', match_result['config_desc'])
+        if m:
+            displacement = m.group(1)
+    
+    # 2. 从发动机代码中提取排量,如 "VR5\n(1.999L)\n5MT"
+    if not displacement and match_result and match_result.get('engine_code'):
+        m = re.search(r'\(([\d.]+L)\)', match_result['engine_code'])
+        if m:
+            displacement = m.group(1)
+    
+    # 3. 从版本信息推断
+    if not displacement and vehicle.get('version'):
+        m = re.search(r'([\d.]+L)', vehicle['version'])
+        if m:
+            displacement = m.group(1)
+    
+    # 4. 从车型名称中推断(如 "2.0L 5MT")
+    if not displacement and vehicle.get('name_cn'):
+        m = re.search(r'([\d.]+L)', vehicle['name_cn'])
+        if m:
+            displacement = m.group(1)
+    
+    parts = [p for p in [f'Wuling {model_code}', engine_code, displacement] if p]
+    return ', '.join(parts)
+
+
+if __name__ == '__main__':
+    # 测试
+    result = match_vehicle('LZW5030XXYLGHUG', 'FCA')
+    print(result)
+    
+    result2 = match_vehicle('LZW1028SPY', 'FCA')
+    print(result2)

+ 66 - 0
scripts/number_to_words.py

@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+金额英文大写转换
+"""
+
+from num2words import num2words
+import math
+
+
+def amount_to_english_words(amount: float) -> str:
+    """
+    将金额转换为英文大写
+    
+    Args:
+        amount: 金额数字,如 14488
+    
+    Returns:
+        英文大写字符串,如 "FOURTEEN THOUSAND FOUR HUNDRED AND EIGHTY-EIGHT"
+    """
+    if amount is None or amount < 0:
+        return ''
+    
+    # 四舍五入到整数
+    amount_int = round(amount)
+    
+    # 转换为英文单词并大写
+    words = num2words(amount_int, lang='en')
+    return words.upper().replace('-', ' ').replace(',', '')
+
+
+def calculate_payment_split(total_amount: float) -> tuple:
+    """
+    计算30%和70%的付款金额
+    
+    规则:30%向上取整,70% = 总金额 - 30%
+    确保 30% + 70% = 总金额
+    
+    Returns:
+        (deposit_30, balance_70)
+    """
+    total_int = round(total_amount)
+    deposit_30 = math.ceil(total_int * 0.30)
+    balance_70 = total_int - deposit_30
+    return deposit_30, balance_70
+
+
+def format_say_us_only(amount: float) -> str:
+    """
+    格式化为 "SAY US {大写} only"
+    
+    Args:
+        amount: 金额数字
+    
+    Returns:
+        如 "SAY US FOURTEEN THOUSAND FOUR HUNDRED AND EIGHTY EIGHT only"
+    """
+    words = amount_to_english_words(amount)
+    return f"SAY US {words} only"
+
+
+if __name__ == '__main__':
+    # 测试
+    print(format_say_us_only(14488))
+    print(format_say_us_only(4346.4))
+    print(format_say_us_only(10141.6))

+ 258 - 0
scripts/parse_order_info.py

@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+订单信息解析脚本:支持编号列表格式和自然语言格式
+"""
+
+import re
+
+
+def parse_structured_order(text: str) -> dict:
+    """
+    解析编号列表格式的订单信息
+    """
+    result = {}
+    
+    # 基本信息提取
+    m = re.search(r'姓名[::]\s*(.+?)[;;\n]', text)
+    result['sales_name'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'任职[::]\s*(.+?)[;;\n]', text)
+    result['sales_title'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'合同签约公司中文名称[::]\s*(.+?)[;;\n]', text)
+    result['buyer_cn'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'合同签约公司英文名称[::]\s*(.+?)(?:\n|$)', text)
+    result['buyer_en'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'合同签约公司中文地址[::]\s*(.+?)[;;\n]', text)
+    result['address_cn'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'合同签约公司英文地址[::]\s*(.+?)(?:\n|$)', text)
+    result['address_en'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'合同联系电话[::]\s*([\d+\-]+)', text)
+    result['tel'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'国内出发港口[::]\s*(.+?)[;;\n]', text)
+    result['departure_port'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'目的地国家[::]\s*(.+?)(?:\n|$)', text)
+    result['destination_country'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'目的地港口[::]\s*(.+?)(?:\n|$)', text)
+    result['destination_port'] = m.group(1).strip() if m else ''
+    
+    m = re.search(r'结算方式[::]\s*(FCA|FOB|EXW|CIF)', text, re.IGNORECASE)
+    result['trade_term'] = m.group(1).upper() if m else 'FCA'
+    
+    # 车型列表解析 - 使用行扫描方式
+    vehicles = []
+    lines = text.split('\n')
+    
+    i = 0
+    while i < len(lines):
+        line = lines[i].strip()
+        # 匹配车型起始行: (数字)... 型号:XXX
+        m = re.match(r'[((](\d+)[))]\s*(.+?)\s*型号[::]\s*(\w+)', line)
+        if m:
+            seq = m.group(1)
+            name_part = m.group(2).strip()
+            model_code = m.group(3).strip()
+            
+            # 解析车型名称(中英文分离)
+            name_cn, name_en = split_name_cn_en(name_part)
+            
+            # 下一行提取排放、发动机代码、数量、颜色
+            engine_code = ''
+            emission = ''
+            quantity = 1
+            color = ''
+            
+            if i + 1 < len(lines):
+                next_line = lines[i + 1].strip()
+                # 模式: 排放标准  代码:XXX  X台颜色
+                m2 = re.search(r'(.+?)\s+代码[::]\s*(\w+)\s+(\d+)台(\w+)', next_line)
+                if m2:
+                    emission = m2.group(1).strip()
+                    engine_code = m2.group(2).strip()
+                    quantity = int(m2.group(3))
+                    color = m2.group(4).strip()
+                    i += 1  # 跳过已处理的下一行
+            
+            vehicles.append({
+                'seq': seq,
+                'name_cn': name_cn,
+                'name_en': name_en,
+                'model_code': model_code,
+                'emission': emission,
+                'engine_code': engine_code,
+                'quantity': quantity,
+                'color': color,
+                'unit_price_usd': None,
+                'config_desc': '',
+            })
+        i += 1
+    
+    result['vehicles'] = vehicles
+    return result
+
+
+def split_name_cn_en(name_part: str) -> tuple:
+    """
+    将车型描述分割为中文名和英文名
+    
+    例如:
+    '荣光新双排货车  N350 Double Cab Pickup,-' -> ('荣光新双排货车', 'N350 Double Cab Pickup')
+    '五菱荣光新卡双后轮 2.0L 5MT 单排' -> ('五菱荣光新卡双后轮 2.0L 5MT 单排', '')
+    """
+    # 移除末尾的标点符号
+    name_part = re.sub(r'[,,、\-]+$', '', name_part).strip()
+    
+    # 查找中英文分界点(连续英文字母序列)
+    # 策略:找到最后一个中文字符之后的主要英文部分
+    # 更简单的策略:如果包含明显的中英分界(中文字符后接大段英文)
+    
+    # 匹配模式: 中文部分 + 空格 + 英文部分(英文部分以字母/数字开头,包含字母、数字和空格)
+    m = re.match(r'([\u4e00-\u9fff][\u4e00-\u9fff\s\d.]+?)\s+([A-Za-z0-9][A-Za-z0-9\s\(\)]+)$', name_part)
+    if m:
+        return m.group(1).strip(), m.group(2).strip()
+    
+    # 如果没有明显的英文部分,全部作为中文名
+    if re.search(r'[\u4e00-\u9fff]', name_part):
+        return name_part, ''
+    
+    # 如果没有中文,全部作为英文名
+    return '', name_part
+
+
+def parse_natural_language(text: str) -> dict:
+    """
+    使用正则从自然语言中提取关键字段
+    """
+    result = {}
+    
+    # 买方公司名称
+    m = re.search(r'(?:买方|买方公司|签约公司|客户)[是|::]\s*([^,,;;。\n]+)', text)
+    if m:
+        name = m.group(1).strip()
+        if re.search(r'[\u4e00-\u9fff]', name):
+            result['buyer_cn'] = name
+            result['buyer_en'] = ''
+        else:
+            result['buyer_en'] = name
+            result['buyer_cn'] = ''
+    
+    if not result.get('buyer_en'):
+        m = re.search(r'英文名称[是|::]\s*([^,,;;。\n]+)', text)
+        if m:
+            result['buyer_en'] = m.group(1).strip()
+    
+    if not result.get('buyer_cn'):
+        m = re.search(r'中文名称[是|::]\s*([^,,;;。\n]+)', text)
+        if m:
+            result['buyer_cn'] = m.group(1).strip()
+    
+    # 地址
+    m = re.search(r'(?:地址|ADD)[是|::]\s*([^,,;;。\n]+)', text)
+    if m:
+        addr = m.group(1).strip()
+        if re.search(r'[\u4e00-\u9fff]', addr):
+            result['address_cn'] = addr
+            result['address_en'] = ''
+        else:
+            result['address_en'] = addr
+            result['address_cn'] = ''
+    
+    # 电话
+    m = re.search(r'(?:电话|Tel|联系电话)[是|::]\s*([\d+\-]+)', text)
+    if m:
+        result['tel'] = m.group(1).strip()
+    
+    # 贸易条款
+    m = re.search(r'(?:贸易条款|结算方式|Incoterm)[是|::]?\s*(FCA|FOB|EXW|CIF)', text, re.IGNORECASE)
+    result['trade_term'] = m.group(1).upper() if m else 'FCA'
+    
+    # 出发港口
+    m = re.search(r'(?:出发港|出发港口|起运港)[是|::]\s*([^,,;;。\n]+)', text)
+    result['departure_port'] = m.group(1).strip() if m else ''
+    
+    # 目的地国家
+    m = re.search(r'(?:目的地国家|目的国|出口到|发往)[是|::]?\s*([^,,;;。\n]+)', text)
+    result['destination_country'] = m.group(1).strip() if m else ''
+    
+    # 车型解析
+    vehicles = []
+    
+    # 模式1: ... 型号:XXX ... 代码:XXX ... X台
+    pattern = r'(?:车型[\d]*[::、..]?\s*)?(.+?)\s+(?:型号[::]\s*)?(LZW\w+).*?(?:代码[::]\s*)?(\w+).*?(\d+)台'
+    matches = re.findall(pattern, text, re.DOTALL)
+    for m in matches:
+        name_part = m[0].strip()
+        name_cn, name_en = split_name_cn_en(name_part)
+        vehicles.append({
+            'name_cn': name_cn,
+            'name_en': name_en,
+            'model_code': m[1].strip(),
+            'emission': '',
+            'engine_code': m[2].strip(),
+            'quantity': int(m[3]),
+            'color': '',
+            'unit_price_usd': None,
+            'config_desc': '',
+        })
+    
+    if not vehicles:
+        pattern2 = r'(LZW\w+)\s+(?:发动机代码[::]\s*)?(\w+).*?(\d+)台'
+        matches = re.findall(pattern2, text)
+        for m in matches:
+            vehicles.append({
+                'name_cn': '',
+                'name_en': '',
+                'model_code': m[0].strip(),
+                'emission': '',
+                'engine_code': m[1].strip(),
+                'quantity': int(m[2]),
+                'color': '',
+                'unit_price_usd': None,
+                'config_desc': '',
+            })
+    
+    result['vehicles'] = vehicles
+    return result
+
+
+def parse_order_info(text: str) -> dict:
+    """
+    主解析函数:自动判断格式并解析
+    """
+    text = text.strip()
+    
+    # 判断是否为结构化编号列表格式
+    is_structured = bool(re.search(r'^\d+[..]\s*(姓名|任职|意向车型|合同签约公司)', text, re.MULTILINE))
+    
+    if is_structured:
+        result = parse_structured_order(text)
+    else:
+        result = parse_natural_language(text)
+    
+    # 填充默认值
+    if not result.get('trade_term'):
+        result['trade_term'] = 'FCA'
+    if not result.get('departure_port'):
+        result['departure_port'] = 'Guangzhou nansha Port'
+    
+    return result
+
+
+if __name__ == '__main__':
+    import json
+    import sys
+    sys.stdout.reconfigure(encoding='utf-8')
+    
+    with open('../assets/订单合同信息案例.txt', encoding='utf-8') as f:
+        test_text = f.read()
+    
+    result = parse_order_info(test_text)
+    print(json.dumps(result, ensure_ascii=False, indent=2))