Selaa lähdekoodia

v0.3(修复日期不更新问题,优化出口目的地匹配,优化买方信息重复问题)

kyle 1 päivä sitten
vanhempi
commit
022c0562fa
1 muutettua tiedostoa jossa 166 lisäystä ja 90 poistoa
  1. 166 90
      auto-generate-export-contracts/scripts/generate_contracts.py

+ 166 - 90
auto-generate-export-contracts/scripts/generate_contracts.py

@@ -49,8 +49,8 @@ def generate_contract_no() -> str:
 
 
 def format_date() -> str:
-    """格式化当前日期"""
-    return datetime.now().strftime('%Y-%m-%d')
+    """格式化当前日期: 2026/06/01"""
+    return datetime.now().strftime('%Y/%m/%d')
 
 
 def format_date_for_cell():
@@ -136,10 +136,11 @@ def number_to_chinese(num: int) -> str:
     return result
 
 
-def fill_paragraph_value(para, marker: str, value: str) -> bool:
+def fill_paragraph_value(para, marker: str, value: str, force: bool = False) -> bool:
     """
     在段落中 marker 标记后填入 value,保留原有格式。
     不直接设置 para.text(这会破坏所有 run 格式)。
+    force=True 时强制替换已有值(用于模板中预填了旧数据的场景)。
     """
     text = para.text
     if marker not in text:
@@ -150,10 +151,35 @@ def fill_paragraph_value(para, marker: str, value: str) -> bool:
         return False
 
     after = parts[1].strip()
-    if after and after != value.strip():
-        # 已有内容且不是我们要填的值,视为已填充
+    if not force and after and after != value.strip():
+        # 已有内容且不是我们要填的值,视为已填充(仅非force模式跳过)
         return False
 
+    if force and after:
+        # 强制模式:清除 marker 之后的所有 run 文本,只保留 marker 部分
+        # 因为 marker 可能跨多个 run,用累积文本定位 marker 结束位置
+        full_text = ''
+        marker_end_run_idx = -1
+        marker_end_char_in_run = -1
+        for i, run in enumerate(para.runs):
+            prev_len = len(full_text)
+            full_text += run.text
+            marker_pos = full_text.find(marker)
+            if marker_pos >= 0:
+                marker_end_pos = marker_pos + len(marker)
+                if marker_end_pos <= len(full_text):
+                    marker_end_run_idx = i
+                    marker_end_char_in_run = marker_end_pos - prev_len
+                    break
+
+        if marker_end_run_idx >= 0:
+            # 清除 marker 结束位置之后的所有 run
+            for i in range(marker_end_run_idx + 1, len(para.runs)):
+                para.runs[i].text = ''
+            # 截断 marker 所在 run(保留 marker 部分)
+            run = para.runs[marker_end_run_idx]
+            run.text = run.text[:marker_end_char_in_run]
+
     # 找到最后一个 run
     if not para.runs:
         run = para.add_run(value)
@@ -263,29 +289,42 @@ def set_cell_multiline(cell, text):
         tc.append(new_p)
 
 
+COLOR_MAP = {
+    '银色': ('silver', '银色'), '白色': ('white', '白色'), '黑色': ('black', '黑色'),
+    '红色': ('red', '红色'), '蓝色': ('blue', '蓝色'), '灰色': ('grey', '灰色'),
+    '金色': ('gold', '金色'), '绿色': ('green', '绿色'), '棕色': ('brown', '棕色'),
+    '橙色': ('orange', '橙色'), '粉色': ('pink', '粉色'), '紫色': ('purple', '紫色'),
+}
+
+
 def build_full_description(vehicle: dict, match_result: dict = None) -> str:
     """
     构建销售合同中 Commodity 表格的完整货物描述。
-    格式参考模板:
+    格式严格对齐模板:
       Model: {model_code}, {engine_code} {中文车型名}
-      {英文车型名}
-      {中文配置}
-      {英文配置}
-      Color: {color}
-      型号:{model_code}, {engine_code} {英文车型名}
+      {中文配置描述}
+      Color: {color_en} {color_code}
+      型号:{model_code}, {engine_code} {英文车型名/中文车型名}
+      {英文配置描述}
+      颜色:{color_cn} {color_code}
     """
     model_code = vehicle.get('model_code', '')
     engine_code = vehicle.get('engine_code', '')
-    color = vehicle.get('color', '1')
+    color = vehicle.get('color', '')
     name_cn = vehicle.get('name_cn', '')
     name_en = vehicle.get('name_en', '')
 
+    # 颜色中英文映射
+    color_en, color_cn = COLOR_MAP.get(color, (color, color))
+    color_display = color if color else ''
+
     lines = []
 
     if match_result:
-        # 有价格表匹配,使用价格表信息
         desc_cn = match_result.get('description_cn', '')
         series_en = match_result.get('series_en', '')
+        # 用价格表的engine_code作为详细规格(如 "VR5\n(1.999L)\n5MT"),但Model行用用户提供的engine_code
+        engine_specs = match_result.get('engine_code', '').replace('\n', ' ')
         config_desc = match_result.get('config_desc', '')
 
         # 分离中英文配置描述
@@ -299,52 +338,58 @@ def build_full_description(vehicle: dict, match_result: dict = None) -> str:
                 else:
                     config_en = part
 
-        # Model 行(英文)
+        # === 英文部分 ===
+        # Model 行: Model: LZW5030XXYLGHUG, AGMC
         model_line = f"Model: {model_code}, {engine_code}"
-        if series_en:
-            model_line += f" {series_en}"
+        if desc_cn:
+            model_line += f" {desc_cn}"
         lines.append(model_line)
 
+        # 中文配置描述(在英文Model行之后,如 "基本型 (5座...)")
+        if config_cn:
+            lines.append(config_cn)
+
+        # Color (English)
+        lines.append(f"Color: {color_en}" if color_display else "Color: ")
+
+        # === 中文部分 ===
+        # 型号行
+        model_cn_line = f"型号:{model_code}, {engine_code}"
+        if series_en:
+            model_cn_line += f" {series_en}"
+        elif name_en:
+            model_cn_line += f" {name_en}"
+        elif desc_cn:
+            model_cn_line += f" {desc_cn}"
+        lines.append(model_cn_line)
+
         # 英文配置描述
         if config_en:
             lines.append(config_en)
+
+        # 颜色(中文)
+        lines.append(f"颜色:{color_cn}" if color_display else "颜色:")
     else:
         # 无价格表匹配,使用订单信息
+        # === 英文部分 ===
         model_line = f"Model: {model_code}, {engine_code}"
-        if name_en:
-            model_line += f" {name_en}"
-        elif name_cn:
+        if name_cn:
             model_line += f" {name_cn}"
+        elif name_en:
+            model_line += f" {name_en}"
         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("颜色:")
+        lines.append(f"Color: {color_en}" if color_display else "Color: ")
+
+        # === 中文部分 ===
+        model_cn_line = f"型号:{model_code}, {engine_code}"
+        if name_en:
+            model_cn_line += f" {name_en}"
+        elif name_cn:
+            model_cn_line += f" {name_cn}"
+        lines.append(model_cn_line)
+
+        lines.append(f"颜色:{color_cn}" if color_display else "颜色:")
 
     return '\n'.join(lines)
 
@@ -528,13 +573,13 @@ def generate_sales_contract(order: dict, vehicles: list, summary: dict,
         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}')
+        # 合同编号: No.: → No.: HLXYWPA...(强制替换模板中预填的旧编号)
+        if 'No.:' in text:
+            fill_paragraph_value(para, 'No.:', f' {contract_no}', force=True)
 
-        # 签署日期
-        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 'Signature Date:' in text:
+            fill_paragraph_value(para, 'Signature Date:', f' {format_date()}', force=True)
 
         # 买方英文
         if text.strip() == 'Buyer:':
@@ -561,22 +606,41 @@ def generate_sales_contract(order: dict, vehicles: list, summary: dict,
         if text.strip() == 'Tel电话:':
             fill_paragraph_value(para, 'Tel电话:', f' {order.get("tel", "")}')
 
-        # 出口目的地(英文)
+        # 出口目的地(英文)— 替换模板中已有的国家名
         if 'Export Destination:' in text:
-            if para.runs:
+            dest_raw = order.get('destination_country', '')
+            if dest_raw and para.runs:
+                # 从混合字符串中提取英文部分(如 "巴拿马Panama" → "Panama")
+                dest_en = re.sub(r'[\u4e00-\u9fff]+', '', dest_raw).strip()
+                if not dest_en:
+                    dest_en = dest_raw
+                # 找到包含国家名的 run(非 bold、非括号说明部分),替换为新国家名
                 for run in para.runs:
-                    if run.text == ' ' and run.bold is None:
-                        run.text = f' {order.get("destination_country", "")} '
+                    if run.text.strip() and not run.bold and 'hereinafter' not in run.text and 'Export Destination' not in run.text:
+                        run.text = dest_en
                         break
 
-        # 出口目的地(中文)
+        # 出口目的地(中文)— 替换模板中已有的国家名
         if '出口目的地:' in text:
-            dest_cn = order.get('destination_country', '')
-            if dest_cn and para.runs:
-                # 在":"后插入国家名,保留后面的(下称"区域")
+            dest_raw = order.get('destination_country_cn', '') or order.get('destination_country', '')
+            if dest_raw and para.runs:
+                # 从混合字符串中提取中文部分(如 "巴拿马Panama" → "巴拿马")
+                dest_cn = re.sub(r'[A-Za-z0-9\s]+', '', dest_raw).strip()
+                if not dest_cn:
+                    dest_cn = dest_raw
+                # 找到":"run 之后的第一个内容 run(非括号说明),替换国家名
+                colon_seen = False
                 for run in para.runs:
-                    if '(下称' in run.text:
-                        run.text = run.text.replace('(下称', f'{dest_cn}(下称')
+                    if ':' in run.text:
+                        colon_seen = True
+                        # 如果":"和国家名在同一个run中,需要分离
+                        after_colon = run.text.split(':', 1)
+                        if len(after_colon) > 1 and after_colon[1].strip():
+                            run.text = ':' + dest_cn
+                            break
+                        continue
+                    if colon_seen and run.text.strip() and '(下称' not in run.text:
+                        run.text = dest_cn
                         break
 
         # 贸易条款(中英文同段落)
@@ -614,36 +678,48 @@ def generate_sales_contract(order: dict, vehicles: list, summary: dict,
                 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)
+        # 检查买方信息是否已存在于签字区(模板可能预填了)
+        has_buyer_in_sig = False
+        for para in doc.paragraphs:
+            if buyer_en and buyer_en in para.text:
+                has_buyer_in_sig = True
+                break
+            if buyer_cn and buyer_cn in para.text:
+                has_buyer_in_sig = True
+                break
+
+        if not has_buyer_in_sig:
+            # 找到第二个 "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: