mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
Add project parts tracking separate from plates
Track individual parts/objects separately from print plates in projects. Useful for multi-part builds like Voron where 25 plates produce 150 parts. Backend: - Add target_parts_count field to Project model - Calculate parts_progress_percent and remaining_parts in stats - Auto-detect quantity from 3MF printable objects when archiving - Sum archive quantities for completed_count (parts) - Use archive_count for plates progress Frontend: - Add "Target Parts" input in project create/edit modal - Show separate progress bars for plates vs parts - Stats footer displays both plates and parts count - Header badge shows parts progress when target set Scripts: - Add update_archive_quantities.py to migrate existing archives Tests: - Add 5 integration tests for parts tracking - Add 3 unit tests for 3MF object extraction Closes #85
This commit is contained in:
@@ -113,6 +113,155 @@ class TestProjectsAPI:
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestProjectPartsTracking:
|
||||
"""Tests for project parts tracking feature."""
|
||||
|
||||
@pytest.fixture
|
||||
async def project_factory(self, db_session):
|
||||
"""Factory to create test projects."""
|
||||
|
||||
async def _create_project(**kwargs):
|
||||
from backend.app.models.project import Project
|
||||
|
||||
defaults = {
|
||||
"name": "Parts Test Project",
|
||||
"description": "Test project",
|
||||
"color": "#FF0000",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
project = Project(**defaults)
|
||||
db_session.add(project)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(project)
|
||||
return project
|
||||
|
||||
return _create_project
|
||||
|
||||
@pytest.fixture
|
||||
async def archive_factory(self, db_session):
|
||||
"""Factory to create test archives."""
|
||||
|
||||
async def _create_archive(**kwargs):
|
||||
from backend.app.models.archive import PrintArchive
|
||||
|
||||
defaults = {
|
||||
"filename": "test.3mf",
|
||||
"file_path": "test/test.3mf",
|
||||
"file_size": 1000,
|
||||
"print_name": "Test Print",
|
||||
"status": "completed",
|
||||
"quantity": 1,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
archive = PrintArchive(**defaults)
|
||||
db_session.add(archive)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(archive)
|
||||
return archive
|
||||
|
||||
return _create_archive
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_project_with_target_parts_count(self, async_client: AsyncClient):
|
||||
"""Verify project can be created with target_parts_count."""
|
||||
data = {
|
||||
"name": "Parts Project",
|
||||
"target_count": 10, # 10 plates
|
||||
"target_parts_count": 50, # 50 parts total
|
||||
}
|
||||
response = await async_client.post("/api/v1/projects/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["target_count"] == 10
|
||||
assert result["target_parts_count"] == 50
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_project_target_parts_count(self, async_client: AsyncClient, project_factory, db_session):
|
||||
"""Verify target_parts_count can be updated."""
|
||||
project = await project_factory()
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/projects/{project.id}",
|
||||
json={"target_parts_count": 100},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["target_parts_count"] == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_project_parts_progress_calculation(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Verify parts progress is calculated from archive quantities."""
|
||||
# Create project with target of 20 parts
|
||||
project = await project_factory(target_parts_count=20)
|
||||
|
||||
# Create archives with different quantities
|
||||
await archive_factory(project_id=project.id, quantity=3, status="completed") # 3 parts
|
||||
await archive_factory(project_id=project.id, quantity=5, status="completed") # 5 parts
|
||||
await archive_factory(project_id=project.id, quantity=2, status="completed") # 2 parts
|
||||
# Total: 10 parts completed out of 20 = 50%
|
||||
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check stats
|
||||
assert data["stats"]["completed_prints"] == 10 # Sum of quantities
|
||||
assert data["stats"]["parts_progress_percent"] == 50.0 # 10/20 = 50%
|
||||
assert data["stats"]["remaining_parts"] == 10 # 20 - 10 = 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_project_list_shows_parts_count(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Verify project list returns correct completed_count (parts sum)."""
|
||||
project = await project_factory(name="List Parts Project", target_parts_count=100)
|
||||
|
||||
# Create archives with quantities
|
||||
await archive_factory(project_id=project.id, quantity=4, status="completed")
|
||||
await archive_factory(project_id=project.id, quantity=6, status="completed")
|
||||
# Total: 10 parts, 2 plates
|
||||
|
||||
response = await async_client.get("/api/v1/projects/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Find our project
|
||||
our_project = next((p for p in data if p["name"] == "List Parts Project"), None)
|
||||
assert our_project is not None
|
||||
assert our_project["archive_count"] == 2 # 2 plates
|
||||
assert our_project["completed_count"] == 10 # 10 parts (sum of quantities)
|
||||
assert our_project["target_parts_count"] == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_plates_vs_parts_progress(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Verify plates and parts progress are calculated separately."""
|
||||
# Project needs 5 plates producing 25 parts total (5 parts per plate)
|
||||
project = await project_factory(target_count=5, target_parts_count=25)
|
||||
|
||||
# Complete 2 plates, each with 5 parts
|
||||
await archive_factory(project_id=project.id, quantity=5, status="completed")
|
||||
await archive_factory(project_id=project.id, quantity=5, status="completed")
|
||||
# Plates: 2/5 = 40%, Parts: 10/25 = 40%
|
||||
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
assert data["stats"]["total_archives"] == 2 # 2 plates
|
||||
assert data["stats"]["completed_prints"] == 10 # 10 parts
|
||||
assert data["stats"]["progress_percent"] == 40.0 # plates: 2/5
|
||||
assert data["stats"]["parts_progress_percent"] == 40.0 # parts: 10/25
|
||||
|
||||
|
||||
class TestProjectArchivesAPI:
|
||||
"""Tests for project-archive relationships."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user