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})
|
|
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
You are viewing this article in public mode. Some features may be limited.