You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

abstract_models.py 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. from decimal import Decimal as D
  2. from django.conf import settings
  3. from django.db import models
  4. from django.utils.translation import ugettext_lazy as _
  5. from django.utils.importlib import import_module as django_import_module
  6. from oscar.core.loading import import_module
  7. import_module('partner.wrappers', ['DefaultWrapper'], locals())
  8. # Cache the partners for quicklookups
  9. default_wrapper = DefaultWrapper()
  10. partner_wrappers = {}
  11. for partner, class_str in settings.OSCAR_PARTNER_WRAPPERS.items():
  12. bits = class_str.split('.')
  13. class_name = bits.pop()
  14. module_str = '.'.join(bits)
  15. module = django_import_module(module_str)
  16. partner_wrappers[partner] = getattr(module, class_name)()
  17. def get_partner_wrapper(partner_name):
  18. """
  19. Returns the appropriate partner wrapper given the partner name
  20. """
  21. return partner_wrappers.get(partner_name, default_wrapper)
  22. class AbstractPartner(models.Model):
  23. u"""Fulfillment partner"""
  24. name = models.CharField(max_length=128, unique=True)
  25. # A partner can have users assigned to it. These can be used
  26. # to provide authentication for webservices etc.
  27. users = models.ManyToManyField('auth.User', related_name="partners", blank=True, null=True)
  28. class Meta:
  29. verbose_name_plural = 'Fulfillment partners'
  30. abstract = True
  31. permissions = (
  32. ("can_edit_stock_records", "Can edit stock records"),
  33. ("can_view_stock_records", "Can view stock records"),
  34. ("can_edit_product_range", "Can edit product range"),
  35. ("can_view_product_range", "Can view product range"),
  36. ("can_edit_order_lines", "Can edit order lines"),
  37. ("can_view_order_lines", "Can view order lines"),
  38. )
  39. def __unicode__(self):
  40. return self.name
  41. class AbstractStockRecord(models.Model):
  42. """
  43. A basic stock record.
  44. This links a product to a partner, together with price and availability
  45. information. Most projects will need to subclass this object to add custom
  46. fields such as lead_time, report_code, min_quantity.
  47. """
  48. product = models.OneToOneField('catalogue.Product', related_name="stockrecord")
  49. partner = models.ForeignKey('partner.Partner')
  50. # The fulfilment partner will often have their own SKU for a product, which
  51. # we store here.
  52. partner_sku = models.CharField(_("Partner SKU"), max_length=128, blank=True)
  53. # Price info:
  54. # We deliberately don't store tax information to allow each project
  55. # to subclass this model and put its own fields for convey tax.
  56. price_currency = models.CharField(max_length=12, default=settings.OSCAR_DEFAULT_CURRENCY)
  57. # This is the base price for calculations - tax should be applied
  58. # by the appropriate method. We don't store it here as its calculation is
  59. # highly domain-specific. It is NULLable because some items don't have a fixed
  60. # price.
  61. price_excl_tax = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True)
  62. # Retail price for this item
  63. price_retail = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True)
  64. # Cost price is optional as not all partner supply it
  65. cost_price = models.DecimalField(decimal_places=2, max_digits=12, blank=True, null=True)
  66. # Stock level information
  67. num_in_stock = models.IntegerField(default=0, blank=True, null=True)
  68. # The amount of stock allocated to orders but not fed back to the master
  69. # stock system. A typical stock update process will set the num_in_stock
  70. # variable to a new value and reset num_allocated to zero
  71. num_allocated = models.IntegerField(default=0, blank=True, null=True)
  72. # Date information
  73. date_created = models.DateTimeField(auto_now_add=True)
  74. date_updated = models.DateTimeField(auto_now=True, db_index=True)
  75. class Meta:
  76. abstract = True
  77. def allocate(self, quantity):
  78. """
  79. Record a stock allocation.
  80. We don't alter the num_in_stock variable as it is assumed that this
  81. will be set by a batch "stock update" process.
  82. """
  83. self.num_allocated = int(self.num_allocated)
  84. self.num_allocated += quantity
  85. self.save()
  86. def set_discount_price(self, price):
  87. """
  88. A setter method for setting a new price.
  89. This is called from within the "discount" app, which is responsible
  90. for applying fixed-discount offers to products. We use a setter method
  91. so that this behaviour can be customised in projects.
  92. """
  93. self.price_excl_tax = price
  94. self.save()
  95. # Price retrieval methods - these default to no tax being applicable
  96. # These are intended to be overridden.
  97. @property
  98. def is_available_to_buy(self):
  99. """
  100. Return whether this stockrecord allows the product to be purchased
  101. """
  102. return get_partner_wrapper(self.partner.name).is_available_to_buy(self)
  103. @property
  104. def net_stock_level(self):
  105. """
  106. Return the effective number in stock
  107. """
  108. if self.num_in_stock is None:
  109. return 0
  110. if self.num_allocated is None:
  111. return self.num_in_stock
  112. return self.num_in_stock - self.num_allocated
  113. @property
  114. def availability(self):
  115. """
  116. Return an item's availability as a string
  117. """
  118. return get_partner_wrapper(self.partner.name).availability(self)
  119. @property
  120. def dispatch_date(self):
  121. """
  122. Return the estimated dispatch date for a line
  123. """
  124. return get_partner_wrapper(self.partner.name).dispatch_date(self)
  125. @property
  126. def lead_time(self):
  127. return get_partner_wrapper(self.partner.name).lead_time(self)
  128. @property
  129. def price_incl_tax(self):
  130. """
  131. Return a product's price including tax.
  132. This defaults to the price_excl_tax as tax calculations are
  133. domain specific. This class needs to be subclassed and tax logic
  134. added to this method.
  135. """
  136. if self.price_excl_tax is None:
  137. return D('0.00')
  138. return self.price_excl_tax + self.price_tax
  139. @property
  140. def price_tax(self):
  141. """
  142. Return a product's tax value
  143. """
  144. return get_partner_wrapper(self.partner.name).calculate_tax(self)
  145. def __unicode__(self):
  146. if self.partner_sku:
  147. return "%s (%s): %s" % (self.partner.name, self.partner_sku, self.product.title)
  148. else:
  149. return "%s: %s" % (self.partner.name, self.product.title)