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 13KB

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