فهرست منبع

v0.7(增加可导入报价表功能)

kyle 3 روز پیش
والد
کامیت
c39491884b

+ 14 - 3
auto-generate-export-contracts/SKILL.md

@@ -1,4 +1,4 @@
----
+---
 name: auto-generate-export-contracts
 description: "根据订单合同信息自动生成车辆出口销售合同(Word)和Proforma Invoice(Excel)。用于处理海外车辆出口业务。触发词:生成合同、生成PI、生成Proforma Invoice、做合同、做PI、合同信息、订单信息、出口合同、销售合同、车辆出口、帮我做合同、做出口合同、生成销售合同、FCA合同、FOB合同、买方信息、目的港、意向车型、合同签约公司、车辆明细。当用户提供包含以下任意字段的订单信息时触发:姓名/联系人、意向车型/车辆型号(LZW开头)、合同签约公司、买方公司、出发港口、目的港、结算方式(FCA/FOB/EXW)。支持格式:编号列表(1.姓名 2.任职 3.意向车型...)或自然语言描述。"
 ---
@@ -93,11 +93,12 @@ python scripts/generate_contracts.py order.txt -o ./output -q "报价表.xlsx"
 
 1. 加载报价表 xlsx,自动识别中英文两个 Sheet(中文报价表 / English Quotation)
 2. 按车型型号(D列 型号/Model)精确匹配订单中的每一款车
-3. 提取 N列(FCA价格USD)作为单价(已包含运费和加装费)
+3. 从报价表的 `导出设置` 行自动识别价格类型(FCA/FOB/EXW),提取对应列的价格
 4. 提取 I列配置信息、F列发动机描述,用于生成车型描述
 5. 提取英文 Sheet 的配置描述优先用于合同和PI
 6. 如果报价表中找不到某车型 → 提示用户手动补充价格(同现有流程)
 
+**支持的价格类型:** FCA / FOB / EXW 三种报价表均可自动识别,解析器按 `导出设置` 中的 `价格类型` 字段自动从对应列(`FCA价格(USD)` / `FOB价格(USD)` / `EXW价格(USD)`)提取单价。
 **适用场景:** 报价系统(API)返回的价格不含运费加装费时,直接使用报价表的数据。
 
 ### Step 3: 计算汇总
@@ -173,6 +174,8 @@ result = generate_contracts(order_text, output_dir='.', quotation_path='报价
 - `python-docx`
 - `openpyxl`
 - `num2words`
+- `requests`
+- `Pillow`
 
 ## 模板文件
 
@@ -184,4 +187,12 @@ result = generate_contracts(order_text, output_dir='.', quotation_path='报价
 
 ## 字段映射详情
 
-详见 [references/field-mapping.md](references/field-mapping.md)
+详见 [references/field-mapping.md](references/field-mapping.md)
+
+## ⚠️ 维护注意事项
+
+- **Windows 源更新 skill 时**:`scripts/parse_order_info.py` 的车型续行修复(无编号前缀续行匹配)在 Windows 源文件中不存在。更新后需重新在 `parse_structured_order()` 的 while 循环中添加续行匹配代码,参考之前的 patch。此修复添加在第二个车型匹配模式(`\d+[..]\s*意向车型`)后面,第三个 `if not m:` 分支中。
+- **`generate_contracts.py` 中的 `COUNTRY_NAME_MAP`**:如果需要新增国家映射,在字典中添加 `'国家中文名': 'EnglishName'` 条目。
+- **`_get_clean_template()`** 会在 skill 目录生成 `_pi_clean_template.xlsx` 缓存文件,可删除(会自动重建)。
+- **修改 skill 后同步回 Windows 源**:在 agent 中直接修改了 skill 文件后(如添加新功能、修复 bug),记得用 `cp` 将改动同步到 `C:\Users\86136\Desktop\海外合同skill\auto-generate-export-contracts\` 对应的文件,避免下次从桌面更新时覆盖丢失。需同步的文件通常包括:`scripts/generate_contracts.py`、`scripts/parse_order_info.py`、`scripts/parse_quotation.py`、`SKILL.md`。
+- **报价表解析器 `parse_quotation.py` 的列名支持**:`CN_COL_MAP` 和 `EN_COL_MAP` 中需同时维护 FCA / FOB / EXW 三种列名映射。`load_quotation()` 中的 `price_field` 选择逻辑默认用 FCA,检测到 `价格类型=FOB` 或 `价格类型=EXW` 时自动切换对应列。如果出现报价表解析不到价格,先用 openpyxl 检查实际列名是否已覆盖,再添加映射。

BIN
auto-generate-export-contracts/assets/~$hicle-sales-contract-template.docx


+ 9 - 0
auto-generate-export-contracts/scripts/parse_order_info.py

@@ -67,6 +67,15 @@ def parse_structured_order(text: str) -> dict:
                         # group(1)=name_part, group(2)=model_code → map to group(2), group(3)
                         self.group = lambda n: ['', '', real_m.group(1), real_m.group(2)][n]
                 m = _M(m)
+        if not m:
+            # 车型续行:无编号前缀,直接以车型名开头 + 型号 + 代码 + X台颜色
+            m = re.match(r'(.+?)\s*型号[::]\s*(LZW\w+).+?代码[::]\s*(\w+)', line)
+            if m:
+                class _M:
+                    def __init__(self, real_m):
+                        self._m = real_m
+                        self.group = lambda n: ['', '', real_m.group(1), real_m.group(2)][n]
+                m = _M(m)
         if m:
             seq = m.group(1)
             name_part = m.group(2).strip()

+ 43 - 22
auto-generate-export-contracts/scripts/parse_quotation.py

@@ -20,26 +20,34 @@ logger = logging.getLogger(__name__)
 
 # Chinese header -> internal field mapping
 CN_COL_MAP = {
-    '\u5e8f\u53f7': 'seq_no',
-    '\u63a8\u5e7f\u547d\u540d': 'promotion_name',
-    '\u7248\u672c': 'version',
-    '\u578b\u53f7': 'model_code',
-    '\u8f66\u578b\u4ee3\u7801': 'model_code2',
-    '\u53d1\u52a8\u673a': 'engine',
-    '\u6392\u653e\u6807\u51c6': 'emission_std',
-    '\u8f66\u8f86\u7c7b\u578b': 'vehicle_type',
-    '\u914d\u7f6e\u4fe1\u606f': 'config',
-    '\u56fe\u7247': 'picture',
-    '\u91c7\u8d2d\u6570\u91cf': 'quantity',
-    '\u52a0\u88c5\u4fe1\u606f': 'add_on_info',
-    'FCA\u4ef7\u683c(RMB)': 'fca_price_rmb',
-    'FCA\u4ef7\u683c(USD)': 'fca_price_usd',
-    '\u8fd0\u8d39(RMB)': 'freight_rmb',
-    '\u8fd0\u8d39(USD)': 'freight_usd',
-    '\u52a0\u88c5(RMB)': 'installation_rmb',
-    '\u52a0\u88c5(USD)': 'installation_usd',
-    'FCA\u5c0f\u8ba1(RMB)': 'fca_subtotal_rmb',
-    'FCA\u5c0f\u8ba1(USD)': 'fca_subtotal_usd',
+    '序号': 'seq_no',
+    '推广命名': 'promotion_name',
+    '版本': 'version',
+    '型号': 'model_code',
+    '车型代码': 'model_code2',
+    '发动机': 'engine',
+    '排放标准': 'emission_std',
+    '车辆类型': 'vehicle_type',
+    '配置信息': 'config',
+    '图片': 'picture',
+    '采购数量': 'quantity',
+    '加装信息': 'add_on_info',
+    'FCA价格(RMB)': 'fca_price_rmb',
+    'FCA价格(USD)': 'fca_price_usd',
+    'FOB价格(RMB)': 'fob_price_rmb',
+    'FOB价格(USD)': 'fob_price_usd',
+    'EXW价格(RMB)': 'exw_price_rmb',
+    'EXW价格(USD)': 'exw_price_usd',
+    '运费(RMB)': 'freight_rmb',
+    '运费(USD)': 'freight_usd',
+    '加装(RMB)': 'installation_rmb',
+    '加装(USD)': 'installation_usd',
+    'FCA小计(RMB)': 'fca_subtotal_rmb',
+    'FCA小计(USD)': 'fca_subtotal_usd',
+    'FOB小计(RMB)': 'fob_subtotal_rmb',
+    'FOB小计(USD)': 'fob_subtotal_usd',
+    'EXW小计(RMB)': 'exw_subtotal_rmb',
+    'EXW小计(USD)': 'exw_subtotal_usd',
 }
 
 # English header -> internal field mapping
@@ -58,12 +66,20 @@ EN_COL_MAP = {
     'Add-on': 'add_on_info',
     'FCA Price(RMB)': 'fca_price_rmb',
     'FCA Price(USD)': 'fca_price_usd',
+    'FOB Price(RMB)': 'fob_price_rmb',
+    'FOB Price(USD)': 'fob_price_usd',
+    'EXW Price(RMB)': 'exw_price_rmb',
+    'EXW Price(USD)': 'exw_price_usd',
     'Freight(RMB)': 'freight_rmb',
     'Freight(USD)': 'freight_usd',
     'Add-on(RMB)': 'installation_rmb',
     'Add-on(USD)': 'installation_usd',
     'FCA Subtotal(RMB)': 'fca_subtotal_rmb',
     'FCA Subtotal(USD)': 'fca_subtotal_usd',
+    'FOB Subtotal(RMB)': 'fob_subtotal_rmb',
+    'FOB Subtotal(USD)': 'fob_subtotal_usd',
+    'EXW Subtotal(RMB)': 'exw_subtotal_rmb',
+    'EXW Subtotal(USD)': 'exw_subtotal_usd',
 }
 
 
@@ -167,6 +183,11 @@ def load_quotation(excel_path: str) -> dict:
     en_rows, _, _ = _parse_sheet(wb['English Quotation'])
     en_by_model = {r['model_code']: r for r in en_rows if r.get('model_code')}
     merged = []
+    price_type = meta.get('price_type', 'FCA').upper()
+    price_field = {
+        'FOB': 'fob_price_usd',
+        'EXW': 'exw_price_usd',
+    }.get(price_type, 'fca_price_usd')
     for cr in cn_rows:
         mc = cr.get('model_code')
         er = en_by_model.get(mc, {})
@@ -175,8 +196,8 @@ def load_quotation(excel_path: str) -> dict:
             'name_cn': cr.get('promotion_name', ''),
             'name_en': er.get('promotion_name', ''),
             'version': cr.get('version', ''),
-            'unit_price_usd': cr.get('fca_price_usd'),
-            'unit_price_rmb': cr.get('fca_price_rmb'),
+            'unit_price_usd': cr.get(price_field) or cr.get('fca_price_usd'),
+            'unit_price_rmb': cr.get(price_field.replace('usd', 'rmb')) or cr.get('fca_price_rmb'),
             'freight_usd': cr.get('freight_usd'),
             'installation_usd': cr.get('installation_usd', 0),
             'config_desc_cn': cr.get('config', ''),