예제 #1
0
 def __init__(self, *args, **kwargs):
     super(Order, self).__init__(*args, **kwargs)
     self.addresses = AddressHelper(self, 'order')
예제 #2
0
class Order(models.Model):

    _mutated_purchases = None

    def __init__(self, *args, **kwargs):
        super(Order, self).__init__(*args, **kwargs)
        self.addresses = AddressHelper(self, 'order')

    @staticmethod
    def generate_order_number(order):
        return unicode(order.pk)

    @classmethod
    def from_request(cls, request):
        cart = _cart.models.from_request(request)
        if cart is None or not hasattr(cart, 'order'):
            order = cls()
            order.cart = cart
        else:
            order = cart.order

        if order.needs_calculation():
            order.calculate()

        return order

    number = models.CharField(
        max_length=80,
        blank=True,
        verbose_name=_("order number"),
    )

    created = models.DateTimeField(auto_now_add=True, editable=False)

    modified = models.DateTimeField(auto_now=True, editable=False)

    calculated = models.DateTimeField(editable=False)

    state = models.CharField(
        max_length=20,
        editable=False,
        default=ORDER_STATE_UNPLACED
    )

    customer = models.ForeignKey(
        'customer.Customer',
        null=not CUSTOMER_REQUIRED,
        blank=not CUSTOMER_REQUIRED,
        related_name='orders',
        verbose_name=_("customer"),
    )

    status = models.CharField(
        max_length=40,
        default=ORDER_STATUSES.initial,
        choices=ORDER_STATUSES.choices,
        verbose_name=_("status"),
    )

    cart = models.OneToOneField(
        'cart.Cart',
        related_name='order',
        editable=False,
        null=True,
        on_delete=models.PROTECT
    )

    subtotal = PriceField(verbose_name=_("subtotal"))

    total = PriceField(verbose_name=_("total"))

    paid = PriceField(default=0, components=None, verbose_name=_("paid"))

    def add(self, purchase):
        self._mutate_purchases()
        self._mutated_purchases.append(purchase)
        self.invalidate()

    def update(self, purchase):
        self._mutate_purchases()
        # Replace the persisted purchase (identified by pk) with
        # an (most likely) changed instance.
        idx = self._mutated_purchases.index(purchase)
        self._mutated_purchases[idx] = purchase
        self.invalidate()

    def remove(self, purchase):
        self._mutate_purchases()
        self._mutated_purchases.remove(purchase)
        self.invalidate()

    def clear(self):
        self._mutated_purchases = []
        self.invalidate()

    def needs_calculation(self):
        return (
            self.may_change and self.calculated is None or any(
                [
                    purchase.needs_calculation() for purchase in self
                ]
            )
        )

    def calculate(self):

        zones = {}
        for address in self.addresses:
            zones[address.address_type] = address.get_zone()

        with _customer.models.AddressZone.activate_zones(**zones):

            self._ensure_state(ORDER_STATE_UNPLACED)
            subtotal = Price(0)

            recalculated = False
            for purchase in self:
                # If we calculate order, we always calculate the purchase
                recalculated |= purchase.calculate()
                if purchase.total is not None:
                    subtotal += purchase.total

            # In case our purchases do not belong to a cart and recalculation
            # is required, then it's our responsibility to calculate AND save
            # the purchases. If purchases belong to a cart, then ONLY
            # recalculate the purchases in order to get a correct total.
            if recalculated and self.cart is None:
                self._mutate_purchases()

            subtotal = Price.calculate.with_result(subtotal)(subtotal=True, order=self)
            total = Price.calculate.with_result(subtotal)(total=True, order=self)

        recalculated |= subtotal != self.subtotal or total != self.total
        self.subtotal = subtotal
        self.total = total
        self.calculated = timezone.now()
        return recalculated

    def invalidate(self):
        self._ensure_state(ORDER_STATE_UNPLACED)

        try:
            del self._purchases_queryset
        except AttributeError:
            pass

        self.number = ''
        self.total = None
        self.subtotal = None
        self.calculated = None

    def place(self):
        self._ensure_state(ORDER_STATE_UNPLACED)
        self.number = self.generate_order_number(self)
        self.state = ORDER_STATE_PLACED

    def cancel(self):
        self.state = ORDER_STATE_CANCELLED

    @property
    def is_unplaced(self):
        return self.state == ORDER_STATE_UNPLACED

    @property
    def is_placed(self):
        return self.state == ORDER_STATE_PLACED

    @property
    def is_completed(self):
        return self.state == ORDER_STATE_COMPLETED

    @property
    def is_closed(self):
        return self.state == ORDER_STATE_CLOSED

    @property
    def is_cancelled(self):
        return self.state == ORDER_STATE_CANCELLED

    @property
    def may_cancel(self):
        return self.is_placed and not self.paid.amount

    @property
    def may_change(self):
        return self.state == ORDER_STATE_UNPLACED

    @property
    def is_paid(self):
        return self.total.amount == self.paid.amount

    @property
    def remaining(self):
        return self.total - self.paid

    def can_change_status(self, status):
        can_change = False

        # Verify new status
        if status not in ORDER_STATUSES:
            raise ValueError(status)

        # Lookup current status
        entry = ORDER_STATUSES[self.status]
        config = entry[1] if len(entry) == 2 else {}
        if 'flow' in config:
            # Check against flow
            if status in config['flow']:
                can_change = True
        return can_change

    def clean(self):
        old = None
        if self.pk:
            old = type(self).objects.get(pk=self.pk)
        if old is not None and self.status != old.status:
            if not old.can_change_status(self.status):
                raise ValidationError(
                    "Cannot transition order status "
                    "from '{0}' to '{1}'".format(old.status, self.status)
                )

    def save(self, *args, **kwargs):

        old = None
        if self.pk:
            old = type(self).objects.get(pk=self.pk)

        # See if status is explicitly changed
        if old is not None and self.status != old.status:
            # Make sure this change is a valid flow
            if not old.can_change_status(self.status):
                raise OrderFlowInvalid()

        # Check for new status
        status_changed = (
            old is None and self.status != ORDER_STATUSES.initial or
            old is not None and self.status != old.status
        )

        # Check for new state
        state_changed = (
            old is None and self.state != ORDER_STATE_UNPLACED or
            old is not None and self.state != old.state
        )

        # Check for now paid
        now_paid = (old is None or not old.is_paid) and self.is_paid

        # Get new status from new state
        if (
            not status_changed and state_changed and
            'on_{0}'.format(self.state) in ORDER_STATUSES.hook_to_status
        ):
            self.status = ORDER_STATUSES.hook_to_status['on_{0}'.format(
                self.state)]

        # Get new status from on_paid
        if (
            not status_changed and now_paid and
            'on_paid' in ORDER_STATUSES.hook_to_status
        ):
            self.status = ORDER_STATUSES.hook_to_status['on_paid']

        # Get new state from new status
        if (
            not state_changed and status_changed and
            self.status in ORDER_STATUSES.status_to_state
        ):
            self.state = ORDER_STATUSES.status_to_state[self.status]

        # Check for new status (again)
        status_changed = old is None or self.status != old.status

        # Check for new state (again)
        state_changed = old is None or self.state != old.state

        cart = None
        if state_changed and self.state == ORDER_STATE_PLACED:
            cart = self.cart
            self.cart = None

        # At this point we save
        super(Order, self).save(*args, **kwargs)

        if cart is not None:
            # The order has been placed, we'll want to move over
            # purchases from cart.
            cart.purchases.update(cart=None, order=self)
            cart.delete()

        # Handle mutated purchases in case this order was directly created
        # or modified.
        if self._mutated_purchases is not None:
            for purchase in self._mutated_purchases:
                purchase.order = self
                purchase.save()
            stale = self.purchases.exclude(
                pk__in=[
                    purchase.pk for purchase in self._mutated_purchases
                ]
            )
            stale.delete()

        self._mutated_purchases = None
        try:
            del self._purchases_queryset
        except AttributeError:
            pass

        # Save addresses
        self.addresses.save_or_delete_addresses()

        # Finally signal
        if status_changed:
            order_status_changed.send(
                sender=self,
                order=self,
                new_status=self.status,
                old_status=old.status if old is not None else None
            )

        if state_changed:
            order_state_changed.send(
                sender=self,
                order=self,
                new_state=self.state,
                old_state=old.state if old is not None else None
            )

            # Handle shortcuts
            if self.state == ORDER_STATE_PLACED:
                order_placed.send(sender=self, order=self)
            elif self.state == ORDER_STATE_COMPLETED:
                order_completed.send(sender=self, order=self)
            elif self.state == ORDER_STATE_CANCELLED:
                order_cancelled.send(sender=self, order=self)
            elif self.state == ORDER_STATE_CLOSED:
                order_closed.send(sender=self, order=self)

        if now_paid:
            order_paid.send(sender=self, order=self)

    @cached_property
    def _purchases_queryset(self):
        return self.purchases.polymorphic().all()

    def _ensure_state(self, state):
        if self.state != state:
            raise OrderStateInvalid("State '{0}' expected".format(state))

    def _mutate_purchases(self):
        if self.cart is not None:
            raise Exception("Cannot mutate purchases belonging to cart.")
        if self._mutated_purchases is None:
            self._mutated_purchases = list(self._purchases_queryset)

    def __contains__(self, purchase):
        if self.cart is not None:
            return purchase in self.cart
        elif self._mutated_purchases is not None:
            return purchase in self._mutated_purchases
        return purchase.order == self

    def __iter__(self):
        purchases = self._purchases_queryset
        if self.cart is not None:
            purchases = self.cart
        elif self._mutated_purchases is not None:
            purchases = self._mutated_purchases

        for purchase in purchases:
            yield purchase

    def __len__(self):
        if self.cart is not None:
            return len(self.cart)
        elif self._mutated_purchases is not None:
            return len(self._mutated_purchases)
        return self._purchases_queryset.count()

    def __nonzero__(self):
        return len(self) > 0

    def __unicode__(self):
        if self.number:
            return unicode(_(u"order #{0}").format(unicode(self.number)))
        else:
            return unicode(_(u"unplaced order"))

    class Meta:
        abstract = True
        verbose_name = _("order")
        verbose_name_plural = _("orders")
        ordering = ['-pk']