From 0f461851b50fed80afe5a65004cddc8f514fd40a Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Tue, 18 Aug 2026 09:36:33 +0800 Subject: [PATCH] feat(backend): add QC material batch stockStatus helpers. Normalize available/exhausted filter values and document remaining_qty matching for list filtering. Co-authored-by: Cursor --- .../crud/qc_material_batch_stock_status.py | 24 ++++++++++++ .../test_qc_material_batch_stock_status.py | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 backend/app/crud/qc_material_batch_stock_status.py create mode 100644 backend/tests/test_qc_material_batch_stock_status.py diff --git a/backend/app/crud/qc_material_batch_stock_status.py b/backend/app/crud/qc_material_batch_stock_status.py new file mode 100644 index 00000000..a1f9939a --- /dev/null +++ b/backend/app/crud/qc_material_batch_stock_status.py @@ -0,0 +1,24 @@ +"""质检物料入库批次:按剩余数量过滤的语义辅助。""" +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Optional + + +def normalize_batch_stock_status(stock_status: Optional[str]) -> Optional[str]: + """返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。""" + s = (stock_status or "").strip().lower() + if s in ("available", "exhausted"): + return s + return None + + +def batch_matches_stock_status(remaining_qty: Any, stock_status: Optional[str]) -> bool: + """纯函数判定(供单测);list_batches 用 SQL 等价条件。""" + mode = normalize_batch_stock_status(stock_status) + if mode is None: + return True + qty = Decimal(str(remaining_qty if remaining_qty is not None else 0)) + if mode == "available": + return qty > 0 + return qty == 0 diff --git a/backend/tests/test_qc_material_batch_stock_status.py b/backend/tests/test_qc_material_batch_stock_status.py new file mode 100644 index 00000000..86911433 --- /dev/null +++ b/backend/tests/test_qc_material_batch_stock_status.py @@ -0,0 +1,37 @@ +"""质检物料批次列表:剩余库存过滤语义。""" +import unittest +from decimal import Decimal + +from app.crud.qc_material_batch_stock_status import ( + batch_matches_stock_status, + normalize_batch_stock_status, +) + + +class TestNormalizeBatchStockStatus(unittest.TestCase): + def test_available_and_exhausted(self): + self.assertEqual(normalize_batch_stock_status("available"), "available") + self.assertEqual(normalize_batch_stock_status("exhausted"), "exhausted") + self.assertEqual(normalize_batch_stock_status(" Available "), "available") + + def test_all_empty_invalid_map_to_none(self): + self.assertIsNone(normalize_batch_stock_status(None)) + self.assertIsNone(normalize_batch_stock_status("")) + self.assertIsNone(normalize_batch_stock_status("all")) + self.assertIsNone(normalize_batch_stock_status("bogus")) + + +class TestBatchMatchesStockStatus(unittest.TestCase): + def test_all_always_true(self): + self.assertTrue(batch_matches_stock_status(0, "all")) + self.assertTrue(batch_matches_stock_status(1, None)) + + def test_available_gt_zero(self): + self.assertTrue(batch_matches_stock_status(Decimal("0.1"), "available")) + self.assertFalse(batch_matches_stock_status(0, "available")) + self.assertFalse(batch_matches_stock_status(Decimal("0"), "available")) + + def test_exhausted_eq_zero(self): + self.assertTrue(batch_matches_stock_status(0, "exhausted")) + self.assertTrue(batch_matches_stock_status(Decimal("0.0000"), "exhausted")) + self.assertFalse(batch_matches_stock_status(1, "exhausted"))