Kaynağa Gözat

feat(backend): add RK+date stock-in number allocator for qc materials

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 ay önce
ebeveyn
işleme
ca4b9a15c5

+ 79 - 0
backend/app/crud/qc_material_stock_in_no.py

@@ -0,0 +1,79 @@
+"""质检物料入库编号生成:RK + YYYYMMDD + 3位序号。"""
+from __future__ import annotations
+
+import re
+from datetime import date
+from typing import Iterable, List
+
+from sqlalchemy.orm import Session
+
+from app.models.qc_material import BizQcMaterialStockBatch
+
+_SUFFIX_RE = re.compile(r"^(\d{3})$")
+
+
+def format_stock_in_no(d: date, seq: int) -> str:
+    if seq < 1 or seq > 999:
+        raise ValueError(f"入库编号序号超出范围: {seq}")
+    return f"RK{d.strftime('%Y%m%d')}{seq:03d}"
+
+
+def prefix_for_date(d: date) -> str:
+    return f"RK{d.strftime('%Y%m%d')}"
+
+
+def max_seq_from_stock_in_nos(prefix: str, nos: Iterable[str]) -> int:
+    max_seq = 0
+    for raw in nos:
+        s = (raw or "").strip()
+        if not s.startswith(prefix):
+            continue
+        suffix = s[len(prefix) :]
+        m = _SUFFIX_RE.match(suffix)
+        if not m:
+            continue
+        max_seq = max(max_seq, int(m.group(1)))
+    return max_seq
+
+
+def next_stock_in_nos(
+    d: date,
+    *,
+    count: int,
+    existing_nos: Iterable[str],
+) -> List[str]:
+    if count <= 0:
+        return []
+    prefix = prefix_for_date(d)
+    start = max_seq_from_stock_in_nos(prefix, existing_nos) + 1
+    if start + count - 1 > 999:
+        raise ValueError("当日入库编号序号已用尽(上限 999)")
+    return [format_stock_in_no(d, start + i) for i in range(count)]
+
+
+def list_existing_stock_in_nos_for_prefix(
+    db: Session, *, tenant_no: str, prefix: str
+) -> List[str]:
+    like = f"{prefix}%"
+    rows = (
+        db.query(BizQcMaterialStockBatch.stock_in_no)
+        .filter(
+            BizQcMaterialStockBatch.tenant_no == tenant_no,
+            BizQcMaterialStockBatch.del_flag == 0,
+            BizQcMaterialStockBatch.stock_in_no.like(like),
+        )
+        .all()
+    )
+    return [r[0] for r in rows if r[0]]
+
+
+def allocate_stock_in_nos(
+    db: Session,
+    *,
+    tenant_no: str,
+    stock_in_date: date,
+    count: int,
+) -> List[str]:
+    prefix = prefix_for_date(stock_in_date)
+    existing = list_existing_stock_in_nos_for_prefix(db, tenant_no=tenant_no, prefix=prefix)
+    return next_stock_in_nos(stock_in_date, count=count, existing_nos=existing)

+ 38 - 0
backend/tests/test_qc_material_stock_in_no.py

@@ -0,0 +1,38 @@
+"""质检物料入库编号:RK + YYYYMMDD + 3位序号。"""
+import unittest
+from datetime import date
+
+from app.crud.qc_material_stock_in_no import (
+    format_stock_in_no,
+    max_seq_from_stock_in_nos,
+    next_stock_in_nos,
+)
+
+
+class TestFormatStockInNo(unittest.TestCase):
+    def test_format(self):
+        self.assertEqual(format_stock_in_no(date(2026, 8, 19), 1), "RK20260819001")
+        self.assertEqual(format_stock_in_no(date(2026, 8, 19), 12), "RK20260819012")
+        self.assertEqual(format_stock_in_no(date(2026, 8, 19), 999), "RK20260819999")
+
+
+class TestMaxSeq(unittest.TestCase):
+    def test_empty(self):
+        self.assertEqual(max_seq_from_stock_in_nos("RK20260819", []), 0)
+
+    def test_parses_matching_prefix(self):
+        nos = ["RK20260819001", "RK20260819007", "OTHER", "RK20260818099"]
+        self.assertEqual(max_seq_from_stock_in_nos("RK20260819", nos), 7)
+
+    def test_ignores_bad_suffix(self):
+        nos = ["RK20260819ABC", "RK20260819100"]
+        self.assertEqual(max_seq_from_stock_in_nos("RK20260819", nos), 100)
+
+
+class TestNextStockInNos(unittest.TestCase):
+    def test_allocates_contiguous(self):
+        got = next_stock_in_nos(date(2026, 8, 19), count=3, existing_nos=["RK20260819002"])
+        self.assertEqual(got, ["RK20260819003", "RK20260819004", "RK20260819005"])
+
+    def test_zero_count(self):
+        self.assertEqual(next_stock_in_nos(date(2026, 8, 19), count=0, existing_nos=[]), [])