From b4445f07d9c40a57e80bc1954a3a01381b12a12e Mon Sep 17 00:00:00 2001 From: yueli Date: Fri, 18 Sep 2026 14:31:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(material):=20=E7=89=A9=E6=96=99=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=96=B0=E5=A2=9E=E3=80=8C=E9=87=87=E8=B4=AD=E5=9C=A8?= =?UTF-8?q?=E9=80=94=E3=80=8D=E8=87=AA=E5=8A=A8=E6=A0=87=E8=AF=86=EF=BC=8C?= =?UTF-8?q?=E9=A2=84=E8=AD=A6=E9=82=AE=E4=BB=B6=E6=94=B9=E7=94=A8=E6=B4=BB?= =?UTF-8?q?=E8=B7=83=E5=8D=95=E9=9D=99=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit base_service.get_list: 新增 isPurchasing 字段,与待采购池共用同一份活跃单判定。放在预警权限 判断之外 —— 能看到物料列表的人都该知道这个物料已经在采购了。 inventory_task: 静音条件从人工标记 is_ordered 改为「该物料无活跃采购单」。 ★ 为什么必须同时改邮件:预警邮件由出库执行流程触发(outbound_service 出库 完成后的调用),功能是活的;而它的静音入口(前端人工「标记已采购」)随本次 改造一并移除。若不改这里,用户会彻底失去静音能力,每个缺货物料在每次出库后 都会发信。自动判定在建单时静默、单据驳回或强制结案后自动恢复,比人工标记准。 两张表的判定口径都由 utils/purchase_activity.py 提供,不各写一遍。 --- .../app/services/inbound/base_service.py | 16 +++++++++++++++- inventory-backend/app/services/inventory_task.py | 15 ++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py index c03eeb9..f0d394d 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -171,6 +171,10 @@ class MaterialBaseService: # 导入预警设置模型 from app.models.base import MaterialWarningSetting + # 「采购在途」判定 —— 与待采购池(purchase_service.get_pending_purchase_pool) + # 共用同一份定义,避免两处口径漂移:有活跃采购单(待审批/已通过/部分到货未齐) + # 即为在途;单据驳回或强制结案后自动解除。 + from app.utils.purchase_activity import active_purchase_exists # ============================================================ # 【核心修复】使用子查询方案:将聚合结果作为子查询 @@ -199,7 +203,11 @@ class MaterialBaseService: MaterialWarningSetting.yellow_threshold.label('warning_yellow'), MaterialWarningSetting.red_threshold.label('warning_red'), MaterialWarningSetting.red_emails.label('warning_red_emails'), - MaterialWarningSetting.yellow_emails.label('warning_yellow_emails') + MaterialWarningSetting.yellow_emails.label('warning_yellow_emails'), + # ★ 只加列、不参与排序:enableWarningSort 用的是固定的 order_by + # 表达式,与 SELECT 列表无关;且 sort_field_map 里没有它, + # 前端无法按此列排序。既有行为逐位不变。 + active_purchase_exists(MaterialBase.id).label('is_purchasing') ).outerjoin(inner_sub, MaterialBase.id == inner_sub.c.base_id) \ .outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id) @@ -423,6 +431,7 @@ class MaterialBaseService: # 说明查询只返回了 MaterialBase 单一对象 item = row inv = avail = warning_enabled = warning_yellow = warning_red = None + is_purchasing = False else: # 说明返回了 Row 对象 (包含多个字段) item = row[0] @@ -433,6 +442,7 @@ class MaterialBaseService: warning_red = row[5] if len(row) > 5 else 0 warning_red_emails = row[6] if len(row) > 6 else None warning_yellow_emails = row[7] if len(row) > 7 else None + is_purchasing = row[8] if len(row) > 8 else False # 安全兜底 if not hasattr(item, 'to_dict'): @@ -442,6 +452,10 @@ class MaterialBaseService: item_dict['inventoryCount'] = float(inv) if inv is not None else 0 item_dict['availableCount'] = float(avail) if avail is not None else 0 + # ★ 「采购在途」放在预警权限判断**之外**:能看到物料列表的人 + # 都应该知道这个物料已经在采购了(即使他没有查看预警的门槛)。 + item_dict['isPurchasing'] = bool(is_purchasing) + # 处理预警信息(仅当用户有权限时) if has_warning_permission: item_dict['warningEnabled'] = bool(warning_enabled) if warning_enabled is not None else False diff --git a/inventory-backend/app/services/inventory_task.py b/inventory-backend/app/services/inventory_task.py index 1e08d0e..d25092d 100644 --- a/inventory-backend/app/services/inventory_task.py +++ b/inventory-backend/app/services/inventory_task.py @@ -116,10 +116,16 @@ class InventoryWarningService: """ 执行库存预警扫描与邮件发送 - 1. 查询所有 is_enabled=True 且 is_ordered=False 的预警配置 + 1. 查询所有 is_enabled=True 且**没有活跃采购单**的预警配置 2. 按 level 归类物料,按邮箱聚合(同一邮箱 → 一封邮件) 3. 调用 send_email 发送,更新 last_notified_at + ★ 静音条件从「人工标记 is_ordered」改成了「是否有活跃采购单」: + 人工标记的前端入口已随「采购在途」自动化改造一并移除,若不改这里, + 用户将彻底失去静音能力(本函数由出库执行流程触发,见 + outbound_service.py 出库完成后的调用)。自动化判定与待采购池、 + 物料列表 isPurchasing 共用同一份定义,口径一致。 + Returns: { "red_count": N, # 触发红色预警的物料数 @@ -134,10 +140,13 @@ class InventoryWarningService: beijing_tz = timezone(timedelta(hours=8)) now = datetime.now(beijing_tz) - # 查询启用了预警且未标记采购的配置 + # 查询启用了预警、且当前没有活跃采购单的配置。 + # 有在途单 → 不必再催采购,静音;单据被驳回或强制结案后自动恢复告警。 + from app.utils.purchase_activity import active_purchase_exists + settings = MaterialWarningSetting.query.filter( MaterialWarningSetting.is_enabled == True, - MaterialWarningSetting.is_ordered == False + ~active_purchase_exists(MaterialWarningSetting.base_id), ).all() red_rows_by_email = defaultdict(list) # email -> [物料row, ...]