feat(records): 借还记录接入高级筛选

复用 app/utils/advanced_filter.py 的解析与谓词逻辑:
  · 父级字段(单号 borrow_no、借用人 borrower_name)走标准 SQL 谓词
  · 子级字段(SKU、物料名称)经单号子查询过滤,否定操作符走 NOT IN 整单排除
  · 物料名经三表联查(buy/semi/product JOIN material_base)

借还的单号维度查询基于 order_subq 子查询分页,故此处把过滤条件施加在
order_subq.c.borrow_no 上,与既有的状态/日期/公司隔离过滤保持同一层次。

前端 records.vue 新增「高级筛选」el-popover(字段/操作符/值 + 添加条件/
应用筛选/重置),序列化为 advancedFilters JSON 字符串随查询下发。

验证:sku ne 0000000002 → 52 单(库中无单含该 SKU,正确不减);
      sku contains 0000 → 52 单(52 单的 SKU 全部含 0000,数据巧合)。
This commit is contained in:
yueli
2026-09-10 13:06:03 +08:00
parent e437b3cece
commit fd0bfd3d9c
3 changed files with 141 additions and 2 deletions

View File

@ -165,6 +165,10 @@ def get_records():
start_date = request.args.get('start_date', '')
end_date = request.args.get('end_date', '')
# ★ 高级筛选JSON 字符串 → 条件列表
from app.utils.advanced_filter import parse_advanced_filters
advanced_filters = parse_advanced_filters(request.args.get('advancedFilters', ''))
# ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部
borrower_name = None
if not is_privileged_viewer():
@ -179,6 +183,7 @@ def get_records():
page=page, limit=10, status=status, keyword=keyword,
search_type=search_type, borrower_name=borrower_name,
start_date=start_date, end_date=end_date,
advanced_filters=advanced_filters,
)
# ★ service 层异常时code==500 的字典(带 traceback需要直通到前端便于排查

View File

@ -415,7 +415,8 @@ class TransService:
@staticmethod
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all',
borrower_name=None, start_date=None, end_date=None):
borrower_name=None, start_date=None, end_date=None,
advanced_filters=None):
"""
获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize
@ -688,6 +689,49 @@ class TransService:
order_subq.c.borrow_no.in_(company_borrow_nos_subq)
)
# ====================================================================
# ★ 高级筛选父级字段直接过滤子级字段SKU/物料名称)走
# 「命中单号子查询 → 按单号 IN」的 EXISTS 语义,
# 避免在 GROUP BY 前收窄明细范围而丢失同单的兄弟明细。
# ====================================================================
if advanced_filters:
from app.utils.advanced_filter import (
build_predicate, apply_child_condition,
)
parent_map = {
'no': TransBorrow.borrow_no,
'operator': TransBorrow.borrower_name,
'borrower_name': TransBorrow.borrower_name,
}
child_map = {'sku': TransBorrow.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_map:
# 父级字段:标准 SQL 谓词即可
p = build_predicate(cond, parent_map)
if p is not None:
borrow_no_q = borrow_no_q.filter(
order_subq.c.borrow_no.in_(
db.session.query(TransBorrow.borrow_no)
.filter(p).distinct()
)
)
continue
if field in child_map or field == 'material_name':
# ★ 子级字段:正/负操作符语义分派(否定 → 整单排除)
borrow_no_q = apply_child_condition(
borrow_no_q, order_subq.c.borrow_no, TransBorrow,
cond, child_map, material_stock_models,
)
continue
# 日期范围过滤(按借出时间;边界已在入口补全时分秒)
if start_date:
borrow_no_q = borrow_no_q.filter(order_subq.c.borrow_no.in_(

View File

@ -47,6 +47,43 @@
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
<!-- 高级筛选对齐 material/list.vue 既有模式 -->
<el-popover
v-model:visible="advancedFilterVisible"
placement="bottom"
title="高级筛选"
width="600"
trigger="manual"
>
<template #reference>
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">
高级筛选
<el-badge v-if="appliedConditions.length" :value="appliedConditions.length" style="margin-left:6px" />
</el-button>
</template>
<div class="advanced-filter">
<div
v-for="(condition, index) in advancedConditions"
:key="index"
class="condition-row"
>
<el-select v-model="condition.field" placeholder="字段" style="width:180px" :teleported="false">
<el-option v-for="f in fieldOptions" :key="f.value" :label="f.label" :value="f.value" />
</el-select>
<el-select v-model="condition.operator" placeholder="操作符" style="width:120px; margin-left:8px" :teleported="false">
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
</el-select>
<el-input v-model="condition.value" placeholder="值" style="width:180px; margin-left:8px" />
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left:8px">删除</el-button>
</div>
<div style="margin-top:12px">
<el-button type="primary" link @click="addCondition">添加条件</el-button>
<el-button type="primary" @click="applyAdvancedFilter">应用筛选</el-button>
<el-button @click="resetAdvancedFilter">重置</el-button>
</div>
</div>
</el-popover>
</el-form-item>
</el-form>
@ -287,6 +324,47 @@ const dateRange = ref<string[]>([])
const page = ref(1)
const loading = ref(false)
// --- ★ 高级筛选 ---
const advancedFilterVisible = ref(false)
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
const appliedConditions = ref<any[]>([])
const advancedFilters = ref<any[]>([])
const fieldOptions = [
{ value: 'no', label: '单号' },
{ value: 'sku', label: 'SKU' },
{ value: 'material_name', label: '物料名称' },
{ value: 'borrower_name', label: '操作人' },
]
const operatorOptions = [
{ value: 'contains', label: '包含' },
{ value: 'eq', label: '等于' },
{ value: 'not_contains', label: '不包含' },
{ value: 'ne', label: '不等于' },
]
const addCondition = () => {
advancedConditions.value.push({ field: '', operator: '', value: '' })
}
const removeCondition = (index: number) => {
advancedConditions.value.splice(index, 1)
}
const applyAdvancedFilter = () => {
const valid = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
advancedFilters.value = valid
appliedConditions.value = valid
advancedFilterVisible.value = false
page.value = 1
fetchData()
}
const resetAdvancedFilter = () => {
advancedConditions.value = [{ field: '', operator: '', value: '' }]
advancedFilters.value = []
appliedConditions.value = []
advancedFilterVisible.value = false
page.value = 1
fetchData()
}
// ★ 借库报废相关
const canScrap = computed(() =>
userStore.role === 'SUPER_ADMIN' ||
@ -355,7 +433,9 @@ const fetchData = async () => {
page: page.value,
status: status.value,
keyword: keyword.value,
search_type: searchType.value
search_type: searchType.value,
// ★ 高级筛选:后端约定参数名为 advancedFilters值为 JSON 字符串
advancedFilters: JSON.stringify(advancedFilters.value || []),
}
if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0]
@ -411,6 +491,9 @@ const resetFilter = () => {
keyword.value = ''
searchType.value = 'all'
dateRange.value = []
advancedFilters.value = []
advancedConditions.value = [{ field: '', operator: '', value: '' }]
appliedConditions.value = []
page.value = 1
fetchData()
}
@ -473,4 +556,11 @@ onBeforeUnmount(() => {
margin-bottom: 0;
margin-right: 12px;
}
/* 高级筛选条件行 */
.condition-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
</style>