测试版
This commit is contained in:
758
new_页面内容.py
Normal file
758
new_页面内容.py
Normal file
@ -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("<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()
|
||||||
221
商品明细.py
Normal file
221
商品明细.py
Normal file
@ -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()
|
||||||
2
拿取内容.py
2
拿取内容.py
@ -15,7 +15,7 @@ login_payload = {
|
|||||||
"return_module": "Users",
|
"return_module": "Users",
|
||||||
"return_action": "Login",
|
"return_action": "Login",
|
||||||
"user_name": "TEST", # 在这里填入真实的用户名
|
"user_name": "TEST", # 在这里填入真实的用户名
|
||||||
"user_password": "test", # 在这里填入真实的密码
|
"user_password": "***", # 在这里填入真实的密码
|
||||||
"login_theme": "newskin"
|
"login_theme": "newskin"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user