Просмотр исходного кода

fix(inventory): persist storage_location for internal spools (#1291)

  The column existed on the Spool ORM model but was missing from
  SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic silently
  dropped writes and reads omitted the field, so the inventory
  table always showed "—" in the Storage Location column even
  after saving. Adding the field to the two schemas is enough —
  the update route already uses model_dump + setattr.
maziggy 2 недель назад
Родитель
Сommit
1d6e1b9e88

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 5 - 0
backend/app/schemas/spool.py

@@ -116,6 +116,10 @@ class SpoolBase(BaseModel):
     # User-defined category + per-spool low-stock threshold override (#729).
     category: str | None = Field(default=None, max_length=50)
     low_stock_threshold_pct: int | None = Field(default=None, ge=1, le=99)
+    # Free-text storage location, distinct from `location` (AMS slot
+    # assignment). Column has lived on the ORM since the inventory rework
+    # but was missing from this schema, so writes were silently dropped (#1291).
+    storage_location: str | None = Field(default=None, max_length=255)
 
 
 class SpoolCreate(SpoolBase):
@@ -164,6 +168,7 @@ class SpoolUpdate(BaseModel):
     # User-defined category + per-spool low-stock threshold override (#729).
     category: str | None = Field(default=None, max_length=50)
     low_stock_threshold_pct: int | None = Field(default=None, ge=1, le=99)
+    storage_location: str | None = Field(default=None, max_length=255)
 
 
 class SpoolKProfileBase(BaseModel):

+ 76 - 0
backend/tests/unit/test_spool_schemas_storage_location.py

@@ -0,0 +1,76 @@
+"""Schema tests for the spool storage_location field (#1291).
+
+Reporter @needo37: the `storage_location` column existed on the Spool ORM model
+but was missing from SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic
+silently drops unknown fields, so PATCH writes never reached the DB and reads
+omitted the field entirely. The fix is purely additive on the schema layer.
+"""
+
+import pytest
+from pydantic import ValidationError
+
+from backend.app.schemas.spool import SpoolCreate, SpoolResponse, SpoolUpdate
+
+
+class TestStorageLocationRoundtrips:
+    """The bug was that storage_location wasn't on the schemas at all — pin
+    the round-trip so a future refactor can't quietly drop it again."""
+
+    def test_create_accepts_storage_location(self):
+        spool = SpoolCreate(material="PLA", storage_location="Drybox #1")
+        assert spool.storage_location == "Drybox #1"
+
+    def test_create_storage_location_optional(self):
+        spool = SpoolCreate(material="PLA")
+        assert spool.storage_location is None
+
+    def test_update_accepts_storage_location(self):
+        update = SpoolUpdate(storage_location="Top shelf")
+        assert update.storage_location == "Top shelf"
+
+    def test_update_omits_unset_storage_location(self):
+        """A PATCH that doesn't mention storage_location must NOT clear it —
+        model_dump(exclude_unset=True) keeps the field out of the update dict
+        so the route's setattr loop skips it."""
+        update = SpoolUpdate.model_validate({})
+        dumped = update.model_dump(exclude_unset=True)
+        assert "storage_location" not in dumped
+
+    def test_update_explicit_null_clears_storage_location(self):
+        """A PATCH that explicitly sends storage_location=null must reach
+        the route's update_data dict as None, so setattr writes NULL to the
+        DB — that's how the UI clears the field."""
+        update = SpoolUpdate.model_validate({"storage_location": None})
+        dumped = update.model_dump(exclude_unset=True)
+        assert "storage_location" in dumped
+        assert dumped["storage_location"] is None
+
+    def test_response_carries_storage_location(self):
+        """SpoolResponse inherits from SpoolBase, so the field must surface
+        on read too — otherwise the inventory table silently always shows '-'."""
+        from datetime import datetime, timezone
+
+        now = datetime.now(timezone.utc)
+        response = SpoolResponse.model_validate(
+            {
+                "id": 1,
+                "material": "PLA",
+                "storage_location": "Drybox #1",
+                "created_at": now,
+                "updated_at": now,
+            }
+        )
+        assert response.storage_location == "Drybox #1"
+
+
+class TestStorageLocationLength:
+    """The DB column is String(255). Schema must enforce the same cap so the
+    API rejects too-long input cleanly instead of letting SQLAlchemy raise."""
+
+    def test_accepts_max_length(self):
+        update = SpoolUpdate(storage_location="x" * 255)
+        assert len(update.storage_location) == 255
+
+    def test_rejects_over_max_length(self):
+        with pytest.raises(ValidationError, match="storage_location"):
+            SpoolUpdate(storage_location="x" * 256)

Некоторые файлы не были показаны из-за большого количества измененных файлов