material_number.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. """Material-number inheritance for newly created spools (#2870).
  2. The material number is the internal purchasing/article identifier a business
  3. costs by (e.g. "15" = Bambu Lab PLA Basic). All spools of the same product
  4. share it, so a new spool of an already-numbered product should arrive with
  5. the number filled instead of blank — whether it is created manually, via the
  6. API, or by the RFID auto-add.
  7. """
  8. from sqlalchemy import select
  9. from sqlalchemy.ext.asyncio import AsyncSession
  10. from backend.app.models.spool import Spool
  11. async def find_material_number_for_product(
  12. db: AsyncSession,
  13. *,
  14. material: str | None,
  15. subtype: str | None,
  16. brand: str | None,
  17. color_name: str | None,
  18. ) -> str | None:
  19. """Return the material number an existing spool of this product carries.
  20. Product identity is the (material, subtype, brand, color_name) string
  21. tuple — the same key FilamentSkuSettings groups by. The most recently
  22. updated match wins, newest row on a tie. `updated_at` moves on every
  23. write, usage included, so this is "most recently touched", not "most
  24. recently numbered": when a product's spools disagree, bulk-edit them to
  25. one number rather than relying on which one wins. Archived spools count:
  26. a product being out of stock doesn't change its number.
  27. """
  28. if not material:
  29. return None
  30. def _same(column, value):
  31. return column.is_(None) if value is None else column == value
  32. result = await db.execute(
  33. select(Spool.material_number)
  34. .where(
  35. # Normalised to NULL-or-non-empty by the schema validator, so
  36. # NULL is the only "unset" state left to exclude.
  37. Spool.material_number.is_not(None),
  38. Spool.material == material,
  39. _same(Spool.subtype, subtype),
  40. _same(Spool.brand, brand),
  41. _same(Spool.color_name, color_name),
  42. )
  43. .order_by(Spool.updated_at.desc(), Spool.id.desc())
  44. .limit(1)
  45. )
  46. return result.scalars().first()
  47. async def apply_material_number_inheritance(db: AsyncSession, payload: dict) -> dict:
  48. """Fill ``payload["material_number"]`` from a matching existing spool.
  49. No-op when the caller already supplied a non-empty number. Only used on
  50. the create paths — editing a spool never overwrites what the user set.
  51. """
  52. if payload.get("material_number"):
  53. return payload
  54. number = await find_material_number_for_product(
  55. db,
  56. material=payload.get("material"),
  57. subtype=payload.get("subtype"),
  58. brand=payload.get("brand"),
  59. color_name=payload.get("color_name"),
  60. )
  61. if number:
  62. payload = dict(payload)
  63. payload["material_number"] = number
  64. return payload