test_one_print_cost_b5.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. """An archive's cost is the cost of one print of it.
  2. Completion stores the first run's cost on the archive and every run's own cost
  3. on its Print History entry (#1378). "Recalculate costs" and "Rescan" used to
  4. sum the spool usage of every run, so an archive printed three times was priced
  5. three times over; editing its grams repriced nothing.
  6. """
  7. import zipfile
  8. import pytest
  9. from httpx import AsyncClient
  10. from sqlalchemy import select
  11. from backend.app.core.config import settings
  12. from backend.app.models.print_log import PrintLogEntry
  13. from backend.app.models.spool_usage_history import SpoolUsageHistory
  14. async def _archive(archive_factory, printer_factory, db_session, *, grams, cost=None, with_run=True):
  15. printer = await printer_factory()
  16. archive = await archive_factory(
  17. printer.id,
  18. print_name="Bracket",
  19. status="completed",
  20. cost=cost,
  21. filament_used_grams=grams,
  22. filament_type="PLA",
  23. with_run=with_run,
  24. )
  25. return archive.id
  26. async def _usage(db_session, archive_id, *rows):
  27. for grams, cost in rows:
  28. db_session.add(SpoolUsageHistory(archive_id=archive_id, weight_used=grams, cost=cost, print_name="Bracket"))
  29. await db_session.commit()
  30. async def _cost(async_client, archive_id):
  31. response = await async_client.get(f"/api/v1/archives/{archive_id}")
  32. assert response.status_code == 200
  33. return response.json()["cost"]
  34. @pytest.mark.asyncio
  35. @pytest.mark.integration
  36. async def test_recalculate_prices_one_print_of_an_archive_printed_twice(
  37. async_client: AsyncClient, archive_factory, printer_factory, db_session
  38. ):
  39. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0)
  40. await _usage(db_session, archive_id, (100.0, 3.0), (100.0, 3.0))
  41. assert (await async_client.post("/api/v1/archives/recalculate-costs")).status_code == 200
  42. assert await _cost(async_client, archive_id) == 3.0
  43. @pytest.mark.asyncio
  44. @pytest.mark.integration
  45. async def test_recalculate_still_prices_untracked_grams_at_the_default(
  46. async_client: AsyncClient, archive_factory, printer_factory, db_session
  47. ):
  48. """One run, half the grams on an inventory spool (#1344): 50 g at the
  49. spool's 2.00 plus 50 g at the 25/kg default."""
  50. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0)
  51. await _usage(db_session, archive_id, (50.0, 2.0))
  52. assert (await async_client.post("/api/v1/archives/recalculate-costs")).status_code == 200
  53. assert await _cost(async_client, archive_id) == 3.25
  54. @pytest.mark.asyncio
  55. @pytest.mark.integration
  56. async def test_rescan_prices_one_print(
  57. async_client: AsyncClient, archive_factory, printer_factory, db_session, tmp_path, monkeypatch
  58. ):
  59. monkeypatch.setattr(settings, "base_dir", tmp_path)
  60. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0)
  61. (tmp_path / "archives/test").mkdir(parents=True)
  62. with zipfile.ZipFile(tmp_path / "archives/test/test_print.gcode.3mf", "w") as zf:
  63. zf.writestr("3D/3dmodel.model", "<model/>")
  64. await _usage(db_session, archive_id, (100.0, 3.0), (100.0, 3.0), (100.0, 3.0))
  65. assert (await async_client.post(f"/api/v1/archives/{archive_id}/rescan")).status_code == 200
  66. assert await _cost(async_client, archive_id) == 3.0
  67. @pytest.mark.asyncio
  68. @pytest.mark.integration
  69. async def test_editing_grams_prices_the_archive_and_its_run(
  70. async_client: AsyncClient, archive_factory, printer_factory, db_session
  71. ):
  72. """A print archived without its 3MF has neither grams nor cost (#1820)."""
  73. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=None)
  74. response = await async_client.patch(f"/api/v1/archives/{archive_id}", json={"filament_used_grams": 120})
  75. assert response.status_code == 200
  76. assert response.json()["cost"] == 3.0
  77. run = await db_session.scalar(select(PrintLogEntry).where(PrintLogEntry.archive_id == archive_id))
  78. await db_session.refresh(run)
  79. assert (run.filament_used_grams, run.cost) == (120, 3.0)
  80. @pytest.mark.asyncio
  81. @pytest.mark.integration
  82. async def test_a_cost_given_with_the_grams_is_kept(
  83. async_client: AsyncClient, archive_factory, printer_factory, db_session
  84. ):
  85. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0, cost=2.5)
  86. response = await async_client.patch(
  87. f"/api/v1/archives/{archive_id}", json={"filament_used_grams": 200, "cost": 9.99}
  88. )
  89. assert response.json()["cost"] == 9.99
  90. @pytest.mark.asyncio
  91. @pytest.mark.integration
  92. async def test_a_run_that_measured_its_own_grams_keeps_its_cost(
  93. async_client: AsyncClient, archive_factory, printer_factory, db_session
  94. ):
  95. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0, cost=2.5)
  96. run = await db_session.scalar(select(PrintLogEntry).where(PrintLogEntry.archive_id == archive_id))
  97. run.filament_used_grams, run.cost = 37.0, 1.11
  98. await db_session.commit()
  99. response = await async_client.patch(f"/api/v1/archives/{archive_id}", json={"filament_used_grams": 200})
  100. assert response.json()["cost"] == 5.0
  101. await db_session.refresh(run)
  102. assert (run.filament_used_grams, run.cost) == (37.0, 1.11)
  103. async def _runs(db_session, archive_id, *grams):
  104. for g in grams:
  105. db_session.add(PrintLogEntry(archive_id=archive_id, status="completed", filament_used_grams=g))
  106. await db_session.commit()
  107. @pytest.mark.asyncio
  108. @pytest.mark.integration
  109. async def test_recalculate_keeps_the_untracked_share_of_every_run(
  110. async_client: AsyncClient, archive_factory, printer_factory, db_session
  111. ):
  112. """Printed three times, half of each 100 g run on a 60/kg spool: one print
  113. is 50 g at 0.06 plus 50 g at the 25/kg default, as completion priced it."""
  114. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0)
  115. await _runs(db_session, archive_id, 100.0, 100.0)
  116. await _usage(db_session, archive_id, (50.0, 3.0), (50.0, 3.0), (50.0, 3.0))
  117. assert (await async_client.post("/api/v1/archives/recalculate-costs")).status_code == 200
  118. assert await _cost(async_client, archive_id) == 4.25
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_recalculate_prices_unpriced_usage_at_the_default(
  122. async_client: AsyncClient, archive_factory, printer_factory, db_session
  123. ):
  124. """A usage row recorded without a price is not free filament."""
  125. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=100.0)
  126. await _usage(db_session, archive_id, (50.0, 2.0), (50.0, None))
  127. assert (await async_client.post("/api/v1/archives/recalculate-costs")).status_code == 200
  128. assert await _cost(async_client, archive_id) == 3.25
  129. @pytest.mark.asyncio
  130. @pytest.mark.integration
  131. async def test_recalculate_without_grams_prices_an_average_run(
  132. async_client: AsyncClient, archive_factory, printer_factory, db_session
  133. ):
  134. """Archived without its 3MF, so no grams: the spool usage of its two runs
  135. still prices one of them."""
  136. archive_id = await _archive(archive_factory, printer_factory, db_session, grams=None)
  137. await _runs(db_session, archive_id, None)
  138. await _usage(db_session, archive_id, (40.0, 2.0), (40.0, 2.0))
  139. assert (await async_client.post("/api/v1/archives/recalculate-costs")).status_code == 200
  140. assert await _cost(async_client, archive_id) == 2.0