123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- import re
- import zlib
-
- from django.db import models
- from django.utils.encoding import python_2_unicode_compatible
- from django.utils.translation import ugettext_lazy as _, pgettext_lazy
- from django.core import exceptions
-
- from oscar.core.compat import AUTH_USER_MODEL
- from oscar.models.fields import UppercaseCharField, PhoneNumberField
- from django.utils.six.moves import filter
-
-
- @python_2_unicode_compatible
- class AbstractAddress(models.Model):
- """
- Superclass address object
-
- This is subclassed and extended to provide models for
- user, shipping and billing addresses.
- """
- MR, MISS, MRS, MS, DR = ('Mr', 'Miss', 'Mrs', 'Ms', 'Dr')
- TITLE_CHOICES = (
- (MR, _("Mr")),
- (MISS, _("Miss")),
- (MRS, _("Mrs")),
- (MS, _("Ms")),
- (DR, _("Dr")),
- )
-
- # Regex for each country. Not listed countries don't use postcodes
- # Based on http://en.wikipedia.org/wiki/List_of_postal_codes
- POSTCODES_REGEX = {
- 'AC': r'^[A-Z]{4}[0-9][A-Z]$',
- 'AD': r'^AD[0-9]{3}$',
- 'AF': r'^[0-9]{4}$',
- 'AI': r'^AI-2640$',
- 'AL': r'^[0-9]{4}$',
- 'AM': r'^[0-9]{4}$',
- 'AR': r'^([0-9]{4}|[A-Z][0-9]{4}[A-Z]{3})$',
- 'AS': r'^[0-9]{5}(-[0-9]{4}|-[0-9]{6})?$',
- 'AT': r'^[0-9]{4}$',
- 'AU': r'^[0-9]{4}$',
- 'AX': r'^[0-9]{5}$',
- 'AZ': r'^AZ[0-9]{4}$',
- 'BA': r'^[0-9]{5}$',
- 'BB': r'^BB[0-9]{5}$',
- 'BD': r'^[0-9]{4}$',
- 'BE': r'^[0-9]{4}$',
- 'BG': r'^[0-9]{4}$',
- 'BH': r'^[0-9]{3,4}$',
- 'BL': r'^[0-9]{5}$',
- 'BM': r'^[A-Z]{2}([0-9]{2}|[A-Z]{2})',
- 'BN': r'^[A-Z}{2}[0-9]]{4}$',
- 'BO': r'^[0-9]{4}$',
- 'BR': r'^[0-9]{5}(-[0-9]{3})?$',
- 'BT': r'^[0-9]{3}$',
- 'BY': r'^[0-9]{6}$',
- 'CA': r'^[A-Z][0-9][A-Z][0-9][A-Z][0-9]$',
- 'CC': r'^[0-9]{4}$',
- 'CH': r'^[0-9]{4}$',
- 'CL': r'^([0-9]{7}|[0-9]{3}-[0-9]{4})$',
- 'CN': r'^[0-9]{6}$',
- 'CO': r'^[0-9]{6}$',
- 'CR': r'^[0-9]{4,5}$',
- 'CU': r'^[0-9]{5}$',
- 'CV': r'^[0-9]{4}$',
- 'CX': r'^[0-9]{4}$',
- 'CY': r'^[0-9]{4}$',
- 'CZ': r'^[0-9]{5}$',
- 'DE': r'^[0-9]{5}$',
- 'DK': r'^[0-9]{4}$',
- 'DO': r'^[0-9]{5}$',
- 'DZ': r'^[0-9]{5}$',
- 'EC': r'^EC[0-9]{6}$',
- 'EE': r'^[0-9]{5}$',
- 'EG': r'^[0-9]{5}$',
- 'ES': r'^[0-9]{5}$',
- 'ET': r'^[0-9]{4}$',
- 'FI': r'^[0-9]{5}$',
- 'FK': r'^[A-Z]{4}[0-9][A-Z]{2}$',
- 'FM': r'^[0-9]{5}(-[0-9]{4})?$',
- 'FO': r'^[0-9]{3}$',
- 'FR': r'^[0-9]{5}$',
- 'GA': r'^[0-9]{2}.*[0-9]{2}$',
- 'GB': r'^[A-Z][A-Z0-9]{1,3}[0-9][A-Z]{2}$',
- 'GE': r'^[0-9]{4}$',
- 'GF': r'^[0-9]{5}$',
- 'GG': r'^([A-Z]{2}[0-9]{2,3}[A-Z]{2})$',
- 'GI': r'^GX111AA$',
- 'GL': r'^[0-9]{4}$',
- 'GP': r'^[0-9]{5}$',
- 'GR': r'^[0-9]{5}$',
- 'GS': r'^SIQQ1ZZ$',
- 'GT': r'^[0-9]{5}$',
- 'GU': r'^[0-9]{5}$',
- 'GW': r'^[0-9]{4}$',
- 'HM': r'^[0-9]{4}$',
- 'HN': r'^[0-9]{5}$',
- 'HR': r'^[0-9]{5}$',
- 'HT': r'^[0-9]{4}$',
- 'HU': r'^[0-9]{4}$',
- 'ID': r'^[0-9]{5}$',
- 'IL': r'^[0-9]{7}$',
- 'IM': r'^IM[0-9]{2,3}[A-Z]{2}$$',
- 'IN': r'^[0-9]{6}$',
- 'IO': r'^[A-Z]{4}[0-9][A-Z]{2}$',
- 'IQ': r'^[0-9]{5}$',
- 'IR': r'^[0-9]{5}-[0-9]{5}$',
- 'IS': r'^[0-9]{3}$',
- 'IT': r'^[0-9]{5}$',
- 'JE': r'^JE[0-9]{2}[A-Z]{2}$',
- 'JM': r'^JM[A-Z]{3}[0-9]{2}$',
- 'JO': r'^[0-9]{5}$',
- 'JP': r'^[0-9]{3}-?[0-9]{4}$',
- 'KE': r'^[0-9]{5}$',
- 'KG': r'^[0-9]{6}$',
- 'KH': r'^[0-9]{5}$',
- 'KR': r'^[0-9]{3}-?[0-9]{3}$',
- 'KY': r'^KY[0-9]-[0-9]{4}$',
- 'KZ': r'^[0-9]{6}$',
- 'LA': r'^[0-9]{5}$',
- 'LB': r'^[0-9]{8}$',
- 'LI': r'^[0-9]{4}$',
- 'LK': r'^[0-9]{5}$',
- 'LR': r'^[0-9]{4}$',
- 'LS': r'^[0-9]{3}$',
- 'LT': r'^(LT-)?[0-9]{5}$',
- 'LU': r'^[0-9]{4}$',
- 'LV': r'^LV-[0-9]{4}$',
- 'LY': r'^[0-9]{5}$',
- 'MA': r'^[0-9]{5}$',
- 'MC': r'^980[0-9]{2}$',
- 'MD': r'^MD-?[0-9]{4}$',
- 'ME': r'^[0-9]{5}$',
- 'MF': r'^[0-9]{5}$',
- 'MG': r'^[0-9]{3}$',
- 'MH': r'^[0-9]{5}$',
- 'MK': r'^[0-9]{4}$',
- 'MM': r'^[0-9]{5}$',
- 'MN': r'^[0-9]{5}$',
- 'MP': r'^[0-9]{5}$',
- 'MQ': r'^[0-9]{5}$',
- 'MT': r'^[A-Z]{3}[0-9]{4}$',
- 'MV': r'^[0-9]{4,5}$',
- 'MX': r'^[0-9]{5}$',
- 'MY': r'^[0-9]{5}$',
- 'MZ': r'^[0-9]{4}$',
- 'NA': r'^[0-9]{5}$',
- 'NC': r'^[0-9]{5}$',
- 'NE': r'^[0-9]{4}$',
- 'NF': r'^[0-9]{4}$',
- 'NG': r'^[0-9]{6}$',
- 'NI': r'^[0-9]{3}-[0-9]{3}-[0-9]$',
- 'NL': r'^[0-9]{4}[A-Z]{2}$',
- 'NO': r'^[0-9]{4}$',
- 'NP': r'^[0-9]{5}$',
- 'NZ': r'^[0-9]{4}$',
- 'OM': r'^[0-9]{3}$',
- 'PA': r'^[0-9]{6}$',
- 'PE': r'^[0-9]{5}$',
- 'PF': r'^[0-9]{5}$',
- 'PG': r'^[0-9]{3}$',
- 'PH': r'^[0-9]{4}$',
- 'PK': r'^[0-9]{5}$',
- 'PL': r'^[0-9]{2}-?[0-9]{3}$',
- 'PM': r'^[0-9]{5}$',
- 'PN': r'^[A-Z]{4}[0-9][A-Z]{2}$',
- 'PR': r'^[0-9]{5}$',
- 'PT': r'^[0-9]{4}(-?[0-9]{3})?$',
- 'PW': r'^[0-9]{5}$',
- 'PY': r'^[0-9]{4}$',
- 'RE': r'^[0-9]{5}$',
- 'RO': r'^[0-9]{6}$',
- 'RS': r'^[0-9]{5}$',
- 'RU': r'^[0-9]{6}$',
- 'SA': r'^[0-9]{5}$',
- 'SD': r'^[0-9]{5}$',
- 'SE': r'^[0-9]{5}$',
- 'SG': r'^([0-9]{2}|[0-9]{4}|[0-9]{6})$',
- 'SH': r'^(STHL1ZZ|TDCU1ZZ)$',
- 'SI': r'^(SI-)?[0-9]{4}$',
- 'SK': r'^[0-9]{5}$',
- 'SM': r'^[0-9]{5}$',
- 'SN': r'^[0-9]{5}$',
- 'SV': r'^01101$',
- 'SZ': r'^[A-Z][0-9]{3}$',
- 'TC': r'^TKCA1ZZ$',
- 'TD': r'^[0-9]{5}$',
- 'TH': r'^[0-9]{5}$',
- 'TJ': r'^[0-9]{6}$',
- 'TM': r'^[0-9]{6}$',
- 'TN': r'^[0-9]{4}$',
- 'TR': r'^[0-9]{5}$',
- 'TT': r'^[0-9]{6}$',
- 'TW': r'^[0-9]{5}$',
- 'UA': r'^[0-9]{5}$',
- 'US': r'^[0-9]{5}(-[0-9]{4}|-[0-9]{6})?$',
- 'UY': r'^[0-9]{5}$',
- 'UZ': r'^[0-9]{6}$',
- 'VA': r'^00120$',
- 'VC': r'^VC[0-9]{4}',
- 'VE': r'^[0-9]{4}[A-Z]?$',
- 'VG': r'^VG[0-9]{4}$',
- 'VI': r'^[0-9]{5}$',
- 'VN': r'^[0-9]{6}$',
- 'WF': r'^[0-9]{5}$',
- 'XK': r'^[0-9]{5}$',
- 'YT': r'^[0-9]{5}$',
- 'ZA': r'^[0-9]{4}$',
- 'ZM': r'^[0-9]{5}$',
- }
-
- title = models.CharField(
- pgettext_lazy(u"Treatment Pronouns for the customer", u"Title"),
- max_length=64, choices=TITLE_CHOICES, blank=True)
- first_name = models.CharField(_("First name"), max_length=255, blank=True)
- last_name = models.CharField(_("Last name"), max_length=255, blank=True)
-
- # We use quite a few lines of an address as they are often quite long and
- # it's easier to just hide the unnecessary ones than add extra ones.
- line1 = models.CharField(_("First line of address"), max_length=255)
- line2 = models.CharField(
- _("Second line of address"), max_length=255, blank=True)
- line3 = models.CharField(
- _("Third line of address"), max_length=255, blank=True)
- line4 = models.CharField(_("City"), max_length=255, blank=True)
- state = models.CharField(_("State/County"), max_length=255, blank=True)
- postcode = UppercaseCharField(
- _("Post/Zip-code"), max_length=64, blank=True)
- country = models.ForeignKey('address.Country', verbose_name=_("Country"))
-
- #: A field only used for searching addresses - this contains all the
- #: relevant fields. This is effectively a poor man's Solr text field.
- search_text = models.TextField(
- _("Search text - used only for searching addresses"), editable=False)
-
- def __str__(self):
- return self.summary
-
- class Meta:
- abstract = True
- verbose_name = _('Address')
- verbose_name_plural = _('Addresses')
-
- # Saving
-
- def save(self, *args, **kwargs):
- self._update_search_text()
- super(AbstractAddress, self).save(*args, **kwargs)
-
- def clean(self):
- # Strip all whitespace
- for field in ['first_name', 'last_name', 'line1', 'line2', 'line3',
- 'line4', 'state', 'postcode']:
- if self.__dict__[field]:
- self.__dict__[field] = self.__dict__[field].strip()
-
- # Ensure postcodes are valid for country
- self.ensure_postcode_is_valid_for_country()
-
- def ensure_postcode_is_valid_for_country(self):
- """
- Validate postcode given the country
- """
- if not self.postcode and self.country_id:
- country_code = self.country.iso_3166_1_a2
- regex = self.POSTCODES_REGEX.get(country_code, None)
- if regex:
- msg = _("Addresses in %(country)s require a valid postcode") \
- % {'country': self.country}
- raise exceptions.ValidationError(msg)
-
- if self.postcode and self.country_id:
- # Ensure postcodes are always uppercase
- postcode = self.postcode.upper().replace(' ', '')
- country_code = self.country.iso_3166_1_a2
- regex = self.POSTCODES_REGEX.get(country_code, None)
-
- # Validate postcode against regex for the country if available
- if regex and not re.match(regex, postcode):
- msg = _("The postcode '%(postcode)s' is not valid "
- "for %(country)s") \
- % {'postcode': self.postcode,
- 'country': self.country}
- raise exceptions.ValidationError(
- {'postcode': [msg]})
-
- def _update_search_text(self):
- search_fields = filter(
- bool, [self.first_name, self.last_name,
- self.line1, self.line2, self.line3, self.line4,
- self.state, self.postcode, self.country.name])
- self.search_text = ' '.join(search_fields)
-
- # Properties
-
- @property
- def city(self):
- # Common alias
- return self.line4
-
- @property
- def summary(self):
- """
- Returns a single string summary of the address,
- separating fields using commas.
- """
- return u", ".join(self.active_address_fields())
-
- @property
- def salutation(self):
- """
- Name (including title)
- """
- return self.join_fields(
- ('title', 'first_name', 'last_name'),
- separator=u" ")
-
- @property
- def name(self):
- return self.join_fields(('first_name', 'last_name'), separator=u" ")
-
- # Helpers
-
- def generate_hash(self):
- """
- Returns a hash of the address summary
- """
- # We use an upper-case version of the summary
- return zlib.crc32(self.summary.strip().upper().encode('UTF8'))
-
- def join_fields(self, fields, separator=u", "):
- """
- Join a sequence of fields using the specified separator
- """
- field_values = []
- for field in fields:
- # Title is special case
- if field == 'title':
- value = self.get_title_display()
- else:
- value = getattr(self, field)
- field_values.append(value)
- return separator.join(filter(bool, field_values))
-
- def populate_alternative_model(self, address_model):
- """
- For populating an address model using the matching fields
- from this one.
-
- This is used to convert a user address to a shipping address
- as part of the checkout process.
- """
- destination_field_names = [
- field.name for field in address_model._meta.fields]
- for field_name in [field.name for field in self._meta.fields]:
- if field_name in destination_field_names and field_name != 'id':
- setattr(address_model, field_name, getattr(self, field_name))
-
- def active_address_fields(self, include_salutation=True):
- """
- Return the non-empty components of the address, but merging the
- title, first_name and last_name into a single line.
- """
- fields = [self.line1, self.line2, self.line3,
- self.line4, self.state, self.postcode]
- if include_salutation:
- fields = [self.salutation] + fields
- fields = [f.strip() for f in fields if f]
- try:
- fields.append(self.country.name)
- except exceptions.ObjectDoesNotExist:
- pass
- return fields
-
-
- @python_2_unicode_compatible
- class AbstractCountry(models.Model):
- """
- International Organization for Standardization (ISO) 3166-1 Country list.
-
- The field names are a bit awkward, but kept for backwards compatibility.
- pycountry's syntax of alpha2, alpha3, name and official_name seems sane.
- """
- iso_3166_1_a2 = models.CharField(
- _('ISO 3166-1 alpha-2'), max_length=2, primary_key=True)
- iso_3166_1_a3 = models.CharField(
- _('ISO 3166-1 alpha-3'), max_length=3, blank=True)
- iso_3166_1_numeric = models.CharField(
- _('ISO 3166-1 numeric'), blank=True, max_length=3)
-
- #: The commonly used name; e.g. 'United Kingdom'
- printable_name = models.CharField(_('Country name'), max_length=128)
- #: The full official name of a country
- #: e.g. 'United Kingdom of Great Britain and Northern Ireland'
- name = models.CharField(_('Official name'), max_length=128)
-
- display_order = models.PositiveSmallIntegerField(
- _("Display order"), default=0, db_index=True,
- help_text=_('Higher the number, higher the country in the list.'))
-
- is_shipping_country = models.BooleanField(
- _("Is shipping country"), default=False, db_index=True)
-
- class Meta:
- abstract = True
- app_label = 'address'
- verbose_name = _('Country')
- verbose_name_plural = _('Countries')
- ordering = ('-display_order', 'printable_name',)
-
- def __str__(self):
- return self.printable_name or self.name
-
- @property
- def code(self):
- """
- Shorthand for the ISO 3166 Alpha-2 code
- """
- return self.iso_3166_1_a2
-
- @property
- def numeric_code(self):
- """
- Shorthand for the ISO 3166 numeric code.
-
- iso_3166_1_numeric used to wrongly be a integer field, but has to be
- padded with leading zeroes. It's since been converted to a char field,
- but the database might still contain non-padded strings. That's why
- the padding is kept.
- """
- return u"%.03d" % int(self.iso_3166_1_numeric)
-
-
- class AbstractShippingAddress(AbstractAddress):
- """
- A shipping address.
-
- A shipping address should not be edited once the order has been placed -
- it should be read-only after that.
-
- NOTE:
- ShippingAddress is a model of the order app. But moving it there is tricky
- due to circular import issues that are amplified by get_model/get_class
- calls pre-Django 1.7 to register receivers. So...
- TODO: Once Django 1.6 support is dropped, move AbstractBillingAddress and
- AbstractShippingAddress to the order app, and move
- PartnerAddress to the partner app.
- """
-
- phone_number = PhoneNumberField(
- _("Phone number"), blank=True,
- help_text=_("In case we need to call you about your order"))
- notes = models.TextField(
- blank=True, verbose_name=_('Instructions'),
- help_text=_("Tell us anything we should know when delivering "
- "your order."))
-
- class Meta:
- abstract = True
- # ShippingAddress is registered in order/models.py
- app_label = 'order'
- verbose_name = _("Shipping address")
- verbose_name_plural = _("Shipping addresses")
-
- @property
- def order(self):
- """
- Return the order linked to this shipping address
- """
- try:
- return self.order_set.all()[0]
- except IndexError:
- return None
-
-
- class AbstractUserAddress(AbstractShippingAddress):
- """
- A user's address. A user can have many of these and together they form an
- 'address book' of sorts for the user.
-
- We use a separate model for shipping and billing (even though there will be
- some data duplication) because we don't want shipping/billing addresses
- changed or deleted once an order has been placed. By having a separate
- model, we allow users the ability to add/edit/delete from their address
- book without affecting orders already placed.
- """
- user = models.ForeignKey(
- AUTH_USER_MODEL, related_name='addresses', verbose_name=_("User"))
-
- #: Whether this address is the default for shipping
- is_default_for_shipping = models.BooleanField(
- _("Default shipping address?"), default=False)
-
- #: Whether this address should be the default for billing.
- is_default_for_billing = models.BooleanField(
- _("Default billing address?"), default=False)
-
- #: We keep track of the number of times an address has been used
- #: as a shipping address so we can show the most popular ones
- #: first at the checkout.
- num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)
-
- #: A hash is kept to try and avoid duplicate addresses being added
- #: to the address book.
- hash = models.CharField(_("Address Hash"), max_length=255, db_index=True,
- editable=False)
- date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
-
- def save(self, *args, **kwargs):
- """
- Save a hash of the address fields
- """
- # Save a hash of the address fields so we can check whether two
- # addresses are the same to avoid saving duplicates
- self.hash = self.generate_hash()
-
- # Ensure that each user only has one default shipping address
- # and billing address
- self._ensure_defaults_integrity()
- super(AbstractUserAddress, self).save(*args, **kwargs)
-
- def _ensure_defaults_integrity(self):
- if self.is_default_for_shipping:
- self.__class__._default_manager\
- .filter(user=self.user, is_default_for_shipping=True)\
- .update(is_default_for_shipping=False)
- if self.is_default_for_billing:
- self.__class__._default_manager\
- .filter(user=self.user, is_default_for_billing=True)\
- .update(is_default_for_billing=False)
-
- class Meta:
- abstract = True
- app_label = 'address'
- verbose_name = _("User address")
- verbose_name_plural = _("User addresses")
- ordering = ['-num_orders']
- unique_together = ('user', 'hash')
-
- def validate_unique(self, exclude=None):
- super(AbstractAddress, self).validate_unique(exclude)
- qs = self.__class__.objects.filter(
- user=self.user,
- hash=self.generate_hash())
- if self.id:
- qs = qs.exclude(id=self.id)
- if qs.exists():
- raise exceptions.ValidationError({
- '__all__': [_("This address is already in your address"
- " book")]})
-
-
- class AbstractBillingAddress(AbstractAddress):
- class Meta:
- abstract = True
- # BillingAddress is registered in order/models.py
- app_label = 'order'
- verbose_name = _("Billing address")
- verbose_name_plural = _("Billing addresses")
-
- @property
- def order(self):
- """
- Return the order linked to this shipping address
- """
- try:
- return self.order_set.all()[0]
- except IndexError:
- return None
-
-
- class AbstractPartnerAddress(AbstractAddress):
- """
- A partner can have one or more addresses. This can be useful e.g. when
- determining US tax which depends on the origin of the shipment.
- """
- partner = models.ForeignKey('partner.Partner', related_name='addresses',
- verbose_name=_('Partner'))
-
- class Meta:
- abstract = True
- app_label = 'partner'
- verbose_name = _("Partner address")
- verbose_name_plural = _("Partner addresses")
|