604 lines
22 KiB
Python
604 lines
22 KiB
Python
import calendar
|
|
import json
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from django.contrib.auth import update_session_auth_hash
|
|
from django.contrib.auth.models import User
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
from django.utils import timezone
|
|
from rest_framework import viewsets, permissions, status
|
|
from rest_framework.decorators import api_view, permission_classes, action
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from .google import create_event, export_punchlist_to_sheet
|
|
from .models import (
|
|
Profile, Customer, Account, Revenue, Labor,
|
|
Schedule, Service, Project, Invoice, Report, Punchlist
|
|
)
|
|
from .serializers import (
|
|
UserSerializer, ProfileSerializer, CustomerSerializer,
|
|
AccountSerializer, RevenueSerializer, LaborSerializer,
|
|
ScheduleSerializer, ServiceSerializer, ProjectSerializer,
|
|
InvoiceSerializer, ReportSerializer, PunchlistSerializer
|
|
)
|
|
|
|
|
|
# Users API
|
|
class UserViewSet(viewsets.ModelViewSet):
|
|
queryset = User.objects.all()
|
|
serializer_class = UserSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
|
|
# Profiles API
|
|
class ProfileViewSet(viewsets.ModelViewSet):
|
|
queryset = Profile.objects.all()
|
|
serializer_class = ProfileSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
|
|
# Customers API
|
|
class CustomerViewSet(viewsets.ModelViewSet):
|
|
queryset = Customer.objects.all()
|
|
serializer_class = CustomerSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
|
|
# Accounts API
|
|
class AccountViewSet(viewsets.ModelViewSet):
|
|
queryset = Account.objects.all()
|
|
serializer_class = AccountSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Account.objects.all()
|
|
customer_id = self.request.query_params.get('customer_id')
|
|
if customer_id is not None:
|
|
queryset = queryset.filter(customer_id=customer_id)
|
|
return queryset
|
|
|
|
|
|
# Revenues API
|
|
class RevenueViewSet(viewsets.ModelViewSet):
|
|
queryset = Revenue.objects.all()
|
|
serializer_class = RevenueSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Revenue.objects.all()
|
|
account_id = self.request.query_params.get('account_id')
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
return queryset
|
|
|
|
|
|
# Labor API
|
|
class LaborViewSet(viewsets.ModelViewSet):
|
|
queryset = Labor.objects.all()
|
|
serializer_class = LaborSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Labor.objects.all()
|
|
account_id = self.request.query_params.get('account_id')
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
return queryset
|
|
|
|
|
|
# Schedules API
|
|
class ScheduleViewSet(viewsets.ModelViewSet):
|
|
queryset = Schedule.objects.all()
|
|
serializer_class = ScheduleSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Schedule.objects.all()
|
|
account_id = self.request.query_params.get('account_id')
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
return queryset
|
|
|
|
|
|
# Services API
|
|
class ServiceViewSet(viewsets.ModelViewSet):
|
|
queryset = Service.objects.all()
|
|
serializer_class = ServiceSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Service.objects.all()
|
|
|
|
# Filter by account
|
|
account_id = self.request.query_params.get('account_id')
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
|
|
# Filter by team member
|
|
team_member_id = self.request.query_params.get('team_member_id')
|
|
if team_member_id is not None:
|
|
queryset = queryset.filter(team_members__id=team_member_id)
|
|
|
|
return queryset
|
|
|
|
|
|
# Projects API
|
|
class ProjectViewSet(viewsets.ModelViewSet):
|
|
queryset = Project.objects.all()
|
|
serializer_class = ProjectSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Project.objects.all()
|
|
customer_id = self.request.query_params.get('customer_id')
|
|
account_id = self.request.query_params.get('account_id')
|
|
|
|
if customer_id is not None:
|
|
queryset = queryset.filter(customer_id=customer_id)
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
return queryset
|
|
|
|
|
|
# Invoices API
|
|
class InvoiceViewSet(viewsets.ModelViewSet):
|
|
queryset = Invoice.objects.all()
|
|
serializer_class = InvoiceSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Invoice.objects.all()
|
|
customer_id = self.request.query_params.get('customer_id')
|
|
if customer_id is not None:
|
|
queryset = queryset.filter(customer_id=customer_id)
|
|
return queryset
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def mark_as_paid(self, request, pk=None):
|
|
"""
|
|
Mark an invoice as paid with payment details
|
|
"""
|
|
invoice = self.get_object()
|
|
|
|
# Update invoice with payment information
|
|
invoice.status = 'paid'
|
|
|
|
# Set payment date if provided, otherwise use today
|
|
date_paid = request.data.get('date_paid')
|
|
if date_paid:
|
|
invoice.date_paid = date_paid
|
|
else:
|
|
invoice.date_paid = timezone.now().date()
|
|
|
|
# Set payment type if provided
|
|
payment_type = request.data.get('payment_type')
|
|
if payment_type:
|
|
invoice.payment_type = payment_type
|
|
|
|
invoice.save()
|
|
|
|
# Return the updated invoice
|
|
serializer = self.get_serializer(invoice)
|
|
return Response(serializer.data)
|
|
|
|
|
|
# Reports API
|
|
class ReportViewSet(viewsets.ModelViewSet):
|
|
queryset = Report.objects.all()
|
|
serializer_class = ReportSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Report.objects.all()
|
|
team_member_id = self.request.query_params.get('team_member_id')
|
|
if team_member_id is not None:
|
|
queryset = queryset.filter(team_member_id=team_member_id)
|
|
return queryset
|
|
|
|
|
|
# User Profile API
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def current_user(request):
|
|
"""
|
|
Endpoint to get the current authenticated user and their profile
|
|
"""
|
|
user = request.user
|
|
user_serializer = UserSerializer(user)
|
|
# Get the profile associated with the user
|
|
try:
|
|
profile = Profile.objects.get(user=user)
|
|
profile_serializer = ProfileSerializer(profile)
|
|
# Return combined data
|
|
return Response({
|
|
'user': user_serializer.data,
|
|
'profile': profile_serializer.data
|
|
})
|
|
except ObjectDoesNotExist:
|
|
# Return just the user data if no profile exists
|
|
return Response({
|
|
'user': user_serializer.data,
|
|
'profile': None
|
|
})
|
|
|
|
|
|
# Punchlist API
|
|
class PunchlistViewSet(viewsets.ModelViewSet):
|
|
queryset = Punchlist.objects.all()
|
|
serializer_class = PunchlistSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = Punchlist.objects.all()
|
|
project_id = self.request.query_params.get('project_id')
|
|
account_id = self.request.query_params.get('account_id')
|
|
|
|
if project_id is not None:
|
|
queryset = queryset.filter(project_id=project_id)
|
|
if account_id is not None:
|
|
queryset = queryset.filter(account_id=account_id)
|
|
return queryset
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
"""
|
|
Create a new punchlist
|
|
"""
|
|
# Extract project_id from request data
|
|
project_id = request.data.get('project_id')
|
|
if not project_id:
|
|
return Response(
|
|
{"error": "project_id is required"},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Verify project exists
|
|
try:
|
|
project = Project.objects.get(id=project_id)
|
|
except Project.DoesNotExist:
|
|
return Response(
|
|
{"error": f"Project with id {project_id} does not exist"},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Extract account_id from request data
|
|
account_id = request.data.get('account_id')
|
|
if not account_id:
|
|
return Response(
|
|
{"error": "account_id is required"},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Verify account exists
|
|
try:
|
|
account = Account.objects.get(id=account_id)
|
|
except Account.DoesNotExist:
|
|
return Response(
|
|
{"error": f"Account with id {account_id} does not exist"},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Create a modified data dictionary with project and account fields
|
|
modified_data = request.data.copy()
|
|
modified_data['project'] = project_id # Map project_id to project
|
|
modified_data['account'] = account_id # Map account_id to account
|
|
|
|
# Create serializer with modified data
|
|
serializer = self.get_serializer(data=modified_data)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_create(serializer)
|
|
|
|
headers = self.get_success_headers(serializer.data)
|
|
return Response(
|
|
serializer.data,
|
|
status=status.HTTP_201_CREATED,
|
|
headers=headers
|
|
)
|
|
|
|
|
|
# Change Password API
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def change_password(request):
|
|
"""
|
|
Endpoint to change user password
|
|
"""
|
|
user = request.user
|
|
# Check if required fields are present
|
|
if not all(k in request.data for k in ('current_password', 'new_password')):
|
|
return Response(
|
|
{'error': 'Current password and new password are required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
# Verify current password
|
|
if not user.check_password(request.data['current_password']):
|
|
return Response(
|
|
{'error': 'Current password is incorrect'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
# Set new password
|
|
user.set_password(request.data['new_password'])
|
|
user.save()
|
|
# Update session to prevent logout
|
|
update_session_auth_hash(request, user)
|
|
return Response({'message': 'Password changed successfully'}, status=status.HTTP_200_OK)
|
|
|
|
|
|
# Reset Password API
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def reset_password(request):
|
|
"""
|
|
Endpoint for admins to reset a user's password
|
|
"""
|
|
# Check if user is admin
|
|
try:
|
|
profile = Profile.objects.get(user=request.user)
|
|
if profile.role != 'admin':
|
|
return Response(
|
|
{"error": "Only administrators can reset passwords"},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
except ObjectDoesNotExist:
|
|
return Response(
|
|
{"error": "User profile not found"},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
# Get parameters
|
|
user_id = request.data.get('user_id')
|
|
new_password = request.data.get('new_password')
|
|
if not user_id or not new_password:
|
|
return Response(
|
|
{"error": "Missing required parameters"},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
# Find the target profile and user
|
|
try:
|
|
target_profile = Profile.objects.get(id=user_id)
|
|
target_user = target_profile.user
|
|
# Set new password
|
|
target_user.set_password(new_password)
|
|
target_user.save()
|
|
return Response({"message": "Password reset successful"}, status=status.HTTP_200_OK)
|
|
except ObjectDoesNotExist:
|
|
return Response(
|
|
{"error": "Target user profile not found"},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{"error": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
|
|
# Generate Services API
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def generate_services(request):
|
|
"""
|
|
Batch generate services for a given month based on active schedules
|
|
"""
|
|
try:
|
|
# Expected data from request
|
|
month = int(request.data.get('month'))
|
|
year = int(request.data.get('year'))
|
|
account_ids = request.data.get('account_ids', None)
|
|
# Fixed holiday dates for 2025
|
|
holidays = [
|
|
datetime(2025, 1, 1).date(), # New Year's Day
|
|
datetime(2025, 1, 20).date(), # MLK Day
|
|
datetime(2025, 2, 17).date(), # Presidents' Day
|
|
datetime(2025, 5, 26).date(), # Memorial Day
|
|
datetime(2025, 7, 4).date(), # Independence Day
|
|
datetime(2025, 9, 1).date(), # Labor Day
|
|
datetime(2025, 11, 27).date(), # Thanksgiving
|
|
datetime(2025, 12, 25).date(), # Christmas
|
|
]
|
|
# Validate month
|
|
if month < 0 or month > 11:
|
|
return Response(
|
|
{'error': 'Month must be between 0 (January) and 11 (December)'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
except (ValueError, TypeError):
|
|
return Response(
|
|
{'error': 'Invalid parameters. Month and year must be integers.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
# Get calendar for specified month
|
|
cal = calendar.monthcalendar(year, (month + 1))
|
|
# Get first and last day of month (for filtering schedules)
|
|
first_day = datetime(year, (month + 1), 1).date()
|
|
last_day = datetime(year, (month + 1), calendar.monthrange(year, (month + 1))[1]).date()
|
|
# Get active schedules for the month
|
|
schedules_query = Schedule.objects.filter(
|
|
start_date__lte=last_day
|
|
).filter(
|
|
Q(end_date__isnull=True) |
|
|
Q(end_date__gte=first_day)
|
|
)
|
|
# Filter by account IDs if provided
|
|
if account_ids:
|
|
schedules_query = schedules_query.filter(account__id__in=account_ids)
|
|
# Initialize service count
|
|
services_created = 0
|
|
with transaction.atomic():
|
|
for schedule in schedules_query:
|
|
# Use the monthcalendar to iterate through days
|
|
for week in cal:
|
|
for weekday, day in enumerate(week):
|
|
if day == 0:
|
|
continue
|
|
# Iterated date object
|
|
iter_date = datetime(year, month + 1, day).date()
|
|
# Check if date is in schedule's range
|
|
if not (schedule.start_date <= iter_date and (
|
|
schedule.end_date is None or schedule.end_date >= iter_date)):
|
|
continue
|
|
# Check if date is a holiday
|
|
if iter_date in holidays:
|
|
continue
|
|
# Initial service requirement
|
|
service_needed = False
|
|
# Check regular weekday service requirement
|
|
if weekday == 0 and schedule.monday_service:
|
|
service_needed = True
|
|
elif weekday == 1 and schedule.tuesday_service:
|
|
service_needed = True
|
|
elif weekday == 2 and schedule.wednesday_service:
|
|
service_needed = True
|
|
elif weekday == 3 and schedule.thursday_service:
|
|
service_needed = True
|
|
elif weekday == 4 and schedule.friday_service:
|
|
service_needed = True
|
|
elif weekday == 5 and schedule.saturday_service:
|
|
service_needed = True
|
|
elif weekday == 6 and schedule.sunday_service:
|
|
service_needed = True
|
|
# Separate check for weekend service
|
|
if (schedule.weekend_service and weekday in [4]) and not schedule.friday_service:
|
|
service_needed = True
|
|
# Create service if needed and doesn't already exist
|
|
if service_needed:
|
|
# Check for existing service
|
|
existing_service = Service.objects.filter(
|
|
account=schedule.account,
|
|
date=iter_date
|
|
).exists()
|
|
# If service does not exist
|
|
if not existing_service:
|
|
# Service starts at 6:00PM
|
|
deadline_start = datetime.combine(
|
|
iter_date,
|
|
datetime.min.time()
|
|
).replace(hour=18)
|
|
# Service ends at 6:00AM
|
|
deadline_end = datetime.combine(
|
|
iter_date + timedelta(days=1),
|
|
datetime.min.time()
|
|
).replace(hour=6)
|
|
# Prepare notes
|
|
notes = ""
|
|
if schedule.schedule_exception:
|
|
notes += f"Schedule exception: {schedule.schedule_exception}"
|
|
if schedule.weekend_service and weekday in [4]:
|
|
notes += " Weekend Service"
|
|
# Create the service
|
|
Service.objects.create(
|
|
account=schedule.account,
|
|
date=iter_date,
|
|
status='scheduled',
|
|
notes=notes.strip(),
|
|
deadline_start=deadline_start,
|
|
deadline_end=deadline_end
|
|
)
|
|
# Add to the counter
|
|
services_created += 1
|
|
# Return a summary of the operation
|
|
return Response({
|
|
'message': f'Successfully generated {services_created} services for {calendar.month_name[month + 1]} {year}.',
|
|
'count': services_created,
|
|
'month': month,
|
|
'year': year,
|
|
'accountsProcessed': schedules_query.count()
|
|
})
|
|
|
|
|
|
key_file_path = os.path.join(settings.BASE_DIR, 'google-sa.json')
|
|
|
|
|
|
# Google Calendar Event API
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def create_calendar_event(request):
|
|
"""
|
|
Create a Google Calendar event for a project or service by reading key from file
|
|
"""
|
|
try:
|
|
try:
|
|
with open(key_file_path, 'r') as f:
|
|
google_service_account_key = json.load(f)
|
|
except FileNotFoundError:
|
|
return Response(
|
|
{"error": "Google service account key file not found"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
return Response(
|
|
{"error": "Invalid Google service account key file format", "details": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
result = create_event(google_service_account_key, request.data)
|
|
if "error" in result:
|
|
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
|
return Response(result, status=status.HTTP_201_CREATED)
|
|
except Exception as e:
|
|
return Response(
|
|
{"error": "Failed to create calendar event", "details": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def export_punchlist(request):
|
|
"""
|
|
Export a punchlist to Google Sheets and generate a PDF
|
|
"""
|
|
try:
|
|
# Validate required parameters
|
|
punchlist_id = request.data.get('punchlistId')
|
|
if not punchlist_id:
|
|
return Response(
|
|
{"error": "punchlistId is required"},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Get optional template ID
|
|
template_id = request.data.get('templateId')
|
|
|
|
# Load Google service account key
|
|
try:
|
|
with open(key_file_path, 'r') as f:
|
|
google_service_account_key = json.load(f)
|
|
except FileNotFoundError:
|
|
return Response(
|
|
{"error": "Google service account key file not found"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
return Response(
|
|
{"error": "Invalid Google service account key file format", "details": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Call the export function with the service account key
|
|
from .google import export_punchlist_to_sheet
|
|
result = export_punchlist_to_sheet(google_service_account_key, punchlist_id, template_id)
|
|
|
|
# Handle errors from the export function
|
|
if not result.get('success', False):
|
|
return Response(
|
|
{"error": result.get('message', 'Unknown error during export')},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Return success response with URLs
|
|
return Response({
|
|
"success": True,
|
|
"message": "Punchlist exported successfully",
|
|
"sheetUrl": result.get('sheetUrl'),
|
|
"pdfUrl": result.get('pdfUrl'),
|
|
"alreadyExported": result.get('already_exported', False)
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
# Handle any unexpected errors
|
|
return Response(
|
|
{"error": "Failed to export punchlist", "details": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|