Nenhuma Descrição

seed_ecoloop.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. from __future__ import annotations
  2. from decimal import Decimal
  3. from django.core.management.base import BaseCommand
  4. from django.contrib.auth import get_user_model
  5. from django.utils import timezone
  6. from orgs.models import Organization, UserProfile
  7. from recycle_core.models import (
  8. MaterialCategory,
  9. Material,
  10. ProvidedService,
  11. PriceList,
  12. PriceListItem,
  13. Customer,
  14. CustomerSite,
  15. PickupOrder,
  16. PickupItem,
  17. WeighTicket,
  18. WeighLine,
  19. ScrapListing,
  20. ScrapListingItem,
  21. ScrapBid,
  22. )
  23. from recycle_core.services.billing import generate_invoice_for_pickup
  24. User = get_user_model()
  25. class Command(BaseCommand):
  26. help = "Seed demo data for Ecoloop: org, materials, price list, customer, pickup, weigh ticket, invoice"
  27. def add_arguments(self, parser):
  28. parser.add_argument("--org", default="DEMO", help="Organization code/id/name to seed (default: DEMO)")
  29. parser.add_argument("--bidder-org", dest="bidder_org", default="REC1", help="Bidder org code/id/name (default: REC1)")
  30. def handle(self, *args, **options):
  31. now = timezone.now()
  32. def _resolve_org(ident: str, *, default_name: str) -> Organization:
  33. if ident and ident.isdigit():
  34. org = Organization.objects.filter(pk=int(ident)).first()
  35. if org:
  36. return org
  37. org = (
  38. Organization.objects.filter(code=ident).first()
  39. or Organization.objects.filter(name=ident).first()
  40. )
  41. if org:
  42. return org
  43. # Create with defaults if not found
  44. return Organization.objects.create(code=ident, name=default_name, timezone="UTC", currency_code="THB")
  45. org_ident = options.get("org") or "DEMO"
  46. bidder_ident = options.get("bidder_org") or "REC1"
  47. org = _resolve_org(org_ident, default_name=("Ecoloop " + str(org_ident)))
  48. bidder_org = _resolve_org(bidder_ident, default_name="Recycler Co.")
  49. # Users
  50. manager = User.objects.filter(username="manager").first()
  51. if not manager:
  52. manager = User.objects.create_user(username="manager", email="manager@example.com", password="manager123")
  53. driver = User.objects.filter(username="driver").first()
  54. if not driver:
  55. driver = User.objects.create_user(username="driver", email="driver@example.com", password="driver123")
  56. buyer = User.objects.filter(username="buyer").first()
  57. if not buyer:
  58. buyer = User.objects.create_user(username="buyer", email="buyer@example.com", password="buyer123")
  59. # Ensure recycle_core user profiles and roles
  60. UserProfile.objects.get_or_create(user=manager, defaults={"organization": org, "role": UserProfile.ROLE_MANAGER})
  61. UserProfile.objects.get_or_create(user=driver, defaults={"organization": org, "role": UserProfile.ROLE_DRIVER})
  62. UserProfile.objects.get_or_create(user=buyer, defaults={"organization": bidder_org, "role": UserProfile.ROLE_MANAGER})
  63. # Materials and categories
  64. plastics, _ = MaterialCategory.objects.get_or_create(organization=org, name="Plastics")
  65. metals, _ = MaterialCategory.objects.get_or_create(organization=org, name="Metals")
  66. paper, _ = MaterialCategory.objects.get_or_create(organization=org, name="Paper")
  67. pet, _ = Material.objects.get_or_create(organization=org, category=plastics, name="PET", defaults={"default_unit": Material.UNIT_KG})
  68. hdpe, _ = Material.objects.get_or_create(organization=org, category=plastics, name="HDPE", defaults={"default_unit": Material.UNIT_KG})
  69. can, _ = Material.objects.get_or_create(organization=org, category=metals, name="Aluminum Can", defaults={"default_unit": Material.UNIT_KG})
  70. cardboard, _ = Material.objects.get_or_create(organization=org, category=paper, name="Cardboard", defaults={"default_unit": Material.UNIT_KG})
  71. # Price list
  72. pl, _ = PriceList.objects.get_or_create(
  73. organization=org,
  74. name="Standard",
  75. defaults={"currency_code": "THB"},
  76. )
  77. # Sell prices (invoice customer)
  78. PriceListItem.objects.get_or_create(price_list=pl, material=pet, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("5.00")})
  79. PriceListItem.objects.get_or_create(price_list=pl, material=hdpe, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("4.00")})
  80. PriceListItem.objects.get_or_create(price_list=pl, material=can, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("12.00")})
  81. PriceListItem.objects.get_or_create(price_list=pl, material=cardboard, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("2.00")})
  82. # Buy prices (pay customer)
  83. PriceListItem.objects.get_or_create(price_list=pl, material=pet, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_BUY, defaults={"unit_price": Decimal("1.50")})
  84. PriceListItem.objects.get_or_create(price_list=pl, material=hdpe, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_BUY, defaults={"unit_price": Decimal("1.20")})
  85. # Customer and site
  86. customer, _ = Customer.objects.get_or_create(
  87. organization=org,
  88. name="Acme Factory",
  89. defaults={
  90. "email": "ops@acme.example",
  91. "phone": "+66 000 0000",
  92. "billing_address": "123 Demo Rd, Bangkok",
  93. "price_list": pl,
  94. },
  95. )
  96. site, _ = CustomerSite.objects.get_or_create(
  97. customer=customer,
  98. name="Acme Plant #1",
  99. defaults={
  100. "address": "123 Demo Rd, Bangkok",
  101. "contact_name": "Somchai",
  102. "contact_phone": "+66 111 1111",
  103. "contact_email": "somchai@acme.example",
  104. },
  105. )
  106. # Provided services for the public website
  107. demo_services = [
  108. ("Pickup & Logistics", "Scheduled and on-demand scrap pickups handled safely and on time.",
  109. "We provide reliable pickup scheduling, routing, and documentation for your facilities.\n\n- Route planning and dispatch\n- On-demand requests\n- Driver assignments and tracking"),
  110. ("Material Sorting", "Sorting and consolidation to maximize recycling value.",
  111. "Our team sorts materials to your specifications to improve purity and value.\n\n- On-site sorting support\n- Bale and bag standards\n- Quality checks"),
  112. ("Weighing & Ticketing", "Accurate weighing with digital tickets and audit trail.",
  113. "Every pickup is weighed with calibrated equipment and recorded.\n\n- Calibrated scale records\n- Digital weigh tickets\n- Audit logs"),
  114. ("Invoicing & Payouts", "Transparent invoices and fast payouts.",
  115. "Automated invoicing and payouts reduce admin overhead.\n\n- Invoice generation\n- Payment tracking\n- Reconciliations"),
  116. ("Reporting & Analytics", "Reports that track volumes, value, and sustainability.",
  117. "Dashboards keep stakeholders informed.\n\n- Material volumes\n- Revenue and cost\n- ESG metrics"),
  118. ("Marketplace & Bidding", "Invite vetted recyclers and get competitive bids.",
  119. "Run open or sealed listings to find the best offer.\n\n- Public or invite-only\n- Bid history\n- Award workflows"),
  120. ("Compliance & Audits", "Documentation and controls for compliance.",
  121. "Stay compliant with audit-ready records.\n\n- Document control\n- Chain of custody\n- Access controls"),
  122. ("Consulting & Training", "Best practices and training for your team.",
  123. "Improve recycling outcomes with training and SOPs.\n\n- SOP development\n- Staff workshops\n- Continuous improvement"),
  124. ]
  125. for idx, (title, desc, body) in enumerate(demo_services):
  126. ProvidedService.objects.get_or_create(
  127. organization=org,
  128. title=title,
  129. defaults={
  130. "description": desc,
  131. "body": body,
  132. "display_order": idx,
  133. "is_enabled": True,
  134. },
  135. )
  136. pickup = PickupOrder.objects.create(
  137. organization=org,
  138. customer=customer,
  139. site=site,
  140. status=PickupOrder.STATUS_SCHEDULED,
  141. scheduled_at=now + timezone.timedelta(days=1),
  142. assigned_driver=driver,
  143. created_by=manager,
  144. notes="Demo pickup order",
  145. )
  146. PickupItem.objects.create(pickup=pickup, material=pet, estimated_qty=Decimal("100.0"), unit=Material.UNIT_KG)
  147. PickupItem.objects.create(pickup=pickup, material=can, estimated_qty=Decimal("50.0"), unit=Material.UNIT_KG)
  148. ticket = WeighTicket.objects.create(
  149. pickup=pickup,
  150. ticket_number=f"WT-{pickup.id}",
  151. gross_weight=Decimal("200.000"),
  152. tare_weight=Decimal("40.000"),
  153. net_weight=Decimal("160.000"),
  154. unit=Material.UNIT_KG,
  155. recorded_by=manager,
  156. )
  157. WeighLine.objects.create(ticket=ticket, material=pet, quantity=Decimal("110.000"), unit=Material.UNIT_KG)
  158. WeighLine.objects.create(ticket=ticket, material=can, quantity=Decimal("50.000"), unit=Material.UNIT_KG)
  159. pickup.status = PickupOrder.STATUS_WEIGHED
  160. pickup.save(update_fields=["status"])
  161. invoice = generate_invoice_for_pickup(pickup)
  162. # Create a demo scrap listing and a bid
  163. listing = ScrapListing.objects.create(
  164. organization=org,
  165. customer=customer,
  166. site=site,
  167. title="Monthly PET + Cans lot",
  168. description="Estimated quantities of PET and aluminum cans available",
  169. auction_type=ScrapListing.TYPE_OPEN,
  170. currency_code="THB",
  171. reserve_price=Decimal("500.00"),
  172. min_increment=Decimal("50.00"),
  173. status=ScrapListing.STATUS_OPEN,
  174. is_public=False,
  175. starts_at=now,
  176. created_by=manager,
  177. )
  178. ScrapListingItem.objects.create(listing=listing, material=pet, quantity_estimate=Decimal("100.0"), unit=Material.UNIT_KG)
  179. ScrapListingItem.objects.create(listing=listing, material=can, quantity_estimate=Decimal("50.0"), unit=Material.UNIT_KG)
  180. # Invite-only demo: invite bidder_org then place bid
  181. from recycle_core.models import ScrapListingInvite
  182. ScrapListingInvite.objects.get_or_create(listing=listing, invited_org=bidder_org, invited_user=buyer)
  183. ScrapBid.objects.create(listing=listing, bidder_org=bidder_org, bidder_user=buyer, price_total=Decimal("550.00"), message="Ready to collect within 48h")
  184. self.stdout.write(self.style.SUCCESS("Seeded Ecoloop demo data"))
  185. self.stdout.write(f"Organization: {org.name} ({org.code})")
  186. self.stdout.write(f"Customer: {customer.name}")
  187. self.stdout.write(f"Pickup: {pickup.id} status={pickup.status}")
  188. self.stdout.write(f"WeighTicket: {ticket.ticket_number}")
  189. self.stdout.write(f"Invoice: {invoice.id} total={invoice.total_amount} {invoice.currency_code}")
  190. self.stdout.write(f"Scrap Listing: {listing.id} status={listing.status}")