fix: allow filtering products with empty brands

This commit is contained in:
Codex 2026-10-12 17:47:38 +08:00
parent ae0cae8cbf
commit 8c3aac43a9
3 changed files with 45 additions and 5 deletions

View File

@ -265,7 +265,9 @@ def products_list():
if supplier_filter: if supplier_filter:
where.append("(s.supplier_name LIKE %s OR p.supplier_name_match LIKE %s)") where.append("(s.supplier_name LIKE %s OR p.supplier_name_match LIKE %s)")
params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"]) params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"])
if brand_filter: if brand_filter == '__EMPTY__':
where.append("(p.brand IS NULL OR TRIM(p.brand)='')")
elif brand_filter:
where.append("TRIM(p.brand)=%s") where.append("TRIM(p.brand)=%s")
params.append(brand_filter) params.append(brand_filter)
if category1_filter: if category1_filter:
@ -405,10 +407,17 @@ def products_filters():
f"SELECT DISTINCT category1 FROM products WHERE {scope_clause}" f"SELECT DISTINCT category1 FROM products WHERE {scope_clause}"
"category1 IS NOT NULL AND category1 != '' ORDER BY category1" "category1 IS NOT NULL AND category1 != '' ORDER BY category1"
) )
empty_brand_count = query(
f"SELECT COUNT(*) AS cnt FROM products WHERE {scope_clause}"
"(brand IS NULL OR TRIM(brand) = '')"
)[0]['cnt']
brand_values = [b['brand'] for b in brands]
if empty_brand_count:
brand_values.insert(0, '__EMPTY__')
return jsonify({ return jsonify({
"code": 0, "code": 0,
"data": { "data": {
"brands": [b['brand'] for b in brands], "brands": brand_values,
"categories": [c['category1'] for c in categories] "categories": [c['category1'] for c in categories]
} }
}) })
@ -2086,7 +2095,9 @@ def products_recycle_bin():
if supplier_filter: if supplier_filter:
where.append("(p.supplier_name_match LIKE %s OR s.supplier_name LIKE %s)") where.append("(p.supplier_name_match LIKE %s OR s.supplier_name LIKE %s)")
params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"]) params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"])
if brand_filter: if brand_filter == '__EMPTY__':
where.append("(p.brand IS NULL OR TRIM(p.brand)='')")
elif brand_filter:
where.append("TRIM(p.brand)=%s") where.append("TRIM(p.brand)=%s")
params.append(brand_filter) params.append(brand_filter)
if product_type: if product_type:

View File

@ -8322,7 +8322,8 @@ function loadProductBrandFilter(tab) {
if (!document.body.contains(sel) || !res || res.code !== 0) return; if (!document.body.contains(sel) || !res || res.code !== 0) return;
var brands = (res.data && res.data.brands) || []; var brands = (res.data && res.data.brands) || [];
sel.innerHTML = '<option value="">全部品牌</option>' + brands.map(function(brand) { sel.innerHTML = '<option value="">全部品牌</option>' + brands.map(function(brand) {
return '<option value="' + escHtml(brand) + '">' + escHtml(brand) + '</option>'; var label = brand === '__EMPTY__' ? '(品牌为空)' : brand;
return '<option value="' + escHtml(brand) + '">' + escHtml(label) + '</option>';
}).join(''); }).join('');
}); });
} }

View File

@ -18,6 +18,7 @@ class ProductBrandFilterStaticTest(unittest.TestCase):
self.assertIn("loadProductBrandFilter('成品')", APP_JS) self.assertIn("loadProductBrandFilter('成品')", APP_JS)
self.assertIn('loadProductBrandFilter(tab)', APP_JS) self.assertIn('loadProductBrandFilter(tab)', APP_JS)
self.assertGreaterEqual(APP_JS.count("url += '&brand='"), 2) self.assertGreaterEqual(APP_JS.count("url += '&brand='"), 2)
self.assertIn("brand === '__EMPTY__' ? '(品牌为空)' : brand", APP_JS)
class ProductBrandFilterApiTest(unittest.TestCase): class ProductBrandFilterApiTest(unittest.TestCase):
@ -35,16 +36,43 @@ class ProductBrandFilterApiTest(unittest.TestCase):
self.assertIn('TRIM(p.brand)=%s', query.call_args_list[0].args[0]) self.assertIn('TRIM(p.brand)=%s', query.call_args_list[0].args[0])
self.assertEqual(query.call_args_list[0].args[1], ['无']) self.assertEqual(query.call_args_list[0].args[1], ['无'])
def test_active_products_filter_empty_brand(self):
with mock.patch.object(products, 'query', side_effect=[[], [{'cnt': 0}], []]) as query:
response = self.client.get('/api/products/list?brand=__EMPTY__')
self.assertEqual(response.get_json()['code'], 0)
self.assertIn("(p.brand IS NULL OR TRIM(p.brand)='')", query.call_args_list[1].args[0])
self.assertEqual(query.call_args_list[1].args[1], [])
def test_recycle_bin_filters_empty_brand(self):
with mock.patch.object(products, 'query', side_effect=[[{'cnt': 0}], []]) as query:
response = self.client.get('/api/products/recycle_bin?brand=__EMPTY__')
self.assertEqual(response.get_json()['code'], 0)
self.assertIn("(p.brand IS NULL OR TRIM(p.brand)='')", query.call_args_list[0].args[0])
self.assertEqual(query.call_args_list[0].args[1], [])
def test_filter_options_can_be_scoped_to_recycle_bin(self): def test_filter_options_can_be_scoped_to_recycle_bin(self):
with mock.patch.object(products, 'query', side_effect=[ with mock.patch.object(products, 'query', side_effect=[
[{'brand': '回收品牌'}], [{'brand': '回收品牌'}],
[{'category1': '分类'}], [{'category1': '分类'}],
[{'cnt': 2}],
]) as query: ]) as query:
response = self.client.get('/api/products/filters?scope=recycle') response = self.client.get('/api/products/filters?scope=recycle')
self.assertEqual(response.get_json()['data']['brands'], ['回收品牌']) self.assertEqual(response.get_json()['data']['brands'], ['__EMPTY__', '回收品牌'])
self.assertIn('deleted_at IS NOT NULL', query.call_args_list[0].args[0]) self.assertIn('deleted_at IS NOT NULL', query.call_args_list[0].args[0])
def test_filter_options_omit_empty_brand_when_scope_has_none(self):
with mock.patch.object(products, 'query', side_effect=[
[{'brand': '正常品牌'}],
[],
[{'cnt': 0}],
]):
response = self.client.get('/api/products/filters?scope=active')
self.assertEqual(response.get_json()['data']['brands'], ['正常品牌'])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()