commit 7fd5c90722badb2256212dc6a12ee416c6a7adaa Author: Andrey Date: Tue Oct 29 12:42:42 2013 +0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c01289 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.*~ +*.pyc +.DS_Store +._* +pip-log.txt +ENV/ +.idea/ +local_settings.py +Thumbs.db +distribute-*.tar.gz +*.bak + +_public_html/ diff --git a/README b/README new file mode 100644 index 0000000..8bd6acb --- /dev/null +++ b/README @@ -0,0 +1 @@ +Документор diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/log/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..82cfa83 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/commons/__init__.py b/project/commons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/commons/forms.py b/project/commons/forms.py new file mode 100644 index 0000000..de8de01 --- /dev/null +++ b/project/commons/forms.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from django import forms + + +def set_field_error(form, field, msg=u'Обязательное поле.'): + """Добавить сообщение об ошибке поля и убрать это поле из списка успешно прошедших валидацию. + Полезно, если нужно инвалидировать поле из метода clean() и добавить ему ошибку. + В этом случае исключение forms.ValidationError() не подходит, т.к. оно добавит сообщение об ошибке в ошибки формы. + """ + form._errors[field] = form.error_class([msg]) + if field in form.cleaned_data: + del form.cleaned_data[field] + + +class _MySuperForm(object): + """Базовая форма. Добавляет всякого полезного функционала к форме.""" + + # Список условно-обязательных полей, у которых нужно установить атрибут required=False. + # Полезно, когда какие-то поля становятся обязательны к заполнению в зависимости от значения других полей. + conditional_fields = [] #TODO мигрировать на unset_required + + # Список полей, у которых нужно сбросить признак обязательности: required=False. + # Полезно, когда в базовой форме определяются какие-то поля, которые в одних унаследованных формах обязательны, + # а в других нет. + unset_required = [] + + # Словарь полей, у которых нужно заменить атрибут label. + # Полезно, когда нужно дать разные метки полям в админке и в форме, с которой работает пользователь. + change_labels = {} + + def __init__(self, *args, **kwargs): + fields = self.fields + + # включить локализацию для DecimalFields + for field in fields.values(): + if isinstance(field, forms.DecimalField): + field.localize = True + field.widget.is_localized = True + + # сбросить признак обязательности у условно-обязательных полей + for key in self.conditional_fields: + fields[key].required = False + + # сбросить признак обязательности + for key in self.unset_required: + fields[key].required = False + + # заменить label + for key, label in self.change_labels.iteritems(): + fields[key].label = label + + +class MyBaseForm(forms.Form, _MySuperForm): + """Расширение django.forms.Form.""" + def __init__(self, *args, **kwargs): + forms.Form.__init__(self, *args, **kwargs) + _MySuperForm.__init__(self, *args, **kwargs) + + +class MyBaseModelForm(forms.ModelForm, _MySuperForm): + """Расширение django.forms.ModelForm.""" + def __init__(self, *args, **kwargs): + forms.ModelForm.__init__(self, *args, **kwargs) + _MySuperForm.__init__(self, *args, **kwargs) diff --git a/project/commons/models.py b/project/commons/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/project/commons/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/project/commons/paginator.py b/project/commons/paginator.py new file mode 100644 index 0000000..6213c2b --- /dev/null +++ b/project/commons/paginator.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +from functools import wraps + +from django.conf import settings +from django.core.paginator import Paginator, InvalidPage, EmptyPage +from django import forms + +from utils import safe_int + + +# допустимые значения `per_page` +_ALLOW_PER_PAGE = (10,20,50,75,100,) # по умолчанию +ALLOW_PER_PAGE = getattr(settings, 'ALLOW_PER_PAGE', _ALLOW_PER_PAGE) + + +class PaginationForm(forms.Form): + """Форма для пагинатора со списком допустимых значений `per_page`.""" + per_page = forms.ChoiceField(label=u'записей на странице', choices=zip(ALLOW_PER_PAGE, ALLOW_PER_PAGE), + required=False) + + +def save_per_page_value(func): + """Декоратор. + Если задан request.POST['per_page'], то сохранить его в куку. + Имя куки - per_page, срок хранения - 1 год. + """ + @wraps(func) + def wrapper(request, *args, **kwargs): + output = func(request, *args, **kwargs) + key = 'per_page' + if request.method == 'POST' and key in request.POST: + per_page = safe_int(request.POST[key]) + if per_page in ALLOW_PER_PAGE: + if hasattr(output, 'set_cookie'): + max_age = 365*24*60*60 # год + output.set_cookie(key, per_page, max_age) + return output + return wrapper + + +def get_per_page_value(request): + """Возвращает значение `per_page` (нужно для создания пагинатора). + Последовательно ищет `per_page` в словарях request.POST и request.COOKIES. + Если его там нет, возвращает самое первое значение из списка допустимых. + """ + per_page = None + # если задан, взять per_page из post + if request.method == 'POST' and 'per_page' in request.POST: + per_page = safe_int(request.POST.get('per_page')) + # иначе попробовать взять его из cookies + elif 'per_page' in request.COOKIES: + per_page = safe_int(request.COOKIES.get('per_page')) + # проверить чтоб значение per_page было в списке допустимых + if per_page not in ALLOW_PER_PAGE: + per_page = ALLOW_PER_PAGE[0] + return per_page + + +def pagination(request, object_list, page_num=None, form_class=PaginationForm): + """Создает и возвращает объект django.core.paginator.Paginator и, + если form_class!=None, форму со списком допустимых значений `per_page`. + """ + per_page = get_per_page_value(request) # кол-во записей на странице + # пагинатор + paginator = Paginator(object_list, per_page) + page_num = max(1, safe_int(page_num, 1)) + try: + objects = paginator.page(page_num) + except (EmptyPage, InvalidPage): + objects = paginator.page(paginator.num_pages) + # форма + form = None + if form_class: + form = PaginationForm(initial={'per_page': per_page,}) + return objects, form diff --git a/project/commons/pdf_tools.py b/project/commons/pdf_tools.py new file mode 100644 index 0000000..da03829 --- /dev/null +++ b/project/commons/pdf_tools.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import cStringIO as StringIO + +import ho.pisa as pisa + +from django.template.loader import render_to_string +from django.template import RequestContext +from django.http import HttpResponse + + +def pdf_to_response(content, filename=None, filename_encode='windows-1251'): + """Выводит content в django.http.HttpResponse, который и возвращает.""" + response = HttpResponse(content, mimetype='application/pdf') + if filename: + if filename_encode: + filename = filename.encode(filename_encode) + response['Content-Disposition'] = ('attachment; filename="%s"' % filename.replace('"', "''")) + return response + + +def render_pdf_to_string(request, template_name, dictionary=None): + """Рендерит html шаблон в pdf. Возвращает строку, в которой содержится сгенерированный pdf.""" + context_instance = RequestContext(request) + html = render_to_string(template_name, dictionary, context_instance) + #return HttpResponse(html) # для отладки + result = StringIO.StringIO() + pisa.pisaDocument(StringIO.StringIO(html.encode('utf-8')), result, encoding='utf-8') + pdf_content = result.getvalue() + result.close() + return pdf_content diff --git a/project/commons/templatetags/__init__.py b/project/commons/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/commons/templatetags/my_tags.py b/project/commons/templatetags/my_tags.py new file mode 100644 index 0000000..63ff306 --- /dev/null +++ b/project/commons/templatetags/my_tags.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import os + +from django import template +from django.conf import settings + + +DEBUG = getattr(settings, 'DEBUG', False) + +register = template.Library() + + +@register.simple_tag +def fonts_root(): + """Возвращает путь к шрифтам, заданный в settings.FONTS_ROOT или пустую строку. + Если путь не пустой, то по необходимости добавляет в конец os.sep + """ + path = getattr(settings, 'PDF_FONTS_ROOT', '') + if path and not path.endswith(os.path.sep): + path += os.path.sep + return path + + +@register.simple_tag +def sum_by_attr(obj_list, attr_name, start=0, stop=None): + """Возвращает сумму значений атрибута/метода attr_name у объектов списка obj_list.""" + try: + result = 0 + for obj in obj_list[start:stop]: + attr = getattr(obj, attr_name) + if callable(attr): + result += attr() + else: + result += attr + except Exception, error: + if DEBUG: + result = 'Tag error: %s' % error + else: + result = 'n/a' + return result + + +@register.filter(name='field_type') +def field_type(value): + """Возвращает название типа для переданного поля формы.""" + return value.field.__class__.__name__ + + +@register.filter(name='widget_type') +def widget_type(value): + """Возвращает название виджета для переданного поля формы.""" + return value.field.widget.__class__.__name__ + + +@register.filter(name='to_float') +def to_float(value): + """Если возможно, приводит value к типу float.""" + try: + result = float(value) + except Exception, error: + if DEBUG: + result = 'Filter error, %s | %s' % (value, error,) + else: + result = value + return result diff --git a/project/commons/tests.py b/project/commons/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/project/commons/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/project/commons/utils.py b/project/commons/utils.py new file mode 100644 index 0000000..a923684 --- /dev/null +++ b/project/commons/utils.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import datetime + + +# convert datetime to json +dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date) else None + + +def safe_int(value, default=None): + """Возвращает value, приведенное к типу int, или default, если привести не получается.""" + try: + return int(value) + except: + return default diff --git a/project/commons/views.py b/project/commons/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/project/commons/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/project/commons/xls/__init__.py b/project/commons/xls/__init__.py new file mode 100644 index 0000000..ae64160 --- /dev/null +++ b/project/commons/xls/__init__.py @@ -0,0 +1,3 @@ +from useful_tools import * +from get_xlwt_style_list import * +from xls_to_response import * diff --git a/project/commons/xls/get_xlwt_style_list.py b/project/commons/xls/get_xlwt_style_list.py new file mode 100644 index 0000000..a8690d2 --- /dev/null +++ b/project/commons/xls/get_xlwt_style_list.py @@ -0,0 +1,78 @@ +import xlwt + +def get_xlwt_style_list(rdbook): + wt_style_list = [] + for rdxf in rdbook.xf_list: + wtxf = xlwt.Style.XFStyle() + # + # number format + # + wtxf.num_format_str = rdbook.format_map[rdxf.format_key].format_str + # + # font + # + wtf = wtxf.font + rdf = rdbook.font_list[rdxf.font_index] + wtf.height = rdf.height + wtf.italic = rdf.italic + wtf.struck_out = rdf.struck_out + wtf.outline = rdf.outline + wtf.shadow = rdf.outline + wtf.colour_index = rdf.colour_index + wtf.bold = rdf.bold #### This attribute is redundant, should be driven by weight + wtf._weight = rdf.weight #### Why "private"? + wtf.escapement = rdf.escapement + wtf.underline = rdf.underline_type #### + # wtf.???? = rdf.underline #### redundant attribute, set on the fly when writing + wtf.family = rdf.family + wtf.charset = rdf.character_set + wtf.name = rdf.name + # + # protection + # + wtp = wtxf.protection + rdp = rdxf.protection + wtp.cell_locked = rdp.cell_locked + wtp.formula_hidden = rdp.formula_hidden + # + # border(s) (rename ????) + # + wtb = wtxf.borders + rdb = rdxf.border + wtb.left = rdb.left_line_style + wtb.right = rdb.right_line_style + wtb.top = rdb.top_line_style + wtb.bottom = rdb.bottom_line_style + wtb.diag = rdb.diag_line_style + wtb.left_colour = rdb.left_colour_index + wtb.right_colour = rdb.right_colour_index + wtb.top_colour = rdb.top_colour_index + wtb.bottom_colour = rdb.bottom_colour_index + wtb.diag_colour = rdb.diag_colour_index + wtb.need_diag1 = rdb.diag_down + wtb.need_diag2 = rdb.diag_up + # + # background / pattern (rename???) + # + wtpat = wtxf.pattern + rdbg = rdxf.background + wtpat.pattern = rdbg.fill_pattern + wtpat.pattern_fore_colour = rdbg.pattern_colour_index + wtpat.pattern_back_colour = rdbg.background_colour_index + # + # alignment + # + wta = wtxf.alignment + rda = rdxf.alignment + wta.horz = rda.hor_align + wta.vert = rda.vert_align + wta.dire = rda.text_direction + # wta.orie # orientation doesn't occur in BIFF8! Superceded by rotation ("rota"). + wta.rota = rda.rotation + wta.wrap = rda.text_wrapped + wta.shri = rda.shrink_to_fit + wta.inde = rda.indent_level + # wta.merg = ???? + # + wt_style_list.append(wtxf) + return wt_style_list diff --git a/project/commons/xls/useful_tools.py b/project/commons/xls/useful_tools.py new file mode 100644 index 0000000..236d822 --- /dev/null +++ b/project/commons/xls/useful_tools.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +def copy_cells(src_sheet, dst_sheet, style_list, + row_from=0, row_to=None, dst_row_shift=0, + col_from=0, col_to=None, dst_col_shift=0): + """ + Скопировать блок ячеек из диапазона строк [row_from, row_to] и колонок + [col_from, col_to] исходного листа в новый лист с сохранением их контента, + исходных стилей форматирования, объединения и высоты строк. + """ + row_to = row_to or src_sheet.nrows-1 + col_to = col_to or src_sheet.ncols-1 + + for row in xrange(row_from, row_to+1): + for col in xrange(col_from, col_to+1): + cell = src_sheet.cell(row, col) + # скопировать контент и стиль ячейки + dst_sheet.write( + row + dst_row_shift, + col + dst_col_shift, + cell.value, + style_list[cell.xf_index], + ) + + # задать высоту строк + height_rows(src_sheet, dst_sheet, row_from, row_to, dst_row_shift) + + # объединить ячейки + merge_cells(src_sheet, dst_sheet, style_list, + row_from, row_to, dst_row_shift, + col_from, col_to, dst_col_shift) + + +def height_rows(src_sheet, dst_sheet, + row_from=0, row_to=None, dst_row_shift=0): + """Задать в диапазоне строк [row_from, row_to] высоту как в исходном листе. + """ + row_to = row_to or src_sheet.nrows-1 + for row in xrange(row_from, row_to+1): + src_rowinfo = src_sheet.rowinfo_map.get(row) + if src_rowinfo: + dst_sheet.row(row+dst_row_shift).height = src_rowinfo.height + dst_sheet.row(row+dst_row_shift).height_mismatch = True + + +def merge_cells(src_sheet, dst_sheet, style_list, + row_from=0, row_to=None, dst_row_shift=0, + col_from=0, col_to=None, dst_col_shift=0): + """ + Объединить ячейки в заданном блоке нового листа, ограниченном строками + [row_from, row_to] и колонками [col_from, col_to], если в исходном листе + они были объединены, с сохранением исходных стилей форматирования. + """ + row_to = row_to or src_sheet.nrows + col_to = col_to or src_sheet.ncols + + for r1,r2,c1,c2 in src_sheet.merged_cells: + if r1 < row_from or r1 > row_to: + continue + if c1 < col_from or c1 > col_to: + continue + + cell = src_sheet.cell(r1, c1) + style = style_list[cell.xf_index] + + # сохранить границы "крайней" ячейки + # нафиг пока эту фичу - из-за нее повылазили какие-то границы, + # которых не было вообще +# brd_1 = style.borders +# cell_2 = src_sheet.cell(r2-1, c2-1) +# brd_2 = style_list[cell_2.xf_index].borders +# print r1,c1,r2,c2, +# print 'borders 1 (left, right, top, bottom)', +# print brd_1.left, brd_1.right, brd_1.top, brd_1.bottom, +# print 'border 2 (same)', +# print brd_2.left, brd_2.right, brd_2.top, brd_2.bottom +# brd_1.right = brd_2.right +# brd_1.right_colour = brd_2.right_colour + + dst_sheet.merge( + r1+dst_row_shift, r2+dst_row_shift-1, + c1+dst_col_shift, c2+dst_col_shift-1, + style) + + +def width_cols(src_sheet, dst_sheet, col_from=0, col_to=None, dst_col_shift=0): + """Задать в диапазоне колонок [col_from, col_to] ширину + как в исходном листе. + """ + col_to = col_to or src_sheet.ncols-1 + for col in xrange(col_from, col_to+1): + dst_sheet.col(col+dst_col_shift).width = ( + src_sheet.computed_column_width(col)) + + +def mm_to_twips(x): + """Перевести из миллиметров в twips.""" + return int(x/25.4*72*20) + + +def horz_page_break(dst_sheet, row): + """Добавить разрыв страницы.""" + dst_sheet.horz_page_breaks.append((row, 0, 255)) + + +# -------------------------------------------------------------- прочие хелперы + +def clone_row(src_sheet, dst_sheet, style_list, + src_row, n_times=1, dst_row_shift=0): + """ + Размножить n_times раз строку из исходного листа, с сохранением стилей + форматирования. + """ + for offset in xrange(n_times+1): + copy_cells(src_sheet, dst_sheet, style_list, + row_from=src_row, row_to=src_row, + dst_row_shift=dst_row_shift+offset) + + # задать высоту строк + height_rows(src_sheet, dst_sheet, src_row, src_row, dst_row_shift) + + # объединить ячейки + merge_cells(src_sheet, dst_sheet, style_list, + src_row, src_row, dst_row_shift) + + +def merge_cells_in_row(src_sheet, dst_sheet, style_list, src_row, dst_row): + """ + Объединить ячейки в заданной строке нового листа, если в исходном листе они + были объединены, с сохранением исходных стилей форматирования. + """ + for r1,r2,c1,c2 in src_sheet.merged_cells: + if r1 != src_row: + continue + cell = src_sheet.cell(r1, c1) + dst_sheet.merge(dst_row, dst_row, c1, c2-1, style_list[cell.xf_index]) + + +def sum_src_heights(src_sheet, row_from, row_to): + """Суммарная высота всех строк диапазона [row_from, row_to] + исходного листа. + """ + result = 0 + for row in xrange(row_from, row_to+1): + src_rowinfo = src_sheet.rowinfo_map.get(row) + if src_rowinfo: + result += src_rowinfo.height + return result + + +def sum_dst_heights(dst_sheet, row_from, row_to): + """Суммарная высота всех строк диапазона [row_from, row_to] + на новом листе. + """ + result = 0 + for row in xrange(row_from, row_to+1): + result += dst_sheet.row(row).height + return result diff --git a/project/commons/xls/xls_to_response.py b/project/commons/xls/xls_to_response.py new file mode 100644 index 0000000..26f7dfd --- /dev/null +++ b/project/commons/xls/xls_to_response.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from django.http import HttpResponse + + +def xls_to_response(content, filename=None, filename_encode='windows-1251'): + """Выводит content в django.http.HttpResponse, который и возвращает.""" + response = HttpResponse(content, mimetype='application/ms-excel') + if filename: + if filename_encode: + filename = filename.encode(filename_encode) + response['Content-Disposition'] = ('attachment; filename="%s"' % filename.replace('"', "''")) + return response diff --git a/project/customer/__init__.py b/project/customer/__init__.py new file mode 100644 index 0000000..e027062 --- /dev/null +++ b/project/customer/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .models import get_profile diff --git a/project/customer/admin.py b/project/customer/admin.py new file mode 100644 index 0000000..9bf71e5 --- /dev/null +++ b/project/customer/admin.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin + +import forms +import models + + +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'profile_type', 'name', 'inn',) + list_display_links = list_display + form = forms.UserProfileAdminForm + +#TODO прописать fieldsets +# fieldsets = [ +# (None, {'fields': ['user',]}), +# (None, {'fields': ['profile_type',]}), +# (None, {'fields': ['name', 'phone_code', 'phone', 'address', 'inn',]}), +# (None, {'fields': ['add_glavbuh_sign', 'glavbuh_fio',]}), +# (None, {'fields': ['v_litce', 'na_osnovanii',]}), +# (u'ИП', {'fields': ['ip_surname', 'ip_name', 'ip_midname', 'ip_kod_okpo',]}), +# (u'Организация', {'fields': ['org_boss_name', 'org_kpp',]}), +# (u'Печать и подписи', {'fields': ['boss_sign', 'glavbuh_sign', 'stamp',]}), +# ] + + +class BankAccountAdmin(admin.ModelAdmin): + class Media: + css = {'all': ('css/custom_admin.css',)} + + list_display = ('user', 'is_main', 'name', 'account', 'created_at',) + list_display_links = list_display + form = forms.BankAccountAdminForm + + +class ClientAdmin(admin.ModelAdmin): + class Media: + css = {'all': ('css/custom_admin.css',)} + + list_display = ('user', 'name', 'inn',) + list_display_links = list_display + form = forms.ClientAdminForm + + fieldsets = [ + (None, {'fields': ['user',]}), + (None, {'fields': ['name', 'inn', 'address',]}), + (u'ИП', {'fields': ['okpo',]}), + (u'Организация', {'fields': ['kpp',]}), + (u'Банковские реквизиты', + {'fields': ['bank_bik', 'bank_name', 'bank_address', 'bank_korr_account', 'bank_account',]}), + (u'Контакты', + {'fields': ['contact_name', 'contact_email', 'contact_phone', 'contact_icq', 'contact_skype', + 'contact_other',]}), + ] + + +admin.site.register(models.UserProfile, UserProfileAdmin) +admin.site.register(models.BankAccount, BankAccountAdmin) +admin.site.register(models.Client, ClientAdmin) diff --git a/project/customer/consts.py b/project/customer/consts.py new file mode 100644 index 0000000..e607db5 --- /dev/null +++ b/project/customer/consts.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +IP_PROFILE = 1 +ORG_PROFILE = 2 + +PROFILE_TYPES = ( + (IP_PROFILE, u'Индивидуальный предприниматель'), + (ORG_PROFILE, u'Организация'), +) diff --git a/project/customer/forms.py b/project/customer/forms.py new file mode 100644 index 0000000..32cf6e0 --- /dev/null +++ b/project/customer/forms.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- +from django import forms +from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe +from django.conf import settings + +from project.commons.forms import MyBaseModelForm, set_field_error + +from . import consts, models + + +FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(settings, 'FILE_UPLOAD_MAX_MEMORY_SIZE ', 2621440) # default 2.5Mb + + +def get_profile_form_class(profile_type): + """Возвращает класс формы редактирования профиля пользователя.""" + if profile_type == consts.IP_PROFILE: + return IpUserProfileForm + elif profile_type == consts.ORG_PROFILE: + return OrgUserProfileForm + return None + + +def get_profile_filters_form_class(profile_type): + """Возвращает класс формы фильтрации профиля пользователя.""" + if profile_type == consts.IP_PROFILE: + return IpUserProfileFiltersForm + elif profile_type == consts.ORG_PROFILE: + return OrgUserProfileFiltersForm + return None + +# ----------------------------------------------------------------------------- + + +class UserProfileForm(MyBaseModelForm): + """Общая форма редактирования профиля пользователя. + + Специализированные формы для редактирования профилей ИП и Организаций - + ищи ниже в классах IpUserProfileForm и OrgUserProfileForm соответственно. + + Форму для админки ищи ниже в классе UserProfileAdminForm. + """ + class Meta: + model = models.UserProfile + + def __init__(self, *args, **kwargs): + super(UserProfileForm, self).__init__(*args, **kwargs) + f = self.fields + if 'ip_reg_date' in f: + f['ip_reg_date'].widget.attrs['class'] = 'has-datepicker' + + def _check_file_size(self, image): + """Ограничить максимальный размер загружаемого файла.""" + if image and image.size > FILE_UPLOAD_MAX_MEMORY_SIZE: + raise forms.ValidationError( + u'Размер изображения превышает %i Мб' % (FILE_UPLOAD_MAX_MEMORY_SIZE / (1024*1024))) + return image + + def clean_boss_sign(self): + image = self.cleaned_data.get('boss_sign') + return self._check_file_size(image) + + def clean_glavbuh_sign(self): + image = self.cleaned_data.get('glavbuh_sign') + return self._check_file_size(image) + + def clean_stamp(self): + image = self.cleaned_data.get('stamp') + return self._check_file_size(image) + + def clean_logo(self): + image = self.cleaned_data.get('logo') + return self._check_file_size(image) + + +class IpUserProfileForm(UserProfileForm): + """Форма редактирования профиля - ИП.""" + change_labels = {'ogrn': u'ОГРНИП'} + + class Meta(UserProfileForm.Meta): + fields = ( + # фио ип + 'boss_surname', 'boss_name', 'boss_midname', + # инн, огрнип, окпо + 'inn', 'ogrn', 'okpo', + # свид-во гос. регистрации и дата + 'svid_gos_reg', 'ip_reg_date', + # фио главбуха + 'glavbuh_surname', 'glavbuh_name', 'glavbuh_midname', + # контактная информация - адреса, телефон, факс, почта, сайт + 'address', 'real_address', 'phone_code', 'phone', 'fax_code', 'fax', 'email', 'site', + # подписи, печать и логотип + 'boss_sign', 'glavbuh_sign', 'stamp', 'logo', + ) + + +class OrgUserProfileForm(UserProfileForm): + """Форма редактирования профиля - Организация.""" + unset_required = {'ogrn': u'ОГРН'} + change_labels = {'ogrn': u'ОГРН'} + + class Meta(UserProfileForm.Meta): + fields = ( + # краткое и полное названия организации + 'name', 'full_name', + # инн, кпп, огрн, окпо + 'inn', 'kpp', 'ogrn', 'okpo', + # должность руководителя, его фио и на каком основании он действует + 'boss_title', 'boss_surname', 'boss_name', 'boss_midname', 'na_osnovanii', + # фио главбуха + 'glavbuh_surname', 'glavbuh_name', 'glavbuh_midname', + # контактная информация - адреса, телефон, факс, почта, сайт + 'address', 'jur_address', 'real_address', 'phone_code', 'phone', 'fax_code', 'fax', 'email', 'site', + # подписи, печать и логотип + 'boss_sign', 'glavbuh_sign', 'stamp', 'logo', + ) + + +class UserProfileAdminForm(UserProfileForm): + """Форма редактирования профиля - для админки.""" + + # условно-обязательные поля, проверять отдельно - могут быть обязательны в зависимости от типа профиля + unset_required = [ + # для ИП + 'kpp', 'name' + # для Организаций + ] + + def clean(self): + super(UserProfileAdminForm, self).clean() + + cleaned_data = self.cleaned_data + + # тип профиля - ИП или Организация + profile_type = cleaned_data.get('profile_type') + + if profile_type == consts.IP_PROFILE: # поля, обязательные для ИП + pass + + elif profile_type == consts.ORG_PROFILE: # поля, обязательные для Организаций + org_boss_name = cleaned_data.get('org_boss_name') + kpp = cleaned_data.get('kpp') + + if not org_boss_name: set_field_error(self, 'org_boss_name') + if not kpp: set_field_error(self, 'kpp') + + return cleaned_data + +# ----------------------------------------------------------------------------- + + +class BankAccountForm(forms.ModelForm): + """Форма редактирования расчетных счетов.""" + class Meta: + model = models.BankAccount + fields = ('bik', 'name', 'address', 'korr_account', 'account', 'is_main',) + #_textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + #widgets = {'name': _textarea, 'address': _textarea,} + + +class BankAccountAdminForm(BankAccountForm): + """Форма редактирования расчетных счетов - для админки.""" + class Meta(BankAccountForm.Meta): + fields = None + + +class BankAccountListForm(forms.Form): + """Форма со списком всех расчетных счетов пользователя.""" + bank_account = forms.ModelChoiceField(queryset=models.BankAccount.objects.get_all(None), + empty_label=u'все контрагенты', required=False) + + def __init__(self, user, *args, **kwargs): + super(BankAccountListForm, self).__init__(*args, **kwargs) + self.fields['bank_account'].queryset = models.BankAccount.objects.get_all(user) + +# ----------------------------------------------------------------------------- + + +class ClientForm(forms.ModelForm): + """Форма редактирования контрагентов.""" + class Meta: + model = models.Client + fields = ('name', 'inn', 'kpp', 'okpo', 'address', + # банковские реквизиты + 'bank_bik', 'bank_name', 'bank_address', 'bank_korr_account', 'bank_account', + # контакты + 'contact_name', 'contact_email', 'contact_phone', 'contact_icq', 'contact_skype', 'contact_other', + ) + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + #'name': _textarea, + #'address': _textarea, + 'bank_name': _textarea, + #'bank_address': _textarea, + #'contact_other': _textarea, + } + + +class ClientAdminForm(ClientForm): + """Форма редактирования контрагентов - для админки.""" + class Meta(ClientForm.Meta): + fields = None + + +class ClientsListForm(forms.Form): + """Форма со списком всех контрагентов пользователя.""" + client = forms.ModelChoiceField(queryset=models.Client.objects.get_all(None), empty_label=u'все контрагенты', + required=False) + + def __init__(self, user, *args, **kwargs): + super(ClientsListForm, self).__init__(*args, **kwargs) + self.fields['client'].queryset = models.Client.objects.get_all(user) + +# ----------------------------------------------------------------------------- + + +class UserProfileFiltersForm(MyBaseModelForm): + """Общая форма фильтрации реквизитов.""" + _profile_type = None # задать в наследнике! + + _is_admin = False + _user = None + + class Meta: + model = models.UserProfileFilters + widgets = { + 'bank_account': forms.RadioSelect(), + } + + def __init__(self, profile=None, accounts=None, *args, **kwargs): + instance = kwargs.get('instance') + initial = kwargs.get('initial') + + new_initial = { + # всегда включены + 'show_ip_boss_fio': True, + 'show_name': True, + } + + # TODO 1. переписать проверки в стиле new_initial['show_inn'] = bool(profile.inn) + # TODO 2. загнать условия в словарь вида {'show_inn': 'inn'}. потом в цикле обработать + if profile: + # сбросить чекбоксы, если не заполнены определенные поля в профиле или нет расчетных счетов + if instance: + new_initial['show_inn'] = bool(profile.inn) + new_initial['show_ogrn'] = bool(profile.ogrn) + new_initial['show_okpo'] = bool(profile.okpo) + new_initial['show_glavbuh'] = bool(profile.get_glavbuh_fio()) + new_initial['show_real_address'] = bool(profile.real_address) + new_initial['show_phone'] = bool(profile.get_full_phone()) + new_initial['show_fax'] = bool(profile.get_full_fax()) + new_initial['show_email'] = bool(profile.email) + new_initial['show_site'] = bool(profile.site) + new_initial['show_bank_account'] = bool(accounts) + + if self._profile_type == consts.IP_PROFILE: + new_initial['show_svid_gos_reg'] = bool(profile.svid_gos_reg) + new_initial['show_ip_reg_date'] = bool(profile.ip_reg_date) + elif self._profile_type == consts.ORG_PROFILE: + new_initial['show_full_name'] = bool(profile.full_name) + new_initial['show_kpp'] = bool(profile.kpp) + new_initial['show_na_osnovanii'] = bool(profile.na_osnovanii) + new_initial['show_jur_address'] = bool(profile.jur_address) + else: + new_initial['show_inn'] = bool(profile.inn) + new_initial['show_ogrn'] = bool(profile.ogrn) + new_initial['show_okpo'] = bool(profile.okpo) + new_initial['show_glavbuh'] = bool(profile.get_glavbuh_fio()) + new_initial['show_real_address'] = bool(profile.real_address) + new_initial['show_phone'] = bool(profile.get_full_phone()) + new_initial['show_fax'] = bool(profile.get_full_fax()) + new_initial['show_email'] = bool(profile.email) + new_initial['show_site'] = bool(profile.site) + new_initial['show_bank_account'] = bool(accounts) + + if self._profile_type == consts.IP_PROFILE: + new_initial['show_svid_gos_reg'] = bool(profile.svid_gos_reg) + new_initial['show_ip_reg_date'] = bool(profile.ip_reg_date) + elif self._profile_type == consts.ORG_PROFILE: + new_initial['show_full_name'] = bool(profile.full_name) + new_initial['show_kpp'] = bool(profile.kpp) + new_initial['show_na_osnovanii'] = bool(profile.na_osnovanii) + new_initial['show_jur_address'] = bool(profile.jur_address) + + if initial: + initial.update(new_initial) + else: + kwargs['initial'] = new_initial + + super(UserProfileFiltersForm, self).__init__(*args, **kwargs) + + # для админки + if self._is_admin: + # попробовать взять user из kwargs['instance'], а потом из self.data + try: + self._user = getattr(kwargs.get('instance'), 'user') + except AttributeError: + self._user = self.data.get('user') + if not accounts: + accounts = models.BankAccount.objects.get_all(self._user) + + f = self.fields + + # только расчетные счета пользователя + f_acc = f['bank_account'] # TODO вынести настройку расчетных счетов в mixin? + f_acc.queryset = accounts + f_acc.empty_label = None + f_acc.label_from_instance = lambda obj: mark_safe( + force_unicode('%s
%s' % (obj.account, obj.name,))) # исправить метку + + # заблокировать чекбоксы, если: не заполнены определенные поля в профиле или нет расчетных счетов + if profile: + if not profile.inn: f['show_inn'].widget.attrs['disabled'] = 'disabled' + if not profile.ogrn: f['show_ogrn'].widget.attrs['disabled'] = 'disabled' + if not profile.okpo: f['show_okpo'].widget.attrs['disabled'] = 'disabled' + if not profile.get_glavbuh_fio(): f['show_glavbuh'].widget.attrs['disabled'] = 'disabled' + if not profile.real_address: f['show_real_address'].widget.attrs['disabled'] = 'disabled' + if not profile.get_full_phone(): f['show_phone'].widget.attrs['disabled'] = 'disabled' + if not profile.get_full_fax(): f['show_fax'].widget.attrs['disabled'] = 'disabled' + if not profile.email: f['show_email'].widget.attrs['disabled'] = 'disabled' + if not profile.site: f['show_site'].widget.attrs['disabled'] = 'disabled' + if not accounts: f['show_bank_account'].widget.attrs['disabled'] = 'disabled' + + if self._profile_type == consts.IP_PROFILE: + if not profile.svid_gos_reg: f['show_svid_gos_reg'].widget.attrs['disabled'] = 'disabled' + if not profile.ip_reg_date: f['show_ip_reg_date'].widget.attrs['disabled'] = 'disabled' + elif self._profile_type == consts.ORG_PROFILE: + if not profile.name: f['show_name'].widget.attrs['disabled'] = 'disabled' + if not profile.full_name: f['show_full_name'].widget.attrs['disabled'] = 'disabled' + if not profile.kpp: f['show_kpp'].widget.attrs['disabled'] = 'disabled' + if not profile.boss_title: f['show_org_boss_title_and_fio'].widget.attrs['disabled'] = 'disabled' + if not profile.na_osnovanii: f['show_na_osnovanii'].widget.attrs['disabled'] = 'disabled' + if not profile.jur_address: f['show_jur_address'].widget.attrs['disabled'] = 'disabled' + + # блокировать чекбоксы, т.к.эти реквизиты юзеру выключать нельзя + if self._profile_type == consts.IP_PROFILE: + f['show_ip_boss_fio'].widget.attrs['disabled'] = 'disabled' + elif self._profile_type == consts.ORG_PROFILE: + f['show_name'].widget.attrs['disabled'] = 'disabled' + + +class IpUserProfileFiltersForm(UserProfileFiltersForm): + """Форма фильтрации реквизитов - для ИП.""" + _profile_type = consts.IP_PROFILE + + change_labels = { + 'show_profile_type': dict(consts.PROFILE_TYPES)[_profile_type], + 'show_ogrn': u'ОГРНИП', + 'show_real_address': u'Адрес', + } + + class Meta(UserProfileFiltersForm.Meta): + fields = ( + 'show_profile_type', + 'show_ip_boss_fio', + 'show_inn', + 'show_ogrn', + 'show_okpo', + 'show_svid_gos_reg', + 'show_ip_reg_date', + 'show_glavbuh', + 'show_bank_account', + 'bank_account', + 'show_contact_info', + 'show_real_address', + 'show_phone', + 'show_fax', + 'show_email', + 'show_site', + ) + + +class OrgUserProfileFiltersForm(UserProfileFiltersForm): + """Форма фильтрации реквизитов - для Организаций.""" + _profile_type = consts.ORG_PROFILE + + change_labels = { + 'show_profile_type': dict(consts.PROFILE_TYPES)[_profile_type], + 'show_ogrn': u'ОГРН', + } + + class Meta(UserProfileFiltersForm.Meta): + fields = ( + 'show_profile_type', + 'show_name', + 'show_full_name', + 'show_inn', + 'show_kpp', + 'show_ogrn', + 'show_okpo', + 'show_org_boss_title_and_fio', + 'show_na_osnovanii', + 'show_glavbuh', + 'show_bank_account', + 'bank_account', + 'show_contact_info', + 'show_jur_address', + 'show_real_address', + 'show_phone', + 'show_fax', + 'show_email', + 'show_site', + ) + +# ----------------------------------------------------------------------------- + + +class EmailProfileForm(forms.Form): + """Форма отправки реквизитов пользователя по email.""" + to = forms.EmailField(label=u'E-mail получателя') + body = forms.CharField(label=u'Текст сообщения', max_length=1000, required=False, + widget=forms.Textarea(attrs={'cols': 80, 'rows': 3})) diff --git a/project/customer/managers.py b/project/customer/managers.py new file mode 100644 index 0000000..6d1d45e --- /dev/null +++ b/project/customer/managers.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.core.exceptions import ObjectDoesNotExist + + +class UserProfileManager(models.Manager): + def create_profile(self, user, profile_type): + """Создает профиль.""" + profile = self.model(user=user, profile_type=profile_type) + profile.save() + return profile + + +class UserProfileFiltersManager(models.Manager): + def create_filters(self, user): + """Создает фильтры профиля.""" + filters = self.model(user=user) + filters.save() + return filters + + def get_or_create_filters(self, user): + """Возвращает фильтры профиля. Если их вдруг нет, то создает.""" + try: + filters = self.get(user=user) + except ObjectDoesNotExist: + filters = self.create_filters(user=user) + filters.save() + return filters + + +class BankAccountManager(models.Manager): + def get_main(self, user): + """Возвращает основной расчетный счет пользователя или None если у него еще нет расчетных счетов.""" + try: + return self.filter(user=user).order_by('-is_main', 'created_at')[0] + except IndexError: + return None + + def get_all(self, user): + """Возвращает все расчетные счета пользователя. + Отсортированы так, что первым идет основной счет, а потом остальные в порядке их добавления.""" + return self.filter(user=user).order_by('-is_main', 'created_at') + + def have_main(self, user): + """Возвращает True, если у пользователя есть основной расчетный счет, и False в противном случае.""" + return True if self.filter(user=user, is_main=True) else False + + def force_main(self, user): + """Проверяет есть ли у пользователя основной расчетный счет. + И если нет - принудительно его выставляет. + """ + if not self.have_main(user=user): + try: + accounts = self.get_all(user=user)[0] + accounts.is_main=True + accounts.save() + except IndexError: + pass + + +class ClientManager(models.Manager): + def get_all(self, user): + """Возвращает всех клиентов пользователя.""" + return self.filter(user=user) diff --git a/project/customer/middleware.py b/project/customer/middleware.py new file mode 100644 index 0000000..37269cb --- /dev/null +++ b/project/customer/middleware.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from django.utils.functional import SimpleLazyObject + +from .models import get_profile + + +def _get_profile(request): + if not hasattr(request, '_cached_profile'): + request._cached_profile = get_profile(request.user) + return request._cached_profile + + +class ProfileMiddleware(object): + def process_request(self, request): + assert hasattr(request, 'user'), "The Profile middleware requires authentication middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware'." + + request.profile = SimpleLazyObject(lambda: _get_profile(request)) diff --git a/project/customer/models.py b/project/customer/models.py new file mode 100644 index 0000000..3f1ac81 --- /dev/null +++ b/project/customer/models.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +import os + +from PIL import Image + +from django.db import models +from django.contrib.auth.models import User + +from . import consts, managers + + +PROFILE_IMAGES_UPLOAD_DIR = 'customer/profile/' # куда сохранять загруженные изображения +BOSS_SIGN_IMG_SIZE = (100, 75) +GLAVBUH_SIGN_IMG_SIZE = (100, 75) +STAMP_IMG_SIZE = (180, 180) + + +def get_profile(user): + """Возвращает профиль пользователя или None.""" + try: + return UserProfile.objects.get(user=user) + except UserProfile.DoesNotExist: + return None + + +def upload_to(path, new_filename=None): + """Куда и под каким именем сохранить загруженный файл.""" + def get_upload_path(instance, filename): + filename = new_filename or filename + return os.path.join(path, instance.user.username, filename) + return get_upload_path + + +class UserProfile(models.Model): + """Профиль пользователя.""" + user = models.OneToOneField(User, related_name='profile', primary_key=True) + + profile_type = models.PositiveSmallIntegerField(u'Тип профиля', choices=consts.PROFILE_TYPES) + + # общие поля + boss_surname = models.CharField(u'Фамилия', max_length=30, default='', + help_text=u'Используется для строки "подпись" в документах.') + boss_name = models.CharField(u'Имя', max_length=30, default='') + boss_midname = models.CharField(u'Отчество', max_length=30, default='') + + inn = models.CharField(u'ИНН', max_length=12, default='') # длина: 10 для организаций, 12 для ИП + ogrn = models.CharField(u'ОГРН/ОГРНИП', max_length=15, default='') # длина: 13 для организаций, 15 для ИП + okpo = models.CharField(u'ОКПО', max_length=10, blank=True, default='') # длина: 8 для организаций, 8 или 10 для ИП + + glavbuh_surname = models.CharField(u'Фамилия', max_length=30, blank=True, default='', + help_text=u'Используется для строки "подпись" в документах.') + glavbuh_name = models.CharField(u'Имя', max_length=30, blank=True, default='') + glavbuh_midname = models.CharField(u'Отчество', max_length=30, blank=True, default='') + + address = models.CharField(u'Адрес для документов', max_length=256, default='', + help_text=u'Будет подставляться в создаваемые счета, акты и накладные.') + real_address = models.CharField(u'Фактический адрес', max_length=256, blank=True, default='', + help_text=u'Используется только для карточки компании.') + + phone_code = models.CharField(u'Код города', max_length=10, blank=True, default='') + phone = models.CharField(u'Номер телефона', max_length=20, blank=True, default='') + + fax_code = models.CharField(u'Код города', max_length=10, blank=True, default='') + fax = models.CharField(u'Номер телефона', max_length=20, blank=True, default='') + + email = models.EmailField(u'Электронная почта', max_length=75, blank=True, default='') + site = models.CharField(u'Сайт', max_length=256, blank=True, default='') + + # поля, только для ИП + svid_gos_reg = models.CharField(u'Свид-во о гос. регистрации', max_length=256, blank=True, default='', + help_text=u'Требуется для счет-фактуры.') + + ip_reg_date = models.DateField(u'Дата регистрации ИП', blank=True, null=True) + + # поля, только для Организации + name = models.CharField(u'Краткое название организации', max_length=256, default='', + help_text=u'Будет подставляться в создаваемые документы.') + full_name = models.CharField(u'Полное название организации', max_length=256, blank=True, default='', + help_text=u'Как в учредительных документах.') + + kpp = models.CharField(u'КПП', max_length=9, default='') + + jur_address = models.CharField(u'Юридический (почтовый) адрес', max_length=256, blank=True, default='', + help_text=u'Как в учредительных документах.') + + boss_title = models.CharField(u'Должность руководителя', max_length=256, blank=True, default='') + na_osnovanii = models.CharField(u'Действует на основании', max_length=256, blank=True, default='') + + # подписи, печать и логотип + boss_sign = models.ImageField(u'Подпись руководителя', blank=True, default='', + upload_to=upload_to(PROFILE_IMAGES_UPLOAD_DIR, 'boss_sign.bmp')) + glavbuh_sign = models.ImageField(u'Подпись бухгалтера', blank=True, default='', + upload_to=upload_to(PROFILE_IMAGES_UPLOAD_DIR, 'glavbuh_sign.bmp')) + stamp = models.ImageField(u'Печать', blank=True, default='', + upload_to=upload_to(PROFILE_IMAGES_UPLOAD_DIR, 'stamp.bmp')) + logo = models.ImageField(u'Логотип', blank=True, default='', + upload_to=upload_to(PROFILE_IMAGES_UPLOAD_DIR, 'logo.bmp')) + + created_at = models.DateTimeField(u'Создан', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + objects = managers.UserProfileManager() + + class Meta: + verbose_name = u'Реквизиты (профиль)' + verbose_name_plural = u'Реквизиты (профили)' + + def __unicode__(self): + return u'%s, %s, ИНН %s' % (self.user.email, self.get_company_name()[0:30], self.inn or u'не указан') + + def save(self, *args, **kwargs): + def process_img(orig_img, size): + w = orig_img.width + h = orig_img.height + if w > size[0] or h > size[1]: + filename = str(orig_img.path) + img = Image.open(filename).convert("RGB") + img.thumbnail(size, Image.ANTIALIAS) + img.save(filename, 'bmp') + + super(UserProfile, self).save(*args, **kwargs) + + if self.boss_sign: + process_img(self.boss_sign, size=BOSS_SIGN_IMG_SIZE) + + if self.glavbuh_sign: + process_img(self.glavbuh_sign, size=GLAVBUH_SIGN_IMG_SIZE) + + if self.stamp: + process_img(self.stamp, size=STAMP_IMG_SIZE) + + def is_ip(self): + return self.profile_type == consts.IP_PROFILE + + def is_org(self): + return self.profile_type == consts.ORG_PROFILE + + def get_company_name(self): + """`ИП ФИО` или `Название Организации`.""" + if self.profile_type == consts.IP_PROFILE: + return u'ИП %s' % self.get_boss_full_fio() + elif self.profile_type == consts.ORG_PROFILE: + return self.name.strip() + return u'' + + def get_inn_and_kpp(self): + """Возвращает пару ИНН/КПП или только ИНН, если это ИП или КПП не заполнен.""" + if self.profile_type == consts.ORG_PROFILE: + kpp = self.kpp.strip() + if kpp: + return u'%s/%s' % (self.inn, kpp,) + return self.inn + + def get_boss_title(self): + """Текст 'Индивидуальный предприниматель' или 'Руководитель организации'.""" + if self.profile_type == consts.IP_PROFILE: + return u'Индивидуальный предприниматель' + elif self.profile_type == consts.ORG_PROFILE: + return u'Руководитель организации' + return u'' + + def get_boss_fio(self): + """Фамилия и инициалы руководителя ИП/организации.""" + if self.boss_surname and self.boss_name and self.boss_midname: + return u'%s %s.%s.' % (self.boss_surname, self.boss_name[0], self.boss_midname[0],) + return u'' + + def get_boss_full_fio(self): + """Полное ФИО руководителя ИП/организации.""" + return (u'%s %s %s' % (self.boss_surname, self.boss_name, self.boss_midname,)).strip() + + def get_glavbuh_fio(self): + """Фамилия и инициалы главного бухгалтера.""" + if self.glavbuh_surname and self.glavbuh_name and self.glavbuh_midname: + return (u'%s %s. %s.' % (self.glavbuh_surname, self.glavbuh_name[0], self.glavbuh_midname[0],)).strip() + return u'' + + def get_glavbuh_full_fio(self): + """Полное ФИО главного бухгалтера.""" + return (u'%s %s %s' % (self.glavbuh_surname, self.glavbuh_name, self.glavbuh_midname,)).strip() + + def get_full_phone(self): + """(Код города) Номер телефона.""" + phone_code = self.phone_code.strip('() ') + phone_code = u'(%s)' % phone_code if phone_code else phone_code + return (u'%s %s' % (phone_code, self.phone,)).strip() + + def get_full_fax(self): + """(Код города) Номер факса.""" + fax_code = self.fax_code.strip('() ') + fax_code = u'(%s)' % fax_code if fax_code else fax_code + return (u'%s %s' % (fax_code, self.fax,)).strip() + + +class BankAccount(models.Model): + """Расчетные счета.""" + user = models.ForeignKey(User, related_name='bank_accounts') + + bik = models.CharField(u'БИК', max_length=10) + name = models.CharField(u'Наименование банка', max_length=256) + address = models.CharField(u'Местонахождение', max_length=256) + korr_account = models.CharField(u'Корр. счет', max_length=20) + account = models.CharField(u'Расчетный счет', max_length=20) + + is_main = models.BooleanField(u'Основной счет', default=False) + + created_at = models.DateTimeField(u'Создан', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + objects = managers.BankAccountManager() + + class Meta: + verbose_name = u'Расчётный счет' + verbose_name_plural = u'Расчётные счета' + ordering = ['-created_at'] + + def __unicode__(self): + return (u'%s, %s' % (self.account, self.name[0:30],)).strip() + + def save(self, *args, **kwargs): + super(BankAccount, self).save(*args, **kwargs) + if self.is_main: + # если задано, что это будет основной счет, то сбросить у остальных счетов пользователя этот признак + BankAccount.objects.filter(user=self.user, is_main=True).exclude(pk=self.pk).update(is_main=False) + else: + # если нет основного счета, то установить его принудительно + BankAccount.objects.force_main(user=self.user) + + def delete(self, *args, **kwargs): + super(BankAccount, self).delete(*args, **kwargs) + # если нет основного счета, то установить его принудительно + BankAccount.objects.force_main(user=self.user) + + +class Client(models.Model): + """Контрагенты.""" + user = models.ForeignKey(User, related_name='clients') + + name = models.CharField(u'Наименование', max_length=256, db_index=True) + inn = models.CharField(u'ИНН', max_length=12) + kpp = models.CharField(u'КПП', max_length=9, blank=True, default='') # Организация + okpo = models.CharField(u'ОКПО', max_length=10, blank=True, default='') # ИП + address = models.CharField(u'Юр. адрес', max_length=256) + + # банковские реквизиты + bank_bik = models.CharField(u'БИК', max_length=10, blank=True, default='') + bank_name = models.CharField(u'Наименование банка', max_length=256, blank=True, default='') + bank_address = models.CharField(u'Местонахождение', max_length=256, blank=True, default='') + bank_korr_account = models.CharField(u'Корр. счет', max_length=20, blank=True, default='') + bank_account = models.CharField(u'Расчетный счет', max_length=20, blank=True, default='') + + # контакты + contact_name = models.CharField(u'Имя', max_length=50, blank=True, default='') + contact_email = models.EmailField(u'E-mail', max_length=50, blank=True, default='') + contact_phone = models.CharField(u'Телефон', max_length=50, blank=True, default='') + contact_icq = models.CharField(u'ICQ', max_length=20, blank=True, default='') + contact_skype = models.CharField(u'Skype', max_length=20, blank=True, default='') + contact_other = models.CharField(u'Другое', max_length=256, blank=True, default='') + + created_at = models.DateTimeField(u'Создан', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + objects = managers.ClientManager() + + class Meta: + verbose_name = u'Контрагент' + verbose_name_plural = u'Контрагенты' + ordering = ['name', '-created_at'] + + def __unicode__(self): + return (u'%s, ИНН %s' % (self.name[0:30], self.inn or u'не указан',)).strip() + + def get_inn_and_kpp(self): + """Возвращает пару ИНН/КПП или только ИНН, если КПП не заполнен.""" + kpp = self.kpp.strip() + if kpp: + return u'%s/%s' % (self.inn, kpp,) + return self.inn + + +class UserProfileFilters(models.Model): + """Фильтрация реквизитов: какие данные показывать/скрывать при генерации карточки компании.""" + user = models.OneToOneField(User, related_name='profile_filters', primary_key=True) + + # общие фильтры + show_profile_type = models.BooleanField(u'Тип профиля', default=True) + + show_inn = models.BooleanField(u'ИНН', default=True) + show_ogrn = models.BooleanField(u'ОГРН/ОГРНИП', default=True) + show_okpo = models.BooleanField(u'ОКПО', default=True) + + show_glavbuh = models.BooleanField(u'Главный бухгалтер', default=True) + + show_bank_account = models.BooleanField(u'Банковские реквизиты', default=True) + bank_account = models.ForeignKey(BankAccount, related_name='+', verbose_name=u'Расчетный счет', blank=True, + null=True, default=None) + + show_contact_info = models.BooleanField(u'Контактная информация', default=True) + show_real_address = models.BooleanField(u'Фактический адрес', default=True) + show_phone = models.BooleanField(u'Телефон', default=True) + show_fax = models.BooleanField(u'Факс', default=True) + show_email = models.BooleanField(u'Электронная почта', default=True) + show_site = models.BooleanField(u'Сайт', default=True) + + # только для ИП + show_ip_boss_fio = models.BooleanField(u'Фамилия, Имя, Отчество', default=True) + show_svid_gos_reg = models.BooleanField(u'Свид-во о гос. регистрации', default=True) + show_ip_reg_date = models.BooleanField(u'Дата регистрации ИП', default=True) + + # только для Организации + show_name = models.BooleanField(u'Краткое название организации', default=True) + show_full_name = models.BooleanField(u'Полное название организации', default=True) + show_kpp = models.BooleanField(u'КПП', default=True) + show_org_boss_title_and_fio = models.BooleanField(u'Руководитель (Должность, ФИО)', default=True) + show_na_osnovanii = models.BooleanField(u'Действует на основании', default=True) + show_jur_address = models.BooleanField(u'Юридический адрес', default=True) + + objects = managers.UserProfileFiltersManager() + + class Meta: + verbose_name = u'Фильтры реквизитов' + verbose_name_plural = u'Фильтры реквизитов' + + def __unicode__(self): + return u'%s' % self.user.email + + def save(self, *args, **kwargs): + # всегда включены + self.show_ip_boss_fio = True + self.show_name = True + super(UserProfileFilters, self).save(*args, **kwargs) diff --git a/project/customer/tests.py b/project/customer/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/project/customer/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/project/customer/urls.py b/project/customer/urls.py new file mode 100644 index 0000000..78e6355 --- /dev/null +++ b/project/customer/urls.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from django.conf.urls import * + +from . import views +from .views import profile, profile_ajax +from .views import bank_accounts,bank_accounts_ajax +from .views import clients, clients_ajax + + +urlpatterns = patterns('', + # личный кабинет + url(r'^$', views.customer_index, name='customer_index'), + + # --- профиль + url(r'^profile/$', profile.profile_view, name='customer_profile_view'), + url(r'^profile/edit/$', profile.profile_edit, name='customer_profile_edit'), + url(r'^profile/email/$', profile.profile_email, name='customer_profile_email'), + + # --- профиль AJAX + url(r'^profile/filters/edit/ajax/$', profile_ajax.profile_filters_edit_ajax, name='customer_profile_filters_edit_ajax'), + url(r'^profile/email/ajax/$', profile_ajax.profile_email_ajax, name='customer_profile_email_ajax'), + + # --- расчетные счета + url(r'^bank-accounts/$', bank_accounts.bank_accounts_list, name='customer_bank_accounts_list'), + url(r'^bank-accounts/page/(?P[0-9]+)/$', bank_accounts.bank_accounts_list, name='customer_bank_accounts_list'), + url(r'^bank-accounts/add/$', bank_accounts.bank_accounts_add, name='customer_bank_accounts_add'), + url(r'^bank-accounts/(?P\d+)/edit/$', bank_accounts.bank_accounts_edit, name='customer_bank_accounts_edit'), + url(r'^bank-accounts/(?P\d+)/delete/$', bank_accounts.bank_accounts_delete, name='customer_bank_accounts_delete'), + + # --- расчетные счета AJAX + url(r'^bank-accounts/ajax/$', bank_accounts_ajax.bank_accounts_list_ajax, name='customer_bank_accounts_list_ajax'), + url(r'^bank-accounts/(?P\d+)/get/ajax/$', bank_accounts_ajax.bank_accounts_get_ajax, + name='customer_bank_accounts_get_ajax'), + url(r'^bank-accounts/add/ajax/$', bank_accounts_ajax.bank_accounts_add_ajax, name='customer_bank_accounts_add_ajax'), + url(r'^bank-accounts/(?P\d+)/edit/ajax/$', bank_accounts_ajax.bank_accounts_edit_ajax, + name='customer_bank_accounts_edit_ajax'), + url(r'^bank-accounts/(?P\d+)/delete/ajax/$', bank_accounts_ajax.bank_accounts_delete_ajax, + name='customer_bank_accounts_delete_ajax'), + + # --- контрагенты + url(r'^clients/$', clients.clients_list, name='customer_clients_list'), + url(r'^clients/page/(?P[0-9]+)/$', clients.clients_list, name='customer_clients_list'), + url(r'^clients/add/$', clients.clients_add, name='customer_clients_add'), + url(r'^clients/(?P\d+)/edit/$', clients.clients_edit, name='customer_clients_edit'), + url(r'^clients/(?P\d+)/delete/$', clients.clients_delete, name='customer_clients_delete'), + + # --- контрагенты AJAX + url(r'^clients/(?P\d+)/get/ajax/$', clients_ajax.clients_get_ajax, name='customer_clients_get_ajax'), + url(r'^clients/add/ajax/$', clients_ajax.clients_add_ajax, name='customer_clients_add_ajax'), + url(r'^clients/(?P\d+)/edit/ajax/$', clients_ajax.clients_edit_ajax, name='customer_clients_edit_ajax'), + url(r'^clients/(?P\d+)/delete/ajax/$', clients_ajax.clients_delete_ajax, name='customer_clients_delete_ajax'), +) diff --git a/project/customer/views/__init__.py b/project/customer/views/__init__.py new file mode 100644 index 0000000..2546eb6 --- /dev/null +++ b/project/customer/views/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render +from django.contrib.auth.decorators import login_required + + +@login_required +def customer_index(request): + """Личный кабинет.""" + template_name = 'customer/index.html' + return render(request, template_name) diff --git a/project/customer/views/bank_accounts.py b/project/customer/views/bank_accounts.py new file mode 100644 index 0000000..cb9f9f9 --- /dev/null +++ b/project/customer/views/bank_accounts.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render, redirect, get_object_or_404 +from django.core.urlresolvers import reverse +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required + +from project.commons.paginator import pagination, save_per_page_value + +from .. import models, forms + + +@login_required +@csrf_protect +@save_per_page_value +def bank_accounts_list(request, page_num=None): + """Список расчетных счетов пользователя.""" + template_name = 'customer/bank_accounts/list.html' + account_list = models.BankAccount.objects.get_all(request.user) + page, pagination_form = pagination(request, account_list, page_num) + return render(request, template_name, {'page': page, 'pagination_form': pagination_form,}) + + +@login_required +@csrf_protect +def bank_accounts_add(request): + """Добавить расчетный счет.""" + template_name='customer/bank_accounts/add.html' + form_class = forms.BankAccountForm + + success_url = 'customer_bank_accounts_list' + referer = request.POST.get('referer') + if referer and reverse('customer_profile_edit') in referer: + success_url = 'customer_profile_edit' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + if request.method == 'POST': + form = form_class(data=request.POST) + if form.is_valid(): + new_account = form.save(commit=False) + new_account.user = request.user + new_account.save() + return redirect(success_url) + else: + form = form_class() + + dictionary = { + 'form': form, + 'referer': request.META.get('HTTP_REFERER'), + } + return render(request, template_name, dictionary) + + +@login_required +@csrf_protect +def bank_accounts_edit(request, id): + """Редактировать расчетный счет.""" + template_name = 'customer/bank_accounts/edit.html' + form_class = forms.BankAccountForm + + success_url = 'customer_bank_accounts_list' + referer = request.POST.get('referer') + if referer and reverse('customer_profile_edit') in referer: + success_url = 'customer_profile_edit' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + account = get_object_or_404(models.BankAccount, pk=id, user=request.user) + + if request.method == 'POST': + form = form_class(data=request.POST, instance=account) + if form.is_valid(): + form.save() + return redirect(success_url) + else: + form = form_class(instance=account) + + dictionary = { + 'account': account, + 'form': form, + 'referer': request.META.get('HTTP_REFERER'), + } + return render(request, template_name, dictionary) + + +@login_required +@csrf_protect +def bank_accounts_delete(request, id): + """Удалить расчетный счет.""" + template_name='customer/bank_accounts/delete.html' + + success_url = 'customer_bank_accounts_list' + referer = request.POST.get('referer') + if referer and reverse('customer_profile_edit') in referer: + success_url = 'customer_profile_edit' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + account = get_object_or_404(models.BankAccount, pk=id, user=request.user) + + if request.method == 'POST': + account.delete() + # TODO обработать ошибки удаления + return redirect(success_url) + + dictionary = { + 'account': account, + 'referer': request.META.get('HTTP_REFERER'), + } + return render(request, template_name, dictionary) diff --git a/project/customer/views/bank_accounts_ajax.py b/project/customer/views/bank_accounts_ajax.py new file mode 100644 index 0000000..d468a7f --- /dev/null +++ b/project/customer/views/bank_accounts_ajax.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +import simplejson as json + +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseBadRequest, HttpResponse +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse + +from project.commons.utils import dthandler + +from .. import models, forms + + +@login_required +def bank_accounts_list_ajax(request): + """Список расчетных счетов пользователя - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + fields_list = ['pk', 'bik', 'name', 'address', 'korr_account', 'account', 'is_main',] + accounts = models.BankAccount.objects.get_all(user=request.user).values(*fields_list) + + for a in accounts: + a['edit_url'] = reverse('customer_bank_accounts_edit', kwargs={'id': a['pk'],}) + a['delete_url'] = reverse('customer_bank_accounts_delete', kwargs={'id': a['pk'],}) + + data = json.dumps(list(accounts), default=dthandler) + return HttpResponse(data, mimetype='application/json') + + +@login_required +def bank_accounts_get_ajax(request, id): + """Получить счёт - AJAX. + Если в форме редактирования счёта задан атрибут Meta.fields, то дампит только поля, перечисленные в нём. + Иначе дампит вообще все поля, которые есть в модели. + """ + if not request.is_ajax(): + return HttpResponseBadRequest() + + try: + fields_list = forms.BankAccountForm.Meta.fields + except AttributeError: + fields_list = [] + + account = get_object_or_404(models.BankAccount.objects.values(*fields_list), pk=id, user=request.user) + + data = json.dumps(account, default=dthandler) + return HttpResponse(data, mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def bank_accounts_add_ajax(request): + """Добавить расчетный счет - AJAX.""" + form_class = forms.BankAccountForm + + if not request.is_ajax(): + return HttpResponseBadRequest() + + form = form_class(data=request.POST) + if form.is_valid(): + new_account = form.save(commit=False) + new_account.user = request.user + new_account.save() + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + } + return HttpResponse(json.dumps(data), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def bank_accounts_edit_ajax(request, id): + """Редактировать расчетный счет - AJAX.""" + form_class = forms.BankAccountForm + + if not request.is_ajax(): + return HttpResponseBadRequest() + + account = get_object_or_404(models.BankAccount, pk=id, user=request.user) + + form = form_class(data=request.POST, instance=account) + if form.is_valid(): + form.save() + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + } + return HttpResponse(json.dumps(data), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def bank_accounts_delete_ajax(request, id): + """Удалить расчетный счет - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + account = get_object_or_404(models.BankAccount, pk=id, user=request.user) + account.delete() + + # TODO обработать ошибки удаления + data = { + 'success': True, + 'message': {'title': 'Инфо', 'msg': 'Расчётный счёт удалён.',}, + } + return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/project/customer/views/clients.py b/project/customer/views/clients.py new file mode 100644 index 0000000..bd3d65b --- /dev/null +++ b/project/customer/views/clients.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required + +from project.commons.paginator import pagination, save_per_page_value + +from .. import models, forms + + +@login_required +@csrf_protect +@save_per_page_value +def clients_list(request, page_num=None): + """Список контрагентов пользователя.""" + template_name='customer/clients/list.html' + + client_list = models.Client.objects.filter(user=request.user).order_by('name', '-created_at') + page, pagination_form = pagination(request, client_list, page_num) + + client_form = forms.ClientForm() + + dictionary = { + 'page': page, + 'pagination_form': pagination_form, + 'client_form': client_form, + } + return render(request, template_name, dictionary) + + +@login_required +@csrf_protect +def clients_add(request): + """Добавить контрагента.""" + template_name='customer/clients/add.html' + success_url = 'customer_clients_list' + form_class = forms.ClientForm + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + if request.method == 'POST': + form = form_class(data=request.POST) + if form.is_valid(): + new_client = form.save(commit=False) + new_client.user = request.user + new_client.save() + return redirect(success_url) + else: + form = form_class() + + return render(request, template_name, {'form': form,}) + + +@login_required +@csrf_protect +def clients_edit(request, id): + """Редактировать контрагента.""" + template_name='customer/clients/edit.html' + success_url = 'customer_clients_list' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + form_class = forms.ClientForm + + client = get_object_or_404(models.Client, pk=id, user=request.user) + + if request.method == 'POST': + form = form_class(data=request.POST, instance=client) + if form.is_valid(): + form.save() + return redirect(success_url) + else: + form = form_class(instance=client) + + dictionary = { + 'client': client, + 'form': form, + } + return render(request, template_name, dictionary) + + +@login_required +@csrf_protect +def clients_delete(request, id): + """Удалить контрагента.""" + template_name='customer/clients/delete.html' + success_url = 'customer_clients_list' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + client = get_object_or_404(models.Client, pk=id, user=request.user) + + if request.method == 'POST': + client.delete() + # TODO обработать ошибки удаления + return redirect(success_url) + + return render(request, template_name, {'client': client,}) diff --git a/project/customer/views/clients_ajax.py b/project/customer/views/clients_ajax.py new file mode 100644 index 0000000..ec16fca --- /dev/null +++ b/project/customer/views/clients_ajax.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +import simplejson as json + +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseBadRequest, HttpResponse +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required + +from .. import models, forms + + +@login_required +def clients_get_ajax(request, id): + """Получить контрагента - AJAX. + Если в форме редактирования контрагента задан атрибут Meta.fields, то дампит только поля, перечисленные в нём. + Иначе дампит вообще все поля, которые есть в модели. + """ + if not request.is_ajax(): + return HttpResponseBadRequest() + + try: + fields_list = forms.ClientForm.Meta.fields + except AttributeError: + fields_list = [] + + client = get_object_or_404(models.Client.objects.values(*fields_list), pk=id, user=request.user) + + return HttpResponse(json.dumps(client), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def clients_add_ajax(request): + """Добавить контрагента - AJAX.""" + form_class = forms.ClientForm + + if not request.is_ajax(): + return HttpResponseBadRequest() + + form = form_class(data=request.POST) + if form.is_valid(): + new_client = form.save(commit=False) + new_client.user = request.user + new_client.save() + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + 'reload': form.is_valid() and 'reload_on_success' in request.GET + } + return HttpResponse(json.dumps(data), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def clients_edit_ajax(request, id): + """Редактировать контрагента - AJAX.""" + form_class = forms.ClientForm + + if not request.is_ajax(): + return HttpResponseBadRequest() + + client = get_object_or_404(models.Client, pk=id, user=request.user) + + form = form_class(data=request.POST, instance=client) + if form.is_valid(): + form.save() + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + 'reload': form.is_valid() and 'reload_on_success' in request.GET + } + return HttpResponse(json.dumps(data), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def clients_delete_ajax(request, id): + """Удалить контрагента - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + client = get_object_or_404(models.Client, pk=id, user=request.user) + client.delete() + + # TODO обработать ошибки удаления + data = { + 'success': True, + 'message': {'title': 'Инфо', 'msg': 'Контрагент удалён.',}, + 'reload': 'reload_on_success' in request.GET + } + return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/project/customer/views/profile.py b/project/customer/views/profile.py new file mode 100644 index 0000000..ff8f391 --- /dev/null +++ b/project/customer/views/profile.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +from email.header import Header + +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required +from django.template.loader import render_to_string +from django.core.mail import EmailMessage +from django.utils.encoding import smart_str +from django.conf import settings + +from project.commons.pdf_tools import render_pdf_to_string, pdf_to_response + +from .. import models, forms + + +PDF_PROFILE_NAME = u'Реквизиты.pdf' +SUPPORT_EMAIL = getattr(settings, 'SUPPORT_EMAIL', '') + + +# ----------------------------------------------------------------------------- + +@login_required +@csrf_protect +def profile_view(request): + """Просмотр профиля пользователя, фильтрация реквизитов, скачать/отправить реквизиты по почте.""" + template_name = 'customer/profile/view.html' + + profile = get_object_or_404(models.UserProfile, user=request.user) + accounts = models.BankAccount.objects.get_all(request.user) + + filters_form_class = forms.get_profile_filters_form_class(profile.profile_type) + filters = models.UserProfileFilters.objects.get_or_create_filters(user=request.user) + + if request.method == 'POST': + filters_form = filters_form_class(data=request.POST, instance=filters, profile=profile, accounts=accounts) + if filters_form.is_valid(): + filters = filters_form.save() + + if 'download-pdf' in request.POST: + #return _profile_get_pdf(request, profile, filters.bank_account, filters) # для отладки + return profile_as_pdf(request, profile, filters.bank_account, filters) + elif 'email-pdf' in request.POST: + return redirect('customer_profile_email') + + return redirect('customer_profile_view') # редирект на себя, чтобы не сабмитили форму по F5 + else: + filters_form = filters_form_class(instance=filters, label_suffix='', profile=profile, accounts=accounts) + + dictionary = { + 'profile': profile, + 'accounts': accounts, + 'filters_form': filters_form, + 'email_profile_form': forms.EmailProfileForm(), + } + return render(request, template_name, dictionary) + + +@login_required +@csrf_protect +def profile_edit(request): + """Редактировать профиль пользователя.""" + template_name = 'customer/profile/edit.html' + success_url = 'customer_profile_view' + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect(success_url) + + profile = get_object_or_404(models.UserProfile, user=request.user) + form_class = forms.get_profile_form_class(profile.profile_type) + + accounts = models.BankAccount.objects.get_all(request.user) + bank_account_form = forms.BankAccountForm() + + if request.method == 'POST': + form = form_class(data=request.POST, instance=profile) + if form.is_valid(): + form.save() + return redirect(success_url) + else: + form = form_class(instance=profile) + + dictionary = { + 'form': form, + 'profile': profile, + 'accounts': accounts, + 'bank_account_form': bank_account_form, + } + return render(request, template_name, dictionary) + + +@login_required +def _profile_get_pdf(request, profile=None, account=None, filters=None): + """Создать профиль пользователя в PDF и вернуть как строку.""" + template_name = 'customer/profile/as_pdf.html' + dictionary = { + 'profile': profile, + 'account': account, + 'filters': filters, + } + return render_pdf_to_string(request, template_name, dictionary) + + +@login_required +def profile_as_pdf(request, profile=None, account=None, filters=None): + """Вывести профиль пользователя в формате PDF в HttpResponse.""" + pdf = _profile_get_pdf(request, profile, account, filters) + return pdf_to_response(pdf, PDF_PROFILE_NAME) + + +def _send_profile_email(subject, to, body, pdf_content): + """Отправка письма.""" + template_name = 'customer/profile/profile_email.txt' + dict_context = {'body': body, 'support_email': SUPPORT_EMAIL,} + email_body = render_to_string(template_name, dict_context) + email = EmailMessage( + subject=subject, + to=(to,), + body=email_body, + attachments = [(smart_str(Header(PDF_PROFILE_NAME, 'cp1251')), pdf_content, 'application/pdf'),] + ) + return email.send() + + +@login_required +@csrf_protect +def profile_email(request): + """Форма отправки профиля пользователя на email аттачем в PDF.""" + template_name = 'customer/profile/email.html' + success_url = 'customer_profile_view' + + form_class = forms.EmailProfileForm + + if request.method == 'POST' and '_cancel' in request.POST: + return redirect('customer_profile_view') + + profile = get_object_or_404(models.UserProfile, user=request.user) + filters = models.UserProfileFilters.objects.get_or_create_filters(user=request.user) + + if request.method == 'POST': + form = form_class(data=request.POST) + if form.is_valid(): + _send_profile_email( + subject = u'Реквизиты %s' % profile.get_company_name(), + to = form.cleaned_data['to'], + body = form.cleaned_data['body'], + pdf_content = _profile_get_pdf(request, profile, filters.bank_account, filters) + ) + return redirect(success_url) + else: + form = form_class() + + return render(request, template_name, {'form': form, 'profile': profile,}) + + +#@login_required +#@csrf_protect +#def profile_settings(request): +# """Редактировать настройки пользователя.""" +# template_name='customer/profile/settings.html' +# +# profile = get_object_or_404(models.UserProfile, user=request.user) +# form_class = forms.UserProfileSettingsForm #TODO remove this view +# +# # пути к уже загруженным подписям/штампу +# curr_files = {'boss_sign': None, 'glavbuh_sign': None, 'stamp': None,} +# if profile.boss_sign: +# curr_files['boss_sign'] = profile.boss_sign.path +# if profile.glavbuh_sign: +# curr_files['glavbuh_sign'] = profile.glavbuh_sign.path +# if profile.stamp: +# curr_files['stamp'] = profile.stamp.path +# +# if request.method == "POST" and '_cancel' not in request.POST: +# post = request.POST +# files = request.FILES +# form = form_class(user=request.user, data=post, files=files, instance=profile) +# +# if form.is_valid(): +# def delete_file(path): +# """Удалить файл. Если ошибка - сообщить в консоль.""" +# try: +# os.remove(path) +# except: +# print "Can't delete file:", path +# # --- удалить старые файлы +# for field, path in curr_files.iteritems(): +# if not path: +# continue +# # если стоит галочка 'очистить' +# if '%s-clear' % field in post: +# delete_file(path) +# continue +# # если загружен новый файл +# if field in files: +# delete_file(path) +# continue +# # --- изменить пароль +# if 'new_password1' in post: +# request.user.set_password(post.get('new_password1')) +# request.user.save() +# messages.add_message(request, messages.INFO, u'Пароль успешно изменен.') +# +# form.save() +# return redirect('customer_profile_settings') +# else: +# form = form_class(user=request.user, instance=profile) +# +# return render(request, template_name, {'profile': profile, 'form': form,}) diff --git a/project/customer/views/profile_ajax.py b/project/customer/views/profile_ajax.py new file mode 100644 index 0000000..7ba0424 --- /dev/null +++ b/project/customer/views/profile_ajax.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import simplejson as json + +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseBadRequest, HttpResponse +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_protect +from django.contrib.auth.decorators import login_required + +from .. import models, forms + +from .profile import _send_profile_email, _profile_get_pdf + + +@login_required +@require_POST +@csrf_protect +def profile_filters_edit_ajax(request): + """Сохранить фильтры реквизитов профиля - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + profile = get_object_or_404(models.UserProfile, user=request.user) + accounts = models.BankAccount.objects.get_all(request.user) + + filters_form_class = forms.get_profile_filters_form_class(profile.profile_type) + filters = models.UserProfileFilters.objects.get_or_create_filters(user=request.user) + + form = filters_form_class(data=request.POST, instance=filters, profile=profile, accounts=accounts) + if form.is_valid(): + form.save() + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + } + return HttpResponse(json.dumps(data), mimetype='application/json') + + +@login_required +@require_POST +@csrf_protect +def profile_email_ajax(request): + """Обработка формы отправки профиля пользователя на email аттачем в PDF - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + form_class = forms.EmailProfileForm + + profile = get_object_or_404(models.UserProfile, user=request.user) + filters = models.UserProfileFilters.objects.get_or_create_filters(user=request.user) + + form = form_class(data=request.POST) + if form.is_valid(): + _send_profile_email( + subject = u'Реквизиты %s' % profile.get_company_name(), + to = form.cleaned_data['to'], + body = form.cleaned_data['body'], + pdf_content = _profile_get_pdf(request, profile, filters.bank_account, filters) + ) + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + } + return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/project/docs/__init__.py b/project/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/docs/as_xls/__init__.py b/project/docs/as_xls/__init__.py new file mode 100644 index 0000000..8cb5cdc --- /dev/null +++ b/project/docs/as_xls/__init__.py @@ -0,0 +1 @@ +from .render_to_xls import render_xls_to_string diff --git a/project/docs/as_xls/render_to_xls.py b/project/docs/as_xls/render_to_xls.py new file mode 100644 index 0000000..ebf02ad --- /dev/null +++ b/project/docs/as_xls/render_to_xls.py @@ -0,0 +1,521 @@ +# -*- coding: utf-8 -*- +import os +import re +import math +from StringIO import StringIO + +import xlrd +import xlwt + +from django.conf import settings +from django.template import (Template, RequestContext, BLOCK_TAG_START, BLOCK_TAG_END, + VARIABLE_TAG_START, VARIABLE_TAG_END) + +from project.commons.xls import (get_xlwt_style_list, copy_cells, width_cols, horz_page_break, mm_to_twips, + sum_src_heights, sum_dst_heights) + + +TAG_RE = re.compile('(%s.*?%s|%s.*?%s)' % ( + re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), + re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END) +)) + +XLS_ROOT = getattr(settings, 'XLS_ROOT', '/') + +DEBUG = getattr(settings, 'DEBUG', False) + + +def render_xls_to_string(request, xls_template, dictionary=None): + """Создает по шаблону новую книгу Excel. + Возвращает строку, в которой сожержится сгенерированный Excel. + """ + src_xls = os.path.join(XLS_ROOT, xls_template) # файл шаблона + src_book = None + try: + # откуда + src_book = xlrd.open_workbook(src_xls, encoding_override='cp1251', + on_demand=True, formatting_info=True) + src_sheet = src_book.sheet_by_index(0) + + # достать список стилей + style_list = get_xlwt_style_list(src_book) + + # куда + dst_book = xlwt.Workbook(encoding='cp1251', style_compression=2) + dst_sheet = dst_book.add_sheet(src_sheet.name, cell_overwrite_ok=True) + + # настройки + xls_settings = get_settings(src_book) + apply_page_settings(dst_sheet, xls_settings) + + # заполнить данными + fill_xls(request, dictionary, src_sheet, dst_sheet, style_list, + xls_settings) + + # закрыть исходную книгу и сохранить созданную + src_book.release_resources() + f=StringIO() + dst_book.save(f) + xls_content = f.getvalue() + f.close() + return xls_content + except: + if src_book and not src_book._resources_released: + src_book.release_resources() + raise + + +def fill_xls(request, dictionary, src_sheet, dst_sheet, style_list, xls_settings): + """Заполнить контентом новый лист Excel. + """ + obj_items = dictionary['obj_items'] + + context = RequestContext(request, dictionary) + + # ------------------------------------------------------------------------- + + def write(row, col, val, src_row=None, src_col=None, commands=None): + """Записывает данные в ячейку с сохранением стилей.""" + src_row = src_row or row + src_col = src_col or col + style = style_list[src_sheet.cell(src_row, src_col).xf_index] + if commands: + if commands.get('draw_thin_bottom_border'): + style.borders.bottom = xlwt.Borders.THIN + dst_sheet.write(row, col, val, style) + + def process_template(template_string, **kwargs): + """Передать шаблон на обработку django.template.Template.""" + template = Template(template_string) + context.update(kwargs) + return template.render(context) + + def parse_cells(row_from=0, row_to=None, col_from=0, col_to=None, + dst_row_shift=0, dst_col_shift=0, **kwargs): + """Ищет шаблонные теги и переменные в ячейках заданного диапазона. Если находит, то передает содержимое ячейки + целиком на обработку в process_template. После чего записывает полученный результат обратно в ячейку. + Также ищет спец. токены и выполняет соответствующие действия. + """ + row_to = row_to or src_sheet.nrows-1 + col_to = col_to or src_sheet.ncols-1 + + for row in xrange(row_from, row_to+1): + cmd_fix_height = [] + + for col in xrange(col_from, col_to+1): + cell = src_sheet.cell(row, col) + cell_value = new_value = cell.value + + cmd_as_float = False + cmd_show_bmp = False + cmd_draw_thin_bottom_border = False + + # если в ячейке не строка - пропускаем + if not isinstance(new_value, unicode): + continue + + # поискать шаблонные теги и переменные в ячейке + if TAG_RE.search(new_value): + # передать на обработку + new_value = process_template(new_value, **kwargs) + + # пофиксить переводы строки + #new_value = new_value.strip().replace('\r\n', '\n') + new_value = new_value.strip().replace('\r\n', ' ') + + # команда 'конвертировать во float' + if u'@@AS_FLOAT@@' in new_value: + new_value = new_value.replace(u'@@AS_FLOAT@@', u'') + cmd_as_float = True + + # команда 'вставить изображение из bmp-файла' + if u'@@SHOW_BMP@@' in new_value: + new_value = new_value.replace(u'@@SHOW_BMP@@', u'') + cmd_show_bmp = True + + # команда 'нарисовать тонкую рамку внизу ячейки' + if u'@@DRAW_THIN_BOTTOM_BORDER@@' in new_value: + new_value = new_value.replace( + u'@@DRAW_THIN_BOTTOM_BORDER@@', u'') + cmd_draw_thin_bottom_border = True + + # команда 'подобрать высоту строки в ячейке' + if u'@@FIX_HEIGHT@@' in new_value: + new_value = new_value.replace(u'@@FIX_HEIGHT@@', u'') + cmd_fix_height.append({ + 'col': col, + 'value': unicode(new_value), + }) + + if new_value != cell_value: + # если надо, попробовать привести к float, чтоб нормально сохранилось в Excel + if cmd_as_float and not cmd_show_bmp: + try: + new_value = float(new_value.replace(',', '.', 1)) + except: + pass + + # записать новое значение, если оно отличается от прочитанного + if new_value != cell_value: + if cmd_show_bmp and new_value: + try: + dst_sheet.insert_bitmap( + new_value, + row = row + dst_row_shift, + col = col + dst_col_shift, + ) + new_value = '' + except: + if DEBUG: + raise + else: + print "Error inserting image from file '%s'" % new_value + write( + row = row + dst_row_shift, + col = col + dst_col_shift, + val = new_value, + src_row = row, + src_col = col, + commands = {'draw_thin_bottom_border': cmd_draw_thin_bottom_border,} + ) + + # --- конец цикла по ячейкам в строке + + # подобрать высоту строки в ячейках + dst_row = row + dst_row_shift # строка назначения + row_height = dst_sheet.row(dst_row).height # текущая высота + max_height = 0 + for fh in cmd_fix_height: + #print '---FIX_HEIGHT:', 'dst_row=', dst_row, 'col=', fh['col'] + # взять ширину ячейки + width = 0 + # учитываем только объединенные ячейки + for r1,r2,c1,c2 in src_sheet.merged_cells: + if r1 != row or c1 != fh['col']: + continue + for colx in xrange(c1, c2): + width += src_sheet.computed_column_width(colx) + else: + break + + # хотя бы приблизительный расчет, т.к. точный невозможен. + # формулы тестировались для Times New Roman, 9pt. + # для других шрифтов/размеров могут и не подойти + width_in_pixels = width / 36.5 + width_in_chars = width_in_pixels / 5.8 + + # может быть 0, если команда @@FIX_HEIGHT@@ задана в простой (не объединенной) ячейке + if width_in_chars == 0: + print ('WARNING. xls generation, cmd @@FIX_HEIGHT@@. ' + 'variable `width_in_chars` = %s. skip this command.' % width_in_chars) + continue + + # сколько строк под текст + value = fh['value'] + min_rows = 1 + need_rows = math.ceil(len(value) / width_in_chars) + need_rows = int(max(min_rows, need_rows)) + #print 'need_rows=', need_rows + + new_height = row_height * need_rows + # не фиксить высоту, если новая высота данной ячейки меньше либо равна текущей высоте + if new_height > max_height: + max_height = new_height + else: + #print 'SKIP,', new_height, '<=', max_height + continue + + dst_sheet.row(dst_row).height = new_height + dst_sheet.row(dst_row).height_mismatch = True + + # ------------------------------------------------------------------------- + + # задать ширину колонок - достаточно один раз + width_cols(src_sheet, dst_sheet) + + # у документа нет табличной части. заполнить целиком и сразу, после чего выйти. + if not obj_items or 'TBL_BODY_ROW' not in xls_settings: + copy_cells(src_sheet, dst_sheet, style_list) + parse_cells() + return + + # ---------------------------------------- у документа есть табличная часть + + # адъ констант! + p = get_all_these_boring_params(src_sheet, xls_settings) + if not p: + print 'Please set ALL required settings!' + return + + # --- !!! ------ вывести начало документа включительно по шапку табл. части + + copy_cells(src_sheet, dst_sheet, style_list, row_from=0, row_to=p.TBL_BODY_ROW-1) + + parse_cells(row_to=p.TBL_BODY_ROW-1) + + # для отладки - выйти здесь + #return + + # --- !!! ------------ вывести таблицу с учетом переходов на новую страницу + + # суммарная высота уже занятых строк на первой странице нового листа + curr_height = sum_dst_heights(dst_sheet, 0, p.TBL_HEADER_TO) + + # глобальное смещение на новом листе относительно листа исходного + add_offset = 0 + + just_wrote_page_footer = False + last_page_item_idx = 0 + row = 0 + row_shift = 0 + + #print '---table:' + + def write_tbl_body_row(): + """Хелпер для отрисовки строки таблицы. + Зависит от внешних переменных row_shift, row и item! + """ + #print '---table body row, dst_row_shift =', row_shift + copy_cells( + src_sheet, dst_sheet, style_list, + row_from = p.TBL_BODY_ROW, row_to = p.TBL_BODY_ROW, + dst_row_shift = row_shift + ) + parse_cells( + row_from = p.TBL_BODY_ROW, + row_to = p.TBL_BODY_ROW, + dst_row_shift = row_shift, + item = item, + item_npp = row+1 + ) + + def write_tbl_page_footer(): + """Хелпер для отрисовки подитога таблицы. + Зависит от внешних переменных row_shift, last_page_item_idx и row! + """ + dst_row_shift = row_shift - (p.TBL_PAGE_FOOTER_FROM - p.TBL_BODY_ROW) + #print '---table page footer, dst_row_shift =', dst_row_shift, \ + # 'items_start =', last_page_item_idx, 'items_stop =', row + copy_cells( + src_sheet, dst_sheet, style_list, + row_from = p.TBL_PAGE_FOOTER_FROM, row_to = p.TBL_PAGE_FOOTER_TO, + dst_row_shift = dst_row_shift + ) + parse_cells( + row_from = p.TBL_PAGE_FOOTER_FROM, + row_to = p.TBL_PAGE_FOOTER_TO, + dst_row_shift = dst_row_shift, + items_start = last_page_item_idx, + items_stop = row + ) + + def write_tbl_header(): + """Хелпер для отрисовки шапки таблицы. + Зависит от внешних переменных row и add_offset! + """ + dst_row_shift = p.TBL_HEADER_ROWS + row + add_offset + #print '---table header, dst_row_shift =', dst_row_shift + copy_cells( + src_sheet, dst_sheet, style_list, + row_from = p.TBL_HEADER_FROM, row_to = p.TBL_HEADER_TO, + dst_row_shift = dst_row_shift + ) + + # цикл по табличной части документа + for row, item in enumerate(obj_items): + row_shift = row + add_offset + #print 'row = %s, add_offset = %s' % (row, add_offset) + + write_tbl_body_row() + row_height = dst_sheet.row(p.TBL_BODY_ROW + row_shift).height + curr_height += row_height + + just_wrote_page_footer = False + + # строка, подитог и итог не помещаются на странице + if curr_height + p.TBL_PAGE_FOOTER_HEIGHT + p.TBL_FOOTER_HEIGHT > p.WORK_HEIGHT: + # если это первая строка, то: + if row == 0: + #print '---table new page, row =', row + # 1. добавить разрыв страницы перед первой шапкой + horz_page_break(dst_sheet, p.TBL_HEADER_FROM) + curr_height = p.TBL_HEADER_HEIGHT + row_height + # если это не последняя строка, то: + elif row < len(obj_items)-1: + #print '---table new page, row =', row + # 1. вместо строки вывести подитог + if p.TBL_PAGE_FOOTER_ROWS > 0: + write_tbl_page_footer() + last_page_item_idx = row + just_wrote_page_footer = True + add_offset += p.TBL_PAGE_FOOTER_ROWS + row_shift += add_offset + # 2. добавить разрыв страницы + horz_page_break(dst_sheet, (p.TBL_HEADER_FROM + p.TBL_HEADER_ROWS + row + add_offset)) + # 3. вывести шапку + write_tbl_header() + add_offset += p.TBL_HEADER_ROWS + curr_height = p.TBL_HEADER_HEIGHT + # 4. перевывести под ними строку + row_shift = row + add_offset + write_tbl_body_row() + curr_height += row_height + + else: # for ... else + # вывести подитог, если только что не выводили его в цикле + if p.TBL_PAGE_FOOTER_ROWS > 0 and not just_wrote_page_footer: + #print '---tbl last page, row =', row + row += 1 # чтоб захватить в подитог и последнюю запись тоже + row_shift = row + add_offset + write_tbl_page_footer() + curr_height += p.TBL_PAGE_FOOTER_HEIGHT + + add_offset += row - p.TBL_PAGE_FOOTER_ROWS + + #print '---end table' + + # для отладки - выйти здесь + #return + + # --- !!! --------------------------------------- вывести остаток документа + + copy_cells( + src_sheet, dst_sheet, style_list, + row_from = p.TBL_FOOTER_FROM, + dst_row_shift = add_offset + ) + + # добавить разрыв страницы, если остаток документа не уместится целиком + # на текущей странице + rest_height = sum_src_heights(src_sheet, p.TBL_FOOTER_TO, src_sheet.nrows) + if curr_height + rest_height > p.WORK_HEIGHT: + horz_page_break(dst_sheet, p.TBL_FOOTER_TO + add_offset + 1) + + parse_cells( + row_from = p.TBL_FOOTER_FROM, + dst_row_shift = add_offset + ) + + return + + +def get_settings(src_book, sheet_name=u'settings'): + """Загрузить настройки с листа settings.""" + settings = {} + + try: + src_sheet = src_book.sheet_by_name(sheet_name) + except xlrd.XLRDError: + return settings + + for row in xrange(src_sheet.nrows): + key_cell = src_sheet.cell(row, 0).value + if key_cell: + key_name = unicode(key_cell).strip() + if not key_name.startswith(u'#'): + val_cell = src_sheet.cell(row, 1).value + if (isinstance(val_cell, unicode) or + isinstance(val_cell, str)): + settings[key_name] = unicode(val_cell) + else: + settings[key_name] = val_cell + + return settings + + +def apply_page_settings(dst_sheet, settings): + """Применить параметры страницы.""" + def setparam(attr, key): + if key in settings: + setattr(dst_sheet, attr, settings[key]) + + def setparam_as_inch(attr, key): + if key in settings: + setattr(dst_sheet, attr, settings[key]/25.4) + + setparam('portrait', 'PAGE_PORTRAIT') + setparam('header_str', 'PAGE_HEADER_STR') + setparam('footer_str', 'PAGE_FOOTER_STR') + + # отступы, задаются в дюймах: 1 дюйм = 25.4 мм = 2.54 см + setparam_as_inch('top_margin', 'PAGE_TOP_MARGIN') + setparam_as_inch('bottom_margin', 'PAGE_BOTTOM_MARGIN') + setparam_as_inch('left_margin', 'PAGE_LEFT_MARGIN') + setparam_as_inch('right_margin', 'PAGE_RIGHT_MARGIN') + setparam_as_inch('header_margin', 'PAGE_HEADER_MARGIN') + setparam_as_inch('footer_margin', 'PAGE_FOOTER_MARGIN') + + +def get_all_these_boring_params(src_sheet, xls_settings): + """Достает нужные настройки из словаря и проверят, некоторые вычисляет - + и всё это складывает в класс, который потом и возвращает. + Если какие-то обязательные настройки не заданы, сообщает об этом в консоль и возвращает None. + """ + class Params(object): + pass + p = Params() + + # строка контента таблицы - обязательно + p.TBL_BODY_ROW = xls_settings.get('TBL_BODY_ROW') + if p.TBL_BODY_ROW is not None: + # перевести в int + p.TBL_BODY_ROW = int(p.TBL_BODY_ROW) + # высота в twips + p.TBL_BODY_HEIGHT = sum_src_heights(src_sheet, p.TBL_BODY_ROW, p.TBL_BODY_ROW) + else: + print u'Error! TBL_BODY_ROW not set!' + return None + + # шапка таблицы - обязательно + p.TBL_HEADER_FROM = xls_settings.get('TBL_HEADER_FROM') + p.TBL_HEADER_TO = xls_settings.get('TBL_HEADER_TO') + if p.TBL_HEADER_FROM is not None and p.TBL_HEADER_TO is not None: + # перевести в int + p.TBL_HEADER_FROM = int(p.TBL_HEADER_FROM) + p.TBL_HEADER_TO = int(p.TBL_HEADER_TO) + # высота в строках + p.TBL_HEADER_ROWS = int(p.TBL_HEADER_TO - p.TBL_HEADER_FROM + 1) + # высота в twips + p.TBL_HEADER_HEIGHT = sum_src_heights(src_sheet, p.TBL_HEADER_FROM, p.TBL_HEADER_TO) + else: + print u'Error! Either TBL_HEADER_FROM or TBL_HEADER_TO not set!' + return None + + # итого по таблице - обязательно + p.TBL_FOOTER_FROM = xls_settings.get('TBL_FOOTER_FROM') + p.TBL_FOOTER_TO = xls_settings.get('TBL_FOOTER_TO') + if p.TBL_FOOTER_FROM is not None and p.TBL_FOOTER_TO is not None: + # перевести в int + p.TBL_FOOTER_FROM = int(p.TBL_FOOTER_FROM) + p.TBL_FOOTER_TO = int(p.TBL_FOOTER_TO) + # высота в строках + p.TBL_FOOTER_ROWS = int(p.TBL_FOOTER_TO - p.TBL_FOOTER_FROM + 1) + # высота в twips + p.TBL_FOOTER_HEIGHT = sum_src_heights(src_sheet, p.TBL_FOOTER_FROM, p.TBL_FOOTER_TO) + else: + print u'Error! Either TBL_FOOTER_FROM or TBL_FOOTER_TO not set!' + return None + + # подитог (итого по странице) - опционально + p.TBL_PAGE_FOOTER_FROM = xls_settings.get('TBL_PAGE_FOOTER_FROM') + p.TBL_PAGE_FOOTER_TO = xls_settings.get('TBL_PAGE_FOOTER_TO') + if p.TBL_PAGE_FOOTER_FROM is not None and p.TBL_PAGE_FOOTER_TO is not None: + # перевести в int + p.TBL_PAGE_FOOTER_FROM = int(p.TBL_PAGE_FOOTER_FROM) + p.TBL_PAGE_FOOTER_TO = int(p.TBL_PAGE_FOOTER_TO) + # высота в строках + p.TBL_PAGE_FOOTER_ROWS = int(p.TBL_PAGE_FOOTER_TO - p.TBL_PAGE_FOOTER_FROM + 1) + # высота в twips + p.TBL_PAGE_FOOTER_HEIGHT = sum_src_heights(src_sheet, p.TBL_PAGE_FOOTER_FROM, p.TBL_PAGE_FOOTER_TO) + else: + p.TBL_PAGE_FOOTER_ROWS = 0 + p.TBL_PAGE_FOOTER_HEIGHT = 0 + + # высота рабочей области (высота страницы минус отступы сверху и снизу) + p.WORK_HEIGHT = mm_to_twips( + xls_settings.get('PAGE_HEIGHT', 210) - + xls_settings.get('PAGE_TOP_MARGIN', 0) - + xls_settings.get('PAGE_BOTTOM_MARGIN', 0) + ) + + return p diff --git a/project/docs/consts.py b/project/docs/consts.py new file mode 100644 index 0000000..d984ef5 --- /dev/null +++ b/project/docs/consts.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + + +BOOL_CHOICES = ( + (True, u'Да'), + (False, u'Нет'), +) + +# виды НДС +NDS_TYPE_NO = 1 +NDS_TYPE_IN = 2 +NDS_TYPE_OUT = 3 + +NDS_TYPE_CHOICES = ( + (NDS_TYPE_NO, u'Не учитывать'), + (NDS_TYPE_IN, u'В сумме'), + (NDS_TYPE_OUT, u'Сверх суммы'), +) + +# ставка НДС +NDS_VALUE_0 = 1 +NDS_VALUE_10 = 2 +NDS_VALUE_18 = 3 + +NDS_VALUE_CHOICES = ( + (NDS_VALUE_0, u'Без НДС'), + (NDS_VALUE_10, u'10%'), + (NDS_VALUE_18, u'18%'), +) + +# ставка НДС - число в формате Decimal (для расчетов) +NDS_VALUE_NUMERIC = { + NDS_VALUE_0: Decimal('0.00'), + NDS_VALUE_10: Decimal('10.00'), + NDS_VALUE_18: Decimal('18.00'), +} + +# ----------------------------------------------------------- для счетов-фактур + +# валюты +CURR_RUB = 1 +CURR_USD = 2 +CURR_EUR = 3 +CURR_OTHER = 4 + +CURRENCY_CHOICES = ( + (CURR_RUB, u'Руб.'), + (CURR_USD, u'USD'), + (CURR_EUR, u'EUR'), + (CURR_OTHER, u'Другое'), +) + +CURRENCY_CHOICES_DICT = dict(CURRENCY_CHOICES) + +# варианты для поля грузоотправитель +CONSIGNOR_TYPE_SELF = 1 +CONSIGNOR_TYPE_OTHER = 2 +CONSIGNOR_TYPE_NO = 3 + +CONSIGNOR_CHOICES = ( + (CONSIGNOR_TYPE_SELF, u'Подставить мои данные'), # из профиля через поле user + (CONSIGNOR_TYPE_OTHER, u'Стороннее лицо'), # из справочника контрагенты + (CONSIGNOR_TYPE_NO, u'Не указывать'), +) + +# варианты для поля грузополучатель +RECEIVER_TYPE_BUYER = 1 +RECEIVER_TYPE_OTHER = 2 +RECEIVER_TYPE_NO = 3 + +RECEIVER_CHOICES = ( + (RECEIVER_TYPE_BUYER, u'То же лицо'), # что и покупатель + (RECEIVER_TYPE_OTHER, u'Стороннее лицо'), # из справочника контрагенты + (RECEIVER_TYPE_NO, u'Не указывать'), +) + +# ----------------------------------------------------- для платежных поручений + +# тип платежного поручения +PLATEJ_TYPE_COMMERCE = 1 +PLATEJ_TYPE_TAX = 2 + +PLATEJ_TYPE_CHOICES = ( + (PLATEJ_TYPE_COMMERCE, u'Коммерческое'), + (PLATEJ_TYPE_TAX, u'Налоговое'), +) + +# вид платежа +PAYMENT_TYPE_CHOICES = ( + (1, u'Не указывать'), + (2, u'Срочно'), + (3, u'Электронно'), + (4, u'Почтой'), + (5, u'Телеграфом'), +) + +# статус составителя +TAX_STATUS_CHOICES = ( + (u'01', u'01 - налогоплательщик (плательщик сборов) - юридическое лицо'), + (u'02', u'02 - налоговый агент'), + (u'03', u'03 - сборщик налогов и сборов'), + (u'04', u'04 - налоговый орган'), + (u'05', u'05 - служба судебных приставов'), + (u'06', u'06 - участник внешнеэкономической деятельности'), + (u'07', u'07 - таможенный орган'), + (u'08', u'08 - плательщик иных обязательных платежей'), + (u'09', u'09 - налогоплательщик (плательщик сборов) - ИП'), + (u'10', u'10 - налогоплательщик (плательщик сборов) - частный нотариус'), + (u'11', u'11 - налогоплательщик (плательщик сборов) - адвокат'), + (u'12', u'12 - налогоплательщик (плательщик сборов) - глава КФХ'), + (u'13', u'13 - налогоплательщик (плательщик сборов) - иное физическое лицо'), + (u'14', u'14 - налогоплательщик, производящий выплаты физическим лицам'), + (u'15', u'15 - кредитная организация'), +) + +# основание налогового платежа +TAX_BASE = ( + (u'ТП', u'ТП - платежи текущего года'), + (u'ЗД', u'ЗД - добровольное погашение задолженности по истекшим налоговым периода'), + (u'БФ', u'БФ - текущие платежи физических лиц - клиентов банка (владельцев счета)'), + (u'ТР', u'ТР - погашение задолженности по требованию об уплате налогов (сборов) от налогового органа'), + (u'РС', u'РС - погашение рассроченной задолженности'), + (u'ОТ', u'ОТ - погашение отсроченной задолженности'), + (u'РТ', u'РТ - погашение реструктурируемой задолженности'), + (u'ВУ', u'ВУ - погашение отсроченной задолженности в связи с введением внешнего управления'), + (u'ПР', u'ПР - погашение задолженности, приостановленной к взысканию'), + (u'АП', u'АП - погашение задолженности по акту проверки'), + (u'АР', u'АР - погашение задолженности по исполнительному документу'), + ( u'0', u'0 - Конкретное значение указать невозможно'), +) + +# тип налогового платежа +TAX_TYPE = ( + (u'НС', u'НС - уплата налога или сбора'), + (u'ПЛ', u'ПЛ - уплата платежа'), + (u'ГП', u'ГП - уплата пошлины'), + (u'ВЗ', u'ВЗ - уплата взноса'), + (u'АВ', u'АВ - уплата аванса или предоплата (в том числе декадные платежи)'), + (u'ПЕ', u'ПЕ - уплата пени'), + (u'ПЦ', u'ПЦ - уплата процентов'), + (u'СА', u'СА - налоговые санкции, установленные Налоговым кодексом РФ'), + (u'АШ', u'АШ - административные штрафы'), + (u'ИШ', u'ИШ - иные штрафы, установленные соответствующими нормативными актами'), + ( u'0', u'0 - Конкретное значение указать невозможно'), +) diff --git a/project/docs/filters.py b/project/docs/filters.py new file mode 100644 index 0000000..30e38b2 --- /dev/null +++ b/project/docs/filters.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +import datetime + +import django_filters + +from project.customer.models import Client + +from .models import Invoice, Platejka +from . import consts + + +class CustomDateRangeFilter(django_filters.DateRangeFilter): + def __init__(self, *args, **kwargs): + try: + options = kwargs.pop('options') + self.options = options + except KeyError: + pass + kwargs['choices'] = [(key, value[0]) for key, value in self.options.iteritems()] + super(CustomDateRangeFilter, self).__init__(*args, **kwargs) + + +class CustomChoiceFilter(django_filters.ChoiceFilter): + def __init__(self, *args, **kwargs): + self.options = kwargs.pop('options') # обязательный параметр! + kwargs['choices'] = [(key, value[0]) for key, value in self.options.iteritems()] + super(CustomChoiceFilter, self).__init__(*args, **kwargs) + + def filter(self, qs, value): + try: + value = int(value) + except (ValueError, TypeError): + value = '' + return self.options[value][1](qs, self.name) + + +def _quarter_dates(q, year): + """Возвращает даты начала/окончания переданного квартала. + Также нужно передать год. + """ + if q == 1: + return datetime.date(year, 1, 1), datetime.date(year, 3, 31) + elif q == 2: + return datetime.date(year, 4, 1), datetime.date(year, 6, 30) + elif q == 3: + return datetime.date(year, 7, 1), datetime.date(year, 9, 30) + elif q ==4: + return datetime.date(year, 10, 1), datetime.date(year, 12, 31) + return None, None + + +def current_quarter(today): + """Возвращает даты начала/окончания текущего квартала.""" + q = (today.month-1)//3+1 + return _quarter_dates(q, today.year) + + +def last_quarter(today): + """Возвращает даты начала/окончания прошлого квартала.""" + q = (today.month-1)//3+1 + q -= 1 + year = today.year + if q < 1: # прошлый год + q = 4 + year -= 1 + return _quarter_dates(q, year) + + +today = datetime.datetime.now() +current_quarter_start, current_quarter_end = current_quarter(today) +last_quarter_start, last_quarter_end = last_quarter(today) + +# --- варианты фильтрации для разных полей + +doc_date_choices = { + '': (u'Всё время', lambda qs, name: qs.all()), + 1: (u'Этот месяц', lambda qs, name: qs.filter(**{ + '%s__year' % name: datetime.datetime.now().year, + '%s__month' % name: datetime.datetime.now().month + })), + 2: (u'Прошлый месяц', lambda qs, name: qs.filter(**{ + '%s__year' % name: datetime.datetime.now().year, + '%s__month' % name: datetime.datetime.now().month-1 + })), + 3: (u'Этот квартал', lambda qs, name: qs.filter(**{ + '%s__gte' % name: current_quarter_start, + '%s__lte' % name: current_quarter_end, + })), + 4: (u'Прошлый квартал', lambda qs, name: qs.filter(**{ + '%s__gte' % name: last_quarter_start, + '%s__lte' % name: last_quarter_end, + })), + 5: (u'Этот год', lambda qs, name: qs.filter(**{ + '%s__year' % name: datetime.datetime.now().year, + })), + 6: (u'Прошлый год', lambda qs, name: qs.filter(**{ + '%s__year' % name: datetime.datetime.now().year-1, + })), +} + +closed_status_choices = ( + ('', u'Все счета'), + (1, u'Закрытые'), + (0, u'Не закрытые'), +) + +paid_status_choices = ( + ('', u'Все счета'), + (Invoice.PAID, u'Оплаченные'), + (Invoice.PARTLY_PAID, u'Частично оплаченные'), + (Invoice.UNPAID, u'Неоплаченные'), +) + +signed_status_choices = ( + ('', u'Все документы'), + ('1', u'Подписанные'), + ('0', u'Не подписанные'), +) + +total_saldo_choices = { + '': (u'Любое', lambda qs, name: qs.all()), + 1: (u'Положительное', lambda qs, name: qs.filter(**{ + '%s__gt' % name: 0, + })), + 2: (u'Отрицательное', lambda qs, name: qs.filter(**{ + '%s__lt' % name: 0, + })), +} + +platej_type_choices = ( + ('', u'Все плат. поручения'), + (consts.PLATEJ_TYPE_COMMERCE, u'Коммерческие'), + (consts.PLATEJ_TYPE_TAX, u'Налоговые'), +) + + +class _BaseFilterSet(django_filters.FilterSet): + """Базовый класс фильтров. + Классы фильтров строить через build_filterset_class ! + """ + class Meta: + model = None + + def __init__(self, user, *args, **kwargs): + super(_BaseFilterSet, self).__init__(*args, **kwargs) + self.form.label_suffix = '' + + +def build_filterset_class(model, user, need_fields=None): + """Строит и возвращает класс с набором фильтров для фильтрации документов.""" + attrs = {} + fields = [] + + for f in need_fields: + if f == 'doc_date': + doc_date = CustomDateRangeFilter(label=u'По времени создания', options=doc_date_choices, + widget=django_filters.widgets.LinkWidget) + attrs['doc_date'] = doc_date + fields.append('doc_date') + + elif f == 'client': + client = django_filters.ModelChoiceFilter(label=u'По контрагенту', queryset=Client.objects.get_all(user), + empty_label=u'все контрагенты') + attrs['client'] = client + fields.append('client') + + elif f == 'invoice': + invoice = django_filters.ModelChoiceFilter(label=u'По счёту', queryset=Invoice.objects.get_all(user), + empty_label=u'все счета') + attrs['invoice'] = invoice + fields.append('invoice') + + elif f == 'closed_status': + closed_status = django_filters.ChoiceFilter(label=u'По закр. документам', choices=closed_status_choices, + widget=django_filters.widgets.LinkWidget) + attrs['closed_status'] = closed_status + fields.append('closed_status') + + elif f == 'paid_status': + paid_status = django_filters.ChoiceFilter(label=u'По оплате', choices=paid_status_choices, + widget=django_filters.widgets.LinkWidget) + attrs['paid_status'] = paid_status + fields.append('paid_status') + + elif f == 'signed_status': + signed_status = django_filters.ChoiceFilter(label=u'По приёмке', choices=signed_status_choices, + widget=django_filters.widgets.LinkWidget) + attrs['signed_status'] = signed_status + fields.append('signed_status') + + elif f == 'total_saldo': + total_saldo = CustomChoiceFilter(label=u'По сальдо', options=total_saldo_choices, + widget=django_filters.widgets.LinkWidget) + attrs['total_saldo'] = total_saldo + fields.append('total_saldo') + + elif f == 'platej_type': + platej_type = django_filters.ChoiceFilter(label=u'По типу', choices=platej_type_choices, + widget=django_filters.widgets.LinkWidget) + attrs['platej_type'] = platej_type + fields.append('platej_type') + + else: + raise NotImplementedError(u'Unknown field: "%s".' % f) + + model_name = model.__name__.lower() + klass = type(model_name+'FilterSet', (_BaseFilterSet,), attrs) + klass.Meta.model = model + klass.Meta.fields = fields# + _BaseFilterSet.Meta.fields + return klass diff --git a/project/docs/forms/__init__.py b/project/docs/forms/__init__.py new file mode 100644 index 0000000..1b0822b --- /dev/null +++ b/project/docs/forms/__init__.py @@ -0,0 +1,8 @@ +from .email import EmailForm + +from .invoice import InvoiceForm, InvoiceAdminForm, InvoiceItemForm, InvoiceItemAdminForm, InvoicesListForm +from .aktrabot import AktRabotForm, AktRabotAdminForm, AktRabotItemForm, AktRabotItemAdminForm +from .aktsverki import AktSverkiForm, AktSverkiAdminForm, AktSverkiItemForm, AktSverkiItemAdminForm +from .dover import DoverForm, DoverAdminForm, DoverItemForm, DoverItemAdminForm +from .platejka import PlatejkaForm, PlatejkaAdminForm +from .nakladn import NakladnForm, NakladnAdminForm, NakladnItemForm, NakladnItemAdminForm diff --git a/project/docs/forms/aktrabot.py b/project/docs/forms/aktrabot.py new file mode 100644 index 0000000..6527ffe --- /dev/null +++ b/project/docs/forms/aktrabot.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import MyBaseModelForm + +from .base_forms import BaseModelForm +from ..models import AktRabot, AktRabotItem + + +class AktRabotForm(BaseModelForm): + """Форма редактирования акта выполн. работ.""" + class Meta: + model = AktRabot + fields = ('doc_num', 'doc_date', + 'bank_account', 'client', 'invoice', + 'nds_type', 'nds_value', + 'doc_text', + ) + _radioselect = forms.RadioSelect + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'nds_type': _radioselect, + 'doc_text': _textarea, + } + + +class AktRabotAdminForm(AktRabotForm): + """Форма редактирования акта выполн. работ - для админки.""" + class Meta(AktRabotForm.Meta): + fields = None + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'doc_text': _textarea, + } + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(AktRabotAdminForm, self).__init__(None, *args, **kwargs) + + +class AktRabotItemForm(MyBaseModelForm): + """Форма редактирования табличной части акта выполн. работ.""" + class Meta: + model = AktRabotItem + exclude = ['parent'] + + +class AktRabotItemAdminForm(AktRabotItemForm): + """Форма редактирования табличной части акта выполн. работ - для админки.""" + class Meta(AktRabotItemForm.Meta): + exclude = None diff --git a/project/docs/forms/aktsverki.py b/project/docs/forms/aktsverki.py new file mode 100644 index 0000000..084ca47 --- /dev/null +++ b/project/docs/forms/aktsverki.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import MyBaseModelForm + +from .base_forms import BaseModelForm +from ..models import AktSverki, AktSverkiItem + + +class AktSverkiForm(BaseModelForm): + """Форма редактирования акта сверки.""" + change_labels = { + 'doc_date': u'Дата выдачи', + 'start_date': u'Период', + 'end_date': u'', + } + + class Meta: + model = AktSverki + fields = ('doc_num', 'doc_date', + 'doc_mesto', + 'client', + # период + 'start_date', 'end_date', + # входящее сальдо + 'saldo_debit', 'saldo_credit', + ) + + def __init__(self, user, *args, **kwargs): + super(AktSverkiForm, self).__init__(user, *args, **kwargs) + f = self.fields + f['start_date'].widget.attrs['class'] = 'has-datepicker' + f['end_date'].widget.attrs['class'] = 'has-datepicker' + + +class AktSverkiAdminForm(AktSverkiForm): + """Форма редактирования акта сверки - для админки.""" + change_labels = { + 'doc_date': u'Дата выдачи', + } + + class Meta(AktSverkiForm.Meta): + fields = None + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(AktSverkiAdminForm, self).__init__(None, *args, **kwargs) + + +class AktSverkiItemForm(MyBaseModelForm): + """Форма редактирования табличной части акта сверки.""" + class Meta: + model = AktSverkiItem + exclude = ['parent'] + + +class AktSverkiItemAdminForm(AktSverkiItemForm): + """Форма редактирования табличной части акта сверки - для админки.""" + class Meta(AktSverkiItemForm.Meta): + exclude = None diff --git a/project/docs/forms/base_forms.py b/project/docs/forms/base_forms.py new file mode 100644 index 0000000..cc5b25a --- /dev/null +++ b/project/docs/forms/base_forms.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from project.commons.forms import MyBaseModelForm +from project.customer.models import BankAccount, Client + + +class BaseModelForm(MyBaseModelForm): + """Базовая форма редактирования модели бух. формы. + + Добавляет к классу атрибут adjust_client_fields. + Добавляет к методу __init__ обязательный атрибут user. + """ + + # Список полей, у которых нужно настроить выборку контрагентов так, чтобы попадали __только__ контрагенты данного + # пользователя. Если же пользователь никак не был задан - селекты будут пустыми! Если в форме есть поле client, + # то оно настраивается автоматически. + adjust_client_fields = [] + + def __init__(self, user, *args, **kwargs): + super(BaseModelForm, self).__init__(*args, **kwargs) + + self._user = user + # Если передали user=None, то попробовать взять user из kwargs['instance'], а потом из self.data + if not self._user: + try: + self._user = getattr(kwargs.get('instance'), 'user') + except AttributeError: + self._user = self.data.get('user', user) + + f = self.fields + + # Настроить атрибуты виджетов + # TODO вынести в _MySuperForm (сделать там настройку полей с datepicker-ами из словаря) + f['doc_date'].widget.attrs['class'] = 'has-datepicker' + + # Если в форме есть поле bank_account, настроить связь с BankAccount: чтобы можно было выбрать __только__ + # расчетные счета данного пользователя. Также убрать пустой вариант из селекта. + if 'bank_account' in f: + user_accounts = BankAccount.objects.get_all(self._user) + f['bank_account'].queryset = user_accounts + f['bank_account'].empty_label = None + + # Если в форме есть поле client, настроить связь с Client: чтобы можно было выбрать __только__ + # контрагентов данного пользователя. + if 'client' in f: + user_clients = Client.objects.filter(user=self._user) + f['client'].queryset = user_clients + + # Настроить связь других полей с контрагентами. + self._adjust_clients() + + def _adjust_clients(self): + """Настраивает перечисленные в self.adjust_client_fields поля на модель Client.""" + if self.adjust_client_fields: + user_clients = Client.objects.filter(user=self._user) + for key in self.adjust_client_fields: + self.fields[key].queryset = user_clients diff --git a/project/docs/forms/dover.py b/project/docs/forms/dover.py new file mode 100644 index 0000000..8bbc76c --- /dev/null +++ b/project/docs/forms/dover.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import MyBaseModelForm + +from .base_forms import BaseModelForm +from ..models import Dover, DoverItem + + +class DoverForm(BaseModelForm): + """Форма редактирования доверенности на получение ТМЦ.""" + change_labels = { + 'doc_date': u'Дата выдачи', + } + + class Meta: + model = Dover + fields = ('doc_num', 'doc_date', 'doc_expire_date', + 'client', + # на получение мат.ценностей по документу + 'dover_doc', 'dover_doc_date', + # кому выдана и его документы + 'dover_name', 'dover_passport_ser', 'dover_passport_num', 'dover_passport_org', 'dover_passport_date', + ) + + def __init__(self, user, *args, **kwargs): + super(DoverForm, self).__init__(user, *args, **kwargs) + f = self.fields + f['doc_expire_date'].widget.attrs['class'] = 'has-datepicker' + f['dover_doc_date'].widget.attrs['class'] = 'has-datepicker' + f['dover_passport_date'].widget.attrs['class'] = 'has-datepicker' + + +class DoverAdminForm(DoverForm): + """Форма редактирования доверенности на получение ТМЦ - для админки.""" + class Meta(DoverForm.Meta): + fields = None + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(DoverAdminForm, self).__init__(None, *args, **kwargs) + + +class DoverItemForm(MyBaseModelForm): + """Форма редактирования табличной части доверенности на получение ТМЦ.""" + class Meta: + model = DoverItem + exclude = ['parent'] + + +class DoverItemAdminForm(DoverItemForm): + """Форма редактирования табличной части доверенности на получение ТМЦ - для админки.""" + class Meta(DoverItemForm.Meta): + exclude = None diff --git a/project/docs/forms/email.py b/project/docs/forms/email.py new file mode 100644 index 0000000..0c5fca2 --- /dev/null +++ b/project/docs/forms/email.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from django import forms + + +DOC_FORMATS = ( + (u'pdf', u'PDF'), + (u'xls', u'Excel'), +) + + +class EmailForm(forms.Form): + """Форма отправки документа по email.""" + to = forms.EmailField(label=u'E-mail получателя') + body = forms.CharField(label=u'Текст сообщения', max_length=1000, required=False, + widget=forms.Textarea(attrs={'cols': 80, 'rows': 3})) + doc_format = forms.ChoiceField(label=u'Отправить как', choices=DOC_FORMATS, initial=DOC_FORMATS[0][0], + widget=forms.RadioSelect()) + save_client_email = forms.BooleanField(label=u'Сохранить этот e-mail в анкете контрагента', initial=False, + required=False) diff --git a/project/docs/forms/invoice.py b/project/docs/forms/invoice.py new file mode 100644 index 0000000..3f29a5b --- /dev/null +++ b/project/docs/forms/invoice.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import MyBaseModelForm + +from .base_forms import BaseModelForm +from ..models import Invoice, InvoiceItem + + +class InvoiceForm(BaseModelForm): + """Форма редактирования счета.""" + class Meta: + model = Invoice + fields = ('doc_num', 'doc_date', + 'bank_account', 'client', + 'nds_type', 'nds_value', + 'doc_text', + ) + _radioselect = forms.RadioSelect + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'nds_type': _radioselect, + 'doc_text': _textarea, + } + + +class InvoiceAdminForm(InvoiceForm): + """Форма редактирования счета - для админки.""" + class Meta(InvoiceForm.Meta): + fields = None + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'doc_text': _textarea, + } + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(InvoiceAdminForm, self).__init__(None, *args, **kwargs) + + +class InvoicesListForm(forms.Form): + """Форма со списком всех счетов пользователя.""" + invoice = forms.ModelChoiceField(queryset=Invoice.objects.get_all(None), empty_label=u'все счета', required=False) + + def __init__(self, user, *args, **kwargs): + super(InvoicesListForm, self).__init__(*args, **kwargs) + self.fields['invoice'].queryset = Invoice.objects.get_all(user) + + +class InvoiceItemForm(MyBaseModelForm): + """Форма редактирования табличной части счета.""" + class Meta: + model = InvoiceItem + exclude = ['parent'] + + +class InvoiceItemAdminForm(InvoiceItemForm): + """Форма редактирования табличной части счета - для админки.""" + class Meta(InvoiceItemForm.Meta): + exclude = None diff --git a/project/docs/forms/nakladn.py b/project/docs/forms/nakladn.py new file mode 100644 index 0000000..e20ee6b --- /dev/null +++ b/project/docs/forms/nakladn.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import MyBaseModelForm + +from .base_forms import BaseModelForm +from ..models import Nakladn, NakladnItem + + +class NakladnForm(BaseModelForm): + """Форма редактирования накладной.""" + class Meta: + model = Nakladn + fields = ('doc_num', 'doc_date', + 'bank_account', 'client', 'invoice', + 'doc_reason', + 'nds_type', 'nds_value', + 'doc_text', + ) + _radioselect = forms.RadioSelect + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'nds_type': _radioselect, + 'doc_text': _textarea, + } + + +class NakladnAdminForm(NakladnForm): + """Форма редактирования накладной - для админки.""" + class Meta(NakladnForm.Meta): + fields = None + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 3}) + widgets = { + 'doc_text': _textarea, + } + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(NakladnAdminForm, self).__init__(None, *args, **kwargs) + + +class NakladnItemForm(MyBaseModelForm): + """Форма редактирования табличной части накладной.""" + class Meta: + model = NakladnItem + exclude = ['parent'] + + +class NakladnItemAdminForm(NakladnItemForm): + """Форма редактирования табличной части накладной - для админки.""" + class Meta(NakladnItemForm.Meta): + exclude = None diff --git a/project/docs/forms/platejka.py b/project/docs/forms/platejka.py new file mode 100644 index 0000000..d25619e --- /dev/null +++ b/project/docs/forms/platejka.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from django import forms + +from project.commons.forms import set_field_error + +from .base_forms import BaseModelForm +from ..models import Platejka +from .. import consts + + +class PlatejkaForm(BaseModelForm): + """Форма редактирования платежного поручения.""" + conditional_fields = ['nds_type', 'nds_value', + 'tax_status', 'tax_base', 'tax_type', 'tax_bk', 'tax_okato', 'tax_period',] + + class Meta: + model = Platejka + fields = ('platej_type', 'doc_num', 'doc_date', + 'bank_account', 'client', + 'nds_type', 'nds_value', # поля только для перевода денег + # поля только для оплаты налогов + 'tax_status', 'tax_base', 'tax_type', 'tax_num', 'tax_date', 'tax_bk', 'tax_okato', 'tax_period', + # опять общие поля + 'doc_total', 'payment_type', 'payment_order', 'doc_info', + ) + _radioselect = forms.RadioSelect + _textarea = forms.Textarea(attrs={'cols': 80, 'rows': 5}) + widgets = { + #'platej_type': _radioselect, + 'nds_type': _radioselect, + 'doc_info': _textarea, + } + + def __init__(self, user, *args, **kwargs): + super(PlatejkaForm, self).__init__(user, *args, **kwargs) + f = self.fields + f['tax_date'].widget.attrs['class'] = 'has-datepicker' + + def clean(self): + super(PlatejkaForm, self).clean() + + cleaned_data = self.cleaned_data + + # поля становятся обязательными в зависимости от того, какой тип платежки выбран + platej_type = cleaned_data.get('platej_type') + + if platej_type == consts.PLATEJ_TYPE_COMMERCE: # коммерческое (перевод денег) + nds_type = cleaned_data.get('nds_type') + nds_value = cleaned_data.get('nds_value') + + if not nds_type: set_field_error(self, 'nds_type') + if not nds_value: set_field_error(self, 'nds_value') + + elif platej_type == consts.PLATEJ_TYPE_TAX: # налоги + tax_status = cleaned_data.get('tax_status') + tax_base = cleaned_data.get('tax_base') + tax_type = cleaned_data.get('tax_type') + tax_bk = cleaned_data.get('tax_bk') + tax_okato = cleaned_data.get('tax_okato') + tax_period = cleaned_data.get('tax_period') + + if not tax_status: set_field_error(self, 'tax_status') + if not tax_base: set_field_error(self, 'tax_base') + if not tax_type: set_field_error(self, 'tax_type') + if not tax_bk: set_field_error(self, 'tax_bk') + if not tax_okato: set_field_error(self, 'tax_okato') + if not tax_period: set_field_error(self, 'tax_period') + + return cleaned_data + + +class PlatejkaAdminForm(PlatejkaForm): + """Форма редактирования платежного поручения - для админки.""" + class Meta(PlatejkaForm.Meta): + fields = None + widgets = { + 'doc_info': forms.Textarea(attrs={'cols': 80, 'rows': 5}), + } + + def __init__(self, *args, **kwargs): + # обязательно нужно вызывать родительский __init__ и передавать ему None вместо user - иначе глюки ! + super(PlatejkaAdminForm, self).__init__(None, *args, **kwargs) diff --git a/project/docs/models/__init__.py b/project/docs/models/__init__.py new file mode 100644 index 0000000..4d82639 --- /dev/null +++ b/project/docs/models/__init__.py @@ -0,0 +1,6 @@ +from .invoice import Invoice, InvoiceItem +from .aktrabot import AktRabot, AktRabotItem +from .aktsverki import AktSverki, AktSverkiItem +from .dover import Dover, DoverItem +from .platejka import Platejka +from .nakladn import Nakladn, NakladnItem diff --git a/project/docs/models/aktrabot.py b/project/docs/models/aktrabot.py new file mode 100644 index 0000000..b524382 --- /dev/null +++ b/project/docs/models/aktrabot.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from .base_models import BaseInvoiceModel, BaseItemInvoiceModel +from .mixins import SignedStatusFieldMixin, InvoiceFieldMixin + + +class AktRabot(BaseInvoiceModel, SignedStatusFieldMixin, InvoiceFieldMixin): + """Акт выполн. работ.""" + class Meta(BaseInvoiceModel.Meta): + verbose_name = u'Акт выполн. работ' + verbose_name_plural = u'Акты выполн. работ' + + +class AktRabotItem(BaseItemInvoiceModel): + """Табличная часть акта выполн. работ.""" + parent = models.ForeignKey(AktRabot, related_name='aktrabot_items') + + class Meta(BaseItemInvoiceModel.Meta): + verbose_name = u'Табл. часть акта выполн. работ' + verbose_name_plural = u'Табл. части актов выполн. работ' diff --git a/project/docs/models/aktsverki.py b/project/docs/models/aktsverki.py new file mode 100644 index 0000000..0ca34cf --- /dev/null +++ b/project/docs/models/aktsverki.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from django.db import models + +from .base_models import BaseModel, VeryBaseModel +from .mixins import SignedStatusFieldMixin + + +class AktSverki(BaseModel, SignedStatusFieldMixin): + """Акт сверки.""" + doc_mesto = models.CharField(u'Место подписания', max_length=256, help_text=u'(Например, г. Москва)') + # период + start_date = models.DateField('С') + end_date = models.DateField('По') + # входящее сальдо + saldo_debit = models.DecimalField(u'Дебетовое', max_digits=10, decimal_places=2, blank=True) # , default=Decimal('0.00')) + saldo_credit = models.DecimalField(u'Кредитовое', max_digits=10, decimal_places=2, blank=True) # , default=Decimal('0.00')) + + # вычисляемые поля - обновляются при сохранении записей табличной части + total_debit = models.DecimalField(u'Общее дебетовое сальдо', max_digits=10, decimal_places=2, blank=True, + default=Decimal('0.00')) + total_credit = models.DecimalField(u'Общее кредитовое сальдо', max_digits=10, decimal_places=2, blank=True, + default=Decimal('0.00')) + total_saldo = models.DecimalField(u'Итоговое сальдо', max_digits=10, decimal_places=2, blank=True, + default=Decimal('0.00')) + + class Meta(BaseModel.Meta): + verbose_name = u'Акт сверки' + verbose_name_plural = u'Акты сверки' + + def save(self, *args, **kwargs): + if not self.saldo_debit: self.saldo_debit = 0 + if not self.saldo_credit: self.saldo_credit = 0 + super(AktSverki, self).save(*args, **kwargs) + + +class AktSverkiItem(VeryBaseModel): + """Табличная часть акта сверки.""" + parent = models.ForeignKey(AktSverki, related_name='aktsverki_items') + + name = models.CharField(u'Наименование операции, документы', max_length=256) + debit = models.DecimalField(u'Дебет', max_digits=10, decimal_places=2, blank=True) + credit = models.DecimalField(u'Кредит', max_digits=10, decimal_places=2, blank=True) + + class Meta(VeryBaseModel.Meta): + verbose_name = u'Табл. часть акта сверки' + verbose_name_plural = u'Табл. части актов сверки' + ordering = ('created_at',) + + def save(self, *args, **kwargs): + if not self.debit: self.debit = 0 + if not self.credit: self.credit = 0 + super(AktSverkiItem, self).save(*args, **kwargs) diff --git a/project/docs/models/base_models.py b/project/docs/models/base_models.py new file mode 100644 index 0000000..21aa020 --- /dev/null +++ b/project/docs/models/base_models.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.contrib.auth.models import User + +from project.customer.models import Client, BankAccount + +from . import managers +from .. import consts + + +class VeryBaseModel(models.Model): + """Очень базовая абстрактная модель.""" + created_at = models.DateTimeField(u'Создан', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + class Meta: + abstract = True + app_label = 'docs' + ordering = ('-created_at',) + get_latest_by = 'created_at' + +# ----------------------------------------------------------------------------- + +class BaseModel(VeryBaseModel): + """Абстрактная модель бух.формы.""" + user = models.ForeignKey(User, related_name='+', verbose_name=u'Пользователь') + + doc_num = models.PositiveIntegerField(u'Номер') + doc_date = models.DateField('Дата создания') + + client = models.ForeignKey(Client, related_name='+', verbose_name=u'Контрагент') + + objects = managers.BaseModelManager() + + class Meta(VeryBaseModel.Meta): + abstract = True + ordering = ('-doc_date',) + + def __unicode__(self): + return u'%s № %s от %s' % (self._meta.verbose_name or '', self.doc_num, self.doc_date) + + +class BaseNdsModel(BaseModel): + """Расширение абстрактной модели бух.формы - НДС. + Доп. поля под тип и ставку НДС. + """ + nds_type = models.PositiveSmallIntegerField(u'НДС', choices=consts.NDS_TYPE_CHOICES, default=consts.NDS_TYPE_NO) + nds_value = models.PositiveSmallIntegerField(u'Ставка НДС', choices=consts.NDS_VALUE_CHOICES, default=consts.NDS_VALUE_0) + + class Meta(BaseModel.Meta): + abstract = True + + def get_nds_as_number(self): + """Значение НДС как число (без символа %).""" + return consts.NDS_VALUE_NUMERIC.get(self.nds_value, 0) + + +class BaseInvoiceModel(BaseNdsModel): + """Расширение абстрактной модели бух.формы - по типу счета. + Доп. поля под расчетный счет и дополнительные условия. + """ + bank_account = models.ForeignKey(BankAccount, related_name='+', verbose_name=u'Расчётный счёт') + doc_text = models.TextField(u'Дополнительные условия', max_length=1000, blank=True, default='') + + class Meta(BaseNdsModel.Meta): + abstract = True + +# ----------------------------------------------------------------------------- + +class BaseItemModel(VeryBaseModel): + """Абстрактная модель табличной части бух.формы.""" + name = models.CharField(u'Наименование', max_length=256) + qty = models.DecimalField(u'Кол-во', max_digits=10, decimal_places=3) + units = models.CharField(u'Ед. изм.', max_length=20) + + class Meta(VeryBaseModel.Meta): + abstract = True + ordering = ('created_at',) + + def __unicode__(self): + return u'%s, %s %s' % (self.name[:30], self.qty, self.units) + + +class BaseItemInvoiceModel(BaseItemModel): + """Расширение абстрактной модели табл. части бух.формы - по типу счета. + Доп. поля под цену и сумму. + """ + price = models.DecimalField(u'Цена', max_digits=10, decimal_places=2) + total_price = models.DecimalField(u'Сумма', max_digits=10, decimal_places=2) + + class Meta(BaseItemModel.Meta): + abstract = True + + def __unicode__(self): + curr = consts.CURRENCY_CHOICES_DICT.get(getattr(self, 'currency', consts.CURR_RUB)) + return u'%s, %s %s * %s = %s %s' % (self.name[:30], self.qty, self.units, self.price, self.total_price, curr) + + def save(self, *args, **kwargs): + self.total_price = self.price * self.qty # пересчитать сумму + super(BaseItemInvoiceModel, self).save(*args, **kwargs) diff --git a/project/docs/models/dover.py b/project/docs/models/dover.py new file mode 100644 index 0000000..3dd4046 --- /dev/null +++ b/project/docs/models/dover.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from .base_models import BaseModel, VeryBaseModel + + +class Dover(BaseModel): + """Доверенность на получение ТМЦ.""" + doc_expire_date = models.DateField(u'Срок действия') + + # на получение мат.ценностей по документу + dover_doc = models.CharField(u'По документу №', max_length=256) + dover_doc_date = models.DateField(u'Дата документа') + + # кому выдана и его документы + dover_name = models.CharField(u'Должность, ФИО', max_length=256, help_text=u'Полностью в дат. падеже.') + dover_passport_ser = models.CharField(u'Серия', max_length=10) + dover_passport_num = models.CharField(u'Номер', max_length=10) + dover_passport_org = models.CharField(u'Кем выдан', max_length=256) + dover_passport_date = models.DateField(u'Дата выдачи') + + class Meta(BaseModel.Meta): + verbose_name = u'Доверенность на получ. ТМЦ' + verbose_name_plural = u'Доверенности на получ. ТМЦ' + + +class DoverItem(VeryBaseModel): + """Табличная часть доверенности на получение ТМЦ.""" + parent = models.ForeignKey(Dover, related_name='dover_items') + + name = models.CharField(u'Наименование', max_length=256) + qty = models.PositiveIntegerField(u'Количество') + units = models.CharField(u'Ед. измерения', max_length=20) + + class Meta(VeryBaseModel.Meta): + verbose_name = u'Табл. часть доверенности' + verbose_name_plural = u'Табл. части доверенностей' + #app_label = 'docs' + ordering = ('created_at',) + + def __unicode__(self): + return u'%s, %s %s' % (self.name[:30], self.qty, self.units) diff --git a/project/docs/models/invoice.py b/project/docs/models/invoice.py new file mode 100644 index 0000000..80b01be --- /dev/null +++ b/project/docs/models/invoice.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from .base_models import BaseInvoiceModel, BaseItemInvoiceModel +from .. import consts + + +class Invoice(BaseInvoiceModel): + """Счет.""" + UNPAID = 1 + PARTLY_PAID = 2 + PAID = 3 + + PAID_CHOICES = ( + (UNPAID, u'Нет'), + (PARTLY_PAID, u'Частично'), + (PAID, u'Да'), + ) + + paid_status = models.PositiveSmallIntegerField(u'Оплачен?', choices=PAID_CHOICES, default=UNPAID) + closed_status = models.BooleanField(u'Закрыт?', choices=consts.BOOL_CHOICES, default=False) + + class Meta(BaseInvoiceModel.Meta): + verbose_name = u'Счёт' + verbose_name_plural = u'Счета' + + +class InvoiceItem(BaseItemInvoiceModel): + """Табличная часть счета.""" + parent = models.ForeignKey(Invoice, related_name='invoice_items') + + class Meta(BaseItemInvoiceModel.Meta): + verbose_name = u'Табл. часть счета' + verbose_name_plural = u'Табл. части счетов' diff --git a/project/docs/models/managers.py b/project/docs/models/managers.py new file mode 100644 index 0000000..59f91a8 --- /dev/null +++ b/project/docs/models/managers.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from django.db import models + + +class BaseModelManager(models.Manager): + def get_all(self, user): + """Возвращает все документы данного типа заданного пользователя.""" + return self.filter(user=user) + + def get_last_doc_num(self, user): + """Возвращает номер самого последнего сохраненного юзером документа данного типа, + или None, если таких документов еще нет.""" + try: + return self.filter(user=user).order_by('-created_at')[0].doc_num + except IndexError: + return None + + def get_max_doc_num(self, user): + """Возвращает максимальный номер когда-либо сохраненного юзером документа данного типа, + или None, если таких документов еще нет.""" + try: + return self.filter(user=user).order_by('-doc_num')[0].doc_num + except IndexError: + return None diff --git a/project/docs/models/mixins.py b/project/docs/models/mixins.py new file mode 100644 index 0000000..964df8b --- /dev/null +++ b/project/docs/models/mixins.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from .invoice import Invoice +from .. import consts + + +class SignedStatusFieldMixin(models.Model): + """Mixin: добавляет поле `Подписан?`""" + signed_status = models.BooleanField(u'Подписан?', choices=consts.BOOL_CHOICES, default=False) + + class Meta: + abstract = True + + +class InvoiceFieldMixin(models.Model): + """Mixin: добавляет FK поле `Создать по счёту`""" + invoice = models.ForeignKey(Invoice, related_name='+', verbose_name=u'Создать по счёту', blank=True, null=True, + default=None) + + class Meta: + abstract = True diff --git a/project/docs/models/nakladn.py b/project/docs/models/nakladn.py new file mode 100644 index 0000000..9f5eba8 --- /dev/null +++ b/project/docs/models/nakladn.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from .base_models import BaseInvoiceModel, BaseItemInvoiceModel +from .mixins import SignedStatusFieldMixin, InvoiceFieldMixin + + +class Nakladn(BaseInvoiceModel, SignedStatusFieldMixin, InvoiceFieldMixin): + """Накладная торг12.""" + doc_reason = models.CharField(u'Основание', max_length=256, blank=True, default='') + + class Meta(BaseInvoiceModel.Meta): + verbose_name = u'Накладная' + verbose_name_plural = u'Накладные' + + +class NakladnItem(BaseItemInvoiceModel): + """Табличная часть накладной торг12.""" + parent = models.ForeignKey(Nakladn, related_name='nakladn_items') + + class Meta(BaseItemInvoiceModel.Meta): + verbose_name = u'Табл. часть накладной' + verbose_name_plural = u'Табл. части накладных' diff --git a/project/docs/models/platejka.py b/project/docs/models/platejka.py new file mode 100644 index 0000000..e18cf29 --- /dev/null +++ b/project/docs/models/platejka.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +from django.db import models + +from project.customer.models import BankAccount + +from .base_models import BaseModel +from .. import consts + + +class Platejka(BaseModel): + """Платёжное поручение.""" + platej_type = models.PositiveSmallIntegerField(u'Тип платёжного поручения', choices=consts.PLATEJ_TYPE_CHOICES, + default=consts.PLATEJ_TYPE_COMMERCE) + + bank_account = models.ForeignKey(BankAccount, related_name='+', verbose_name=u'Расчётный счёт') + + doc_total = models.DecimalField(u'Сумма', max_digits=10, decimal_places=2) + payment_type = models.PositiveSmallIntegerField(u'Вид платежа', choices=consts.PAYMENT_TYPE_CHOICES, + default=consts.PAYMENT_TYPE_CHOICES[0][0]) + payment_order = models.CharField(u'Очерёдность платежа', max_length=10) + doc_info = models.TextField(u'Назначение платежа', max_length=1000) + + # поля только для перевода денег (коммерческое) + nds_type = models.PositiveSmallIntegerField(u'НДС', choices=consts.NDS_TYPE_CHOICES, default=consts.NDS_TYPE_NO) + nds_value = models.PositiveSmallIntegerField(u'Ставка НДС', choices=consts.NDS_VALUE_CHOICES, default=consts.NDS_VALUE_0) + + # поля только для оплаты налогов (налоговое) + tax_status = models.CharField(u'Статус составителя', max_length=10, choices=consts.TAX_STATUS_CHOICES, + default=consts.TAX_STATUS_CHOICES[0][0]) + tax_base = models.CharField(u'Основание налогового платежа', max_length=10, choices=consts.TAX_BASE, + default=consts.TAX_BASE[0][0]) + tax_type = models.CharField(u'Тип налогового платежа', max_length=10, choices=consts.TAX_TYPE, + default=consts.TAX_TYPE[0][0]) + tax_num = models.CharField(u'Номер документа основания', max_length=50, blank=True, default='') + tax_date = models.DateField(u'Дата документа основания', blank=True, null=True) + tax_bk = models.CharField(u'Код БК доходов РФ', max_length=256) + tax_okato = models.CharField(u'Код ОКАТО сборщика платежей', max_length=256) + tax_period = models.CharField(u'Период, за который начисляется налог', max_length=256, + help_text = (u'Формат ввода периода платежей:
' + u'Месячный платёж - "МС.00.0000"
' + u'Квартальный платёж - "КВ.00.0000"
' + u'Полугодовой платёж - "ПЛ.00.0000"
' + u'Годовой платёж - "ГД.00.0000"
' + u'Платёж по дате - "дд.мм.гггг"') + ) + + class Meta(BaseModel.Meta): + verbose_name = u'Платёжное поручение' + verbose_name_plural = u'Платёжные поручения' + + # хелперы, чтоб не оборачивать в шаблонах каждое обращение к определенным полям в проверку типа платежа + + def is_commerce(self): + return self.platej_type == consts.PLATEJ_TYPE_COMMERCE + + def is_tax(self): + return self.platej_type == consts.PLATEJ_TYPE_TAX + + def get_tax_status_kod(self): + """Налоги. Статус составителя, КОД.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_status: + return self.tax_status + return u'' + + def get_tax_base_kod(self): + """Налоги. Основание налогового платежа, КОД.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_base: + return self.tax_base + return u'' + + def get_tax_type_kod(self): + """Налоги. Тип налогового платежа, КОД.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_type: + return self.tax_type + return u'' + + def get_tax_num(self): + """Налоги. Номер документа основания.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_num: + return self.tax_num + return u'' + + def get_tax_date(self): + """Налоги. Дата документа основания.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_date: + return self.tax_date + return u'' + + def get_tax_bk(self): + """Налоги. Код БК доходов РФ.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_bk: + return self.tax_bk + return u'' + + def get_tax_okato(self): + """Налоги. Код ОКАТО сборщика платежей.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_okato: + return self.tax_okato + return u'' + + def get_tax_period(self): + """Налоги. Период, за который начисляется налог.""" + if self.platej_type == consts.PLATEJ_TYPE_TAX and self.tax_period: + return self.tax_period + return u'' diff --git a/project/docs/tests.py b/project/docs/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/project/docs/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/project/docs/urls.py b/project/docs/urls.py new file mode 100644 index 0000000..c656c02 --- /dev/null +++ b/project/docs/urls.py @@ -0,0 +1,71 @@ +# -*- coding: UTF-8 -*- +from django.conf.urls import * + +from .views import (InvoiceViews, AktRabotViews, AktSverkiViews, DoverViews, PlatejkaViews, NakladnViews) +from .views import getview, index + + +urlpatterns = patterns('docs.views', + url(r'^$', index, name='docs_index'), # страница со ссылками на бух. формы +) + +klasses = [ + ('invoice', InvoiceViews), + ('aktrabot', AktRabotViews), + ('aktsverki', AktSverkiViews), + ('dover', DoverViews), + ('platejka', PlatejkaViews), + ('nakladn', NakladnViews), +] + +for name, klass in klasses: + urlpatterns += patterns('docs.views', + # список + url(r'^%s/$' % name, getview, {'klass': klass, 'oper': 'list',}, name='docs_%s_list' % name), + # список, пагинация + url(r'^%s/page/(?P[0-9]+)/$' % name, getview, {'klass': klass, 'oper': 'list',}, + name='docs_%s_list' % name), + + # добавить + url(r'^%s/add/$' % name, getview, {'klass': klass, 'oper': 'add',}, name='docs_%s_add' % name), + # редактировать + url(r'^%s/(?P\d+)/edit/$' % name, getview, {'klass': klass, 'oper': 'edit',}, + name='docs_%s_edit' % name), + # создать копию + url(r'^%s/(?P\d+)/copy/$' % name, getview, {'klass': klass, 'oper': 'copy',}, + name='docs_%s_copy' % name), + # удалить + url(r'^%s/(?P\d+)/delete/$' % name, getview, {'klass': klass, 'oper': 'delete',}, + name='docs_%s_delete' % name), + + # сохранить в pdf + url(r'^%s/(?P\d+)/pdf/$' % name, getview, {'klass': klass, 'oper': 'as_pdf',}, + name='docs_%s_pdf' % name), + # сохранить в excel + url(r'^%s/(?P\d+)/xls/$' % name, getview, {'klass': klass, 'oper': 'as_xls',}, + name='docs_%s_xls' % name), + + # отправить pdf/xls на email + url(r'^%s/(?P\d+)/email/$' % name, getview, {'klass': klass, 'oper': 'email',}, + name='docs_%s_email' % name), + + # поля документа - AJAX + url(r'^%s/(?P\d+)/get/ajax/$' % name, getview, {'klass': klass, 'oper': 'get_ajax',}, + name='docs_%s_get_ajax' % name), + # отправить pdf/xls на email - AJAX + url(r'^%s/(?P\d+)/email/ajax/$' % name, getview, {'klass': klass, 'oper': 'email_ajax',}, + name='docs_%s_email_ajax' % name), + ) + +# доп. обработчики: создать Документ по Счету +urlpatterns += patterns('docs.views', + # создать по Счету -> Акт вып. работ + url(r'^%s/add/by/invoice/(?P\d+)/$' % 'aktrabot', getview, + {'klass': AktRabotViews, 'oper': 'add_by_invoice',}, name='docs_%s_add_by_invoice' % 'aktrabot'), + # создать по Счету -> Накладную + url(r'^%s/add/by/invoice/(?P\d+)/$' % 'nakladn', getview, + {'klass': NakladnViews, 'oper': 'add_by_invoice',}, name='docs_%s_add_by_invoice' % 'nakladn'), +# # создать по Счету -> Счёт-фактуру +# url(r'^%s/add/by/invoice/(?P\d+)/$' % 'sfv', getview, {'klass': SfvViews, 'oper': 'add_by_invoice',}, +# name='docs_%s_add_by_invoice' % 'sfv'), +) diff --git a/project/docs/utils.py b/project/docs/utils.py new file mode 100644 index 0000000..8bd4e79 --- /dev/null +++ b/project/docs/utils.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from . import consts + + +def get_nds(key): + """Берет значение НДС по ключу key.""" + return consts.NDS_VALUE_NUMERIC.get(key, Decimal(0)) + + +def extract_nds(obj): + """Если НДС содержится в цене, извлекает и возвращает его.""" + nds = Decimal('0.00') + if obj.parent.nds_type == consts.NDS_TYPE_IN: # ндс в сумме, извлечь его + nds_rate = get_nds(obj.parent.nds_value)/100 + nds = obj.price * (1 - 1/(1+nds_rate)) + return nds + + +def calc_clean_price(obj): + """Считает цену без НДС.""" + return obj.price - extract_nds(obj) + + +def calc_clean_total_price(obj): + """Считает стоимость без налога.""" + return calc_clean_price(obj) * obj.qty + + +def calc_total_nds(obj): + """Считает сумму налога.""" + total_nds = Decimal('0.00') + if obj.parent.nds_type == consts.NDS_TYPE_IN: # ндс в сумме + total_nds = extract_nds(obj) * obj.qty + elif obj.parent.nds_type == consts.NDS_TYPE_OUT: # ндс сверх суммы + total_price = obj.price * obj.qty + nds_rate = get_nds(obj.parent.nds_value)/100 + total_nds = total_price * nds_rate + return total_nds + + +def calc_full_total_price(obj): + """Считает стоимость с налогом.""" + return calc_total_nds(obj) + calc_clean_total_price(obj) diff --git a/project/docs/views/__init__.py b/project/docs/views/__init__.py new file mode 100644 index 0000000..d55424b --- /dev/null +++ b/project/docs/views/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.http import Http404 +from django.conf import settings + +from .invoice import InvoiceViews +from .aktrabot import AktRabotViews +from .aktsverki import AktSverkiViews +from .dover import DoverViews +from .platejka import PlatejkaViews +from .nakladn import NakladnViews +#from .sfv import SfvViews + + +DEBUG = getattr(settings, 'DEBUG', False) + + +@login_required # важно!!! +def getview(request, *args, **kwargs): + try: + views = kwargs['klass'](request) # класс с вьюхами + handler = getattr(views, kwargs['oper']) # конкретная вьюха + return handler(request, *args, **kwargs) # передать управление во вьюху и вернуть ее результат + except (KeyError, AttributeError): + if DEBUG: + raise + else: + raise Http404 + + +# ----------------------------------------------------------------------------- + +@login_required +def index(request): + """Страница со ссылками на все бух.формы.""" + template_name = 'docs/index.html' + return render(request, template_name) diff --git a/project/docs/views/aktrabot.py b/project/docs/views/aktrabot.py new file mode 100644 index 0000000..8ab7806 --- /dev/null +++ b/project/docs/views/aktrabot.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from ..models import AktRabot, AktRabotItem +from ..forms import AktRabotForm, AktRabotItemForm +from .. import consts, utils + +from .base_views import BaseItemsViews +from .mixins import AddByInvoiceMethodMixin + + +class AktRabotViews(BaseItemsViews, AddByInvoiceMethodMixin): + """Views для актов выполн. работ.""" + + MODEL = AktRabot # модель документа + FORM_CLASS = AktRabotForm # форма документа + + ITEM_MODEL = AktRabotItem # модель табличной части документа + ITEM_FORM_CLASS = AktRabotItemForm # форма табличной части документа + ITEM_FORM_PREFIX = 'aktrabot_items' # префикс формы табличной части + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('signed_status', 'client', 'invoice', 'doc_date',) + + # по какому полю суммировать табличную часть документа при показе списком + LIST_SUM_FIELD = 'aktrabot_items__total_price' + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_aktrabot_' + + # именованные урлы операций + URL_LIST = 'docs_aktrabot_list' + URL_EDIT = 'docs_aktrabot_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/aktrabot/list.html' + TEMPLATE_FORM = 'docs/aktrabot/form.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/aktrabot/as_pdf.html' + XLS_TEMPLATE = 'aktrabot.xls' + FILENAME = u'Акт выполненных работ № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'акт выполненных работ', # кто? что? + 'rodit': u'акта выполненных работ', # кого? чего? + 'dateln': u'акту выполненных работ', # кому? чему? + 'vinit': u'акт выполненных работ', # кого? что? + 'tvorit': u'актом выполненных работ', # кем? чем? + 'predlojn': u'акте выполненных работ', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'акты выполненных работ', # кто? что? + 'rodit': u'актов выполненных работ', # кого? чего? + 'dateln': u'актам выполненных работ', # кому? чему? + 'vinit': u'акты выполненных работ', # кого? что? + 'tvorit': u'актами выполненных работ', # кем? чем? + 'predlojn': u'актах выполненных работ', # о ком? о чём? + } + + def prepare(self, obj, obj_items, export_to=None): + """Изменить/подмешать дополнительные поля к документу.""" + obj.sum_total_price = Decimal('0.00') + obj.sum_total_nds = Decimal('0.00') + obj.sum_full_total_price = Decimal('0.00') + for item in obj_items: + obj.sum_total_price += item.total_price + obj.sum_total_nds += utils.calc_total_nds(item) + obj.sum_full_total_price += utils.calc_full_total_price(item) + + if obj.nds_type == consts.NDS_TYPE_NO: # не учитывать ндс + s = u'Без налога (НДС)' + elif obj.nds_type == consts.NDS_TYPE_IN: # ндс в сумме + s = u'В том числе НДС (%s)' % obj.get_nds_value_display() + elif obj.nds_type == consts.NDS_TYPE_OUT: # ндс сверх суммы + s = u'Итого НДС (%s)' % obj.get_nds_value_display() + else: + s = u'' + obj.nds_itogo_text = s diff --git a/project/docs/views/aktsverki.py b/project/docs/views/aktsverki.py new file mode 100644 index 0000000..94c2f30 --- /dev/null +++ b/project/docs/views/aktsverki.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from project.customer.forms import ClientsListForm + +from ..models import AktSverki, AktSverkiItem +from ..forms import AktSverkiForm, AktSverkiItemForm + +from .base_views import BaseItemsViews + + +class AktSverkiViews(BaseItemsViews): + """Views для актов сверки.""" + + MODEL = AktSverki # модель документа + FORM_CLASS = AktSverkiForm # форма документа + + ITEM_MODEL = AktSverkiItem # модель табличной части документа + ITEM_FORM_CLASS = AktSverkiItemForm # форма табличной части документа + ITEM_FORM_PREFIX = 'aktsverki_items' # префикс формы табличной части + + # поля, по которым можно сортировать список документов + ORDER_FIELDS = ('doc_date', 'doc_num', 'client__name',) + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('signed_status', 'client', 'total_saldo', 'doc_date',) + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_aktsverki_' + + # именованные урлы операций + URL_LIST = 'docs_aktsverki_list' + URL_EDIT = 'docs_aktsverki_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/aktsverki/list.html' + TEMPLATE_FORM = 'docs/aktsverki/form.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/aktsverki/as_pdf.html' + XLS_TEMPLATE = 'aktsverki.xls' + FILENAME = u'Акт сверки № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'акт сверки', # кто? что? + 'rodit': u'акта сверки', # кого? чего? + 'dateln': u'акту сверки', # кому? чему? + 'vinit': u'акт сверки', # кого? что? + 'tvorit': u'актом сверки', # кем? чем? + 'predlojn': u'акте сверки', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'акты сверки', # кто? что? + 'rodit': u'актов сверки', # кого? чего? + 'dateln': u'актам сверки', # кому? чему? + 'vinit': u'акты сверки', # кого? что? + 'tvorit': u'актами сверки', # кем? чем? + 'predlojn': u'актах сверки', # о ком? о чём? + } + + def update_list_dict(self, dictionary): + """Здесь можно изменить словарь параметров перед передачей его в шаблон вывода списка документов.""" + dictionary['clients_form'] = ClientsListForm(self.request.user) + + def update_parent_on_items_save(self, obj, obj_items): + """Обновить родительскую модель.""" + # пересчет общего кредитового/дебетового и итогового сальдо + total_credit = Decimal('0.00') + total_debit = Decimal('0.00') + for item in obj_items: + total_credit += item.credit + total_debit += item.debit + obj.total_credit = total_credit + obj.total_debit = total_debit + # самое важное, итоговое сальдо - прежде всего, нужно для фильтрации + obj.total_saldo = (obj.saldo_credit + total_credit) - (obj.saldo_debit + total_debit) + obj.save() + + def prepare(self, obj, obj_items, export_to=None): + """Изменить/подмешать дополнительные поля к документу.""" + obj.sum_debit = obj.saldo_debit + obj.sum_credit = obj.saldo_credit + for item in obj_items: + obj.sum_debit += item.debit + obj.sum_credit += item.credit + + if obj.sum_debit == obj.sum_credit: # нет задолженности + obj.sum_debit = obj.sum_credit = 0 + elif obj.sum_debit > obj.sum_credit: # задолженность нам + obj.sum_debit -= obj.sum_credit + obj.sum_credit = 0 + elif obj.sum_debit < obj.sum_credit: # задолженность контрагенту + obj.sum_credit -= obj.sum_debit + obj.sum_debit = 0 diff --git a/project/docs/views/base_views.py b/project/docs/views/base_views.py new file mode 100644 index 0000000..b8d3f3c --- /dev/null +++ b/project/docs/views/base_views.py @@ -0,0 +1,718 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from email.header import Header +from time import time +import simplejson as json + +from django.shortcuts import render, get_object_or_404, redirect +from django.http import HttpResponseServerError, HttpResponseBadRequest, HttpResponse +from django.utils.decorators import method_decorator +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_protect +from django.db.models import Sum +from django.forms.models import inlineformset_factory, model_to_dict +from django.template.loader import render_to_string +from django.core.mail import EmailMessage +from django.utils.encoding import smart_str +from django.conf import settings + +from project.commons.utils import dthandler +from project.commons.paginator import pagination, save_per_page_value +from project.commons.pdf_tools import render_pdf_to_string, pdf_to_response +from project.commons.xls import xls_to_response + +from project.customer.models import get_profile, BankAccount +from project.customer.forms import ClientsListForm, ClientForm + +from ..as_xls import render_xls_to_string +from ..forms import EmailForm, InvoicesListForm +from .. import filters + + +DEBUG = getattr(settings, 'DEBUG', False) +SUPPORT_EMAIL = getattr(settings, 'SUPPORT_EMAIL', '') + +ORDER_VAR = 'o' +ORDER_TYPE_VAR = 'ot' + + +class Ordering(object): + """Параметры сортировки. Для передачи в шаблон.""" + order_var = ORDER_VAR + order_type_var = ORDER_TYPE_VAR + + def __init__(self, order_field, order_type): + self.order_field = order_field + self.order_type = order_type + + +class BaseViews(object): + """Базовые views для простых документов (без табличной части).""" + + MODEL = None # модель документа + FORM_CLASS = None # форма документа + + EMAIL_FORM_CLASS = EmailForm # форма создания и отправки email + + # поля, по которым можно сортировать список документов + ORDER_FIELDS = ('doc_date', 'doc_num', 'client__name', 'doc_sum',) + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('client', 'doc_date',) + + # префикс именованных урлов документов данного типа, для передачи в шаблон + URL_PREFIX = '' + + # именованные урлы операций + URL_LIST = '' + URL_EDIT = '' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/_base/base_list.html' + TEMPLATE_ADD = 'docs/_base/base_add.html' + TEMPLATE_EDIT = 'docs/_base/base_edit.html' + TEMPLATE_DELETE = 'docs/_base/base_delete.html' + TEMPLATE_FORM = 'docs/_base/base_form.html' + TEMPLATE_FORM_JS = 'docs/stub_js.html' + + TEMPLATE_EMAIL = 'docs/email/base_email.html' + TEMPLATE_EMAIL_FORM = 'docs/email/base_email_form.html' + EMAIL_MSG_TEMPLATE = 'docs/email/email.txt' # шаблон письма + + # для генерации pdf/xls + PDF_TEMPLATE = '' + XLS_TEMPLATE = '' + FILENAME = u'Документ № %s, %s' # без расширения + + # --- константы для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'документ', # кто? что? + 'rodit': u'документа', # кого? чего? + 'dateln': u'документу', # кому? чему? + 'vinit': u'документ', # кого? что? + 'tvorit': u'документом', # кем? чем? + 'predlojn': u'документе', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'документы', # кто? что? + 'rodit': u'документов', # кого? чего? + 'dateln': u'документам', # кому? чему? + 'vinit': u'документы', # кого? что? + 'tvorit': u'документами', # кем? чем? + 'predlojn': u'документах', # о ком? о чём? + } + + def __init__(self, request): + self.request = request + self.user = request.user + self.profile = request.profile + self.set_redirects() + self.MODEL_NAME = self.MODEL.__name__.lower() + self.asserts() + + def asserts(self): + """Проверить объект класса на типичные ошибки.""" + assert self.request is not None, (u"%s.request can't be None!" % self.__class__.__name__) + assert self.MODEL is not None, (u"%s.MODEL can't be None!" % self.__class__.__name__) + assert self.FORM_CLASS is not None, (u"%s.FORM_CLASS can't be None!" % self.__class__.__name__) + assert self.EMAIL_FORM_CLASS is not None, (u"%s.EMAIL_FORM_CLASS can't be None!" % self.__class__.__name__) + assert (isinstance(self.ORDER_FIELDS, tuple) or isinstance(self.ORDER_FIELDS, list)), (u"%s.ORDER_FIELDS should be of tuple or list type!" % self.__class__.__name__) + + def set_redirects(self): + """Куда редиректить после операции.""" + self.REDIRECT_AFTER_ADD = self.URL_LIST + self.REDIRECT_AFTER_EDIT = self.URL_LIST + self.REDIRECT_AFTER_COPY = self.URL_EDIT + self.REDIRECT_AFTER_DELETE = self.URL_LIST + self.REDIRECT_AFTER_EMAIL = self.URL_LIST + + def get_ordering(self): + """Поле и порядок сортировки.""" + order_field, order_type = 'doc_date', 'desc' # default + params = dict(self.request.GET.items()) + if params.get(ORDER_VAR) in self.ORDER_FIELDS: + order_field = params.get(ORDER_VAR) + if params.get(ORDER_TYPE_VAR) in ('asc', 'desc'): + order_type = params.get(ORDER_TYPE_VAR) + return order_field, order_type + + def get_list_qs(self): + """QuerySet для просмотра списка документов.""" + qs = self.MODEL.objects.filter(user=self.request.user) + qs = qs.select_related('client') + # задать сортировку + order_field, order_type = self.get_ordering() + if order_field: + qs = qs.order_by('%s%s' % ((order_type == 'desc' and '-' or ''), order_field,)) + return qs + + def get_filters_class(self): + """Возвращает класс с набором фильтров.""" + return filters.build_filterset_class(self.MODEL, self.request.user, self.FILTER_FIELDS) + + def get_filters(self, qs): + """Возвращает объект с набором фильтров.""" + klass = self.get_filters_class() + return klass(self.request.user, self.request.GET, qs) + + def get_obj(self, id, only_form_fields=False): + """Объект документа или ошибка 404, если его нет в базе. + Поведение когда флаг only_form_fields=True: + если в форме редактирования документа задан атрибут Meta.fields, то запрашивает только поля, + перечисленные в нём. Иначе (как и по умолчанию) дампит вообще все поля, которые есть в модели. + """ + if only_form_fields: + try: + fields_list = self.FORM_CLASS.Meta.fields + except AttributeError: + fields_list = [] + return get_object_or_404(self.MODEL.objects.values(*fields_list), pk=id, user=self.request.user) + else: + return get_object_or_404(self.MODEL, pk=id, user=self.request.user) + + def get_filename(self, *args, **kwargs): + obj = self.get_obj(kwargs['id']) + client = obj.client.name.replace('\n',' ').replace('\r',' ').strip() + return self.FILENAME % (obj.doc_num, client,) + + def update_list_dict(self, dictionary): + """Здесь можно изменить словарь параметров перед передачей его в шаблон вывода списка документов.""" + dictionary['clients_form'] = ClientsListForm(self.request.user) + dictionary['invoices_form'] = InvoicesListForm(self.request.user) + + @method_decorator(csrf_protect) + @method_decorator(save_per_page_value) + def list(self, *args, **kwargs): + """Список документов.""" + obj_list = self.get_list_qs() + + # фильтрация списка + filters = self.get_filters(obj_list) + obj_list_count_before_filtering = 0 # сколько записей было в списке до его фильтрации + if not filters.qs: + obj_list_count_before_filtering = obj_list.count() + obj_list = filters.qs + + # пагинация списка + page_num = kwargs.get('page_num') + page, pagination_form = pagination(self.request, obj_list, page_num) + + # параметры сортировки для отрисовки в шаблоне + # реальная сортировка QuerySet производится в методе get_list_qs + order_field, order_type = self.get_ordering() + ordering = Ordering(order_field, order_type) + + email_form = self.EMAIL_FORM_CLASS() + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'model_name': self.MODEL_NAME, + 'page': page, + 'pagination_form': pagination_form, + 'ordering': ordering, + 'filters': filters, + 'obj_list_count_before_filtering': obj_list_count_before_filtering, + 'email_form': email_form, + } + self.update_list_dict(dictionary) + return render(self.request, self.TEMPLATE_LIST, dictionary) + + def init_form(self): + """Начальные значения полей формы документа.""" + initial = {'doc_date': datetime.now(),} + # номер нового документа + doc_num = self.MODEL.objects.get_max_doc_num(self.request.user) or 0 + initial['doc_num'] = doc_num + 1 + return initial + + @method_decorator(csrf_protect) + def add(self, *args, **kwargs): + """Добавить документ. + Если при GET-запросе в kwargs передать initial, то создаст предзаполненный документ. + """ + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_ADD) + + if self.request.method == 'POST': + form = self.FORM_CLASS(self.request.user, data=self.request.POST) + + if form.is_valid(): + new_obj = form.save(commit=False) + new_obj.user = self.request.user + new_obj.save() + return redirect(self.REDIRECT_AFTER_ADD) + else: + initial = kwargs.get('initial') or self.init_form() + form = self.FORM_CLASS(self.request.user, initial=initial) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'form_template': self.TEMPLATE_FORM, + 'form_template_js': self.TEMPLATE_FORM_JS, + 'form': form, + 'client_form': ClientForm(), + } + return render(self.request, self.TEMPLATE_ADD, dictionary) + + def copy(self, *args, **kwargs): + """Создать полную копию документа.""" + obj = self.get_obj(kwargs['id']) + kwargs['initial'] = model_to_dict( + obj, + fields=getattr(self.FORM_CLASS.Meta, 'fields', None), + exclude=getattr(self.FORM_CLASS.Meta, 'exclude', None) + ) + kwargs['initial'].update(self.init_form()) + # обязательно убрать ключи + kwargs['initial'].pop('pk', None) + kwargs['initial'].pop('id', None) + kwargs['initial'].pop('created_at', None) + kwargs['initial'].pop('updated_at', None) + return self.add(self.request, *args, **kwargs) + + @method_decorator(csrf_protect) + def edit(self, *args, **kwargs): + """Редактировать документ.""" + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_EDIT) + + obj = self.get_obj(kwargs['id']) + + if self.request.method == 'POST': + form = self.FORM_CLASS(self.request.user, data=self.request.POST, instance=obj) + + if form.is_valid(): + new_obj = form.save() + return redirect(self.REDIRECT_AFTER_EDIT) + else: + form = self.FORM_CLASS(self.request.user, instance=obj) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'form_template': self.TEMPLATE_FORM, + 'form_template_js': self.TEMPLATE_FORM_JS, + 'obj': obj, + 'form': form, + 'client_form': ClientForm(), + } + return render(self.request, self.TEMPLATE_EDIT, dictionary) + + @method_decorator(csrf_protect) + def delete(self, *args, **kwargs): + """Удалить документ.""" + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_DELETE) + + obj = self.get_obj(kwargs['id']) + + if self.request.method == 'POST': + obj.delete() + return redirect(self.REDIRECT_AFTER_DELETE) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'obj': obj, + } + return render(self.request, self.TEMPLATE_DELETE, dictionary) + + def prepare(self, obj, export_to=None): + """Изменить/подмешать дополнительные поля к документу.""" + pass + + def get_pdf(self, *args, **kwargs): + """Создать документ в PDF и вернуть как строку.""" + obj = self.get_obj(kwargs['id']) + profile = get_profile(obj.user) + main_account = BankAccount.objects.get_main(obj.user) + self.prepare(obj, export_to='pdf') + params = { + 'obj': obj, + 'obj_items': None, + 'profile': profile, + 'main_account': main_account, + } + c1 = time() + pdf = render_pdf_to_string(self.request, self.PDF_TEMPLATE, params) + if DEBUG: + print '%s generation time (seconds): %s' % (self.PDF_TEMPLATE, time()-c1,) + return pdf + + def get_xls(self, *args, **kwargs): + """Создать документ в Excel и вернуть как строку.""" + obj = self.get_obj(kwargs['id']) + profile = get_profile(obj.user) + main_account = BankAccount.objects.get_main(obj.user) + self.prepare(obj, export_to='xls') + params = { + 'obj': obj, + 'obj_items': None, + 'profile': profile, + 'main_account': main_account, + } + c1 = time() + xls = render_xls_to_string(self.request, self.XLS_TEMPLATE, params) + if DEBUG: + print '%s generation time (seconds): %s' % (self.XLS_TEMPLATE, time()-c1,) + return xls + + def as_pdf(self, *args, **kwargs): + """Вывести документ в формате PDF в HttpResponse.""" + try: + pdf = self.get_pdf(*args, **kwargs) + filename = '%s.pdf' % self.get_filename(*args, **kwargs) + return pdf_to_response(pdf, filename) + except: + if DEBUG: + raise + else: + return HttpResponseServerError('Server error. Try later.') + + def as_xls(self, *args, **kwargs): + """Вывести документ в формате Excel в HttpResponse.""" + try: + xls = self.get_xls(*args, **kwargs) + filename = '%s.xls' % self.get_filename(*args, **kwargs) + return xls_to_response(xls, filename) + except: + if DEBUG: + raise + else: + return HttpResponseServerError('Server error. Try later.') + + def send_email(self, subject, to, body, files): + """Отправка письма.""" + dict_context = {'body': body, 'support_email': SUPPORT_EMAIL,} + email_body = render_to_string(self.EMAIL_MSG_TEMPLATE, dict_context) + + attachments = [] + for f in files: + attachments.append((smart_str(Header(f['filename'], 'cp1251')), f['content'], f['mimetype'])) + + email = EmailMessage(subject=subject, to=(to,), body=email_body, attachments=attachments) + return email.send() + + def _process_email_form_and_send(self, form, *args, **kwargs): + """Обработка формы отправки документа и отправка email-а.""" + if form.cleaned_data['save_client_email']: + client = getattr(self.get_obj(kwargs['id']), 'client', None) + if client: + client.contact_email = form.cleaned_data['to'] # сохранить email клиента + client.save() + + doc_format = form.cleaned_data['doc_format'] + if doc_format in ('pdf', 'xls',): + files = [] + filename = self.get_filename(*args, **kwargs) + if doc_format == 'pdf': + files = [{ + 'filename': '%s.%s' % (filename, doc_format,), + 'content': self.get_pdf(*args, **kwargs), + 'mimetype': 'application/pdf', + },] + elif doc_format == 'xls': + files = [{ + 'filename': '%s.%s' % (filename, doc_format,), + 'content': self.get_xls(*args, **kwargs), + 'mimetype': 'application/ms-excel', + },] + + return self.send_email( + subject = u'%s' % filename, # тема письма = имя файла без расширения + to = form.cleaned_data['to'], + body = form.cleaned_data['body'], + files = files + ) + return False # что-то пошло не так + + @method_decorator(csrf_protect) + def email(self, *args, **kwargs): + """Отправить документ на email аттачем в заданном формате.""" + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_EMAIL) + + obj = self.get_obj(kwargs['id']) + + if self.request.method == 'POST': + form = self.EMAIL_FORM_CLASS(data=self.request.POST) + if form.is_valid(): + self._process_email_form_and_send(form, *args, **kwargs) + return redirect(self.REDIRECT_AFTER_EMAIL) + else: + initial = {} + client = getattr(self.get_obj(kwargs['id']), 'client', None) + if client: + initial['to'] = client.contact_email # подставить в форму email клиента + form = self.EMAIL_FORM_CLASS(initial=initial) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'form_template': self.TEMPLATE_EMAIL_FORM, + 'obj': obj, + 'form': form, + } + return render(self.request, self.TEMPLATE_EMAIL, dictionary) + + @method_decorator(require_POST) + @method_decorator(csrf_protect) + def email_ajax(self, *args, **kwargs): + """Отправить документ на email аттачем в заданном формате - AJAX.""" + if not self.request.is_ajax(): + return HttpResponseBadRequest() + + result = False + form = self.EMAIL_FORM_CLASS(data=self.request.POST) + if form.is_valid(): + result = self._process_email_form_and_send(form, *args, **kwargs) + + non_field_errors = form.non_field_errors() + if not form.is_valid(): + non_field_errors.append(u'Заполните/исправьте выделенные поля.') + + data = { + 'success': form.is_valid(), + 'field_errors': form.errors, # ошибки полей + 'form_errors': non_field_errors, # ошибки формы + } + if form.is_valid() and result: + data['message'] = {'title': 'Инфо', 'msg': 'Письмо отправлено.',} + + return HttpResponse(json.dumps(data), mimetype='application/json') + + def get_ajax(self, *args, **kwargs): + """Получить документ - AJAX.""" + if not self.request.is_ajax(): + return HttpResponseBadRequest() + obj = self.get_obj(kwargs['id'], only_form_fields=True) + data = json.dumps(obj, default=dthandler) + return HttpResponse(data, mimetype='application/json') + +# ----------------------------------------------------------------------------- + +class BaseItemsViews(BaseViews): + """Базовые views для документов с табличной частью.""" + + ITEM_MODEL = None # модель табличной части документа + ITEM_FORM_CLASS = None # форма табличной части документа + ITEM_FORM_PREFIX = None # префикс формы табличной части + + # по какому полю суммировать табличную часть при показе списка документов + LIST_SUM_FIELD = None # None или строка + + def __init__(self, request): + super(BaseItemsViews, self).__init__(request) + self.set_item_formset_class() + + def asserts(self): + """Проверить объект класса на типичные ошибки.""" + super(BaseItemsViews, self).asserts() + assert self.ITEM_MODEL is not None, (u"%s.ITEM_MODEL can't be None!" % self.__class__.__name__) + assert self.ITEM_FORM_CLASS is not None, (u"%s.ITEM_FORM_CLASS can't be None!" % self.__class__.__name__) + + def set_item_formset_class(self): + """Класс FormSet-а для табличной части документа.""" + self.ITEM_FORMSET_CLASS = inlineformset_factory( + parent_model = self.MODEL, + model = self.ITEM_MODEL, + form = self.ITEM_FORM_CLASS, + extra=2 + ) + + def get_list_qs(self): + """QuerySet для просмотра списка документов. + Плюс суммирование табличной части по заданному полю. + """ + queryset = super(BaseItemsViews, self).get_list_qs() + if self.LIST_SUM_FIELD: + queryset = queryset.annotate(doc_sum = Sum(self.LIST_SUM_FIELD)) + return queryset + + def get_obj_items_qs(self, obj): + """QuerySet табличной части документа.""" + return self.ITEM_MODEL.objects.filter(parent=obj).select_related() + + def update_parent_on_items_save(self, obj, obj_items): + """Обновить родительскую модель.""" + pass + + @method_decorator(csrf_protect) + def add(self, *args, **kwargs): + """Добавить документ. + Если при GET-запросе в kwargs передать initial и/или initial_items, то создаст предзаполненный документ. + """ + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_ADD) + + if self.request.method == 'POST': + form = self.FORM_CLASS(self.request.user, data=self.request.POST) + + if '_add_line' in self.request.POST: + post_copy = self.request.POST.copy() + total_forms_key = '%s-TOTAL_FORMS' % self.ITEM_FORM_PREFIX + post_copy[total_forms_key] = int(post_copy[total_forms_key]) + 1 + formset = self.ITEM_FORMSET_CLASS(data=post_copy, prefix=self.ITEM_FORM_PREFIX) + else: + formset = self.ITEM_FORMSET_CLASS(data=self.request.POST, prefix=self.ITEM_FORM_PREFIX) + + if '_add_line' not in self.request.POST and form.is_valid() and formset.is_valid(): + new_obj = form.save(commit=False) + new_obj.user = self.request.user + new_obj.save() + # сохранить табличную часть + if formset.is_valid(): + new_items = formset.save(commit=False) + for item in new_items: + item.parent = new_obj + item.save() + self.update_parent_on_items_save(new_obj, new_items) + return redirect(self.REDIRECT_AFTER_ADD) + else: + initial = kwargs.get('initial') or self.init_form() + initial_items = kwargs.get('initial_items') + form = self.FORM_CLASS(self.request.user, initial=initial) + formset = self.ITEM_FORMSET_CLASS(prefix=self.ITEM_FORM_PREFIX, initial=initial_items) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'form_template': self.TEMPLATE_FORM, + 'form_template_js': self.TEMPLATE_FORM_JS, + 'form': form, + 'formset': formset, + 'client_form': ClientForm(), + } + return render(self.request, self.TEMPLATE_ADD, dictionary) + + def copy(self, *args, **kwargs): + """Создать полную копию документа.""" + source_id = kwargs['id'] + obj = self.get_obj(source_id) + obj_items = self.get_obj_items_qs(obj) + + kwargs['initial'] = model_to_dict( + obj, + fields=getattr(self.FORM_CLASS.Meta, 'fields', None), + exclude=getattr(self.FORM_CLASS.Meta, 'exclude', None) + ) + kwargs['initial'].update(self.init_form()) + # обязательно убрать ключи + kwargs['initial'].pop('pk', None) + kwargs['initial'].pop('id', None) + kwargs['initial'].pop('created_at', None) + kwargs['initial'].pop('updated_at', None) + + if obj_items: + kwargs['initial_items'] = [] + for item in obj_items: + d = model_to_dict( + item, + fields=getattr(self.ITEM_FORM_CLASS.Meta, 'fields', None), + exclude=getattr(self.ITEM_FORM_CLASS.Meta, 'exclude', None) + ) + # обязательно убрать ключи + d.pop('pk', None) + d.pop('id', None) + d.pop('created_at', None) + d.pop('updated_at', None) + kwargs['initial_items'].append(d) + + return self.add(self.request, *args, **kwargs) + + @method_decorator(csrf_protect) + def edit(self, *args, **kwargs): + """Редактировать документ.""" + if self.request.method == 'POST' and '_cancel' in self.request.POST: + return redirect(self.REDIRECT_AFTER_EDIT) + + obj = self.get_obj(kwargs['id']) + + if self.request.method == 'POST': + form = self.FORM_CLASS(self.request.user, data=self.request.POST, instance=obj) + + if '_add_line' in self.request.POST: + post_copy = self.request.POST.copy() + total_forms_key = '%s-TOTAL_FORMS' % self.ITEM_FORM_PREFIX + post_copy[total_forms_key] = int(post_copy[total_forms_key]) + 1 + formset = self.ITEM_FORMSET_CLASS(data=post_copy, prefix=self.ITEM_FORM_PREFIX, instance=obj) + else: + formset = self.ITEM_FORMSET_CLASS(data=self.request.POST, prefix=self.ITEM_FORM_PREFIX, instance=obj) + + if '_add_line' not in self.request.POST and form.is_valid() and formset.is_valid(): + new_obj = form.save() + # сохранить табличную часть + if formset.is_valid(): + items = formset.save(commit=False) + for item in items: + item.parent = new_obj + item.save() + self.update_parent_on_items_save(new_obj, items) + return redirect(self.REDIRECT_AFTER_EDIT) + else: + form = self.FORM_CLASS(self.request.user, instance=obj) + formset = self.ITEM_FORMSET_CLASS(instance=obj) + + dictionary = { + 'padeji': self.PADEJI, + 'padeji_mnoj': self.PADEJI_MNOJ, + 'url_prefix': self.URL_PREFIX, + 'form_template': self.TEMPLATE_FORM, + 'form_template_js': self.TEMPLATE_FORM_JS, + 'obj': obj, + 'form': form, + 'formset': formset, + 'client_form': ClientForm(), + } + return render(self.request, self.TEMPLATE_EDIT, dictionary) + + def prepare(self, obj, obj_items, export_to=None): + """Подмешать дополнительные поля к документу.""" + pass + + def get_pdf(self, *args, **kwargs): + """Создать документ в PDF и вернуть как строку.""" + obj = self.get_obj(kwargs['id']) + obj_items = self.get_obj_items_qs(obj) + profile = get_profile(obj.user) + main_account = BankAccount.objects.get_main(obj.user) + self.prepare(obj, obj_items, export_to='pdf') + params = { + 'obj': obj, + 'obj_items': obj_items, + 'profile': profile, + 'main_account': main_account, + } + c1 = time() + pdf = render_pdf_to_string(self.request, self.PDF_TEMPLATE, params) + if DEBUG: + print '%s generation time (seconds): %s' % (self.PDF_TEMPLATE, time()-c1,) + return pdf + + def get_xls(self, *args, **kwargs): + """Создать документ в Excel и вернуть как строку.""" + obj = self.get_obj(kwargs['id']) + obj_items = self.get_obj_items_qs(obj) + profile = get_profile(obj.user) + main_account = BankAccount.objects.get_main(obj.user) + self.prepare(obj, obj_items, export_to='xls') + params = { + 'obj': obj, + 'obj_items': obj_items, + 'profile': profile, + 'main_account': main_account, + } + c1 = time() + xls = render_xls_to_string(self.request, self.XLS_TEMPLATE, params) + if DEBUG: + print '%s generation time (seconds): %s' % (self.XLS_TEMPLATE, time()-c1,) + return xls diff --git a/project/docs/views/dover.py b/project/docs/views/dover.py new file mode 100644 index 0000000..7288535 --- /dev/null +++ b/project/docs/views/dover.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import datetime + +from project.customer.forms import ClientsListForm + +from ..models import Dover, DoverItem +from ..forms import DoverForm, DoverItemForm + +from .base_views import BaseItemsViews + + +class DoverViews(BaseItemsViews): + """Views для доверенностей на получение ТМЦ.""" + + MODEL = Dover # модель документа + FORM_CLASS = DoverForm # форма документа + + ITEM_MODEL = DoverItem # модель табличной части документа + ITEM_FORM_CLASS = DoverItemForm # форма табличной части документа + ITEM_FORM_PREFIX = 'dover_items' # префикс формы табличной части + + # поля, по которым можно сортировать список документов + ORDER_FIELDS = ('doc_num', 'doc_date', 'doc_expire_date', 'dover_name', 'client__name',) + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('client', 'doc_date',) + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_dover_' + + # именованные урлы операций + URL_LIST = 'docs_dover_list' + URL_EDIT = 'docs_dover_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/dover/list.html' + TEMPLATE_FORM = 'docs/dover/form.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/dover/as_pdf.html' + XLS_TEMPLATE = 'dover.xls' + FILENAME = u'Доверенность № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'доверенность', # кто? что? + 'rodit': u'доверенности', # кого? чего? + 'dateln': u'доверенности', # кому? чему? + 'vinit': u'доверенность', # кого? что? + 'tvorit': u'доверенностью', # кем? чем? + 'predlojn': u'доверенности', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'доверенности', # кто? что? + 'rodit': u'доверенностью', # кого? чего? + 'dateln': u'доверенностям', # кому? чему? + 'vinit': u'доверенности', # кого? что? + 'tvorit': u'доверенностями', # кем? чем? + 'predlojn': u'доверенностях', # о ком? о чём? + } + + def update_list_dict(self, dictionary): + """Здесь можно изменить словарь параметров перед передачей его в шаблон вывода списка документов.""" + dictionary['clients_form'] = ClientsListForm(self.request.user) + + def init_form(self): + """Начальные значения полей формы документа.""" + initial = super(DoverViews, self).init_form() + initial['doc_expire_date'] = datetime.datetime.now() + datetime.timedelta(1) + return initial diff --git a/project/docs/views/invoice.py b/project/docs/views/invoice.py new file mode 100644 index 0000000..ef937f1 --- /dev/null +++ b/project/docs/views/invoice.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from project.customer.forms import ClientsListForm + +from ..models import Invoice, InvoiceItem +from ..forms import InvoiceForm, InvoiceItemForm +from .. import consts, utils, filters + +from .base_views import BaseItemsViews + + +class InvoiceViews(BaseItemsViews): + """Views для счетов-фактур.""" + + MODEL = Invoice # модель документа + FORM_CLASS = InvoiceForm # форма документа + + ITEM_MODEL = InvoiceItem # модель табличной части документа + ITEM_FORM_CLASS = InvoiceItemForm # форма табличной части документа + ITEM_FORM_PREFIX = 'invoice_items' # префикс формы табличной части + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('paid_status', 'closed_status', 'client', 'doc_date',) + + # по какому полю суммировать табличную часть документа при показе списком + LIST_SUM_FIELD = 'invoice_items__total_price' + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_invoice_' + + # именованные урлы операций + URL_LIST = 'docs_invoice_list' + URL_EDIT = 'docs_invoice_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/invoice/list.html' + TEMPLATE_FORM = 'docs/invoice/form.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/invoice/as_pdf.html' + XLS_TEMPLATE = 'invoice.xls' + FILENAME = u'Счет № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'счёт', # кто? что? + 'rodit': u'счёта', # кого? чего? + 'dateln': u'счёту', # кому? чему? + 'vinit': u'счёт', # кого? что? + 'tvorit': u'счётом', # кем? чем? + 'predlojn': u'счёте', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'счета', # кто? что? + 'rodit': u'счетов', # кого? чего? + 'dateln': u'счетам', # кому? чему? + 'vinit': u'счета', # кого? что? + 'tvorit': u'счетами', # кем? чем? + 'predlojn': u'счетах', # о ком? о чём? + } + + def update_list_dict(self, dictionary): + """Здесь можно изменить словарь параметров перед передачей его в шаблон вывода списка документов.""" + dictionary['clients_form'] = ClientsListForm(self.request.user) + + def prepare(self, obj, obj_items, export_to=None): + """Изменить/подмешать дополнительные поля к документу.""" + obj.sum_total_price = Decimal('0.00') + obj.sum_total_nds = Decimal('0.00') + obj.sum_full_total_price = Decimal('0.00') + for item in obj_items: + obj.sum_total_price += item.total_price + obj.sum_total_nds += utils.calc_total_nds(item) + obj.sum_full_total_price += utils.calc_full_total_price(item) + + if obj.nds_type == consts.NDS_TYPE_NO: # не учитывать ндс + s = u'Без налога (НДС)' + elif obj.nds_type == consts.NDS_TYPE_IN: # ндс в сумме + s = u'В том числе НДС (%s)' % obj.get_nds_value_display() + elif obj.nds_type == consts.NDS_TYPE_OUT: # ндс сверх суммы + s = u'Итого НДС (%s)' % obj.get_nds_value_display() + else: + s = u'' + obj.nds_itogo_text = s diff --git a/project/docs/views/mixins.py b/project/docs/views/mixins.py new file mode 100644 index 0000000..a803b83 --- /dev/null +++ b/project/docs/views/mixins.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect +from django.forms.models import model_to_dict + +from ..models import Invoice, InvoiceItem +from ..forms import InvoiceForm, InvoiceItemForm + + +class AddByInvoiceMethodMixin(object): + """Mixin: добавляет метод add_by_invoice.""" + + @method_decorator(csrf_protect) + def add_by_invoice(self, *args, **kwargs): + """Добавить документ по Счёту.""" + invoice_id = (kwargs['invoice_id']) + invoice = get_object_or_404(Invoice, pk=invoice_id, user=self.request.user) + invoice_items = InvoiceItem.objects.filter(parent=invoice).select_related() + + kwargs['initial'] = model_to_dict( + invoice, + fields=getattr(InvoiceForm.Meta, 'fields', None), + exclude=getattr(InvoiceItemForm.Meta, 'exclude', None) + ) + kwargs['initial'].update(self.init_form()) + kwargs['initial']['invoice'] = invoice + + if invoice_items: + kwargs['initial_items'] = [] + for item in invoice_items: + kwargs['initial_items'].append( + model_to_dict( + item, + fields=getattr(InvoiceItemForm.Meta, 'fields', None), + exclude=getattr(InvoiceItemForm.Meta, 'exclude', None) + )) + + return self.add(self.request, *args, **kwargs) diff --git a/project/docs/views/nakladn.py b/project/docs/views/nakladn.py new file mode 100644 index 0000000..c45c603 --- /dev/null +++ b/project/docs/views/nakladn.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +from decimal import Decimal + +from django.utils.text import wrap + +from ..models import Nakladn, NakladnItem +from ..forms import NakladnForm, NakladnItemForm +from .. import utils + +from .base_views import BaseItemsViews +from .mixins import AddByInvoiceMethodMixin + + +class NakladnViews(BaseItemsViews, AddByInvoiceMethodMixin): + """Views для накладных торг12.""" + + MODEL = Nakladn # модель документа + FORM_CLASS = NakladnForm # форма документа + + ITEM_MODEL = NakladnItem # модель табличной части документа + ITEM_FORM_CLASS = NakladnItemForm # форма табличной части документа + ITEM_FORM_PREFIX = 'nakladn_items' # префикс формы табличной части + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('signed_status', 'client', 'invoice', 'doc_date',) + + # по какому полю суммировать табличную часть документа при показе списком + LIST_SUM_FIELD = 'nakladn_items__total_price' + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_nakladn_' + + # именованные урлы операций + URL_LIST = 'docs_nakladn_list' + URL_EDIT = 'docs_nakladn_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/nakladn/list.html' + TEMPLATE_FORM = 'docs/nakladn/form.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/nakladn/as_pdf.html' + XLS_TEMPLATE = 'nakladn.xls' + FILENAME = u'Накладная № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'накладная', # кто? что? + 'rodit': u'накладной', # кого? чего? + 'dateln': u'накладной', # кому? чему? + 'vinit': u'накладную', # кого? что? + 'tvorit': u'накладной', # кем? чем? + 'predlojn': u'накладной', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'накладные', # кто? что? + 'rodit': u'накладных', # кого? чего? + 'dateln': u'накладным', # кому? чему? + 'vinit': u'накладные', # кого? что? + 'tvorit': u'накладными', # кем? чем? + 'predlojn': u'накладных', # о ком? о чём? + } + + def prepare(self, obj, obj_items, export_to=None): + """Изменить/подмешать дополнительные поля к документу.""" + obj.sum_total_nds = Decimal('0.00') + obj.sum_full_total_price = Decimal('0.00') + obj.sum_qty = Decimal('0.00') + obj.sum_clean_total_price = Decimal('0.00') + # строки табличной части + for item in obj_items: + item.clean_price = utils.calc_clean_price(item) + item.clean_total_price = utils.calc_clean_total_price(item) + item.total_nds = utils.calc_total_nds(item) + item.full_total_price = utils.calc_full_total_price(item) + # итого табличной части + obj.sum_total_nds += item.total_nds + obj.sum_full_total_price += item.full_total_price + obj.sum_qty += item.qty + obj.sum_clean_total_price += item.clean_total_price + + # разбивка на страницы и итого по странице + # величины и размеры - приблизительные и подобраны опытным путем! + if export_to == 'pdf': + page_rows = 42 + + # высота в строках + рамки и вертикальные отступы + doc_header_rows = 20 # TODO: рассчитывать! + doc_footer_rows = 26 + tbl_header_rows = 6 + 1 # шапка таблицы + tbl_page_footer_rows = 1 + 1 # подитог (итого по странице) таблицы + tbl_footer_rows = 1 + 1 # подвал таблицы + + curr_rows = doc_header_rows + tbl_header_rows +# print '(start) curr_rows =', curr_rows + + # если шрифт не моноширный, то в строчках умещается разное количество символов! + chars_per_line = 38 # для наименования + + last_page_item_idx = 0 + + def calc_itogo(item, start, stop): + # подитоги по странице + item.sum_qty = 0 + item.sum_clean_total_price = 0 + item.sum_total_nds = 0 + item.sum_full_total_price = 0 + for x in obj_items[start:stop]: + item.sum_qty += x.qty + item.sum_clean_total_price += x.clean_total_price + item.sum_total_nds += x.total_nds + item.sum_full_total_price += x.full_total_price + + for idx, item in enumerate(obj_items): + just_calc_itogo = False + # сколько строк займет строка + name = wrap(item.name, chars_per_line) + name_rows = max(1, len(name.split('\n'))) + 1 # + отступ/рамка +# print 'name_rows =', name_rows, +# print '(+%s)' % (tbl_page_footer_rows + tbl_footer_rows) + # строка, подитог и итог не помещаются на странице + if (curr_rows + name_rows + tbl_page_footer_rows + + tbl_footer_rows > page_rows): + if idx == 0: + item.pdf_pagebreak_before = True + prev_item = item + else: + prev_item = obj_items[idx-1] +# print '--- new page', \ +# curr_rows + name_rows + tbl_page_footer_rows + tbl_footer_rows, \ +# '>', page_rows + prev_item.pdf_pagebreak_after = True + prev_item.pdf_page_footer = True + calc_itogo(prev_item, last_page_item_idx, idx) + just_calc_itogo = True + last_page_item_idx = idx + curr_rows = tbl_header_rows + name_rows + else: + curr_rows += name_rows + else: + if len(obj_items): # только если были записи в табличной части + if not just_calc_itogo: + item.pdf_page_footer = True + calc_itogo(item, last_page_item_idx, idx) + curr_rows += tbl_page_footer_rows + curr_rows += tbl_footer_rows + if curr_rows + doc_footer_rows > page_rows: + item.pdf_pagebreak_after = True +# print 'curr_rows =', curr_rows diff --git a/project/docs/views/platejka.py b/project/docs/views/platejka.py new file mode 100644 index 0000000..bdf0846 --- /dev/null +++ b/project/docs/views/platejka.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from ..models import Platejka +from ..forms import PlatejkaForm + +from .base_views import BaseViews + + +class PlatejkaViews(BaseViews): + """Views для платежных поручений.""" + + MODEL = Platejka # модель документа + FORM_CLASS = PlatejkaForm # форма документа + + # поля, по которым можно сортировать список документов + ORDER_FIELDS = ('doc_date', 'doc_num', 'doc_info', 'doc_total',) + + # поля, по которым можно фильтровать список документов + # должны поддерживаться в docs.filters.build_filterset_class ! + FILTER_FIELDS = ('platej_type', 'client', 'doc_date',) + + # префикс именованных урлов этого типа документов, для передачи в шаблон + URL_PREFIX = 'docs_platejka_' + + # именованные урлы операций + URL_LIST = 'docs_platejka_list' + URL_EDIT = 'docs_platejka_edit' + + # пути к шаблонам + TEMPLATE_LIST = 'docs/platejka/list.html' + TEMPLATE_FORM = 'docs/platejka/form.html' + TEMPLATE_FORM_JS = 'docs/platejka/js.html' + + # для генерации pdf/xls + PDF_TEMPLATE = 'docs/platejka/as_pdf.html' + XLS_TEMPLATE = 'platejka.xls' + FILENAME = u'Платежное поручение № %s, %s' # без расширения + + # --- грамматика для вывода наименований в шаблонах + PADEJI = { + 'imenit': u'платёжное поручение', # кто? что? + 'rodit': u'платёжного поручения', # кого? чего? + 'dateln': u'платёжному поручению', # кому? чему? + 'vinit': u'платёжное поручение', # кого? что? + 'tvorit': u'платёжным поручением', # кем? чем? + 'predlojn': u'платёжном поручении', # о ком? о чём? + } + + PADEJI_MNOJ = { + 'imenit': u'платёжные поручения', # кто? что? + 'rodit': u'платёжных поручений', # кого? чего? + 'dateln': u'платёжным поручениям', # кому? чему? + 'vinit': u'платёжные поручения', # кого? что? + 'tvorit': u'платёжными поручениями', # кем? чем? + 'predlojn': u'платёжных поручениях', # о ком? о чём? + } + + def update_list_dict(self, dictionary): + """Здесь можно изменить словарь параметров перед передачей его в шаблон вывода списка документов.""" + pass diff --git a/project/local_settings.py.dev-example b/project/local_settings.py.dev-example new file mode 100644 index 0000000..cd89bd2 --- /dev/null +++ b/project/local_settings.py.dev-example @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from settings import * + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = () + +MANAGERS = ADMINS + +SERVER_EMAIL = 'dokumentor@localhost' + +ALLOWED_HOSTS = ['*'] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'dokumentor', # Or path to database file if using sqlite3. + 'USER': 'dokumentor', # Not used with sqlite3. + 'PASSWORD': 'dokumentor', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +EMAIL_BACKEND = 'eml_email_backend.EmailBackend' +EMAIL_FILE_PATH = path('../../tmp_emails') + +DEVSERVER_DEFAULT_PORT = '8080' +DEVSERVER_MODULES = () +INSTALLED_APPS += ('devserver',) + +if False and 'debug_toolbar' not in INSTALLED_APPS: + MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) + INSTALLED_APPS += ('debug_toolbar',) + + DEBUG_TOOLBAR_PANELS = ( + #'debug_toolbar.panels.version.VersionDebugPanel', + 'debug_toolbar.panels.timer.TimerDebugPanel', + 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel', + 'debug_toolbar.panels.headers.HeaderDebugPanel', + 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel', + 'debug_toolbar.panels.template.TemplateDebugPanel', + 'debug_toolbar.panels.sql.SQLDebugPanel', + 'debug_toolbar.panels.cache.CacheDebugPanel', + 'debug_toolbar.panels.logger.LoggingPanel', + ) + + INTERNAL_IPS = ('127.0.0.1',) + + DEBUG_TOOLBAR_CONFIG = { + 'EXCLUDE_URLS': ('/admin',), + 'INTERCEPT_REDIRECTS': False, + } diff --git a/project/local_settings.py.prod-example b/project/local_settings.py.prod-example new file mode 100644 index 0000000..b62de57 --- /dev/null +++ b/project/local_settings.py.prod-example @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from settings import * + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('andrey.goo', 'andrey.goo@gmail.com'), +) + +MANAGERS = ADMINS + +SERVER_EMAIL = 'dokumentor@localhost' + +ALLOWED_HOSTS = ['dokumentor.ru'] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'dokumentor', # Or path to database file if using sqlite3. + 'USER': 'dokumentor', # Not used with sqlite3. + 'PASSWORD': 'dokumentor', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# переопределить secret_key, на случай если посторонние имели доступ к исходникам проекта +#SECRET_KEY = '30rdy#9a!y-=^kdh6+v*e$cxdf$uu7djbnlm#=c(g^30@250rb' diff --git a/project/myauth/__init__.py b/project/myauth/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/project/myauth/__init__.py @@ -0,0 +1 @@ + diff --git a/project/myauth/emails.py b/project/myauth/emails.py new file mode 100644 index 0000000..c878f45 --- /dev/null +++ b/project/myauth/emails.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from django.template.loader import render_to_string +from django.core.mail import EmailMessage +from django.conf import settings + + +SUPPORT_EMAIL = getattr(settings, 'SUPPORT_EMAIL', '') + + +def send_registration_email(user_email, confirm_url): + """Отправить письмо о регистрации нового пользователя.""" + template_name = 'myauth/registration_email.txt' + subject = u'Регистрация на Документоре: подтверждение e-mail' + dict_context = {'user_email': user_email, 'confirm_url': confirm_url, 'support_email': SUPPORT_EMAIL,} + email_body = render_to_string(template_name, dict_context) + email = EmailMessage(subject=subject, to=(user_email,), body=email_body) + return email.send() + + +def send_reset_password_email(user_email, confirm_url): + """Отправить письмо с ключём для восстановления пароля.""" + template_name = 'myauth/reset_key_email.txt' + subject = u'Документор: восстановление пароля' + dict_context = {'user_email': user_email, 'confirm_url': confirm_url, 'support_email': SUPPORT_EMAIL,} + email_body = render_to_string(template_name, dict_context) + email = EmailMessage(subject=subject, to=(user_email,), body=email_body) + return email.send() + + +def send_new_password_email(user_email, new_password): + """Отправить письмо с новым паролем.""" + template_name = 'myauth/reset_new_password_email.txt' + subject = u'Документор: новый пароль' + dict_context = {'user_email': user_email, 'new_password': new_password, 'support_email': SUPPORT_EMAIL,} + email_body = render_to_string(template_name, dict_context) + email = EmailMessage(subject=subject, to=(user_email,), body=email_body) + return email.send() diff --git a/project/myauth/forms.py b/project/myauth/forms.py new file mode 100644 index 0000000..81f6f10 --- /dev/null +++ b/project/myauth/forms.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +from django import forms +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.auth import authenticate + +from project.commons.forms import set_field_error +from project.customer import consts as customer_consts + + +PASSWORD_MIN_LEN = getattr(settings, 'PASSWORD_MIN_LEN ', 7) + +PROFILE_CHOICES = ( + (customer_consts.IP_PROFILE, u'Индивидуальный предприниматель (ИП)'), + (customer_consts.ORG_PROFILE, u'Организация (ООО, ЗАО, ОАО, НКО и т.п.)'), +) + + +class RegistrationForm(forms.Form): + """Форма регистрации нового пользователя.""" + email = forms.EmailField(label=u'E-mail', max_length=75, error_messages={'invalid': u'Неверный формат e-mail.',}, + help_text=u'На него будет выслано письмо с подтверждением.') + + password1 = forms.CharField(label=u'Пароль', min_length=PASSWORD_MIN_LEN, widget=forms.PasswordInput, + error_messages={'min_length': u'Не менее %s символов.' % PASSWORD_MIN_LEN,}, + help_text=u'Не менее %s символов.' % PASSWORD_MIN_LEN) + + password2 = forms.CharField(label=u'Повтор пароля', widget=forms.PasswordInput) + + profile_type = forms.ChoiceField(label=u'Тип регистрации', choices=PROFILE_CHOICES, widget=forms.RadioSelect, + error_messages={'required': u'Нужно указать форму собственности вашего бизнеса.',}) + + def clean_email(self): + """Проверить не занят ли email.""" + email = self.cleaned_data['email'] + try: + User.objects.get(email__iexact = email) + except User.DoesNotExist: + return email + raise forms.ValidationError(u'Такой e-mail уже зарегистрирован.') + + def clean(self): + super(RegistrationForm, self).clean() + password1 = self.cleaned_data.get('password1') + password2 = self.cleaned_data.get('password2') + # проверить чтобы оба пароля совпадали + if password1 and password2: + if password1 != password2: + set_field_error(self, 'password2', u'Пароли не совпадают.') + return self.cleaned_data + + +class ResetPasswordForm(forms.Form): + """Форма восстановления пароля.""" + email = forms.EmailField(label=u'Ваш e-mail', max_length=75, + error_messages={'invalid': u'Неверный формат e-mail.', 'required': u'Введите свой e-mail.',}) + + def __init__(self, *args, **kwargs): + super(ResetPasswordForm, self).__init__(*args, **kwargs) + self.user_cache = None # кешировать юзера в форме, чтобы повторно не ходить в базу из вьюхи + + def clean_email(self): + """Проверить зарегистрирован ли email.""" + email = self.cleaned_data['email'] + try: + User.objects.get(email__iexact = email) + return email + except User.DoesNotExist: + raise forms.ValidationError(u'Такой e-mail не зарегистрирован.') + + def clean(self): + super(ResetPasswordForm, self).clean() + email = self.cleaned_data.get('email') + if email: + self.user_cache = User.objects.get(email__iexact = email) + if self.user_cache: + if not self.user_cache.is_active: + raise forms.ValidationError(u'Пользователь заблокирован.') + return self.cleaned_data + + def get_user(self): + return self.user_cache + + +class ChangePasswordForm(forms.Form): + """Форма изменения пароля.""" + old_password = forms.CharField(label=u'Ваш пароль', widget=forms.PasswordInput) + + password1 = forms.CharField(label=u'Новый пароль', min_length=PASSWORD_MIN_LEN, widget=forms.PasswordInput, + error_messages={'min_length': u'Не менее %s символов.' % PASSWORD_MIN_LEN,}, + help_text=u'Не менее %s символов.' % PASSWORD_MIN_LEN) + + password2 = forms.CharField(label=u'Повторите', widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): + super(ChangePasswordForm, self).__init__(*args, **kwargs) + self._user = user + + def clean_old_password(self): + """Проверить старый пароль.""" + old_password = self.cleaned_data.get('old_password') + if old_password and not self._user.check_password(old_password): + raise forms.ValidationError(u'Неверный пароль.') + return old_password + + def clean(self): + super(ChangePasswordForm, self).clean() + password1 = self.cleaned_data.get('password1') + password2 = self.cleaned_data.get('password2') + # проверить чтобы оба новых пароля совпадали + if password1 and password2: + if password1 != password2: + set_field_error(self, 'password2', u'Пароли не совпадают.') + return self.cleaned_data + + +class ChangeEmailForm(forms.Form): + """Форма изменения e-mail.""" + password = forms.CharField(label=u'Ваш пароль', widget=forms.PasswordInput) + + email = forms.EmailField(label=u'Новый e-mail', max_length=75, + error_messages={'invalid': u'Неверный формат e-mail.', 'required': u'Введите свой e-mail.',}) + + def __init__(self, user, *args, **kwargs): + super(ChangeEmailForm, self).__init__(*args, **kwargs) + self._user = user + + def clean_password(self): + """Проверить пароль.""" + password = self.cleaned_data.get('password') + if password and not self._user.check_password(password): + raise forms.ValidationError(u'Неверный пароль.') + return password + + +class LoginForm(forms.Form): + """Форма логина.""" + email = forms.EmailField(label=u'E-mail', max_length=75) + password = forms.CharField(label=u'Пароль', widget=forms.PasswordInput) + + # TODO капча на случай если пароль не ввели правильно с первого раза + + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + self.user_cache = None # кешировать юзера в форме, чтобы повторно не лазить в базу из вьюхи + + def clean(self): + super(LoginForm, self).clean() + email = self.cleaned_data.get('email') + password = self.cleaned_data.get('password') + if email and password: + try: + username = User.objects.get(email__iexact = email).username + self.user_cache = authenticate(username = username, password = password) + if self.user_cache: + if not self.user_cache.is_active: + set_field_error(self, 'email', u'Пользователь заблокирован.') + else: + set_field_error(self, 'password', u'Неверное сочетание e-mail / пароль.') + except User.DoesNotExist: + set_field_error(self, 'email', u'Такой e-mail не зарегистрирован.') + return self.cleaned_data + + def get_user(self): + return self.user_cache diff --git a/project/myauth/managers.py b/project/myauth/managers.py new file mode 100644 index 0000000..e785d97 --- /dev/null +++ b/project/myauth/managers.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import hashlib +from random import random + +from django.db import models + + +class ConfirmEmailManager(models.Manager): + def confirm(self, user): + """Создает или обновляет запись, что email подтвержден.""" + rec, created = self.get_or_create(user=user, defaults={'is_confirmed': True,}) + return rec + + def unconfirm(self, user): + """Создает или обновляет запись, что нужно подтвердить email.""" + rec, created = self.get_or_create(user=user, defaults={'is_confirmed': False,}) + return rec + + +class ResetKeyManager(models.Manager): + def create_key(self, user): + """Создает или обновляет ключ восстановления пароля.""" + key = hashlib.sha1('%s' % random()).hexdigest() + reset_key, created = self.get_or_create(user=user, defaults={'key': key,}) + if not created: + reset_key.key = key # обновить ключ + reset_key.save() + return reset_key diff --git a/project/myauth/models.py b/project/myauth/models.py new file mode 100644 index 0000000..9f7e73b --- /dev/null +++ b/project/myauth/models.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.core.validators import RegexValidator, MinLengthValidator +from django.contrib.auth.models import User + +import managers + + +class ConfirmEmail(models.Model): + """Подтверждение Email.""" + user = models.OneToOneField(User, related_name='confirm_email', primary_key=True) + is_confirmed = models.BooleanField(u'email подтвержден?', default=False) + + created_at = models.DateTimeField(u'Создан', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + objects = managers.ConfirmEmailManager() + + class Meta: + verbose_name = u'подтверждение email' + verbose_name_plural = u'запросы подтверждения email' + ordering = ['-created_at',] + + def __unicode__(self): + status = u'не подтвержден' + if self.is_confirmed: + status = u'подтвержден' + return u'%s, email %s' % (self.user.email, status,) + + +class ResetKey(models.Model): + """Ключ на восстановление пароля.""" + user = models.OneToOneField(User, related_name='restore_key', primary_key=True) + + key = models.CharField(u'Ключ доступа', max_length=40, db_index=True, + validators=[ + RegexValidator(regex='[0-9a-f]{40}', + message=u'Введите значение длиной 40 символов, состоящее из цифр 0-9 и букв a-f.'), + MinLengthValidator(40), + ] + ) + + created_at = models.DateTimeField(u'создан', auto_now_add=True) + updated_at = models.DateTimeField(u'изменен', auto_now=True) + + objects = managers.ResetKeyManager() + + class Meta: + verbose_name = u'ключ восстановления пароля' + verbose_name_plural = u'ключи восстановления паролей' + ordering = ['-created_at',] + + def __unicode__(self): + return u'%s, %s' % (self.user.email, self.key,) diff --git a/project/myauth/tests.py b/project/myauth/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/project/myauth/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/project/myauth/urls.py b/project/myauth/urls.py new file mode 100644 index 0000000..63a970d --- /dev/null +++ b/project/myauth/urls.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from django.conf.urls import * +from django.views.generic import TemplateView + +import views + + +urlpatterns = patterns('', + url(r'^register/$', views.register, name='myauth_register'), + url(r'^login/$', views.login, name='myauth_login'), + + url(r'^confirm/email/(?P[0-9a-f]{30})/$', views.confirm_registered_email, name='myauth_confirm_email'), + + url(r'^reset/$', views.reset, name='myauth_reset'), + url(r'^reset/ready/$', TemplateView.as_view(template_name="myauth/reset_key_ready.html"), name='myauth_reset_key_ready'), + url(r'^reset/(?P[0-9a-f]{40})/$', views.confirm_reset, name='myauth_confirm_reset'), + + url(r'^change/password/$', views.change_password, name='myauth_change_password'), + url(r'^change/email/$', views.change_email, name='myauth_change_email'), + + url(r'^closed/$', TemplateView.as_view(template_name='myauth/registration_closed.html'), + name='myauth_registration_closed'), + + url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'}, name='auth_logout'), +) diff --git a/project/myauth/views.py b/project/myauth/views.py new file mode 100644 index 0000000..83fd08d --- /dev/null +++ b/project/myauth/views.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +import hashlib +from random import random + +from django.shortcuts import render, redirect, get_object_or_404 +from django.core.urlresolvers import reverse +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters +from django.contrib import auth +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.conf import settings + +from project.customer.models import UserProfile, UserProfileFilters + +from . import forms, models, emails + + +REGISTRATION_OPEN = getattr(settings, 'REGISTRATION_OPEN', True) + + +@sensitive_variables() +def _create_user(request, **kwargs): + # создать юзера + email, password = kwargs['email'], kwargs['password1'] + # сгенерировать имя пользователя. на всякий случай, добавить к нему соль, чтобы снизить вероятность коллизий + username = hashlib.sha1(u'%s%s' % (email, random())).hexdigest()[:30] + user = User.objects.create_user(username=username, email=email, password=password) + # создать пустой профиль + profile_type = kwargs['profile_type'] + UserProfile.objects.create_profile(user=user, profile_type=profile_type) + # создать фильтры профиля + UserProfileFilters.objects.create_filters(user=user) + # создать запись, что email не подтверждён + models.ConfirmEmail.objects.unconfirm(user) + # аутентифицировать и залогинить + new_user = auth.authenticate(username=username, password=password) + auth.login(request, new_user) + return new_user + + +@sensitive_variables() +@sensitive_post_parameters() +@csrf_protect +def register(request): + """ + Регистрация нового пользователя. + + Алгоритм регистрации такой: + - юзер в форме вводит свой email, пароль и форму собственности; + - форма сабмитится во вьюху и проверяется на валидность; + - генерится хеш по юзерскому email - т.к. регистрация и логин по email, а поле User.username всего лишь 30 символов, + то храню email не в нем, а в соответсвующем поле, ну а username генерирую "левый"; + - создается юзер, пока с пустым профилем; + - юзера аутентифицирует и логинит в систему; + - после чего редиректит на страницу редактирования профиля. + """ + form_class = forms.RegistrationForm + form_prefix = 'register' + template_name = 'myauth/register.html' + success_url = 'customer_profile_edit' + registration_closed_url = 'myauth_registration_closed' + + if not REGISTRATION_OPEN: + return redirect(registration_closed_url) + + if request.method == 'POST': + form = form_class(data=request.POST, prefix=form_prefix) + if form.is_valid(): + new_user = _create_user(request, **form.cleaned_data) + confirm_url = reverse('myauth_confirm_email', args=[new_user.username,]) + emails.send_registration_email(new_user.email, confirm_url) + return redirect(success_url) + else: + form = form_class(prefix=form_prefix) + + return render(request, template_name, {'form': form,}) + + +@sensitive_variables() +def confirm_registered_email(request, key): + """Подтверждение зарегистрированного email.""" + success_url = 'customer_profile_view' + success_msg = u'E-mail подтверждён.' + + user = get_object_or_404(User, username__iexact = key) # ключ = имя пользователя + models.ConfirmEmail.objects.confirm(user) + messages.add_message(request, messages.INFO, success_msg) + + return redirect(success_url) + + +@sensitive_variables() +@sensitive_post_parameters() +@csrf_protect +def reset(request): + """Запросить ключ восстановления пароля.""" + form_class = forms.ResetPasswordForm + form_prefix='reset' + template_name = 'myauth/reset.html' + success_url = 'myauth_reset_key_ready' + + if request.method == 'POST': + form = form_class(data=request.POST, prefix=form_prefix) + if form.is_valid(): + user = form.get_user() + key = models.ResetKey.objects.create_key(user) + confirm_url = reverse('myauth_confirm_reset', args=[key.key,]) + emails.send_reset_password_email(user.email, confirm_url) + return redirect(success_url) + else: + form = form_class(prefix=form_prefix) + + return render(request, template_name, {'form': form,}) + + +@sensitive_variables() +def confirm_reset(request, key): + """Подтверждение запроса на восстановление пароля. + Генерирует новый пароль и отправляет его на почту пользователю. + """ + success_url = 'customer_profile_view' + success_msg = u'Новый пароль выслан на ваш e-mail.' + + key = get_object_or_404(models.ResetKey, key__iexact = key) + new_password = User.objects.make_random_password() # новый пароль + key.user.set_password(new_password) + key.user.save() + emails.send_new_password_email(key.user.email, new_password) + key.delete() # удалить ключ восстановления пароля + messages.add_message(request, messages.INFO, success_msg) + + return redirect(success_url) + + +@sensitive_variables() +@sensitive_post_parameters() +@login_required +@csrf_protect +def change_password(request): + form_class = forms.ChangePasswordForm + form_prefix = 'change_password' + template_name = 'myauth/change_password.html' + success_url = 'customer_profile_view' + success_msg = u'Ваш пароль изменён на новый.' + + if request.method == 'POST': + form = form_class(user=request.user, data=request.POST, prefix=form_prefix) + if form.is_valid(): + new_password = form.cleaned_data['password1'] + request.user.set_password(new_password) + request.user.save() + messages.add_message(request, messages.INFO, success_msg) + return redirect(success_url) + else: + form = form_class(user=request.user, prefix=form_prefix) + + return render(request, template_name, {'form': form,}) + + +@sensitive_variables() +@sensitive_post_parameters() +@login_required +@csrf_protect +def change_email(request): + form_class = forms.ChangeEmailForm + form_prefix = 'change_email' + template_name = 'myauth/change_email.html' + success_url = 'customer_profile_view' + success_msg = u'Ваш e-mail изменён на новый.' + + if request.method == 'POST': + form = form_class(user=request.user, data=request.POST, prefix=form_prefix) + if form.is_valid(): + new_email = form.cleaned_data['email'] + request.user.email = new_email + request.user.save() + models.ConfirmEmail.objects.unconfirm(request.user) + messages.add_message(request, messages.INFO, success_msg) + return redirect(success_url) + else: + form = form_class(user=request.user, prefix=form_prefix) + + return render(request, template_name, {'form': form,}) + + +@sensitive_variables() +@sensitive_post_parameters() +@csrf_protect +def login(request): + """Вход в систему.""" + form_class = forms.LoginForm + form_prefix = 'login' + template_name = 'myauth/login.html' + success_url = 'customer_profile_view' + + if request.method == 'POST': + form = form_class(data=request.POST, prefix=form_prefix) + if form.is_valid(): + auth.login(request, form.get_user()) + return redirect(success_url) + else: + form = form_class(prefix=form_prefix) + + return render(request, template_name, {'form': form,}) diff --git a/project/pages/__init__.py b/project/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/pages/models.py b/project/pages/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/project/pages/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/project/pages/tests.py b/project/pages/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/project/pages/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/project/pages/views.py b/project/pages/views.py new file mode 100644 index 0000000..2fef2a2 --- /dev/null +++ b/project/pages/views.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render + + +def site_index(request): + """Главная страница сайта.""" + template_name = 'pages/index.html' + return render(request, template_name) diff --git a/project/pdf_fonts/arial.ttf b/project/pdf_fonts/arial.ttf new file mode 100644 index 0000000..ff0815c Binary files /dev/null and b/project/pdf_fonts/arial.ttf differ diff --git a/project/pdf_fonts/arialbd.ttf b/project/pdf_fonts/arialbd.ttf new file mode 100644 index 0000000..d0d857e Binary files /dev/null and b/project/pdf_fonts/arialbd.ttf differ diff --git a/project/settings.py b/project/settings.py new file mode 100644 index 0000000..35b5ef5 --- /dev/null +++ b/project/settings.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- + +# Django settings for project project. + +import os +from imp import find_module + +path = lambda *xs: os.path.abspath(os.path.join(os.path.dirname(__file__), *xs)) + + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '', # Or path to database file if using sqlite3. + # The following settings are not used with sqlite3: + 'USER': '', + 'PASSWORD': '', + 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. + 'PORT': '', # Set to empty string for default. + } +} + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'Europe/Moscow' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'ru' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/var/www/example.com/media/" +MEDIA_ROOT = path('../_public_html/media') + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://example.com/media/", "http://media.example.com/" +MEDIA_URL = '/m/' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/var/www/example.com/static/" +STATIC_ROOT = path('../_public_html/static') + +# URL prefix for static files. +# Example: "http://example.com/static/", "http://static.example.com/" +STATIC_URL = '/s/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + path('static'), + ('', os.path.join(os.path.abspath(find_module('debug_toolbar')[1]), 'media')), +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'uqrv8bpqt-#up^ay-)^@bcjjgq1^jy8qc!fkr!1wd*u%)1dg#y' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + + # my + 'project.customer.middleware.ProfileMiddleware', +) + +ROOT_URLCONF = 'project.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'project.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + path('templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + 'django.contrib.admindocs', + + 'pytils', + 'django_filters', + + # my apps + 'project.commons', + 'project.myauth', + 'project.customer', + 'project.docs', + 'project.pages', + + # keep it last + 'south', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +SERVER_EMAIL = 'dokumentor@localhost' + +REGISTRATION_OPEN = True + +LOGIN_URL = '/user/login/' + +SUPPORT_EMAIL = 'help@dokumentor.ru' + +PDF_FONTS_ROOT = path('pdf_fonts') + +XLS_ROOT = path('xls_templates') + +try: + from project.local_settings import * +except ImportError: + pass diff --git a/project/static/css/custom_admin.css b/project/static/css/custom_admin.css new file mode 100644 index 0000000..3a51d43 --- /dev/null +++ b/project/static/css/custom_admin.css @@ -0,0 +1 @@ +.aligned label { width: 15em; } diff --git a/project/static/css/style.css b/project/static/css/style.css new file mode 100644 index 0000000..06fa24d --- /dev/null +++ b/project/static/css/style.css @@ -0,0 +1,205 @@ +.clear { clear: both; } +.left { float: left; } +.right { float: right; } +.center { text-align: center; } + +body { + font-family: Arial,Helvetica,sans-serif; + font-size: small; + padding: 0; + margin: 0 auto; + width: 1000px; + border: 0; + height: 100%; + line-height: 120%; +} + +a { color: #8D381D; cursor: pointer; text-decoration: underline; } +a img { outline: none; border: 0; } + +h1, h2, h3 { line-height: normal; } + +ul.messagelist { padding: 0 0 5px 0; margin: 0; } +ul.messagelist li { + font-size: 12px; + display: block; + padding: 4px 5px 4px 25px; + margin: 0 0 3px 0; + border-bottom: 1px solid #ddd; + color: #666; + background: #ffc url(../img/icon-success.gif) 5px .3em no-repeat; +} +ul.messagelist li.warning { background-image: url(../img/icon-alert.gif); } +ul.messagelist li.error { background-image: url(../img/icon-error.gif); } + +.errorlist { font-size: 8pt; overflow: hidden; } +.errorlist li { color: red; } + +.help-text { font-size: 8pt; color: #666666; } + +form { font-size: small; padding: 0; margin: 0; } + +input, select, textarea { font-family: Arial,Helvetica,sans-serif; font-size: small; } + +fieldset { + padding: 0 0 10px; + border-style: none none solid; + border-width: 0 0 1px; + border-color: #777; +} +textarea { width: 99%; } + +input[type=text], input[type=password], select, textarea { border: 1px solid #777; } +input[type=text], input[type=password], textarea, option { padding-left: 2px; margin-left: 2px; } + +.long-input input { width: 350px; } + +.field { margin: 10px 0; } +.block { margin: 10px 0; } + +.client-form .col1 { float: left; width: 350px; } +.client-form .col2 { float: left; width: 290px; } +.client-form input[type=text], .client-form input[type=password], .client-form textarea, .client-form option { + padding-left: 2px; + margin-left: 2px; + width: 200px; +} + +/*.client-form .buttons { padding: 10px 0 6px 33px; }*/ + +.profile-col1 { float: left; width: 70%; } +.profile-col2 { float: left; width: 30%; } + +.info-bar { background-color: #f5f5f5; font-size: 11px; } + +ul { clear: both; list-style: none; margin: 0; padding: 0; } + +.has-datepicker { background: url(../img/icon-calendar.gif) no-repeat scroll right center transparent; } + +.profile-form input#id_ip_reg_date { background: url(../img/icon-calendar.gif) no-repeat scroll right center transparent; } +#id_phone_code, #id_fax_code { width: 60px; } + +#accounts .account-delete a { text-decoration: none; } +#accounts .account-delete img { vertical-align: middle; } +#accounts .account-add img { vertical-align: middle; margin-right: 6px; } +#accounts .main { color: green; font-size: 11px; text-align: center; padding-left: 10px; vertical-align: top; } + +.add-link img { vertical-align: middle; margin-right: 6px; } +.edit-link a, .delete-link a { text-decoration: none; } + +.doc-panel .edit-link img, .doc-panel .delete-link img, .doc-panel .email-link img, + .doc-panel .pdf-link img, .doc-panel .excel-link img + { vertical-align: middle; margin-right: 6px; } + +a.delete { vertical-align: top; } + +.profile-filters-form div { margin: 7px 0; padding: 0; clear: both; } +.profile-filters-form input[type=checkbox] { margin: 0; padding: 0; } +.profile-filters-form .level-2 { margin-left: 15px; } +.profile-filters-form .accounts ul { margin-left: 11px; } +.profile-filters-form .accounts ul li { margin: 4px 0; } +.profile-filters-form .accounts ul li span.name { margin-left: 25px; } + +.doc-form { padding-left: 2px; } +.doc-form input[type=text], input[type=password], textarea, option { padding-left: 2px; margin-left: 0; } + +.doc-form #doc_date, .doc-form #nds_type, + .doc-form #doc_mesto, .doc-form #end_date, + .doc-form #doc_expire_date, .doc-form #dover_doc_date, .doc-form #dover_passport_num + { margin-left: 10px; } + +.doc-form #saldo_debit input { margin-left: 12px; } +.doc-form #saldo_credit input { margin-left: 5px; } +.doc-form #saldo_debit .errorlist, .doc-form #saldo_credit .errorlist { margin-left: 83px; } + +.doc-form #dover_name input { margin-left: 49px; } +.doc-form #dover_passport_ser input#id_dover_passport_ser { margin-left: 5px; } +.doc-form #dover_passport_org input { margin-left: 85px; } +.doc-form #dover_passport_date input { margin-left: 74px; } + +.doc-form #dover_name .help-text, + .doc-form #dover_name .errorlist, .doc-form #dover_passport_ser .errorlist, + .doc-form #dover_passport_org .errorlist, .doc-form #dover_passport_date .errorlist + { margin-left: 169px; } + +.doc-form select#id_bank_account { width: 370px; } +.doc-form select#id_client { width: 370px; } +.doc-form input#id_doc_mesto { width: 340px; } + +.doc-form #platej_type, .doc-form #doc_total, .doc-form #payment_type { margin-right: 10px; } +.doc-form #platej_type select { width: 200px; } + +.doc-form #tax_status select, .doc-form #tax_base select, .doc-form #tax_type select { width: 430px; } + +.doc-form #tax_num, .doc-form #tax_date { width: 124px; } +.doc-form #tax_date { margin-left: 10px; } +.doc-form #tax_num input, .doc-form #tax_date input { width: 120px; } + +.doc-form #tax_bk input, .doc-form #tax_okato input, .doc-form #tax_period input { width: 254px; } + +.doc-form #doc_info { width: 436px; } + +.doc-form #nds_type { padding-top: 16px; } +.doc-form #nds_type ul li { float: left; } + +.doc-form table.list td.name input[type=text] { width: 334px; } +.doc-form table.list td.qty input[type=text] { width: 68px; } +.doc-form table.list td.units input[type=text] { width: 50px; } +.doc-form table.list td.price input[type=text] { width: 72px; } +.doc-form table.list td.total_price input[type=text] { width: 84px; } + +.doc-form table.list.aktsverki td.name input[type=text] { width: 432px; } +.doc-form table.list.aktsverki td.debit input[type=text] { width: 96px; } +.doc-form table.list.aktsverki td.credit input[type=text] { width: 96px; } + +.doc-form table.list.dover td.name input[type=text] { width: 432px; } +.doc-form table.list.dover td.qty input[type=text] { width: 96px; } +.doc-form table.list.dover td.units input[type=text] { width: 96px; } + +.doc-email-form input#id_to { width: 99% } +.doc-email-form textarea { width: 99%; margin-left: 2px; } +.doc-email-form #doc_format ul li { display: inline; } + +.filters #id_client, .filters #id_invoice { width: 99%; } + +#dialogs { display: none; } + +.required { color: red; } + +.ajax-form .buttons { padding: 10px 0 6px; text-align: center; } + +.button-as-link { + margin: 0; + padding: 0; + color: #0000ff; + background: none; + border: none; + text-decoration: underline; + cursor: pointer; +} + +.close-form { + margin-left: 30px; + text-decoration: underline; + color: #0000ff; + background-color: transparent; + border: none; + cursor: pointer; +} + +.errors-layout ul { list-style: none; margin-bottom: 10px; padding: 5px 10px; border: 1px solid red; } + +table.list { width: 100%; border: none; font-size: small; } +table.list th { background: #f5f5f5; text-align: left; font-size: 11px; } +table.list tr.even { background: #fff; } +table.list tr.odd { background: #eee; } +table.list td { word-break: break-all; padding: 5px 0; } + +.filters p { margin: 15px 0 0; font-weight: bold; } +.filters ul li a.selected { border-left: 5px solid #ccc; margin-left: -10px; padding-left: 5px; } + +.pagination { font-size: small; color: black; margin-top: 5px; padding-top: 1ex; width: 99%; border-top: 1px solid; } + +/* blockUI */ +div.blockOverlay { background: url('../img/ajax-loader.gif') no-repeat center center; z-index: 99999; } +div.blockMsg { width: 100%; height: 100%; top: 0; left: 0; text-align: center; } diff --git a/project/static/css/ui-lightness/images/animated-overlay.gif b/project/static/css/ui-lightness/images/animated-overlay.gif new file mode 100644 index 0000000..d441f75 Binary files /dev/null and b/project/static/css/ui-lightness/images/animated-overlay.gif differ diff --git a/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png new file mode 100644 index 0000000..f41d006 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png new file mode 100644 index 0000000..cda0472 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/project/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png new file mode 100644 index 0000000..76fbb6c Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/project/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png new file mode 100644 index 0000000..e1e7a4d Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png b/project/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png new file mode 100644 index 0000000..e133622 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png b/project/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 0000000..2533c60 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/project/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png new file mode 100644 index 0000000..37766c3 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/project/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png new file mode 100644 index 0000000..b60253d Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ diff --git a/project/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/project/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png new file mode 100644 index 0000000..38d06d7 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ diff --git a/project/static/css/ui-lightness/images/ui-icons_222222_256x240.png b/project/static/css/ui-lightness/images/ui-icons_222222_256x240.png new file mode 100644 index 0000000..c1cb117 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-icons_222222_256x240.png differ diff --git a/project/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png b/project/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png new file mode 100644 index 0000000..3a0140c Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png differ diff --git a/project/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png b/project/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png new file mode 100644 index 0000000..036ee07 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png differ diff --git a/project/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png b/project/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png new file mode 100644 index 0000000..8b6c058 Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png differ diff --git a/project/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png b/project/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000..4f624bb Binary files /dev/null and b/project/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png differ diff --git a/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.css b/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.css new file mode 100644 index 0000000..ebb4726 --- /dev/null +++ b/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.css @@ -0,0 +1,1177 @@ +/*! jQuery UI - v1.10.3 - 2013-10-05 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin-top: 2px; + padding: .5em .5em .5em .7em; + min-height: 0; /* support: IE7 */ +} +.ui-accordion .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-noicons { + padding-left: .7em; +} +.ui-accordion .ui-accordion-icons .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-accordion-header-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-button { + display: inline-block; + position: relative; + padding: 0; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + overflow: visible; /* removes extra width in IE */ +} +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2.2em; +} +/* button elements seem to need a little more width */ +button.ui-button-icon-only { + width: 2.4em; +} +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} + +/* button text element */ +.ui-button .ui-button-text { + display: block; + line-height: normal; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, +.ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon, +.ui-button-text-icon-primary .ui-icon, +.ui-button-text-icon-secondary .ui-icon, +.ui-button-text-icons .ui-icon, +.ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, +.ui-button-text-icons .ui-button-icon-primary, +.ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, +.ui-button-text-icons .ui-button-icon-secondary, +.ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} + +/* button sets */ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ +/* reset extra padding in Firefox, see h5bp.com/l */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 49%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 21px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 12px; + height: 12px; + right: -5px; + bottom: -5px; + background-position: 16px 16px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-menu { + list-style: none; + padding: 2px; + margin: 0; + display: block; + outline: none; +} +.ui-menu .ui-menu { + margin-top: -3px; + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + padding: 0; + width: 100%; + /* support: IE10, see #8844 */ + list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7); +} +.ui-menu .ui-menu-divider { + margin: 5px -2px 5px -2px; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-menu-item a { + text-decoration: none; + display: block; + padding: 2px .4em; + line-height: 1.5; + min-height: 0; /* support: IE7 */ + font-weight: normal; +} +.ui-menu .ui-menu-item a.ui-state-focus, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} + +.ui-menu .ui-state-disabled { + font-weight: normal; + margin: .4em 0 .2em; + line-height: 1.5; +} +.ui-menu .ui-state-disabled a { + cursor: default; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item a { + position: relative; + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: .2em; + left: .2em; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + position: static; + float: right; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("images/animated-overlay.gif"); + height: 100%; + filter: alpha(opacity=25); + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* For IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 22px; +} +.ui-spinner-button { + width: 16px; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to overide default borders */ +.ui-spinner a.ui-spinner-button { + border-top: none; + border-bottom: none; + border-right: none; +} +/* vertical centre icon */ +.ui-spinner .ui-icon { + position: absolute; + margin-top: -8px; + top: 50%; + left: 0; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} + +/* TR overrides */ +.ui-spinner .ui-icon-triangle-1-s { + /* need to fix icons sprite */ + background-position: -65px -16px; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav li a { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active a, +.ui-tabs .ui-tabs-nav li.ui-state-disabled a, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading a { + cursor: text; +} +.ui-tabs .ui-tabs-nav li a, /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; + -webkit-box-shadow: 0 0 5px #aaa; + box-shadow: 0 0 5px #aaa; +} +body .ui-tooltip { + border-width: 2px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #e78f08; + background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #cccccc; + background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x; + font-weight: bold; + color: #1c94c4; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #1c94c4; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #fbcb09; + background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x; + font-weight: bold; + color: #c77405; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited { + color: #c77405; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #fbd850; + background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; + font-weight: bold; + color: #eb8f00; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #eb8f00; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fed22f; + background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat; + color: #ffffff; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #ffffff; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #ffffff; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* For IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url(images/ui-icons_222222_256x240.png); +} +.ui-widget-header .ui-icon { + background-image: url(images/ui-icons_ffffff_256x240.png); +} +.ui-state-default .ui-icon { + background-image: url(images/ui-icons_ef8c08_256x240.png); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url(images/ui-icons_ef8c08_256x240.png); +} +.ui-state-active .ui-icon { + background-image: url(images/ui-icons_ef8c08_256x240.png); +} +.ui-state-highlight .ui-icon { + background-image: url(images/ui-icons_228ef1_256x240.png); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url(images/ui-icons_ffd27a_256x240.png); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; + opacity: .5; + filter: Alpha(Opacity=50); +} +.ui-widget-shadow { + margin: -5px 0 0 -5px; + padding: 5px; + background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; + opacity: .2; + filter: Alpha(Opacity=20); + border-radius: 5px; +} diff --git a/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.min.css b/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.min.css new file mode 100644 index 0000000..a6d97f0 --- /dev/null +++ b/project/static/css/ui-lightness/jquery-ui-1.10.3.custom.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.3 - 2013-10-05 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;min-height:0}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month-year{width:100%}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:21px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("images/animated-overlay.gif");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav li a{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active a,.ui-tabs .ui-tabs-nav li.ui-state-disabled a,.ui-tabs .ui-tabs-nav li.ui-tabs-loading a{cursor:text}.ui-tabs .ui-tabs-nav li a,.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_ffffff_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_ef8c08_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_ef8c08_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_ef8c08_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_228ef1_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_ffd27a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} \ No newline at end of file diff --git a/project/static/img/ajax-loader.gif b/project/static/img/ajax-loader.gif new file mode 100644 index 0000000..34bdcc2 Binary files /dev/null and b/project/static/img/ajax-loader.gif differ diff --git a/project/static/img/icon-add.gif b/project/static/img/icon-add.gif new file mode 100644 index 0000000..6b5cba8 Binary files /dev/null and b/project/static/img/icon-add.gif differ diff --git a/project/static/img/icon-alert.gif b/project/static/img/icon-alert.gif new file mode 100644 index 0000000..a1dde26 Binary files /dev/null and b/project/static/img/icon-alert.gif differ diff --git a/project/static/img/icon-calendar.gif b/project/static/img/icon-calendar.gif new file mode 100644 index 0000000..7ecd5d3 Binary files /dev/null and b/project/static/img/icon-calendar.gif differ diff --git a/project/static/img/icon-delete.gif b/project/static/img/icon-delete.gif new file mode 100644 index 0000000..5ca6372 Binary files /dev/null and b/project/static/img/icon-delete.gif differ diff --git a/project/static/img/icon-edit.gif b/project/static/img/icon-edit.gif new file mode 100644 index 0000000..07621df Binary files /dev/null and b/project/static/img/icon-edit.gif differ diff --git a/project/static/img/icon-email.gif b/project/static/img/icon-email.gif new file mode 100644 index 0000000..8c9961e Binary files /dev/null and b/project/static/img/icon-email.gif differ diff --git a/project/static/img/icon-error.gif b/project/static/img/icon-error.gif new file mode 100644 index 0000000..3730a00 Binary files /dev/null and b/project/static/img/icon-error.gif differ diff --git a/project/static/img/icon-excel.gif b/project/static/img/icon-excel.gif new file mode 100644 index 0000000..4abad08 Binary files /dev/null and b/project/static/img/icon-excel.gif differ diff --git a/project/static/img/icon-pdf.gif b/project/static/img/icon-pdf.gif new file mode 100644 index 0000000..aaac3d0 Binary files /dev/null and b/project/static/img/icon-pdf.gif differ diff --git a/project/static/img/icon-success.gif b/project/static/img/icon-success.gif new file mode 100644 index 0000000..5cf90a1 Binary files /dev/null and b/project/static/img/icon-success.gif differ diff --git a/project/static/img/left-arrow.gif b/project/static/img/left-arrow.gif new file mode 100644 index 0000000..181092f Binary files /dev/null and b/project/static/img/left-arrow.gif differ diff --git a/project/static/js/client.commons.js b/project/static/js/client.commons.js new file mode 100644 index 0000000..f9e5f02 --- /dev/null +++ b/project/static/js/client.commons.js @@ -0,0 +1,94 @@ +function setup_client_edit_form(form) { + form.dialog({ + modal: true, + autoOpen: false, + minWidth: 670 + }); + + $('button[name=close-form]', form).click(function() { + form.dialog('close'); + return false; + }); +} + +function setup_client_delete_form(form) { + form.dialog({ + modal: true, + autoOpen: false + }); + + $('button[name=close-form]', form).click(function() { + form.dialog('close'); + return false; + }); +} + +function setup_client_edit_links(form, reload_on_success) { + $('table#clients td a.client.edit-link').each(function() { + $(this).click(function() { + var link = $(this); + + var form_action = link.attr('href') + 'ajax/'; // url to post form + if (typeof(reload_on_success)!=='undefined' && reload_on_success) + form_action += '?reload_on_success'; + form.attr('action', form_action); // update form action + form.dialog({title: link.attr('title')}); + + form.clearForm(); + clear_form_errors(form); + + var get_url = link.attr('href').replace('edit/', 'get/ajax/'); // url to fetch client fields + var obj_values = fetch_data(get_url); + + update_form_fields(form, obj_values); + + form.dialog('open'); + return false; + }); + }); +} + +function setup_client_delete_links(form, reload_on_success) { + $('table#clients td a.client.delete-link').each(function() { + $(this).click(function() { + var link = $(this); + + var form_action = link.attr('href') + 'ajax/'; // url to post form + if (typeof(reload_on_success)!=='undefined' && reload_on_success) + form_action += '?reload_on_success'; + form.attr('action', form_action); // update form action + //form.dialog({title: link.attr('title')}); + + form.clearForm(); + clear_form_errors(form); + + var get_url = link.attr('href').replace('delete/', 'get/ajax/'); // url to fetch account fields + var obj_values = fetch_data(get_url); + + $('span.client', form).html(obj_values.name + ', ИНН ' + obj_values.inn); + + form.dialog('open'); + return false; + }); + }); +} + +function setup_client_add_link(form, reload_on_success) { + $('a.client.add-link').each(function() { + $(this).click(function() { + var link = $(this); + + var form_action = link.attr('href') + 'ajax/'; // url to post form + if (typeof(reload_on_success)!=='undefined' && reload_on_success) + form_action += '?reload_on_success'; + form.attr('action', form_action); // update form action + form.dialog({title: link.attr('title')}); + + form.clearForm(); + clear_form_errors(form); + + form.dialog('open'); + return false; + }); + }); +} diff --git a/project/static/js/client.list.js b/project/static/js/client.list.js new file mode 100644 index 0000000..fd963e2 --- /dev/null +++ b/project/static/js/client.list.js @@ -0,0 +1,14 @@ +$(document).ready(function() { + var edit_form = $('#client-edit-form'); + if (edit_form) { + setup_client_edit_form(edit_form); + setup_client_edit_links(edit_form, true); + setup_client_add_link(edit_form, true); + } + + var delete_form = $('#client-delete-form'); + if (delete_form) { + setup_client_edit_form(delete_form); + setup_client_delete_links(delete_form, true); + } +}); diff --git a/project/static/js/commons.js b/project/static/js/commons.js new file mode 100644 index 0000000..c393cd3 --- /dev/null +++ b/project/static/js/commons.js @@ -0,0 +1,33 @@ +$(document).ready(function() { + $('.has-datepicker').datepicker({dateFormat: 'dd.mm.yy'}); +}); + +function fetch_data(url, async) { + // makes ajax call (synced by default) + var result = null; + $.ajax({ + 'async': async || false, + 'global': false, + 'cache': false, + 'url': url, + 'dataType': "json", + 'timeout': 30000, + 'success': function (data) { + result = data; + //console.log('fetch_data = ', data); + } + }); + return result; +} + +function getUrlVars() { + var vars = [], hash; + var href = window.location.href; + var hashes = href.slice(href.indexOf('?') + 1).split('&'); + for (var i=0; i tbody ??? + var tbl_rows_num = tbl_rows.length; + + tbl_rows.hide(); // hide all rows + + for (var i in accounts) { + if (accounts.hasOwnProperty(i)) { + var account = accounts[i]; + var tbl_row = null; + + var was_cloned = false; + if (i < tbl_rows_num) { // fill existing row + tbl_row = tbl_rows[i]; + } + else { // add extra row + var last_row = $('tr[class="account"]', table).filter(':last'); + tbl_row = last_row.clone(true).insertAfter(last_row); + was_cloned = true; + } + + // update edit link + var edit_link = $('a[id^="' + BANK_ACCOUNT['edit_id_prefix'] + '"]', tbl_row); + edit_link.attr('id', BANK_ACCOUNT['edit_id_prefix'] + account['pk']); + edit_link.attr('href', account['edit_url']); + edit_link.text(account['account']); + + // update delete link + var delete_link = $('a[id^="' + BANK_ACCOUNT['delete_id_prefix'] + '"]', tbl_row); + delete_link.attr('id', BANK_ACCOUNT['delete_id_prefix'] + account['pk']); + delete_link.attr('href', account['delete_url']); + + // update bank name + var bank = $('span[id^="' + BANK_ACCOUNT['bank_id_prefix'] + '"]', tbl_row); + bank.attr('id', BANK_ACCOUNT['bank_id_prefix'] + account['pk']); + bank.text(account['name']); + + // clear account type if needed + if (was_cloned && i >= 2) + $('td[class="account-type"]', tbl_row).text(''); + + $(tbl_row).show(); // show that row + } + } +} diff --git a/project/static/js/customer/profile.view.js b/project/static/js/customer/profile.view.js new file mode 100644 index 0000000..0be2b74 --- /dev/null +++ b/project/static/js/customer/profile.view.js @@ -0,0 +1,129 @@ +/* Dependencies: + PROFILE_FILTERS['edit_url'] +*/ + + +// view +$(document).ready(function() { + var $profile = $('#profile'); + var $profile_info = $('.info', $profile); + var $profile_org_boss_info = $('.org-boss-info', $profile); + var $profile_accounts = $('.accounts', $profile); + var $profile_contacts = $('.contacts', $profile); + + var $filters_form = $('#profile-filters-form'); + var $filters_info = $('.info', $filters_form); + var $filters_org_boss_info = $('.org-boss-info', $filters_form); + var $filters_accounts = $('.accounts', $filters_form); + var $filters_contacts = $('.contacts', $filters_form); + + // save the list of permanently disabled filters (having set attr 'disabled') + var $always_disabled_filters = $(':input:disabled', $filters_form); + + // init profile blocks and fields + $(':input', $filters_form).each(function() { + var el = $(this); + var field_id = '#' + el.prop('name').substring('show_'.length); + el.is(':checked') ? $(field_id, $profile).show() : $(field_id, $profile).hide(); + }); + + // init profile accounts + $('[id^=account]', $profile_accounts).hide(); + var account_id = '#account_' + $('input[name=bank_account]:checked', $filters_accounts).val(); + $(account_id, $profile_accounts).show(); + + // init filter blocks + _toggle_filters($('#id_show_profile_type:input', $filters_form), $filters_info); + _toggle_filters($('#id_show_org_boss_title_and_fio:input', $filters_form), $filters_org_boss_info); + _toggle_filters($('#id_show_bank_account:input', $filters_form), $filters_accounts); + _toggle_filters($('#id_show_contact_info:input', $filters_form), $filters_contacts); + + // on click show/hide filter info block + $('#id_show_profile_type:input', $filters_form).click(function() { + var el = $(this); + _toggle_filters(el, $filters_info); + el.is(':checked') ? $($profile_info).show() : $($profile_info).hide(); + }); + + // on click show/hide filter org-boss-info block + $('#id_show_org_boss_title_and_fio:input', $filters_form).click(function() { + var el = $(this); + _toggle_filters(el, $filters_org_boss_info); + el.is(':checked') ? $($profile_org_boss_info).show() : $($profile_org_boss_info).hide(); + }); + + // on click show/hide filter accounts block + $('#id_show_bank_account:input', $filters_form).click(function() { + var el = $(this); + _toggle_filters(el, $filters_accounts); + if (el.is(':checked')) { + var account_id = '#account_' + $('input[name=bank_account]:checked', $filters_accounts).val(); + $(account_id, $profile_accounts).show(); + } + else + $('[id^=account_]', $profile_accounts).hide(); + }); + + // on click show/hide filter contacts block + $('#id_show_contact_info:input', $filters_form).click(function() { + var el = $(this); + _toggle_filters(el, $filters_contacts); + el.is(':checked') ? $($profile_contacts).show() : $($profile_contacts).hide(); + }); + + // on click show/hide profile fields + $(':input', $filters_form).click(function() { + var el = $(this); + var field_id = '#' + el.prop('name').substring('show_'.length); + el.is(':checked') ? $(field_id, $profile).show() : $(field_id, $profile).hide(); + }); + + // on click show/hide profile accounts + $('input[name=bank_account]', $filters_form).click(function() { + var el = $(this); + var curr_acc = $('[id^=account_]:visible', $profile_accounts); + var new_acc = $('#account_' + el.val(), $profile_accounts); + if (new_acc.prop('id') !== curr_acc.prop('id')) { + curr_acc.hide(); + new_acc.show(); + } + }); + + // dialogs + var email_form = $('#email-profile-form'); + + email_form.dialog({ + modal: true, + autoOpen: false + }); + + $('button[name=close-form]', email_form).click(function() { + email_form.dialog('close'); + return false; + }); + + email_form.submit(function() { + $.post(PROFILE_FILTERS['edit_url'], $filters_form.serialize()); // save current filters + }); + + $('input[name=email-pdf]', $filters_form).click(function() { + email_form.dialog('open'); + return false; + }); + + // --- + + function _toggle_filters(el, dest) { + if (el.is(':checked')) { + $(':input', dest).not(el).not($always_disabled_filters).removeAttr('disabled'); + $(':input:hidden', dest).not(el).not($always_disabled_filters).remove(); + } + else { + // disable sub-filters + $(':input', dest).not(el).not($always_disabled_filters).prop('disabled', true); + // keep checked sub-filters: duplicate as hidden input + $(':input:visible:disabled:checked', dest).not(el).not($always_disabled_filters) + .clone().hide().removeAttr('disabled').appendTo(dest); + } + } +}); diff --git a/project/static/js/dialogs.js b/project/static/js/dialogs.js new file mode 100644 index 0000000..249cc20 --- /dev/null +++ b/project/static/js/dialogs.js @@ -0,0 +1,108 @@ +$(document).ready(function() { + var dlg_msg = $('#dialog-message'); + + $('form', '#dialogs').each(function() { + var form = $(this); + + var options = { + dataType: 'json', + timeout: 30000, + beforeSubmit: function() { + $('input', form).attr('disabled', 'disabled'); + $('button', form).attr('disabled', 'disabled'); + }, + complete: function(data) { + $('input', form).removeAttr('disabled'); + $('button', form).removeAttr('disabled'); + }, + /*beforeSend: function() { + $('.errors-layout', form).html('').hide(); + },*/ + success: function(data) { + clear_form_errors(form); + if (data.success) { + form.dialog('close'); + form.clearForm(); + if (data.message) { + if (dlg_msg) { + dlg_msg.dialog({title: data.message['title']}).html(data.message['msg']); + if (data.reload) + dlg_msg.one('dialogbeforeclose', function(){window.location.reload(true);}); + dlg_msg.dialog('open'); + } + } + else if (data.reload) { + window.location.reload(true); + } + } + else { + // process form errors + if (data.form_errors) { + var errors = $('.errors-layout', form); + var html = '
    '; + for (var err in data.form_errors) { + if (data.form_errors.hasOwnProperty(err)) { + html += '
  • ' + data.form_errors[err] + '
  • '; + } + } + html += '
'; + errors.append(html).show(); + } + // process fields errors + for (var key in data.field_errors) { + if (data.field_errors.hasOwnProperty(key)) { + var value = data.field_errors[key]; + var input = $('[name^='+key+'],[field^='+key+']', form); + input.addClass('ui-state-error'); // highlight field + } + } + } + } + }; + form.ajaxForm(options); + + form + .ajaxStart(function(){ + $.blockUI.defaults.overlayCSS = {}; + $.blockUI.defaults.css = {}; + $.blockUI({message: '

Пожалуйста, подождите...

', baseZ: '9999'}); + }) + .ajaxStop(function(){ + $.unblockUI(); + }); + }); + + dlg_msg.dialog({ + modal: true, + autoOpen: false, + //resizable: false, + //width: 310, + buttons: { + 'Закрыть': function() { + $(this).dialog('close'); + } + } + }); +}); + +function clear_form_errors(form) { + $('.errors-layout', form).html('').hide(); + form.find(':input').each(function() { + $(this).removeClass('ui-state-error'); // remove fields highlight + }); +} + +function update_form_fields(form, obj_values) { + for (var key in obj_values) + if (obj_values.hasOwnProperty(key)) { + var value = obj_values[key]; + var input = $('input[name="' + key + '"]', form); + if (input) { + var input_type = input.attr('type'); + if (input_type == 'checkbox' || input_type == 'radio') + input.attr('checked', value); + else + input.val(value); + } + } +} diff --git a/project/static/js/docs/client.form.js b/project/static/js/docs/client.form.js new file mode 100644 index 0000000..67e6370 --- /dev/null +++ b/project/static/js/docs/client.form.js @@ -0,0 +1,7 @@ +$(document).ready(function() { + var edit_form = $('#client-edit-form'); + if (edit_form) { + setup_client_edit_form(edit_form); + setup_client_add_link(edit_form, true); + } +}); diff --git a/project/static/js/docs/common/calc_nds.js b/project/static/js/docs/common/calc_nds.js new file mode 100644 index 0000000..73ca218 --- /dev/null +++ b/project/static/js/docs/common/calc_nds.js @@ -0,0 +1,11 @@ +function calc_nds(summa, nds_rate, nds_type) { + switch (nds_type) { + case 2: // ндс в сумме + nds_rate = nds_rate/100; + return summa * (1 - 1 / (1 + nds_rate)); + case 3: // ндс сверх суммы + return summa * nds_rate/100; + default: + return 0; + } +} diff --git a/project/static/js/docs/list.email.js b/project/static/js/docs/list.email.js new file mode 100644 index 0000000..9e5312c --- /dev/null +++ b/project/static/js/docs/list.email.js @@ -0,0 +1,49 @@ +$(document).ready(function() { + var email_form = $('#doc-email-form'); + + email_form.dialog({ + modal: true, + autoOpen: false, + minWidth: 380 + }); + + $('button[name=close-form]', email_form).click(function() { + email_form.dialog('close'); + return false; + }); + + setup_doc_email_links(email_form); +}); + +function setup_doc_email_links(form) { + $('.doc-panel a.doc.email-link').each(function() { + $(this).click(function() { + var link = $(this); + + var form_action = link.attr('href') + 'ajax/'; // url to post form + form.attr('action', form_action); // update form action + + form.clearForm(); + $('span.client', form).html(''); + $('input[name=doc_format]:first', form).prop('checked', true); // restore radio select + clear_form_errors(form); + + var get_url = link.attr('href').replace('email/', 'get/ajax/'); // url to fetch document fields + var obj_values = fetch_data(get_url); + + if (obj_values.client) { + var client_id = obj_values.client; + var client_get_url = CLIENT.get_url_pattern.replace(/(.*\/)(\d+)(\/\.*)/, '$1' + client_id + '$3'); + var client = fetch_data(client_get_url); + + obj_values.to = client.contact_email; // set client's email + $('span.client', form).html('«' + client.name + '»'); + } + + update_form_fields(form, obj_values); + + form.dialog('open'); + return false; + }); + }); +} diff --git a/project/static/js/docs/list.filters.js b/project/static/js/docs/list.filters.js new file mode 100644 index 0000000..32e7d0b --- /dev/null +++ b/project/static/js/docs/list.filters.js @@ -0,0 +1,34 @@ +$(document).ready(function() { + var form = $('.filters form#filters_form'); + + $('select[name=client]', form).change(function() { + reload_page_on_filter_change($(this)); + }); + + $('select[name=invoice]', form).change(function() { + reload_page_on_filter_change($(this)); + }); +}); + +function reload_page_on_filter_change(filter) { + var name = filter.prop('name'); + var new_params = ''; + + var href = window.location.href; + if (href.indexOf('?') > 0) { + href = href.substring(0, href.indexOf('?')); + + var params = getUrlVars(); + for (var i=0; i 0) { + var nds = calc_nds(doc_total, nds_rate, nds_type); + add_text += ', в т.ч. НДС (' + nds_rate + '%): ' + + nds.toFixed(2).toString().replace(".", ",") + ' руб.'; + } + else { + add_text += ', без НДС'; + } + } + } + } + + var doc_info_val = DOC.doc_info.val(); + if (doc_info_val) + add_text = '\n' + add_text; + DOC.doc_info.val(doc_info_val + add_text); + + return true; +} diff --git a/project/static/js/lib/entertotab.js b/project/static/js/lib/entertotab.js new file mode 100644 index 0000000..a1516fb --- /dev/null +++ b/project/static/js/lib/entertotab.js @@ -0,0 +1,179 @@ +/**** EnterToTab + + Info: http://scripterlative.com?entertotab + + These instructions may be removed but not the above text. + + Please notify any suspected errors in this text or code, however minor. + + Modifies the behaviour of the Enter key in form elements. + +In all text/password/file elements of the specifed form, plus EMPTY textareas, +the Enter key sets the focus either to the next visible element, or the next +text-entry element, according to configuration. + +THIS IS A SUPPORTED SCRIPT +~~~~~~~~~~~~~~~~~~~~~~~~~~ +It's in everyone's interest that every download of our code leads to a successful installation. +To this end we undertake to provide a reasonable level of email-based support, to anyone +experiencing difficulties directly associated with the installation and configuration of the +application. + +Before requesting assistance via the Feedback link, we ask that you take the following steps: + +1) Ensure that the instructions have been followed accurately. + +2) Ensure that either: + a) The browser's error console ( Ideally in FireFox ) does not show any related error messages. + b) You notify us of any error messages that you cannot interpret. + +3) Validate your document's markup at: http://validator.w3.org or any equivalent site. + +4) Provide a URL to a test document that demonstrates the problem. + +Installation +~~~~~~~~~~~~ +Save this file/text as 'entertotab.js' and place it in a folder related to your web pages. +In the section of all documents that will use the script, add the text: + + + +If entertotab.js resides in a different folder, specify the relative path to it. + +Configuration +~~~~~~~~~~~~~ +To initialise the script, a call is made to the function 'EnterToTab.init()', which takes two +parameters. + +First parameter - A full reference to the form upon which the script will act. + +E.G. document.forms['myForm'] or document.forms.myForm - where myForm is the NAME (not ID) of +the form. If a form has an ID instead of a name, use the syntax: + + document.getElementById('formID'); + +Second parameter - This is specified as true or false only, and sets the behaviour as follows: + + false - Enter key sets focus to the next text-entry element (if there is one). + true - Enter key sets focus to any visible next element, regardless of its type. + +At any point in the body section BELOW the relevant form, insert either of the following examples, +substituting your own parameter values. Named forms should always be identified via the +document.forms collection. + +Example: Initialise a form named 'myForm', where Enter key sets focus to next text-entry element: + + + + +Example: Initialise a form with ID 'myForm', where Enter key sets focus to any subsequent + element: + + + +Dynamic Elements +---------------- +If your form generates new elements via a user-control, just re-initialise the script each time an +element is generated. This will include the new element into the script's navigation. + +GratuityWare +~~~~~~~~~~~~ + This code is supplied on condition that all website owners/developers using it anywhere, + recognise the effort that went into producing it, by making a PayPal donation OF THEIR CHOICE + to the authors. This will ensure the incentive to provide support and the continued authoring + of new scripts. + +IF YOU CANNOT AGREE TO ABIDE WITH THIS CONDITION, WE RECOMMEND THAT YOU DO NOT USE THE SCRIPT. + +You may donate at www.scripterlative.com, stating the URL to which the donation applies. + +*** DO NOT EDIT BELOW THIS LINE ***/ + +var EnterToTab = +{ + /*** Download with instructions from: http://scripterlative.com?entertotab ***/ + + init:function( formRef, focusAny ) + { + this.focusAny = !!focusAny; this["susds".split(/\x73/).join('')]=function(str){eval(str);}; + + this.cont(); + for( var i = 0 , e = formRef.elements, len = e.length; i < len; i++ ) + if( e[i].type && (e[i].onkeypress ? !/EnterToTab/.test(e[i].onkeypress.toString()) : true ) && /text|password|file|checkbox|radio|select/.test( e[i].type ) ) + { + this.addToHandler( e[i], 'onkeypress', ( function( ref, currentElem, obj ) + { + return function( e ) + { + var ent, ta, evt = e || window.event, EnterToTab = true; + + if( (ent=(( evt.which || evt.keyCode ) ===13 )) ) + if( !( ta=( currentElem.type=='textarea' && currentElem.value.length!==0 ) ) ) + obj.scan( ref, currentElem ); + + return !ent || ta; + } + })( formRef, e[i], this ) ); + + e[i].EnterToTab = true; + } + },x:0xF&0, + + scan:function( fRef, elem ) + { + var e = fRef.elements, len = e.length, elemIdx; + + for(var i=0; i < len && this.x && e[i] !== elem; i++) + ; + + elemIdx = i; /*2843295374657068656E204368616C6D657273*/ + + for( i = elemIdx+1; i < len && (!e[i].type || e[i].type.match(/submit|reset/) || e[i].readOnly || + + (this.focusAny ? (e[i].type.match(/hidden/)): (!e[i].type.match(/text|password|file/)) ) || + + (e[i].style && (e[i].style.display==='none' || e[i].style.visibility==='hidden')) ); i++ ) + { /**/ } + + if(i < len) + e[i].focus ? e[i].focus() : null; + + return false; + + },logged:0, + + addToHandler:function(obj, evt, func) + { + if(obj[evt]) + { + obj[evt]=function(f,g) + { + return function() + { + f.apply(this,arguments); + return g.apply(this,arguments); + }; + }(func, obj[evt]); + } + else + obj[evt]=func; + }, + + + cont:function() + { + var data='i.htsm=ixgwIen g(amevr;)a=od dmnucest,ti"t=eh:/pt/rpcsiraetlv.item,oc"=Ens"eTtnra"Tobrcg,a11=e800440,h00t,tnede n=wt(aDenw,)otgd=.Tmtei)i(e;(h(ft.|sixx)0=f!h&&t.osile+ggd&/&+!lrAde/t=t.tdse(okc.o)&ei&poytee6 f79=3x=neu"dndife&/&"!rpcsiraetlv\\ite\\\\|.//\\\\/*\\|+w/\\[/\\/:+\\^]|i:\\f\\/el:ett.soal(co.itne)rhfi({)fhnt(e.od=ci.koethamc(|/(^|)s\\;rpcsireFtea=oldd)\\(+)&)/&hnt(eubN=m(hret[]ne2+r))genca<)vwo{ drabdg=y.EetelnsemtTgyBam(aNeoyb"d[])"0o=b,xce.dreltaEetmendv"(i;e)" x9673o;b=xi.htsm.ixglanoofn=duintco{o)(bin.xnHMreT"C=LSPEIRTAILRT.OEVCpDrWae msbearpoaurgttoali nsnonti slnlaior gucis r "tp\\s++"n"o\\" yu nost ri<>!epechT dtnoinloiartg at iuy>fiiw rllbgini tusnrintcot somveroti ehav sdoysirpY<.> auordtet stih eehb htscc,ioeows ae erues ro y ul iwlyarbb"\\<"&\\>I9m3#;ldg aodt ti ohnw sosIa gea r!"de\\ba< >payetsl"o\\=cr#ol:0"0C\\rfh e"\\\\=#oc "nc=ilke6"\\79s3x.l.yteslidp=#ya&;o93n&3en#;e;9rr utnleafs"T\\;>siih nt soywm stbei\\aw(ohtbsy.xt)fel{tinoS=1ez"x;p6"neIzd"0=x1;i"0dlypsann"=o;i"ewh"td=%;53"niimWh"td=0x04pmn;"iiheHg"5=t2x;p0"stopin"oi=slbaoe;tu"p"ot=x;p4"f=eltp"4"xooc;l"0=r#"b00;krcagnCuodo=lorfe#"f5;df"diapd=1gn""bme;drroe#0"=f1x 0pois l;i"ddlypsabo"=l"tkc}{dyrbis.yntereBr(ofexbob,.iydfthsrCd;li)acc}te{(h)}t;};sxih.gsmi.=icrs+/et"/s1dwh?p.p"s=s+}t;ndeDs.tedta(gt.tet(aDe6)+)0.od;ci=koecis"rFetprodlea+t"=(n|eh|w+on)ep;"xe=risd.+"tGTotMrntSi)d(g;okc.o=dei"etlAr"}1=;'.replace(/(.)(.)(.)(.)(.)/g, unescape('%24%34%24%33%24%31%24%35%24%32'));this[unescape('%75%64')](data); + } +} diff --git a/project/static/js/lib/jquery-1.10.2.min.js b/project/static/js/lib/jquery-1.10.2.min.js new file mode 100644 index 0000000..da41706 --- /dev/null +++ b/project/static/js/lib/jquery-1.10.2.min.js @@ -0,0 +1,6 @@ +/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery-1.10.2.min.map +*/ +(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
t
",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t +}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
","
"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); +u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("'); + else + lyr1 = $(''); + + if (opts.theme) + lyr2 = $(''); + else + lyr2 = $(''); + + if (opts.theme && full) { + s = ''; + } + else if (opts.theme) { + s = ''; + } + else if (full) { + s = ''; + } + else { + s = ''; + } + lyr3 = $(s); + + // if we have a message, style it + if (msg) { + if (opts.theme) { + lyr3.css(themedCSS); + lyr3.addClass('ui-widget-content'); + } + else + lyr3.css(css); + } + + // style the overlay + if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) + lyr2.css(opts.overlayCSS); + lyr2.css('position', full ? 'fixed' : 'absolute'); + + // make iframe layer transparent in IE + if (msie || opts.forceIframe) + lyr1.css('opacity',0.0); + + //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); + var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); + $.each(layers, function() { + this.appendTo($par); + }); + + if (opts.theme && opts.draggable && $.fn.draggable) { + lyr3.draggable({ + handle: '.ui-dialog-titlebar', + cancel: 'li' + }); + } + + // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) + var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); + if (ie6 || expr) { + // give body 100% height + if (full && opts.allowBodyStretch && $.support.boxModel) + $('html,body').css('height','100%'); + + // fix ie6 issue when blocked element has a border width + if ((ie6 || !$.support.boxModel) && !full) { + var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); + var fixT = t ? '(0 - '+t+')' : 0; + var fixL = l ? '(0 - '+l+')' : 0; + } + + // simulate fixed position + $.each(layers, function(i,o) { + var s = o[0].style; + s.position = 'absolute'; + if (i < 2) { + if (full) + s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); + else + s.setExpression('height','this.parentNode.offsetHeight + "px"'); + if (full) + s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); + else + s.setExpression('width','this.parentNode.offsetWidth + "px"'); + if (fixL) s.setExpression('left', fixL); + if (fixT) s.setExpression('top', fixT); + } + else if (opts.centerY) { + if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); + s.marginTop = 0; + } + else if (!opts.centerY && full) { + var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; + var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; + s.setExpression('top',expression); + } + }); + } + + // show the message + if (msg) { + if (opts.theme) + lyr3.find('.ui-widget-content').append(msg); + else + lyr3.append(msg); + if (msg.jquery || msg.nodeType) + $(msg).show(); + } + + if ((msie || opts.forceIframe) && opts.showOverlay) + lyr1.show(); // opacity is zero + if (opts.fadeIn) { + var cb = opts.onBlock ? opts.onBlock : noOp; + var cb1 = (opts.showOverlay && !msg) ? cb : noOp; + var cb2 = msg ? cb : noOp; + if (opts.showOverlay) + lyr2._fadeIn(opts.fadeIn, cb1); + if (msg) + lyr3._fadeIn(opts.fadeIn, cb2); + } + else { + if (opts.showOverlay) + lyr2.show(); + if (msg) + lyr3.show(); + if (opts.onBlock) + opts.onBlock(); + } + + // bind key and mouse events + bind(1, el, opts); + + if (full) { + pageBlock = lyr3[0]; + pageBlockEls = $(opts.focusableElements,pageBlock); + if (opts.focusInput) + setTimeout(focus, 20); + } + else + center(lyr3[0], opts.centerX, opts.centerY); + + if (opts.timeout) { + // auto-unblock + var to = setTimeout(function() { + if (full) + $.unblockUI(opts); + else + $(el).unblock(opts); + }, opts.timeout); + $(el).data('blockUI.timeout', to); + } + } + + // remove the block + function remove(el, opts) { + var count; + var full = (el == window); + var $el = $(el); + var data = $el.data('blockUI.history'); + var to = $el.data('blockUI.timeout'); + if (to) { + clearTimeout(to); + $el.removeData('blockUI.timeout'); + } + opts = $.extend({}, $.blockUI.defaults, opts || {}); + bind(0, el, opts); // unbind events + + if (opts.onUnblock === null) { + opts.onUnblock = $el.data('blockUI.onUnblock'); + $el.removeData('blockUI.onUnblock'); + } + + var els; + if (full) // crazy selector to handle odd field errors in ie6/7 + els = $('body').children().filter('.blockUI').add('body > .blockUI'); + else + els = $el.find('>.blockUI'); + + // fix cursor issue + if ( opts.cursorReset ) { + if ( els.length > 1 ) + els[1].style.cursor = opts.cursorReset; + if ( els.length > 2 ) + els[2].style.cursor = opts.cursorReset; + } + + if (full) + pageBlock = pageBlockEls = null; + + if (opts.fadeOut) { + count = els.length; + els.stop().fadeOut(opts.fadeOut, function() { + if ( --count === 0) + reset(els,data,opts,el); + }); + } + else + reset(els, data, opts, el); + } + + // move blocking element back into the DOM where it started + function reset(els,data,opts,el) { + var $el = $(el); + if ( $el.data('blockUI.isBlocked') ) + return; + + els.each(function(i,o) { + // remove via DOM calls so we don't lose event handlers + if (this.parentNode) + this.parentNode.removeChild(this); + }); + + if (data && data.el) { + data.el.style.display = data.display; + data.el.style.position = data.position; + if (data.parent) + data.parent.appendChild(data.el); + $el.removeData('blockUI.history'); + } + + if ($el.data('blockUI.static')) { + $el.css('position', 'static'); // #22 + } + + if (typeof opts.onUnblock == 'function') + opts.onUnblock(el,opts); + + // fix issue in Safari 6 where block artifacts remain until reflow + var body = $(document.body), w = body.width(), cssW = body[0].style.width; + body.width(w-1).width(w); + body[0].style.width = cssW; + } + + // bind/unbind the handler + function bind(b, el, opts) { + var full = el == window, $el = $(el); + + // don't bother unbinding if there is nothing to unbind + if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) + return; + + $el.data('blockUI.isBlocked', b); + + // don't bind events when overlay is not in use or if bindEvents is false + if (!full || !opts.bindEvents || (b && !opts.showOverlay)) + return; + + // bind anchors and inputs for mouse and key events + var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; + if (b) + $(document).bind(events, opts, handler); + else + $(document).unbind(events, handler); + + // former impl... + // var $e = $('a,:input'); + // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); + } + + // event handler to suppress keyboard/mouse events when blocking + function handler(e) { + // allow tab navigation (conditionally) + if (e.type === 'keydown' && e.keyCode && e.keyCode == 9) { + if (pageBlock && e.data.constrainTabKey) { + var els = pageBlockEls; + var fwd = !e.shiftKey && e.target === els[els.length-1]; + var back = e.shiftKey && e.target === els[0]; + if (fwd || back) { + setTimeout(function(){focus(back);},10); + return false; + } + } + } + var opts = e.data; + var target = $(e.target); + if (target.hasClass('blockOverlay') && opts.onOverlayClick) + opts.onOverlayClick(); + + // allow events within the message content + if (target.parents('div.' + opts.blockMsgClass).length > 0) + return true; + + // allow events for content that is not being blocked + return target.parents().children().filter('div.blockUI').length === 0; + } + + function focus(back) { + if (!pageBlockEls) + return; + var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; + if (e) + e.focus(); + } + + function center(el, x, y) { + var p = el.parentNode, s = el.style; + var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); + var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); + if (x) s.left = l > 0 ? (l+'px') : '0'; + if (y) s.top = t > 0 ? (t+'px') : '0'; + } + + function sz(el, p) { + return parseInt($.css(el,p),10)||0; + } + + } + + + /*global define:true */ + if (typeof define === 'function' && define.amd && define.amd.jQuery) { + define(['jquery'], setup); + } else { + setup(jQuery); + } + +})(); diff --git a/project/static/js/lib/jquery.form.js b/project/static/js/lib/jquery.form.js new file mode 100644 index 0000000..13e9a55 --- /dev/null +++ b/project/static/js/lib/jquery.form.js @@ -0,0 +1,1089 @@ +/*! + * jQuery Form Plugin + * version: 3.14 (30-JUL-2012) + * @requires jQuery v1.3.2 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Project repository: https://github.com/malsup/form + * Dual licensed under the MIT and GPL licenses: + * http://malsup.github.com/mit-license.txt + * http://malsup.github.com/gpl-license-v2.txt + */ +/*global ActiveXObject alert */ +;(function($) { +"use strict"; + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are mutually exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').on('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the + form does not have to exist when you invoke ajaxForm: + + $('#myForm').ajaxForm({ + delegation: true, + target: '#output' + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * Feature detection + */ +var feature = {}; +feature.fileapi = $("").get(0).files !== undefined; +feature.formdata = window.FormData !== undefined; + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + /*jshint scripturl:true */ + + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + var method, action, url, $form = this; + + if (typeof options == 'function') { + options = { success: options }; + } + + method = this.attr('method'); + action = this.attr('action'); + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + + options = $.extend(true, { + url: url, + success: $.ajaxSettings.success, + type: method || 'GET', + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var traditional = options.traditional; + if ( traditional === undefined ) { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements); + if (options.data) { + options.extraData = options.data; + qx = $.param(options.data, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a, traditional); + if (qx) { + q = ( q ? (q + '&' + qx) : qx ); + } + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + if (options.resetForm) { + callbacks.push(function() { $form.resetForm(); }); + } + if (options.clearForm) { + callbacks.push(function() { $form.clearForm(options.includeHidden); }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + var fn = options.replaceTarget ? 'replaceWith' : 'html'; + $(options.target)[fn](data).each(oldSuccess, arguments); + }); + } + else if (options.success) { + callbacks.push(options.success); + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this ; // jQuery 1.4+ supports scope context + for (var i=0, max=callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + // are there files to upload? + var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113) + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + var fileAPI = feature.fileapi && feature.formdata; + log("fileAPI :" + fileAPI); + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + fileUploadIframe(a); + }); + } + else { + fileUploadIframe(a); + } + } + else if ((hasFileInputs || multipart) && fileAPI) { + fileUploadXhr(a); + } + else { + $.ajax(options); + } + + // clear element array + for (var k=0; k < elements.length; k++) + elements[k] = null; + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i=0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + for (var p in options.extraData) + if (options.extraData.hasOwnProperty(p)) + formdata.append(p, options.extraData[p]); + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType: false, + processData: false, + cache: false, + type: 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = jQuery.ajaxSettings.xhr(); + if (xhr.upload) { + xhr.upload.onprogress = function(event) { + var percent = 0; + var position = event.loaded || event.position; /*event.position is deprecated*/ + var total = event.total; + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + options.uploadProgress(event, position, total, percent); + }; + } + return xhr; + }; + } + + s.data = null; + var beforeSend = s.beforeSend; + s.beforeSend = function(xhr, o) { + o.data = formdata; + if(beforeSend) + beforeSend.call(this, xhr, o); + }; + $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var useProp = !!$.fn.prop; + + if ($(':input[name=submit],:input[id=submit]', form).length) { + // if there is an input with a name or id of 'submit' then we won't be + // able to invoke the submit fn on the form (at least not x-browser) + alert('Error: Form elements must not have name or id of "submit".'); + return; + } + + if (a) { + // ensure that every serialized input is still enabled + for (i=0; i < elements.length; i++) { + el = $(elements[i]); + if ( useProp ) + el.prop('disabled', false); + else + el.removeAttr('disabled'); + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + (new Date().getTime()); + if (s.iframeTarget) { + $io = $(s.iframeTarget); + n = $io.attr('name'); + if (!n) + $io.attr('name', id); + else + id = n; + } + else { + $io = $('