| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- from __future__ import annotations
- from decimal import Decimal
- from django.core.management.base import BaseCommand
- from django.contrib.auth import get_user_model
- from django.utils import timezone
- from orgs.models import Organization, UserProfile
- from recycle_core.models import (
- MaterialCategory,
- Material,
- ProvidedService,
- PriceList,
- PriceListItem,
- Customer,
- CustomerSite,
- PickupOrder,
- PickupItem,
- WeighTicket,
- WeighLine,
- ScrapListing,
- ScrapListingItem,
- ScrapBid,
- )
- from recycle_core.services.billing import generate_invoice_for_pickup
- User = get_user_model()
- class Command(BaseCommand):
- help = "Seed demo data for Ecoloop: org, materials, price list, customer, pickup, weigh ticket, invoice"
- def add_arguments(self, parser):
- parser.add_argument("--org", default="DEMO", help="Organization code/id/name to seed (default: DEMO)")
- parser.add_argument("--bidder-org", dest="bidder_org", default="REC1", help="Bidder org code/id/name (default: REC1)")
- parser.add_argument("--reset", action="store_true", help="Delete existing data for the target orgs before seeding")
- def handle(self, *args, **options):
- now = timezone.now()
- def _resolve_org(ident: str, *, default_name: str) -> Organization:
- if ident and ident.isdigit():
- org = Organization.objects.filter(pk=int(ident)).first()
- if org:
- return org
- org = (
- Organization.objects.filter(code=ident).first()
- or Organization.objects.filter(name=ident).first()
- )
- if org:
- return org
- # Create with defaults if not found
- return Organization.objects.create(code=ident, name=default_name, timezone="UTC", currency_code="THB")
- org_ident = options.get("org") or "DEMO"
- bidder_ident = options.get("bidder_org") or "REC1"
- org = _resolve_org(org_ident, default_name=("Ecoloop " + str(org_ident)))
- bidder_org = _resolve_org(bidder_ident, default_name="Recycler Co.")
- # Optionally reset existing demo data (scoped to the selected orgs)
- if options.get("reset"):
- from recycle_core.models import (
- ScrapAward,
- ScrapBid,
- ScrapListingInvite,
- ScrapListingItem,
- ScrapListing,
- WeighLine,
- WeighTicket,
- PickupItem,
- PickupOrder,
- InvoiceLine,
- Invoice,
- Payment,
- Payout,
- ServiceAgreement,
- CustomerSite,
- Customer,
- PriceListItem,
- PriceList,
- Material,
- MaterialCategory,
- ProvidedService,
- )
- def _wipe_for(o: Organization):
- # Marketplace
- ScrapAward.objects.filter(listing__organization=o).delete()
- ScrapBid.objects.filter(listing__organization=o).delete()
- ScrapListingInvite.objects.filter(listing__organization=o).delete()
- ScrapListingItem.objects.filter(listing__organization=o).delete()
- ScrapListing.objects.filter(organization=o).delete()
- # Operations
- WeighLine.objects.filter(ticket__pickup__organization=o).delete()
- WeighTicket.objects.filter(pickup__organization=o).delete()
- PickupItem.objects.filter(pickup__organization=o).delete()
- PickupOrder.objects.filter(organization=o).delete()
- # Billing
- InvoiceLine.objects.filter(invoice__organization=o).delete()
- Payment.objects.filter(invoice__organization=o).delete()
- Invoice.objects.filter(organization=o).delete()
- Payout.objects.filter(organization=o).delete()
- # Customers and agreements
- ServiceAgreement.objects.filter(customer__organization=o).delete()
- CustomerSite.objects.filter(customer__organization=o).delete()
- Customer.objects.filter(organization=o).delete()
- # Pricing
- PriceListItem.objects.filter(price_list__organization=o).delete()
- PriceList.objects.filter(organization=o).delete()
- # Inventory and services
- Material.objects.filter(organization=o).delete()
- ProvidedService.objects.filter(organization=o).delete()
- MaterialCategory.objects.filter(organization=o).delete()
- _wipe_for(org)
- _wipe_for(bidder_org)
- self.stdout.write(self.style.WARNING("Existing data removed for selected orgs (reset)."))
- # Users
- manager = User.objects.filter(username="manager").first()
- if not manager:
- manager = User.objects.create_user(username="manager", email="manager@example.com", password="manager123")
- driver = User.objects.filter(username="driver").first()
- if not driver:
- driver = User.objects.create_user(username="driver", email="driver@example.com", password="driver123")
- buyer = User.objects.filter(username="buyer").first()
- if not buyer:
- buyer = User.objects.create_user(username="buyer", email="buyer@example.com", password="buyer123")
- # Ensure recycle_core user profiles and roles
- UserProfile.objects.get_or_create(user=manager, defaults={"organization": org, "role": UserProfile.ROLE_MANAGER})
- UserProfile.objects.get_or_create(user=driver, defaults={"organization": org, "role": UserProfile.ROLE_DRIVER})
- UserProfile.objects.get_or_create(user=buyer, defaults={"organization": bidder_org, "role": UserProfile.ROLE_MANAGER})
- # Materials and categories
- plastics, _ = MaterialCategory.objects.get_or_create(organization=org, name="Plastics")
- metals, _ = MaterialCategory.objects.get_or_create(organization=org, name="Metals")
- paper, _ = MaterialCategory.objects.get_or_create(organization=org, name="Paper")
- pet, _ = Material.objects.get_or_create(organization=org, category="Plastics", name="PET", defaults={"default_unit": Material.UNIT_KG})
- hdpe, _ = Material.objects.get_or_create(organization=org, category="Plastics", name="HDPE", defaults={"default_unit": Material.UNIT_KG})
- can, _ = Material.objects.get_or_create(organization=org, category="Metals", name="Aluminum Can", defaults={"default_unit": Material.UNIT_KG})
- cardboard, _ = Material.objects.get_or_create(organization=org, category="Paper", name="Cardboard", defaults={"default_unit": Material.UNIT_KG})
- # Price list
- pl, _ = PriceList.objects.get_or_create(
- organization=org,
- name="Standard",
- defaults={"currency_code": "THB"},
- )
- # Sell prices (invoice customer)
- PriceListItem.objects.get_or_create(price_list=pl, material=pet, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("5.00")})
- PriceListItem.objects.get_or_create(price_list=pl, material=hdpe, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("4.00")})
- PriceListItem.objects.get_or_create(price_list=pl, material=can, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("12.00")})
- PriceListItem.objects.get_or_create(price_list=pl, material=cardboard, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_SELL, defaults={"unit_price": Decimal("2.00")})
- # Buy prices (pay customer)
- PriceListItem.objects.get_or_create(price_list=pl, material=pet, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_BUY, defaults={"unit_price": Decimal("1.50")})
- PriceListItem.objects.get_or_create(price_list=pl, material=hdpe, unit=Material.UNIT_KG, direction=PriceListItem.DIRECTION_BUY, defaults={"unit_price": Decimal("1.20")})
- # Customer and site
- customer, _ = Customer.objects.get_or_create(
- organization=org,
- name="Acme Factory",
- defaults={
- "email": "ops@acme.example",
- "phone": "+66 000 0000",
- "billing_address": "123 Demo Rd, Bangkok",
- "price_list": pl,
- },
- )
- site, _ = CustomerSite.objects.get_or_create(
- customer=customer,
- name="Acme Plant #1",
- defaults={
- "address": "123 Demo Rd, Bangkok",
- "contact_name": "Somchai",
- "contact_phone": "+66 111 1111",
- "contact_email": "somchai@acme.example",
- },
- )
- # Provided services for the public website
- demo_services = [
- ("Pickup & Logistics", "Scheduled and on-demand scrap pickups handled safely and on time.",
- "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"),
- ("Material Sorting", "Sorting and consolidation to maximize recycling value.",
- "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"),
- ("Weighing & Ticketing", "Accurate weighing with digital tickets and audit trail.",
- "Every pickup is weighed with calibrated equipment and recorded.\n\n- Calibrated scale records\n- Digital weigh tickets\n- Audit logs"),
- ("Invoicing & Payouts", "Transparent invoices and fast payouts.",
- "Automated invoicing and payouts reduce admin overhead.\n\n- Invoice generation\n- Payment tracking\n- Reconciliations"),
- ("Reporting & Analytics", "Reports that track volumes, value, and sustainability.",
- "Dashboards keep stakeholders informed.\n\n- Material volumes\n- Revenue and cost\n- ESG metrics"),
- ("Marketplace & Bidding", "Invite vetted recyclers and get competitive bids.",
- "Run open or sealed listings to find the best offer.\n\n- Public or invite-only\n- Bid history\n- Award workflows"),
- ("Compliance & Audits", "Documentation and controls for compliance.",
- "Stay compliant with audit-ready records.\n\n- Document control\n- Chain of custody\n- Access controls"),
- ("Consulting & Training", "Best practices and training for your team.",
- "Improve recycling outcomes with training and SOPs.\n\n- SOP development\n- Staff workshops\n- Continuous improvement"),
- ]
- for idx, (title, desc, body) in enumerate(demo_services):
- ProvidedService.objects.get_or_create(
- organization=org,
- title=title,
- defaults={
- "description": desc,
- "body": body,
- "display_order": idx,
- "is_enabled": True,
- },
- )
- pickup = PickupOrder.objects.create(
- organization=org,
- customer=customer,
- site=site,
- status=PickupOrder.STATUS_SCHEDULED,
- scheduled_at=now + timezone.timedelta(days=1),
- assigned_driver=driver,
- created_by=manager,
- notes="Demo pickup order",
- )
- PickupItem.objects.create(pickup=pickup, material=pet, estimated_qty=Decimal("100.0"), unit=Material.UNIT_KG)
- PickupItem.objects.create(pickup=pickup, material=can, estimated_qty=Decimal("50.0"), unit=Material.UNIT_KG)
- ticket = WeighTicket.objects.create(
- pickup=pickup,
- ticket_number=f"WT-{pickup.id}",
- gross_weight=Decimal("200.000"),
- tare_weight=Decimal("40.000"),
- net_weight=Decimal("160.000"),
- unit=Material.UNIT_KG,
- recorded_by=manager,
- )
- WeighLine.objects.create(ticket=ticket, material=pet, quantity=Decimal("110.000"), unit=Material.UNIT_KG)
- WeighLine.objects.create(ticket=ticket, material=can, quantity=Decimal("50.000"), unit=Material.UNIT_KG)
- pickup.status = PickupOrder.STATUS_WEIGHED
- pickup.save(update_fields=["status"])
- invoice = generate_invoice_for_pickup(pickup)
- # Create a demo scrap listing and a bid
- listing = ScrapListing.objects.create(
- organization=org,
- customer=customer,
- site=site,
- title="Monthly PET + Cans lot",
- description="Estimated quantities of PET and aluminum cans available",
- auction_type=ScrapListing.TYPE_OPEN,
- currency_code="THB",
- reserve_price=Decimal("500.00"),
- min_increment=Decimal("50.00"),
- status=ScrapListing.STATUS_OPEN,
- is_public=False,
- starts_at=now,
- created_by=manager,
- )
- ScrapListingItem.objects.create(listing=listing, material=pet, quantity_estimate=Decimal("100.0"), unit=Material.UNIT_KG)
- ScrapListingItem.objects.create(listing=listing, material=can, quantity_estimate=Decimal("50.0"), unit=Material.UNIT_KG)
- # Invite-only demo: invite bidder_org then place bid
- from recycle_core.models import ScrapListingInvite
- ScrapListingInvite.objects.get_or_create(listing=listing, invited_org=bidder_org, invited_user=buyer)
- ScrapBid.objects.create(listing=listing, bidder_org=bidder_org, bidder_user=buyer, price_total=Decimal("550.00"), message="Ready to collect within 48h")
- self.stdout.write(self.style.SUCCESS("Seeded Ecoloop demo data"))
- self.stdout.write(f"Organization: {org.name} ({org.code})")
- self.stdout.write(f"Customer: {customer.name}")
- self.stdout.write(f"Pickup: {pickup.id} status={pickup.status}")
- self.stdout.write(f"WeighTicket: {ticket.ticket_number}")
- self.stdout.write(f"Invoice: {invoice.id} total={invoice.total_amount} {invoice.currency_code}")
- self.stdout.write(f"Scrap Listing: {listing.id} status={listing.status}")
|