Add bulk spool addition and stock spools

Add Quick Add mode to the spool form for simplified entry (material +
color + weight only), and a quantity field (1-100) for creating multiple
identical spools at once. Stock spools (no slicer profile) are computed
rather than stored — any spool without slicer_filament shows an amber
"Stock" badge. New filter chips (All/Stock/Configured) on the inventory
page. Backend bulk endpoint creates N spools in one transaction.

Backend:
- SpoolBulkCreate schema with quantity validation (1-100)
- POST /inventory/spools/bulk endpoint

Frontend:
- Quick Add toggle in SpoolFormModal (create mode only)
- Quantity field in FilamentSection (both modes)
- bulkCreateSpools API method and bulkCreateMutation
- Stock column (hidden by default) and stock filter chips
- Inline error rendering moved into FilamentSection
- CellCtx extended with t() for translatable cell content

Tests:
- 13 backend tests (schema validation + endpoint logic)
- 10 frontend tests (validateForm quickAdd + UI behavior)

i18n: 6 locales (en, de, fr, it, ja, pt-BR)
Docs: CHANGELOG, README, website features, wiki inventory
This commit is contained in:
maziggy
2026-02-21 10:39:18 +01:00
parent 7d46b44561
commit 16fa84bc16
19 changed files with 883 additions and 262 deletions
+19
View File
@@ -22,6 +22,7 @@ from backend.app.models.user import User
from backend.app.schemas.spool import (
SpoolAssignmentCreate,
SpoolAssignmentResponse,
SpoolBulkCreate,
SpoolCreate,
SpoolKProfileBase,
SpoolKProfileResponse,
@@ -482,6 +483,24 @@ async def create_spool(
return result.scalar_one()
@router.post("/spools/bulk", response_model=list[SpoolResponse])
async def bulk_create_spools(
data: SpoolBulkCreate,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
):
"""Create multiple identical spools."""
spools = []
for _ in range(data.quantity):
spool = Spool(**data.spool.model_dump())
db.add(spool)
spools.append(spool)
await db.commit()
ids = [s.id for s in spools]
result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id.in_(ids)))
return list(result.scalars().all())
@router.patch("/spools/{spool_id}", response_model=SpoolResponse)
async def update_spool(
spool_id: int,