import logging from decimal import Decimal from PIL import Image from measurement.measures import Weight from django.db import models from django.db.models.functions import Coalesce from django.conf import settings from django.utils import timezone from django.urls import reverse from django.core.validators import MinValueValidator, MaxValueValidator from django_measurement.models import MeasurementField from accounts.models import User, Address from . import ( DiscountValueType, VoucherType, TransactionStatus, OrderStatus, ShippingMethodType ) from .weight import WeightUnits, zero_weight logger = logging.getLogger(__name__) class ProductManager(models.Manager): def get_queryset(self): return super().get_queryset().annotate( num_ordered=models.Sum('order_lines__quantity') ) class Product(models.Model): name = models.CharField(max_length=250) description = models.TextField(blank=True) sku = models.CharField(max_length=255, unique=True) price = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True, ) weight = MeasurementField( measurement=Weight, unit_choices=WeightUnits.CHOICES, blank=True, null=True ) visible_in_listings = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = ProductManager() def __str__(self): return self.name class ProductPhoto(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE) image = models.ImageField(upload_to='products/images') def __str__(self): return self.product.name # def save(self, *args, **kwargs): # super().save(*args, **kwargs) # img = Image.open(self.image.path) # if img.height > 400 or img.width > 400: # output_size = (400, 400) # img.thumbnail(output_size) # img.save(self.image.path) class Coupon(models.Model): type = models.CharField( max_length=20, choices=VoucherType.CHOICES, default=VoucherType.ENTIRE_ORDER ) name = models.CharField(max_length=255, null=True, blank=True) code = models.CharField(max_length=12, unique=True, db_index=True) valid_from = models.DateTimeField(default=timezone.now) valid_to = models.DateTimeField(null=True, blank=True) discount_value_type = models.CharField( max_length=10, choices=DiscountValueType.CHOICES, default=DiscountValueType.FIXED, ) discount_value = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, ) products = models.ManyToManyField(Product, blank=True) class Meta: ordering = ("code",) @property def is_valid(self): today = timezone.localtime(timezone.now()).date() return True if today >= self.valid_from and today <= self.valid_to else False class ShippingMethod(models.Model): name = models.CharField(max_length=100) type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES) class OrderManager(models.Manager): def with_lines(self): return self.select_related('lines') def with_fulfillment(self): return self.annotate( total_quantity_fulfilled=models.Sum('lines__quantity_fulfilled'), total_quantity_ordered=models.Sum('lines__quantity') ) class Order(models.Model): customer = models.ForeignKey( User, related_name="orders", on_delete=models.SET_NULL, null=True ) status = models.CharField( max_length=32, default=OrderStatus.UNFULFILLED, choices=OrderStatus.CHOICES ) billing_address = models.ForeignKey( Address, related_name="+", editable=False, null=True, on_delete=models.SET_NULL, ) shipping_address = models.ForeignKey( Address, related_name="+", editable=False, null=True, on_delete=models.SET_NULL, ) shipping_method = models.ForeignKey( ShippingMethod, blank=True, null=True, related_name="orders", on_delete=models.SET_NULL, ) total_net_amount = models.DecimalField( max_digits=10, decimal_places=2, default=0, ) weight = MeasurementField( measurement=Weight, unit_choices=WeightUnits.CHOICES, default=zero_weight, ) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True) objects = OrderManager() def get_total_quantity(self): return sum([line.quantity for line in self]) def get_absolute_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) class Transaction(models.Model): status = models.CharField( max_length=32, blank=True, default=TransactionStatus.CREATED, choices=TransactionStatus.CHOICES ) paypal_id = models.CharField(max_length=64, blank=True) confirmation_email_sent = models.BooleanField(default=False) order = models.OneToOneField( Order, editable=False, blank=True, null=True, on_delete=models.CASCADE ) class OrderLine(models.Model): order = models.ForeignKey( Order, related_name="lines", editable=False, on_delete=models.CASCADE ) product = models.ForeignKey( Product, related_name="order_lines", on_delete=models.SET_NULL, blank=True, null=True, ) quantity = models.IntegerField(validators=[MinValueValidator(1)]) quantity_fulfilled = models.IntegerField( validators=[MinValueValidator(0)], default=0 ) customer_note = models.TextField(blank=True, default="") currency = models.CharField( max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, default=settings.DEFAULT_CURRENCY, ) unit_price = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, ) tax_rate = models.DecimalField( max_digits=5, decimal_places=2, default=Decimal("0.0") ) def get_total(self): return self.unit_price * self.quantity @property def quantity_unfulfilled(self): return self.quantity - self.quantity_fulfilled