from django import forms from django.forms import ValidationError from django.forms.utils import ErrorList from django.db.models import Q from django.core.validators import MaxValueValidator, MinValueValidator from django.contrib.auth.models import User from django.conf import settings from .models import Sponsor, SponsorMail, SponsorshipLevel, SponsorshipContract from .models import vat_status_choices from .models import Shipment from postgresqleu.confreg.models import RegistrationType, DiscountCode from postgresqleu.countries.models import EuropeCountry from postgresqleu.confreg.models import ConferenceAdditionalOption from postgresqleu.confreg.twitter import get_all_conference_social_media from postgresqleu.util.fields import UserModelChoiceField from postgresqleu.util.validators import BeforeValidator, AfterValidator from postgresqleu.util.validators import Http200Validator from postgresqleu.util.widgets import Bootstrap4CheckboxSelectMultiple, EmailTextWidget from postgresqleu.util.widgets import Bootstrap4HtmlDateTimeInput from postgresqleu.util.time import today_conference from datetime import timedelta from decimal import Decimal def _int_with_default(s, default): try: return int(s) except ValueError: return default except TypeError: return default class SponsorSignupForm(forms.Form): name = forms.CharField(label="Company name *", min_length=3, max_length=100, help_text="This name is used on invoices and in internal communication") displayname = forms.CharField(label="Display name *", min_length=3, max_length=100, help_text="This name is displayed on websites and in public communication") address = forms.CharField(label="Company invoice address *", min_length=10, max_length=500, widget=forms.Textarea, help_text="The sponsor name is automatically included at beginning of address. The VAT number is automatically included at end of address.") vatstatus = forms.ChoiceField(label="Company VAT status", choices=vat_status_choices) vatnumber = forms.CharField(label="EU VAT Number", min_length=5, max_length=50, help_text="Enter EU VAT Number to be included on invoices if assigned one. Leave empty if outside the EU or without assigned VAT number.", required=False) url = forms.URLField(label="Company URL *", validators=[Http200Validator, ]) def __init__(self, conference, *args, **kwargs): self.conference = conference super(SponsorSignupForm, self).__init__(*args, **kwargs) for classname, social, impl in sorted(get_all_conference_social_media('sponsor'), key=lambda x: x[1]): fn = "social_{}".format(social) self.fields[fn] = forms.CharField(label="Company {}".format(social.title()), max_length=250, required=False) if hasattr(impl, 'get_field_help'): self.fields[fn].help_text = impl.get_field_help('sponsor') if not settings.EU_VAT: del self.fields['vatstatus'] del self.fields['vatnumber'] def clean_name(self): if Sponsor.objects.filter(conference=self.conference, name__iexact=self.cleaned_data['name']).exists(): raise ValidationError("A sponsor with this name is already signed up for this conference!") return self.cleaned_data['name'] def clean_displayname(self): if Sponsor.objects.filter(conference=self.conference, displayname__iexact=self.cleaned_data['displayname']).exists(): raise ValidationError("A sponsor with this display name is already signed up for this conference!") return self.cleaned_data['displayname'] def clean_vatnumber(self): # EU VAT numbers begin with a two letter country-code, so let's # validate that first v = self.cleaned_data['vatnumber'].upper().replace(' ', '') if v == "": # We allow empty VAT numbers, for sponsors from outside of # europe. return v countrycode = v[:2] if countrycode == 'EL': # Greece for some reason uses EL instead of their ISO code GR # (ref: https://en.wikipedia.org/wiki/VAT_identification_number#Structure) countrycode = 'GR' if not EuropeCountry.objects.filter(iso=countrycode).exists(): raise ValidationError("VAT numbers must begin with the two letter country code") if settings.EU_VAT_VALIDATE: from . import vatutil r = vatutil.validate_eu_vat_number(v) if r: raise ValidationError("Invalid VAT number: %s" % r) return v def clean(self): cleaned_data = super(SponsorSignupForm, self).clean() for classname, social, impl in get_all_conference_social_media('sponsor'): fn = 'social_{}'.format(social) if cleaned_data.get(fn, None): try: cleaned_data[fn] = impl.clean_identifier_form_value('sponsor', cleaned_data[fn]) except ValidationError as v: self.add_error(fn, v) if settings.EU_VAT: if int(cleaned_data['vatstatus']) == 0: # Company inside EU and has VAT number if not cleaned_data.get('vatnumber', None): self.add_error('vatnumber', 'VAT number must be specified for companies inside EU with VAT number') elif int(cleaned_data['vatstatus']) == 1: # Company inside EU but without VAT number if cleaned_data.get('vatnumber', None): self.add_error('vatnumber', 'VAT number should not be specified for companies without one!') else: # Company outside EU if cleaned_data.get('vatnumber', None): self.add_error('vatnumber', 'VAT number should not be specified for companies outside EU') return cleaned_data class SponsorSendEmailForm(forms.ModelForm): confirm = forms.BooleanField(label="Confirm", required=False) class Meta: model = SponsorMail exclude = ('conference', 'sent', ) widgets = { 'message': EmailTextWidget(), } def __init__(self, conference, sendto, *args, **kwargs): self.conference = conference self.sendto = sendto super(SponsorSendEmailForm, self).__init__(*args, **kwargs) if self.sendto == 'level': self.fields['levels'].widget = forms.CheckboxSelectMultiple() self.fields['levels'].queryset = SponsorshipLevel.objects.filter(conference=self.conference) self.fields['levels'].required = True del self.fields['sponsors'] else: self.fields['sponsors'].widget = forms.CheckboxSelectMultiple() self.fields['sponsors'].queryset = Sponsor.objects.select_related('level').filter(conference=self.conference) self.fields['sponsors'].required = True self.fields['sponsors'].label_from_instance = lambda s: "{0} ({1}, {2})".format(s, s.level.levelname, s.confirmed and "confirmed {:%Y-%m-%d %H:%M}".format(s.confirmedat) or "NOT confirmed") del self.fields['levels'] self.fields['subject'].help_text = 'Subject will be prefixed with [{}]'.format(conference) if not ((self.data.get('levels') or self.data.get('sponsors')) and self.data.get('subject') and self.data.get('message')): del self.fields['confirm'] def clean_confirm(self): if not self.cleaned_data['confirm']: raise ValidationError("Please check this box to confirm that you are really sending this email! There is no going back!") class PurchaseVouchersForm(forms.Form): regtype = forms.ModelChoiceField(queryset=None, required=True, label="Registration type") num = forms.IntegerField(required=True, initial=2, label="Number of vouchers", validators=[MinValueValidator(1), ]) confirm = forms.BooleanField(help_text="Check this form to confirm that you will pay the generated invoice") def __init__(self, conference, *args, **kwargs): self.conference = conference super(PurchaseVouchersForm, self).__init__(*args, **kwargs) activeQ = Q(activeuntil__isnull=True) | Q(activeuntil__gte=today_conference()) if self.data and self.data.get('regtype', None) and self.data.get('num', None) and _int_with_default(self.data['num'], 0) > 0: RegistrationType.objects.get(pk=self.data['regtype']) self.fields['confirm'].help_text = 'Check this box to confirm that you will pay the generated invoice' self.fields['num'].widget.attrs['readonly'] = True self.fields['regtype'].queryset = RegistrationType.objects.filter(pk=self.data['regtype']) else: self.fields['regtype'].queryset = RegistrationType.objects.filter(Q(conference=self.conference, active=True, specialtype__isnull=True, cost__gt=0) & activeQ) del self.fields['confirm'] class PurchaseDiscountForm(forms.Form): code = forms.CharField(required=True, max_length=100, min_length=4, help_text='Enter the code you want to use to provide the discount.') amount = forms.IntegerField(required=False, initial=0, label="Fixed discount in {0}".format(settings.CURRENCY_ABBREV), validators=[MinValueValidator(0), ]) percent = forms.IntegerField(required=False, initial=0, label="Percent discount", validators=[MinValueValidator(0), MaxValueValidator(100), ]) maxuses = forms.IntegerField(required=True, initial=1, label="Maximum uses", validators=[MinValueValidator(1), MaxValueValidator(30), ]) expires = forms.DateField(required=True, label="Expiry date") requiredoptions = forms.ModelMultipleChoiceField( required=False, queryset=None, label="Attendee add-ons required to use the code", widget=Bootstrap4CheckboxSelectMultiple, help_text="Check any additional options that are required. Registrations without those options will not be able to use the discount code." ) confirm = forms.BooleanField(help_text="Check this form to confirm that you will pay the costs generated by the people using this code, as specified by the invoice.") def __init__(self, conference, showconfirm=False, *args, **kwargs): self.conference = conference super(PurchaseDiscountForm, self).__init__(*args, **kwargs) self.fields['requiredoptions'].queryset = ConferenceAdditionalOption.objects.filter(conference=conference, public=True) self.fields['expires'].initial = conference.startdate - timedelta(days=2) self.fields['expires'].validators.append(BeforeValidator(conference.startdate - timedelta(days=1))) self.fields['expires'].validators.append(AfterValidator(today_conference() - timedelta(days=1))) if not showconfirm: del self.fields['confirm'] def clean_code(self): # Check if code is already in use for this conference if DiscountCode.objects.filter(conference=self.conference, code=self.cleaned_data['code'].upper()).exists(): raise ValidationError("This discount code is already in use for this conference") # Force to uppercase. CSS takes care of that at the presentation layer return self.cleaned_data['code'].upper() def clean(self): cleaned_data = super(PurchaseDiscountForm, self).clean() if 'amount' in cleaned_data and 'percent' in cleaned_data: # Only one can be specified if _int_with_default(cleaned_data['amount'], 0) > 0 and _int_with_default(cleaned_data['percent'], 0) > 0: self._errors['amount'] = ErrorList(['Cannot specify both amount and percent!']) self._errors['percent'] = ErrorList(['Cannot specify both amount and percent!']) elif _int_with_default(cleaned_data['amount'], 0) == 0 and _int_with_default(cleaned_data['percent'], 0) == 0: self._errors['amount'] = ErrorList(['Must specify amount or percent!']) self._errors['percent'] = ErrorList(['Must specify amount or percent!']) return cleaned_data class SponsorDetailsForm(forms.ModelForm): class Meta: model = Sponsor fields = ('extra_cc', ) class SponsorRefundForm(forms.Form): refundamount = forms.ChoiceField(choices=( (0, "Refund full invoice cost"), (1, "Refund custom amount"), (2, "Don't refund"), ), label="Refund amount") customrefundamount = forms.DecimalField(decimal_places=2, required=False, label="Custom refund amount (ex VAT)") customrefundamountvat = forms.DecimalField(decimal_places=2, required=False, label="Custom VAT refund amount") cancelmethod = forms.ChoiceField(choices=( (0, "Cancel sponsorship"), (1, "Leave sponsorship active"), ), label="Cancel method") confirm = forms.BooleanField(help_text="Confirm that you want to cancel or refund this sponsorship!") def __init__(self, invoice, *args, **kwargs): self.invoice = invoice super().__init__(*args, **kwargs) if not invoice.total_vat: self.fields['customrefundamount'].label = 'Custom refund amount' del self.fields['customrefundamountvat'] if 'refundamount' not in self.data or 'cancelmethod' not in self.data: del self.fields['confirm'] def clean(self): d = super().clean() if int(d['refundamount']) == 1: # Custom amount, so make sure both those fields are set if d['customrefundamount'] is None: self.add_error('customrefundamount', 'This field is required when doing custom amount refund') elif Decimal(d['customrefundamount'] <= 0): self.add_error('customrefundamount', 'Must be >0 when performing custom refund') if self.invoice.total_vat and d['customrefundamountvat'] is None: self.add_error('customrefundamountvat', 'This field is required when doing custom amount refund') else: if d['customrefundamount'] is not None: self.add_error('customrefundamount', 'This field must be left empty when doing non-custom refund') if self.invoice.total_vat and d['customrefundamountvat'] is not None: self.add_error('customrefundamountvat', 'This field must be left empty when doing non-custom refund') return d class SponsorReissueForm(forms.Form): confirm = forms.BooleanField(required=True, label='Confirm reissuing of invoice', help_text='New invoice will be automatically sent to the sponsor') class SponsorShipmentForm(forms.ModelForm): sent_parcels = forms.ChoiceField(choices=[], required=True) class Meta: model = Shipment fields = ('description', 'sent_parcels', 'sent_at', 'trackingnumber', 'shippingcompany', 'trackinglink') widgets = { 'sent_at': Bootstrap4HtmlDateTimeInput, } fieldsets = [ { 'id': 'shipment', 'legend': 'Shipment', 'fields': ['description', 'sent_parcels', 'sent_at', ], }, { 'id': 'tracking', 'legend': 'Tracking', 'fields': ['trackingnumber', 'shippingcompany', 'trackinglink', ], } ] def __init__(self, *args, **kwargs): super(SponsorShipmentForm, self).__init__(*args, **kwargs) self.fields['sent_at'].help_text = "Date and (approximate) time when parcels were sent. DO NOT set until shipment is actually sent" self.fields['sent_parcels'].choices = [('0', " * Don't know yet"), ] + [(str(x), str(x)) for x in range(1, 20)] self.fields['trackinglink'].validators.append(Http200Validator) def get(self, name, default=None): return self[name] class ShipmentReceiverForm(forms.ModelForm): arrived_parcels = forms.ChoiceField(choices=[], required=True) class Meta: model = Shipment fields = ['arrived_parcels', ] def __init__(self, *args, **kwargs): super(ShipmentReceiverForm, self).__init__(*args, **kwargs) self.fields['arrived_parcels'].choices = [(str(x), str(x)) for x in range(1, 20)] class SponsorAddContractForm(forms.Form): subject = forms.CharField(max_length=100, required=True) contract = forms.ModelChoiceField(SponsorshipContract.objects.all()) manager = UserModelChoiceField(User.objects.all(), label="Manager to send to") message = forms.CharField(label="Message to send in signing email", widget=forms.Textarea) def __init__(self, sponsor, *args, **kwargs): self.sponsor = sponsor super().__init__(*args, **kwargs) self.fields['subject'].help_text = "Subject of contract, for example 'Training contract'. Will be prefixed with '[{}]' in all emails.".format(self.sponsor.conference.conferencename) self.fields['contract'].queryset = SponsorshipContract.objects.filter(conference=self.sponsor.conference, sponsorshiplevel=None) if sponsor.signmethod == 1: # Manual contracts are always sent to all managers del self.fields['manager'] else: self.fields['manager'].queryset = self.sponsor.managers