Files
Contract-document-crawling-…/new_页面内容.py
2026-01-19 10:46:05 +08:00

758 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import os
import time
import threading
import re
import urllib.parse
import webbrowser
import json
from datetime import datetime
import tkinter as tk
from tkinter import filedialog, messagebox
import requests
import pandas as pd
from lxml import html
# ================= 1. 导入 UI 库 =================
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
# 兼容导入
try:
from ttkbootstrap.widgets import ScrolledText, Tableview, ToastNotification
except ImportError:
from ttkbootstrap.scrolled import ScrolledText
from ttkbootstrap.tableview import Tableview
from ttkbootstrap.toast import ToastNotification
# ================= 2. 后端核心逻辑 =================
class CRMCrawler:
def __init__(self, log_callback, data_callback):
self.log = log_callback
self.on_data = data_callback
self.stop_flag = False
self.session = requests.Session()
self.base_url = "http://111.198.24.44:88/index.php"
self.http_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json, text/javascript, */*; q=0.01"
}
def login(self, username, password):
self.log(f"🔑 正在登录... 用户: {username}")
login_payload = {
"module": "Users", "action": "Authenticate", "return_module": "Users",
"return_action": "Login", "user_name": username, "user_password": password, "login_theme": "newskin"
}
try:
self.session.get(self.base_url, headers=self.http_headers)
self.session.post(self.base_url, data=login_payload, headers=self.http_headers)
if 'PHPSESSID' in self.session.cookies:
self.log("✅ 登录成功!")
return True
else:
self.log("❌ 登录失败:请检查账号密码")
return False
except Exception as e:
self.log(f"❌ 网络错误: {str(e)}")
return False
def get_timestamp(self):
return int(time.time() * 1000)
def clean_num(self, val):
"""将 1.0000 转换为 1保留必要的小数为空则返回空字符串"""
if val is None or val == "": return ""
try:
f_val = float(val)
if f_val.is_integer():
return str(int(f_val))
else:
return str(f_val)
except:
return str(val)
def _safe_float(self, val):
try:
return float(val)
except:
return 0.0
def fetch_product_details(self, record_id, contract_no, sales_person, outsourced_desc_from_html):
"""
修正后的价格逻辑:
1. 销售单价/销售总价 -> 永远不填 (代码中置空)
2. 厂家是"外购" -> 报价单价/总价置空,总金额填入"外购"
3. 厂家非"外购" -> 金额填入报价单价/总价,"外购"列置空
"""
detail_payload = {
"module": "Plugins", "pluginName": "DetailProductTable", "action": "getTableData",
"moduleName": "SalesOrder", "record": record_id, "actionId": self.get_timestamp(), "isTool": "1"
}
product_rows = []
try:
res = self.session.post(self.base_url, data=detail_payload, headers=self.http_headers)
try:
detail_json = res.json()
except:
return []
products = []
raw_data = detail_json.get('data')
if isinstance(raw_data, list):
products = raw_data
elif isinstance(raw_data, dict):
if 'rows' in raw_data:
products = raw_data['rows']
else:
for v in raw_data.values():
if isinstance(v, dict) and ('productid' in v or 'productname' in v):
products.append(v)
for prod in products:
# 1. 基础信息
manufacturer = self._get_nested_val(prod, 'cf_2128') or self._get_nested_val(prod, 'manufacturer')
prod_desc_text = prod.get('productname', '')
unit = self._get_nested_val(prod, 'usageunit')
qty_raw = self._get_nested_val(prod, 'qty')
discount = self.clean_num(self._get_nested_val(prod, 'discount_percent'))
currency = self._get_nested_val(prod, 'cf_534')
# 2. 价格获取
list_price_raw = self._get_nested_val(prod, 'listPrice')
f_qty = self._safe_float(qty_raw)
f_list_price = self._safe_float(list_price_raw)
f_total_val = f_list_price * f_qty # 计算总价
# 3. 判断外购逻辑
is_outsourced = False
if manufacturer and "外购" in manufacturer:
is_outsourced = True
# 4. 处理产品描述
final_desc = prod_desc_text
if is_outsourced and outsourced_desc_from_html:
final_desc = outsourced_desc_from_html
# 5. 分配金额到指定列
col_quote_unit = ""
col_quote_total = ""
col_sales_unit = ""
col_sales_total = ""
col_outsourced = ""
if is_outsourced:
# 外购:报价列为空,金额填入外购列(总价)
col_outsourced = self.clean_num(f_total_val)
else:
# 非外购:金额填入报价列,外购列为空
col_quote_unit = self.clean_num(f_list_price)
col_quote_total = self.clean_num(f_total_val)
# 构建行数据
row = {
"合同编号": contract_no,
"销售员": sales_person,
"厂家": manufacturer,
"货号": prod.get('productcode', ''),
"产品描述": final_desc,
"数量": self.clean_num(qty_raw),
"单位": unit,
"币种": currency,
"报价单价": col_quote_unit,
"报价总价": col_quote_total,
"销售单价": col_sales_unit, # 留空
"销售总价": col_sales_total, # 留空
"折扣率": discount,
"外购": col_outsourced,
# 预留列
"合同币种/美元": "",
"外购转美元": "",
"报价总价美元": "",
"净合同额美元": ""
}
product_rows.append(row)
except Exception:
pass
return product_rows
def _get_nested_val(self, item, key):
if not item or key not in item: return ""
val = item[key]
if isinstance(val, dict) and 'value' in val: return val['value']
return val
def fetch_detail_html(self, record_id):
try:
url = f"{self.base_url}?module=SalesOrder&action=DetailView&record={record_id}"
resp = self.session.get(url, headers=self.http_headers, timeout=10)
if resp.status_code == 200:
tree = html.fromstring(resp.content)
target = tree.xpath("/html/body/div[1]/div/div[2]/div[2]/form/div[1]/div[1]/div[2]")
if target:
import copy
el = copy.deepcopy(target[0])
for bad in el.xpath('.//script | .//style'): bad.drop_tree()
for br in el.xpath('.//br'): br.tail = "\n" + (br.tail if br.tail else "")
return "\n".join([line.strip() for line in el.text_content().splitlines() if line.strip()])
except Exception:
pass
return ""
def parse_data(self, text, cid):
if not text: return None
data = {
"系统ID": cid,
"合同编号": "", "内贸合同号": "", "外贸合同号": "",
"签署公司": "", "收款情况": "", "签订日期": "", "销售员": "",
"最终用户单位": "", "最终用户信息联系人": "", "最终用户信息电话": "", "最终用户信息邮箱": "",
"最终用户所在地": "",
"买方单位": "", "买方信息联系人": "", "买方信息电话": "", "买方信息邮箱": "",
"厂家": "", "厂家型号": "", "合同标的": "", "数量": "", "单位": "台/套",
"折扣率(%)": "", "合同额": "", "合同总额": "",
"外购付款方式": "", "最晚发货期": "", "已收款": "", "未收款": "", "收款日期": "",
"IS_ASD": False, "_temp_second_code": "",
"OUTSOURCED_DESC_HTML": "",
"product_list": []
}
lines = [line.strip() for line in text.split('\n') if line.strip()]
key_map = {
"收款账户": "签署公司", "收款状态": "收款情况", "签约日期": "签订日期",
"负责人": "销售员", "客户名称": "最终用户单位", "联系人姓名": "最终用户信息联系人",
"合同总额": "合同总额", "最新收款日期": "收款日期", "最晚发货期": "最晚发货期",
"付款比例及期限": "外购付款方式", "地址": "最终用户所在地", "厂家": "厂家",
"外购产品明细": "OUTSOURCED_DESC_HTML"
}
for i, line in enumerate(lines):
if line == "合同订单编号" and i + 1 < len(lines):
parts = lines[i + 1].strip().split()
if len(parts) >= 1: data["合同编号"] = parts[0]
if len(parts) >= 2: data["_temp_second_code"] = parts[1]
elif line in key_map and i + 1 < len(lines):
target = key_map[line]
if not data[target]: data[target] = lines[i + 1]
elif "合同标的" in line and "品名/型号" in line and i + 1 < len(lines):
parts = lines[i + 1].split('/')
if len(parts) >= 1: data["合同标的"] = parts[0]
if len(parts) >= 2: data["厂家型号"] = parts[1]
if len(parts) >= 3: data["数量"] = self.clean_num(parts[2])
if len(parts) >= 5: data["合同额"] = parts[4]
if not data["买方单位"]:
buyer_match = re.search(r"(?:买方|The Buyer)[:]\s*(.*?)(?:\n|$)", text)
if buyer_match and len(buyer_match.group(1)) > 1: data["买方单位"] = buyer_match.group(1).strip()
try:
total = float(data["合同总额"]) if data["合同总额"] else 0
if "已收" in data["收款情况"]:
data["已收款"] = self.clean_num(total);
data["未收款"] = "0"
elif "" in data["收款情况"]:
data["已收款"] = "0";
data["未收款"] = self.clean_num(total)
except:
pass
factory_val = data.get("厂家", "")
if factory_val and "ASD" in factory_val.upper():
data["IS_ASD"] = True
else:
data["IS_ASD"] = False
c_no = data.get("合同编号", "").strip().upper()
sec_code = data.pop("_temp_second_code", "")
if c_no.startswith('W'):
data["外贸合同号"] = sec_code
elif c_no.startswith('N'):
data["内贸合同号"] = sec_code
else:
data["内贸合同号"] = sec_code
if not c_no: return None
return data
def extract_time(self, text):
matches = re.findall(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})", text)
if matches:
dt_objects = [datetime.strptime(m, "%Y-%m-%d %H:%M:%S") for m in matches]
return max(dt_objects)
return None
def run_task(self, mode, **kwargs):
crmids = []
if mode == 'search':
query = kwargs.get('query')
self.log(f"🔍 正在搜索: {query}")
url = f"{self.base_url}?module=Home&action=UnifiedSearch&selectedmodule=undefined&query_string={urllib.parse.quote(query)}"
resp = self.session.get(url, headers=self.http_headers)
tree = html.fromstring(resp.content)
links = tree.xpath('//a[contains(@onclick, "record=")]')
for link in links:
match = re.search(r"record=(\d+)", link.get('onclick', ''))
if match: crmids.append(match.group(1))
crmids = list(set(crmids))
elif mode == 'date':
s_date = kwargs.get('start');
e_date = kwargs.get('end')
self.log(f"📅 时间筛选: {s_date} ~ {e_date}")
self._process_date_range(s_date, e_date)
return
self.log(f" 共找到 {len(crmids)} 条记录,开始解析详情...")
for i, cid in enumerate(crmids):
if self.stop_flag: break
self._process_single_id(cid)
self.log(f" 进度: {i + 1}/{len(crmids)}")
def _process_date_range(self, s_str, e_str):
try:
t_start = datetime.strptime(s_str, "%Y-%m-%d")
t_end = datetime.strptime(e_str, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
except:
self.log("❌ 日期格式错误");
return
page = 1
while not self.stop_flag:
ts = int(time.time() * 1000)
url = f"{self.base_url}?module=SalesOrder&action=SalesOrderAjax&file=ListViewData&sorder=DESC&order_by=modifiedtime&start={page}&pagesize=50&actionId={ts}&isFilter=true&search%5Bviewscope%5D=all_to_me&search%5Bviewname%5D=476"
try:
resp = self.session.get(url, headers=self.http_headers)
data = resp.json()
entries = data.get('data', []) or data.get('entries', [])
if not entries: break
page_ids = [x.get('crmid') or x.get('id') for x in entries if isinstance(x, dict)]
self.log(f" 🔎 正在检查第 {page} 页 ({len(page_ids)} 条)...")
valid_cnt = 0
for cid in page_ids:
if self.stop_flag: break
text_html = self.fetch_detail_html(cid)
r_time = self.extract_time(text_html)
if r_time:
if r_time > t_end: continue
if r_time < t_start:
self.log(f" 🛑 遇到旧数据 ({r_time}),停止爬取")
self.stop_flag = True;
break
self._process_data_payload(text_html, cid)
valid_cnt += 1
if valid_cnt > 0: self.log(f" ✅ 第 {page} 页入库 {valid_cnt}")
page += 1
if self.stop_flag: break
except Exception as e:
self.log(f"❌ 错误: {e}");
break
def _process_single_id(self, cid):
text_html = self.fetch_detail_html(cid)
self._process_data_payload(text_html, cid)
def _process_data_payload(self, text_html, cid):
parsed = self.parse_data(text_html, cid)
if parsed:
c_no = parsed.get("合同编号", "")
s_person = parsed.get("销售员", "")
outsourced_html_val = parsed.get("OUTSOURCED_DESC_HTML", "")
detail_rows = self.fetch_product_details(cid, c_no, s_person, outsourced_html_val)
parsed['product_list'] = detail_rows
self.on_data(parsed)
# ================= 3. 界面显示类 =================
class CRMGUI(ttk.Window):
def __init__(self):
super().__init__(themename="cosmo")
self.title("CRM 智能数据助手 测试版")
self.geometry("1400x900")
self.crawler = CRMCrawler(self.log_msg, self.add_record_to_table)
self.is_running = False
self.stored_data = {
'ASD': {'Domestic': [], 'Foreign': [], 'Other': []},
'NON_ASD': {'Domestic': [], 'Foreign': [], 'Other': []}
}
self.treeviews = {}
# 1. 主表字段
self.base_cols = [
"合同编号", "签署公司", "收款情况", "签订日期", "销售员", "厂家",
"最终用户单位", "最终用户信息联系人", "最终用户信息电话", "买方单位",
"厂家型号", "合同标的", "数量", "合同额", "合同总额",
"最晚发货期", "已收款", "未收款", "收款日期"
]
self.cols_domestic = ["内贸合同号"] + self.base_cols + ["系统ID"]
self.cols_foreign = ["外贸合同号"] + self.base_cols + ["系统ID"]
self.cols_other = self.base_cols + ["系统ID"]
# 2. 明细表字段
self.cols_detail = [
"合同编号", "销售员", "厂家", "货号", "产品描述",
"数量", "单位", "币种",
"报价单价", "报价总价", "销售单价", "销售总价", "折扣率", "外购",
"合同币种/美元", "外购转美元", "报价总价美元", "净合同额美元"
]
self.create_widgets()
def create_widgets(self):
# --- 1. 顶部控制 ---
control_frame = ttk.Frame(self, padding=10, bootstyle="light")
control_frame.pack(fill=X)
login_grp = ttk.Labelframe(control_frame, text="身份验证", padding=10)
login_grp.pack(side=LEFT, padx=5, fill=Y)
ttk.Label(login_grp, text="用户:").pack(side=LEFT)
self.user_ent = ttk.Entry(login_grp, width=10);
self.user_ent.insert(0, "TEST");
self.user_ent.pack(side=LEFT, padx=5)
ttk.Label(login_grp, text="密码:").pack(side=LEFT)
self.pass_ent = ttk.Entry(login_grp, width=10, show="*");
self.pass_ent.insert(0, "***");
self.pass_ent.pack(side=LEFT, padx=5)
mode_grp = ttk.Labelframe(control_frame, text="任务类型", padding=10)
mode_grp.pack(side=LEFT, padx=10, fill=Y, expand=True)
self.nb_mode = ttk.Notebook(mode_grp, bootstyle="primary")
self.nb_mode.pack(fill=BOTH, expand=True)
f_date = ttk.Frame(self.nb_mode, padding=10)
self.nb_mode.add(f_date, text="📅 按时间范围")
self.ent_start = ttk.Entry(f_date, width=12);
self.ent_start.insert(0, "2026-01-14");
self.ent_start.pack(side=LEFT, padx=5)
ttk.Label(f_date, text="").pack(side=LEFT)
self.ent_end = ttk.Entry(f_date, width=12);
self.ent_end.insert(0, "2026-01-15");
self.ent_end.pack(side=LEFT, padx=5)
f_search = ttk.Frame(self.nb_mode, padding=10)
self.nb_mode.add(f_search, text="🔍 关键词搜索")
self.ent_query = ttk.Entry(f_search, width=25);
self.ent_query.pack(fill=X)
self.nb_mode.select(f_date)
btn_grp = ttk.Frame(control_frame, padding=10)
btn_grp.pack(side=RIGHT, fill=Y)
self.btn_run = ttk.Button(btn_grp, text="▶ 开始", bootstyle="success", command=self.start_thread, width=10)
self.btn_run.pack(side=TOP, pady=2)
self.btn_stop = ttk.Button(btn_grp, text="⏹ 停止", bootstyle="danger", command=self.stop_task, state=DISABLED,
width=10)
self.btn_stop.pack(side=TOP, pady=2)
# --- 2. 核心展示区 ---
toggle_frame = ttk.Frame(self, padding=(10, 5))
toggle_frame.pack(fill=X)
self.curr_view = tk.StringVar(value="ASD")
self.btn_view_asd = ttk.Button(toggle_frame, text="ASD 产品列表", command=lambda: self.switch_view("ASD"),
width=20)
self.btn_view_asd.pack(side=LEFT, padx=5)
self.btn_view_non = ttk.Button(toggle_frame, text="非 ASD 产品列表",
command=lambda: self.switch_view("NON_ASD"), width=20)
self.btn_view_non.pack(side=LEFT, padx=5)
self.container = ttk.Frame(self)
self.container.pack(fill=BOTH, expand=True, padx=10)
self.frame_asd = ttk.Frame(self.container)
self.frame_non = ttk.Frame(self.container)
self._init_inner_tabs(self.frame_asd, "ASD")
self._init_inner_tabs(self.frame_non, "NON_ASD")
self.switch_view("ASD")
# --- 3. 底部区 ---
bottom_frame = ttk.Frame(self, padding=5)
bottom_frame.pack(fill=X, padx=10, pady=5)
log_frame = ttk.Labelframe(bottom_frame, text="系统日志", padding=5)
log_frame.pack(side=LEFT, fill=BOTH, expand=True)
self.txt_log = ScrolledText(log_frame, height=5);
self.txt_log.text.configure(state=DISABLED);
self.txt_log.pack(fill=BOTH, expand=True)
export_frame = ttk.Frame(bottom_frame, padding=10)
export_frame.pack(side=RIGHT, fill=Y)
ttk.Button(export_frame, text="📂 导出完整 Excel", bootstyle="primary", command=self.export_data).pack(fill=X,
pady=10)
def _init_inner_tabs(self, parent_frame, prefix):
nb = ttk.Notebook(parent_frame, bootstyle="info")
nb.pack(fill=BOTH, expand=True)
# 汇总 Tab
f_dom = ttk.Frame(nb);
nb.add(f_dom, text="📜 内贸汇总");
self._create_treeview(f_dom, self.cols_domestic, f"{prefix}_Domestic")
f_for = ttk.Frame(nb);
nb.add(f_for, text="📜 外贸汇总");
self._create_treeview(f_for, self.cols_foreign, f"{prefix}_Foreign")
f_oth = ttk.Frame(nb);
nb.add(f_oth, text="📜 其他汇总");
self._create_treeview(f_oth, self.cols_other, f"{prefix}_Other")
# 明细 Tab
f_detail_dom = ttk.Frame(nb);
nb.add(f_detail_dom, text="📦 内贸明细清单")
self._create_treeview(f_detail_dom, self.cols_detail, f"{prefix}_Detail_Domestic")
f_detail_for = ttk.Frame(nb);
nb.add(f_detail_for, text="📦 外贸明细清单")
self._create_treeview(f_detail_for, self.cols_detail, f"{prefix}_Detail_Foreign")
def _create_treeview(self, parent, cols, key):
sy = ttk.Scrollbar(parent, orient=VERTICAL)
sx = ttk.Scrollbar(parent, orient=HORIZONTAL)
tv = ttk.Treeview(parent, columns=cols, show="headings", selectmode="browse", yscrollcommand=sy.set,
xscrollcommand=sx.set)
sy.config(command=tv.yview);
sy.pack(side=RIGHT, fill=Y)
sx.config(command=tv.xview);
sx.pack(side=BOTTOM, fill=X)
tv.pack(side=LEFT, fill=BOTH, expand=True)
for c in cols:
# === 全居中设置 ===
tv.heading(c, text=c, anchor="center")
w = 100
if "描述" in c or "标的" in c or "公司" in c or "单位" in c:
w = 200
elif "编号" in c:
w = 120
elif "系统ID" in c:
w = 0
elif "" in c or "" in c or "外购" in c:
w = 80
tv.column(c, width=w, minwidth=50, anchor="center")
# 移除双击编辑,保留右键菜单(仅用于浏览器打开)
tv.bind("<Button-3>", lambda e: self.on_right_click(e, tv, key))
self.treeviews[key] = tv
return tv
def switch_view(self, view_name):
self.curr_view.set(view_name)
if view_name == "ASD":
self.frame_non.pack_forget();
self.frame_asd.pack(fill=BOTH, expand=True)
self.btn_view_asd.configure(bootstyle="primary")
self.btn_view_non.configure(bootstyle="secondary-outline")
else:
self.frame_asd.pack_forget();
self.frame_non.pack(fill=BOTH, expand=True)
self.btn_view_asd.configure(bootstyle="secondary-outline")
self.btn_view_non.configure(bootstyle="primary")
def start_thread(self):
if self.is_running: return
self.stored_data = {'ASD': {'Domestic': [], 'Foreign': [], 'Other': []},
'NON_ASD': {'Domestic': [], 'Foreign': [], 'Other': []}}
for tv in self.treeviews.values():
for item in tv.get_children(): tv.delete(item)
self.is_running = True
self.crawler.stop_flag = False
self.btn_run.config(state=DISABLED);
self.btn_stop.config(state=NORMAL)
t = threading.Thread(target=self._worker);
t.daemon = True;
t.start()
def stop_task(self):
self.crawler.stop_flag = True
self.log_msg("🛑 正在停止...")
def _worker(self):
user = self.user_ent.get();
pwd = self.pass_ent.get()
if not self.crawler.login(user, pwd): self._reset_ui(); return
curr_idx = self.nb_mode.index(self.nb_mode.select())
mode = "date";
kwargs = {}
if curr_idx == 0:
mode = "date"
kwargs = {'start': self.ent_start.get(), 'end': self.ent_end.get()}
elif curr_idx == 1:
mode = "search"
kwargs = {'query': self.ent_query.get()}
try:
self.crawler.run_task(mode, **kwargs); self.log_msg("🎉 完成!")
except Exception as e:
self.log_msg(f"❌ 错误: {e}")
finally:
self._reset_ui()
def _reset_ui(self):
self.is_running = False
self.after(0, lambda: self.btn_run.config(state=NORMAL))
self.after(0, lambda: self.btn_stop.config(state=DISABLED))
def log_msg(self, msg):
self.after(0, lambda: self._append_log(msg))
def _append_log(self, msg):
self.txt_log.text.configure(state=NORMAL)
self.txt_log.text.insert(END, f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
self.txt_log.text.see(END);
self.txt_log.text.configure(state=DISABLED)
def add_record_to_table(self, record):
def _update():
main_key = 'ASD' if record['IS_ASD'] else 'NON_ASD'
c_no = str(record.get("合同编号", "")).strip().upper()
sub_key = "Other"
if c_no.startswith('N'):
sub_key = "Domestic"
elif c_no.startswith('W'):
sub_key = "Foreign"
self.stored_data[main_key][sub_key].append(record)
record_idx = len(self.stored_data[main_key][sub_key]) - 1
# 主表
tv_key = f"{main_key}_{sub_key}"
tv = self.treeviews.get(tv_key)
if tv:
cols = list(tv['columns'])
vals = [record.get(c, "") for c in cols]
tv.insert("", END, iid=f"main_{main_key}_{sub_key}_{record_idx}", values=vals)
# 明细表
detail_key_suffix = ""
if sub_key == "Domestic":
detail_key_suffix = "Domestic"
elif sub_key == "Foreign":
detail_key_suffix = "Foreign"
if detail_key_suffix:
tv_detail_key = f"{main_key}_Detail_{detail_key_suffix}"
tv_detail = self.treeviews.get(tv_detail_key)
if tv_detail and record.get('product_list'):
detail_cols = list(tv_detail['columns'])
for p_idx, prod_row in enumerate(record['product_list']):
d_vals = [prod_row.get(c, "") for c in detail_cols]
unique_id = f"detail_{main_key}_{sub_key}_{record_idx}_{p_idx}"
tv_detail.insert("", END, iid=unique_id, values=d_vals)
self.after(0, _update)
def on_right_click(self, event, tv, key):
item_id = tv.identify_row(event.y)
if not item_id: return
tv.selection_set(item_id)
# 仅在主表行点击时提供浏览器打开功能
if item_id.startswith("main_"):
parts = item_id.split('_')
main_key, sub_key, idx = parts[1], parts[2], int(parts[3])
record = self.stored_data[main_key][sub_key][idx]
crm_id = record.get("系统ID", "")
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="🌐 在浏览器查看", command=lambda: self.open_browser(crm_id))
menu.post(event.x_root, event.y_root)
def open_browser(self, crm_id):
if crm_id:
url = f"http://111.198.24.44:88/index.php?module=SalesOrder&action=DetailView&record={crm_id}"
webbrowser.open(url)
# --- 导出功能 ---
def export_data(self):
folder = filedialog.askdirectory()
if not folder: return
self.log_msg(f"💾 正在导出...")
ts = time.strftime("%Y%m%d_%H%M%S")
export_cols = [
"合同编号", "签署公司", "收款情况", "签订日期", "销售员", "厂家",
"最终用户单位", "最终用户信息联系人", "最终用户信息电话", "最终用户信息邮箱", "最终用户所在地",
"买方单位", "买方信息联系人", "买方信息电话", "买方信息邮箱",
"厂家型号", "合同标的", "数量", "单位", "折扣率(%)", "合同额", "合同总额",
"外购付款方式", "最晚发货期", "已收款", "未收款", "收款日期"
]
detail_cols_order = self.cols_detail
for main_key, prefix in [('ASD', 'ASD_产品表'), ('NON_ASD', 'Non_ASD_产品表')]:
data_map = self.stored_data[main_key]
total = sum(len(v) for v in data_map.values())
if total == 0: continue
detail_domestic_rows = []
detail_foreign_rows = []
for sub_key in data_map:
for rec in data_map[sub_key]:
products = rec.get('product_list', [])
contract_no = rec.get('合同编号', '').upper()
if contract_no.startswith('W'):
detail_foreign_rows.extend(products)
else:
detail_domestic_rows.extend(products)
path = os.path.join(folder, f"{prefix}_{ts}.xlsx")
try:
with pd.ExcelWriter(path, engine='openpyxl') as writer:
if data_map['Domestic']:
df = pd.DataFrame(data_map['Domestic'])
for c in export_cols:
if c not in df.columns: df[c] = ""
cols = export_cols[:2] + ["内贸合同号"] + export_cols[2:]
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='内贸汇总', index=False)
if data_map['Foreign']:
df = pd.DataFrame(data_map['Foreign'])
for c in export_cols:
if c not in df.columns: df[c] = ""
cols = export_cols[:2] + ["外贸合同号"] + export_cols[2:]
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='外贸汇总', index=False)
if data_map['Other']:
df = pd.DataFrame(data_map['Other'])
for c in export_cols:
if c not in df.columns: df[c] = ""
cols = export_cols[:2] + ["内贸合同号"] + export_cols[2:]
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='其他汇总', index=False)
if detail_domestic_rows:
df_d = pd.DataFrame(detail_domestic_rows)
df_d = df_d.reindex(columns=detail_cols_order)
df_d.to_excel(writer, sheet_name='内贸明细', index=False)
if detail_foreign_rows:
df_f = pd.DataFrame(detail_foreign_rows)
df_f = df_f.reindex(columns=detail_cols_order)
df_f.to_excel(writer, sheet_name='外贸明细', index=False)
self.log_msg(f" ✅ 导出成功: {os.path.basename(path)}")
except Exception as e:
self.log_msg(f" ❌ 导出失败: {e}")
Messagebox.show_info("导出完成", "Excel文件已生成")
if __name__ == "__main__":
app = CRMGUI()
app.mainloop()