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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import zlib
  2. from django.db import models
  3. from django.utils.translation import ugettext_lazy as _
  4. from django.core import exceptions
  5. from oscar.core.compat import AUTH_USER_MODEL
  6. class AbstractAddress(models.Model):
  7. """
  8. Superclass address object
  9. This is subclassed and extended to provide models for
  10. user, shipping and billing addresses.
  11. """
  12. MR, MISS, MRS, MS, DR = ('Mr', 'Miss', 'Mrs', 'Ms', 'Dr')
  13. TITLE_CHOICES = (
  14. (MR, _("Mr")),
  15. (MISS, _("Miss")),
  16. (MRS, _("Mrs")),
  17. (MS, _("Ms")),
  18. (DR, _("Dr")),
  19. )
  20. title = models.CharField(
  21. _("Title"), max_length=64, choices=TITLE_CHOICES,
  22. blank=True, null=True)
  23. first_name = models.CharField(
  24. _("First name"), max_length=255, blank=True, null=True)
  25. last_name = models.CharField(_("Last name"), max_length=255, blank=True)
  26. # We use quite a few lines of an address as they are often quite long and
  27. # it's easier to just hide the unnecessary ones than add extra ones.
  28. line1 = models.CharField(_("First line of address"), max_length=255)
  29. line2 = models.CharField(
  30. _("Second line of address"), max_length=255, blank=True, null=True)
  31. line3 = models.CharField(
  32. _("Third line of address"), max_length=255, blank=True, null=True)
  33. line4 = models.CharField(_("City"), max_length=255, blank=True, null=True)
  34. state = models.CharField(
  35. _("State/County"), max_length=255, blank=True, null=True)
  36. postcode = models.CharField(
  37. _("Post/Zip-code"), max_length=64, blank=True, null=True)
  38. country = models.ForeignKey('address.Country', verbose_name=_("Country"))
  39. #: A field only used for searching addresses - this contains all the
  40. #: relevant fields. This is effectively a poor man's Solr text field.
  41. search_text = models.CharField(
  42. _("Search text - used only for searching addresses"),
  43. max_length=1000)
  44. def __unicode__(self):
  45. return self.summary
  46. class Meta:
  47. abstract = True
  48. verbose_name = _('Address')
  49. verbose_name_plural = _('Addresses')
  50. # Saving
  51. def save(self, *args, **kwargs):
  52. self._clean_fields()
  53. self._update_search_text()
  54. super(AbstractAddress, self).save(*args, **kwargs)
  55. def _clean_fields(self):
  56. for field in ['first_name', 'last_name', 'line1', 'line2', 'line3',
  57. 'line4', 'state', 'postcode']:
  58. if self.__dict__[field]:
  59. self.__dict__[field] = self.__dict__[field].strip()
  60. # Ensure postcodes are always uppercase
  61. if self.postcode:
  62. self.postcode = self.postcode.upper()
  63. def _update_search_text(self):
  64. search_fields = filter(
  65. bool, [self.first_name, self.last_name,
  66. self.line1, self.line2, self.line3, self.line4,
  67. self.state, self.postcode, self.country.name])
  68. self.search_text = ' '.join(search_fields)
  69. # Properties
  70. @property
  71. def city(self):
  72. # Common alias
  73. return self.line4
  74. @property
  75. def summary(self):
  76. """
  77. Returns a single string summary of the address,
  78. separating fields using commas.
  79. """
  80. return u", ".join(self.active_address_fields())
  81. @property
  82. def salutation(self):
  83. """
  84. Name (including title)
  85. """
  86. return self.join_fields(
  87. ('title', 'first_name', 'last_name'),
  88. separator=u" ")
  89. @property
  90. def name(self):
  91. return self.join_fields(('first_name', 'last_name'), separator=u" ")
  92. # Helpers
  93. def generate_hash(self):
  94. """
  95. Returns a hash of the address summary
  96. """
  97. # We use an upper-case version of the summary
  98. return zlib.crc32(self.summary.strip().upper().encode('UTF8'))
  99. def join_fields(self, fields, separator=u", "):
  100. """
  101. Join a sequence of fields using the specified separator
  102. """
  103. field_values = []
  104. for field in fields:
  105. # Title is special case
  106. if field == 'title':
  107. value = self.get_title_display()
  108. else:
  109. value = getattr(self, field)
  110. field_values.append(value)
  111. return separator.join(filter(bool, field_values))
  112. def populate_alternative_model(self, address_model):
  113. """
  114. For populating an address model using the matching fields
  115. from this one.
  116. This is used to convert a user address to a shipping address
  117. as part of the checkout process.
  118. """
  119. destination_field_names = [
  120. field.name for field in address_model._meta.fields]
  121. for field_name in [field.name for field in self._meta.fields]:
  122. if field_name in destination_field_names and field_name != 'id':
  123. setattr(address_model, field_name, getattr(self, field_name))
  124. def active_address_fields(self):
  125. """
  126. Return the non-empty components of the address, but merging the
  127. title, first_name and last_name into a single line.
  128. """
  129. self._clean_fields()
  130. fields = filter(
  131. bool, [self.salutation, self.line1, self.line2,
  132. self.line3, self.line4, self.state, self.postcode])
  133. try:
  134. fields.append(self.country.name)
  135. except exceptions.ObjectDoesNotExist:
  136. pass
  137. return fields
  138. class AbstractCountry(models.Model):
  139. """
  140. International Organization for Standardization (ISO) 3166-1 Country list.
  141. """
  142. iso_3166_1_a2 = models.CharField(_('ISO 3166-1 alpha-2'), max_length=2,
  143. primary_key=True)
  144. iso_3166_1_a3 = models.CharField(_('ISO 3166-1 alpha-3'), max_length=3,
  145. null=True, db_index=True)
  146. iso_3166_1_numeric = models.PositiveSmallIntegerField(
  147. _('ISO 3166-1 numeric'), null=True, db_index=True)
  148. name = models.CharField(_('Official name (CAPS)'), max_length=128)
  149. printable_name = models.CharField(_('Country name'), max_length=128)
  150. display_order = models.PositiveSmallIntegerField(
  151. _("Display order"), default=0, db_index=True,
  152. help_text=_('Higher the number, higher the country in the list.'))
  153. is_shipping_country = models.BooleanField(_("Is Shipping Country"),
  154. default=False, db_index=True)
  155. class Meta:
  156. abstract = True
  157. verbose_name = _('Country')
  158. verbose_name_plural = _('Countries')
  159. ordering = ('-display_order', 'name',)
  160. def __unicode__(self):
  161. return self.printable_name or self.name
  162. class AbstractShippingAddress(AbstractAddress):
  163. """
  164. A shipping address.
  165. A shipping address should not be edited once the order has been placed -
  166. it should be read-only after that.
  167. """
  168. phone_number = models.CharField(_("Phone number"), max_length=32,
  169. blank=True, null=True)
  170. notes = models.TextField(
  171. blank=True, null=True,
  172. verbose_name=_('Courier instructions'),
  173. help_text=_("For example, leave the parcel in the wheelie bin "
  174. "if I'm not in."))
  175. class Meta:
  176. abstract = True
  177. verbose_name = _("Shipping address")
  178. verbose_name_plural = _("Shipping addresses")
  179. @property
  180. def order(self):
  181. """
  182. Return the order linked to this shipping address
  183. """
  184. orders = self.order_set.all()
  185. if not orders:
  186. return None
  187. return orders[0]
  188. class AbstractUserAddress(AbstractShippingAddress):
  189. """
  190. A user's address. A user can have many of these and together they form an
  191. 'address book' of sorts for the user.
  192. We use a separate model for shipping and billing (even though there will be
  193. some data duplication) because we don't want shipping/billing addresses
  194. changed or deleted once an order has been placed. By having a separate
  195. model, we allow users the ability to add/edit/delete from their address
  196. book without affecting orders already placed.
  197. """
  198. user = models.ForeignKey(
  199. AUTH_USER_MODEL, related_name='addresses', verbose_name=_("User"))
  200. #: Whether this address is the default for shipping
  201. is_default_for_shipping = models.BooleanField(
  202. _("Default shipping address?"), default=False)
  203. #: Whether this address should be the default for billing.
  204. is_default_for_billing = models.BooleanField(
  205. _("Default billing address?"), default=False)
  206. #: We keep track of the number of times an address has been used
  207. #: as a shipping address so we can show the most popular ones
  208. #: first at the checkout.
  209. num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)
  210. #: A hash is kept to try and avoid duplicate addresses being added
  211. #: to the address book.
  212. hash = models.CharField(_("Address Hash"), max_length=255, db_index=True)
  213. date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
  214. def save(self, *args, **kwargs):
  215. """
  216. Save a hash of the address fields
  217. """
  218. # Save a hash of the address fields so we can check whether two
  219. # addresses are the same to avoid saving duplicates
  220. self.hash = self.generate_hash()
  221. # Ensure that each user only has one default shipping address
  222. # and billing address
  223. self._ensure_defaults_integrity()
  224. super(AbstractUserAddress, self).save(*args, **kwargs)
  225. def _ensure_defaults_integrity(self):
  226. if self.is_default_for_shipping:
  227. self.__class__._default_manager.filter(
  228. user=self.user,
  229. is_default_for_shipping=True).update(
  230. is_default_for_shipping=False)
  231. if self.is_default_for_billing:
  232. self.__class__._default_manager.filter(
  233. user=self.user,
  234. is_default_for_billing=True).update(
  235. is_default_for_billing=False)
  236. class Meta:
  237. abstract = True
  238. verbose_name = _("User address")
  239. verbose_name_plural = _("User addresses")
  240. ordering = ['-num_orders']
  241. unique_together = ('user', 'hash')
  242. class AbstractBillingAddress(AbstractAddress):
  243. class Meta:
  244. abstract = True
  245. verbose_name_plural = _("Billing address")
  246. verbose_name_plural = _("Billing addresses")
  247. @property
  248. def order(self):
  249. """
  250. Return the order linked to this shipping address
  251. """
  252. orders = self.order_set.all()
  253. if not orders:
  254. return None
  255. return orders[0]