From eb8e1221fee0324cf06ce894282a9e765e0f2866 Mon Sep 17 00:00:00 2001 From: DXC Date: Mon, 19 Jan 2026 10:46:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- new_页面内容.py | 758 ++++++++++++++++++++++++++++++++++++++++++++ 商品明细.py | 221 +++++++++++++ 拿取内容.py | 2 +- 3 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 new_页面内容.py create mode 100644 商品明细.py diff --git a/new_页面内容.py b/new_页面内容.py new file mode 100644 index 0000000..a7e7da5 --- /dev/null +++ b/new_页面内容.py @@ -0,0 +1,758 @@ +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("", 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() \ No newline at end of file diff --git a/商品明细.py b/商品明细.py new file mode 100644 index 0000000..262b6e7 --- /dev/null +++ b/商品明细.py @@ -0,0 +1,221 @@ +import requests +import json +import time +import os +import pandas as pd +import re + +# ================= 1. 配置区域 ================= +base_url = "http://111.198.24.44:88/index.php" + +# 登录信息 +login_payload = { + "module": "Users", + "action": "Authenticate", + "return_module": "Users", + "return_action": "Login", + "user_name": "TEST", # <--- 【请修改】这里填用户名 + "user_password": "****", # <--- 【请修改】这里填密码 + "login_theme": "newskin" +} + +# 列表查询参数 +list_payload = { + "module": "SalesOrder", + "action": "SalesOrderAjax", + "file": "ListViewData", + "sorder": "", + "start": "1", + "pagesize": "50", + "actionId": "", + "isFilter": "true", + "search[viewscope]": "all_to_me", + "search[viewname]": "324126", + # 筛选条件 + "filter[Fields0]": "subject", + "filter[Condition0]": "cts", + "filter[Srch_value0]": "W25A", + "filter[type0]": "text", + "filter[dateCondition1]": "prevfy", + "filter[Fields1]": "duedate", + "filter[Condition1]": "btwa", + "filter[Srch_value1]": "2025-01-01,2025-12-31", + "filter[type1]": "date", + "filter[Fields2]": "subject", + "filter[Condition2]": "dcts", + "filter[Srch_value2]": "取消", + "filter[type2]": "text", + "filter[search_cnt]": "3", + "filter[matchtype]": "all" +} + +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", + "Referer": "http://111.198.24.44:88/index.php?module=SalesOrder&action=index" +} + + +# ================= 2. 辅助工具 ================= + +def get_timestamp(): + return int(time.time() * 1000) + + +def extract_nested_value(item, key): + """提取 {'value': '...'} 结构的值""" + 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 clean_html(text): + """清洗HTML标签,只留纯文本""" + if not isinstance(text, str): return str(text) + text = re.sub(r'<[^>]+>', '', text) + return text.strip() + + +# ================= 3. 主程序 ================= +def main(): + session = requests.Session() + all_rows = [] + + try: + # --- 1. 登录 --- + print("1. 正在登录...") + session.post(base_url, data=login_payload, headers=headers) + + # --- 2. 获取列表 --- + print("2. 获取订单列表...") + list_payload['actionId'] = get_timestamp() + res = session.post(base_url, data=list_payload, headers=headers) + + raw_data = res.json() + orders = [] + + # 列表解析 + if isinstance(raw_data, list): + orders = raw_data + elif isinstance(raw_data, dict): + for k in ['entries', 'rows', 'data', 'records']: + if k in raw_data and isinstance(raw_data[k], list): + orders = raw_data[k] + break + if not orders: + for v in raw_data.values(): + if isinstance(v, list) and len(v) > 0: + orders = v + break + + print(f"✅ 找到 {len(orders)} 个订单,开始处理...") + + # --- 3. 逐个提取 --- + for i, order in enumerate(orders): + rid = order.get('crmid') or order.get('salesorderid') or order.get('id') + if not rid: continue + + # 列表页基本信息 + contract_no = clean_html(order.get('subject', '')) + salesperson = order.get('assigned_user_id', '') or order.get('smownerid', '') + + print(f" [{i + 1}/{len(orders)}] 提取: {contract_no}") + + # 请求产品详情 + detail_payload = { + "module": "Plugins", + "pluginName": "DetailProductTable", + "action": "getTableData", + "moduleName": "SalesOrder", + "record": rid, + "actionId": get_timestamp(), + "isTool": "1" + } + + try: + detail_res = session.post(base_url, data=detail_payload, headers=headers) + detail_json = detail_res.json() + + # 寻找产品列表 data + products = [] + raw_data_content = detail_json.get('data') + + if isinstance(raw_data_content, list): + products = raw_data_content + elif isinstance(raw_data_content, dict): + if 'rows' in raw_data_content: + products = raw_data_content['rows'] + else: + for v in raw_data_content.values(): + if isinstance(v, dict) and ('productid' in v or 'productname' in v): + products.append(v) + + if not products: + continue + + # --- 核心:严格按你要求的表头填充 --- + for prod in products: + row_data = { + # === 第一部分:确定的字段 === + "合同编号": contract_no, + "销售员": salesperson, + "厂家": prod.get('cf_2128', ''), # ASD + "货号": prod.get('productcode', ''), # 135636 + "产品描述": prod.get('productname', ''), # Full Range... + "数量": extract_nested_value(prod, 'qty'), + "单位": prod.get('usageunit', ''), # 通常字段,如果没有也没关系 + "币种": prod.get('cf_534', ''), # USD + "报价单价": extract_nested_value(prod, 'listPrice'), # 4022.20 + "报价总价": extract_nested_value(prod, 'subtotal'), # 4022.20 + + # === 第二部分:按照指示全部留空的字段 === + "销售单价": "", + "销售总价": "", + "折扣率": "", + "外购": "", + "合同币种/美元": "", + "外购转美元": "", + "报价总价美元": "", + "净合同额美元": "" + } + all_rows.append(row_data) + + except Exception as e: + print(f" ❌ 解析错误: {e}") + + time.sleep(0.1) + + # --- 4. 生成 Excel --- + if all_rows: + # 严格按照你的表头顺序定义 + strict_columns = [ + '合同编号', '销售员', '厂家', '货号', '产品描述', + '数量', '单位', '币种', '报价单价', '报价总价', + '销售单价', '销售总价', '折扣率', '外购', + '合同币种/美元', '外购转美元', '报价总价美元', '净合同额美元' + ] + + df = pd.DataFrame(all_rows) + + # 确保列存在 + for col in strict_columns: + if col not in df.columns: + df[col] = "" + + # 强制列顺序 + df = df[strict_columns] + + filename = "Strict_Format_Export.xlsx" + df.to_excel(filename, index=False) + print(f"\n✅ 表格生成成功!已严格留空指定列,保存至: {os.path.abspath(filename)}") + else: + print("\n❌ 未提取到数据。") + + except Exception as e: + print(f"❌ 程序错误: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/拿取内容.py b/拿取内容.py index 6d306f2..c22873b 100644 --- a/拿取内容.py +++ b/拿取内容.py @@ -15,7 +15,7 @@ login_payload = { "return_module": "Users", "return_action": "Login", "user_name": "TEST", # 在这里填入真实的用户名 - "user_password": "test", # 在这里填入真实的密码 + "user_password": "***", # 在这里填入真实的密码 "login_theme": "newskin" }