Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

abstract_models.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. """
  2. Models of products
  3. """
  4. import re
  5. from itertools import chain
  6. from django.db import models
  7. from django.utils.translation import ugettext_lazy as _
  8. from django.template.defaultfilters import slugify
  9. from django.core.urlresolvers import reverse
  10. from django.core.exceptions import ObjectDoesNotExist
  11. from oscar.apps.product.managers import BrowsableItemManager
  12. def _convert_to_underscores(str):
  13. u"""
  14. For converting a string in CamelCase or normal text with spaces
  15. to the normal underscored variety
  16. """
  17. without_whitespace = re.sub('\s+', '_', str.strip())
  18. s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', without_whitespace)
  19. return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
  20. class AbstractItemClass(models.Model):
  21. u"""Defines an item type (equivqlent to Taoshop's MediaType)."""
  22. name = models.CharField(_('name'), max_length=128)
  23. slug = models.SlugField(max_length=128, unique=True)
  24. options = models.ManyToManyField('product.Option', blank=True)
  25. class Meta:
  26. abstract = True
  27. ordering = ['name']
  28. verbose_name_plural = "Item classes"
  29. def save(self, *args, **kwargs):
  30. if not self.slug:
  31. self.slug= slugify(self.name)
  32. super(AbstractItemClass, self).save(*args, **kwargs)
  33. def get_absolute_url(self):
  34. return reverse('oscar-product-item-class', kwargs={'item_class_slug': self.slug})
  35. def __unicode__(self):
  36. return self.name
  37. class AbstractItem(models.Model):
  38. u"""The base product object"""
  39. # If an item has no parent, then it is the "canonical" or abstract version of a product
  40. # which essentially represents a set of products. If a product has a parent
  41. # then it is a specific version of a product.
  42. #
  43. # For example, a canonical product would have a title like "Green fleece" while its
  44. # children would be "Green fleece - size L".
  45. # Universal product code
  46. upc = models.CharField(_("UPC"), max_length=64, blank=True, null=True, db_index=True,
  47. help_text="""Universal Product Code (UPC) is an identifier for a product which is
  48. not specific to a particular supplier. Eg an ISBN for a book.""")
  49. # No canonical product should have a stock record as they cannot be bought.
  50. parent = models.ForeignKey('self', null=True, blank=True, related_name='variants',
  51. help_text="""Only choose a parent product if this is a 'variant' of a canonical product. For example
  52. if this is a size 4 of a particular t-shirt. Leave blank if this is a CANONICAL PRODUCT (ie
  53. there is only one version of this product).""")
  54. # Title is mandatory for canonical products but optional for child products
  55. title = models.CharField(_('Title'), max_length=255, blank=True, null=True)
  56. slug = models.SlugField(max_length=255, unique=False)
  57. description = models.TextField(_('Description'), blank=True, null=True)
  58. item_class = models.ForeignKey('product.ItemClass', verbose_name=_('item class'), null=True,
  59. help_text="""Choose what type of product this is""")
  60. attribute_types = models.ManyToManyField('product.AttributeType', through='ItemAttributeValue',
  61. help_text="""An attribute type is something that this product MUST have, such as a size""")
  62. item_options = models.ManyToManyField('product.Option', blank=True,
  63. help_text="""Options are values that can be associated with a item when it is added to
  64. a customer's basket. This could be something like a personalised message to be
  65. printed on a T-shirt.<br/>""")
  66. related_items = models.ManyToManyField('product.Item', related_name='relations', blank=True, help_text="""Related
  67. items are things like different formats of the same book. Grouping them together allows
  68. better linking betwen products on the site.<br/>""")
  69. # Recommended products
  70. recommended_items = models.ManyToManyField('product.Item', through='ProductRecommendation', blank=True)
  71. date_created = models.DateTimeField(auto_now_add=True)
  72. # This field is used by Haystack to reindex search
  73. date_updated = models.DateTimeField(auto_now=True, db_index=True)
  74. objects = models.Manager()
  75. browsable = BrowsableItemManager()
  76. # Properties
  77. @property
  78. def options(self):
  79. return list(chain(self.item_options.all(), self.get_item_class().options.all()))
  80. @property
  81. def is_top_level(self):
  82. u"""Return True if this is a parent product"""
  83. return self.parent_id == None
  84. @property
  85. def is_group(self):
  86. u"""Return True if this is a top level product and has more than 0 variants"""
  87. return self.is_top_level and self.variants.count() > 0
  88. @property
  89. def is_variant(self):
  90. u"""Return True if a product is not a top level product"""
  91. return not self.is_top_level
  92. @property
  93. def min_variant_price_incl_tax(self):
  94. u"""Return minimum variant price including tax"""
  95. return self._min_variant_price('price_incl_tax')
  96. @property
  97. def min_variant_price_excl_tax(self):
  98. u"""Return minimum variant price excluding tax"""
  99. return self._min_variant_price('price_excl_tax')
  100. @property
  101. def has_stockrecord(self):
  102. u"""Return True if a product has a stock record, False if not"""
  103. try:
  104. sr = self.stockrecord
  105. return True
  106. except ObjectDoesNotExist:
  107. return False
  108. @property
  109. def score(self):
  110. try:
  111. pr = self.productrecord
  112. return pr.score
  113. except ObjectDoesNotExist:
  114. return 0
  115. def attribute_summary(self):
  116. u"""Return a string of all of a product's attributes"""
  117. return ", ".join([attribute.__unicode__() for attribute in self.attributes.all()])
  118. def get_title(self):
  119. u"""Return a product's title or it's parent's title if it has no title"""
  120. title = self.__dict__.setdefault('title', '')
  121. if not title and self.parent_id:
  122. title = self.parent.title
  123. return title
  124. def get_item_class(self):
  125. u"""Return a product's item class"""
  126. if self.item_class:
  127. return self.item_class
  128. if self.parent.item_class:
  129. return self.parent.item_class
  130. return None
  131. # Helpers
  132. def _min_variant_price(self, property):
  133. u"""Return minimum variant price"""
  134. prices = []
  135. for variant in self.variants.all():
  136. if variant.has_stockrecord:
  137. prices.append(getattr(variant.stockrecord, property))
  138. if not prices:
  139. return None
  140. prices.sort()
  141. return prices[0]
  142. class Meta:
  143. abstract = True
  144. ordering = ['-date_created']
  145. def __unicode__(self):
  146. if self.is_variant:
  147. return "%s (%s)" % (self.get_title(), self.attribute_summary())
  148. return self.get_title()
  149. @models.permalink
  150. def get_absolute_url(self):
  151. u"""Return a product's absolute url"""
  152. return ('oscar-product-item', (), {
  153. 'item_class_slug': self.get_item_class().slug,
  154. 'item_slug': self.slug,
  155. 'item_id': self.id})
  156. def save(self, *args, **kwargs):
  157. if self.is_top_level and not self.title:
  158. from django.core.exceptions import ValidationError
  159. raise ValidationError("Canonical products must have a title")
  160. if not self.slug:
  161. self.slug = slugify(self.get_title())
  162. super(AbstractItem, self).save(*args, **kwargs)
  163. class ProductRecommendation(models.Model):
  164. u"""
  165. 'Through' model for product recommendations
  166. """
  167. primary = models.ForeignKey('product.Item', related_name='primary_recommendations')
  168. recommendation = models.ForeignKey('product.Item')
  169. ranking = models.PositiveSmallIntegerField(default=0)
  170. class AbstractAttributeType(models.Model):
  171. u"""Defines an attribute. (Eg. size)"""
  172. name = models.CharField(_('name'), max_length=128)
  173. code = models.SlugField(_('code'), max_length=128)
  174. has_choices = models.BooleanField(default=False)
  175. class Meta:
  176. abstract = True
  177. ordering = ['code']
  178. def __unicode__(self):
  179. return self.name
  180. def save(self, *args, **kwargs):
  181. if not self.code:
  182. self.code = _convert_to_underscores(self.name)
  183. super(AbstractAttributeType, self).save(*args, **kwargs)
  184. class AbstractAttributeValueOption(models.Model):
  185. u"""Defines an attribute value choice (Eg: S,M,L,XL for a size attribute type)"""
  186. type = models.ForeignKey('product.AttributeType', related_name='options')
  187. value = models.CharField(max_length=255)
  188. class Meta:
  189. abstract = True
  190. def __unicode__(self):
  191. return u"%s = %s" % (self.type, self.value)
  192. class AbstractItemAttributeValue(models.Model):
  193. u"""
  194. The "through" model for the m2m relationship between product.Item
  195. and product.AttributeType. This specifies the value of the attribute
  196. for a particular product.
  197. Eg: size = L
  198. """
  199. product = models.ForeignKey('product.Item', related_name='attributes')
  200. type = models.ForeignKey('product.AttributeType')
  201. value = models.CharField(max_length=255)
  202. class Meta:
  203. abstract = True
  204. def __unicode__(self):
  205. return u"%s: %s" % (self.type.name, self.value)
  206. class AbstractOption(models.Model):
  207. u"""
  208. An option that can be selected for a particular item when the product
  209. is added to the basket.
  210. Eg a list ID for an SMS message send, or a personalised message to
  211. print on a T-shirt.
  212. This is not the same as an attribute as options do not have a fixed value for
  213. a particular item - options, they need to be specified by the customer.
  214. """
  215. name = models.CharField(_('name'), max_length=128)
  216. code = models.SlugField(_('code'), max_length=128)
  217. REQUIRED, OPTIONAL = ('Required', 'Optional')
  218. TYPE_CHOICES = (
  219. (REQUIRED, _("Required - a value for this option must be specified")),
  220. (OPTIONAL, _("Optional - a value for this option can be omitted")),
  221. )
  222. type = models.CharField(_("Status"), max_length=128, default=REQUIRED, choices=TYPE_CHOICES)
  223. class Meta:
  224. abstract = True
  225. def __unicode__(self):
  226. return self.name
  227. def save(self, *args, **kwargs):
  228. if not self.code:
  229. self.code = _convert_to_underscores(self.name)
  230. super(AbstractOption, self).save(*args, **kwargs)