您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

abstract_models.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import re
  2. import zlib
  3. from django.db import models
  4. from django.utils.encoding import python_2_unicode_compatible
  5. from django.utils.translation import ugettext_lazy as _, pgettext_lazy
  6. from django.core import exceptions
  7. from oscar.core.compat import AUTH_USER_MODEL
  8. from oscar.models.fields import UppercaseCharField, PhoneNumberField
  9. from django.utils.six.moves import filter
  10. @python_2_unicode_compatible
  11. class AbstractAddress(models.Model):
  12. """
  13. Superclass address object
  14. This is subclassed and extended to provide models for
  15. user, shipping and billing addresses.
  16. """
  17. MR, MISS, MRS, MS, DR = ('Mr', 'Miss', 'Mrs', 'Ms', 'Dr')
  18. TITLE_CHOICES = (
  19. (MR, _("Mr")),
  20. (MISS, _("Miss")),
  21. (MRS, _("Mrs")),
  22. (MS, _("Ms")),
  23. (DR, _("Dr")),
  24. )
  25. # Regex for each country. Not listed countries don't use postcodes
  26. # Based on http://en.wikipedia.org/wiki/List_of_postal_codes
  27. POSTCODES_REGEX = {
  28. 'AC': r'^[A-Z]{4}[0-9][A-Z]$',
  29. 'AD': r'^AD[0-9]{3}$',
  30. 'AF': r'^[0-9]{4}$',
  31. 'AI': r'^AI-2640$',
  32. 'AL': r'^[0-9]{4}$',
  33. 'AM': r'^[0-9]{4}$',
  34. 'AR': r'^([0-9]{4}|[A-Z][0-9]{4}[A-Z]{3})$',
  35. 'AS': r'^[0-9]{5}(-[0-9]{4}|-[0-9]{6})?$',
  36. 'AT': r'^[0-9]{4}$',
  37. 'AU': r'^[0-9]{4}$',
  38. 'AX': r'^[0-9]{5}$',
  39. 'AZ': r'^AZ[0-9]{4}$',
  40. 'BA': r'^[0-9]{5}$',
  41. 'BB': r'^BB[0-9]{5}$',
  42. 'BD': r'^[0-9]{4}$',
  43. 'BE': r'^[0-9]{4}$',
  44. 'BG': r'^[0-9]{4}$',
  45. 'BH': r'^[0-9]{3,4}$',
  46. 'BL': r'^[0-9]{5}$',
  47. 'BM': r'^[A-Z]{2}([0-9]{2}|[A-Z]{2})',
  48. 'BN': r'^[A-Z}{2}[0-9]]{4}$',
  49. 'BO': r'^[0-9]{4}$',
  50. 'BR': r'^[0-9]{5}(-[0-9]{3})?$',
  51. 'BT': r'^[0-9]{3}$',
  52. 'BY': r'^[0-9]{6}$',
  53. 'CA': r'^[A-Z][0-9][A-Z][0-9][A-Z][0-9]$',
  54. 'CC': r'^[0-9]{4}$',
  55. 'CH': r'^[0-9]{4}$',
  56. 'CL': r'^([0-9]{7}|[0-9]{3}-[0-9]{4})$',
  57. 'CN': r'^[0-9]{6}$',
  58. 'CO': r'^[0-9]{6}$',
  59. 'CR': r'^[0-9]{4,5}$',
  60. 'CU': r'^[0-9]{5}$',
  61. 'CV': r'^[0-9]{4}$',
  62. 'CX': r'^[0-9]{4}$',
  63. 'CY': r'^[0-9]{4}$',
  64. 'CZ': r'^[0-9]{5}$',
  65. 'DE': r'^[0-9]{5}$',
  66. 'DK': r'^[0-9]{4}$',
  67. 'DO': r'^[0-9]{5}$',
  68. 'DZ': r'^[0-9]{5}$',
  69. 'EC': r'^EC[0-9]{6}$',
  70. 'EE': r'^[0-9]{5}$',
  71. 'EG': r'^[0-9]{5}$',
  72. 'ES': r'^[0-9]{5}$',
  73. 'ET': r'^[0-9]{4}$',
  74. 'FI': r'^[0-9]{5}$',
  75. 'FK': r'^[A-Z]{4}[0-9][A-Z]{2}$',
  76. 'FM': r'^[0-9]{5}(-[0-9]{4})?$',
  77. 'FO': r'^[0-9]{3}$',
  78. 'FR': r'^[0-9]{5}$',
  79. 'GA': r'^[0-9]{2}.*[0-9]{2}$',
  80. 'GB': r'^[A-Z][A-Z0-9]{1,3}[0-9][A-Z]{2}$',
  81. 'GE': r'^[0-9]{4}$',
  82. 'GF': r'^[0-9]{5}$',
  83. 'GG': r'^([A-Z]{2}[0-9]{2,3}[A-Z]{2})$',
  84. 'GI': r'^GX111AA$',
  85. 'GL': r'^[0-9]{4}$',
  86. 'GP': r'^[0-9]{5}$',
  87. 'GR': r'^[0-9]{5}$',
  88. 'GS': r'^SIQQ1ZZ$',
  89. 'GT': r'^[0-9]{5}$',
  90. 'GU': r'^[0-9]{5}$',
  91. 'GW': r'^[0-9]{4}$',
  92. 'HM': r'^[0-9]{4}$',
  93. 'HN': r'^[0-9]{5}$',
  94. 'HR': r'^[0-9]{5}$',
  95. 'HT': r'^[0-9]{4}$',
  96. 'HU': r'^[0-9]{4}$',
  97. 'ID': r'^[0-9]{5}$',
  98. 'IL': r'^[0-9]{7}$',
  99. 'IM': r'^IM[0-9]{2,3}[A-Z]{2}$$',
  100. 'IN': r'^[0-9]{6}$',
  101. 'IO': r'^[A-Z]{4}[0-9][A-Z]{2}$',
  102. 'IQ': r'^[0-9]{5}$',
  103. 'IR': r'^[0-9]{5}-[0-9]{5}$',
  104. 'IS': r'^[0-9]{3}$',
  105. 'IT': r'^[0-9]{5}$',
  106. 'JE': r'^JE[0-9]{2}[A-Z]{2}$',
  107. 'JM': r'^JM[A-Z]{3}[0-9]{2}$',
  108. 'JO': r'^[0-9]{5}$',
  109. 'JP': r'^[0-9]{3}-?[0-9]{4}$',
  110. 'KE': r'^[0-9]{5}$',
  111. 'KG': r'^[0-9]{6}$',
  112. 'KH': r'^[0-9]{5}$',
  113. 'KR': r'^[0-9]{3}-?[0-9]{3}$',
  114. 'KY': r'^KY[0-9]-[0-9]{4}$',
  115. 'KZ': r'^[0-9]{6}$',
  116. 'LA': r'^[0-9]{5}$',
  117. 'LB': r'^[0-9]{8}$',
  118. 'LI': r'^[0-9]{4}$',
  119. 'LK': r'^[0-9]{5}$',
  120. 'LR': r'^[0-9]{4}$',
  121. 'LS': r'^[0-9]{3}$',
  122. 'LT': r'^(LT-)?[0-9]{5}$',
  123. 'LU': r'^[0-9]{4}$',
  124. 'LV': r'^LV-[0-9]{4}$',
  125. 'LY': r'^[0-9]{5}$',
  126. 'MA': r'^[0-9]{5}$',
  127. 'MC': r'^980[0-9]{2}$',
  128. 'MD': r'^MD-?[0-9]{4}$',
  129. 'ME': r'^[0-9]{5}$',
  130. 'MF': r'^[0-9]{5}$',
  131. 'MG': r'^[0-9]{3}$',
  132. 'MH': r'^[0-9]{5}$',
  133. 'MK': r'^[0-9]{4}$',
  134. 'MM': r'^[0-9]{5}$',
  135. 'MN': r'^[0-9]{5}$',
  136. 'MP': r'^[0-9]{5}$',
  137. 'MQ': r'^[0-9]{5}$',
  138. 'MT': r'^[A-Z]{3}[0-9]{4}$',
  139. 'MV': r'^[0-9]{4,5}$',
  140. 'MX': r'^[0-9]{5}$',
  141. 'MY': r'^[0-9]{5}$',
  142. 'MZ': r'^[0-9]{4}$',
  143. 'NA': r'^[0-9]{5}$',
  144. 'NC': r'^[0-9]{5}$',
  145. 'NE': r'^[0-9]{4}$',
  146. 'NF': r'^[0-9]{4}$',
  147. 'NG': r'^[0-9]{6}$',
  148. 'NI': r'^[0-9]{3}-[0-9]{3}-[0-9]$',
  149. 'NL': r'^[0-9]{4}[A-Z]{2}$',
  150. 'NO': r'^[0-9]{4}$',
  151. 'NP': r'^[0-9]{5}$',
  152. 'NZ': r'^[0-9]{4}$',
  153. 'OM': r'^[0-9]{3}$',
  154. 'PA': r'^[0-9]{6}$',
  155. 'PE': r'^[0-9]{5}$',
  156. 'PF': r'^[0-9]{5}$',
  157. 'PG': r'^[0-9]{3}$',
  158. 'PH': r'^[0-9]{4}$',
  159. 'PK': r'^[0-9]{5}$',
  160. 'PL': r'^[0-9]{2}-?[0-9]{3}$',
  161. 'PM': r'^[0-9]{5}$',
  162. 'PN': r'^[A-Z]{4}[0-9][A-Z]{2}$',
  163. 'PR': r'^[0-9]{5}$',
  164. 'PT': r'^[0-9]{4}(-?[0-9]{3})?$',
  165. 'PW': r'^[0-9]{5}$',
  166. 'PY': r'^[0-9]{4}$',
  167. 'RE': r'^[0-9]{5}$',
  168. 'RO': r'^[0-9]{6}$',
  169. 'RS': r'^[0-9]{5}$',
  170. 'RU': r'^[0-9]{6}$',
  171. 'SA': r'^[0-9]{5}$',
  172. 'SD': r'^[0-9]{5}$',
  173. 'SE': r'^[0-9]{5}$',
  174. 'SG': r'^([0-9]{2}|[0-9]{4}|[0-9]{6})$',
  175. 'SH': r'^(STHL1ZZ|TDCU1ZZ)$',
  176. 'SI': r'^(SI-)?[0-9]{4}$',
  177. 'SK': r'^[0-9]{5}$',
  178. 'SM': r'^[0-9]{5}$',
  179. 'SN': r'^[0-9]{5}$',
  180. 'SV': r'^01101$',
  181. 'SZ': r'^[A-Z][0-9]{3}$',
  182. 'TC': r'^TKCA1ZZ$',
  183. 'TD': r'^[0-9]{5}$',
  184. 'TH': r'^[0-9]{5}$',
  185. 'TJ': r'^[0-9]{6}$',
  186. 'TM': r'^[0-9]{6}$',
  187. 'TN': r'^[0-9]{4}$',
  188. 'TR': r'^[0-9]{5}$',
  189. 'TT': r'^[0-9]{6}$',
  190. 'TW': r'^[0-9]{5}$',
  191. 'UA': r'^[0-9]{5}$',
  192. 'US': r'^[0-9]{5}(-[0-9]{4}|-[0-9]{6})?$',
  193. 'UY': r'^[0-9]{5}$',
  194. 'UZ': r'^[0-9]{6}$',
  195. 'VA': r'^00120$',
  196. 'VC': r'^VC[0-9]{4}',
  197. 'VE': r'^[0-9]{4}[A-Z]?$',
  198. 'VG': r'^VG[0-9]{4}$',
  199. 'VI': r'^[0-9]{5}$',
  200. 'VN': r'^[0-9]{6}$',
  201. 'WF': r'^[0-9]{5}$',
  202. 'XK': r'^[0-9]{5}$',
  203. 'YT': r'^[0-9]{5}$',
  204. 'ZA': r'^[0-9]{4}$',
  205. 'ZM': r'^[0-9]{5}$',
  206. }
  207. title = models.CharField(
  208. pgettext_lazy(u"Treatment Pronouns for the customer", u"Title"),
  209. max_length=64, choices=TITLE_CHOICES, blank=True)
  210. first_name = models.CharField(_("First name"), max_length=255, blank=True)
  211. last_name = models.CharField(_("Last name"), max_length=255, blank=True)
  212. # We use quite a few lines of an address as they are often quite long and
  213. # it's easier to just hide the unnecessary ones than add extra ones.
  214. line1 = models.CharField(_("First line of address"), max_length=255)
  215. line2 = models.CharField(
  216. _("Second line of address"), max_length=255, blank=True)
  217. line3 = models.CharField(
  218. _("Third line of address"), max_length=255, blank=True)
  219. line4 = models.CharField(_("City"), max_length=255, blank=True)
  220. state = models.CharField(_("State/County"), max_length=255, blank=True)
  221. postcode = UppercaseCharField(
  222. _("Post/Zip-code"), max_length=64, blank=True)
  223. country = models.ForeignKey('address.Country', verbose_name=_("Country"))
  224. #: A field only used for searching addresses - this contains all the
  225. #: relevant fields. This is effectively a poor man's Solr text field.
  226. search_text = models.TextField(
  227. _("Search text - used only for searching addresses"), editable=False)
  228. def __str__(self):
  229. return self.summary
  230. class Meta:
  231. abstract = True
  232. verbose_name = _('Address')
  233. verbose_name_plural = _('Addresses')
  234. # Saving
  235. def save(self, *args, **kwargs):
  236. self._update_search_text()
  237. super(AbstractAddress, self).save(*args, **kwargs)
  238. def clean(self):
  239. # Strip all whitespace
  240. for field in ['first_name', 'last_name', 'line1', 'line2', 'line3',
  241. 'line4', 'state', 'postcode']:
  242. if self.__dict__[field]:
  243. self.__dict__[field] = self.__dict__[field].strip()
  244. # Ensure postcodes are valid for country
  245. self.ensure_postcode_is_valid_for_country()
  246. def ensure_postcode_is_valid_for_country(self):
  247. """
  248. Validate postcode given the country
  249. """
  250. if not self.postcode and self.country_id:
  251. country_code = self.country.iso_3166_1_a2
  252. regex = self.POSTCODES_REGEX.get(country_code, None)
  253. if regex:
  254. msg = _("Addresses in %(country)s require a valid postcode") \
  255. % {'country': self.country}
  256. raise exceptions.ValidationError(msg)
  257. if self.postcode and self.country_id:
  258. # Ensure postcodes are always uppercase
  259. postcode = self.postcode.upper().replace(' ', '')
  260. country_code = self.country.iso_3166_1_a2
  261. regex = self.POSTCODES_REGEX.get(country_code, None)
  262. # Validate postcode against regex for the country if available
  263. if regex and not re.match(regex, postcode):
  264. msg = _("The postcode '%(postcode)s' is not valid "
  265. "for %(country)s") \
  266. % {'postcode': self.postcode,
  267. 'country': self.country}
  268. raise exceptions.ValidationError(
  269. {'postcode': [msg]})
  270. def _update_search_text(self):
  271. search_fields = filter(
  272. bool, [self.first_name, self.last_name,
  273. self.line1, self.line2, self.line3, self.line4,
  274. self.state, self.postcode, self.country.name])
  275. self.search_text = ' '.join(search_fields)
  276. # Properties
  277. @property
  278. def city(self):
  279. # Common alias
  280. return self.line4
  281. @property
  282. def summary(self):
  283. """
  284. Returns a single string summary of the address,
  285. separating fields using commas.
  286. """
  287. return u", ".join(self.active_address_fields())
  288. @property
  289. def salutation(self):
  290. """
  291. Name (including title)
  292. """
  293. return self.join_fields(
  294. ('title', 'first_name', 'last_name'),
  295. separator=u" ")
  296. @property
  297. def name(self):
  298. return self.join_fields(('first_name', 'last_name'), separator=u" ")
  299. # Helpers
  300. def generate_hash(self):
  301. """
  302. Returns a hash of the address summary
  303. """
  304. # We use an upper-case version of the summary
  305. return zlib.crc32(self.summary.strip().upper().encode('UTF8'))
  306. def join_fields(self, fields, separator=u", "):
  307. """
  308. Join a sequence of fields using the specified separator
  309. """
  310. field_values = []
  311. for field in fields:
  312. # Title is special case
  313. if field == 'title':
  314. value = self.get_title_display()
  315. else:
  316. value = getattr(self, field)
  317. field_values.append(value)
  318. return separator.join(filter(bool, field_values))
  319. def populate_alternative_model(self, address_model):
  320. """
  321. For populating an address model using the matching fields
  322. from this one.
  323. This is used to convert a user address to a shipping address
  324. as part of the checkout process.
  325. """
  326. destination_field_names = [
  327. field.name for field in address_model._meta.fields]
  328. for field_name in [field.name for field in self._meta.fields]:
  329. if field_name in destination_field_names and field_name != 'id':
  330. setattr(address_model, field_name, getattr(self, field_name))
  331. def active_address_fields(self, include_salutation=True):
  332. """
  333. Return the non-empty components of the address, but merging the
  334. title, first_name and last_name into a single line.
  335. """
  336. fields = [self.line1, self.line2, self.line3,
  337. self.line4, self.state, self.postcode]
  338. if include_salutation:
  339. fields = [self.salutation] + fields
  340. fields = [f.strip() for f in fields if f]
  341. try:
  342. fields.append(self.country.name)
  343. except exceptions.ObjectDoesNotExist:
  344. pass
  345. return fields
  346. @python_2_unicode_compatible
  347. class AbstractCountry(models.Model):
  348. """
  349. International Organization for Standardization (ISO) 3166-1 Country list.
  350. The field names are a bit awkward, but kept for backwards compatibility.
  351. pycountry's syntax of alpha2, alpha3, name and official_name seems sane.
  352. """
  353. iso_3166_1_a2 = models.CharField(
  354. _('ISO 3166-1 alpha-2'), max_length=2, primary_key=True)
  355. iso_3166_1_a3 = models.CharField(
  356. _('ISO 3166-1 alpha-3'), max_length=3, blank=True)
  357. iso_3166_1_numeric = models.CharField(
  358. _('ISO 3166-1 numeric'), blank=True, max_length=3)
  359. #: The commonly used name; e.g. 'United Kingdom'
  360. printable_name = models.CharField(_('Country name'), max_length=128)
  361. #: The full official name of a country
  362. #: e.g. 'United Kingdom of Great Britain and Northern Ireland'
  363. name = models.CharField(_('Official name'), max_length=128)
  364. display_order = models.PositiveSmallIntegerField(
  365. _("Display order"), default=0, db_index=True,
  366. help_text=_('Higher the number, higher the country in the list.'))
  367. is_shipping_country = models.BooleanField(
  368. _("Is shipping country"), default=False, db_index=True)
  369. class Meta:
  370. abstract = True
  371. app_label = 'address'
  372. verbose_name = _('Country')
  373. verbose_name_plural = _('Countries')
  374. ordering = ('-display_order', 'printable_name',)
  375. def __str__(self):
  376. return self.printable_name or self.name
  377. @property
  378. def code(self):
  379. """
  380. Shorthand for the ISO 3166 Alpha-2 code
  381. """
  382. return self.iso_3166_1_a2
  383. @property
  384. def numeric_code(self):
  385. """
  386. Shorthand for the ISO 3166 numeric code.
  387. iso_3166_1_numeric used to wrongly be a integer field, but has to be
  388. padded with leading zeroes. It's since been converted to a char field,
  389. but the database might still contain non-padded strings. That's why
  390. the padding is kept.
  391. """
  392. return u"%.03d" % int(self.iso_3166_1_numeric)
  393. class AbstractShippingAddress(AbstractAddress):
  394. """
  395. A shipping address.
  396. A shipping address should not be edited once the order has been placed -
  397. it should be read-only after that.
  398. NOTE:
  399. ShippingAddress is a model of the order app. But moving it there is tricky
  400. due to circular import issues that are amplified by get_model/get_class
  401. calls pre-Django 1.7 to register receivers. So...
  402. TODO: Once Django 1.6 support is dropped, move AbstractBillingAddress and
  403. AbstractShippingAddress to the order app, and move
  404. PartnerAddress to the partner app.
  405. """
  406. phone_number = PhoneNumberField(
  407. _("Phone number"), blank=True,
  408. help_text=_("In case we need to call you about your order"))
  409. notes = models.TextField(
  410. blank=True, verbose_name=_('Instructions'),
  411. help_text=_("Tell us anything we should know when delivering "
  412. "your order."))
  413. class Meta:
  414. abstract = True
  415. # ShippingAddress is registered in order/models.py
  416. app_label = 'order'
  417. verbose_name = _("Shipping address")
  418. verbose_name_plural = _("Shipping addresses")
  419. @property
  420. def order(self):
  421. """
  422. Return the order linked to this shipping address
  423. """
  424. try:
  425. return self.order_set.all()[0]
  426. except IndexError:
  427. return None
  428. class AbstractUserAddress(AbstractShippingAddress):
  429. """
  430. A user's address. A user can have many of these and together they form an
  431. 'address book' of sorts for the user.
  432. We use a separate model for shipping and billing (even though there will be
  433. some data duplication) because we don't want shipping/billing addresses
  434. changed or deleted once an order has been placed. By having a separate
  435. model, we allow users the ability to add/edit/delete from their address
  436. book without affecting orders already placed.
  437. """
  438. user = models.ForeignKey(
  439. AUTH_USER_MODEL, related_name='addresses', verbose_name=_("User"))
  440. #: Whether this address is the default for shipping
  441. is_default_for_shipping = models.BooleanField(
  442. _("Default shipping address?"), default=False)
  443. #: Whether this address should be the default for billing.
  444. is_default_for_billing = models.BooleanField(
  445. _("Default billing address?"), default=False)
  446. #: We keep track of the number of times an address has been used
  447. #: as a shipping address so we can show the most popular ones
  448. #: first at the checkout.
  449. num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)
  450. #: A hash is kept to try and avoid duplicate addresses being added
  451. #: to the address book.
  452. hash = models.CharField(_("Address Hash"), max_length=255, db_index=True,
  453. editable=False)
  454. date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
  455. def save(self, *args, **kwargs):
  456. """
  457. Save a hash of the address fields
  458. """
  459. # Save a hash of the address fields so we can check whether two
  460. # addresses are the same to avoid saving duplicates
  461. self.hash = self.generate_hash()
  462. # Ensure that each user only has one default shipping address
  463. # and billing address
  464. self._ensure_defaults_integrity()
  465. super(AbstractUserAddress, self).save(*args, **kwargs)
  466. def _ensure_defaults_integrity(self):
  467. if self.is_default_for_shipping:
  468. self.__class__._default_manager\
  469. .filter(user=self.user, is_default_for_shipping=True)\
  470. .update(is_default_for_shipping=False)
  471. if self.is_default_for_billing:
  472. self.__class__._default_manager\
  473. .filter(user=self.user, is_default_for_billing=True)\
  474. .update(is_default_for_billing=False)
  475. class Meta:
  476. abstract = True
  477. app_label = 'address'
  478. verbose_name = _("User address")
  479. verbose_name_plural = _("User addresses")
  480. ordering = ['-num_orders']
  481. unique_together = ('user', 'hash')
  482. def validate_unique(self, exclude=None):
  483. super(AbstractAddress, self).validate_unique(exclude)
  484. qs = self.__class__.objects.filter(
  485. user=self.user,
  486. hash=self.generate_hash())
  487. if self.id:
  488. qs = qs.exclude(id=self.id)
  489. if qs.exists():
  490. raise exceptions.ValidationError({
  491. '__all__': [_("This address is already in your address"
  492. " book")]})
  493. class AbstractBillingAddress(AbstractAddress):
  494. class Meta:
  495. abstract = True
  496. # BillingAddress is registered in order/models.py
  497. app_label = 'order'
  498. verbose_name = _("Billing address")
  499. verbose_name_plural = _("Billing addresses")
  500. @property
  501. def order(self):
  502. """
  503. Return the order linked to this shipping address
  504. """
  505. try:
  506. return self.order_set.all()[0]
  507. except IndexError:
  508. return None
  509. class AbstractPartnerAddress(AbstractAddress):
  510. """
  511. A partner can have one or more addresses. This can be useful e.g. when
  512. determining US tax which depends on the origin of the shipment.
  513. """
  514. partner = models.ForeignKey('partner.Partner', related_name='addresses',
  515. verbose_name=_('Partner'))
  516. class Meta:
  517. abstract = True
  518. app_label = 'partner'
  519. verbose_name = _("Partner address")
  520. verbose_name_plural = _("Partner addresses")