from django.db import models from django.core.validators import MinValueValidator from django.utils.translation import gettext_lazy as _ from decimal import Decimal from inventory.models import Customer # Customer model removed - using inventory.models.Customer instead class SaleOrder(models.Model): """Sales order model""" ORDER_STATUS = [ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('shipped', 'Shipped'), ('delivered', 'Delivered'), ('cancelled', 'Cancelled'), ] order_number = models.CharField(max_length=50, unique=True, help_text='Unique sales order number') customer = models.ForeignKey('inventory.Customer', on_delete=models.CASCADE, related_name='sale_orders') date = models.DateField() expected_delivery_date = models.DateField(blank=True, null=True) status = models.CharField(max_length=20, choices=ORDER_STATUS, default='draft') # Financial information subtotal = models.DecimalField( max_digits=12, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) tax_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) discount_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) shipping_cost = models.DecimalField( max_digits=12, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) total_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))] ) # Additional information notes = models.TextField(blank=True) terms_conditions = models.TextField(blank=True) # User tracking created_by = models.ForeignKey('users.CustomUser', on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-date', '-created_at'] verbose_name = _('Sales Order') verbose_name_plural = _('Sales Orders') def __str__(self): return f"SO-{self.order_number} - {self.customer.name}" def save(self, *args, **kwargs): """Override save to calculate total amount""" self.total_amount = self.subtotal + self.tax_amount - self.discount_amount + self.shipping_cost super().save(*args, **kwargs) def get_item_count(self): """Get total number of items in this sales order""" return self.items.count() def is_overdue(self): """Check if delivery is overdue""" if self.expected_delivery_date and self.status not in ['delivered', 'cancelled']: from django.utils import timezone return timezone.now().date() > self.expected_delivery_date return False def get_profit_margin(self): """Calculate profit margin for this order""" total_cost = Decimal('0') for item in self.items.all(): if item.product.cost_price: total_cost += item.quantity * item.product.cost_price if total_cost > 0: return ((self.total_amount - total_cost) / total_cost) * 100 return Decimal('0') class SaleOrderItem(models.Model): """Individual items in sales orders""" sale_order = models.ForeignKey(SaleOrder, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey('inventory.Product', on_delete=models.CASCADE, related_name='sale_order_items') quantity = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.01'))] ) unit_price = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0'))] ) discount_percent = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))], help_text='Discount percentage (0-100)' ) total_price = models.DecimalField( max_digits=12, decimal_places=2, validators=[MinValueValidator(Decimal('0'))] ) # Additional information description = models.CharField(max_length=200, blank=True) notes = models.TextField(blank=True) class Meta: verbose_name = _('Sales Order Item') verbose_name_plural = _('Sales Order Items') def __str__(self): return f"{self.sale_order.order_number} - {self.product.name} ({self.quantity})" def save(self, *args, **kwargs): """Override save to calculate total price and update stock""" # Calculate total price with discount discount_amount = (self.unit_price * self.quantity * self.discount_percent) / 100 self.total_price = (self.unit_price * self.quantity) - discount_amount super().save(*args, **kwargs) # Update sales order subtotal self.sale_order.subtotal = self.sale_order.items.aggregate( total=models.Sum('total_price') )['total'] or Decimal('0') self.sale_order.save() # Update product stock if order is confirmed or shipped if self.sale_order.status in ['confirmed', 'shipped', 'delivered']: # Create stock movement from inventory.models import StockMovement StockMovement.objects.create( product=self.product, movement_type='sale', quantity=self.quantity, unit_price=self.unit_price, reference_type='SaleOrder', reference_id=self.sale_order.id, date=self.sale_order.date, notes=f"Sale order {self.sale_order.order_number}", created_by=self.sale_order.created_by ) class DeliveryNote(models.Model): """Delivery note for shipped sales orders""" delivery_number = models.CharField(max_length=50, unique=True) sale_order = models.ForeignKey(SaleOrder, on_delete=models.CASCADE, related_name='deliveries') delivery_date = models.DateField() # Delivery details shipping_method = models.CharField(max_length=100, blank=True) tracking_number = models.CharField(max_length=100, blank=True) notes = models.TextField(blank=True) # User tracking delivered_by = models.ForeignKey('users.CustomUser', on_delete=models.SET_NULL, null=True) # Timestamps created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-delivery_date'] verbose_name = _('Delivery Note') verbose_name_plural = _('Delivery Notes') def __str__(self): return f"DN-{self.delivery_number} for SO-{self.sale_order.order_number}"