Source code for salesman.orders.models

from decimal import Decimal
from secrets import token_urlsafe
from typing import Type

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import TextChoices
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.text import Truncator, slugify
from django.utils.translation import gettext_lazy as _

from salesman.basket.models import Basket
from salesman.conf import app_settings
from salesman.core.models import JSONField

from .signals import status_changed

try:
    # Add support for Wagtail admin.
    from modelcluster.models import ClusterableModel
    from modelcluster.fields import ParentalKey
except ImportError:  # pragma: no cover
    ClusterableModel = models.Model
    ParentalKey = models.ForeignKey


class ParentalForeignKey(ParentalKey):
    """
    Use this foreign key to add support for Wagtail
    in case modelcluster is installed.
    """


[docs]class OrderManager(models.Manager):
[docs] def create_from_request(self, request: HttpRequest, **kwargs) -> 'Order': """ Create new order with reference. Items are still in basket and should be added using ``order.populate_from_basket(basket, request)`` method. Returns: Order: Order instance """ generate_ref = app_settings.SALESMAN_ORDER_REFERENCE_GENERATOR kwargs['ref'] = slugify(generate_ref(request)) return super().create(**kwargs)
[docs] def create_from_basket( self, basket: Basket, request: HttpRequest, **kwargs ) -> 'Order': """ Create and populate new order from basket. Returns: Order: Order instance """ order = self.create_from_request(request, **kwargs) order.populate_from_basket(basket, request) return order
[docs]class Order(ClusterableModel): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, verbose_name=_("User"), ) # A unique reference to this order, could be used as order number. ref = models.SlugField( _("Reference"), max_length=128, unique=True, help_text=_("A unique order reference."), ) # Current order status. status = models.CharField( _("Status"), max_length=128, default='NEW', help_text=_("Changing order status might trigger a notification to customer."), ) # A unique token to allow non-authenticated user access to order. token = models.CharField( _("Token"), max_length=128, unique=True, default=token_urlsafe, help_text=_( "Allow non-authenticated customer to access the order with token. " "To access order suply a '?token={token}' in url querystring." ), ) # Customer contact info. email = models.EmailField(_("Email"), blank=True) shipping_address = models.TextField(_("Shipping address"), blank=True) billing_address = models.TextField(_("Billing address"), blank=True) subtotal = models.DecimalField( _("Subtotal"), max_digits=18, decimal_places=2, default=Decimal(0) ) total = models.DecimalField( _("Total"), max_digits=18, decimal_places=2, default=Decimal(0) ) _extra = JSONField(_("Extra"), blank=True) date_created = models.DateTimeField(_("Date created"), auto_now_add=True) date_updated = models.DateTimeField(_("Date updated"), auto_now=True) objects = OrderManager() # Separate rows from `_extra` to `extra_rows`. extra = None extra_rows = None _current_status = None class Meta: verbose_name = _("Order") verbose_name_plural = _("Orders") ordering = ['-date_created'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.extra = self._extra self.extra_rows = self.extra.pop('rows', []) self._current_status = self.status def __str__(self): return self.ref def save(self, *args, **kwargs): self._extra = dict(self.extra, rows=self.extra_rows) if 'extra' in kwargs.get('update_fields', []): kwargs['update_fields'].remove('extra') kwargs['update_fields'].append('_extra') new_status, old_status = self.status, self._current_status super().save(*args, **kwargs) # Send signal if status changed. if new_status != old_status: status_changed.send( Order, order=self, new_status=new_status, old_status=old_status )
[docs] def pay( self, amount: Decimal, transaction_id: str, payment_method: str = '' ) -> 'OrderPayment': """ Create a new payment for order. Args: amount (Decimal): Amount to add transaction_id (str): ID of transaction payment_method (str, optional): Payment method identifier. Defaults to ''. Returns: OrderPayment: New order payment instance """ return OrderPayment.objects.create( order=self, amount=amount, transaction_id=transaction_id, payment_method=payment_method, )
[docs] @transaction.atomic def populate_from_basket( self, basket: Basket, request: HttpRequest, **kwargs, ) -> None: """ Populate order with items from basket. Args: basket (Basket): Basket instance request (HttpRequest): Django request """ from salesman.basket.serializers import ProductField, ExtraRowsField if not hasattr(basket, 'total'): basket.update(request) self.user = basket.owner self.email = basket.extra.pop('email', '') self.shipping_address = basket.extra.pop('shipping_address', '') self.billing_address = basket.extra.pop('billing_address', '') self.subtotal = basket.subtotal self.total = basket.total self.extra = basket.extra self.extra_rows = ExtraRowsField().to_representation(basket.extra_rows) if self.status == self.statuses.NEW: self.status = self.statuses.CREATED for attr, value in kwargs.items(): setattr(self, attr, value) self.save() for item in basket.get_items(): # Store serialized product with `name` and `code`. product_data = ProductField().to_representation(item.product) product_data.update({'name': item.product.name, 'code': item.product.code}) extra_rows = ExtraRowsField().to_representation(item.extra_rows) OrderItem.objects.create( order=self, product_type=item.product._meta.label, product_content_type=item.product_content_type, product_id=item.product_id, product_data=product_data, unit_price=item.unit_price, subtotal=item.subtotal, total=item.total, quantity=item.quantity, _extra=dict(item.extra, rows=extra_rows), )
@property def status_display(self) -> str: """ Returns display label for current status. """ return str(dict(self.get_statuses().choices).get(self.status, self.status)) status_display.fget.short_description = _("Status") status_display.fget.admin_order_field = 'status' @property def statuses(self) -> Type[TextChoices]: """ Shorthand on order instance to get statuses enum. """ return self.get_statuses() @cached_property def amount_paid(self) -> Decimal: """ Returns amount already paid for this order. """ aggr = self.payments.aggregate(amount=models.Sum('amount')) return Decimal(aggr['amount'] or 0) @property def amount_outstanding(self) -> Decimal: """ Returns amount still needed for order to be paid. """ return Decimal(self.total - self.amount_paid) @property def is_paid(self) -> bool: """ Returns if order is paid in full. """ return self.amount_paid >= self.total
[docs] @classmethod def get_statuses(cls) -> Type[TextChoices]: """ Return order status enum from settings. Defaults to ``salesman.orders.status.OrderStatus``. """ return app_settings.SALESMAN_ORDER_STATUS
[docs]class OrderItem(models.Model): order = ParentalForeignKey( Order, on_delete=models.CASCADE, related_name='items', verbose_name=_("Order"), ) product_type = models.CharField(_("Product type"), max_length=128) # Generic relation to product (optional). product_content_type = models.ForeignKey( ContentType, on_delete=models.SET_NULL, null=True ) product_id = models.PositiveIntegerField(_("Product id"), null=True) product = GenericForeignKey('product_content_type', 'product_id') # Stored product serializer data at the moment of purchase. product_data = JSONField(_("Product data"), blank=True) unit_price = models.DecimalField(_("Unit price"), max_digits=18, decimal_places=2) subtotal = models.DecimalField(_("Subtotal"), max_digits=18, decimal_places=2) total = models.DecimalField(_("Total"), max_digits=18, decimal_places=2) quantity = models.PositiveIntegerField(_("Quantity")) _extra = JSONField(_("Extra"), blank=True) # Separate rows from `_extra` to `extra_rows`. extra = None extra_rows = None class Meta: verbose_name = _("Item") verbose_name_plural = _("Items") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.extra = self._extra self.extra_rows = self.extra.pop('rows', []) def __str__(self): return f'{self.quantity}x {self.name} ({self.code})' def save(self, *args, **kwargs): self._extra = dict(self.extra, rows=self.extra_rows) if 'extra' in kwargs.get('update_fields', []): kwargs['update_fields'].remove('extra') kwargs['update_fields'].append('_extra') super().save(*args, **kwargs) @property def name(self): """ Returns product `name` from stored data. """ return self.product_data.get('name', "(no name)") @property def code(self): """ Returns product `name` from stored data. """ return self.product_data.get('code', "(no code)")
[docs]class OrderPayment(models.Model): order = ParentalForeignKey( Order, on_delete=models.CASCADE, related_name='payments', verbose_name=_("Order"), ) amount = models.DecimalField(_("Amount"), max_digits=18, decimal_places=2) transaction_id = models.CharField(_("Transaction ID"), max_length=128) payment_method = models.CharField(_("Payment method"), max_length=128, blank=True) date_created = models.DateTimeField(_("Date created"), auto_now_add=True) class Meta: verbose_name = _("Payment") verbose_name_plural = _("Payments") unique_together = ('order', 'transaction_id') def __str__(self): return f'{self.amount} ({self.transaction_id})'
[docs] def get_payment_method(self): """ Returns payment method instance. """ from salesman.checkout.payment import payment_methods_pool return payment_methods_pool.get_payment(self.payment_method)
@property def payment_method_display(self): """ Returns display label for payment method. """ payment = self.get_payment_method() return payment.label if payment else self.payment_method payment_method_display.fget.short_description = _("Payment method")
[docs]class OrderNote(models.Model): order = ParentalForeignKey( Order, on_delete=models.CASCADE, related_name='notes', verbose_name=_("Order"), ) message = models.TextField(_("Message")) public = models.BooleanField( _("Public"), default=False, help_text=_("Is accessible to the customer?") ) date_created = models.DateTimeField(_("Date created"), auto_now_add=True) class Meta: verbose_name = _("Note") verbose_name_plural = _("Notes") def __str__(self): return Truncator(self.message).words(3)