308 lines
8.4 KiB
Python
308 lines
8.4 KiB
Python
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)
|
|
sorting = models.PositiveIntegerField(blank=True, null=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
objects = ProductManager()
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('dashboard:product-detail', kwargs={'pk': self.pk})
|
|
|
|
class Meta:
|
|
ordering = ['sorting', '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())
|
|
return True if today >= self.valid_from and today <= self.valid_to else False
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
|
|
|
|
|
|
|
|
|
|
class ShippingMethod(models.Model):
|
|
name = models.CharField(max_length=100)
|
|
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
|
|
price = models.DecimalField(
|
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
|
default=0,
|
|
)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('dashboard:shipmeth-detail', kwargs={'pk': self.pk})
|
|
|
|
|
|
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')
|
|
)
|
|
|
|
def with_fulfillment_and_filter(self, query=None):
|
|
return self.annotate(
|
|
total_quantity_fulfilled=models.Sum('lines__quantity_fulfilled'),
|
|
total_quantity_ordered=models.Sum('lines__quantity')
|
|
).filter(pk=query)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
coupon = models.ForeignKey(
|
|
Coupon,
|
|
related_name='orders',
|
|
on_delete=models.SET_NULL,
|
|
null=True
|
|
)
|
|
|
|
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_discount(self):
|
|
if self.coupon:
|
|
if self.coupon.discount_value_type == DiscountValueType.FIXED:
|
|
return self.coupon.discount_value
|
|
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
|
|
return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount
|
|
return Decimal('0')
|
|
|
|
def get_total_price_after_discount(self):
|
|
return round(self.total_net_amount - self.get_discount(), 2)
|
|
|
|
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
|
|
|
|
|
|
class TrackingNumber(models.Model):
|
|
order = models.ForeignKey(
|
|
Order,
|
|
related_name="tracking_numbers",
|
|
editable=False,
|
|
on_delete=models.CASCADE
|
|
)
|
|
tracking_id = models.CharField(max_length=256)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Tracking Number'
|
|
verbose_name_plural = 'Tracking Numbers'
|
|
|
|
def __str__(self):
|
|
return self.tracking_id
|
|
|