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, ServiceAgreement, PickupOrder, PickupItem, WeighTicket, WeighLine, ScrapListing, ScrapListingItem, ScrapBid, ScrapListingInvite, ScrapAward, ) 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 billing.models import Invoice, InvoiceLine, Payment, Payout 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 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}")