diff --git a/assignment/migrations/0002_assignmentitem_role.py b/assignment/migrations/0002_assignmentitem_role.py new file mode 100644 index 0000000..0b8a7d4 --- /dev/null +++ b/assignment/migrations/0002_assignmentitem_role.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-09 01:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='assignmentitem', + name='role', + field=models.CharField(choices=[('pickup', 'Pickup'), ('delivery', 'Delivery')], default='delivery', max_length=10), + ), + ] diff --git a/assignment/models/assignment_item.py b/assignment/models/assignment_item.py index c649b9c..dadb016 100644 --- a/assignment/models/assignment_item.py +++ b/assignment/models/assignment_item.py @@ -1,15 +1,23 @@ from django.db import models - from assignment.models.assignment import Assignment from shipments.models import Shipment class AssignmentItem(models.Model): + ROLE_CHOICES = [ + ("pickup", "Pickup"), + ("delivery", "Delivery"), + ] + assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name='items') shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE) - delivery_sequence = models.PositiveIntegerField() # 1st, 2nd, 3rd drop, etc. + delivery_sequence = models.PositiveIntegerField() # 1st, 2nd, 3rd stop, etc. delivery_location = models.JSONField() # { "lat": ..., "lng": ... } + role = models.CharField(max_length=10, choices=ROLE_CHOICES, default="delivery") # NEW + + # TODO: Consider renaming 'is_delivered' and 'delivered_at' for better clarity. + # Example: 'is_delivered' -> 'has_been_delivered', 'delivered_at' -> 'delivery_timestamp'. is_delivered = models.BooleanField(default=False) delivered_at = models.DateTimeField(null=True, blank=True) @@ -17,4 +25,4 @@ class Meta: ordering = ['delivery_sequence'] def __str__(self): - return f"Shipment {self.shipment.id} in Assignment {self.assignment.id}" + return f"{self.role.capitalize()} for Shipment {self.shipment.id} in Assignment {self.assignment.id}" diff --git a/assignment/serializers.py b/assignment/serializers.py deleted file mode 100644 index 6067b65..0000000 --- a/assignment/serializers.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import serializers - -from assignment.models.assignment import Assignment - - -class AssignmentSerializer(serializers.ModelSerializer): - class Meta: - model = Assignment - fields = '__all__' diff --git a/assignment/serializers/__init__.py b/assignment/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assignment/serializers/assignment.py b/assignment/serializers/assignment.py new file mode 100644 index 0000000..2fe7bd2 --- /dev/null +++ b/assignment/serializers/assignment.py @@ -0,0 +1,11 @@ +from rest_framework import serializers +from assignment.models.assignment import Assignment +from assignment.serializers.assignment_item import AssignmentItemSerializer + +class AssignmentSerializer(serializers.ModelSerializer): + items = AssignmentItemSerializer(many=True, read_only=True) + vehicle = serializers.CharField(source='vehicle.vehicle_id', read_only=True) + + class Meta: + model = Assignment + fields = ['id', 'vehicle', 'total_load', 'status', 'items'] \ No newline at end of file diff --git a/assignment/serializers/assignment_item.py b/assignment/serializers/assignment_item.py new file mode 100644 index 0000000..72c67a4 --- /dev/null +++ b/assignment/serializers/assignment_item.py @@ -0,0 +1,16 @@ +from rest_framework import serializers +from assignment.models.assignment_item import AssignmentItem +from shipments.models import Shipment + + +class ShipmentSerializerForAssignment(serializers.ModelSerializer): + class Meta: + model = Shipment + fields = ['id', 'order_id', 'demand', 'status'] # Add more as needed + +class AssignmentItemSerializer(serializers.ModelSerializer): + shipment = ShipmentSerializerForAssignment(read_only=True) + + class Meta: + model = AssignmentItem + fields = ['shipment', 'role', 'delivery_sequence', 'delivery_location', 'is_delivered', 'delivered_at'] diff --git a/assignment/services/assignment_planner.py b/assignment/services/assignment_planner.py index f333d0f..3e2b7cd 100644 --- a/assignment/services/assignment_planner.py +++ b/assignment/services/assignment_planner.py @@ -54,6 +54,7 @@ def plan_assignments(self) -> List[Assignment]: logger.error("Optimizer failed to find a solution.") raise Exception("Optimization failed") + # Implicit mapping of vehicle in this and vehicle in vrp solver assignments = [] for i, route in enumerate(result["routes"]): vehicle = self.vehicles[i] @@ -66,6 +67,10 @@ def plan_assignments(self) -> List[Assignment]: status='created' ) + # Update vehicle status, not using methods in the vehicle model but ORM directly + vehicle.status = "assigned" + vehicle.save(update_fields=["status"]) + seq = 1 for node in route: if node in vrp_input.task_index_map: @@ -81,7 +86,8 @@ def plan_assignments(self) -> List[Assignment]: delivery_location={ "lat": loc["lat"], "lng": loc["lng"], - } + }, + role=role ) seq += 1 diff --git a/assignment/tests/test_assignment_api.py b/assignment/tests/test_assignment_api.py new file mode 100644 index 0000000..14e8dda --- /dev/null +++ b/assignment/tests/test_assignment_api.py @@ -0,0 +1,173 @@ +import uuid + +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from fleet.models import Vehicle +from shipments.models import Shipment +from assignment.models.assignment import Assignment +from assignment.models.assignment_item import AssignmentItem + + +class AssignmentAPITests(APITestCase): + def setUp(self): + self.vehicle = Vehicle.objects.create( + vehicle_id="TRK001", + name="Truck 1", + capacity=1000, + status="available", + fuel_type="diesel" + ) + + self.shipment = Shipment.objects.create( + shipment_id=str(uuid.uuid4()), + order_id="ORD001", + demand=500, + origin={"lat": 7.2, "lng": 80.1}, + destination={"lat": 7.3, "lng": 80.2}, + status="pending" + ) + + self.create_url = reverse("assignment-list") + self.by_vehicle_url = lambda v_id: reverse("assignment-by-vehicle", kwargs={"vehicle_id": v_id}) + + def test_create_assignment(self): + payload = { + "deliveries": [ + { + "shipment_id": self.shipment.id, + "location": {"lat": 7.2, "lng": 80.1}, + "sequence": 1, + "load": 500, + "role": "pickup" + }, + { + "shipment_id": self.shipment.id, + "location": {"lat": 7.3, "lng": 80.2}, + "sequence": 2, + "load": 0, + "role": "delivery" + } + ] + } + + response = self.client.post(self.create_url, payload, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Assignment.objects.count(), 1) + self.assertEqual(AssignmentItem.objects.count(), 2) + + assignment = Assignment.objects.first() + self.assertEqual(assignment.vehicle.vehicle_id, "TRK001") + self.assertEqual(assignment.total_load, 500) + + def test_get_assignment_by_vehicle(self): + assignment = Assignment.objects.create( + vehicle=self.vehicle, + total_load=500, + status="created" + ) + AssignmentItem.objects.create( + assignment=assignment, + shipment=self.shipment, + delivery_sequence=1, + delivery_location={"lat": 7.2, "lng": 80.1}, + role="pickup" + ) + + response = self.client.get(self.by_vehicle_url("TRK001")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["vehicle"], self.vehicle.vehicle_id) + self.assertEqual(len(response.data["items"]), 1) + + def test_arrival_at_sequence_returns_correct_actions(self): + assignment = Assignment.objects.create( + vehicle=self.vehicle, + total_load=500, + status="created" + ) + AssignmentItem.objects.create( + assignment=assignment, + shipment=self.shipment, + delivery_sequence=1, + delivery_location={"lat": 7.2, "lng": 80.1}, + role="pickup" + ) + AssignmentItem.objects.create( + assignment=assignment, + shipment=self.shipment, + delivery_sequence=2, + delivery_location={"lat": 7.3, "lng": 80.2}, + role="delivery" + ) + + # arrive_url = reverse("assignment-arrive-sequence", kwargs={"pk": assignment.pk, "sequence": 2}) + arrive_url = f"/api/assignment/assignments/{assignment.pk}/arrive/sequence/2/" + response = self.client.post(arrive_url, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["location"], {"lat": 7.3, "lng": 80.2}) + self.assertEqual(len(response.data["actions"]), 1) + self.assertEqual(response.data["actions"][0]["role"], "delivery") + self.assertEqual(response.data["actions"][0]["shipment_id"], self.shipment.id) + + def test_arrival_with_multiple_actions_at_same_location(self): + assignment = Assignment.objects.create( + vehicle=self.vehicle, + total_load=800, + status="created" + ) + + # Add another shipment + shipment2 = Shipment.objects.create( + shipment_id=str(uuid.uuid4()), + order_id="ORD002", + demand=300, + origin={"lat": 7.1, "lng": 80.0}, + destination={"lat": 7.3, "lng": 80.2}, # SAME location as the first delivery + status="pending" + ) + + # First shipment's delivery + AssignmentItem.objects.create( + assignment=assignment, + shipment=self.shipment, + delivery_sequence=2, + delivery_location={"lat": 7.3, "lng": 80.2}, + role="delivery" + ) + + # Second shipment's delivery — same place + AssignmentItem.objects.create( + assignment=assignment, + shipment=shipment2, + delivery_sequence=3, + delivery_location={"lat": 7.3, "lng": 80.2}, + role="delivery" + ) + + # Call the arrival endpoint at sequence 2 (first of the two) + arrive_url = f"/api/assignment/assignments/{assignment.pk}/arrive/sequence/2/" + response = self.client.post(arrive_url, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["location"], {"lat": 7.3, "lng": 80.2}) + self.assertEqual(len(response.data["actions"]), 2) + + roles = [a["role"] for a in response.data["actions"]] + shipment_ids = [a["shipment_id"] for a in response.data["actions"]] + + self.assertIn("delivery", roles) + self.assertIn(self.shipment.id, shipment_ids) + self.assertIn(shipment2.id, shipment_ids) + + def test_arrival_with_invalid_sequence_returns_404(self): + assignment = Assignment.objects.create( + vehicle=self.vehicle, + total_load=500, + status="created" + ) + # arrive_url = reverse("assignment-arrive-sequence", kwargs={"pk": assignment.pk, "sequence": 99}) + arrive_url = f"/api/assignment/assignments/{assignment.pk}/arrive/sequence/99/" + response = self.client.post(arrive_url, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/assignment/tests/test_assignment_complete.py b/assignment/tests/test_assignment_complete.py new file mode 100644 index 0000000..fcbe86c --- /dev/null +++ b/assignment/tests/test_assignment_complete.py @@ -0,0 +1,91 @@ +import uuid +from django.utils import timezone +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from fleet.models import Vehicle +from shipments.models import Shipment +from assignment.models.assignment import Assignment +from assignment.models.assignment_item import AssignmentItem + + +class AssignmentActionCompletionTests(APITestCase): + def setUp(self): + self.vehicle = Vehicle.objects.create( + vehicle_id="TRK001", + name="Truck 1", + capacity=1000, + status="available", + fuel_type="diesel" + ) + + self.shipment = Shipment.objects.create( + shipment_id=str(uuid.uuid4()), + order_id="ORD001", + demand=500, + origin={"lat": 7.2, "lng": 80.1}, + destination={"lat": 7.3, "lng": 80.2}, + status="in_transit" + ) + + self.assignment = Assignment.objects.create( + vehicle=self.vehicle, + total_load=500, + status="created" + ) + + self.pickup_item = AssignmentItem.objects.create( + assignment=self.assignment, + shipment=self.shipment, + delivery_sequence=1, + delivery_location=self.shipment.origin, + role="pickup", + is_delivered=False + ) + + self.delivery_item = AssignmentItem.objects.create( + assignment=self.assignment, + shipment=self.shipment, + delivery_sequence=2, + delivery_location=self.shipment.destination, + role="delivery", + is_delivered=False + ) + + def test_confirm_delivery_action_successfully(self): + url = f"/api/assignment/assignments/{self.assignment.id}/actions/{self.delivery_item.id}/complete/" + response = self.client.post(url, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Delivery confirmed") + self.assertEqual(response.data["shipment_id"], self.shipment.id) + self.assertEqual(response.data["new_status"], "delivered") + + def test_confirm_pickup_action_successfully(self): + self.shipment.status = "scheduled" + self.shipment.save() + + url = f"/api/assignment/assignments/{self.assignment.id}/actions/{self.pickup_item.id}/complete/" + response = self.client.post(url, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Pickup confirmed") + self.assertEqual(response.data["shipment_id"], self.shipment.id) + self.assertEqual(response.data["new_status"], "in_transit") + + def test_confirm_action_invalid_assignment_item(self): + url = f"/api/assignment/assignments/{self.assignment.id}/actions/9999/complete/" + response = self.client.post(url, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_confirm_already_completed_action(self): + self.delivery_item.is_delivered = True + self.delivery_item.delivered_at = timezone.now() + self.delivery_item.save() + + url = f"/api/assignment/assignments/{self.assignment.id}/actions/{self.delivery_item.id}/complete/" + response = self.client.post(url, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Already marked complete") diff --git a/assignment/urls.py b/assignment/urls.py index d1d1500..8c4dd67 100644 --- a/assignment/urls.py +++ b/assignment/urls.py @@ -3,7 +3,7 @@ from .views import AssignmentViewSet router = DefaultRouter() -router.register(r'assignments', AssignmentViewSet) +router.register(r'assignments', AssignmentViewSet, basename='assignment') urlpatterns = [ path('', include(router.urls)), diff --git a/assignment/views.py b/assignment/views.py index 6abfff1..b1f31e1 100644 --- a/assignment/views.py +++ b/assignment/views.py @@ -1,11 +1,13 @@ +from django.utils import timezone from rest_framework import viewsets, status +from rest_framework.decorators import action from rest_framework.response import Response from .models.assignment import Assignment from .models.assignment_item import AssignmentItem -from .serializers import AssignmentSerializer -from fleet.models import Vehicle +from fleet.models import Vehicle, VehicleLocation from shipments.models import Shipment +from .serializers.assignment import AssignmentSerializer class AssignmentViewSet(viewsets.ModelViewSet): @@ -17,30 +19,29 @@ def create(self, request, *args, **kwargs): if not deliveries: return Response({"error": "Deliveries required"}, status=400) - # Calculate total load total_load = sum(d.get("load", 0) for d in deliveries) - - # Find an available vehicle that can handle the load vehicle = Vehicle.objects.filter(status="available", capacity__gte=total_load).first() if not vehicle: return Response({"error": "No available vehicle for the load"}, status=400) - # Update vehicle status vehicle.status = "assigned" vehicle.save() - # Create Assignment assignment = Assignment.objects.create( vehicle=vehicle, total_load=total_load, status='created' ) - # Create AssignmentItem entries for delivery in deliveries: shipment_id = delivery.get("shipment_id") location = delivery.get("location") - sequence = delivery.get("sequence", 1) # fallback if sequence not provided + sequence = delivery.get("sequence", 1) + role = delivery.get("role") + + if role not in ["pickup", "delivery"]: + return Response({"error": f"Invalid role for shipment {shipment_id}. Must be 'pickup' or 'delivery'."}, + status=400) try: shipment = Shipment.objects.get(id=shipment_id) @@ -52,7 +53,100 @@ def create(self, request, *args, **kwargs): shipment=shipment, delivery_sequence=sequence, delivery_location=location, + role=role ) serializer = self.get_serializer(assignment) return Response(serializer.data, status=status.HTTP_201_CREATED) + + @action(detail=False, methods=["get"], url_path="by-vehicle/(?P[^/.]+)") + def by_vehicle(self, request, vehicle_id=None): + try: + vehicle = Vehicle.objects.get(vehicle_id=vehicle_id) + except Vehicle.DoesNotExist: + return Response({"error": "Vehicle not found"}, status=404) + + assignment = Assignment.objects.filter(vehicle=vehicle).order_by('-id').first() + if not assignment: + return Response({"message": "No assignment found for this vehicle"}, status=404) + + serializer = self.get_serializer(assignment) + return Response(serializer.data) + + @action(detail=True, methods=['post'], url_path='arrive/sequence/(?P[0-9]+)') + def mark_arrival(self, request, pk=None, sequence=None): + assignment = self.get_object() + vehicle = assignment.vehicle + sequence = int(sequence) + + try: + current_item = assignment.items.get(delivery_sequence=sequence) + except AssignmentItem.DoesNotExist: + return Response({"error": f"No assignment item found at sequence {sequence}"}, status=404) + + location = current_item.delivery_location + lat, lng = location.get("lat"), location.get("lng") + + if lat is None or lng is None: + return Response({"error": "Location data is missing in assignment item"}, status=400) + + vehicle.update_location(lat, lng) + VehicleLocation.objects.create( + vehicle=vehicle, + latitude=lat, + longitude=lng, + ) + + items_at_location = assignment.items.filter( + delivery_location=location, + delivery_sequence__gte=sequence + ).order_by("delivery_sequence") + + grouped = [ + { + "assignment_item_id": item.id, + "role": item.role, + "shipment_id": item.shipment.id, + "shipment_status": item.shipment.status, + "location": item.delivery_location, + "is_delivered": item.is_delivered + } + for item in items_at_location + ] + + return Response({ + "vehicle": vehicle.vehicle_id, + "arrived_at": timezone.now(), + "location": location, + "actions": grouped + }) + + @action(detail=True, methods=["post"], url_path="actions/(?P[0-9]+)/complete") + def mark_action_complete(self, request, pk=None, item_id=None): + try: + assignment = self.get_object() + item = assignment.items.get(id=item_id) + except AssignmentItem.DoesNotExist: + return Response({"error": "Assignment item not found"}, status=404) + + if item.is_delivered: + return Response({"message": "Already marked complete"}, status=200) + + item.is_delivered = True + item.delivered_at = timezone.now() + item.save(update_fields=["is_delivered", "delivered_at"]) + + # Optional: update shipment status + if item.role == "delivery": + item.shipment.mark_delivered() + elif item.role == "pickup": + item.shipment.mark_dispatched() + item.shipment.mark_in_transit() + item.shipment.save() + + return Response({ + "message": f"{item.role.title()} confirmed", + "shipment_id": item.shipment.id, + "new_status": item.shipment.status, + "timestamp": item.delivered_at + }, status=200) diff --git a/fleet/services/__init__.py b/fleet/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fleet/services/status_services.py b/fleet/services/status_services.py new file mode 100644 index 0000000..0afb4af --- /dev/null +++ b/fleet/services/status_services.py @@ -0,0 +1,19 @@ +from django.utils import timezone +from fleet.models import Vehicle + +def update_vehicle_status(vehicle: Vehicle, new_status: str): + vehicle.status = new_status + vehicle.updated_at = timezone.now() + vehicle.save(update_fields=['status', 'updated_at']) + +def mark_vehicle_available(vehicle: Vehicle): + update_vehicle_status(vehicle, 'available') + +def mark_vehicle_assigned(vehicle: Vehicle): + update_vehicle_status(vehicle, 'assigned') + +def mark_vehicle_maintenance(vehicle: Vehicle): + update_vehicle_status(vehicle, 'maintenance') + +def mark_vehicle_out_of_service(vehicle: Vehicle): + update_vehicle_status(vehicle, 'out_of_service') diff --git a/fleet/tests/test_vehicle_api.py b/fleet/tests/test_vehicle_api.py index 17f86a1..502acb4 100644 --- a/fleet/tests/test_vehicle_api.py +++ b/fleet/tests/test_vehicle_api.py @@ -98,3 +98,46 @@ def test_update_vehicle_location(self): self.assertEqual(history.count(), 1) self.assertAlmostEqual(float(history[0].speed), 65.5) self.assertAlmostEqual(float(history[0].latitude), 42.123456) + + def test_list_all_vehicles(self): + response = self.client.get("/api/fleet/vehicles/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 3) + + def test_filter_by_status(self): + response = self.client.get("/api/fleet/vehicles/?status=assigned") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['vehicle_id'], "TRK003") + + def test_ordering_by_updated_at(self): + response = self.client.get("/api/fleet/vehicles/?ordering=-updated_at") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(len(response.data) >= 1) + self.assertIn('updated_at', response.data[0]) + + def test_mark_vehicle_available(self): + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle3.id}/mark_available/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.vehicle3.refresh_from_db() + self.assertEqual(self.vehicle3.status, 'available') + + def test_mark_vehicle_assigned(self): + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.id}/mark_assigned/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.vehicle1.refresh_from_db() + self.assertEqual(self.vehicle1.status, 'assigned') + + def test_change_status_to_available(self): + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle2.id}/change_status/", { + "status": "available" + }, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.vehicle2.refresh_from_db() + self.assertEqual(self.vehicle2.status, "available") + + def test_change_status_invalid(self): + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.id}/change_status/", { + "status": "nonexistent" + }, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/fleet/views/vehicle.py b/fleet/views/vehicle.py index 1ece585..8fb24b0 100644 --- a/fleet/views/vehicle.py +++ b/fleet/views/vehicle.py @@ -1,6 +1,8 @@ import os import django +from fleet.services.status_services import mark_vehicle_assigned, mark_vehicle_available, update_vehicle_status + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'logistics_core.settings') django.setup() @@ -58,9 +60,34 @@ def get_queryset(self): queryset = queryset.filter(status='available') if depot := params.get('depot_id'): queryset = queryset.filter(depot_id=depot) - + queryset = queryset.order_by('-updated_at') return queryset + @action(detail=True, methods=['post']) + def mark_available(self, request, pk=None): + vehicle = self.get_object() + mark_vehicle_available(vehicle) + return Response({'vehicle_id': vehicle.vehicle_id, 'status': 'available'}) + + @action(detail=True, methods=['post']) + def mark_assigned(self, request, pk=None): + vehicle = self.get_object() + mark_vehicle_assigned(vehicle) + return Response({'vehicle_id': vehicle.vehicle_id, 'status': 'assigned'}) + + # Admin only + @action(detail=True, methods=['post']) + def change_status(self, request, pk=None): + vehicle = self.get_object() + new_status = request.data.get('status') + + valid_statuses = dict(Vehicle.STATUS_CHOICES).keys() + if new_status not in valid_statuses: + return Response({'error': f'Invalid status. Must be one of {list(valid_statuses)}'}, status=400) + + update_vehicle_status(vehicle, new_status) + return Response({'vehicle_id': vehicle.vehicle_id, 'status': new_status}) + @action(detail=True, methods=['post']) def update_location(self, request, pk=None): vehicle = self.get_object() @@ -85,38 +112,6 @@ def update_location(self, request, pk=None): except Exception as e: return Response({'error': str(e)}, status=400) - @action(detail=True, methods=['post']) - def change_status(self, request, pk=None): - vehicle = self.get_object() - new_status = request.data.get('status') - - if not new_status: - return Response({'error': 'Status is required'}, status=400) - - if new_status not in dict(Vehicle.STATUS_CHOICES): - return Response({'error': f'Invalid status: {new_status}'}, status=400) - - if new_status == 'maintenance' and vehicle.status != 'maintenance' and settings.ENABLE_FLEET_EXTENDED_MODELS: - maintenance_type = request.data.get('maintenance_type', 'routine') - description = request.data.get('description', 'Routine maintenance') - scheduled_date = request.data.get('scheduled_date', timezone.now().date().isoformat()) - try: - scheduled_date = datetime.fromisoformat(scheduled_date).date() - except ValueError: - scheduled_date = timezone.now().date() - - MaintenanceRecord.objects.create( - vehicle=vehicle, - maintenance_type=maintenance_type, - description=description, - scheduled_date=scheduled_date, - status='in_progress' - ) - - vehicle.status = new_status - vehicle.save(update_fields=['status', 'updated_at']) - return Response(VehicleSerializer(vehicle).data) - @action(detail=True, methods=['post']) def assign_depot(self, request, pk=None): """ @@ -197,3 +192,36 @@ def depot_stats(self, request): ).order_by('depot_id') return Response({'by_depot': stats}) + + # # To be implemented with maintenance part + # @action(detail=True, methods=['post']) + # def change_status(self, request, pk=None): + # vehicle = self.get_object() + # new_status = request.data.get('status') + # + # if not new_status: + # return Response({'error': 'Status is required'}, status=400) + # + # if new_status not in dict(Vehicle.STATUS_CHOICES): + # return Response({'error': f'Invalid status: {new_status}'}, status=400) + # + # if new_status == 'maintenance' and vehicle.status != 'maintenance' and settings.ENABLE_FLEET_EXTENDED_MODELS: + # maintenance_type = request.data.get('maintenance_type', 'routine') + # description = request.data.get('description', 'Routine maintenance') + # scheduled_date = request.data.get('scheduled_date', timezone.now().date().isoformat()) + # try: + # scheduled_date = datetime.fromisoformat(scheduled_date).date() + # except ValueError: + # scheduled_date = timezone.now().date() + # + # MaintenanceRecord.objects.create( + # vehicle=vehicle, + # maintenance_type=maintenance_type, + # description=description, + # scheduled_date=scheduled_date, + # status='in_progress' + # ) + # + # vehicle.status = new_status + # vehicle.save(update_fields=['status', 'updated_at']) + # return Response(VehicleSerializer(vehicle).data) \ No newline at end of file