修复,同时满足于不同的商品标的以及外购明细
This commit is contained in:
566
页面.py
566
页面.py
@ -3,15 +3,16 @@ import tkinter as tk
|
|||||||
from tkinter import ttk, filedialog, messagebox, simpledialog
|
from tkinter import ttk, filedialog, messagebox, simpledialog
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 第一部分:业务逻辑核心 (保持不变)
|
# 第一部分:业务逻辑核心
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
class DataProcessor:
|
class DataProcessor:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 定义表头配置
|
# 1. 总表表头 (保持不变,严格去空格)
|
||||||
self.columns_general = [
|
self.columns_general = [
|
||||||
"合同编号", "签署公司", "外贸合同号", "收款情况", "合同签订日期",
|
"合同编号", "签署公司", "外贸合同号", "收款情况", "合同签订日期",
|
||||||
"销售员", "最终用户单位", "最终用户信息联系人、电话、邮箱", "最终用户所在地",
|
"销售员", "最终用户单位", "最终用户信息联系人、电话、邮箱", "最终用户所在地",
|
||||||
@ -20,41 +21,86 @@ class DataProcessor:
|
|||||||
"最晚发货期", "付款方式", "发货港", "目的港", "发货日期",
|
"最晚发货期", "付款方式", "发货港", "目的港", "发货日期",
|
||||||
"买方单位", "买方信息联系人、电话、邮箱", "收货人信息"
|
"买方单位", "买方信息联系人、电话、邮箱", "收货人信息"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 内贸总表表头
|
||||||
self.columns_domestic_general = [c if c != "外贸合同号" else "内贸合同号" for c in self.columns_general]
|
self.columns_domestic_general = [c if c != "外贸合同号" else "内贸合同号" for c in self.columns_general]
|
||||||
|
|
||||||
|
# 2. [关键修改] 明细表表头 (完全按照你的截图顺序和名称定义)
|
||||||
self.columns_detail = [
|
self.columns_detail = [
|
||||||
"合同编号", "销售员", "厂家", "合同标的", "货号", "产品描述", "数量", "单位",
|
"合同编号", "销售员", "合同标的", "厂家", "货号", "产品描述",
|
||||||
"币种", "报价单价", "报价总价", "销售单价", "销售总价", "折扣率",
|
"净合同额美元", "外购", "计算汇率", "外购转美元", "报价总价美元",
|
||||||
"外购", "合同币种/美元", "外购转美元", "报价总价美元", "净合同额美元"
|
"数量", "单位", "外币币种", "外币报价单价",
|
||||||
|
"报价RMB单价", "报价RMB总价", "售价RMB单价", "售价RMB总价", "折扣率(%)"
|
||||||
]
|
]
|
||||||
|
|
||||||
self.columns_other = [
|
# OM合同表头 (保持不变)
|
||||||
|
self.columns_om = [
|
||||||
"合同编号", "签署公司", "内贸合同号", "收款情况", "签订日期",
|
"合同编号", "签署公司", "内贸合同号", "收款情况", "签订日期",
|
||||||
"销售员", "最终用户单位", "最终用户信息联系人、电话、邮箱", "最终用户所在地",
|
"销售员", "最终用户单位", "最终用户信息联系人、电话、邮箱", "最终用户所在地",
|
||||||
"买方单位", "买方信息联系人、电话、邮箱", "合同标的", "合同总额",
|
"买方单位", "买方信息联系人、电话、邮箱", "合同标的", "合同总额",
|
||||||
"已收款", "未收款", "收款日期"
|
"已收款", "未收款", "收款日期"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# [修改] 定义需要保留两位小数的金额列 (根据新表头更新)
|
||||||
|
self.money_cols = set([
|
||||||
|
"合同", "总合同额", "外购", "已收款", "未收款",
|
||||||
|
"净合同额美元", "外购转美元", "报价总价美元",
|
||||||
|
"外币报价单价", "报价RMB单价", "报价RMB总价",
|
||||||
|
"售价RMB单价", "售价RMB总价", "外购产品金额"
|
||||||
|
])
|
||||||
|
|
||||||
|
# [修改] 定义需要百分比展示的列 (根据新表头更新)
|
||||||
|
self.percent_cols = set([
|
||||||
|
"折扣率", "折扣率(%)", "计算汇率", "合同币种/美元"
|
||||||
|
])
|
||||||
|
|
||||||
|
# 旧表头映射字典 (现在代码标准已更新为Excel标准,这个字典主要用于兼容总表的旧名称)
|
||||||
|
# 注意:明细表现在不需要映射了,因为 self.columns_detail 已经和 Excel 一样了
|
||||||
|
self.legacy_map = {
|
||||||
|
"外币币种": "币种", # 仅用于总表可能的兼容
|
||||||
|
"汇率": "计算汇率",
|
||||||
|
# 如果旧Excel里的总表还在用"折扣率(%)",映射回总表的"折扣率"
|
||||||
|
"折扣率(%)": "折扣率"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.source_cols_processed = []
|
||||||
|
|
||||||
def safe_float(self, val):
|
def safe_float(self, val):
|
||||||
try:
|
try:
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
val = val.replace(',', '').strip()
|
val = val.replace(',', '').replace('¥', '').replace('$', '').strip()
|
||||||
if val == '': return 0.0
|
if val == '': return 0.0
|
||||||
|
if pd.isna(val): return 0.0
|
||||||
return float(val)
|
return float(val)
|
||||||
except:
|
except:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def normalize_for_compare(self, val):
|
def format_money_str(self, val):
|
||||||
if pd.isna(val) or val is None:
|
if pd.isna(val) or str(val).strip() == "": return ""
|
||||||
return ""
|
|
||||||
s_val = str(val).strip()
|
|
||||||
if s_val.lower() == 'nan':
|
|
||||||
return ""
|
|
||||||
try:
|
try:
|
||||||
f_val = float(s_val)
|
f_val = self.safe_float(val)
|
||||||
if f_val.is_integer():
|
return "{:.2f}".format(f_val)
|
||||||
return str(int(f_val))
|
except:
|
||||||
return str(f_val)
|
return str(val)
|
||||||
|
|
||||||
|
def format_percent_str(self, val):
|
||||||
|
if pd.isna(val) or str(val).strip() == "": return ""
|
||||||
|
try:
|
||||||
|
s_val = str(val).strip()
|
||||||
|
if '%' in s_val: return s_val
|
||||||
|
f_val = self.safe_float(val)
|
||||||
|
return "{:.2f}%".format(f_val * 100)
|
||||||
|
except:
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
def normalize_for_compare(self, val):
|
||||||
|
if pd.isna(val) or val is None: return ""
|
||||||
|
s_val = str(val).strip()
|
||||||
|
if s_val.lower() == 'nan': return ""
|
||||||
|
clean_val = s_val.replace(',', '').replace('%', '')
|
||||||
|
try:
|
||||||
|
f_val = float(clean_val)
|
||||||
|
return "{:.4f}".format(f_val)
|
||||||
except:
|
except:
|
||||||
return s_val
|
return s_val
|
||||||
|
|
||||||
@ -75,27 +121,121 @@ class DataProcessor:
|
|||||||
|
|
||||||
col_factory_general = '厂家'
|
col_factory_general = '厂家'
|
||||||
col_factory_detail = '厂家.1' if '厂家.1' in df.columns else '厂家'
|
col_factory_detail = '厂家.1' if '厂家.1' in df.columns else '厂家'
|
||||||
|
|
||||||
df[col_factory_general] = df[col_factory_general].fillna('').astype(str)
|
df[col_factory_general] = df[col_factory_general].fillna('').astype(str)
|
||||||
df['合同类型'] = df['合同类型'].fillna('').astype(str)
|
df['合同类型'] = df['合同类型'].fillna('').astype(str)
|
||||||
|
|
||||||
return df, (col_factory_general, col_factory_detail)
|
return df, (col_factory_general, col_factory_detail)
|
||||||
|
|
||||||
|
def parse_complex_subject(self, text):
|
||||||
|
res = {'name': '', 'model': '', 'qty': '', 'unit': '', 'price': ''}
|
||||||
|
if not isinstance(text, str) or not text.strip(): return res
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
name_patterns = [r'(?:中文品名|中文名称|名称|Name)[::]\s*(.*?)(?:\n|$)', r'(?:英文名称)[::]\s*(.*?)(?:\n|$)']
|
||||||
|
for p in name_patterns:
|
||||||
|
m = re.search(p, text, re.IGNORECASE)
|
||||||
|
if m and not res['name']: res['name'] = m.group(1).strip()
|
||||||
|
|
||||||
|
model_patterns = [r'(?:型号|Model)[::]\s*(.*?)(?:\n|$)']
|
||||||
|
for p in model_patterns:
|
||||||
|
m = re.search(p, text, re.IGNORECASE)
|
||||||
|
if m: res['model'] = m.group(1).strip()
|
||||||
|
|
||||||
|
brand_match = re.search(r'(?:品牌|Brand)[::]\s*(.*?)(?:\n|$)', text, re.IGNORECASE)
|
||||||
|
if brand_match:
|
||||||
|
brand_str = brand_match.group(1).strip()
|
||||||
|
if res['model']:
|
||||||
|
res['model'] = f"{brand_str} {res['model']}"
|
||||||
|
else:
|
||||||
|
res['model'] = brand_str
|
||||||
|
|
||||||
|
clean_text = text
|
||||||
|
for k in ['中文品名', '中文名称', '英文名称', '名称', '型号', 'Model', '品牌', 'Brand']:
|
||||||
|
clean_text = re.sub(f'{k}.*?(?:\n|$)', '', clean_text, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
if not res['name'] and '/' in text:
|
||||||
|
parts = text.split('/')
|
||||||
|
if len(parts) > 0: res['name'] = parts[0].strip()
|
||||||
|
|
||||||
|
if not res['qty']:
|
||||||
|
qty_slash = re.search(r'/(\d+(\.\d+)?)/', text)
|
||||||
|
if qty_slash:
|
||||||
|
res['qty'] = qty_slash.group(1)
|
||||||
|
else:
|
||||||
|
qty_unit_match = re.search(r'(\d+)\s*([台个套件支箱组setpc]+)|([setpc]+)\s*(\d+)', text, re.IGNORECASE)
|
||||||
|
if qty_unit_match:
|
||||||
|
if qty_unit_match.group(1):
|
||||||
|
res['qty'] = qty_unit_match.group(1)
|
||||||
|
res['unit'] = qty_unit_match.group(2)
|
||||||
|
else:
|
||||||
|
res['qty'] = qty_unit_match.group(4)
|
||||||
|
res['unit'] = qty_unit_match.group(3)
|
||||||
|
|
||||||
|
nums = re.findall(r'\d+(?:\.\d+)?', text.replace(',', '').replace(',', ''))
|
||||||
|
if nums:
|
||||||
|
candidate = nums[-1]
|
||||||
|
if candidate != res['qty']: res['price'] = candidate
|
||||||
|
|
||||||
|
if not res['name'] and not res['model'] and '/' in text:
|
||||||
|
parts = text.split('/')
|
||||||
|
if len(parts) >= 1: res['name'] = parts[0]
|
||||||
|
if len(parts) >= 2: res['model'] = parts[1]
|
||||||
|
if len(parts) >= 3: res['qty'] = parts[2]
|
||||||
|
if len(parts) >= 4: res['price'] = parts[3]
|
||||||
|
return res
|
||||||
|
|
||||||
|
def parse_buyer_info(self, text):
|
||||||
|
info = {'name': '', 'contact_full': ''}
|
||||||
|
if not isinstance(text, str) or not text.strip(): return info
|
||||||
|
lines = [l.strip() for l in text.split('\n') if l.strip()]
|
||||||
|
if not lines: return info
|
||||||
|
info['name'] = lines[0]
|
||||||
|
info['contact_full'] = " ".join(lines[1:])
|
||||||
|
return info
|
||||||
|
|
||||||
def process_row_general(self, row, trade_type, col_factory):
|
def process_row_general(self, row, trade_type, col_factory):
|
||||||
target_cols = self.columns_general if trade_type == '外贸' else self.columns_domestic_general
|
target_cols = self.columns_general if trade_type == '外贸' else self.columns_domestic_general
|
||||||
new_row = {col: "" for col in target_cols}
|
new_row = {col: "" for col in target_cols}
|
||||||
order_no_raw = str(row.get('合同订单编号', ''))
|
|
||||||
parts_no = order_no_raw.split(' ')
|
order_no_raw = str(row.get('合同订单编号', '')).strip()
|
||||||
|
parts_no = order_no_raw.split()
|
||||||
new_row['合同编号'] = parts_no[0] if len(parts_no) > 0 else order_no_raw
|
new_row['合同编号'] = parts_no[0] if len(parts_no) > 0 else order_no_raw
|
||||||
contract_no_col = '外贸合同号' if trade_type == '外贸' else '内贸合同号'
|
contract_no_col = '外贸合同号' if trade_type == '外贸' else '内贸合同号'
|
||||||
new_row[contract_no_col] = parts_no[1] if len(parts_no) > 1 else ""
|
new_row[contract_no_col] = " ".join(parts_no[1:]) if len(parts_no) > 1 else ""
|
||||||
|
|
||||||
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
||||||
parts_target = target_raw.split('/')
|
parsed_target = self.parse_complex_subject(target_raw)
|
||||||
if len(parts_target) >= 1: new_row['合同标的'] = parts_target[0]
|
|
||||||
if len(parts_target) >= 2: new_row['型号/货号'] = parts_target[1]
|
new_row['合同标的'] = parsed_target['name']
|
||||||
if len(parts_target) >= 3: new_row['数量'] = parts_target[2]
|
new_row['型号/货号'] = parsed_target['model']
|
||||||
if len(parts_target) >= 4: new_row['合同'] = parts_target[3]
|
new_row['数量'] = parsed_target['qty']
|
||||||
new_row['总合同额'] = row.get('合同总额', '')
|
new_row['单位'] = parsed_target['unit']
|
||||||
|
new_row['合同'] = parsed_target['price']
|
||||||
|
|
||||||
|
if trade_type == '内贸':
|
||||||
|
buyer_raw = str(row.get('合同买方(名称/联系人/电话/邮箱)', ''))
|
||||||
|
else:
|
||||||
|
buyer_raw = str(row.get('进口代理(名称/USCI/地址/联系人/电话/邮箱)', ''))
|
||||||
|
if buyer_raw == '' or buyer_raw == 'nan':
|
||||||
|
buyer_raw = str(row.get('合同买方(名称/联系人/电话/邮箱)', ''))
|
||||||
|
parsed_buyer = self.parse_buyer_info(buyer_raw)
|
||||||
|
new_row['买方单位'] = parsed_buyer['name']
|
||||||
|
new_row['买方信息联系人、电话、邮箱'] = parsed_buyer['contact_full']
|
||||||
|
new_row['收货人信息'] = parsed_buyer['name']
|
||||||
|
|
||||||
|
total_amount = row.get('合同总额', '')
|
||||||
|
status = str(row.get('收款状态', '')).strip()
|
||||||
|
new_row['总合同额'] = total_amount
|
||||||
|
new_row['收款情况'] = status
|
||||||
|
if '已收' in status:
|
||||||
|
new_row['已收款'] = total_amount
|
||||||
|
new_row['未收款'] = 0
|
||||||
|
else:
|
||||||
|
new_row['已收款'] = ""
|
||||||
|
new_row['未收款'] = ""
|
||||||
|
|
||||||
new_row['签署公司'] = row.get('收款账户', '')
|
new_row['签署公司'] = row.get('收款账户', '')
|
||||||
new_row['收款情况'] = row.get('收款状态', '')
|
|
||||||
new_row['合同签订日期'] = row.get('签约日期', '')
|
new_row['合同签订日期'] = row.get('签约日期', '')
|
||||||
new_row['销售员'] = row.get('负责人', '')
|
new_row['销售员'] = row.get('负责人', '')
|
||||||
new_row['最终用户单位'] = row.get('客户名称', '')
|
new_row['最终用户单位'] = row.get('客户名称', '')
|
||||||
@ -108,70 +248,108 @@ class DataProcessor:
|
|||||||
new_row['付款方式'] = row.get('付款比例及期限', '')
|
new_row['付款方式'] = row.get('付款比例及期限', '')
|
||||||
new_row['发货港'] = row.get('发货地', '')
|
new_row['发货港'] = row.get('发货地', '')
|
||||||
new_row['目的港'] = row.get('目的港', '')
|
new_row['目的港'] = row.get('目的港', '')
|
||||||
new_row['买方单位'] = row.get('合同买方(名称/联系人/电话/邮箱)', '')
|
new_row['折扣率'] = row.get('折扣率', '')
|
||||||
|
|
||||||
return pd.Series(new_row)
|
return pd.Series(new_row)
|
||||||
|
|
||||||
|
# [关键修改] 明细表处理逻辑更新,匹配新表头
|
||||||
def process_row_detail(self, row, col_factory):
|
def process_row_detail(self, row, col_factory):
|
||||||
new_row = {col: "" for col in self.columns_detail}
|
new_row = {col: "" for col in self.columns_detail}
|
||||||
detail_manuf_val = str(row.get(col_factory, ''))
|
detail_manuf_val = str(row.get(col_factory, ''))
|
||||||
order_no_raw = str(row.get('合同订单编号', ''))
|
|
||||||
new_row['合同编号'] = order_no_raw.split(' ')[0] if order_no_raw else ""
|
order_no_raw = str(row.get('合同订单编号', '')).strip()
|
||||||
|
parts_no = order_no_raw.split()
|
||||||
|
new_row['合同编号'] = parts_no[0] if len(parts_no) > 0 else order_no_raw
|
||||||
new_row['销售员'] = row.get('负责人', '')
|
new_row['销售员'] = row.get('负责人', '')
|
||||||
new_row['厂家'] = detail_manuf_val
|
new_row['厂家'] = detail_manuf_val
|
||||||
new_row['货号'] = row.get('产品编码', '')
|
new_row['货号'] = row.get('产品编码', '')
|
||||||
new_row['数量'] = row.get('数量', '')
|
|
||||||
new_row['币种'] = row.get('原币种', '')
|
# 币种 -> 外币币种
|
||||||
new_row['单位'] = ""
|
new_row['外币币种'] = row.get('原币种', '')
|
||||||
new_row['折扣率'] = ""
|
|
||||||
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
||||||
parts_target = target_raw.split('/')
|
parsed_target = self.parse_complex_subject(target_raw)
|
||||||
new_row['合同标的'] = parts_target[0] if len(parts_target) >= 1 else ""
|
new_row['合同标的'] = parsed_target['name']
|
||||||
|
|
||||||
|
csv_qty = str(row.get('数量', '')).strip()
|
||||||
|
if csv_qty and csv_qty.lower() != 'nan':
|
||||||
|
new_row['数量'] = csv_qty
|
||||||
|
else:
|
||||||
|
new_row['数量'] = parsed_target['qty']
|
||||||
|
new_row['单位'] = parsed_target['unit']
|
||||||
|
|
||||||
val_product_subtotal = self.safe_float(row.get('产品小计', 0))
|
val_product_subtotal = self.safe_float(row.get('产品小计', 0))
|
||||||
|
|
||||||
if '外购' in detail_manuf_val:
|
if '外购' in detail_manuf_val:
|
||||||
new_row['外购'] = val_product_subtotal
|
new_row['外购'] = val_product_subtotal
|
||||||
new_row['产品描述'] = row.get('备注', '')
|
remark = str(row.get('备注', '')).strip()
|
||||||
new_row['报价单价'] = ""
|
if not remark or remark.lower() == 'nan':
|
||||||
new_row['报价总价'] = ""
|
outsourced_detail = str(row.get('外购产品明细', '')).strip()
|
||||||
new_row['销售单价'] = ""
|
if outsourced_detail and outsourced_detail.lower() != 'nan':
|
||||||
new_row['销售总价'] = ""
|
new_row['产品描述'] = outsourced_detail
|
||||||
|
else:
|
||||||
|
new_row['产品描述'] = ""
|
||||||
|
else:
|
||||||
|
new_row['产品描述'] = remark
|
||||||
else:
|
else:
|
||||||
new_row['外购'] = ""
|
new_row['外购'] = ""
|
||||||
new_row['产品描述'] = row.get('产品名称', '')
|
new_row['产品描述'] = row.get('产品名称', '')
|
||||||
new_row['报价单价'] = row.get('美元报价', '')
|
# 美元报价 -> 外币报价单价
|
||||||
new_row['报价总价'] = row.get('产品小计', '')
|
new_row['外币报价单价'] = row.get('美元报价', '')
|
||||||
new_row['销售单价'] = ""
|
# 产品小计 -> 报价RMB总价 (假设逻辑)
|
||||||
new_row['销售总价'] = ""
|
new_row['报价RMB总价'] = row.get('产品小计', '')
|
||||||
|
|
||||||
|
new_row['计算汇率'] = row.get('汇率', '')
|
||||||
|
new_row['折扣率(%)'] = row.get('折扣率', '')
|
||||||
|
new_row['售价RMB单价'] = row.get('销售单价', '')
|
||||||
|
new_row['售价RMB总价'] = row.get('销售总价', '')
|
||||||
|
new_row['外购转美元'] = row.get('外购转美元', '')
|
||||||
|
new_row['报价总价美元'] = row.get('报价总价美元', '')
|
||||||
|
new_row['净合同额美元'] = row.get('净合同额美元', '')
|
||||||
|
new_row['报价RMB单价'] = row.get('报价RMB单价', '') # 如果CSV有这一列,如果没有则为空
|
||||||
|
|
||||||
new_row['合同币种/美元'] = row.get('汇率', '')
|
|
||||||
new_row['外购转美元'] = ""
|
|
||||||
new_row['报价总价美元'] = ""
|
|
||||||
new_row['净合同额美元'] = ""
|
|
||||||
return pd.Series(new_row)
|
return pd.Series(new_row)
|
||||||
|
|
||||||
def process_row_other(self, row):
|
def process_row_om(self, row):
|
||||||
new_row = {col: "" for col in self.columns_other}
|
new_row = {col: "" for col in self.columns_om}
|
||||||
order_no_raw = str(row.get('合同订单编号', ''))
|
order_no_raw = str(row.get('合同订单编号', '')).strip()
|
||||||
parts_no = order_no_raw.split(' ')
|
parts_no = order_no_raw.split()
|
||||||
new_row['合同编号'] = parts_no[0] if len(parts_no) > 0 else order_no_raw
|
new_row['合同编号'] = parts_no[0] if len(parts_no) > 0 else order_no_raw
|
||||||
new_row['内贸合同号'] = parts_no[1] if len(parts_no) > 1 else ""
|
if len(parts_no) > 1: new_row['内贸合同号'] = " ".join(parts_no[1:])
|
||||||
|
|
||||||
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
target_raw = str(row.get('合同标的(品名/型号/数量/单价/总价)', ''))
|
||||||
parts_target = target_raw.split('/')
|
parsed_target = self.parse_complex_subject(target_raw)
|
||||||
if len(parts_target) >= 1: new_row['合同标的'] = parts_target[0]
|
new_row['合同标的'] = parsed_target['name']
|
||||||
new_row['合同总额'] = row.get('合同总额', '')
|
|
||||||
|
total_amount = row.get('合同总额', '')
|
||||||
|
status = str(row.get('收款状态', '')).strip()
|
||||||
|
new_row['合同总额'] = total_amount
|
||||||
|
new_row['收款情况'] = status
|
||||||
|
if '已收' in status:
|
||||||
|
new_row['已收款'] = total_amount
|
||||||
|
new_row['未收款'] = 0
|
||||||
|
else:
|
||||||
|
new_row['已收款'] = ""
|
||||||
|
new_row['未收款'] = ""
|
||||||
|
|
||||||
new_row['签署公司'] = row.get('收款账户', '')
|
new_row['签署公司'] = row.get('收款账户', '')
|
||||||
new_row['收款情况'] = row.get('收款状态', '')
|
|
||||||
new_row['签订日期'] = row.get('签约日期', '')
|
new_row['签订日期'] = row.get('签约日期', '')
|
||||||
new_row['销售员'] = row.get('负责人', '')
|
new_row['销售员'] = row.get('负责人', '')
|
||||||
new_row['最终用户单位'] = row.get('客户名称', '')
|
new_row['最终用户单位'] = row.get('客户名称', '')
|
||||||
new_row['最终用户信息联系人、电话、邮箱'] = row.get('联系人姓名', '')
|
new_row['最终用户信息联系人、电话、邮箱'] = row.get('联系人姓名', '')
|
||||||
new_row['买方单位'] = row.get('合同买方(名称/联系人/电话/邮箱)', '')
|
|
||||||
|
buyer_raw = str(row.get('合同买方(名称/联系人/电话/邮箱)', ''))
|
||||||
|
parsed_buyer = self.parse_buyer_info(buyer_raw)
|
||||||
|
new_row['买方单位'] = parsed_buyer['name']
|
||||||
|
new_row['买方信息联系人、电话、邮箱'] = parsed_buyer['contact_full']
|
||||||
new_row['收款日期'] = row.get('最新收款日期', '')
|
new_row['收款日期'] = row.get('最新收款日期', '')
|
||||||
|
|
||||||
return pd.Series(new_row)
|
return pd.Series(new_row)
|
||||||
|
|
||||||
def merge_datasets(self, old_dfs, csv_df, is_asd):
|
def merge_datasets(self, old_dfs, csv_df, is_asd):
|
||||||
col_gen = '厂家'
|
col_gen = '厂家'
|
||||||
col_det = '厂家.1' if '厂家.1' in csv_df.columns else '厂家'
|
col_det = '厂家.1' if '厂家.1' in csv_df.columns else '厂家'
|
||||||
|
|
||||||
if is_asd:
|
if is_asd:
|
||||||
df_subset = csv_df[csv_df[col_gen].str.contains('ASD', case=False, na=False)]
|
df_subset = csv_df[csv_df[col_gen].str.contains('ASD', case=False, na=False)]
|
||||||
else:
|
else:
|
||||||
@ -179,31 +357,28 @@ class DataProcessor:
|
|||||||
|
|
||||||
csv_foreign = df_subset[df_subset['合同类型'] == '外贸'].copy()
|
csv_foreign = df_subset[df_subset['合同类型'] == '外贸'].copy()
|
||||||
csv_domestic = df_subset[df_subset['合同类型'] == '内贸'].copy()
|
csv_domestic = df_subset[df_subset['合同类型'] == '内贸'].copy()
|
||||||
csv_other = df_subset[~df_subset['合同类型'].isin(['外贸', '内贸'])].copy()
|
csv_om = df_subset[~df_subset['合同类型'].isin(['外贸', '内贸'])].copy()
|
||||||
|
|
||||||
result_dfs = {}
|
result_dfs = {}
|
||||||
|
|
||||||
def is_row_different(old_row, new_row, columns):
|
def merge_logic(old_df, new_rows_df, unique_col, target_columns):
|
||||||
for col in columns:
|
|
||||||
if col == '_status': continue
|
|
||||||
v1 = old_row.get(col)
|
|
||||||
v2 = new_row.get(col)
|
|
||||||
if self.normalize_for_compare(v1) != self.normalize_for_compare(v2):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def merge_logic(old_df, new_rows_df, unique_col, sheet_type='general'):
|
|
||||||
if old_df is None or old_df.empty:
|
if old_df is None or old_df.empty:
|
||||||
if new_rows_df.empty: return pd.DataFrame()
|
if new_rows_df.empty: return pd.DataFrame(columns=target_columns + ['_status'])
|
||||||
combined = new_rows_df.copy()
|
combined = new_rows_df.copy()
|
||||||
combined['_status'] = 'new'
|
combined['_status'] = 'new'
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
combined = old_df.copy()
|
combined = old_df.copy()
|
||||||
# 明细表填充逻辑修复
|
|
||||||
if sheet_type == 'detail' and '合同标的' in combined.columns and '合同编号' in combined.columns:
|
# 确保旧数据列名存在
|
||||||
combined['合同标的'] = combined['合同标的'].replace(r'^\s*$', np.nan, regex=True)
|
for col in target_columns:
|
||||||
combined['合同标的'] = combined.groupby('合同编号')['合同标的'].ffill()
|
if col not in combined.columns:
|
||||||
combined['合同标的'] = combined['合同标的'].fillna("")
|
combined[col] = ""
|
||||||
|
|
||||||
|
if unique_col in combined.columns:
|
||||||
|
combined[unique_col] = combined[unique_col].astype(str)
|
||||||
|
if unique_col in new_rows_df.columns:
|
||||||
|
new_rows_df[unique_col] = new_rows_df[unique_col].astype(str)
|
||||||
|
|
||||||
if '_status' not in combined.columns:
|
if '_status' not in combined.columns:
|
||||||
combined['_status'] = ''
|
combined['_status'] = ''
|
||||||
@ -212,140 +387,129 @@ class DataProcessor:
|
|||||||
return combined
|
return combined
|
||||||
|
|
||||||
new_contract_ids = new_rows_df[unique_col].unique()
|
new_contract_ids = new_rows_df[unique_col].unique()
|
||||||
|
rows_to_append = []
|
||||||
|
|
||||||
for cid in new_contract_ids:
|
for cid in new_contract_ids:
|
||||||
new_subset = new_rows_df[new_rows_df[unique_col] == cid].copy()
|
new_subset = new_rows_df[new_rows_df[unique_col] == cid]
|
||||||
old_indices = combined[combined[unique_col] == cid].index
|
old_indices = combined[combined[unique_col] == cid].index
|
||||||
|
|
||||||
if len(old_indices) > 0:
|
if len(old_indices) > 0:
|
||||||
old_subset = combined.loc[old_indices]
|
idx = old_indices[0]
|
||||||
has_changed = False
|
has_changed = False
|
||||||
if len(old_subset) != len(new_subset):
|
new_row_series = new_subset.iloc[0]
|
||||||
|
|
||||||
|
for col in target_columns:
|
||||||
|
if col in new_row_series:
|
||||||
|
new_val = new_row_series[col]
|
||||||
|
old_val = combined.at[idx, col]
|
||||||
|
|
||||||
|
# 保护逻辑:新值非空才覆盖
|
||||||
|
if str(new_val).strip() != "":
|
||||||
|
if self.normalize_for_compare(old_val) != self.normalize_for_compare(new_val):
|
||||||
|
combined.at[idx, col] = new_val
|
||||||
has_changed = True
|
has_changed = True
|
||||||
else:
|
|
||||||
old_comp = old_subset.reset_index(drop=True)
|
|
||||||
new_comp = new_subset.reset_index(drop=True)
|
|
||||||
cols = [c for c in new_subset.columns if c != '_status']
|
|
||||||
for i in range(len(old_comp)):
|
|
||||||
if is_row_different(old_comp.iloc[i], new_comp.iloc[i], cols):
|
|
||||||
has_changed = True
|
|
||||||
break
|
|
||||||
if has_changed:
|
if has_changed:
|
||||||
combined.drop(old_indices, inplace=True)
|
combined.at[idx, '_status'] = 'modified'
|
||||||
new_subset['_status'] = 'modified'
|
|
||||||
combined = pd.concat([combined, new_subset], ignore_index=True)
|
|
||||||
else:
|
else:
|
||||||
combined.drop(old_indices, inplace=True)
|
new_subset_copy = new_subset.copy()
|
||||||
new_subset['_status'] = ''
|
new_subset_copy['_status'] = 'new'
|
||||||
combined = pd.concat([combined, new_subset], ignore_index=True)
|
rows_to_append.append(new_subset_copy)
|
||||||
else:
|
|
||||||
new_subset['_status'] = 'new'
|
if rows_to_append:
|
||||||
combined = pd.concat([combined, new_subset], ignore_index=True)
|
combined = pd.concat([combined] + rows_to_append, ignore_index=True)
|
||||||
|
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
|
# --- 合并执行 ---
|
||||||
if not csv_foreign.empty:
|
if not csv_foreign.empty:
|
||||||
new_gen = csv_foreign.apply(lambda r: self.process_row_general(r, '外贸', col_gen), axis=1)
|
new_gen = csv_foreign.apply(lambda r: self.process_row_general(r, '外贸', col_gen), axis=1)
|
||||||
new_gen = new_gen.drop_duplicates(subset=['合同编号'], keep='first')
|
new_gen = new_gen.drop_duplicates(subset=['合同编号'], keep='first')
|
||||||
else:
|
else:
|
||||||
new_gen = pd.DataFrame(columns=self.columns_general)
|
new_gen = pd.DataFrame(columns=self.columns_general)
|
||||||
old_gen = old_dfs.get('外贸总表', pd.DataFrame(columns=self.columns_general))
|
old_gen = old_dfs.get('外贸', old_dfs.get('外贸总表', pd.DataFrame(columns=self.columns_general)))
|
||||||
result_dfs['外贸总表'] = merge_logic(old_gen, new_gen, '合同编号', 'general')
|
result_dfs['外贸'] = merge_logic(old_gen, new_gen, '合同编号', self.columns_general)
|
||||||
|
|
||||||
if not csv_foreign.empty:
|
if not csv_foreign.empty:
|
||||||
new_det = csv_foreign.apply(lambda r: self.process_row_detail(r, col_det), axis=1)
|
new_det = csv_foreign.apply(lambda r: self.process_row_detail(r, col_det), axis=1)
|
||||||
else:
|
else:
|
||||||
new_det = pd.DataFrame(columns=self.columns_detail)
|
new_det = pd.DataFrame(columns=self.columns_detail)
|
||||||
old_det = old_dfs.get('外贸明细', pd.DataFrame(columns=self.columns_detail))
|
old_det = old_dfs.get('外贸明细', pd.DataFrame(columns=self.columns_detail))
|
||||||
result_dfs['外贸明细'] = merge_logic(old_det, new_det, '合同编号', 'detail')
|
result_dfs['外贸明细'] = merge_logic(old_det, new_det, '合同编号', self.columns_detail)
|
||||||
|
|
||||||
if not csv_domestic.empty:
|
if not csv_domestic.empty:
|
||||||
new_dom_gen = csv_domestic.apply(lambda r: self.process_row_general(r, '内贸', col_gen), axis=1)
|
new_dom_gen = csv_domestic.apply(lambda r: self.process_row_general(r, '内贸', col_gen), axis=1)
|
||||||
new_dom_gen = new_dom_gen.drop_duplicates(subset=['合同编号'], keep='first')
|
new_dom_gen = new_dom_gen.drop_duplicates(subset=['合同编号'], keep='first')
|
||||||
else:
|
else:
|
||||||
new_dom_gen = pd.DataFrame(columns=self.columns_domestic_general)
|
new_dom_gen = pd.DataFrame(columns=self.columns_domestic_general)
|
||||||
old_dom_gen = old_dfs.get('内贸总表', pd.DataFrame(columns=self.columns_domestic_general))
|
old_dom_gen = old_dfs.get('内贸', old_dfs.get('内贸总表', pd.DataFrame(columns=self.columns_domestic_general)))
|
||||||
result_dfs['内贸总表'] = merge_logic(old_dom_gen, new_dom_gen, '合同编号', 'general')
|
result_dfs['内贸'] = merge_logic(old_dom_gen, new_dom_gen, '合同编号', self.columns_domestic_general)
|
||||||
|
|
||||||
if not csv_domestic.empty:
|
if not csv_domestic.empty:
|
||||||
new_dom_det = csv_domestic.apply(lambda r: self.process_row_detail(r, col_det), axis=1)
|
new_dom_det = csv_domestic.apply(lambda r: self.process_row_detail(r, col_det), axis=1)
|
||||||
else:
|
else:
|
||||||
new_dom_det = pd.DataFrame(columns=self.columns_detail)
|
new_dom_det = pd.DataFrame(columns=self.columns_detail)
|
||||||
old_dom_det = old_dfs.get('内贸明细', pd.DataFrame(columns=self.columns_detail))
|
old_dom_det = old_dfs.get('内贸明细', pd.DataFrame(columns=self.columns_detail))
|
||||||
result_dfs['内贸明细'] = merge_logic(old_dom_det, new_dom_det, '合同编号', 'detail')
|
result_dfs['内贸明细'] = merge_logic(old_dom_det, new_dom_det, '合同编号', self.columns_detail)
|
||||||
|
|
||||||
if not csv_other.empty:
|
if not csv_om.empty:
|
||||||
new_other = csv_other.apply(lambda r: self.process_row_other(r), axis=1)
|
new_om = csv_om.apply(lambda r: self.process_row_om(r), axis=1)
|
||||||
new_other = new_other.drop_duplicates(subset=['合同编号'], keep='first')
|
new_om = new_om.drop_duplicates(subset=['合同编号'], keep='first')
|
||||||
else:
|
else:
|
||||||
new_other = pd.DataFrame(columns=self.columns_other)
|
new_om = pd.DataFrame(columns=self.columns_om)
|
||||||
old_other = old_dfs.get('其他', pd.DataFrame(columns=self.columns_other))
|
old_om = old_dfs.get('OM合同', old_dfs.get('其他', pd.DataFrame(columns=self.columns_om)))
|
||||||
result_dfs['其他'] = merge_logic(old_other, new_other, '合同编号', 'general')
|
result_dfs['OM合同'] = merge_logic(old_om, new_om, '合同编号', self.columns_om)
|
||||||
|
|
||||||
return result_dfs
|
return result_dfs
|
||||||
|
|
||||||
|
def apply_formatting_to_all(self, data_dict):
|
||||||
|
for sheet_name, df in data_dict.items():
|
||||||
|
if df.empty: continue
|
||||||
|
for col in self.money_cols:
|
||||||
|
if col in df.columns:
|
||||||
|
df[col] = df[col].apply(self.format_money_str)
|
||||||
|
for col in self.percent_cols:
|
||||||
|
if col in df.columns:
|
||||||
|
df[col] = df[col].apply(self.format_percent_str)
|
||||||
|
return data_dict
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 第二部分:GUI 界面 (美化版)
|
# 第二部分:GUI 界面
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
class ContractApp:
|
class ContractApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("合同数据处理系统 V2.0")
|
self.root.title("合同数据处理系统 V3.2 (表头修正版)")
|
||||||
self.root.geometry("1300x850")
|
self.root.geometry("1300x850")
|
||||||
|
|
||||||
# === 样式配置 ===
|
|
||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.style.theme_use('clam') # 使用 clam 主题作为基础,更易定制
|
self.style.theme_use('clam')
|
||||||
|
self.colors = {'bg': '#F5F6FA', 'primary': '#409EFF', 'success': '#67C23A', 'warning': '#E6A23C',
|
||||||
# 颜色定义
|
'text': '#2C3E50', 'panel': '#FFFFFF'}
|
||||||
self.colors = {
|
|
||||||
'bg': '#F5F6FA', # 整体背景灰白
|
|
||||||
'primary': '#409EFF', # 主色蓝
|
|
||||||
'success': '#67C23A', # 成功绿
|
|
||||||
'warning': '#E6A23C', # 警告黄
|
|
||||||
'text': '#2C3E50', # 文字深灰
|
|
||||||
'panel': '#FFFFFF' # 面板白
|
|
||||||
}
|
|
||||||
|
|
||||||
self.root.configure(bg=self.colors['bg'])
|
self.root.configure(bg=self.colors['bg'])
|
||||||
|
|
||||||
# 配置字体和通用控件样式
|
|
||||||
self.default_font = ("微软雅黑", 10)
|
self.default_font = ("微软雅黑", 10)
|
||||||
self.header_font = ("微软雅黑", 11, "bold")
|
self.header_font = ("微软雅黑", 11, "bold")
|
||||||
|
|
||||||
self.style.configure("TFrame", background=self.colors['bg'])
|
self.style.configure("TFrame", background=self.colors['bg'])
|
||||||
self.style.configure("Panel.TFrame", background=self.colors['panel'], relief="flat")
|
self.style.configure("Panel.TFrame", background=self.colors['panel'], relief="flat")
|
||||||
self.style.configure("TLabel", background=self.colors['panel'], foreground=self.colors['text'],
|
self.style.configure("TLabel", background=self.colors['panel'], foreground=self.colors['text'],
|
||||||
font=self.default_font)
|
font=self.default_font)
|
||||||
self.style.configure("Header.TLabel", font=("微软雅黑", 16, "bold"), background=self.colors['bg'],
|
self.style.configure("Header.TLabel", font=("微软雅黑", 16, "bold"), background=self.colors['bg'],
|
||||||
foreground=self.colors['text'])
|
foreground=self.colors['text'])
|
||||||
|
|
||||||
# 按钮样式
|
|
||||||
self.style.configure("TButton", font=("微软雅黑", 10), borderwidth=0, padding=6)
|
self.style.configure("TButton", font=("微软雅黑", 10), borderwidth=0, padding=6)
|
||||||
self.style.map("TButton", background=[('active', '#E0E0E0')])
|
self.style.map("TButton", background=[('active', '#E0E0E0')])
|
||||||
|
|
||||||
# 主要按钮 (Primary)
|
|
||||||
self.style.configure("Primary.TButton", background=self.colors['primary'], foreground='white')
|
self.style.configure("Primary.TButton", background=self.colors['primary'], foreground='white')
|
||||||
self.style.map("Primary.TButton", background=[('active', '#66B1FF')])
|
self.style.map("Primary.TButton", background=[('active', '#66B1FF')])
|
||||||
|
|
||||||
# 成功按钮 (Success)
|
|
||||||
self.style.configure("Success.TButton", background=self.colors['success'], foreground='white')
|
self.style.configure("Success.TButton", background=self.colors['success'], foreground='white')
|
||||||
self.style.map("Success.TButton", background=[('active', '#85CE61')])
|
self.style.map("Success.TButton", background=[('active', '#85CE61')])
|
||||||
|
self.style.configure("Treeview", background="white", foreground="black", fieldbackground="white", rowheight=28,
|
||||||
# 表格样式 (Treeview)
|
|
||||||
self.style.configure("Treeview",
|
|
||||||
background="white",
|
|
||||||
foreground="black",
|
|
||||||
fieldbackground="white",
|
|
||||||
rowheight=28, # 增加行高
|
|
||||||
font=("微软雅黑", 9))
|
font=("微软雅黑", 9))
|
||||||
self.style.configure("Treeview.Heading",
|
self.style.configure("Treeview.Heading", font=("微软雅黑", 10, "bold"), background="#EBEEF5",
|
||||||
font=("微软雅黑", 10, "bold"),
|
|
||||||
background="#EBEEF5",
|
|
||||||
foreground="#606266")
|
foreground="#606266")
|
||||||
self.style.map("Treeview", background=[('selected', '#409EFF')])
|
self.style.map("Treeview", background=[('selected', '#409EFF')])
|
||||||
|
|
||||||
# 逻辑处理器
|
|
||||||
self.processor = DataProcessor()
|
self.processor = DataProcessor()
|
||||||
self.csv_path = tk.StringVar()
|
self.csv_path = tk.StringVar()
|
||||||
self.asd_path = tk.StringVar()
|
self.asd_path = tk.StringVar()
|
||||||
@ -355,49 +519,34 @@ class ContractApp:
|
|||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
# --- 顶部标题 ---
|
|
||||||
header_frame = ttk.Frame(self.root)
|
header_frame = ttk.Frame(self.root)
|
||||||
header_frame.pack(fill="x", padx=20, pady=(20, 10))
|
header_frame.pack(fill="x", padx=20, pady=(20, 10))
|
||||||
ttk.Label(header_frame, text="📄 合同数据智能合并与处理工具", style="Header.TLabel").pack(side="left")
|
ttk.Label(header_frame, text="📄 合同数据处理工具 (支持 OM合同)", style="Header.TLabel").pack(side="left")
|
||||||
|
|
||||||
# --- 文件选择区 (卡片式) ---
|
|
||||||
input_panel = ttk.Frame(self.root, style="Panel.TFrame", padding=20)
|
input_panel = ttk.Frame(self.root, style="Panel.TFrame", padding=20)
|
||||||
input_panel.pack(fill="x", padx=20, pady=5)
|
input_panel.pack(fill="x", padx=20, pady=5)
|
||||||
|
|
||||||
# 标题提示
|
|
||||||
ttk.Label(input_panel, text="文件配置 (若未选择旧文件,将自动生成新文件)", font=self.header_font).grid(row=0,
|
ttk.Label(input_panel, text="文件配置 (若未选择旧文件,将自动生成新文件)", font=self.header_font).grid(row=0,
|
||||||
column=0,
|
column=0,
|
||||||
columnspan=3,
|
columnspan=3,
|
||||||
sticky="w",
|
sticky="w",
|
||||||
pady=(0,
|
pady=(0,
|
||||||
15))
|
15))
|
||||||
|
|
||||||
self.create_file_row(input_panel, "📂 导入 CSV 源文件:", self.csv_path, 1)
|
self.create_file_row(input_panel, "📂 导入 CSV 源文件:", self.csv_path, 1)
|
||||||
self.create_file_row(input_panel, "📘 旧 ASD Excel 文件:", self.asd_path, 2)
|
self.create_file_row(input_panel, "📘 旧 ASD Excel 文件:", self.asd_path, 2)
|
||||||
self.create_file_row(input_panel, "📗 旧 非ASD Excel 文件:", self.non_asd_path, 3)
|
self.create_file_row(input_panel, "📗 旧 非ASD Excel 文件:", self.non_asd_path, 3)
|
||||||
|
|
||||||
# 处理按钮
|
|
||||||
btn_frame = ttk.Frame(input_panel, style="Panel.TFrame")
|
btn_frame = ttk.Frame(input_panel, style="Panel.TFrame")
|
||||||
btn_frame.grid(row=4, column=0, columnspan=3, pady=(15, 0), sticky="e")
|
btn_frame.grid(row=4, column=0, columnspan=3, pady=(15, 0), sticky="e")
|
||||||
|
|
||||||
ttk.Button(btn_frame, text="▶ 开始处理并预览", style="Primary.TButton", command=self.process_files).pack(
|
ttk.Button(btn_frame, text="▶ 开始处理并预览", style="Primary.TButton", command=self.process_files).pack(
|
||||||
side="right")
|
side="right")
|
||||||
|
|
||||||
# --- 数据展示区 ---
|
|
||||||
self.notebook = ttk.Notebook(self.root)
|
self.notebook = ttk.Notebook(self.root)
|
||||||
self.notebook.pack(fill="both", expand=True, padx=20, pady=10)
|
self.notebook.pack(fill="both", expand=True, padx=20, pady=10)
|
||||||
|
|
||||||
# --- 底部操作栏 ---
|
|
||||||
bottom_bar = ttk.Frame(self.root, style="Panel.TFrame", padding=15)
|
bottom_bar = ttk.Frame(self.root, style="Panel.TFrame", padding=15)
|
||||||
bottom_bar.pack(fill="x", padx=20, pady=(0, 20))
|
bottom_bar.pack(fill="x", padx=20, pady=(0, 20))
|
||||||
|
|
||||||
# 图例
|
|
||||||
legend_frame = ttk.Frame(bottom_bar, style="Panel.TFrame")
|
legend_frame = ttk.Frame(bottom_bar, style="Panel.TFrame")
|
||||||
legend_frame.pack(side="left")
|
legend_frame.pack(side="left")
|
||||||
self.create_legend(legend_frame, "■ 新增数据", "#FFFFCC", "black")
|
self.create_legend(legend_frame, "■ 新增数据", "#FFFFCC", "black")
|
||||||
self.create_legend(legend_frame, "■ 有修改/变动", "#ECF5FF", "#409EFF")
|
self.create_legend(legend_frame, "■ 有修改/变动", "#ECF5FF", "#409EFF")
|
||||||
self.create_legend(legend_frame, "□ 无变动", "white", "black")
|
self.create_legend(legend_frame, "□ 无变动", "white", "black")
|
||||||
|
|
||||||
ttk.Button(bottom_bar, text="💾 保存更改至 Excel", style="Success.TButton", command=self.save_files).pack(
|
ttk.Button(bottom_bar, text="💾 保存更改至 Excel", style="Success.TButton", command=self.save_files).pack(
|
||||||
side="right")
|
side="right")
|
||||||
|
|
||||||
@ -417,28 +566,49 @@ class ContractApp:
|
|||||||
f = filedialog.askopenfilename(filetypes=[("Excel/CSV Files", "*.csv;*.xlsx")])
|
f = filedialog.askopenfilename(filetypes=[("Excel/CSV Files", "*.csv;*.xlsx")])
|
||||||
if f: variable.set(f)
|
if f: variable.set(f)
|
||||||
|
|
||||||
|
def load_excel_safe(self, path):
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
dfs = pd.read_excel(path, sheet_name=None)
|
||||||
|
clean_dfs = {}
|
||||||
|
for k, v in dfs.items():
|
||||||
|
v.columns = v.columns.astype(str).str.replace(r'\s+', '', regex=True)
|
||||||
|
|
||||||
|
# 总表仍可能需要 legacy_map,但明细表不需要了因为我们已经在代码里统一了列名
|
||||||
|
v.rename(columns=self.processor.legacy_map, inplace=True)
|
||||||
|
v = v.loc[:, ~v.columns.duplicated()]
|
||||||
|
|
||||||
|
if '合同编号' in v.columns:
|
||||||
|
v['合同编号'] = v['合同编号'].astype(str)
|
||||||
|
|
||||||
|
clean_dfs[k.strip()] = v
|
||||||
|
return clean_dfs
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showwarning("读取错误", f"读取旧文件失败: {path}\n错误: {str(e)}")
|
||||||
|
return {}
|
||||||
|
|
||||||
def process_files(self):
|
def process_files(self):
|
||||||
if not self.csv_path.get():
|
if not self.csv_path.get():
|
||||||
messagebox.showerror("提示", "请先选择 CSV 源文件!")
|
messagebox.showerror("提示", "请先选择 CSV 源文件!")
|
||||||
return
|
return
|
||||||
|
|
||||||
csv_df, headers = self.processor.load_csv(self.csv_path.get())
|
csv_df, headers = self.processor.load_csv(self.csv_path.get())
|
||||||
if csv_df is None:
|
if csv_df is None:
|
||||||
messagebox.showerror("错误", headers)
|
messagebox.showerror("错误", headers)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.final_data = {}
|
self.final_data = {}
|
||||||
|
|
||||||
# ASD 处理
|
|
||||||
path_asd = self.asd_path.get()
|
path_asd = self.asd_path.get()
|
||||||
asd_old = pd.read_excel(path_asd, sheet_name=None) if path_asd and os.path.exists(path_asd) else {}
|
asd_old = self.load_excel_safe(path_asd)
|
||||||
self.final_data['ASD'] = self.processor.merge_datasets(asd_old, csv_df, True)
|
self.final_data['ASD'] = self.processor.merge_datasets(asd_old, csv_df, True)
|
||||||
|
|
||||||
# 非ASD 处理
|
|
||||||
path_non = self.non_asd_path.get()
|
path_non = self.non_asd_path.get()
|
||||||
non_old = pd.read_excel(path_non, sheet_name=None) if path_non and os.path.exists(path_non) else {}
|
non_old = self.load_excel_safe(path_non)
|
||||||
self.final_data['NonASD'] = self.processor.merge_datasets(non_old, csv_df, False)
|
self.final_data['NonASD'] = self.processor.merge_datasets(non_old, csv_df, False)
|
||||||
|
|
||||||
|
self.final_data['ASD'] = self.processor.apply_formatting_to_all(self.final_data['ASD'])
|
||||||
|
self.final_data['NonASD'] = self.processor.apply_formatting_to_all(self.final_data['NonASD'])
|
||||||
|
|
||||||
self.refresh_preview()
|
self.refresh_preview()
|
||||||
messagebox.showinfo("完成", "数据处理完成!\n请查看预览,确认无误后点击下方保存。")
|
messagebox.showinfo("完成", "数据处理完成!\n请查看预览,确认无误后点击下方保存。")
|
||||||
|
|
||||||
@ -452,54 +622,75 @@ class ContractApp:
|
|||||||
data_dict = self.final_data[file_type]
|
data_dict = self.final_data[file_type]
|
||||||
main_frame = ttk.Frame(self.notebook, style="Panel.TFrame")
|
main_frame = ttk.Frame(self.notebook, style="Panel.TFrame")
|
||||||
self.notebook.add(main_frame, text=f" {file_type} 文件预览 ")
|
self.notebook.add(main_frame, text=f" {file_type} 文件预览 ")
|
||||||
|
|
||||||
inner_notebook = ttk.Notebook(main_frame)
|
inner_notebook = ttk.Notebook(main_frame)
|
||||||
inner_notebook.pack(fill="both", expand=True, padx=5, pady=5)
|
inner_notebook.pack(fill="both", expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
sheet_order = ['外贸总表', '外贸明细', '内贸总表', '内贸明细', '其他']
|
sheet_order = ['外贸', '外贸明细', '内贸', '内贸明细', 'OM合同']
|
||||||
for sheet_name in sheet_order:
|
for sheet_name in sheet_order:
|
||||||
if sheet_name in data_dict:
|
if sheet_name in data_dict:
|
||||||
df = data_dict[sheet_name]
|
df = data_dict[sheet_name]
|
||||||
if not df.empty and '合同编号' in df.columns:
|
if not df.empty:
|
||||||
|
if '合同编号' in df.columns:
|
||||||
|
df['合同编号'] = df['合同编号'].astype(str)
|
||||||
df = df.sort_values(by='合同编号', ascending=True)
|
df = df.sort_values(by='合同编号', ascending=True)
|
||||||
if '明细' in sheet_name:
|
if '明细' in sheet_name:
|
||||||
mask = df.duplicated(subset=['合同编号'], keep='first')
|
mask = df.duplicated(subset=['合同编号'], keep='first')
|
||||||
df.loc[mask, '合同标的'] = ""
|
df.loc[mask, '合同标的'] = ""
|
||||||
self.create_treeview(inner_notebook, df, sheet_name)
|
|
||||||
|
|
||||||
def create_treeview(self, parent, df, title):
|
standard_cols = []
|
||||||
|
if sheet_name == '外贸':
|
||||||
|
standard_cols = self.processor.columns_general
|
||||||
|
elif sheet_name == '内贸':
|
||||||
|
standard_cols = self.processor.columns_domestic_general
|
||||||
|
elif sheet_name == 'OM合同':
|
||||||
|
standard_cols = self.processor.columns_om
|
||||||
|
elif '明细' in sheet_name:
|
||||||
|
standard_cols = self.processor.columns_detail
|
||||||
|
|
||||||
|
self.create_treeview(inner_notebook, df, sheet_name, standard_cols)
|
||||||
|
|
||||||
|
def create_treeview(self, parent, df, title, target_cols):
|
||||||
frame = ttk.Frame(parent)
|
frame = ttk.Frame(parent)
|
||||||
parent.add(frame, text=title)
|
parent.add(frame, text=title)
|
||||||
|
|
||||||
# 滚动条容器
|
|
||||||
scroll_y = ttk.Scrollbar(frame, orient="vertical")
|
scroll_y = ttk.Scrollbar(frame, orient="vertical")
|
||||||
scroll_x = ttk.Scrollbar(frame, orient="horizontal")
|
scroll_x = ttk.Scrollbar(frame, orient="horizontal")
|
||||||
|
|
||||||
display_cols = [c for c in df.columns if c != '_status']
|
display_cols = target_cols
|
||||||
|
|
||||||
tree = ttk.Treeview(frame, columns=display_cols, show='headings',
|
tree = ttk.Treeview(frame, columns=display_cols, show='headings',
|
||||||
yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
|
yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
|
||||||
|
|
||||||
scroll_y.config(command=tree.yview)
|
scroll_y.config(command=tree.yview)
|
||||||
scroll_x.config(command=tree.xview)
|
scroll_x.config(command=tree.xview)
|
||||||
|
|
||||||
scroll_y.pack(side="right", fill="y")
|
scroll_y.pack(side="right", fill="y")
|
||||||
scroll_x.pack(side="bottom", fill="x")
|
scroll_x.pack(side="bottom", fill="x")
|
||||||
tree.pack(fill="both", expand=True)
|
tree.pack(fill="both", expand=True)
|
||||||
|
|
||||||
for col in display_cols:
|
for col in display_cols:
|
||||||
tree.heading(col, text=col)
|
tree.heading(col, text=col)
|
||||||
tree.column(col, width=130, anchor="center") # 居中对齐
|
tree.column(col, width=120, anchor="center")
|
||||||
|
|
||||||
# 颜色标签
|
tree.tag_configure('new', background='#FFFFCC')
|
||||||
tree.tag_configure('new', background='#FFFFCC') # 浅黄
|
|
||||||
# 使用淡蓝色标记有修改的行
|
|
||||||
tree.tag_configure('modified', background='#ECF5FF', foreground='#409EFF')
|
tree.tag_configure('modified', background='#ECF5FF', foreground='#409EFF')
|
||||||
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df_display = df.fillna("")
|
df_display = df.fillna("")
|
||||||
|
last_contract_id = None
|
||||||
|
|
||||||
for idx, row in df_display.iterrows():
|
for idx, row in df_display.iterrows():
|
||||||
values = [row[c] for c in display_cols]
|
values = []
|
||||||
|
for col in display_cols:
|
||||||
|
val = row.get(col, "")
|
||||||
|
if '明细' in title and col == '合同标的':
|
||||||
|
current_id = row.get('合同编号', '')
|
||||||
|
if current_id == last_contract_id:
|
||||||
|
val = ""
|
||||||
|
values.append(val)
|
||||||
|
|
||||||
|
if '明细' in title:
|
||||||
|
last_contract_id = row.get('合同编号', '')
|
||||||
|
|
||||||
status = row.get('_status', '')
|
status = row.get('_status', '')
|
||||||
tree.insert("", "end", values=values, tags=(status,))
|
tree.insert("", "end", values=values, tags=(status,))
|
||||||
|
|
||||||
@ -513,7 +704,6 @@ class ContractApp:
|
|||||||
col_idx = int(column.replace('#', '')) - 1
|
col_idx = int(column.replace('#', '')) - 1
|
||||||
col_name = tree['columns'][col_idx]
|
col_name = tree['columns'][col_idx]
|
||||||
current_val = tree.item(row_id, "values")[col_idx]
|
current_val = tree.item(row_id, "values")[col_idx]
|
||||||
|
|
||||||
new_val = simpledialog.askstring("快速编辑", f"修改 [{col_name}]:", initialvalue=current_val, parent=self.root)
|
new_val = simpledialog.askstring("快速编辑", f"修改 [{col_name}]:", initialvalue=current_val, parent=self.root)
|
||||||
if new_val is not None:
|
if new_val is not None:
|
||||||
current_values = list(tree.item(row_id, "values"))
|
current_values = list(tree.item(row_id, "values"))
|
||||||
@ -521,11 +711,8 @@ class ContractApp:
|
|||||||
tree.item(row_id, values=current_values)
|
tree.item(row_id, values=current_values)
|
||||||
|
|
||||||
def save_files(self):
|
def save_files(self):
|
||||||
if not self.final_data:
|
if not self.final_data: return
|
||||||
return
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(self.csv_path.get()) if self.csv_path.get() else ""
|
base_dir = os.path.dirname(self.csv_path.get()) if self.csv_path.get() else ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for file_type, sheets in self.final_data.items():
|
for file_type, sheets in self.final_data.items():
|
||||||
target_path = ""
|
target_path = ""
|
||||||
@ -537,17 +724,22 @@ class ContractApp:
|
|||||||
if not target_path: target_path = os.path.join(base_dir, "NonASD_Combined.xlsx")
|
if not target_path: target_path = os.path.join(base_dir, "NonASD_Combined.xlsx")
|
||||||
|
|
||||||
with pd.ExcelWriter(target_path, engine='openpyxl') as writer:
|
with pd.ExcelWriter(target_path, engine='openpyxl') as writer:
|
||||||
for sheet_name, df in sheets.items():
|
valid_sheets = ['外贸', '外贸明细', '内贸', '内贸明细', 'OM合同']
|
||||||
|
for sheet_name in valid_sheets:
|
||||||
|
if sheet_name in sheets:
|
||||||
|
df = sheets[sheet_name]
|
||||||
save_df = df.drop(columns=['_status'], errors='ignore')
|
save_df = df.drop(columns=['_status'], errors='ignore')
|
||||||
if not save_df.empty and '合同编号' in save_df.columns:
|
if not save_df.empty:
|
||||||
|
if '合同编号' in save_df.columns:
|
||||||
|
save_df['合同编号'] = save_df['合同编号'].astype(str)
|
||||||
save_df = save_df.sort_values(by='合同编号', ascending=True)
|
save_df = save_df.sort_values(by='合同编号', ascending=True)
|
||||||
if '明细' in sheet_name:
|
if '明细' in sheet_name:
|
||||||
mask = save_df.duplicated(subset=['合同编号'], keep='first')
|
mask = save_df.duplicated(subset=['合同编号'], keep='first')
|
||||||
save_df.loc[mask, '合同标的'] = ""
|
save_df.loc[mask, '合同标的'] = ""
|
||||||
save_df.to_excel(writer, sheet_name=sheet_name, index=False)
|
save_df.to_excel(writer, sheet_name=sheet_name, index=False)
|
||||||
|
|
||||||
messagebox.showinfo("成功", f"文件保存成功!\n位置: {base_dir or '当前目录'}")
|
messagebox.showinfo("成功", f"文件保存成功!\n位置: {base_dir or '当前目录'}")
|
||||||
|
except PermissionError:
|
||||||
|
messagebox.showerror("保存失败", "文件被占用!\n请先关闭 Excel 文件后再点击保存。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("保存失败", str(e))
|
messagebox.showerror("保存失败", str(e))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user