162 lines
No EOL
6.6 KiB
Python
162 lines
No EOL
6.6 KiB
Python
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) |