* Vietnamese translation incomplete

Tour Operator Models

Tour Operator Models (English fallback)

Aug. 17, 2025

Posted by admin

 

from django.db import models

from django.conf import settings

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

from django.contrib.contenttypes.models import ContentType

from django.core.validators import MinValueValidator, MaxValueValidator

from django.urls import reverse

from django.utils.translation import gettext_lazy as _

from decimal import Decimal

import uuid

import time

import os

 

def passport_upload_path(instance, filename):

    """Generate unique upload path for passport images"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'jpg'

   

    # Create unique filename with participant name and timestamp

    safe_name = f"{instance.first_name}_{instance.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"passport_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('passport', unique_filename)

 

def guide_certification_upload_path(instance, filename):

    """Generate unique upload path for guide certification documents"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'pdf'

   

    # Create unique filename with guide name and timestamp

    safe_name = f"{instance.user.first_name}_{instance.user.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"certification_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('guide_certifications', unique_filename)

 

def guide_license_upload_path(instance, filename):

    """Generate unique upload path for guide license documents"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'pdf'

   

    # Create unique filename with guide name and timestamp

    safe_name = f"{instance.user.first_name}_{instance.user.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"license_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('guide_licenses', unique_filename)

import uuid

 

 

 

 

Booking

class TourBooking(models.Model):

    """Tour bookings by customers"""

    BOOKING_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('confirmed', 'Confirmed'),

        ('paid', 'Paid'),

        ('cancelled', 'Cancelled'),

        ('completed', 'Completed'),

        ('refunded', 'Refunded'),

    ]

   

    PAYMENT_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('partial', 'Partially Paid'),

        ('paid', 'Fully Paid'),

        ('refunded', 'Refunded'),

    ]

 

    # Booking identifier

    booking_number = models.CharField(max_length=20, unique=True)

   

    # Tour and Schedule

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='bookings')

    schedule = models.ForeignKey(TourSchedule, on_delete=models.CASCADE, related_name='bookings')

   

    # Customer Information

    customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='tour_bookings')

    lead_traveler_name = models.CharField(max_length=200)

    lead_traveler_email = models.EmailField()

    lead_traveler_phone = models.CharField(max_length=20)

   

    # Participants

    adult_count = models.PositiveIntegerField(default=1)

    child_count = models.PositiveIntegerField(default=0)

    senior_count = models.PositiveIntegerField(default=0)

    total_participants = models.PositiveIntegerField()

   

    # Pricing

    base_price = models.DecimalField(max_digits=10, decimal_places=2)

    total_price = models.DecimalField(max_digits=10, decimal_places=2)

    discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    commission_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

   

    # Status

    booking_status = models.CharField(max_length=20, choices=BOOKING_STATUS_CHOICES, default='pending')

    payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending')

   

    # Additional Information

    special_requests = models.TextField(blank=True)

    dietary_requirements = models.TextField(blank=True)

    accessibility_needs = models.TextField(blank=True)

   

    # External booking (for broker tours)

    external_booking_id = models.CharField(max_length=100, blank=True)

    external_confirmation = models.CharField(max_length=100, blank=True)

   

    # Flight Booking Integration

    flight_booking_required = models.BooleanField(default=False, verbose_name="Flight Booking Required")

    selected_flights = models.ManyToManyField('transport.Flight', blank=True,

                                            related_name='selected_for_tours',

                                            verbose_name="Selected Flights")

    flight_class = models.CharField(max_length=20, blank=True,

                                  choices=[('economy', 'Economy'), ('business', 'Business'), ('first', 'First')],

                                  default='economy', verbose_name="Flight Class")

    flight_total_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                          verbose_name="Total Flight Cost")

    flight_booking_status = models.CharField(max_length=20,

                                           choices=[

                                               ('pending', 'Pending'),

                                               ('confirmed', 'Confirmed'),

                                               ('failed', 'Failed'),

                                               ('cancelled', 'Cancelled')

                                           ],

                                           default='pending',

                                           verbose_name="Flight Booking Status")

    flight_confirmation_number = models.CharField(max_length=100, blank=True,

                                                 verbose_name="Flight Confirmation Number")

    flight_notes = models.TextField(blank=True, verbose_name="Flight Booking Notes")

   

    # Timestamps

    booking_date = models.DateTimeField(auto_now_add=True)

    confirmation_date = models.DateTimeField(null=True, blank=True)

    cancellation_date = models.DateTimeField(null=True, blank=True)

   

    # Staff assignment

    assigned_guide = models.ForeignKey(TourGuide, on_delete=models.SET_NULL, null=True, blank=True)

    sales_agent = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,

                                  null=True, blank=True, related_name='tour_sales')

 

    class Meta:

        ordering = ['-booking_date']

 

    def __str__(self):

        return f"Booking {self.booking_number} - {self.tour.title}"

 

    def save(self, *args, **kwargs):

        if not self.booking_number:

            # Generate unique booking number

            import time

            self.booking_number = f"TB{int(time.time())}"

        super().save(*args, **kwargs)

 

Booking Participant

class TourParticipant(models.Model):

    """Individual participants in a tour booking"""

    PARTICIPANT_TYPE_CHOICES = [

        ('adult', 'Adult'),

        ('child', 'Child'),

        ('senior', 'Senior'),

        ('infant', 'Infant'),

    ]

 

    booking = models.ForeignKey(TourBooking, on_delete=models.CASCADE, related_name='participants')

    participant_type = models.CharField(max_length=20, choices=PARTICIPANT_TYPE_CHOICES)

   

    # Personal Information

    first_name = models.CharField(max_length=100)

    last_name = models.CharField(max_length=100)

    date_of_birth = models.DateField(null=True, blank=True)

    nationality = models.CharField(max_length=100)

    passport_number = models.CharField(max_length=50, blank=True)

    passport_expiry = models.DateField(null=True, blank=True)

    passport_image = models.ImageField(upload_to=passport_upload_path, blank=True, null=True,

                                     help_text="Upload passport image for verification")

   

    # Special requirements

    dietary_requirements = models.TextField(blank=True)

    medical_conditions = models.TextField(blank=True)

    emergency_contact_name = models.CharField(max_length=200, blank=True)

    emergency_contact_phone = models.CharField(max_length=20, blank=True)

 

    class Meta:

        ordering = ['booking', 'id']

 

    def __str__(self):

        return f"{self.first_name} {self.last_name} - {self.booking.booking_number}"

 

Booking Participant flight

class TourFlightBooking(models.Model):

    """Individual flight bookings for tour participants"""

    BOOKING_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('confirmed', 'Confirmed'),

        ('ticketed', 'Ticketed'),

        ('cancelled', 'Cancelled'),

        ('refunded', 'Refunded'),

    ]

 

    tour_booking = models.ForeignKey(TourBooking, on_delete=models.CASCADE, related_name='flight_bookings')

    participant = models.ForeignKey(TourParticipant, on_delete=models.CASCADE, related_name='flight_bookings')

    flight = models.ForeignKey('transport.Flight', on_delete=models.CASCADE, related_name='passenger_bookings')

   

    # Booking details

    seat_class = models.CharField(max_length=20, choices=[

        ('economy', 'Economy'),

        ('premium_economy', 'Premium Economy'),

        ('business', 'Business'),

        ('first', 'First')

    ], default='economy')

    seat_number = models.CharField(max_length=10, blank=True)

    meal_preference = models.CharField(max_length=50, blank=True,

                                     help_text="Special meal requests")

   

    # Pricing

    base_fare = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    taxes_fees = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    total_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)

   

    # Status and references

    booking_status = models.CharField(max_length=20, choices=BOOKING_STATUS_CHOICES, default='pending')

    airline_booking_reference = models.CharField(max_length=50, blank=True)

    ticket_number = models.CharField(max_length=50, blank=True)

   

    # Special requirements

    special_requests = models.TextField(blank=True,

                                      help_text="Wheelchair assistance, extra legroom, etc.")

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['flight__departure_time']

        unique_together = ['tour_booking', 'participant', 'flight']

        verbose_name = 'Tour Flight Booking'

        verbose_name_plural = 'Tour Flight Bookings'

 

    def __str__(self):

        return f"{self.participant.first_name} {self.participant.last_name} - {self.flight}"

 

    def save(self, *args, **kwargs):

        # Calculate total cost

        if self.base_fare and self.taxes_fees:

            self.total_cost = self.base_fare + self.taxes_fees

        super().save(*args, **kwargs)

 

 

Category

class TourCategory(models.Model):

    """Categories for tours (e.g., Adventure, Cultural, Food, etc.)"""

    name = models.CharField(max_length=100)

    name_vi = models.CharField(max_length=100, blank=True)

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True)

    icon = models.CharField(max_length=50, blank=True, help_text="FontAwesome icon class")

    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')

    is_active = models.BooleanField(default=True)

   

    class Meta:

        ordering = ['name']

        verbose_name_plural = 'Tour Categories'

 

    def __str__(self):

        if self.parent:

            return f"{self.parent.name} > {self.name}"

        return self.name

 

Link Apps

Transport

class TourFlight(models.Model):

    """Flight options and requirements for tours"""

    FLIGHT_TYPE_CHOICES = [

        ('outbound', 'Outbound Flight'),

        ('return', 'Return Flight'),

        ('internal', 'Internal Flight'),

        ('connecting', 'Connecting Flight'),

    ]

   

    FLIGHT_STATUS_CHOICES = [

        ('available', 'Available'),

        ('unavailable', 'Unavailable'),

        ('fully_booked', 'Fully Booked'),

        ('cancelled', 'Cancelled'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='flight_options')

    flight = models.ForeignKey('transport.Flight', on_delete=models.CASCADE, related_name='tour_flight_options')

    flight_type = models.CharField(max_length=20, choices=FLIGHT_TYPE_CHOICES, default='outbound')

   

    # Booking details

    is_included_in_price = models.BooleanField(default=False, verbose_name="Included in Tour Price")

    additional_cost_economy = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                                verbose_name="Economy Class Additional Cost")

    additional_cost_business = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                                 verbose_name="Business Class Additional Cost")

    additional_cost_first = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                              verbose_name="First Class Additional Cost")

   

    # Availability and requirements

    booking_deadline = models.DateField(null=True, blank=True, verbose_name="Booking Deadline")

    min_passengers = models.PositiveIntegerField(default=1, verbose_name="Minimum Passengers")

    max_passengers = models.PositiveIntegerField(default=50, verbose_name="Maximum Passengers")

    status = models.CharField(max_length=20, choices=FLIGHT_STATUS_CHOICES, default='available')

   

    # Additional info

    notes_en = models.TextField(blank=True, verbose_name="English Notes")

    notes_vi = models.TextField(blank=True, verbose_name="Vietnamese Notes")

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['flight__departure_time', 'flight_type']

        unique_together = ['tour', 'flight', 'flight_type']

        verbose_name = 'Tour Flight Option'

        verbose_name_plural = 'Tour Flight Options'

 

    def __str__(self):

        return f"{self.tour.title} - {self.get_flight_type_display()}: {self.flight}"

 

    @property

    def is_mandatory(self):

        """Return True if this flight is required for the tour"""

        return self.tour.requires_flights and self.flight_type in ['outbound', 'return']

 

Link apps

Destination

class TourAttraction(models.Model):

    """Attractions/activities included in tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='attractions')

    what_to_see = models.ForeignKey('destinations.WhatToSee', on_delete=models.CASCADE, null=True, blank=True)

   

    # Custom attraction details (if not linked to destinations app)

    name = models.CharField(max_length=200)

    description = models.TextField(blank=True)

    visit_day = models.PositiveIntegerField(help_text="Which day of the tour")

    visit_time = models.TimeField(null=True, blank=True)

    duration_minutes = models.PositiveIntegerField(null=True, blank=True)

   

    # Location

    address = models.CharField(max_length=300, blank=True)

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'})

   

    # Tickets and costs

    entrance_fee_included = models.BooleanField(default=True)

    ticket_price = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['visit_day', 'visit_time']

 

    def __str__(self):

        return f"{self.name} - Day {self.visit_day}"

 

Tour

Itinerary

class TourItinerary(models.Model):

    """Daily itinerary for tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='itinerary_days')

    day_number = models.PositiveIntegerField()

    title = models.CharField(max_length=200)

    title_vi = models.CharField(max_length=200, blank=True)

    description = models.TextField()

    description_vi = models.TextField(blank=True)

   

    # Activities for this day

    activities = models.TextField(blank=True, help_text="Comma-separated list of activities")

    meals_included = models.CharField(max_length=100, blank=True, help_text="e.g., Breakfast, Lunch")

    accommodation = models.CharField(max_length=200, blank=True)

   

    # Location

    location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                                limit_choices_to={'type': 'city'})

   

    class Meta:

        ordering = ['tour', 'day_number']

        unique_together = ['tour', 'day_number']

 

    def __str__(self):

        return f"{self.tour.title} - Day {self.day_number}: {self.title}"

 

Tour

class Tour(models.Model):

    """Main tour model that can be created by operators or fetched from external APIs"""

    TOUR_TYPE_CHOICES = [

        ('group', 'Group Tour'),

        ('private', 'Private Tour'),

        ('self_guided', 'Self-Guided Tour'),

        ('custom', 'Custom Tour'),

    ]

   

    DIFFICULTY_CHOICES = [

        ('easy', 'Easy'),

        ('moderate', 'Moderate'),

        ('challenging', 'Challenging'),

        ('extreme', 'Extreme'),

    ]

   

    TOUR_SOURCE_CHOICES = [

        ('inhouse', 'In-house Created'),

        ('viator', 'Viator'),

        ('getyourguide', 'GetYourGuide'),

        ('partner', 'Partner Operator'),

        ('api', 'External API'),

    ]

 

    # Basic Information

    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    title = models.CharField(max_length=200)

    title_vi = models.CharField(max_length=200, blank=True)

    slug = models.SlugField(max_length=220, unique=True)

    short_description = models.TextField(max_length=500)

    short_description_vi = models.TextField(max_length=500, blank=True)

    description = models.TextField()

    description_vi = models.TextField(blank=True)

   

    # Tour Details

    tour_operator = models.ForeignKey(TourOperator, on_delete=models.CASCADE, related_name='tours')

    categories = models.ManyToManyField(TourCategory, related_name='tours')

    tour_type = models.CharField(max_length=20, choices=TOUR_TYPE_CHOICES, default='group')

    difficulty = models.CharField(max_length=20, choices=DIFFICULTY_CHOICES, default='easy')

   

    # Duration and Scheduling

    duration_days = models.PositiveIntegerField(default=1)

    duration_hours = models.PositiveIntegerField(default=0, help_text="Additional hours beyond full days")

    start_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True,

                                     related_name='tours_starting_here', limit_choices_to={'type': 'city'})

    end_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True,

                                   related_name='tours_ending_here', limit_choices_to={'type': 'city'})

    destinations = models.ManyToManyField('destinations.Destination', related_name='tours', blank=True)

   

    # Flight Requirements (for overseas/international tours)

    requires_flights = models.BooleanField(default=False, verbose_name="Requires Flight Booking",

                                         help_text="Tour requires flight booking for participants")

    departure_airport = models.ForeignKey('transport.Airport', on_delete=models.SET_NULL,

                                        null=True, blank=True, related_name='tour_departures',

                                        verbose_name="Departure Airport")

    arrival_airport = models.ForeignKey('transport.Airport', on_delete=models.SET_NULL,

                                      null=True, blank=True, related_name='tour_arrivals',

                                      verbose_name="Destination Airport")

    return_flight_included = models.BooleanField(default=False, verbose_name="Return Flight Included")

    flight_class_options = models.CharField(max_length=100, blank=True, default="economy,business",

                                          help_text="Available flight classes (comma-separated): economy,business,first",

                                          verbose_name="Flight Class Options")

    flight_booking_deadline_days = models.PositiveIntegerField(default=14,

                                                             help_text="Days before tour to book flights",

                                                             verbose_name="Flight Booking Deadline (Days)")

   

    # Capacity and Logistics

    min_participants = models.PositiveIntegerField(default=1)

    max_participants = models.PositiveIntegerField(default=20)

    min_age = models.PositiveIntegerField(default=0)

    max_age = models.PositiveIntegerField(null=True, blank=True)

   

    # Pricing

    base_price = models.DecimalField(max_digits=10, decimal_places=2)

    price_currency = models.CharField(max_length=3, default='USD')

    child_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    senior_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    group_discount_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)

   

    # External Source Information

    source = models.CharField(max_length=20, choices=TOUR_SOURCE_CHOICES, default='inhouse')

    external_id = models.CharField(max_length=100, blank=True, help_text="ID from external system")

    external_url = models.URLField(blank=True, help_text="Original URL from external source")

   

    # Features and Inclusions

    includes_accommodation = models.BooleanField(default=False)

    includes_meals = models.BooleanField(default=False)

    includes_transport = models.BooleanField(default=False)

    includes_guide = models.BooleanField(default=False)

    includes_tickets = models.BooleanField(default=False)

   

    # Requirements

    fitness_level_required = models.CharField(max_length=20, choices=DIFFICULTY_CHOICES, default='easy')

    special_requirements = models.TextField(blank=True)

    what_to_bring = models.TextField(blank=True)

   

    # Media

    main_image = models.ImageField(upload_to='tours/main/', null=True, blank=True)

    video_url = models.URLField(blank=True)

   

    # Status and Management

    is_active = models.BooleanField(default=True)

    is_featured = models.BooleanField(default=False)

    is_available = models.BooleanField(default=True)

    requires_approval = models.BooleanField(default=False)

   

    # Statistics

    total_bookings = models.PositiveIntegerField(default=0)

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

   

    # Timestamps

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

    last_synced = models.DateTimeField(null=True, blank=True, help_text="Last sync from external API")

 

    # Generic relations for reviews

    reviews = GenericRelation('reviews.Review')

 

    class Meta:

        verbose_name = _("Tour")

        verbose_name_plural = _("Tours")

        ordering = ['-created_at']

        indexes = [

            models.Index(fields=['slug']),

            models.Index(fields=['source', 'external_id']),

            models.Index(fields=['is_active', 'is_available']),

        ]

 

    def __str__(self):

        return self.title

 

    def get_absolute_url(self):

        if not self.slug:

            # If slug is empty, generate one based on title or use ID

            from django.utils.text import slugify

            if self.title:

                self.slug = slugify(self.title)

                # Ensure uniqueness

                if Tour.objects.filter(slug=self.slug).exclude(id=self.id).exists():

                    self.slug = f"{self.slug}-{self.id}"

            else:

                self.slug = f"tour-{self.id}"

            self.save()

        return reverse('tour_operators:tour_detail', kwargs={'slug': self.slug})

 

    @property

    def duration_display(self):

        """Return formatted duration"""

        if self.duration_days == 0:

            return f"{self.duration_hours} hours"

        elif self.duration_hours == 0:

            return f"{self.duration_days} day{'s' if self.duration_days > 1 else ''}"

        else:

            return f"{self.duration_days} day{'s' if self.duration_days > 1 else ''} {self.duration_hours} hours"

 

Tour

class TourSchedule(models.Model):

    """Available dates and times for tours"""

    SCHEDULE_TYPE_CHOICES = [

        ('fixed', 'Fixed Date'),

        ('recurring', 'Recurring'),

        ('on_demand', 'On Demand'),

    ]

   

    DAY_OF_WEEK_CHOICES = [

        (0, 'Monday'),

        (1, 'Tuesday'),

        (2, 'Wednesday'),

        (3, 'Thursday'),

        (4, 'Friday'),

        (5, 'Saturday'),

        (6, 'Sunday'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='schedules')

    schedule_type = models.CharField(max_length=20, choices=SCHEDULE_TYPE_CHOICES, default='fixed')

   

    # For fixed dates

    start_date = models.DateField(null=True, blank=True)

    end_date = models.DateField(null=True, blank=True)

    start_time = models.TimeField(null=True, blank=True)

   

    # For recurring schedules

    days_of_week = models.JSONField(default=list, blank=True, help_text="List of days of week (0=Monday)")

   

    # Capacity management

    available_spots = models.PositiveIntegerField(default=0)

    booked_spots = models.PositiveIntegerField(default=0)

   

    # Pricing override for this schedule

    price_override = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

   

    is_active = models.BooleanField(default=True)

    notes = models.TextField(blank=True)

 

    class Meta:

        ordering = ['start_date', 'start_time']

 

    def __str__(self):

        return f"{self.tour.title} - {self.start_date} {self.start_time or ''}"

 

    @property

    def remaining_spots(self):

        return max(0, self.available_spots - self.booked_spots)

 

 

Tour

class TourTransport(models.Model):

    """Transportation for tours"""

    TRANSPORT_TYPE_CHOICES = [

        ('bus', 'Bus'),

        ('minivan', 'Minivan'),

        ('car', 'Private Car'),

        ('train', 'Train'),

        ('plane', 'Flight'),

        ('boat', 'Boat'),

        ('walking', 'Walking'),

        ('bicycle', 'Bicycle'),

        ('motorcycle', 'Motorcycle'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='transports')

    transport_type = models.CharField(max_length=20, choices=TRANSPORT_TYPE_CHOICES)

   

    # Route Information

    from_location = models.CharField(max_length=200)

    to_location = models.CharField(max_length=200)

    departure_day = models.PositiveIntegerField(help_text="Which day of the tour")

    departure_time = models.TimeField(null=True, blank=True)

    arrival_time = models.TimeField(null=True, blank=True)

   

    # Details

    description = models.TextField(blank=True)

    duration_minutes = models.PositiveIntegerField(null=True, blank=True)

    distance_km = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # Vehicle details

    vehicle_model = models.CharField(max_length=100, blank=True)

    capacity = models.PositiveIntegerField(null=True, blank=True)

    amenities = models.TextField(blank=True)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['departure_day', 'departure_time']

 

    def __str__(self):

        return f"{self.transport_type.title()} - {self.from_location} to {self.to_location}"

 

Tour

class TourAccommodation(models.Model):

    """Accommodation options for tours"""

    ACCOMMODATION_TYPE_CHOICES = [

        ('hotel', 'Hotel'),

        ('resort', 'Resort'),

        ('guesthouse', 'Guesthouse'),

        ('hostel', 'Hostel'),

        ('apartment', 'Apartment'),

        ('villa', 'Villa'),

        ('camp', 'Camp/Tent'),

        ('boat', 'Boat/Cruise'),

    ]

   

    STAR_RATING_CHOICES = [

        (1, '1 Star'),

        (2, '2 Stars'),

        (3, '3 Stars'),

        (4, '4 Stars'),

        (5, '5 Stars'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='accommodations')

    name = models.CharField(max_length=200)

    accommodation_type = models.CharField(max_length=20, choices=ACCOMMODATION_TYPE_CHOICES)

    star_rating = models.PositiveIntegerField(choices=STAR_RATING_CHOICES, null=True, blank=True)

   

    # Location

    address = models.TextField()

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'})

   

    # Details

    description = models.TextField(blank=True)

    amenities = models.TextField(blank=True, help_text="Comma-separated list of amenities")

    check_in_day = models.PositiveIntegerField(help_text="Which day of the tour")

    check_out_day = models.PositiveIntegerField(help_text="Which day of the tour")

   

    # Room information

    room_type = models.CharField(max_length=100, blank=True)

    occupancy = models.PositiveIntegerField(default=2)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['check_in_day']

 

    def __str__(self):

        return f"{self.name} - {self.tour.title} (Day {self.check_in_day}-{self.check_out_day})"

 

 

Tour

class ExternalTourSync(models.Model):

    """Track synchronization with external tour providers"""

    tour = models.OneToOneField(Tour, on_delete=models.CASCADE, related_name='sync_info')

    external_provider = models.CharField(max_length=50)  # viator, getyourguide, etc.

    external_tour_id = models.CharField(max_length=100)

    last_sync_date = models.DateTimeField()

    sync_status = models.CharField(max_length=20, default='active')

    sync_errors = models.TextField(blank=True)

   

    # Pricing sync

    external_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    markup_percentage = models.DecimalField(max_digits=5, decimal_places=2, default=10.00)

   

    class Meta:

        unique_together = ['external_provider', 'external_tour_id']

 

    def __str__(self):

        return f"{self.external_provider} - {self.external_tour_id}"

 

Tour 1

class TourImage(models.Model):

    """Additional images for tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='images')

    image = models.ImageField(upload_to='tours/gallery/')

    caption = models.CharField(max_length=200, blank=True)

    caption_vi = models.CharField(max_length=200, blank=True)

    alt_text = models.CharField(max_length=200, blank=True)

    is_featured = models.BooleanField(default=False)

    order = models.PositiveIntegerField(default=0)

   

    class Meta:

        ordering = ['order', 'id']

 

    def __str__(self):

        return f"{self.tour.title} - Image {self.id}"

 

Tour 2

class TourInclusion(models.Model):

    """What's included/excluded in tours"""

    INCLUSION_TYPE_CHOICES = [

        ('included', 'Included'),

        ('excluded', 'Not Included'),

        ('optional', 'Optional'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='inclusions')

    inclusion_type = models.CharField(max_length=20, choices=INCLUSION_TYPE_CHOICES)

    item = models.CharField(max_length=200)

    item_vi = models.CharField(max_length=200, blank=True)

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True)

    additional_cost = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    class Meta:

        ordering = ['inclusion_type', 'item']

 

    def __str__(self):

        return f"{self.tour.title} - {self.get_inclusion_type_display()}: {self.item}"

 

Tour Operator

class TourOperator(models.Model):

    """Tour operator company that can create and manage tours"""

    OPERATOR_TYPE_CHOICES = [

        ('inhouse', 'In-house Tour Operator'),

        ('partner', 'Partner Tour Operator'),

        ('broker', 'Broker/Agent'),

    ]

   

    CERTIFICATION_CHOICES = [

        ('none', 'No Certification'),

        ('iata', 'IATA Member'),

        ('asta', 'ASTA Member'),

        ('local', 'Local Tourism Board'),

    ]

 

    name = models.CharField(max_length=200)

    name_vi = models.CharField(max_length=200, blank=True, verbose_name="Vietnamese Name")

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True, verbose_name="Vietnamese Description")

    operator_type = models.CharField(max_length=20, choices=OPERATOR_TYPE_CHOICES, default='inhouse')

   

    # Contact Information

    email = models.EmailField()

    phone = models.CharField(max_length=20)

    website = models.URLField(blank=True)

    address = models.TextField()

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'}, related_name='tour_operators_city')

    country = models.ForeignKey('destinations.Country', on_delete=models.SET_NULL, null=True, blank=True)

   

    # Business Information

    license_number = models.CharField(max_length=100, blank=True)

    certification = models.CharField(max_length=20, choices=CERTIFICATION_CHOICES, default='none')

    established_year = models.IntegerField(null=True, blank=True)

   

    # Broker/Agent Information (for external operators)

    api_endpoint = models.URLField(blank=True, help_text="API endpoint for fetching tours")

    api_key = models.CharField(max_length=200, blank=True)

    commission_rate = models.DecimalField(max_digits=5, decimal_places=2, default=10.00,

                                        help_text="Commission percentage for broker sales")

   

    # Status and Management

    is_active = models.BooleanField(default=True)

    is_verified = models.BooleanField(default=False)

    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,

                               null=True, blank=True, related_name='tour_operator_profile')

    managers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='managed_tour_operators', blank=True)

   

    # Rating and Reviews

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

    total_tours = models.PositiveIntegerField(default=0)

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['-created_at']

        verbose_name = 'Tour Operator'

        verbose_name_plural = 'Tour Operators'

 

    def __str__(self):

        return self.name

 

    def get_absolute_url(self):

        return reverse('tour_operators:operator_detail', kwargs={'pk': self.pk})

 

 

Tour Operator

class TourGuide(models.Model):

    """Tour guides associated with tours"""

    LANGUAGE_CHOICES = [

        ('en', 'English'),

        ('vi', 'Vietnamese'),

        ('fr', 'French'),

        ('de', 'German'),

        ('es', 'Spanish'),

        ('zh', 'Chinese'),

        ('ja', 'Japanese'),

        ('ko', 'Korean'),

        ('th', 'Thai'),

        ('ru', 'Russian'),

        ('it', 'Italian'),

        ('pt', 'Portuguese'),

    ]

   

    VERIFICATION_STATUS_CHOICES = [

        ('pending', 'Pending Verification'),

        ('verified', 'Verified'),

        ('rejected', 'Rejected'),

        ('expired', 'Expired'),

    ]

 

    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='guide_profile')

    tour_operator = models.ForeignKey(TourOperator, on_delete=models.CASCADE, related_name='guides')

   

    # Guide Information

    bio = models.TextField(blank=True)

    bio_vi = models.TextField(blank=True, verbose_name="Vietnamese Bio")

    specializations = models.ManyToManyField(TourCategory, related_name='specialized_guides', blank=True)

    languages = models.JSONField(default=list, help_text="List of language codes")

   

    # Experience and Qualifications

    years_experience = models.PositiveIntegerField(default=0)

    certifications = models.TextField(blank=True, help_text="Text description of certifications")

    license_number = models.CharField(max_length=100, blank=True)

   

    # File Uploads for Verification

    certification_document = models.FileField(

        upload_to=guide_certification_upload_path,

        blank=True,

        null=True,

        help_text="Upload certification documents (PDF, DOC, DOCX, JPG, PNG)",

        verbose_name="Certification Document"

    )

    license_document = models.FileField(

        upload_to=guide_license_upload_path,

        blank=True,

        null=True,

        help_text="Upload license documents (PDF, DOC, DOCX, JPG, PNG)",

        verbose_name="License Document"

    )

   

    # Additional verification documents

    photo_id_document = models.FileField(

        upload_to=guide_license_upload_path,

        blank=True,

        null=True,

        help_text="Upload photo ID (passport, driver's license, etc.)",

        verbose_name="Photo ID Document"

    )

   

    # Verification Status

    verification_status = models.CharField(

        max_length=20,

        choices=VERIFICATION_STATUS_CHOICES,

        default='pending',

        verbose_name="Verification Status"

    )

    verification_date = models.DateTimeField(null=True, blank=True, verbose_name="Date Verified")

    verification_notes = models.TextField(blank=True, verbose_name="Admin Verification Notes")

    verified_by = models.ForeignKey(

        settings.AUTH_USER_MODEL,

        on_delete=models.SET_NULL,

        null=True,

        blank=True,

        related_name='verified_guides',

        verbose_name="Verified By"

    )

   

    # Availability

    is_available = models.BooleanField(default=True)

    hourly_rate = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

    daily_rate = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # Rating

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

    total_tours_guided = models.PositiveIntegerField(default=0)

   

    # Location

    base_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                                    limit_choices_to={'type': 'city'})

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['-average_rating', '-total_tours_guided']

        verbose_name = 'Tour Guide'

        verbose_name_plural = 'Tour Guides'

 

    def __str__(self):

        return f"{self.user.get_full_name()} - {self.tour_operator.name}"

   

    @property

    def full_name(self):

        return self.user.get_full_name()

   

    @property

    def is_verified(self):

        return self.verification_status == 'verified'

   

    @property

    def language_names(self):

        """Return human-readable language names"""

        lang_dict = dict(self.LANGUAGE_CHOICES)

        return [lang_dict.get(code, code) for code in self.languages]

   

    def get_absolute_url(self):

        from django.urls import reverse

        return reverse('tour_operators:guide_detail', kwargs={'pk': self.pk})

 

 

Vietnamese translation is not available for this article. Showing English content:

 

from django.db import models

from django.conf import settings

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

from django.contrib.contenttypes.models import ContentType

from django.core.validators import MinValueValidator, MaxValueValidator

from django.urls import reverse

from django.utils.translation import gettext_lazy as _

from decimal import Decimal

import uuid

import time

import os

 

def passport_upload_path(instance, filename):

    """Generate unique upload path for passport images"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'jpg'

   

    # Create unique filename with participant name and timestamp

    safe_name = f"{instance.first_name}_{instance.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"passport_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('passport', unique_filename)

 

def guide_certification_upload_path(instance, filename):

    """Generate unique upload path for guide certification documents"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'pdf'

   

    # Create unique filename with guide name and timestamp

    safe_name = f"{instance.user.first_name}_{instance.user.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"certification_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('guide_certifications', unique_filename)

 

def guide_license_upload_path(instance, filename):

    """Generate unique upload path for guide license documents"""

    # Get file extension

    ext = filename.split('.')[-1] if '.' in filename else 'pdf'

   

    # Create unique filename with guide name and timestamp

    safe_name = f"{instance.user.first_name}_{instance.user.last_name}".replace(' ', '_')

    timestamp = int(time.time())

    unique_filename = f"license_{safe_name}_{timestamp}.{ext}"

   

    return os.path.join('guide_licenses', unique_filename)

import uuid

 

 

 

 

Booking

class TourBooking(models.Model):

    """Tour bookings by customers"""

    BOOKING_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('confirmed', 'Confirmed'),

        ('paid', 'Paid'),

        ('cancelled', 'Cancelled'),

        ('completed', 'Completed'),

        ('refunded', 'Refunded'),

    ]

   

    PAYMENT_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('partial', 'Partially Paid'),

        ('paid', 'Fully Paid'),

        ('refunded', 'Refunded'),

    ]

 

    # Booking identifier

    booking_number = models.CharField(max_length=20, unique=True)

   

    # Tour and Schedule

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='bookings')

    schedule = models.ForeignKey(TourSchedule, on_delete=models.CASCADE, related_name='bookings')

   

    # Customer Information

    customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='tour_bookings')

    lead_traveler_name = models.CharField(max_length=200)

    lead_traveler_email = models.EmailField()

    lead_traveler_phone = models.CharField(max_length=20)

   

    # Participants

    adult_count = models.PositiveIntegerField(default=1)

    child_count = models.PositiveIntegerField(default=0)

    senior_count = models.PositiveIntegerField(default=0)

    total_participants = models.PositiveIntegerField()

   

    # Pricing

    base_price = models.DecimalField(max_digits=10, decimal_places=2)

    total_price = models.DecimalField(max_digits=10, decimal_places=2)

    discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    commission_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

   

    # Status

    booking_status = models.CharField(max_length=20, choices=BOOKING_STATUS_CHOICES, default='pending')

    payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending')

   

    # Additional Information

    special_requests = models.TextField(blank=True)

    dietary_requirements = models.TextField(blank=True)

    accessibility_needs = models.TextField(blank=True)

   

    # External booking (for broker tours)

    external_booking_id = models.CharField(max_length=100, blank=True)

    external_confirmation = models.CharField(max_length=100, blank=True)

   

    # Flight Booking Integration

    flight_booking_required = models.BooleanField(default=False, verbose_name="Flight Booking Required")

    selected_flights = models.ManyToManyField('transport.Flight', blank=True,

                                            related_name='selected_for_tours',

                                            verbose_name="Selected Flights")

    flight_class = models.CharField(max_length=20, blank=True,

                                  choices=[('economy', 'Economy'), ('business', 'Business'), ('first', 'First')],

                                  default='economy', verbose_name="Flight Class")

    flight_total_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                          verbose_name="Total Flight Cost")

    flight_booking_status = models.CharField(max_length=20,

                                           choices=[

                                               ('pending', 'Pending'),

                                               ('confirmed', 'Confirmed'),

                                               ('failed', 'Failed'),

                                               ('cancelled', 'Cancelled')

                                           ],

                                           default='pending',

                                           verbose_name="Flight Booking Status")

    flight_confirmation_number = models.CharField(max_length=100, blank=True,

                                                 verbose_name="Flight Confirmation Number")

    flight_notes = models.TextField(blank=True, verbose_name="Flight Booking Notes")

   

    # Timestamps

    booking_date = models.DateTimeField(auto_now_add=True)

    confirmation_date = models.DateTimeField(null=True, blank=True)

    cancellation_date = models.DateTimeField(null=True, blank=True)

   

    # Staff assignment

    assigned_guide = models.ForeignKey(TourGuide, on_delete=models.SET_NULL, null=True, blank=True)

    sales_agent = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,

                                  null=True, blank=True, related_name='tour_sales')

 

    class Meta:

        ordering = ['-booking_date']

 

    def __str__(self):

        return f"Booking {self.booking_number} - {self.tour.title}"

 

    def save(self, *args, **kwargs):

        if not self.booking_number:

            # Generate unique booking number

            import time

            self.booking_number = f"TB{int(time.time())}"

        super().save(*args, **kwargs)

 

Booking Participant

class TourParticipant(models.Model):

    """Individual participants in a tour booking"""

    PARTICIPANT_TYPE_CHOICES = [

        ('adult', 'Adult'),

        ('child', 'Child'),

        ('senior', 'Senior'),

        ('infant', 'Infant'),

    ]

 

    booking = models.ForeignKey(TourBooking, on_delete=models.CASCADE, related_name='participants')

    participant_type = models.CharField(max_length=20, choices=PARTICIPANT_TYPE_CHOICES)

   

    # Personal Information

    first_name = models.CharField(max_length=100)

    last_name = models.CharField(max_length=100)

    date_of_birth = models.DateField(null=True, blank=True)

    nationality = models.CharField(max_length=100)

    passport_number = models.CharField(max_length=50, blank=True)

    passport_expiry = models.DateField(null=True, blank=True)

    passport_image = models.ImageField(upload_to=passport_upload_path, blank=True, null=True,

                                     help_text="Upload passport image for verification")

   

    # Special requirements

    dietary_requirements = models.TextField(blank=True)

    medical_conditions = models.TextField(blank=True)

    emergency_contact_name = models.CharField(max_length=200, blank=True)

    emergency_contact_phone = models.CharField(max_length=20, blank=True)

 

    class Meta:

        ordering = ['booking', 'id']

 

    def __str__(self):

        return f"{self.first_name} {self.last_name} - {self.booking.booking_number}"

 

Booking Participant flight

class TourFlightBooking(models.Model):

    """Individual flight bookings for tour participants"""

    BOOKING_STATUS_CHOICES = [

        ('pending', 'Pending'),

        ('confirmed', 'Confirmed'),

        ('ticketed', 'Ticketed'),

        ('cancelled', 'Cancelled'),

        ('refunded', 'Refunded'),

    ]

 

    tour_booking = models.ForeignKey(TourBooking, on_delete=models.CASCADE, related_name='flight_bookings')

    participant = models.ForeignKey(TourParticipant, on_delete=models.CASCADE, related_name='flight_bookings')

    flight = models.ForeignKey('transport.Flight', on_delete=models.CASCADE, related_name='passenger_bookings')

   

    # Booking details

    seat_class = models.CharField(max_length=20, choices=[

        ('economy', 'Economy'),

        ('premium_economy', 'Premium Economy'),

        ('business', 'Business'),

        ('first', 'First')

    ], default='economy')

    seat_number = models.CharField(max_length=10, blank=True)

    meal_preference = models.CharField(max_length=50, blank=True,

                                     help_text="Special meal requests")

   

    # Pricing

    base_fare = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    taxes_fees = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    total_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)

   

    # Status and references

    booking_status = models.CharField(max_length=20, choices=BOOKING_STATUS_CHOICES, default='pending')

    airline_booking_reference = models.CharField(max_length=50, blank=True)

    ticket_number = models.CharField(max_length=50, blank=True)

   

    # Special requirements

    special_requests = models.TextField(blank=True,

                                      help_text="Wheelchair assistance, extra legroom, etc.")

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['flight__departure_time']

        unique_together = ['tour_booking', 'participant', 'flight']

        verbose_name = 'Tour Flight Booking'

        verbose_name_plural = 'Tour Flight Bookings'

 

    def __str__(self):

        return f"{self.participant.first_name} {self.participant.last_name} - {self.flight}"

 

    def save(self, *args, **kwargs):

        # Calculate total cost

        if self.base_fare and self.taxes_fees:

            self.total_cost = self.base_fare + self.taxes_fees

        super().save(*args, **kwargs)

 

 

Category

class TourCategory(models.Model):

    """Categories for tours (e.g., Adventure, Cultural, Food, etc.)"""

    name = models.CharField(max_length=100)

    name_vi = models.CharField(max_length=100, blank=True)

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True)

    icon = models.CharField(max_length=50, blank=True, help_text="FontAwesome icon class")

    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')

    is_active = models.BooleanField(default=True)

   

    class Meta:

        ordering = ['name']

        verbose_name_plural = 'Tour Categories'

 

    def __str__(self):

        if self.parent:

            return f"{self.parent.name} > {self.name}"

        return self.name

 

Link Apps

Transport

class TourFlight(models.Model):

    """Flight options and requirements for tours"""

    FLIGHT_TYPE_CHOICES = [

        ('outbound', 'Outbound Flight'),

        ('return', 'Return Flight'),

        ('internal', 'Internal Flight'),

        ('connecting', 'Connecting Flight'),

    ]

   

    FLIGHT_STATUS_CHOICES = [

        ('available', 'Available'),

        ('unavailable', 'Unavailable'),

        ('fully_booked', 'Fully Booked'),

        ('cancelled', 'Cancelled'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='flight_options')

    flight = models.ForeignKey('transport.Flight', on_delete=models.CASCADE, related_name='tour_flight_options')

    flight_type = models.CharField(max_length=20, choices=FLIGHT_TYPE_CHOICES, default='outbound')

   

    # Booking details

    is_included_in_price = models.BooleanField(default=False, verbose_name="Included in Tour Price")

    additional_cost_economy = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                                verbose_name="Economy Class Additional Cost")

    additional_cost_business = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                                 verbose_name="Business Class Additional Cost")

    additional_cost_first = models.DecimalField(max_digits=10, decimal_places=2, default=0,

                                              verbose_name="First Class Additional Cost")

   

    # Availability and requirements

    booking_deadline = models.DateField(null=True, blank=True, verbose_name="Booking Deadline")

    min_passengers = models.PositiveIntegerField(default=1, verbose_name="Minimum Passengers")

    max_passengers = models.PositiveIntegerField(default=50, verbose_name="Maximum Passengers")

    status = models.CharField(max_length=20, choices=FLIGHT_STATUS_CHOICES, default='available')

   

    # Additional info

    notes_en = models.TextField(blank=True, verbose_name="English Notes")

    notes_vi = models.TextField(blank=True, verbose_name="Vietnamese Notes")

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['flight__departure_time', 'flight_type']

        unique_together = ['tour', 'flight', 'flight_type']

        verbose_name = 'Tour Flight Option'

        verbose_name_plural = 'Tour Flight Options'

 

    def __str__(self):

        return f"{self.tour.title} - {self.get_flight_type_display()}: {self.flight}"

 

    @property

    def is_mandatory(self):

        """Return True if this flight is required for the tour"""

        return self.tour.requires_flights and self.flight_type in ['outbound', 'return']

 

Link apps

Destination

class TourAttraction(models.Model):

    """Attractions/activities included in tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='attractions')

    what_to_see = models.ForeignKey('destinations.WhatToSee', on_delete=models.CASCADE, null=True, blank=True)

   

    # Custom attraction details (if not linked to destinations app)

    name = models.CharField(max_length=200)

    description = models.TextField(blank=True)

    visit_day = models.PositiveIntegerField(help_text="Which day of the tour")

    visit_time = models.TimeField(null=True, blank=True)

    duration_minutes = models.PositiveIntegerField(null=True, blank=True)

   

    # Location

    address = models.CharField(max_length=300, blank=True)

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'})

   

    # Tickets and costs

    entrance_fee_included = models.BooleanField(default=True)

    ticket_price = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['visit_day', 'visit_time']

 

    def __str__(self):

        return f"{self.name} - Day {self.visit_day}"

 

Tour

Itinerary

class TourItinerary(models.Model):

    """Daily itinerary for tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='itinerary_days')

    day_number = models.PositiveIntegerField()

    title = models.CharField(max_length=200)

    title_vi = models.CharField(max_length=200, blank=True)

    description = models.TextField()

    description_vi = models.TextField(blank=True)

   

    # Activities for this day

    activities = models.TextField(blank=True, help_text="Comma-separated list of activities")

    meals_included = models.CharField(max_length=100, blank=True, help_text="e.g., Breakfast, Lunch")

    accommodation = models.CharField(max_length=200, blank=True)

   

    # Location

    location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                                limit_choices_to={'type': 'city'})

   

    class Meta:

        ordering = ['tour', 'day_number']

        unique_together = ['tour', 'day_number']

 

    def __str__(self):

        return f"{self.tour.title} - Day {self.day_number}: {self.title}"

 

Tour

class Tour(models.Model):

    """Main tour model that can be created by operators or fetched from external APIs"""

    TOUR_TYPE_CHOICES = [

        ('group', 'Group Tour'),

        ('private', 'Private Tour'),

        ('self_guided', 'Self-Guided Tour'),

        ('custom', 'Custom Tour'),

    ]

   

    DIFFICULTY_CHOICES = [

        ('easy', 'Easy'),

        ('moderate', 'Moderate'),

        ('challenging', 'Challenging'),

        ('extreme', 'Extreme'),

    ]

   

    TOUR_SOURCE_CHOICES = [

        ('inhouse', 'In-house Created'),

        ('viator', 'Viator'),

        ('getyourguide', 'GetYourGuide'),

        ('partner', 'Partner Operator'),

        ('api', 'External API'),

    ]

 

    # Basic Information

    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    title = models.CharField(max_length=200)

    title_vi = models.CharField(max_length=200, blank=True)

    slug = models.SlugField(max_length=220, unique=True)

    short_description = models.TextField(max_length=500)

    short_description_vi = models.TextField(max_length=500, blank=True)

    description = models.TextField()

    description_vi = models.TextField(blank=True)

   

    # Tour Details

    tour_operator = models.ForeignKey(TourOperator, on_delete=models.CASCADE, related_name='tours')

    categories = models.ManyToManyField(TourCategory, related_name='tours')

    tour_type = models.CharField(max_length=20, choices=TOUR_TYPE_CHOICES, default='group')

    difficulty = models.CharField(max_length=20, choices=DIFFICULTY_CHOICES, default='easy')

   

    # Duration and Scheduling

    duration_days = models.PositiveIntegerField(default=1)

    duration_hours = models.PositiveIntegerField(default=0, help_text="Additional hours beyond full days")

    start_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True,

                                     related_name='tours_starting_here', limit_choices_to={'type': 'city'})

    end_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True,

                                   related_name='tours_ending_here', limit_choices_to={'type': 'city'})

    destinations = models.ManyToManyField('destinations.Destination', related_name='tours', blank=True)

   

    # Flight Requirements (for overseas/international tours)

    requires_flights = models.BooleanField(default=False, verbose_name="Requires Flight Booking",

                                         help_text="Tour requires flight booking for participants")

    departure_airport = models.ForeignKey('transport.Airport', on_delete=models.SET_NULL,

                                        null=True, blank=True, related_name='tour_departures',

                                        verbose_name="Departure Airport")

    arrival_airport = models.ForeignKey('transport.Airport', on_delete=models.SET_NULL,

                                      null=True, blank=True, related_name='tour_arrivals',

                                      verbose_name="Destination Airport")

    return_flight_included = models.BooleanField(default=False, verbose_name="Return Flight Included")

    flight_class_options = models.CharField(max_length=100, blank=True, default="economy,business",

                                          help_text="Available flight classes (comma-separated): economy,business,first",

                                          verbose_name="Flight Class Options")

    flight_booking_deadline_days = models.PositiveIntegerField(default=14,

                                                             help_text="Days before tour to book flights",

                                                             verbose_name="Flight Booking Deadline (Days)")

   

    # Capacity and Logistics

    min_participants = models.PositiveIntegerField(default=1)

    max_participants = models.PositiveIntegerField(default=20)

    min_age = models.PositiveIntegerField(default=0)

    max_age = models.PositiveIntegerField(null=True, blank=True)

   

    # Pricing

    base_price = models.DecimalField(max_digits=10, decimal_places=2)

    price_currency = models.CharField(max_length=3, default='USD')

    child_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    senior_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    group_discount_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)

   

    # External Source Information

    source = models.CharField(max_length=20, choices=TOUR_SOURCE_CHOICES, default='inhouse')

    external_id = models.CharField(max_length=100, blank=True, help_text="ID from external system")

    external_url = models.URLField(blank=True, help_text="Original URL from external source")

   

    # Features and Inclusions

    includes_accommodation = models.BooleanField(default=False)

    includes_meals = models.BooleanField(default=False)

    includes_transport = models.BooleanField(default=False)

    includes_guide = models.BooleanField(default=False)

    includes_tickets = models.BooleanField(default=False)

   

    # Requirements

    fitness_level_required = models.CharField(max_length=20, choices=DIFFICULTY_CHOICES, default='easy')

    special_requirements = models.TextField(blank=True)

    what_to_bring = models.TextField(blank=True)

   

    # Media

    main_image = models.ImageField(upload_to='tours/main/', null=True, blank=True)

    video_url = models.URLField(blank=True)

   

    # Status and Management

    is_active = models.BooleanField(default=True)

    is_featured = models.BooleanField(default=False)

    is_available = models.BooleanField(default=True)

    requires_approval = models.BooleanField(default=False)

   

    # Statistics

    total_bookings = models.PositiveIntegerField(default=0)

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

   

    # Timestamps

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

    last_synced = models.DateTimeField(null=True, blank=True, help_text="Last sync from external API")

 

    # Generic relations for reviews

    reviews = GenericRelation('reviews.Review')

 

    class Meta:

        verbose_name = _("Tour")

        verbose_name_plural = _("Tours")

        ordering = ['-created_at']

        indexes = [

            models.Index(fields=['slug']),

            models.Index(fields=['source', 'external_id']),

            models.Index(fields=['is_active', 'is_available']),

        ]

 

    def __str__(self):

        return self.title

 

    def get_absolute_url(self):

        if not self.slug:

            # If slug is empty, generate one based on title or use ID

            from django.utils.text import slugify

            if self.title:

                self.slug = slugify(self.title)

                # Ensure uniqueness

                if Tour.objects.filter(slug=self.slug).exclude(id=self.id).exists():

                    self.slug = f"{self.slug}-{self.id}"

            else:

                self.slug = f"tour-{self.id}"

            self.save()

        return reverse('tour_operators:tour_detail', kwargs={'slug': self.slug})

 

    @property

    def duration_display(self):

        """Return formatted duration"""

        if self.duration_days == 0:

            return f"{self.duration_hours} hours"

        elif self.duration_hours == 0:

            return f"{self.duration_days} day{'s' if self.duration_days > 1 else ''}"

        else:

            return f"{self.duration_days} day{'s' if self.duration_days > 1 else ''} {self.duration_hours} hours"

 

Tour

class TourSchedule(models.Model):

    """Available dates and times for tours"""

    SCHEDULE_TYPE_CHOICES = [

        ('fixed', 'Fixed Date'),

        ('recurring', 'Recurring'),

        ('on_demand', 'On Demand'),

    ]

   

    DAY_OF_WEEK_CHOICES = [

        (0, 'Monday'),

        (1, 'Tuesday'),

        (2, 'Wednesday'),

        (3, 'Thursday'),

        (4, 'Friday'),

        (5, 'Saturday'),

        (6, 'Sunday'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='schedules')

    schedule_type = models.CharField(max_length=20, choices=SCHEDULE_TYPE_CHOICES, default='fixed')

   

    # For fixed dates

    start_date = models.DateField(null=True, blank=True)

    end_date = models.DateField(null=True, blank=True)

    start_time = models.TimeField(null=True, blank=True)

   

    # For recurring schedules

    days_of_week = models.JSONField(default=list, blank=True, help_text="List of days of week (0=Monday)")

   

    # Capacity management

    available_spots = models.PositiveIntegerField(default=0)

    booked_spots = models.PositiveIntegerField(default=0)

   

    # Pricing override for this schedule

    price_override = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

   

    is_active = models.BooleanField(default=True)

    notes = models.TextField(blank=True)

 

    class Meta:

        ordering = ['start_date', 'start_time']

 

    def __str__(self):

        return f"{self.tour.title} - {self.start_date} {self.start_time or ''}"

 

    @property

    def remaining_spots(self):

        return max(0, self.available_spots - self.booked_spots)

 

 

Tour

class TourTransport(models.Model):

    """Transportation for tours"""

    TRANSPORT_TYPE_CHOICES = [

        ('bus', 'Bus'),

        ('minivan', 'Minivan'),

        ('car', 'Private Car'),

        ('train', 'Train'),

        ('plane', 'Flight'),

        ('boat', 'Boat'),

        ('walking', 'Walking'),

        ('bicycle', 'Bicycle'),

        ('motorcycle', 'Motorcycle'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='transports')

    transport_type = models.CharField(max_length=20, choices=TRANSPORT_TYPE_CHOICES)

   

    # Route Information

    from_location = models.CharField(max_length=200)

    to_location = models.CharField(max_length=200)

    departure_day = models.PositiveIntegerField(help_text="Which day of the tour")

    departure_time = models.TimeField(null=True, blank=True)

    arrival_time = models.TimeField(null=True, blank=True)

   

    # Details

    description = models.TextField(blank=True)

    duration_minutes = models.PositiveIntegerField(null=True, blank=True)

    distance_km = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # Vehicle details

    vehicle_model = models.CharField(max_length=100, blank=True)

    capacity = models.PositiveIntegerField(null=True, blank=True)

    amenities = models.TextField(blank=True)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['departure_day', 'departure_time']

 

    def __str__(self):

        return f"{self.transport_type.title()} - {self.from_location} to {self.to_location}"

 

Tour

class TourAccommodation(models.Model):

    """Accommodation options for tours"""

    ACCOMMODATION_TYPE_CHOICES = [

        ('hotel', 'Hotel'),

        ('resort', 'Resort'),

        ('guesthouse', 'Guesthouse'),

        ('hostel', 'Hostel'),

        ('apartment', 'Apartment'),

        ('villa', 'Villa'),

        ('camp', 'Camp/Tent'),

        ('boat', 'Boat/Cruise'),

    ]

   

    STAR_RATING_CHOICES = [

        (1, '1 Star'),

        (2, '2 Stars'),

        (3, '3 Stars'),

        (4, '4 Stars'),

        (5, '5 Stars'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='accommodations')

    name = models.CharField(max_length=200)

    accommodation_type = models.CharField(max_length=20, choices=ACCOMMODATION_TYPE_CHOICES)

    star_rating = models.PositiveIntegerField(choices=STAR_RATING_CHOICES, null=True, blank=True)

   

    # Location

    address = models.TextField()

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'})

   

    # Details

    description = models.TextField(blank=True)

    amenities = models.TextField(blank=True, help_text="Comma-separated list of amenities")

    check_in_day = models.PositiveIntegerField(help_text="Which day of the tour")

    check_out_day = models.PositiveIntegerField(help_text="Which day of the tour")

   

    # Room information

    room_type = models.CharField(max_length=100, blank=True)

    occupancy = models.PositiveIntegerField(default=2)

   

    # External booking

    external_booking_url = models.URLField(blank=True)

    booking_reference = models.CharField(max_length=100, blank=True)

 

    class Meta:

        ordering = ['check_in_day']

 

    def __str__(self):

        return f"{self.name} - {self.tour.title} (Day {self.check_in_day}-{self.check_out_day})"

 

 

Tour

class ExternalTourSync(models.Model):

    """Track synchronization with external tour providers"""

    tour = models.OneToOneField(Tour, on_delete=models.CASCADE, related_name='sync_info')

    external_provider = models.CharField(max_length=50)  # viator, getyourguide, etc.

    external_tour_id = models.CharField(max_length=100)

    last_sync_date = models.DateTimeField()

    sync_status = models.CharField(max_length=20, default='active')

    sync_errors = models.TextField(blank=True)

   

    # Pricing sync

    external_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)

    markup_percentage = models.DecimalField(max_digits=5, decimal_places=2, default=10.00)

   

    class Meta:

        unique_together = ['external_provider', 'external_tour_id']

 

    def __str__(self):

        return f"{self.external_provider} - {self.external_tour_id}"

 

Tour 1

class TourImage(models.Model):

    """Additional images for tours"""

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='images')

    image = models.ImageField(upload_to='tours/gallery/')

    caption = models.CharField(max_length=200, blank=True)

    caption_vi = models.CharField(max_length=200, blank=True)

    alt_text = models.CharField(max_length=200, blank=True)

    is_featured = models.BooleanField(default=False)

    order = models.PositiveIntegerField(default=0)

   

    class Meta:

        ordering = ['order', 'id']

 

    def __str__(self):

        return f"{self.tour.title} - Image {self.id}"

 

Tour 2

class TourInclusion(models.Model):

    """What's included/excluded in tours"""

    INCLUSION_TYPE_CHOICES = [

        ('included', 'Included'),

        ('excluded', 'Not Included'),

        ('optional', 'Optional'),

    ]

 

    tour = models.ForeignKey(Tour, on_delete=models.CASCADE, related_name='inclusions')

    inclusion_type = models.CharField(max_length=20, choices=INCLUSION_TYPE_CHOICES)

    item = models.CharField(max_length=200)

    item_vi = models.CharField(max_length=200, blank=True)

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True)

    additional_cost = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    class Meta:

        ordering = ['inclusion_type', 'item']

 

    def __str__(self):

        return f"{self.tour.title} - {self.get_inclusion_type_display()}: {self.item}"

 

Tour Operator

class TourOperator(models.Model):

    """Tour operator company that can create and manage tours"""

    OPERATOR_TYPE_CHOICES = [

        ('inhouse', 'In-house Tour Operator'),

        ('partner', 'Partner Tour Operator'),

        ('broker', 'Broker/Agent'),

    ]

   

    CERTIFICATION_CHOICES = [

        ('none', 'No Certification'),

        ('iata', 'IATA Member'),

        ('asta', 'ASTA Member'),

        ('local', 'Local Tourism Board'),

    ]

 

    name = models.CharField(max_length=200)

    name_vi = models.CharField(max_length=200, blank=True, verbose_name="Vietnamese Name")

    description = models.TextField(blank=True)

    description_vi = models.TextField(blank=True, verbose_name="Vietnamese Description")

    operator_type = models.CharField(max_length=20, choices=OPERATOR_TYPE_CHOICES, default='inhouse')

   

    # Contact Information

    email = models.EmailField()

    phone = models.CharField(max_length=20)

    website = models.URLField(blank=True)

    address = models.TextField()

    city = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                           limit_choices_to={'type': 'city'}, related_name='tour_operators_city')

    country = models.ForeignKey('destinations.Country', on_delete=models.SET_NULL, null=True, blank=True)

   

    # Business Information

    license_number = models.CharField(max_length=100, blank=True)

    certification = models.CharField(max_length=20, choices=CERTIFICATION_CHOICES, default='none')

    established_year = models.IntegerField(null=True, blank=True)

   

    # Broker/Agent Information (for external operators)

    api_endpoint = models.URLField(blank=True, help_text="API endpoint for fetching tours")

    api_key = models.CharField(max_length=200, blank=True)

    commission_rate = models.DecimalField(max_digits=5, decimal_places=2, default=10.00,

                                        help_text="Commission percentage for broker sales")

   

    # Status and Management

    is_active = models.BooleanField(default=True)

    is_verified = models.BooleanField(default=False)

    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,

                               null=True, blank=True, related_name='tour_operator_profile')

    managers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='managed_tour_operators', blank=True)

   

    # Rating and Reviews

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

    total_tours = models.PositiveIntegerField(default=0)

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['-created_at']

        verbose_name = 'Tour Operator'

        verbose_name_plural = 'Tour Operators'

 

    def __str__(self):

        return self.name

 

    def get_absolute_url(self):

        return reverse('tour_operators:operator_detail', kwargs={'pk': self.pk})

 

 

Tour Operator

class TourGuide(models.Model):

    """Tour guides associated with tours"""

    LANGUAGE_CHOICES = [

        ('en', 'English'),

        ('vi', 'Vietnamese'),

        ('fr', 'French'),

        ('de', 'German'),

        ('es', 'Spanish'),

        ('zh', 'Chinese'),

        ('ja', 'Japanese'),

        ('ko', 'Korean'),

        ('th', 'Thai'),

        ('ru', 'Russian'),

        ('it', 'Italian'),

        ('pt', 'Portuguese'),

    ]

   

    VERIFICATION_STATUS_CHOICES = [

        ('pending', 'Pending Verification'),

        ('verified', 'Verified'),

        ('rejected', 'Rejected'),

        ('expired', 'Expired'),

    ]

 

    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='guide_profile')

    tour_operator = models.ForeignKey(TourOperator, on_delete=models.CASCADE, related_name='guides')

   

    # Guide Information

    bio = models.TextField(blank=True)

    bio_vi = models.TextField(blank=True, verbose_name="Vietnamese Bio")

    specializations = models.ManyToManyField(TourCategory, related_name='specialized_guides', blank=True)

    languages = models.JSONField(default=list, help_text="List of language codes")

   

    # Experience and Qualifications

    years_experience = models.PositiveIntegerField(default=0)

    certifications = models.TextField(blank=True, help_text="Text description of certifications")

    license_number = models.CharField(max_length=100, blank=True)

   

    # File Uploads for Verification

    certification_document = models.FileField(

        upload_to=guide_certification_upload_path,

        blank=True,

        null=True,

        help_text="Upload certification documents (PDF, DOC, DOCX, JPG, PNG)",

        verbose_name="Certification Document"

    )

    license_document = models.FileField(

        upload_to=guide_license_upload_path,

        blank=True,

        null=True,

        help_text="Upload license documents (PDF, DOC, DOCX, JPG, PNG)",

        verbose_name="License Document"

    )

   

    # Additional verification documents

    photo_id_document = models.FileField(

        upload_to=guide_license_upload_path,

        blank=True,

        null=True,

        help_text="Upload photo ID (passport, driver's license, etc.)",

        verbose_name="Photo ID Document"

    )

   

    # Verification Status

    verification_status = models.CharField(

        max_length=20,

        choices=VERIFICATION_STATUS_CHOICES,

        default='pending',

        verbose_name="Verification Status"

    )

    verification_date = models.DateTimeField(null=True, blank=True, verbose_name="Date Verified")

    verification_notes = models.TextField(blank=True, verbose_name="Admin Verification Notes")

    verified_by = models.ForeignKey(

        settings.AUTH_USER_MODEL,

        on_delete=models.SET_NULL,

        null=True,

        blank=True,

        related_name='verified_guides',

        verbose_name="Verified By"

    )

   

    # Availability

    is_available = models.BooleanField(default=True)

    hourly_rate = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

    daily_rate = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)

   

    # Rating

    average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00)

    total_reviews = models.PositiveIntegerField(default=0)

    total_tours_guided = models.PositiveIntegerField(default=0)

   

    # Location

    base_location = models.ForeignKey('geo.Location', on_delete=models.SET_NULL, null=True, blank=True,

                                    limit_choices_to={'type': 'city'})

   

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

 

    class Meta:

        ordering = ['-average_rating', '-total_tours_guided']

        verbose_name = 'Tour Guide'

        verbose_name_plural = 'Tour Guides'

 

    def __str__(self):

        return f"{self.user.get_full_name()} - {self.tour_operator.name}"

   

    @property

    def full_name(self):

        return self.user.get_full_name()

   

    @property

    def is_verified(self):

        return self.verification_status == 'verified'

   

    @property

    def language_names(self):

        """Return human-readable language names"""

        lang_dict = dict(self.LANGUAGE_CHOICES)

        return [lang_dict.get(code, code) for code in self.languages]

   

    def get_absolute_url(self):

        from django.urls import reverse

        return reverse('tour_operators:guide_detail', kwargs={'pk': self.pk})

 

 

Attached Files

0 files found.

You are viewing this article in public mode. Some features may be limited.