import uuid from django.db import models from django.core.exceptions import ValidationError from django.utils import timezone from django_softdelete.models import SoftDeleteModel, SoftDeleteManager from inventory.models import Item from tickets.models import IssueThread class Shipment(SoftDeleteModel): # State choices CREATED = 'CREATED' PENDING_REVIEW = 'PENDING_REVIEW' READY_FOR_SHIPPING = 'READY_FOR_SHIPPING' REJECTED = 'REJECTED' SHIPPED = 'SHIPPED' COMPLETED = 'COMPLETED' SHIPMENT_STATES = [ (CREATED, 'Created'), (PENDING_REVIEW, 'Pending Review'), (READY_FOR_SHIPPING, 'Ready for Shipping'), (REJECTED, 'Rejected'), (SHIPPED, 'Shipped'), (COMPLETED, 'Completed'), ] # Review requirement choices REVIEW_ITEMS = 'ITEMS' REVIEW_ADDRESS = 'ADDRESS' REVIEW_BOTH = 'BOTH' REVIEW_REQUIREMENTS = [ (REVIEW_ITEMS, 'Items Review Required'), (REVIEW_ADDRESS, 'Address Review Required'), (REVIEW_BOTH, 'Both Items and Address Review Required'), ] id = models.AutoField(primary_key=True) public_secret = models.UUIDField(default=uuid.uuid4) state = models.CharField( max_length=20, choices=SHIPMENT_STATES, default=CREATED ) review_requirement = models.CharField( max_length=10, choices=REVIEW_REQUIREMENTS, null=True, blank=True ) # Shipping address fields recipient_name = models.CharField(max_length=255, blank=True, default='') address_supplements = models.TextField(max_length=255, blank=True, default='') street_address = models.CharField(max_length=255, blank=True, default='') city = models.CharField(max_length=100, blank=True, default='') state_province = models.CharField(max_length=100, blank=True, default='') postal_code = models.CharField(max_length=20, blank=True, default='') country = models.CharField(max_length=100, blank=True, default='') related_items = models.ManyToManyField(Item) related_tickets = models.ManyToManyField(IssueThread) created_at = models.DateTimeField(null=True, auto_now_add=True) updated_at = models.DateTimeField(blank=True, null=True) shipped_at = models.DateTimeField(blank=True, null=True) completed_at = models.DateTimeField(blank=True, null=True) all_objects = models.Manager() def __str__(self): return f'[{self.id}] {self.state}' def clean(self): """Validate state transitions and required fields""" if not self.pk: # New instance return old_instance = Shipment.objects.get(pk=self.pk) # Validate state transitions valid_transitions = { self.CREATED: [self.PENDING_REVIEW], self.PENDING_REVIEW: [self.READY_FOR_SHIPPING, self.REJECTED], self.READY_FOR_SHIPPING: [self.SHIPPED], self.SHIPPED: [self.COMPLETED], self.REJECTED: [], # No transitions from rejected self.COMPLETED: [], # No transitions from completed } if (self.state != old_instance.state and self.state not in valid_transitions[old_instance.state]): raise ValidationError(f'Invalid state transition from {old_instance.state} to {self.state}') # Validate review requirement when entering PENDING_REVIEW if self.state == self.PENDING_REVIEW and old_instance.state == self.CREATED: if not self.review_requirement: raise ValidationError('Review requirement must be specified when entering PENDING_REVIEW state') # Validate required fields based on state and review requirement if self.state in [self.READY_FOR_SHIPPING, self.SHIPPED, self.COMPLETED]: if not all([self.recipient_name, self.street_address, self.city, self.state_province, self.postal_code, self.country]): raise ValidationError('All address fields are required for shipping') if self.state != self.CREATED and not self.related_items.exists(): raise ValidationError('Shipment must have at least one item') def add_items(self, review_requirement): """Move shipment to PENDING_REVIEW after items are added""" if self.state == self.CREATED and self.related_items.exists(): if not review_requirement: raise ValidationError('Review requirement must be specified when adding items') if review_requirement not in [self.REVIEW_ITEMS, self.REVIEW_ADDRESS, self.REVIEW_BOTH]: raise ValidationError(f'Invalid review requirement: {review_requirement}') self.review_requirement = review_requirement self.state = self.PENDING_REVIEW self.save() def approve(self): """Approve shipment for shipping""" if self.state == self.PENDING_REVIEW: # Validate that all required reviews are completed if self.review_requirement == self.REVIEW_ITEMS and not self.related_items.exists(): raise ValidationError('Items must be added before approval') elif self.review_requirement == self.REVIEW_ADDRESS and not all([self.recipient_name, self.street_address, self.city, self.state_province, self.postal_code, self.country]): raise ValidationError('Address must be complete before approval') elif self.review_requirement == self.REVIEW_BOTH: if not self.related_items.exists(): raise ValidationError('Items must be added before approval') if not all([self.recipient_name, self.street_address, self.city, self.state_province, self.postal_code, self.country]): raise ValidationError('Address must be complete before approval') self.state = self.READY_FOR_SHIPPING self.save() def reject(self): """Reject shipment""" if self.state == self.PENDING_REVIEW: self.state = self.REJECTED self.save() def mark_shipped(self): """Mark shipment as shipped""" if self.state == self.READY_FOR_SHIPPING: self.state = self.SHIPPED self.shipped_at = timezone.now() self.save() def mark_completed(self): """Mark shipment as completed""" if self.state == self.SHIPPED: self.state = self.COMPLETED self.completed_at = timezone.now() self.save() def save(self, *args, **kwargs): self.clean() super().save(*args, **kwargs)