c3lf-system-3/core/shipments/models.py

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)