aboutsummaryrefslogtreecommitdiffstats
path: root/servo/forms
diff options
context:
space:
mode:
authorFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
committerFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
commit63b0fc6269b38edf7234b9f151b80d81f614c0a3 (patch)
tree555de3068f33f8dddb4619349bbea7d9b7c822fd /servo/forms
downloadServo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.gz
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.bz2
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.zip
Initial commit
First public commit
Diffstat (limited to 'servo/forms')
-rw-r--r--servo/forms/__init__.py33
-rw-r--r--servo/forms/account.py104
-rw-r--r--servo/forms/admin.py557
-rw-r--r--servo/forms/base.py172
-rw-r--r--servo/forms/checkin.py303
-rw-r--r--servo/forms/customer.py113
-rw-r--r--servo/forms/devices.py94
-rw-r--r--servo/forms/invoices.py108
-rw-r--r--servo/forms/notes.py91
-rw-r--r--servo/forms/orders.py167
-rw-r--r--servo/forms/product.py228
-rw-r--r--servo/forms/repairs.py99
-rw-r--r--servo/forms/returns.py112
13 files changed, 2181 insertions, 0 deletions
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'''
+ <div class="input-group">
+ {1}
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="button"><i class="glyphicon glyphicon-search"></i></button>
+ </span>
+ </div>
+ ''', 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'''
+ <div class="input-append date datepicker" data-provide="datepicker" {0}>
+ {1}
+ <span class="add-on">
+ <i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
+ </span>
+ </div>
+ ''', 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'''
+ <div class="input-append date datetimepicker" {0}>
+ {1}
+ <span class="add-on">
+ <i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
+ </span>
+ </div>
+ ''', 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 <a href="/checkin/terms/" target="_blank">terms of service.</a>')
+ 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('&nbsp;'),
+ 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('&nbsp;'),
+ 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('&nbsp;'),
+ 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 = []
+