diff --git a/bambu_run/mcp/jobs.py b/bambu_run/mcp/jobs.py index da14696..5820145 100644 --- a/bambu_run/mcp/jobs.py +++ b/bambu_run/mcp/jobs.py @@ -72,3 +72,45 @@ def _job_summary(job): "cost": None, "has_cover_image": bool(cloud and cloud.cover_url), } + + +def get_print_job(job_id: int) -> dict: + """Full detail for one print job: the list_print_jobs fields plus + per-slot filament breakdown, estimate-vs-actual delta, and linked cloud + metadata. Raises PrintJob.DoesNotExist if job_id is unknown. + """ + job = PrintJob.objects.select_related("cloud_task", "device").get(id=job_id) + summary = _job_summary(job) + + delta_percent = None + if summary["estimated_duration_min"] and summary["duration_min"]: + delta_percent = round( + 100 * (summary["duration_min"] - summary["estimated_duration_min"]) + / summary["estimated_duration_min"], + 1, + ) + + usages = [] + for u in job.filament_usages.select_related("filament"): + consumed_percent, consumed_grams, quality = normalize_consumed( + u.starting_percent, u.ending_percent, + u.filament.initial_weight_grams if u.filament else None, + ) + usages.append({ + "tray_id": u.tray_id, + "ams_unit_id": u.ams_unit_id, + "filament_id": u.filament_id, + "consumed_percent": consumed_percent, + "consumed_grams": consumed_grams, + "data_quality": quality, + "is_primary": u.is_primary, + }) + + cloud = job.cloud_task + return { + **summary, + "estimate_delta_percent": delta_percent, + "filament_usages": usages, + "cloud_design_title": cloud.design_title if cloud else None, + "cover_url": cloud.cover_url if cloud and cloud.cover_url else None, + } diff --git a/tests/test_mcp_jobs.py b/tests/test_mcp_jobs.py index 17c958c..31173e3 100644 --- a/tests/test_mcp_jobs.py +++ b/tests/test_mcp_jobs.py @@ -2,8 +2,8 @@ import pytest from datetime import timedelta from django.utils import timezone -from bambu_run.models import Printer, PrintJob, BambuCloudTask -from bambu_run.mcp.jobs import list_print_jobs +from bambu_run.models import Printer, PrintJob, BambuCloudTask, Filament, FilamentUsage +from bambu_run.mcp.jobs import list_print_jobs, get_print_job @pytest.mark.django_db @@ -69,3 +69,40 @@ def test_list_print_jobs_display_name_falls_back_to_project_name(): result = list_print_jobs() assert result["jobs"][0]["display_name"] == "raw_project" + + +@pytest.mark.django_db +def test_get_print_job_marks_negative_consumption_unreliable(): + p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True) + job = PrintJob.objects.create(device=p, project_name="Bracket", start_time=timezone.now(), final_status="FINISH") + filament = Filament.objects.create(type="ABS", brand="Bambu Lab", color="Mint", initial_weight_grams=1000) + FilamentUsage.objects.create( + print_job=job, filament=filament, tray_id=0, + starting_percent=51, ending_percent=52, # ordinary RFID jitter, spec finding + ) + + result = get_print_job(job.id) + + usage = result["filament_usages"][0] + assert usage["consumed_grams"] is None + assert usage["data_quality"] == "unreliable" + + +@pytest.mark.django_db +def test_get_print_job_computes_estimate_delta_percent(): + p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True) + cloud = BambuCloudTask.objects.create(task_id=1, cost_time_seconds=205 * 60) + job = PrintJob.objects.create( + device=p, project_name="Bracket", start_time=timezone.now(), + final_status="FINISH", cloud_task=cloud, duration_minutes=237, + ) + + result = get_print_job(job.id) + + assert result["estimate_delta_percent"] == round(100 * (237 - 205) / 205, 1) + + +@pytest.mark.django_db +def test_get_print_job_unknown_id_raises(): + with pytest.raises(PrintJob.DoesNotExist): + get_print_job(999999)