class AbstractBasket(models.Model): """ Basket object """ # Baskets can be anonymously owned - hence this field is nullable. When a # anon user signs in, their two baskets are merged. owner = models.ForeignKey('auth.User', related_name='baskets', null=True, verbose_name=_("Owner")) # Basket statuses # - Frozen is for when a basket is in the process of being submitted # and we need to prevent any changes to it. OPEN, MERGED, SAVED, FROZEN, SUBMITTED = ("Open", "Merged", "Saved", "Frozen", "Submitted") STATUS_CHOICES = ( (OPEN, _("Open - currently active")), (MERGED, _("Merged - superceded by another basket")), (SAVED, _("Saved - for items to be purchased later")), (FROZEN, _("Frozen - the basket cannot be modified")), (SUBMITTED, _("Submitted - has been ordered at the checkout")), ) status = models.CharField(_("Status"), max_length=128, default=OPEN, choices=STATUS_CHOICES) # A basket can have many vouchers attached to it. However, it is common # for sites to only allow one voucher per basket - this will need to be # enforced in the project's codebase. vouchers = models.ManyToManyField('voucher.Voucher', null=True, verbose_name=_("Vouchers"), blank=True) date_created = models.DateTimeField(_("Date created"), auto_now_add=True) date_merged = models.DateTimeField(_("Date merged"), null=True, blank=True) date_submitted = models.DateTimeField(_("Date submitted"), null=True, blank=True) # Only if a basket is in one of these statuses can it be edited editable_statuses = (OPEN, SAVED) class Meta: abstract = True verbose_name = _('Basket') verbose_name_plural = _('Baskets') objects = models.Manager() open = OpenBasketManager() saved = SavedBasketManager() def __init__(self, *args, **kwargs): super(AbstractBasket, self).__init__(*args, **kwargs) # We keep a cached copy of the basket lines as we refer to them often # within the same request cycle. Also, applying offers will append # discount data to the basket lines which isn't persisted to the DB and # so we want to avoid reloading them as this would drop the discount # information. self._lines = None # Cached queryset of lines self.offer_applications = results.OfferApplications() self.exempt_from_tax = False def __unicode__(self): return _( u"%(status)s basket (owner: %(owner)s, lines: %(num_lines)d)") % { 'status': self.status, 'owner': self.owner, 'num_lines': self.num_lines } def all_lines(self): """ Return a cached set of basket lines. This is important for offers as they alter the line models and you don't want to reload them from the DB as that information would be lost. """ if self.id is None: return query.EmptyQuerySet(model=self.__class__) if self._lines is None: self._lines = self.lines.select_related( 'product', 'product__stockrecord').all().prefetch_related( 'attributes', 'product__images') return self._lines def is_quantity_allowed(self, qty): basket_threshold = settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD if basket_threshold: total_basket_quantity = self.num_items max_allowed = basket_threshold - total_basket_quantity if qty > max_allowed: return False, _( "Due to technical limitations we are not able " "to ship more than %(threshold)d items in one order.") % { 'threshold': basket_threshold, } return True, None # ============ # Manipulation # ============ def flush(self): """Remove all lines from basket.""" if self.status == self.FROZEN: raise PermissionDenied("A frozen basket cannot be flushed") self.lines.all().delete() self._lines = None def add_product(self, product, quantity=1, options=None): """ Add a product to the basket The 'options' list should contains dicts with keys 'option' and 'value' which link the relevant product.Option model and string value respectively. """ if options is None: options = [] if not self.id: self.save() # Line reference is used to distinguish between variations of the same # product (eg T-shirts with different personalisations) line_ref = self._create_line_reference(product, options) # Determine price to store (if one exists). It is only stored for # audit and sometimes caching. price_excl_tax, price_incl_tax = None, None if product.has_stockrecord: stockrecord = product.stockrecord if stockrecord: price_excl_tax = getattr(stockrecord, 'price_excl_tax', None) price_incl_tax = getattr(stockrecord, 'price_incl_tax', None) line, created = self.lines.get_or_create(line_reference=line_ref, product=product, defaults={ 'quantity': quantity, 'price_excl_tax': price_excl_tax, 'price_incl_tax': price_incl_tax }) if created: for option_dict in options: line.attributes.create(option=option_dict['option'], value=option_dict['value']) else: line.quantity += quantity line.save() self.reset_offer_applications() add_product.alters_data = True def applied_offers(self): """ Return a dict of offers successfully applied to the basket. This is used to compare offers before and after a basket change to see if there is a difference. """ return self.offer_applications.offers def reset_offer_applications(self): """ Remove any discounts so they get recalculated """ self.offer_applications = results.OfferApplications() self._lines = None def merge_line(self, line, add_quantities=True): """ For transferring a line from another basket to this one. This is used with the "Saved" basket functionality. """ try: existing_line = self.lines.get(line_reference=line.line_reference) except ObjectDoesNotExist: # Line does not already exist - reassign its basket line.basket = self line.save() else: # Line already exists - assume the max quantity is correct and # delete the old if add_quantities: existing_line.quantity += line.quantity else: existing_line.quantity = max(existing_line.quantity, line.quantity) existing_line.save() line.delete() finally: self._lines = None merge_line.alters_data = True def merge(self, basket, add_quantities=True): """ Merges another basket with this one. :basket: The basket to merge into this one. :add_quantities: Whether to add line quantities when they are merged. """ for line_to_merge in basket.all_lines(): self.merge_line(line_to_merge, add_quantities) basket.status = self.MERGED basket.date_merged = now() basket._lines = None basket.save() merge.alters_data = True def freeze(self): """ Freezes the basket so it cannot be modified. """ self.status = self.FROZEN self.save() freeze.alters_data = True def thaw(self): """ Unfreezes a basket so it can be modified again """ self.status = self.OPEN self.save() thaw.alters_data = True def submit(self): """ Mark this basket as submitted """ self.status = self.SUBMITTED self.date_submitted = now() self.save() submit.alters_data = True # Kept for backwards compatibility set_as_submitted = submit def set_as_tax_exempt(self): self.exempt_from_tax = True for line in self.all_lines(): line.set_as_tax_exempt() def is_shipping_required(self): """ Test whether the basket contains physical products that require shipping. """ for line in self.all_lines(): if line.product.is_shipping_required: return True return False # ======= # Helpers # ======= def _create_line_reference(self, item, options): """ Returns a reference string for a line based on the item and its options. """ if not options: return item.id return "%d_%s" % (item.id, zlib.crc32(str(options))) def _get_total(self, property): """ For executing a named method on each line of the basket and returning the total. """ total = Decimal('0.00') for line in self.all_lines(): try: total += getattr(line, property) except ObjectDoesNotExist: # Handle situation where the product may have been deleted pass return total # ========== # Properties # ========== @property def is_empty(self): """ Test if this basket is empty """ return self.id is None or self.num_lines == 0 @property def total_excl_tax(self): """Return total line price excluding tax""" return self._get_total('line_price_excl_tax_and_discounts') @property def total_tax(self): """Return total tax for a line""" return self._get_total('line_tax') @property def total_incl_tax(self): """ Return total price inclusive of tax and discounts """ return self._get_total('line_price_incl_tax_and_discounts') @property def total_incl_tax_excl_discounts(self): """ Return total price inclusive of tax but exclusive discounts """ return self._get_total('line_price_incl_tax') @property def total_discount(self): return self._get_total('discount_value') @property def offer_discounts(self): """ Return basket discounts from non-voucher sources. Does not include shipping discounts. """ return self.offer_applications.offer_discounts @property def voucher_discounts(self): """ Return discounts from vouchers """ return self.offer_applications.voucher_discounts @property def shipping_discounts(self): """ Return discounts from vouchers """ return self.offer_applications.shipping_discounts @property def post_order_actions(self): """ Return discounts from vouchers """ return self.offer_applications.post_order_actions @property def grouped_voucher_discounts(self): """ Return discounts from vouchers but grouped so that a voucher which links to multiple offers is aggregated into one object. """ return self.offer_applications.grouped_voucher_discounts @property def total_excl_tax_excl_discounts(self): """ Return total price excluding tax and discounts """ return self._get_total('line_price_excl_tax') @property def num_lines(self): """Return number of lines""" return len(self.all_lines()) @property def num_items(self): """Return number of items""" return reduce(lambda num, line: num + line.quantity, self.all_lines(), 0) @property def num_items_without_discount(self): num = 0 for line in self.all_lines(): num += line.quantity_without_discount return num @property def num_items_with_discount(self): num = 0 for line in self.all_lines(): num += line.quantity_with_discount return num @property def time_before_submit(self): if not self.date_submitted: return None return self.date_submitted - self.date_created @property def time_since_creation(self, test_datetime=None): if not test_datetime: test_datetime = now() return test_datetime - self.date_created @property def contains_a_voucher(self): return self.vouchers.all().count() > 0 # ============= # Query methods # ============= def can_be_edited(self): """ Test if a basket can be edited """ return self.status in self.editable_statuses def contains_voucher(self, code): """ Test whether the basket contains a voucher with a given code """ try: self.vouchers.get(code=code) except ObjectDoesNotExist: return False else: return True def line_quantity(self, product, options=None): """ Return the current quantity of a specific product and options """ ref = self._create_line_reference(product, options) try: return self.lines.get(line_reference=ref).quantity except ObjectDoesNotExist: return 0 def is_submitted(self): return self.status == self.SUBMITTED
class AbstractBasket(models.Model): """ Basket object """ # Baskets can be anonymously owned - hence this field is nullable. When a # anon user signs in, their two baskets are merged. owner = models.ForeignKey(AUTH_USER_MODEL, null=True, related_name='baskets', on_delete=models.CASCADE, verbose_name=_("Owner")) # Basket statuses # - Frozen is for when a basket is in the process of being submitted # and we need to prevent any changes to it. OPEN, MERGED, SAVED, FROZEN, SUBMITTED = ("Open", "Merged", "Saved", "Frozen", "Submitted") STATUS_CHOICES = ( (OPEN, _("Open - currently active")), (MERGED, _("Merged - superceded by another basket")), (SAVED, _("Saved - for items to be purchased later")), (FROZEN, _("Frozen - the basket cannot be modified")), (SUBMITTED, _("Submitted - has been ordered at the checkout")), ) status = models.CharField(_("Status"), max_length=128, default=OPEN, choices=STATUS_CHOICES) # A basket can have many vouchers attached to it. However, it is common # for sites to only allow one voucher per basket - this will need to be # enforced in the project's codebase. vouchers = models.ManyToManyField('voucher.Voucher', verbose_name=_("Vouchers"), blank=True) date_created = models.DateTimeField(_("Date created"), auto_now_add=True) date_merged = models.DateTimeField(_("Date merged"), null=True, blank=True) date_submitted = models.DateTimeField(_("Date submitted"), null=True, blank=True) # Only if a basket is in one of these statuses can it be edited editable_statuses = (OPEN, SAVED) class Meta: abstract = True app_label = 'basket' verbose_name = _('Basket') verbose_name_plural = _('Baskets') objects = models.Manager() open = OpenBasketManager() saved = SavedBasketManager() def __init__(self, *args, **kwargs): super(AbstractBasket, self).__init__(*args, **kwargs) # We keep a cached copy of the basket lines as we refer to them often # within the same request cycle. Also, applying offers will append # discount data to the basket lines which isn't persisted to the DB and # so we want to avoid reloading them as this would drop the discount # information. self._lines = None self.offer_applications = results.OfferApplications() def __str__(self): return _( u"%(status)s basket (owner: %(owner)s, lines: %(num_lines)d)") \ % {'status': self.status, 'owner': self.owner, 'num_lines': self.num_lines} # ======== # Strategy # ======== @property def has_strategy(self): return hasattr(self, '_strategy') def _get_strategy(self): if not self.has_strategy: raise RuntimeError( "No strategy class has been assigned to this basket. " "This is normally assigned to the incoming request in " "oscar.apps.basket.middleware.BasketMiddleware. " "Since it is missing, you must be doing something different. " "Ensure that a strategy instance is assigned to the basket!") return self._strategy def _set_strategy(self, strategy): self._strategy = strategy strategy = property(_get_strategy, _set_strategy) def all_lines(self): """ Return a cached set of basket lines. This is important for offers as they alter the line models and you don't want to reload them from the DB as that information would be lost. """ if self.id is None: return self.lines.none() if self._lines is None: self._lines = (self.lines.select_related( 'product', 'stockrecord').prefetch_related( 'attributes', 'product__images').order_by(self._meta.pk.name)) return self._lines def is_quantity_allowed(self, qty): """ Test whether the passed quantity of items can be added to the basket """ # We enforce a max threshold to prevent a DOS attack via the offers # system. basket_threshold = settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD if basket_threshold: total_basket_quantity = self.num_items max_allowed = basket_threshold - total_basket_quantity if qty > max_allowed: return False, _( "Due to technical limitations we are not able " "to ship more than %(threshold)d items in one order.") \ % {'threshold': basket_threshold} return True, None # ============ # Manipulation # ============ def flush(self): """ Remove all lines from basket. """ if self.status == self.FROZEN: raise PermissionDenied("A frozen basket cannot be flushed") self.lines.all().delete() self._lines = None def add_product(self, product, quantity=1, options=None): """ Add a product to the basket 'stock_info' is the price and availability data returned from a partner strategy class. The 'options' list should contains dicts with keys 'option' and 'value' which link the relevant product.Option model and string value respectively. Returns (line, created). line: the matching basket line created: whether the line was created or updated """ if options is None: options = [] if not self.id: self.save() # Ensure that all lines are the same currency price_currency = self.currency stock_info = self.strategy.fetch_for_product(product) if price_currency and stock_info.price.currency != price_currency: raise ValueError( ("Basket lines must all have the same currency. Proposed " "line has currency %s, while basket has currency %s") % (stock_info.price.currency, price_currency)) if stock_info.stockrecord is None: raise ValueError( ("Basket lines must all have stock records. Strategy hasn't " "found any stock record for product %s") % product) # Line reference is used to distinguish between variations of the same # product (eg T-shirts with different personalisations) line_ref = self._create_line_reference(product, stock_info.stockrecord, options) # Determine price to store (if one exists). It is only stored for # audit and sometimes caching. defaults = { 'quantity': quantity, 'price_excl_tax': stock_info.price.excl_tax, 'price_currency': stock_info.price.currency, } if stock_info.price.is_tax_known: defaults['price_incl_tax'] = stock_info.price.incl_tax line, created = self.lines.get_or_create( line_reference=line_ref, product=product, stockrecord=stock_info.stockrecord, defaults=defaults) if created: for option_dict in options: line.attributes.create(option=option_dict['option'], value=option_dict['value']) else: line.quantity = max(0, line.quantity + quantity) line.save() self.reset_offer_applications() # Returning the line is useful when overriding this method. return line, created add_product.alters_data = True add = add_product def applied_offers(self): """ Return a dict of offers successfully applied to the basket. This is used to compare offers before and after a basket change to see if there is a difference. """ return self.offer_applications.offers def reset_offer_applications(self): """ Remove any discounts so they get recalculated """ self.offer_applications = results.OfferApplications() self._lines = None def merge_line(self, line, add_quantities=True): """ For transferring a line from another basket to this one. This is used with the "Saved" basket functionality. """ try: existing_line = self.lines.get(line_reference=line.line_reference) except ObjectDoesNotExist: # Line does not already exist - reassign its basket line.basket = self line.save() else: # Line already exists - assume the max quantity is correct and # delete the old if add_quantities: existing_line.quantity += line.quantity else: existing_line.quantity = max(existing_line.quantity, line.quantity) existing_line.save() line.delete() finally: self._lines = None merge_line.alters_data = True def merge(self, basket, add_quantities=True): """ Merges another basket with this one. :basket: The basket to merge into this one. :add_quantities: Whether to add line quantities when they are merged. """ # Use basket.lines.all instead of all_lines as this function is called # before a strategy has been assigned. for line_to_merge in basket.lines.all(): self.merge_line(line_to_merge, add_quantities) basket.status = self.MERGED basket.date_merged = now() basket._lines = None basket.save() # Ensure all vouchers are moved to the new basket for voucher in basket.vouchers.all(): basket.vouchers.remove(voucher) self.vouchers.add(voucher) merge.alters_data = True def freeze(self): """ Freezes the basket so it cannot be modified. """ self.status = self.FROZEN self.save() freeze.alters_data = True def thaw(self): """ Unfreezes a basket so it can be modified again """ self.status = self.OPEN self.save() thaw.alters_data = True def submit(self): """ Mark this basket as submitted """ self.status = self.SUBMITTED self.date_submitted = now() self.save() submit.alters_data = True # Kept for backwards compatibility set_as_submitted = submit def is_shipping_required(self): """ Test whether the basket contains physical products that require shipping. """ for line in self.all_lines(): if line.product.is_shipping_required: return True return False # ======= # Helpers # ======= def _create_line_reference(self, product, stockrecord, options): """ Returns a reference string for a line based on the item and its options. """ base = '%s_%s' % (product.id, stockrecord.id) if not options: return base repr_options = [{ 'option': repr(option['option']), 'value': repr(option['value']) } for option in options] return "%s_%s" % (base, zlib.crc32(repr(repr_options).encode('utf8'))) def _get_total(self, property): """ For executing a named method on each line of the basket and returning the total. """ total = D('0.00') for line in self.all_lines(): try: total += getattr(line, property) except ObjectDoesNotExist: # Handle situation where the product may have been deleted pass except TypeError: # Handle Unavailable products with no known price info = self.strategy.fetch_for_product(line.product) if info.availability.is_available_to_buy: raise pass return total # ========== # Properties # ========== @property def is_empty(self): """ Test if this basket is empty """ return self.id is None or self.num_lines == 0 @property def is_tax_known(self): """ Test if tax values are known for this basket """ return all([line.is_tax_known for line in self.all_lines()]) @property def total_excl_tax(self): """ Return total line price excluding tax """ return self._get_total('line_price_excl_tax_incl_discounts') @property def total_tax(self): """Return total tax for a line""" return self._get_total('line_tax') @property def total_incl_tax(self): """ Return total price inclusive of tax and discounts """ return self._get_total('line_price_incl_tax_incl_discounts') @property def total_incl_tax_excl_discounts(self): """ Return total price inclusive of tax but exclusive discounts """ return self._get_total('line_price_incl_tax') @property def total_discount(self): return self._get_total('discount_value') @property def offer_discounts(self): """ Return basket discounts from non-voucher sources. Does not include shipping discounts. """ return self.offer_applications.offer_discounts @property def voucher_discounts(self): """ Return discounts from vouchers """ return self.offer_applications.voucher_discounts @property def has_shipping_discounts(self): return len(self.shipping_discounts) > 0 @property def shipping_discounts(self): """ Return discounts from vouchers """ return self.offer_applications.shipping_discounts @property def post_order_actions(self): """ Return discounts from vouchers """ return self.offer_applications.post_order_actions @property def grouped_voucher_discounts(self): """ Return discounts from vouchers but grouped so that a voucher which links to multiple offers is aggregated into one object. """ return self.offer_applications.grouped_voucher_discounts @property def total_excl_tax_excl_discounts(self): """ Return total price excluding tax and discounts """ return self._get_total('line_price_excl_tax') @property def num_lines(self): """Return number of lines""" return self.all_lines().count() @property def num_items(self): """Return number of items""" return sum(line.quantity for line in self.lines.all()) @property def num_items_without_discount(self): num = 0 for line in self.all_lines(): num += line.quantity_without_discount return num @property def num_items_with_discount(self): num = 0 for line in self.all_lines(): num += line.quantity_with_discount return num @property def time_before_submit(self): if not self.date_submitted: return None return self.date_submitted - self.date_created @property def time_since_creation(self, test_datetime=None): if not test_datetime: test_datetime = now() return test_datetime - self.date_created @property def contains_a_voucher(self): if not self.id: return False return self.vouchers.exists() @property def is_submitted(self): return self.status == self.SUBMITTED @property def can_be_edited(self): """ Test if a basket can be edited """ return self.status in self.editable_statuses @property def currency(self): # Since all lines should have the same currency, return the currency of # the first one found. for line in self.all_lines(): return line.price_currency # ============= # Query methods # ============= def contains_voucher(self, code): """ Test whether the basket contains a voucher with a given code """ if self.id is None: return False try: self.vouchers.get(code=code) except ObjectDoesNotExist: return False else: return True def product_quantity(self, product): """ Return the quantity of a product in the basket The basket can contain multiple lines with the same product, but different options and stockrecords. Those quantities are summed up. """ matching_lines = self.lines.filter(product=product) quantity = matching_lines.aggregate(Sum('quantity'))['quantity__sum'] return quantity or 0 def line_quantity(self, product, stockrecord, options=None): """ Return the current quantity of a specific product and options """ ref = self._create_line_reference(product, stockrecord, options) try: return self.lines.get(line_reference=ref).quantity except ObjectDoesNotExist: return 0
class AbstractBasket(models.Model): """ Basket object """ # Baskets can be anonymously owned (which are merged if the user signs in) owner = models.ForeignKey('auth.User', related_name='baskets', null=True) STATUS_CHOICES = ( (OPEN, _("Open - currently active")), (MERGED, _("Merged - superceded by another basket")), (SAVED, _("Saved - for items to be purchased later")), (FROZEN, _("Frozen - the basket cannot be modified")), (SUBMITTED, _("Submitted - has been ordered at the checkout")), ) status = models.CharField(_("Status"), max_length=128, default=OPEN, choices=STATUS_CHOICES) vouchers = models.ManyToManyField('voucher.Voucher', null=True) date_created = models.DateTimeField(auto_now_add=True) date_merged = models.DateTimeField(null=True, blank=True) date_submitted = models.DateTimeField(null=True, blank=True) # Cached queryset of lines _lines = None discounts = [] class Meta: abstract = True objects = models.Manager() open = OpenBasketManager() saved = SavedBasketManager() def __unicode__(self): return u"%s basket (owner: %s, lines: %d)" % (self.status, self.owner, self.num_lines) def all_lines(self): """ Return a cached set of basket lines. This is important for offers as they alter the line models and you don't want to reload them from the DB. """ if not self._lines: self._lines = self.lines.all() return self._lines # ============ # Manipulation # ============ def flush(self): """Remove all lines from basket.""" if self.status == FROZEN: raise PermissionDenied("A frozen basket cannot be flushed") self.lines_all().delete() self._lines = None def add_product(self, item, quantity=1, options=None): """ Convenience method for adding products to a basket The 'options' list should contains dicts with keys 'option' and 'value' which link the relevant product.Option model and string value respectively. """ if options is None: options = [] if not self.id: self.save() line_ref = self._create_line_reference(item, options) try: line = self.lines.get(line_reference=line_ref) except ObjectDoesNotExist: line = self.lines.create(basket=self, line_reference=line_ref, product=item, quantity=quantity) for option_dict in options: line.attributes.create(line=line, option=option_dict['option'], value=option_dict['value']) else: line.quantity += quantity line.save() self._lines = None def set_discounts(self, discounts): """ Sets the discounts that apply to this basket. This should be a list of dictionaries """ self.discounts = discounts def merge_line(self, line): """ For transferring a line from another basket to this one. This is used with the "Saved" basket functionality. """ try: existing_line = self.lines.get(line_reference=line.line_reference) except ObjectDoesNotExist: # Line does not already exist - reassign its basket line.basket = self line.save() else: # Line already exists - bump its quantity and delete the old existing_line.quantity += line.quantity existing_line.save() line.delete() def merge(self, basket): """ Merges another basket with this one. """ for line_to_merge in basket.all_lines(): self.merge_line(line_to_merge) basket.status = MERGED basket.date_merged = datetime.datetime.now() basket.save() self._lines = None def freeze(self): """ Freezes the basket so it cannot be modified. """ self.status = FROZEN self.save() def thaw(self): """ Unfreezes a basket so it can be modified again """ self.status = OPEN self.save() def set_as_submitted(self): """Mark this basket as submitted.""" self.status = SUBMITTED self.date_submitted = datetime.datetime.now() self.save() # ======= # Helpers # ======= def _create_line_reference(self, item, options): """ Returns a reference string for a line based on the item and its options. """ if not options: return item.id return "%d_%s" % (item.id, zlib.crc32(str(options))) def _get_total(self, property): """ For executing a named method on each line of the basket and returning the total. """ total = Decimal('0.00') for line in self.all_lines(): try: total += getattr(line, property) except ObjectDoesNotExist: # Handle situation where the product may have been deleted pass return total # ========== # Properties # ========== @property def is_empty(self): """Return bool based on basket having 0 lines""" return self.num_lines == 0 @property def total_excl_tax(self): """Return total line price excluding tax""" return self._get_total('line_price_excl_tax_and_discounts') @property def total_tax(self): """Return total tax for a line""" return self._get_total('line_tax') @property def total_incl_tax(self): """ Return total price inclusive of tax and discounts """ return self._get_total('line_price_incl_tax_and_discounts') @property def total_incl_tax_excl_discounts(self): """ Return total price inclusive of tax but exclusive discounts """ return self._get_total('line_price_incl_tax') @property def total_discount(self): return self._get_total('discount_value') @property def offer_discounts(self): """ Return discounts from non-voucher sources. """ offer_discounts = [] for discount in self.discounts: if not discount['voucher']: offer_discounts.append(discount) return offer_discounts @property def voucher_discounts(self): """ Return discounts from vouchers """ voucher_discounts = [] for discount in self.discounts: if discount['voucher']: voucher_discounts.append(discount) return voucher_discounts @property def total_excl_tax_excl_discounts(self): """ Return total price excluding tax and discounts """ return self._get_total('line_price_excl_tax') @property def num_lines(self): """Return number of lines""" return self.all_lines().count() @property def num_items(self): """Return number of items""" return reduce(lambda num, line: num + line.quantity, self.all_lines(), 0) @property def num_items_without_discount(self): """Return number of items""" num = 0 for line in self.all_lines(): num += line.quantity_without_discount return num @property def time_before_submit(self): if not self.date_submitted: return None return self.date_submitted - self.date_created @property def time_since_creation(self, test_datetime=None): if not test_datetime: test_datetime = datetime.datetime.now() return test_datetime - self.date_created