supplier_links.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """Supplier-assignment inheritance for newly created spools (#2988).
  2. A new spool of a product that other spools already carry supplier
  3. assignments for should arrive knowing where it can be bought — whether it
  4. is created manually, via the API, or by the RFID auto-add. Only the source
  5. list is copied; ``is_purchase_source`` is deliberately reset, because where
  6. THIS spool was bought is not something the donor can know.
  7. """
  8. from sqlalchemy import select
  9. from sqlalchemy.ext.asyncio import AsyncSession
  10. from backend.app.models.spool import Spool
  11. from backend.app.models.supplier import SpoolSupplier
  12. async def find_supplier_link_templates_for_product(
  13. db: AsyncSession,
  14. *,
  15. material: str | None,
  16. subtype: str | None,
  17. brand: str | None,
  18. color_name: str | None,
  19. ) -> list[dict]:
  20. """Return supplier-link field dicts from the newest matching spool.
  21. Product identity is the (material, subtype, brand, color_name) string
  22. tuple — the same key FilamentSkuSettings groups by. Archived spools
  23. count as donors: a product being out of stock doesn't change where it
  24. can be bought.
  25. """
  26. if not material:
  27. return []
  28. def _same(column, value):
  29. return column.is_(None) if value is None else column == value
  30. donor = await db.execute(
  31. select(Spool.id)
  32. .join(SpoolSupplier, SpoolSupplier.spool_id == Spool.id)
  33. .where(
  34. Spool.material == material,
  35. _same(Spool.subtype, subtype),
  36. _same(Spool.brand, brand),
  37. _same(Spool.color_name, color_name),
  38. )
  39. .order_by(Spool.updated_at.desc())
  40. .limit(1)
  41. )
  42. donor_id = donor.scalars().first()
  43. if donor_id is None:
  44. return []
  45. links = await db.execute(select(SpoolSupplier).where(SpoolSupplier.spool_id == donor_id))
  46. return [
  47. {
  48. "supplier_id": link.supplier_id,
  49. "supplier_article_number": link.supplier_article_number,
  50. "quoted_price_per_kg": link.quoted_price_per_kg,
  51. "is_purchase_source": False,
  52. }
  53. for link in links.scalars().all()
  54. ]
  55. async def apply_supplier_inheritance(db: AsyncSession, spool: Spool) -> None:
  56. """Copy supplier assignments onto a freshly created (flushed) spool.
  57. No-op when the spool already has assignments. The caller commits.
  58. """
  59. existing = await db.execute(select(SpoolSupplier.id).where(SpoolSupplier.spool_id == spool.id).limit(1))
  60. if existing.first() is not None:
  61. return
  62. templates = await find_supplier_link_templates_for_product(
  63. db,
  64. material=spool.material,
  65. subtype=spool.subtype,
  66. brand=spool.brand,
  67. color_name=spool.color_name,
  68. )
  69. _attach(db, spool, templates)
  70. async def apply_supplier_inheritance_to_batch(db: AsyncSession, spools: list[Spool]) -> None:
  71. """Inheritance for a bulk create: one donor lookup for the whole batch.
  72. Every copy is built from the same payload, so the product tuple — and with
  73. it the answer — is identical for all of them. The caller commits.
  74. """
  75. if not spools:
  76. return
  77. first = spools[0]
  78. templates = await find_supplier_link_templates_for_product(
  79. db,
  80. material=first.material,
  81. subtype=first.subtype,
  82. brand=first.brand,
  83. color_name=first.color_name,
  84. )
  85. for spool in spools:
  86. _attach(db, spool, templates)
  87. def _attach(db: AsyncSession, spool: Spool, templates: list[dict]) -> None:
  88. for template in templates:
  89. db.add(SpoolSupplier(spool_id=spool.id, **template))