|
|
@@ -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:
|