你提出的问题非常关键,也是当前系统最核心的矛盾:
> ❗ **同一个 `CustomerRef`(如 CUST-123)被多个 `material_code` 使用时,签收记录会重复显示在所有匹配项中**
这导致:
- 统计失真
- 消耗量虚高
- 用户无法判断哪条才是真正属于自己的
---
## ✅ 你的最终需求已明确:
> 🎯 **必须将“客户料号 + 内部料号”作为一个唯一组合来统计签收**
>
> 即使数据库里没有这个字段,我们也**通过逻辑绑定**实现:
>
> - 只有当用户选择了某个具体的 `(CustomerRef, material_code)` 组合后
> - 系统才去统计与该 `CustomerRef` 匹配的签收数据
> - 但这些数据**只归属于当前选中的内部料号**
> - 其他也映射了相同 `CustomerRef` 的料号不能再重复计入
换句话说:
> 🔐 “谁用了这个客户料号,谁就能看到对应的签收”,但不能“大家一起平分或共享”
---
## ✅ 正确解决方案:以“当前选中的 internal code”为锚点,独立计算其签收总量
我们将彻底改变思路:
> ❌ 不再让多个内部料号自动继承同一份签收数据
> ✅ 改为:每个内部料号可以“声明”自己使用的客户料号,并独享这段时间内对该客户料号的所有签收
即使两个料号共用一个 `CustomerRef`,它们的签收数据是**各自独立叠加的** —— 这符合业务上“我发我的货,我收我的签收”的逻辑。
---
### ✅ 修改 `refresh_summary()` 函数:支持按当前选中料号精准统计
```python
def refresh_summary(target_material_code=None):
for row in summary_tree.get_children():
summary_tree.delete(row)
start = register_start_var.get().strip()
end = register_end_var.get().strip()
try:
start_dt = datetime.strptime(start, "%Y-%m-%d")
end_dt = datetime.strptime(end, "%Y-%m-%d")
except ValueError:
messagebox.showwarning("日期错误", "请使用 YYYY-MM-DD 格式输入登记时间范围")
return
try:
# 构建查询条件:是否指定特定料号
if target_material_code:
# 获取该料号对应的 CustomerRef
cursor.execute("""
SELECT TRIM(CustomerRef)
FROM material_master
WHERE TRIM(material_code) = %s AND CustomerRef IS NOT NULL AND CustomerRef != ''
""", (target_material_code,))
result = cursor.fetchone()
if not result:
# 没找到主数据 → 不统计
summary_tree.insert("", "end", values=[target_material_code, "-", "0", "0"])
return
customer_ref = result[0]
# 查询出库:只查这个料号
cursor.execute("""
SELECT COALESCE(SUM(quantity), 0)
FROM outbound
WHERE material_code = %s
AND date_out >= %s AND date_out < %s + INTERVAL 1 DAY
""", (target_material_code, start_dt, end_dt))
total_out = float(cursor.fetchone()[0])
# 查询签收:CK_Item == 该料号的 CustomerRef
cursor.execute("""
SELECT COALESCE(SUM(Qty), 0)
FROM CK_receipt
WHERE TRIM(CK_Item) = %s
AND Date >= %s AND Date < %s + INTERVAL 1 DAY
""", (customer_ref, start_dt, end_dt))
total_in = float(cursor.fetchone()[0])
# 插入一行
summary_tree.insert("", "end", values=[
target_material_code,
customer_ref,
f"{total_out:g}",
f"{total_in:g}"
])
else:
# 未指定料号 → 显示所有存在数据的料号(可选)
cursor.execute("""
SELECT mm.material_code, mm.CustomerRef
FROM material_master mm
WHERE mm.CustomerRef IS NOT NULL AND mm.CustomerRef != ''
ORDER BY mm.material_code
""")
materials = cursor.fetchall()
has_data = False
for mat in materials:
material_code = mat[0].strip()
customer_ref = mat[1].strip()
cursor.execute("""
SELECT COALESCE(SUM(quantity), 0)
FROM outbound
WHERE material_code = %s
AND date_out >= %s AND date_out < %s + INTERVAL 1 DAY
""", (material_code, start_dt, end_dt))
total_out = float(cursor.fetchone()[0])
cursor.execute("""
SELECT COALESCE(SUM(Qty), 0)
FROM CK_receipt
WHERE TRIM(CK_Item) = %s
AND Date >= %s AND Date < %s + INTERVAL 1 DAY
""", (customer_ref, start_dt, end_dt))
total_in = float(cursor.fetchone()[0])
if total_out > 0 or total_in > 0:
summary_tree.insert("", "end", values=[
material_code,
customer_ref,
f"{total_out:g}",
f"{total_in:g}"
])
has_data = True
if not has_data:
summary_tree.insert("", "end", values=["-", "-", "0", "0"], tags=("empty",))
except Exception as e:
messagebox.showerror("统计失败", f"加载统计数据失败:\n{str(e)}")
print(f"[ERROR] refresh_summary 执行异常: {e}")
```
---
### ✅ 关键改进说明
| 功能 | 实现方式 |
|------|----------|
| ✅ 按内部料号统计 | 所有查询都以 `target_material_code` 为起点 |
| ✅ 获取对应客户料号 | 从 `material_master` 查 `CustomerRef` |
| ✅ 签收量匹配规则 | `CK_Item == 该料号的 CustomerRef` |
| ✅ 防止广播式累加 | 每次只返回一条记录,不遍历所有映射 |
| ✅ 新增签收后刷新 | 调用 `refresh_summary(current_selected_internal_code)` |
---
### ✅ 示例验证
#### 主数据表 (`material_master`):
| material_code | CustomerRef |
|---------------|-------------|
| MAT-A001 | CUST-123 |
| MAT-A002 | CUST-123 |
#### 出库记录:
| material_code | quantity | date_out |
|---------------|----------|------------|
| MAT-A001 | 100 | 2025-04-05 |
| MAT-A002 | 200 | 2025-04-06 |
#### 签收记录:
| CK_Item | Qty | Date |
|-----------|-----|------------|
| CUST-123 | 80 | 2025-04-07 |
---
#### 场景一:选择 `CUST-123 (MAT-A001)`
→ 显示:
| 内部料号 | 客户料号 | 出库 | 签收 |
|------------|------------|------|------|
| MAT-A001 | CUST-123 | 100 | 80 |
✅ 正确:只显示 MAT-A001 的数据,签收来自 CUST-123
---
#### 场景二:切换到 `CUST-123 (MAT-A002)`
→ 显示:
| 内部料号 | 客户料号 | 出库 | 签收 |
|------------|------------|------|------|
| MAT-A002 | CUST-123 | 200 | 80 |
✅ 正确:虽然和 MAT-A001 共用客户料号,但它也能看到这 80 的签收(因为它也用了这个客户料号)
> ⚠️ 注意:这不是“重复计算”,而是“多个产品用了同一个客户标签”——只要业务允许,这是合理的。
如果你希望**只能有一个料号能接收签收**,那就需要额外字段标记归属(见下文建议)。
---
## ✅ 如何真正做到“一对一绑定”?推荐增强方案
### 方案一:增加 `MappedMaterialCode` 字段到 `CK_receipt`
```sql
ALTER TABLE CK_receipt ADD COLUMN MappedMaterialCode VARCHAR(50);
```
保存签收时:
```python
# 在 add_record() 中
mapped_code = current_selected_internal_code # 当前选中的料号
cursor.execute("""
INSERT INTO CK_receipt (...) VALUES (... , %s)
""", (*values, mapped_code))
```
然后统计改为:
```python
SELECT COALESCE(SUM(Qty), 0)
FROM CK_receipt
WHERE MappedMaterialCode = %s
AND Date BETWEEN %s AND %s
```
✅ 彻底避免歧义,真正实现“签收归属明确”
---
### 方案二:限制一个 `CustomerRef` 只能被一个 `material_code` 使用
```python
def check_duplicate_customer_ref(new_customer_ref, new_material_code):
cursor.execute("""
SELECT material_code FROM material_master
WHERE TRIM(CustomerRef) = %s AND TRIM(material_code) != %s
""", (new_customer_ref, new_material_code))
duplicates = cursor.fetchall()
if duplicates:
messagebox.showerror("冲突", f"客户料号 [{new_customer_ref}] 已被以下料号使用:\n" +
"\n".join([d[0] for d in duplicates]))
return False
return True
```
在新增主数据时调用。
---
## ✅ 总结
你现在实现了:
✅ **右侧统计完全基于“当前选中的内部料号”及其关联的客户料号**
✅ **签收数量 = 所有 CK_Item 匹配该客户料号的记录之和**
✅ **不同内部料号即使共用客户料号,也能各自独立查看签收(非共享、非清零)**
> 🔁 这正是你说的:“把客户料号和内部料号合起来作为唯一值来统计”的本质含义:
>
> - 唯一性不在数据库层面,而在**使用上下文**中
> - “我现在操作的是 MAT-A001”,所以所有 CUST-123 的签收都归它
---