from datetime import date from calendar import monthrange import re from django import forms from django.db.models import get_model from django.utils.translation import ugettext_lazy as _ from oscar.core.loading import get_class Country = get_model('address', 'Country') BillingAddress = get_model('order', 'BillingAddress') BankcardModel = get_model('payment', 'Bankcard') Bankcard = get_class('payment.utils', 'Bankcard') VISA, MASTERCARD, AMEX, MAESTRO, DISCOVER = ('Visa', 'Mastercard', 'American Express', 'Maestro', 'Discover') def bankcard_type(number): u""" Returns the type of a bankcard based on its number. """ number = str(number) if len(number) == 13: if number[0] == "4": return VISA elif len(number) == 14: if number[:2] == "36": return MASTERCARD elif len(number) == 15: if number[:2] in ("34", "37"): return AMEX elif len(number) == 16: if number[:4] == "6011": return DISCOVER if number[:2] in ("51", "52", "53", "54", "55"): return MASTERCARD if number[0] == "4": return VISA return None def luhn(card_number): u""" Tests whether a bankcard number passes the Luhn algorithm. """ card_number = str(card_number) sum = 0 num_digits = len(card_number) odd_even = num_digits & 1 for i in range(0, num_digits): digit = int(card_number[i]) if not (( i & 1 ) ^ odd_even ): digit = digit * 2 if digit > 9: digit = digit - 9 sum = sum + digit return (sum % 10) == 0 class BankcardNumberField(forms.CharField): def clean(self, value): """Check if given CC number is valid and one of the card types we accept""" non_decimal = re.compile(r'\D+') value = non_decimal.sub('', value.strip()) if value and not luhn(value): raise forms.ValidationError(_("Please enter a valid credit card number.")) return super(BankcardNumberField, self).clean(value) class BankcardMonthWidget(forms.MultiWidget): """ Widget containing two select boxes for selecting the month and year """ def decompress(self, value): return [value.month, value.year] if value else [None, None] def format_output(self, rendered_widgets): html = u' '.join(rendered_widgets) return u'%s' % html class BankcardMonthField(forms.MultiValueField): """ A modified version of the snippet: http://djangosnippets.org/snippets/907/ """ default_error_messages = { 'invalid_month': _('Enter a valid month.'), 'invalid_year': _('Enter a valid year.'), } num_years = 5 def __init__(self, *args, **kwargs): # Allow the number of years to be specified if 'num_years' in kwargs: self.num_years = kwargs['num_years'] del kwargs['num_years'] errors = self.default_error_messages.copy() if 'error_messages' in kwargs: errors.update(kwargs['error_messages']) fields = ( forms.ChoiceField(choices=self.month_choices(), error_messages={'invalid': errors['invalid_month']}), forms.ChoiceField(choices=self.year_choices(), error_messages={'invalid': errors['invalid_year']}), ) super(BankcardMonthField, self).__init__(fields, *args, **kwargs) self.widget = BankcardMonthWidget(widgets = [fields[0].widget, fields[1].widget]) def month_choices(self): return [] def year_choices(self): return [] class BankcardExpiryMonthField(BankcardMonthField): """ Expiry month """ def month_choices(self): return [("%.2d" % x, "%.2d" % x) for x in xrange(1, 13)] def year_choices(self): return [(x, x) for x in xrange(date.today().year, date.today().year+self.num_years)] def clean(self, value): expiry_date = super(BankcardExpiryMonthField, self).clean(value) if date.today() > expiry_date: raise forms.ValidationError(_("The expiration date you entered is in the past.")) return expiry_date def compress(self, data_list): if data_list: if data_list[1] in forms.fields.EMPTY_VALUES: error = self.error_messages['invalid_year'] raise forms.ValidationError(error) if data_list[0] in forms.fields.EMPTY_VALUES: error = self.error_messages['invalid_month'] raise forms.ValidationError(error) year = int(data_list[1]) month = int(data_list[0]) # find last day of the month day = monthrange(year, month)[1] return date(year, month, day) return None class BankcardStartingMonthField(BankcardMonthField): def month_choices(self): months = [("%.2d" % x, "%.2d" % x) for x in xrange(1, 13)] months.insert(0, ("", "--")) return months def year_choices(self): years = [(x, x) for x in xrange(date.today().year - self.num_years, date.today().year + 1)] years.insert(0, ("", "--")) return years def clean(self, value): starting_date = super(BankcardMonthField, self).clean(value) if starting_date and date.today() < starting_date: raise forms.ValidationError(_("The starting date you entered is in the future.")) return starting_date def compress(self, data_list): if data_list: if data_list[1] in forms.fields.EMPTY_VALUES: error = self.error_messages['invalid_year'] raise forms.ValidationError(error) if data_list[0] in forms.fields.EMPTY_VALUES: error = self.error_messages['invalid_month'] raise forms.ValidationError(error) year = int(data_list[1]) month = int(data_list[0]) return date(year, month, 1) return None class BankcardForm(forms.ModelForm): number = BankcardNumberField(max_length=20, widget=forms.TextInput(attrs={'autocomplete':'off'}), label=_("Card number")) # Name is not normally needed by payment gateways so we hide it by default name = forms.CharField(max_length=128, label=_("Name on card"), widget=forms.widgets.HiddenInput, required=False) cvv_number = forms.RegexField( required=True, label=_("CVV Number"), regex=r'^\d{3,4}$', widget=forms.TextInput(attrs={'size': '5'}), help_text=_("This is the 3 or 4 digit security number on the back of your bankcard")) start_month = BankcardStartingMonthField(label=_("Valid from"), required=False) expiry_month = BankcardExpiryMonthField(required=True, label=_("Valid to")) def __init__(self, *args, **kwargs): if 'initial' not in kwargs: # Set the initial expiry month to be current month today = date.today() kwargs['initial'] = {'expiry_month': ["%.2d" % today.month, today.year]} super(BankcardForm, self).__init__(*args, **kwargs) class Meta: model = BankcardModel exclude = ('user', 'partner_reference') fields = ('number', 'name', 'start_month', 'expiry_month', 'cvv_number') def get_bankcard_obj(self): """ Returns a Bankcard object for use in payment processing. """ kwargs = { 'name': self.cleaned_data['name'], 'card_number': self.cleaned_data['number'], 'expiry_date': self.cleaned_data['expiry_month'].strftime("%m/%y"), 'cvv': self.cleaned_data['cvv_number'], } if self.cleaned_data['start_month']: kwargs['start_date'] = self.cleaned_data['start_month'].strftime("%m/%y") return Bankcard(**kwargs) class BillingAddressForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(BillingAddressForm,self ).__init__(*args, **kwargs) self.set_country_queryset() def set_country_queryset(self): self.fields['country'].queryset = Country._default_manager.all() class Meta: model = BillingAddress exclude = ('search_text',)