diff --git a/routes/products.py b/routes/products.py
index 3758612..135ec4c 100644
--- a/routes/products.py
+++ b/routes/products.py
@@ -265,7 +265,9 @@ def products_list():
if supplier_filter:
where.append("(s.supplier_name LIKE %s OR p.supplier_name_match LIKE %s)")
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")
params.append(brand_filter)
if category1_filter:
@@ -405,10 +407,17 @@ def products_filters():
f"SELECT DISTINCT category1 FROM products WHERE {scope_clause}"
"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({
"code": 0,
"data": {
- "brands": [b['brand'] for b in brands],
+ "brands": brand_values,
"categories": [c['category1'] for c in categories]
}
})
@@ -2086,7 +2095,9 @@ def products_recycle_bin():
if supplier_filter:
where.append("(p.supplier_name_match LIKE %s OR s.supplier_name LIKE %s)")
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")
params.append(brand_filter)
if product_type:
diff --git a/static/js/app.js b/static/js/app.js
index c4ead29..a901e1a 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -8322,7 +8322,8 @@ function loadProductBrandFilter(tab) {
if (!document.body.contains(sel) || !res || res.code !== 0) return;
var brands = (res.data && res.data.brands) || [];
sel.innerHTML = '' + brands.map(function(brand) {
- return '';
+ var label = brand === '__EMPTY__' ? '(品牌为空)' : brand;
+ return '';
}).join('');
});
}
diff --git a/tests/test_product_brand_filter.py b/tests/test_product_brand_filter.py
index 6ba47a0..894c11b 100644
--- a/tests/test_product_brand_filter.py
+++ b/tests/test_product_brand_filter.py
@@ -18,6 +18,7 @@ class ProductBrandFilterStaticTest(unittest.TestCase):
self.assertIn("loadProductBrandFilter('成品')", APP_JS)
self.assertIn('loadProductBrandFilter(tab)', APP_JS)
self.assertGreaterEqual(APP_JS.count("url += '&brand='"), 2)
+ self.assertIn("brand === '__EMPTY__' ? '(品牌为空)' : brand", APP_JS)
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.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):
with mock.patch.object(products, 'query', side_effect=[
[{'brand': '回收品牌'}],
[{'category1': '分类'}],
+ [{'cnt': 2}],
]) as query:
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])
+ 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__':
unittest.main()