From 63b0fc6269b38edf7234b9f151b80d81f614c0a3 Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Tue, 4 Aug 2015 10:11:24 +0300 Subject: Initial commit First public commit --- servo/forms/__init__.py | 33 +++ servo/forms/account.py | 104 +++++++++ servo/forms/admin.py | 557 ++++++++++++++++++++++++++++++++++++++++++++++++ servo/forms/base.py | 172 +++++++++++++++ servo/forms/checkin.py | 303 ++++++++++++++++++++++++++ servo/forms/customer.py | 113 ++++++++++ servo/forms/devices.py | 94 ++++++++ servo/forms/invoices.py | 108 ++++++++++ servo/forms/notes.py | 91 ++++++++ servo/forms/orders.py | 167 +++++++++++++++ servo/forms/product.py | 228 ++++++++++++++++++++ servo/forms/repairs.py | 99 +++++++++ servo/forms/returns.py | 112 ++++++++++ 13 files changed, 2181 insertions(+) create mode 100644 servo/forms/__init__.py create mode 100644 servo/forms/account.py create mode 100644 servo/forms/admin.py create mode 100644 servo/forms/base.py create mode 100644 servo/forms/checkin.py create mode 100644 servo/forms/customer.py create mode 100644 servo/forms/devices.py create mode 100644 servo/forms/invoices.py create mode 100644 servo/forms/notes.py create mode 100644 servo/forms/orders.py create mode 100644 servo/forms/product.py create mode 100644 servo/forms/repairs.py create mode 100644 servo/forms/returns.py (limited to 'servo/forms') diff --git a/servo/forms/__init__.py b/servo/forms/__init__.py new file mode 100644 index 0000000..f41490b --- /dev/null +++ b/servo/forms/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from base import * +from orders import * +from notes import * +from devices import * +from product import * +from repairs import * +from .checkin import * diff --git a/servo/forms/account.py b/servo/forms/account.py new file mode 100644 index 0000000..775069e --- /dev/null +++ b/servo/forms/account.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from django import forms +from django.utils.translation import ugettext as _ + +from servo.forms.base import BaseForm, BaseModelForm +from servo.models.account import User + + +class ProfileForm(BaseModelForm): + # User Profile form for users + class Meta: + model = User + fields = ( + "location", + "photo", + "locale", + "queues", + "region", + "timezone", + "should_notify", + "notify_by_email", + "autoprint", + "tech_id", + "gsx_userid", + "gsx_password", + "gsx_poprefix", + ) + widgets = { + 'gsx_password': forms.PasswordInput, + 'queues': forms.CheckboxSelectMultiple + } + + password1 = forms.CharField( + widget=forms.PasswordInput, + required=False, + label=_("Password") + ) + password2 = forms.CharField( + widget=forms.PasswordInput, + required=False, + label=_("Confirmation") + ) + + def clean(self): + cd = super(ProfileForm, self).clean() + + if cd.get('gsx_password') == "": + del cd['gsx_password'] + + cd['tech_id'] = cd['tech_id'].upper() + + if cd.get('password1'): + if cd['password1'] != cd['password2']: + raise forms.ValidationError(_("Password and confirmation do not match!")) + + return cd + + def clean_photo(self): + photo = self.cleaned_data.get('photo') + if photo and photo.size > 1*1024*1024: + raise forms.ValidationError(_('File size of photo is too large')) + + return photo + + +class RegistrationForm(BaseForm): + first_name = forms.CharField(label=_("First Name")) + last_name = forms.CharField(label=_("Last Name")) + email = forms.EmailField(label=_("Email Address")) + password = forms.CharField(widget=forms.PasswordInput, label=_("Password")) + + +class LoginForm(BaseForm): + username = forms.CharField( + widget=forms.TextInput(attrs={'placeholder': _('Username')}) + ) + password = forms.CharField( + widget=forms.PasswordInput(attrs={'placeholder': _('Password')}) + ) diff --git a/servo/forms/admin.py b/servo/forms/admin.py new file mode 100644 index 0000000..42a0b0d --- /dev/null +++ b/servo/forms/admin.py @@ -0,0 +1,557 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import re +import io + +from django import forms +from django import template +from django.utils.translation import ugettext as _ +from django.contrib.auth.models import Permission + +from servo.forms.base import BaseForm, BaseModelForm +from servo.forms.account import ProfileForm + +from servo.models.common import * +from servo.models.queue import * +from servo.models import User, Group, Checklist + + +class UserUploadForm(forms.Form): + datafile = forms.FileField() + location = forms.ModelChoiceField(queryset=Location.objects.all()) + queues = forms.ModelMultipleChoiceField( + queryset=Queue.objects.all() + ) + group = forms.ModelChoiceField(queryset=Group.objects.all()) + queues = forms.ModelMultipleChoiceField( + queryset=Queue.objects.all() + ) + + def save(self, **kwargs): + users = [] + string = u'' + cd = self.cleaned_data + data = cd['datafile'].read() + + for i in ('utf-8', 'latin-1',): + try: + string = data.decode(i) + except: + pass + + if not string: + raise ValueError(_('Unsupported file encoding')) + + sio = io.StringIO(string, newline=None) + + for l in sio.readlines(): + cols = l.strip().split("\t") + if len(cols) < 2: + continue # Skip empty rows + + user = User(username=cols[2]) + user.first_name = cols[0] + user.last_name = cols[1] + user.email = cols[3] + user.set_password(cols[4]) + user.save() + + user.location = cd['location'] + user.timezone = user.location.timezone + user.groups.add(cd['group']) + user.queues = cd['queues'] + user.save() + + users.append(user) + + return users + + +class GsxAccountForm(forms.ModelForm): + class Meta: + model = GsxAccount + exclude = [] + widgets = {'password': forms.PasswordInput} + + def clean(self): + cd = super(GsxAccountForm, self).clean() + # Don't save empty passwords + if cd['password'] == '': + del cd['password'] + + return cd + + +class GroupForm(forms.ModelForm): + class Meta: + exclude = [] + model = Group + + user_set = forms.ModelMultipleChoiceField( + required=False, + label=_('Group members'), + queryset=User.active.all(), + widget=forms.CheckboxSelectMultiple + ) + + permissions = forms.ModelMultipleChoiceField( + required=False, + label=_('Permissions'), + widget=forms.CheckboxSelectMultiple, + queryset=Permission.objects.filter(content_type__app_label='servo') + ) + + def __init__(self, *args, **kwargs): + super(GroupForm, self).__init__(*args, **kwargs) + if self.instance.pk: + user_ids = [u.pk for u in self.instance.user_set.all()] + self.fields['user_set'].initial = user_ids + + def save(self, *args, **kwargs): + group = super(GroupForm, self).save(*args, **kwargs) + group.user_set.clear() + for u in self.cleaned_data['user_set']: + group.user_set.add(u) + return group + + +class ChecklistForm(BaseModelForm): + class Meta: + model = Checklist + exclude = [] + widgets = {'queues': forms.CheckboxSelectMultiple} + + +class LocationForm(BaseModelForm): + class Meta: + model = Location + exclude = [] + widgets = {'gsx_accounts': forms.CheckboxSelectMultiple} + + def save(self, **kwargs): + from django.db.utils import IntegrityError + + try: + location = super(LocationForm, self).save(**kwargs) + except IntegrityError: + msg = _('A location with that name already exists') + self._errors['title'] = self.error_class([msg]) + raise forms.ValidationError(msg) + + return location + + +class QueueForm(BaseModelForm): + + gsx_soldto = forms.ChoiceField(required=False, choices=()) + users = forms.ModelMultipleChoiceField(queryset=User.active.all(), + widget=forms.CheckboxSelectMultiple, + required=False) + + class Meta: + model = Queue + exclude = ('statuses',) + widgets = { + 'description' : forms.Textarea(attrs={'rows': 4}), + 'keywords' : forms.Textarea(attrs={'rows': 4}), + 'locations' : forms.CheckboxSelectMultiple, + } + + def __init__(self, *args, **kwargs): + super(QueueForm, self).__init__(*args, **kwargs) + self.fields['gsx_soldto'].choices = GsxAccount.get_soldto_choices() + + if "instance" in kwargs: + queue = kwargs['instance'] + queryset = QueueStatus.objects.filter(queue=queue) + self.fields['status_created'].queryset = queryset + self.fields['status_assigned'].queryset = queryset + self.fields['status_products_ordered'].queryset = queryset + self.fields['status_products_received'].queryset = queryset + self.fields['status_repair_completed'].queryset = queryset + self.fields['status_dispatched'].queryset = queryset + self.fields['status_closed'].queryset = queryset + + +class StatusForm(BaseModelForm): + class Meta: + model = Status + exclude = [] + widgets = { + 'site': forms.HiddenInput, + 'limit_green': forms.TextInput(attrs={'class': 'input-mini'}), + 'limit_yellow': forms.TextInput(attrs={'class': 'input-mini'}), + } + +class UserForm(ProfileForm): + def clean_username(self): + reserved = ( + 'admin', + 'orders', + 'sales', + 'devices', + 'customers', + 'notes', + 'api', + 'checkin', + 'feedback', + ) + username = self.cleaned_data.get('username') + if username in reserved: + raise forms.ValidationError(_(u'"%s" cannot be used as a username') % username) + + return username + + class Meta: + model = User + fields = ( + "first_name", + "last_name", + "username", + "email", + "is_active", + "groups", + "is_staff", + "location", + "locations", + "locale", + "queues", + "region", + "timezone", + "tech_id", + "gsx_userid", + "customer", + "gsx_poprefix", + ) + widgets = { + 'locations': forms.CheckboxSelectMultiple, + 'queues': forms.CheckboxSelectMultiple + } + +class TemplateForm(BaseModelForm): + class Meta: + model = Template + exclude = [] + widgets = { + 'title': forms.TextInput(attrs={'class': 'span12'}), + 'content': forms.Textarea(attrs={'class': 'span12'}) + } + + def clean_content(self): + content = self.cleaned_data.get('content') + try: + template.Template(content) + except template.TemplateSyntaxError, e: + raise forms.ValidationError(_('Syntax error in template: %s') % e) + + return content + + +class SettingsForm(BaseForm): + # Servo's general System Settings form + company_name = forms.CharField(label=_('Company Name')) + company_logo = forms.ImageField( + label=_('Company Logo'), + required=False, + help_text=_('Company-wide logo to use in print templates') + ) + + terms_of_service = forms.CharField( + required=False, + label=_('Terms of Service'), + widget=forms.Textarea(attrs={'class': 'span10'}), + help_text=_('These terms will be added to your work confirmations and public check-in site.') + ) + + autocomplete_repairs = forms.BooleanField( + initial=True, + required=False, + label=_("Autocomplete GSX repairs"), + help_text=_("Complete the GSX repair when closing a Service Order") + ) + + # start checkin fields + checkin_user = forms.ModelChoiceField( + required=False, + label=_('User Account'), + queryset=User.active.all(), + help_text=_('User account to use for the public check-in service'), + ) + checkin_group = forms.ModelChoiceField( + required=False, + label=_('Group'), + queryset=Group.objects.all(), + help_text=_('Users to choose from in the check-in interface'), + ) + checkin_checklist = forms.ModelChoiceField( + required=False, + label=_('Checklist'), + queryset=Checklist.objects.filter(enabled=True), + help_text=_('Checklist to show during check-in'), + ) + checkin_queue = forms.ModelChoiceField( + required=False, + label=_('Queue'), + queryset=Queue.objects.all(), + help_text=_('Orders created through the check-in interface will go into this queue'), + ) + checkin_timeline = forms.BooleanField( + initial=False, + required=False, + label=_('Show timeline'), + help_text=_('Show status timeline on public repair status page'), + ) + checkin_password = forms.BooleanField( + initial=False, + required=False, + label=_('Show password'), + help_text=_('Make checkin device password field readable'), + ) + checkin_report_checklist = forms.BooleanField( + initial=True, + required=False, + label=_('Show checklist results'), + help_text=_('Show checklist results in order confirmation'), + ) + + checkin_require_password = forms.BooleanField( + initial=True, + required=False, + label=_('Require device password'), + ) + checkin_require_condition = forms.BooleanField( + initial=True, + required=False, + label=_('Require device condition'), + ) + + # end checkin fields + + currency = forms.ChoiceField( + label=_('Currency'), + choices=( + ('DKK', 'DKK'), + ('EUR', 'EUR'), + ('GBP', 'GBP'), + ('SEK', 'SEK'), + ('USD', 'USD'), + ), + initial='EUR' + ) + + gsx_account = forms.ModelChoiceField( + required=False, + label=_('Default GSX account'), + queryset=GsxAccount.objects.all(), + help_text=_('Use this GSX account before and order is assigned to a queue') + ) + + pct_margin = forms.CharField( + required=False, + max_length=128, + label=_('Margin %'), + help_text=_('Default margin for new products') + ) + + pct_vat = forms.DecimalField( + max_digits=4, + required=False, + label=_('VAT %'), + help_text=_('Default VAT for new products') + ) + + shipping_cost = forms.DecimalField( + max_digits=4, + required=False, + label=_('Shipping Cost'), + help_text=_('Default shipping cost for new products') + ) + + track_inventory = forms.BooleanField( + initial=True, + required=False, + label=_('Track inventory'), + help_text=_('Unchecking this will disable tracking product amounts in your inventory') + ) + + imap_host = forms.CharField( + label=_('IMAP server'), + max_length=128, + required=False + ) + imap_user = forms.CharField( + label=_('Username'), + max_length=128, + required=False + ) + imap_password = forms.CharField( + max_length=128, + label=_('Password'), + widget=forms.PasswordInput(), + required=False + ) + imap_ssl = forms.BooleanField(label=_('Use SSL'), initial=True, required=False) + imap_act = forms.ModelChoiceField( + required=False, + label=_('User Account'), + queryset=User.active.all(), + help_text=_('User account to use when creating notes from messages'), + ) + + default_sender = forms.ChoiceField( + required=False, + label=_('Default Sender'), + choices=( + ('user', _("User")), + ('location', _("Location")), + ('custom', _("Custom...")) + ), + help_text=_('Select the default sender address for outgoing emails') + ) + default_sender_custom = forms.EmailField( + label=' ', + required=False, + widget=forms.TextInput(attrs={ + 'placeholder': 'user@example.com', 'disabled': 'disabled' + }) + ) + default_subject = forms.CharField( + max_length=128, + required=False, + label=_('Default subject') + ) + smtp_host = forms.CharField( + max_length=128, + required=False, + label=_('SMTP server') + ) + smtp_user = forms.CharField(max_length=128, required=False, label=_('Username')) + smtp_password = forms.CharField( + max_length=128, + required=False, + label=_('Password'), + widget=forms.PasswordInput() + ) + smtp_ssl = forms.BooleanField(initial=True, required=False, label=_('Use SSL')) + + sms_gateway = forms.ChoiceField( + label=_('SMS Gateway'), + choices=( + ('builtin', _('Built-in')), + ('hqsms', 'HQSMS'), + ('http', 'HTTP'), + ('smtp', 'SMTP'), + ('jazz', 'SMSjazz'), + ), + initial='http', + required=False) + sms_smtp_address = forms.EmailField(required=False, label=_('Email address')) + sms_http_url = forms.CharField( + max_length=128, + label=_('URL'), + required=False, + help_text=_('SMS Server URL'), + initial='http://example.com:13013/cgi-bin/sendsms' + ) + sms_http_user = forms.CharField(max_length=128, label=_('Username'), required=False) + sms_http_password = forms.CharField( + max_length=128, + required=False, + label=_('Password'), + widget=forms.PasswordInput() + ) + sms_http_sender = forms.CharField( + max_length=128, + required=False, + label=_('Sender') + ) + sms_http_ssl = forms.BooleanField( + required=False, + label=_('Use SSL'), + initial=True + ) + notify_location = forms.BooleanField( + required=False, + initial=True, + label=_('Notify locations'), + help_text=_("Daily reports will be sent to the location's email address") + ) + notify_address = forms.EmailField( + required=False, + label=_('Email address'), + help_text=_("Send daily reports to this email address") + ) + + def clean_notify_address(self, *args, **kwargs): + """ + Only validate notify_address if it was actually given + """ + from django.core.validators import validate_email + address = self.cleaned_data.get('notify_address') + + if len(address): + validate_email(address) + + return address + + def clean_pct_margin(self, *args, **kwargs): + margin = self.cleaned_data.get('pct_margin') + if re.match('^\d[\-=;\d]*\d$', margin): + return margin + + raise forms.ValidationError(_('Invalid margin %')) + + def save(self, *args, **kwargs): + config = dict() + + if self.cleaned_data.get('company_logo'): + f = self.cleaned_data['company_logo'] + target = 'uploads/logos/%s' % f.name + with open(target, 'wb+') as destination: + for chunk in f.chunks(): + destination.write(chunk) + + self.cleaned_data['company_logo'] = 'logos/%s' % f.name + else: + # @fixme: make the form remember the previous value + self.cleaned_data['company_logo'] = Configuration.get_company_logo() + + for k, v in self.cleaned_data.items(): + field = Configuration.objects.get_or_create(key=k)[0] + + if re.search('password$', k) and v == '': + v = field.value # don't save empty passwords + if hasattr(v, 'pk'): + v = v.pk # so we don't end up with object instances in the cache + + field.value = v or '' + field.save() + config[k] = v + + cache.set('config', config) + + return config diff --git a/servo/forms/base.py b/servo/forms/base.py new file mode 100644 index 0000000..83e2cd4 --- /dev/null +++ b/servo/forms/base.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import json +from django import forms +from django.forms.utils import flatatt +from django.utils.html import format_html +from django.utils.safestring import mark_safe + + +class NullCharField(forms.CharField): + def clean(self, value): + cleaned = super(NullCharField, self).clean(value) + return cleaned if len(cleaned) else None + + +class FullTextArea(forms.CharField): + widget = forms.Textarea(attrs={'class': 'span12'}) + + +class SearchField(forms.CharField): + widget = forms.TextInput(attrs={ + 'class': 'search-query', + 'autocomplete': 'off', + 'placeholder': '', + }) + + +class ChoiceField(forms.ChoiceField): + def __init__(self, *args, **kwargs): + super(ChoiceField, self).__init__(*args, **kwargs) + self.widget.attrs['class'] = 'span12' + + +class TextInput(forms.TextInput): + def __init__(self, *args, **kwargs): + super(TextInput, self).__init__(*args, **kwargs) + self.attrs['class'] = 'span12' + + +class AutocompleteCharField(forms.CharField): + widget = forms.TextInput(attrs={ + 'class' : "input typeahead", + 'data-provide' : "typeahead" + }) + + def __init__(self, values, *args, **kwargs): + super(AutocompleteCharField, self).__init__(*args, **kwargs) + + if not type(values) is str: + values = json.dumps(list(values)) + + self.widget.attrs['data-source'] = values + + +class AutocompleteTextarea(forms.Textarea): + def __init__(self, rows=8, choices=None): + super(AutocompleteTextarea, self).__init__() + self.attrs = { + 'rows': rows, + 'class': "span12 autocomplete", + 'data-source': json.dumps(choices) + } + + +class BaseForm(forms.Form): + required_css_class = "required" + + +class BaseModelForm(forms.ModelForm): + required_css_class = "required" + + +class SearchFieldInput(forms.TextInput): + + def render(self, name, value, attrs=None): + + field = super(SearchFieldInput, self).render(name, value, attrs) + final_attrs = self.build_attrs(attrs, name=name) + + output = format_html(u''' +
+ {1} + + + +
+ ''', flatatt(final_attrs), field) + + return mark_safe(output) + + +class DatepickerInput(forms.DateInput): + def __init__(self, *args, **kwargs): + kwargs['format'] = "%Y-%m-%d" + super(DatepickerInput, self).__init__(*args, **kwargs) + + def render(self, name, value, attrs=None): + + date_format = "yyyy-MM-dd" + + if "format" not in self.attrs: + attrs['format'] = date_format + + if "data-format" not in self.attrs: + attrs['data-format'] = date_format + + field = super(DatepickerInput, self).render(name, value, attrs) + final_attrs = self.build_attrs(attrs, name=name) + + output = format_html(u''' +
+ {1} + + + +
+ ''', flatatt(final_attrs), field) + + return mark_safe(output) + + +class DateTimePickerInput(forms.DateTimeInput): + def __init__(self, *args, **kwargs): + kwargs['format'] = "%Y-%m-%d %H:%M" + super(DateTimePickerInput, self).__init__(*args, **kwargs) + + def render(self, name, value, attrs=None): + + date_format = "yyyy-MM-dd hh:mm" + + if "data-format" not in self.attrs: + attrs['data-format'] = date_format + if "class" not in self.attrs: + attrs['class'] = 'input-medium' + + field = super(DateTimePickerInput, self).render(name, value, attrs) + final_attrs = self.build_attrs(attrs, name=name) + + output = format_html(u''' +
+ {1} + + + +
+ ''', flatatt(final_attrs), field) + + return mark_safe(output) diff --git a/servo/forms/checkin.py b/servo/forms/checkin.py new file mode 100644 index 0000000..732b871 --- /dev/null +++ b/servo/forms/checkin.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import gsxws +import phonenumbers + +from django import forms +from datetime import date +from django.conf import settings +from django_countries import countries +from django.core.validators import RegexValidator +from django.utils.translation import ugettext as _ +from django.forms.extras.widgets import SelectDateWidget + +from servo.validators import apple_sn_validator, phone_validator, file_upload_validator + +from servo.forms.base import SearchFieldInput +from servo.models import (Configuration, + Device, Attachment, Location, Customer,) + + +# Generate list of years for purchase date picker +y = date.today().year +YEARS = [x+1 for x in xrange(y-7, y)] + + +def get_checkin_locations(user): + from servo.models import User + if user.is_authenticated(): + return user.locations.all() + else: + user_id = Configuration.conf('checkin_user') + return User.objects.get(pk=user_id).locations.all() + + +class ConfirmationForm(forms.Form): + confirm = forms.BooleanField(required=False) + + +class DeviceForm(forms.ModelForm): + + required_css_class = 'required' + + purchase_country = forms.ChoiceField( + label=_('Country'), + choices=countries, + initial=settings.INSTALL_COUNTRY + ) + accessories = forms.CharField( + required=False, + label=_('Accessories'), + widget=forms.Textarea(attrs={'class': 'span12', 'rows': 3}), + help_text=_("Please list here any accessories you'd like to check in with your device (cables, power adapters, bags, etc)") + ) + + pop = forms.FileField( + required=False, + label=_('Proof of Purchase'), + validators=[file_upload_validator], + help_text=_('Proof of Purchase is required when setting purchase date manually') + ) + + condition = forms.CharField( + label=_('Condition of device'), + required=False, + widget=forms.Textarea(attrs={'class': 'span12', 'rows': 3}), + help_text=_("Please describe the condition of the device") + ) + + class Meta: + model = Device + fields = ( + 'description', + 'sn', + 'imei', + 'purchased_on', + 'purchase_country', + 'username', + 'password', + ) + widgets = { + 'sn' : SearchFieldInput(), + 'password' : forms.PasswordInput(), + 'username' : forms.TextInput(), + 'purchased_on' : SelectDateWidget(years=YEARS), + 'warranty_status' : forms.Select(attrs={'readonly': 'readonly'}), + } + + def __init__(self, *args, **kwargs): + + super(DeviceForm, self).__init__(*args, **kwargs) + + if Configuration.false('checkin_require_password'): + self.fields['password'].required = False + + if Configuration.true('checkin_require_condition'): + self.fields['condition'].required = True + + if kwargs.get('instance'): + prod = gsxws.Product('') + prod.description = self.instance.description + + if prod.is_ios: + self.fields['password'].label = _('Passcode') + + if not prod.is_ios: + del(self.fields['imei']) + if not prod.is_mac: + del(self.fields['username']) + + if Configuration.true('checkin_password'): + self.fields['password'].widget = forms.TextInput(attrs={'class': 'span12'}) + + +class CustomerForm(forms.Form): + + from django.utils.safestring import mark_safe + + required_css_class = 'required' + + fname = forms.CharField( + label=_('First name'), + #initial='Filipp' + ) + lname = forms.CharField( + label=_('Last name'), + #initial='Lepalaan' + ) + + company = forms.CharField( + required=False, + label=_('Company (optional)') + ) + email = forms.EmailField( + label=_('Email address'), + widget=forms.TextInput(attrs={'class': 'span12'}), + #initial='filipp@fps.ee' + ) + phone = forms.CharField( + label=_('Phone number'), + validators=[phone_validator], + #initial='12345678790' + ) + address = forms.CharField( + label=_('Address'), + #initial='Example street' + ) + country = forms.ChoiceField(label=_('Country'), + choices=Customer.COUNTRY_CHOICES, + initial=settings.INSTALL_COUNTRY) + city = forms.CharField( + label=_('City'), + #initial='Helsinki' + ) + postal_code = forms.CharField( + label=_('Postal Code'), + #initial='000100' + ) + checkin_location = forms.ModelChoiceField( + empty_label=None, + label=_(u'Check-in location'), + queryset=Location.objects.all(), + widget=forms.Select(attrs={'class': 'span12'}), + help_text=_('Choose where you want to leave the device') + ) + checkout_location = forms.ModelChoiceField( + empty_label=None, + label=_(u'Check-out location'), + queryset=Location.objects.all(), + widget=forms.Select(attrs={'class': 'span12'}), + help_text=_('Choose where you want to pick up the device') + ) + TERMS = _('I agree to the terms of service.') + agree_to_terms = forms.BooleanField(initial=False, label=mark_safe(TERMS)) + + notify_by_sms = forms.BooleanField( + initial=True, + required=False, + label=_('Notify by SMS') + ) + notify_by_email = forms.BooleanField( + initial=True, + required=False, + label=_('Notify by Email') + ) + + def clean_fname(self): + v = self.cleaned_data.get('fname') + return v.capitalize() + + def clean_lname(self): + lname = self.cleaned_data.get('lname') + return lname.capitalize() + + def __init__(self, request, *args, **kwargs): + + super(CustomerForm, self).__init__(*args, **kwargs) + user = request.user + + location = request.session['checkin_location'] + locations = get_checkin_locations(user) + + self.fields['checkin_location'].queryset = locations + self.fields['checkin_location'].initial = location + self.fields['checkout_location'].queryset = locations + self.fields['checkout_location'].initial = location + + if request.user.is_authenticated(): + del(self.fields['agree_to_terms']) + self.fields['phone'].widget = SearchFieldInput() + + +class AppleSerialNumberForm(forms.Form): + sn = forms.CharField( + min_length=8, + #initial='C34JTVKYDTWF', + validators=[apple_sn_validator], + label=_(u'Serial number or IMEI') + ) + + def clean_sn(self): + sn = self.cleaned_data.get('sn') + return sn.upper() + + +class SerialNumberForm(forms.Form): + sn = forms.CharField( + min_length=8, + initial='C34JTVKYDTWF', + label=_(u'Serial number') + ) + + def clean_sn(self): + sn = self.cleaned_data.get('sn') + return sn.upper() + + +class StatusCheckForm(forms.Form): + code = forms.CharField( + min_length=8, + label=_('Service Order'), + validators=[RegexValidator(regex=r'\d{8}', message=_('Invalid Service Order number'))] + ) + + +class IssueForm(forms.Form): + + required_css_class = 'required' + + issue_description = forms.CharField( + min_length=10, + #initial='Does not work very well', + label=_(u'Problem description'), + widget=forms.Textarea(attrs={'class': 'span12'}) + ) + attachment = forms.FileField( + required=False, + label=_(u'Attachment'), + validators=[file_upload_validator], + help_text=_(u'Please use this to attach relevant documents') + ) + + notes = forms.CharField( + required=False, + label=_(u'Notes'), + widget=forms.Textarea(attrs={'class': 'span12'}), + help_text=_(u'Will not appear on the print-out') + ) + + +class QuestionForm(forms.Form): + question = forms.CharField(widget=forms.HiddenInput) + answer = forms.CharField(widget=forms.HiddenInput) + + +class AttachmentForm(forms.ModelForm): + class Meta: + model = Attachment + exclude = [] + diff --git a/servo/forms/customer.py b/servo/forms/customer.py new file mode 100644 index 0000000..45752c9 --- /dev/null +++ b/servo/forms/customer.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import phonenumbers +from django import forms +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from servo.forms.base import BaseModelForm, DatepickerInput +from servo.models import Customer + + +class CustomerForm(BaseModelForm): + class Meta: + model = Customer + widgets = { + 'groups': forms.CheckboxSelectMultiple + } + exclude = [] + + def clean_name(self): + name = self.cleaned_data.get('name') + return name.strip() + + def clean(self): + cd = super(CustomerForm, self).clean() + + phone = cd.get('phone') + country = cd.get('country') + + if len(phone) < 1: + return cd + + try: + phonenumbers.parse(phone, country) + except phonenumbers.NumberParseException: + msg = _('Enter a valid phone number') + self._errors["phone"] = self.error_class([msg]) + + return cd + + +class CustomerSearchForm(forms.Form): + name__icontains = forms.CharField( + required=False, + label=_('Name contains') + ) + email__icontains = forms.CharField( + required=False, + label=_('Email contains') + ) + street_address__icontains = forms.CharField( + required=False, + label=_('Address contains') + ) + checked_in_start = forms.DateField( + required=False, + label=_('Checked in between'), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) + checked_in_end = forms.DateField( + required=False, + label=mark_safe(' '), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) + + def clean(self): + cd = super(CustomerSearchForm, self).clean() + + for k, v in cd.items(): + if v not in ['', None]: + return cd + + raise forms.ValidationError(_('Please specify at least one parameter')) + + +class CustomerUploadForm(forms.Form): + datafile = forms.FileField(label=_('CSV file')) + skip_dups = forms.BooleanField( + initial=False, + required=False, + label=_('Skip duplicates'), + help_text=_('Skip customers with existing email addresses') + ) + + def clean_datafile(self): + d = self.cleaned_data.get('datafile') + if not d.content_type.startswith('text'): + raise forms.ValidationError(_('Data file should be in text format')) + return d diff --git a/servo/forms/devices.py b/servo/forms/devices.py new file mode 100644 index 0000000..3028b52 --- /dev/null +++ b/servo/forms/devices.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from django import forms +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from servo.models import Tag, Device, Customer +from servo.forms import DatepickerInput, AutocompleteCharField + +product_lines = [(k, x['name']) for k, x in Device.PRODUCT_LINES.items()] + + +class DeviceSearchForm(forms.Form): + product_line = forms.MultipleChoiceField( + #widget=forms.CheckboxSelectMultiple, + choices=product_lines, + required=False + ) + warranty_status = forms.MultipleChoiceField( + #widget=forms.CheckboxSelectMultiple, + choices=Device.WARRANTY_CHOICES, + required=False, + ) + date_start = forms.DateField( + required=False, + label=_('Created between'), + widget=DatepickerInput(attrs={'class': 'input-small'}) + ) + date_end = forms.DateField( + required=False, + label=mark_safe(' '), + widget=DatepickerInput(attrs={'class': 'input-small'}) + ) + sn = forms.CharField(required=False, label=_('Serial number contains')) + + def __init__(self, *args, **kwargs): + super(DeviceSearchForm, self).__init__(*args, **kwargs) + self.fields['description'] = AutocompleteCharField( + '/api/device_models/', + max_length=128, + required=False, + label=_('Description contains') + ) + + +class DeviceForm(forms.ModelForm): + class Meta: + model = Device + exclude = ('spec', 'customers', 'files', 'image_url', + 'exploded_view_url', 'manual_url',) + widgets = {'purchased_on': DatepickerInput()} + + tags = forms.ModelMultipleChoiceField( + queryset=Tag.objects.filter(type='device'), + required=False + ) + + +class DeviceUploadForm(forms.Form): + datafile = forms.FileField() + customer = forms.ModelChoiceField( + queryset=Customer.objects.all(), + required=False + ) + do_warranty_check = forms.BooleanField(required=False, initial=True) + + +class DiagnosticsForm(forms.Form): + pass + diff --git a/servo/forms/invoices.py b/servo/forms/invoices.py new file mode 100644 index 0000000..eb96f7e --- /dev/null +++ b/servo/forms/invoices.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from django import forms +from django.utils.translation import ugettext as _ + +from servo.forms import DatepickerInput, NullCharField +from servo.models import Invoice, Payment, Status + + +class PaymentForm(forms.ModelForm): + class Meta: + model = Payment + exclude = [] + widgets = { + 'created_by': forms.HiddenInput, + 'method': forms.Select(attrs={'class': 'input-medium'}), + 'amount': forms.NumberInput(attrs={'class': 'input-small'}) + } + + +class InvoiceForm(forms.ModelForm): + class Meta: + model = Invoice + exclude = [] + widgets = { + 'total_net' : forms.TextInput(attrs={'class': 'input-small'}), + 'total_tax' : forms.TextInput(attrs={'class': 'input-small'}), + 'total_gross' : forms.TextInput(attrs={'class': 'input-small'}), + 'customer_name' : forms.TextInput(attrs={'class': 'span12'}), + 'customer_email' : forms.TextInput(attrs={'class': 'span12'}), + 'customer_phone' : forms.TextInput(attrs={'class': 'span12'}), + 'customer_address' : forms.TextInput(attrs={'class': 'span12'}), + 'reference' : forms.TextInput(attrs={'class': 'span12'}), + } + localized_fields = ('total_net', 'total_tax', 'total_gross') + + +class InvoiceSearchForm(forms.Form): + state = forms.ChoiceField( + required=False, + label=_('State is'), + choices=( + ('', _('Any')), + ('OPEN', _('Open')), + ('PAID', _('Paid')), + ), + widget=forms.Select(attrs={'class': 'input-small'}) + ) + payment_method = forms.ChoiceField( + required=False, + label=_('Payment method is'), + choices=(('', _('Any')),) + Payment.METHODS, + widget=forms.Select(attrs={'class': 'input-medium'}) + ) + status_isnot = forms.ModelChoiceField( + required=False, + label=_('Status is not'), + queryset=Status.objects.all(), + widget=forms.Select(attrs={'class': 'input-medium'}) + ) + start_date = forms.DateField( + required=False, + label=_('Start date'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('Start date') + }) + ) + end_date = forms.DateField( + required=False, + label=_('End date'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('End date') + }) + ) + customer_name = NullCharField( + required=False, + label=_('Customer name contains') + ) + service_order = NullCharField( + required=False, + label=_('Service Order is') + ) diff --git a/servo/forms/notes.py b/servo/forms/notes.py new file mode 100644 index 0000000..d74a0de --- /dev/null +++ b/servo/forms/notes.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import json +from django import forms +from gsxws import escalations +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from servo.models import Note, Escalation, Template +from servo.forms import BaseModelForm, AutocompleteTextarea, TextInput + + +class NoteForm(BaseModelForm): + class Meta: + model = Note + exclude = [] + widgets = { + 'recipient' : TextInput, + 'labels' : forms.CheckboxSelectMultiple, + 'order' : forms.HiddenInput, + 'parent' : forms.HiddenInput, + 'customer' : forms.HiddenInput, + 'subject' : TextInput + } + + def __init__(self, *args, **kwargs): + super(NoteForm, self).__init__(*args, **kwargs) + note = kwargs['instance'] + self.fields['sender'] = forms.ChoiceField( + label=_('From'), + choices=note.get_sender_choices(), + widget=forms.Select(attrs={'class': 'span12'}) + ) + self.fields['body'].widget = AutocompleteTextarea( + rows=20, + choices=Template.templates() + ) + + +class NoteSearchForm(forms.Form): + body = forms.CharField(required=False, label=_('Body contains')) + recipient = forms.CharField(required=False, label=_('Recipient contains')) + sender = forms.CharField(required=False, label=_('Sender contains')) + order_code = forms.CharField(required=False, label=_('Service Order is')) + + +class EscalationForm(BaseModelForm): + keys = forms.CharField(required=False) + values = forms.CharField(required=False) + + def clean(self): + contexts = dict() + cd = super(EscalationForm, self).clean() + keys = self.data.getlist('keys') + values = self.data.getlist('values') + for k, v in enumerate(values): + if v != '': + key = keys[k] + contexts[key] = v + + cd['contexts'] = json.dumps(contexts) + return cd + + class Meta: + model = Escalation + fields = ('issue_type', 'status', 'gsx_account', 'contexts',) + diff --git a/servo/forms/orders.py b/servo/forms/orders.py new file mode 100644 index 0000000..82c49da --- /dev/null +++ b/servo/forms/orders.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import gsxws + +from django import forms +from django.utils.translation import ugettext as _ + +from django.utils.safestring import mark_safe + +from servo.models.parts import symptom_codes +from servo.models import Location, Queue, Status, Tag +from servo.models import User, Invoice, Payment + +from servo.models.order import * +from servo.forms.base import * + + +class BatchProcessForm(forms.Form): + orders = forms.CharField( + widget=forms.Textarea, + label=_("Service order(s)") + ) + + status = forms.ModelChoiceField( + required=False, + label=_('Set status to'), + queryset=Status.objects.all() + ) + queue = forms.ModelChoiceField( + required=False, + label=_('Set queue to'), + queryset=Queue.objects.all() + ) + sms = forms.CharField( + required=False, + widget=forms.Textarea, + label=_('Send SMS to customer') + ) + email = forms.CharField( + required=False, + widget=forms.Textarea, + label=_('Send E-mail to customer') + ) + note = forms.CharField( + required=False, + widget=forms.Textarea, + label=_('Add note to order') + ) + + +class FieldsForm(forms.Form): + pass + + +class OrderItemForm(forms.ModelForm): + class Meta: + model = ServiceOrderItem + fields = ('title', 'amount', 'price_category', + 'price', 'sn', 'kbb_sn', 'imei', 'should_report', + 'comptia_code', 'comptia_modifier',) + widgets = { + 'amount': forms.TextInput(attrs={'class': 'input-mini'}), + 'price': forms.TextInput(attrs={'class': 'input-mini'}) + } + + def __init__(self, *args, **kwargs): + super(OrderItemForm, self).__init__(*args, **kwargs) + + if self.instance: + product = self.instance.product + if product.can_order_from_gsx(): + CODES = symptom_codes(product.component_code) + self.fields['comptia_code'] = forms.ChoiceField(choices=CODES) + self.fields['comptia_modifier'] = forms.ChoiceField( + choices=gsxws.MODIFIERS, + initial="B" + ) + + +class OrderSearchForm(forms.Form): + """ + Form for searching Service Orders + """ + checkin_location = forms.ModelMultipleChoiceField( + required=False, + label=_("Checked in at"), + queryset=Location.objects.all(), + ) + location = forms.ModelMultipleChoiceField( + required=False, + label=_("Location is"), + queryset=Location.objects.all(), + ) + state = forms.MultipleChoiceField( + required=False, + label=_("State is"), + choices=Order.STATES, + ) + queue = forms.ModelMultipleChoiceField( + required=False, + label=_("Queue is"), + queryset=Queue.objects.all(), + ) + status = forms.ModelMultipleChoiceField( + required=False, + label=_("Status"), + queryset=Status.objects.all(), + ) + created_by = forms.ModelMultipleChoiceField( + required=False, + label=_("Created by"), + queryset=User.active.all(), + ) + assigned_to = forms.ModelMultipleChoiceField( + required=False, + label=_("Assigned to"), + queryset=User.active.all(), + ) + label = forms.ModelMultipleChoiceField( + required=False, + label=_("Label"), + queryset=Tag.objects.filter(type="order"), + ) + color = forms.MultipleChoiceField( + choices=( + ('green', _("Green")), + ('yellow', _("Yellow")), + ('red', _("Red")), + ('grey', _("Grey")), + ), + label=_("Color"), + required=False, + ) + start_date = forms.DateField( + required=False, + label=_("Created between"), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) + end_date = forms.DateField( + required=False, + label=mark_safe(' '), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) diff --git a/servo/forms/product.py b/servo/forms/product.py new file mode 100644 index 0000000..227a9b3 --- /dev/null +++ b/servo/forms/product.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import re +from django import forms +from django.utils.translation import ugettext as _ +from django.core.exceptions import ValidationError + +from servo.models import Location, User, TaggedItem +from servo.models.purchases import PurchaseOrderItem +from servo.models.product import Product, ProductCategory +from servo.forms.base import BaseModelForm, DatepickerInput, TextInput + + +class ProductSearchForm(forms.Form): + title = forms.CharField( + required=False, + label=_('Name contains') + ) + code = forms.CharField( + required=False, + label=_('Code contains') + ) + description = forms.CharField( + required=False, + label=_('Description contains') + ) + tag = forms.ModelChoiceField( + required=False, + label=_('Device model is'), + queryset=TaggedItem.objects.none() + ) + + def __init__(self, *args, **kwargs): + super(ProductSearchForm, self).__init__(*args, **kwargs) + tags = TaggedItem.objects.filter(content_type__model="product").distinct("tag") + self.fields['tag'].queryset = tags + + +class ProductUploadForm(forms.Form): + datafile = forms.FileField(label=_("Product datafile")) + category = forms.ModelChoiceField( + required=False, + queryset=ProductCategory.objects.all() + ) + + +class PartsImportForm(forms.Form): + partsdb = forms.FileField(label=_("Parts database file")) + import_vintage = forms.BooleanField( + initial=True, + required=False, + label=_("Import vintage parts") + ) + update_prices = forms.BooleanField( + initial=True, + required=False, + label=_("Update product prices") + ) + + +class PurchaseOrderItemEditForm(forms.ModelForm): + class Meta: + model = PurchaseOrderItem + exclude = ('sn',) + widgets = { + 'product': forms.HiddenInput(), + 'code': forms.TextInput(attrs={'class': 'input-small'}), + 'amount': forms.TextInput(attrs={'class': 'input-mini'}), + 'price': forms.TextInput(attrs={'class': 'input-mini'}), + 'title': forms.TextInput(attrs={'class': 'input-xlarge'}), + } + localized_fields = ('price',) + + +class PurchaseOrderItemForm(forms.ModelForm): + class Meta: + model = PurchaseOrderItem + fields = ('sn', 'amount',) + localized_fields = ('price',) + + def clean(self): + cleaned_data = super(PurchaseOrderItemForm, self).clean() + return cleaned_data + + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + exclude = ('files',) + widgets = { + 'code': TextInput(), + 'title': TextInput(attrs={'class': 'input-xlarge'}), + 'categories': forms.CheckboxSelectMultiple(), + 'description': forms.Textarea(attrs={'class': 'span12', 'rows': 6}), + } + localized_fields = ( + 'price_purchase_exchange', + 'pct_margin_exchange', + 'price_notax_exchange', + 'price_sales_exchange', + 'price_purchase_stock', + 'pct_margin_stock', + 'price_notax_stock', + 'price_sales_stock', + 'pct_vat', + 'shipping', + ) + + def clean_code(self): + code = self.cleaned_data.get('code') + if not re.match(r'^[\w\-/]+$', code): + raise ValidationError(_('Product code %s contains invalid characters') % code) + + return code + + +class CategoryForm(BaseModelForm): + class Meta: + model = ProductCategory + exclude = [] + + +class PurchaseOrderSearchForm(forms.Form): + state = forms.ChoiceField( + required=False, + label=_('State is'), + choices=( + ('', _('Any')), + ('open', _('Open')), + ('submitted', _('Submitted')), + ('received', _('Received')), + ), + widget=forms.Select(attrs={'class': 'input-small'}) + ) + created_by = forms.ModelChoiceField( + required=False, + queryset=User.objects.filter(is_active=True) + ) + start_date = forms.DateField( + required=False, + label=_('Start date'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('Start date') + }) + ) + end_date = forms.DateField( + required=False, + label=_('End date'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('End date') + }) + ) + reference = forms.CharField( + required=False, + label=_('Reference contains') + ) + + +class IncomingSearchForm(forms.Form): + """ + A form for searching incoming products + """ + location = forms.ModelChoiceField( + label=_('Location is'), + queryset=Location.objects.all(), + widget=forms.Select(attrs={'class': 'input-medium'}) + ) + ordered_start_date = forms.DateField( + label=_('Ordered between'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('Start date') + }) + ) + ordered_end_date = forms.DateField( + label='', + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('End date') + }) + ) + received_start_date = forms.DateField( + label=_('Received between'), + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('Start date') + }) + ) + received_end_date = forms.DateField( + label='', + widget=DatepickerInput(attrs={ + 'class': "input-small", + 'placeholder': _('End date') + }) + ) + confirmation = forms.CharField( + label=_('Confirmation is') + ) + service_order = forms.CharField( + label=_('Service order is') + ) + diff --git a/servo/forms/repairs.py b/servo/forms/repairs.py new file mode 100644 index 0000000..8459e52 --- /dev/null +++ b/servo/forms/repairs.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import json + +from django import forms +from django_countries import countries + +from django.utils.translation import ugettext as _ + +from servo.models import User, Repair, Template +from servo.forms import BaseForm, AutocompleteTextarea, DateTimePickerInput, ChoiceField + + +class GsxCustomerForm(BaseForm): + firstName = forms.CharField(max_length=100, label=_('First name')) + lastName = forms.CharField(max_length=100, label=_('Last name')) + emailAddress = forms.CharField(max_length=100, label=_('Email')) + primaryPhone = forms.CharField(max_length=100, label=_('Phone')) + addressLine1 = forms.CharField(max_length=100, label=_('Address')) + zipCode = forms.CharField(max_length=100, label=_('ZIP Code')) + city = forms.CharField(max_length=100, label=_('City')) + country = ChoiceField(label=_('Country'), choices=countries) + state = ChoiceField(choices=(('ZZ', _('Other')),), initial="ZZ") + + +class GsxComponentForm(forms.Form): + def __init__(self, *args, **kwargs): + components = kwargs.get('components') + del kwargs['components'] + super(GsxComponentForm, self).__init__(*args, **kwargs) + if len(components): + components = json.loads(components) + for k, v in components.items(): + self.fields[k] = forms.CharField(label=k, required=True, initial=v) + + def clean(self, *args, **kwargs): + super(GsxComponentForm, self).clean(*args, **kwargs) + self.json_data = json.dumps(self.cleaned_data) + + +class GsxRepairForm(forms.ModelForm): + class Meta: + model = Repair + exclude = [] + widgets = { + 'device' : forms.HiddenInput(), + 'parts': forms.CheckboxSelectMultiple(), + 'unit_received_at': DateTimePickerInput(attrs={'readonly': 'readonly'}) + } + + def __init__(self, *args, **kwargs): + super(GsxRepairForm, self).__init__(*args, **kwargs) + repair = kwargs['instance'] + techs = User.techies.filter(location=repair.order.location) + c = [(u.tech_id, u.get_full_name()) for u in techs] + c.insert(0, ('', '-------------------',)) + self.fields['tech_id'] = forms.ChoiceField(choices=c, + required=False, + label=_('Technician')) + self.fields['parts'].initial = repair.order.get_parts() + + if not repair.can_mark_complete: + del self.fields['mark_complete'] + del self.fields['replacement_sn'] + + choices = Template.templates() + for f in ('notes', 'symptom', 'diagnosis'): + self.fields[f].widget = AutocompleteTextarea(choices=choices) + + def clean(self, *args, **kwargs): + cd = super(GsxRepairForm, self).clean(*args, **kwargs) + if self.instance.has_serialized_parts(): + if cd.get('mark_complete') and not cd.get('replacement_sn'): + raise forms.ValidationError(_('Replacement serial number must be set')) + return cd diff --git a/servo/forms/returns.py b/servo/forms/returns.py new file mode 100644 index 0000000..f2ef014 --- /dev/null +++ b/servo/forms/returns.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import gsxws +from django import forms +from django.utils.translation import ugettext as _ + +from servo.models import Location, Shipment, ServicePart + + +class ConvertToStockForm(forms.Form): + partNumber = forms.CharField(widget=forms.HiddenInput()) + + +class GoodPartReturnForm(forms.Form): + comptiaModifier = forms.ChoiceField( + label=_("Reason"), + choices=[ + ('', _("Select...")), + ('A', _("Part not needed")), + ('B', _("Duplicated part")), + ('C', _("Added wrong part")), + ('D', _("Tried to cancel order")), + ('E', _("Customer refused order")), + ] + ) + comptiaCode = forms.ChoiceField( + label=_("Type"), + choices=[ + ('', _("Select...")), + ('DIA', _("Diagnostic")), + ('UOP', _("Un-Opened")), + ] + ) + + +class DoaPartReturnForm(forms.Form): + comptiaCode = forms.ChoiceField( + label=_("Symptom Code"), + choices=[('', _("Select..."))] + ) + comptiaModifier = forms.ChoiceField( + label=_("Symptom Modifier"), + choices=gsxws.comptia.MODIFIERS + ) + + def __init__(self, part, data=None): + super(DoaPartReturnForm, self).__init__(data=data) + self.fields['comptiaCode'].choices += part.get_comptia_symptoms() + + +class BulkReturnSearchForm(forms.Form): + location = forms.ModelChoiceField( + label=_('Location'), + queryset=Location.objects.all() + ) + + +class BulkReturnPartForm(forms.ModelForm): + class Meta: + model = ServicePart + widgets = { + 'box_number': forms.Select(attrs={'class': 'input-small'}), + 'part_number': forms.HiddenInput(), + 'part_title': forms.HiddenInput(), + 'service_order': forms.HiddenInput(), + 'return_order': forms.HiddenInput(), + } + exclude = [] + + def __init__(self, *args, **kwargs): + super(BulkReturnPartForm, self).__init__(*args, **kwargs) + if 'instance' in kwargs: + box_choices = [(0, 'Individual',)] + instance = kwargs['instance'] + # @TODO: This seems like a totally unnecessary hack... + # Why can't I just pass the number of options directly to the form? + part_count = instance.shipment.servicepart_set.all().count() + for x in xrange(1, part_count): + box_choices.append((x, x,)) + + self.fields['box_number'].widget.choices = box_choices + + +class BulkReturnForm(forms.ModelForm): + class Meta: + model = Shipment + exclude = [] + -- cgit v1.2.3