205 lines
8.2 KiB
Python
205 lines
8.2 KiB
Python
from django.urls import re_path
|
|
from rest_framework import routers, viewsets, status
|
|
from rest_framework.decorators import api_view, permission_classes, action
|
|
from rest_framework.response import Response
|
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
|
from rest_framework import serializers
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
from .models import Shipment
|
|
from inventory.models import Item
|
|
from tickets.models import IssueThread
|
|
|
|
|
|
class ShipmentSerializer(serializers.ModelSerializer):
|
|
related_items = serializers.PrimaryKeyRelatedField(
|
|
many=True,
|
|
queryset=Item.objects.all(),
|
|
required=False
|
|
)
|
|
related_tickets = serializers.PrimaryKeyRelatedField(
|
|
many=True,
|
|
queryset=IssueThread.objects.all(),
|
|
required=False
|
|
)
|
|
|
|
class Meta:
|
|
model = Shipment
|
|
fields = [
|
|
'id', 'public_secret', 'state', 'review_requirement',
|
|
'recipient_name', 'address_supplements', 'street_address',
|
|
'city', 'state_province', 'postal_code', 'country',
|
|
'related_items', 'related_tickets',
|
|
'created_at', 'updated_at', 'shipped_at', 'completed_at'
|
|
]
|
|
read_only_fields = ['id', 'public_secret', 'created_at', 'updated_at', 'shipped_at', 'completed_at']
|
|
|
|
|
|
class PublicShipmentSerializer(serializers.ModelSerializer):
|
|
related_items = serializers.PrimaryKeyRelatedField(
|
|
many=True,
|
|
read_only=True
|
|
)
|
|
|
|
class Meta:
|
|
model = Shipment
|
|
fields = [
|
|
'id', 'public_secret', 'state', 'review_requirement',
|
|
'recipient_name', 'address_supplements', 'street_address',
|
|
'city', 'state_province', 'postal_code', 'country',
|
|
'related_items', 'created_at'
|
|
]
|
|
read_only_fields = ['id', 'public_secret', 'state', 'review_requirement', 'created_at']
|
|
|
|
|
|
class ShipmentAddressUpdateSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Shipment
|
|
fields = [
|
|
'recipient_name', 'address_supplements', 'street_address',
|
|
'city', 'state_province', 'postal_code', 'country'
|
|
]
|
|
|
|
|
|
class ShipmentViewSet(viewsets.ModelViewSet):
|
|
serializer_class = ShipmentSerializer
|
|
queryset = Shipment.objects.all()
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def add_items(self, request, pk=None):
|
|
shipment = self.get_object()
|
|
review_requirement = request.data.get('review_requirement')
|
|
item_ids = request.data.get('item_ids', [])
|
|
|
|
try:
|
|
items = Item.objects.filter(id__in=item_ids)
|
|
shipment.related_items.add(*items)
|
|
shipment.add_items(review_requirement)
|
|
return Response(ShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def approve(self, request, pk=None):
|
|
shipment = self.get_object()
|
|
try:
|
|
shipment.approve()
|
|
return Response(ShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def reject(self, request, pk=None):
|
|
shipment = self.get_object()
|
|
try:
|
|
shipment.reject()
|
|
return Response(ShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def mark_shipped(self, request, pk=None):
|
|
shipment = self.get_object()
|
|
try:
|
|
# Validate state transition before attempting to mark as shipped
|
|
if shipment.state != Shipment.READY_FOR_SHIPPING:
|
|
return Response(
|
|
{'error': f'Invalid state transition: Cannot mark shipment as shipped from {shipment.state} state. Shipment must be in READY_FOR_SHIPPING state.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
shipment.mark_shipped()
|
|
return Response(ShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def mark_completed(self, request, pk=None):
|
|
shipment = self.get_object()
|
|
try:
|
|
# Validate state transition before attempting to mark as completed
|
|
if shipment.state != Shipment.SHIPPED:
|
|
return Response(
|
|
{'error': f'Invalid state transition: Cannot mark shipment as completed from {shipment.state} state. Shipment must be in SHIPPED state.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
shipment.mark_completed()
|
|
return Response(ShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# Public API endpoints for shipment recipients
|
|
@api_view(['GET'])
|
|
@permission_classes([AllowAny])
|
|
def public_shipment_detail(request, public_secret):
|
|
"""
|
|
Retrieve shipment details using public_secret.
|
|
Only accessible when shipment is in PENDING_REVIEW state.
|
|
"""
|
|
try:
|
|
shipment = Shipment.objects.get(public_secret=public_secret)
|
|
|
|
# Only allow access to shipments in PENDING_REVIEW state
|
|
if shipment.state != Shipment.PENDING_REVIEW:
|
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = PublicShipmentSerializer(shipment)
|
|
return Response(serializer.data)
|
|
except Shipment.DoesNotExist:
|
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([AllowAny])
|
|
def public_shipment_update(request, public_secret):
|
|
"""
|
|
Update shipment with address information and transition state.
|
|
Only accessible when shipment is in PENDING_REVIEW state.
|
|
"""
|
|
try:
|
|
shipment = Shipment.objects.get(public_secret=public_secret)
|
|
|
|
# Only allow updates to shipments in PENDING_REVIEW state
|
|
if shipment.state != Shipment.PENDING_REVIEW:
|
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = ShipmentAddressUpdateSerializer(shipment, data=request.data, partial=True)
|
|
if serializer.is_valid():
|
|
# Update address fields
|
|
for field, value in serializer.validated_data.items():
|
|
setattr(shipment, field, value)
|
|
|
|
# Update the shipment state based on review requirement
|
|
try:
|
|
if shipment.review_requirement == Shipment.REVIEW_ADDRESS:
|
|
# If only address review was required, we can approve directly
|
|
shipment.approve()
|
|
elif shipment.review_requirement == Shipment.REVIEW_BOTH:
|
|
# If both items and address review were required, we need to check if items exist
|
|
if not shipment.related_items.exists():
|
|
return Response(
|
|
{'error': 'Items must be added before approval'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
shipment.approve()
|
|
else:
|
|
# For ITEMS review requirement, we just save the address but don't change state
|
|
shipment.save()
|
|
|
|
return Response(PublicShipmentSerializer(shipment).data)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
except Shipment.DoesNotExist:
|
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
router = routers.SimpleRouter()
|
|
router.register(r'shipments', ShipmentViewSet, basename='shipments')
|
|
|
|
urlpatterns = router.urls + [
|
|
re_path(r'^public/shipments/(?P<public_secret>[0-9a-f-]+)/$', public_shipment_detail, name='public-shipment-detail'),
|
|
re_path(r'^public/shipments/(?P<public_secret>[0-9a-f-]+)/update/$', public_shipment_update, name='public-shipment-update'),
|
|
]
|