feat(records): 高级筛选引擎 + 出库记录接入
一、新增共享工具 app/utils/advanced_filter.py
系统内已有该模式(material/list.vue、stock/inbound/buy.vue),
沿用其既有约定:参数名 advancedFilters、值为 JSON 字符串、
操作符 eq/ne/contains/not_contains/ge/le。
· parse_advanced_filters() 解析并规整,坏输入退化为空列表不影响主查询
· build_predicate() 单条件 → SQLAlchemy 谓词,未登记字段返回 None 杜绝列注入
· build_material_name_select() 物料名三表联查(buy/semi/product JOIN material_base)
二、★ 父子关系处理(本次核心)
记录接口返回的是**按单号分组的订单**,而用户筛选字段多落在**明细行**上。
若直接 .filter(TransOutbound.sku.ilike(...)),会在 GROUP BY 前收窄明细范围,
展开行里的兄弟明细会凭空消失。正确做法是先求「含匹配明细的单号集合」
再让主查询按单号 IN 过滤。
实测对照(单 OUT-20260811-1519-0003,21 条明细):
按其中一条 SKU 筛选 → 子查询法保住全部 21 条;直接 filter 只剩 1 条。
三、★ 否定操作符语义(NOT IN)
子级字段的 ne / not_contains 不能直接用 SQL != / NOT LIKE —— 那表达的是
「本单存在某条不等于 X 的明细」,多明细单几乎必然成立,等于筛选失效。
用户意图是**整单排除**,故 apply_child_condition() 统一:
肯定 → order_no IN (含匹配明细的单号)
否定 → order_no NOT IN (含匹配明细的单号)
两者子查询完全一致(都用肯定形式谓词),仅外层取反。
父级字段(单号/操作人)仍走标准 SQL 谓词,语义无歧义。
四、出库记录接入(前端弹窗 + 后端接线)
验证:
eq 0000000002 → 1 单;material_name contains 白板 → 16 单
sku ne 0000000002 → 394 = 395-1,含该 SKU 的单被整体排除
material_name not_contains 白板 → 379 = 395-16
This commit is contained in:
@ -327,12 +327,13 @@ class OutboundService:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None, consumer_name=None):
|
||||
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None, consumer_name=None, advanced_filters=None):
|
||||
"""
|
||||
查询出库记录(按出库单号分组),包含详细物品信息
|
||||
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
||||
search_type: all, no, name, sku, material_name, spec_model
|
||||
company: 可选的公司过滤参数
|
||||
advanced_filters: 高级筛选条件列表 [{'field','operator','value'}, ...]
|
||||
"""
|
||||
# 日期补全:解决零点截断问题
|
||||
if end_date and len(str(end_date).strip()) == 10:
|
||||
@ -536,6 +537,51 @@ class OutboundService:
|
||||
if keyword_conditions is not None:
|
||||
stmt = stmt.filter(keyword_conditions)
|
||||
|
||||
# ====================================================================
|
||||
# ★ 高级筛选:父级字段直接过滤,子级字段(SKU/物料名称)走
|
||||
# 「命中单号子查询 → 按单号 IN」的 EXISTS 语义。
|
||||
#
|
||||
# 绝不能写成 stmt.filter(TransOutbound.sku.ilike(...)):
|
||||
# 那样会在 GROUP BY 前收窄明细范围,展开行里的兄弟明细会丢失。
|
||||
# ====================================================================
|
||||
if advanced_filters:
|
||||
from app.utils.advanced_filter import (
|
||||
build_predicate, apply_child_condition,
|
||||
)
|
||||
|
||||
# 父级字段:单号/操作人本身就在流水表上,直接 filter(否定操作符
|
||||
# 走标准 SQL != / NOT LIKE 即可,语义无歧义)
|
||||
parent_field_map = {
|
||||
'no': TransOutbound.outbound_no,
|
||||
'operator': TransOutbound.operator_name,
|
||||
'consumer_name': TransOutbound.consumer_name,
|
||||
'outbound_type': TransOutbound.outbound_type,
|
||||
}
|
||||
# 子级字段:SKU 直接列 + 物料名称(需三表联查)
|
||||
child_field_map = {'sku': TransOutbound.sku}
|
||||
material_stock_models = [
|
||||
(StockBuy, 'stock_buy'),
|
||||
(StockSemi, 'stock_semi'),
|
||||
(StockProduct, 'stock_product'),
|
||||
]
|
||||
|
||||
for cond in advanced_filters:
|
||||
field = cond.get('field')
|
||||
|
||||
if field in parent_field_map:
|
||||
p = build_predicate(cond, parent_field_map)
|
||||
if p is not None:
|
||||
stmt = stmt.filter(p)
|
||||
continue
|
||||
|
||||
if field in child_field_map or field == 'material_name':
|
||||
# ★ 正/负操作符语义分派:否定 → 整单排除(NOT IN)
|
||||
stmt = apply_child_condition(
|
||||
stmt, TransOutbound.outbound_no, TransOutbound, cond,
|
||||
child_field_map, material_stock_models,
|
||||
)
|
||||
continue
|
||||
|
||||
if start_date and end_date:
|
||||
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user