remotes/origin/yandex
Andrey 12 years ago
commit 7fd5c90722
  1. 13
      .gitignore
  2. 1
      README
  3. 1
      log/.gitignore
  4. 10
      manage.py
  5. 0
      project/__init__.py
  6. 0
      project/commons/__init__.py
  7. 64
      project/commons/forms.py
  8. 3
      project/commons/models.py
  9. 75
      project/commons/paginator.py
  10. 30
      project/commons/pdf_tools.py
  11. 0
      project/commons/templatetags/__init__.py
  12. 65
      project/commons/templatetags/my_tags.py
  13. 16
      project/commons/tests.py
  14. 14
      project/commons/utils.py
  15. 1
      project/commons/views.py
  16. 3
      project/commons/xls/__init__.py
  17. 78
      project/commons/xls/get_xlwt_style_list.py
  18. 158
      project/commons/xls/useful_tools.py
  19. 12
      project/commons/xls/xls_to_response.py
  20. 2
      project/customer/__init__.py
  21. 58
      project/customer/admin.py
  22. 9
      project/customer/consts.py
  23. 412
      project/customer/forms.py
  24. 64
      project/customer/managers.py
  25. 17
      project/customer/middleware.py
  26. 331
      project/customer/models.py
  27. 16
      project/customer/tests.py
  28. 52
      project/customer/urls.py
  29. 10
      project/customer/views/__init__.py
  30. 113
      project/customer/views/bank_accounts.py
  31. 125
      project/customer/views/bank_accounts_ajax.py
  32. 101
      project/customer/views/clients.py
  33. 107
      project/customer/views/clients_ajax.py
  34. 209
      project/customer/views/profile.py
  35. 76
      project/customer/views/profile_ajax.py
  36. 0
      project/docs/__init__.py
  37. 1
      project/docs/as_xls/__init__.py
  38. 521
      project/docs/as_xls/render_to_xls.py
  39. 146
      project/docs/consts.py
  40. 210
      project/docs/filters.py
  41. 8
      project/docs/forms/__init__.py
  42. 51
      project/docs/forms/aktrabot.py
  43. 60
      project/docs/forms/aktsverki.py
  44. 56
      project/docs/forms/base_forms.py
  45. 54
      project/docs/forms/dover.py
  46. 19
      project/docs/forms/email.py
  47. 60
      project/docs/forms/invoice.py
  48. 52
      project/docs/forms/nakladn.py
  49. 82
      project/docs/forms/platejka.py
  50. 6
      project/docs/models/__init__.py
  51. 21
      project/docs/models/aktrabot.py
  52. 54
      project/docs/models/aktsverki.py
  53. 100
      project/docs/models/base_models.py
  54. 42
      project/docs/models/dover.py
  55. 34
      project/docs/models/invoice.py
  56. 24
      project/docs/models/managers.py
  57. 22
      project/docs/models/mixins.py
  58. 23
      project/docs/models/nakladn.py
  59. 105
      project/docs/models/platejka.py
  60. 16
      project/docs/tests.py
  61. 71
      project/docs/urls.py
  62. 45
      project/docs/utils.py
  63. 38
      project/docs/views/__init__.py
  64. 82
      project/docs/views/aktrabot.py
  65. 97
      project/docs/views/aktsverki.py
  66. 718
      project/docs/views/base_views.py
  67. 72
      project/docs/views/dover.py
  68. 87
      project/docs/views/invoice.py
  69. 39
      project/docs/views/mixins.py
  70. 151
      project/docs/views/nakladn.py
  71. 59
      project/docs/views/platejka.py
  72. 54
      project/local_settings.py.dev-example
  73. 29
      project/local_settings.py.prod-example
  74. 1
      project/myauth/__init__.py
  75. 37
      project/myauth/emails.py
  76. 165
      project/myauth/forms.py
  77. 28
      project/myauth/managers.py
  78. 54
      project/myauth/models.py
  79. 16
      project/myauth/tests.py
  80. 25
      project/myauth/urls.py
  81. 206
      project/myauth/views.py
  82. 0
      project/pages/__init__.py
  83. 3
      project/pages/models.py
  84. 16
      project/pages/tests.py
  85. 8
      project/pages/views.py
  86. BIN
      project/pdf_fonts/arial.ttf
  87. BIN
      project/pdf_fonts/arialbd.ttf
  88. 200
      project/settings.py
  89. 1
      project/static/css/custom_admin.css
  90. 205
      project/static/css/style.css
  91. BIN
      project/static/css/ui-lightness/images/animated-overlay.gif
  92. BIN
      project/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png
  93. BIN
      project/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png
  94. BIN
      project/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png
  95. BIN
      project/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png
  96. BIN
      project/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png
  97. BIN
      project/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png
  98. BIN
      project/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png
  99. BIN
      project/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
  100. BIN
      project/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
  101. Some files were not shown because too many files have changed in this diff Show More

13
.gitignore vendored

@ -0,0 +1,13 @@
*.*~
*.pyc
.DS_Store
._*
pip-log.txt
ENV/
.idea/
local_settings.py
Thumbs.db
distribute-*.tar.gz
*.bak
_public_html/

@ -0,0 +1 @@
Документор

1
log/.gitignore vendored

@ -0,0 +1 @@
*

@ -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)

@ -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)

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -0,0 +1 @@
# Create your views here.

@ -0,0 +1,3 @@
from useful_tools import *
from get_xlwt_style_list import *
from xls_to_response import *

@ -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

@ -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

@ -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

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from .models import get_profile

@ -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)

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
IP_PROFILE = 1
ORG_PROFILE = 2
PROFILE_TYPES = (
(IP_PROFILE, u'Индивидуальный предприниматель'),
(ORG_PROFILE, u'Организация'),
)

@ -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<br /><span class="name">%s</span>' % (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}))

@ -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)

@ -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))

@ -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)

@ -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)

@ -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<page_num>[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<id>\d+)/edit/$', bank_accounts.bank_accounts_edit, name='customer_bank_accounts_edit'),
url(r'^bank-accounts/(?P<id>\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<id>\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<id>\d+)/edit/ajax/$', bank_accounts_ajax.bank_accounts_edit_ajax,
name='customer_bank_accounts_edit_ajax'),
url(r'^bank-accounts/(?P<id>\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<page_num>[0-9]+)/$', clients.clients_list, name='customer_clients_list'),
url(r'^clients/add/$', clients.clients_add, name='customer_clients_add'),
url(r'^clients/(?P<id>\d+)/edit/$', clients.clients_edit, name='customer_clients_edit'),
url(r'^clients/(?P<id>\d+)/delete/$', clients.clients_delete, name='customer_clients_delete'),
# --- контрагенты AJAX
url(r'^clients/(?P<id>\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<id>\d+)/edit/ajax/$', clients_ajax.clients_edit_ajax, name='customer_clients_edit_ajax'),
url(r'^clients/(?P<id>\d+)/delete/ajax/$', clients_ajax.clients_delete_ajax, name='customer_clients_delete_ajax'),
)

@ -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)

@ -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)

@ -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')

@ -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,})

@ -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')

@ -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,})

@ -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')

@ -0,0 +1 @@
from .render_to_xls import render_xls_to_string

@ -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

@ -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 - Конкретное значение указать невозможно'),
)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -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)

@ -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

@ -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'Табл. части актов выполн. работ'

@ -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)

@ -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)

@ -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)

@ -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'Табл. части счетов'

@ -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

@ -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

@ -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'Табл. части накладных'

@ -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'Формат ввода периода платежей:<br />'
u'Месячный платёж - "МС.00.0000"<br />'
u'Квартальный платёж - "КВ.00.0000"<br />'
u'Полугодовой платёж - "ПЛ.00.0000"<br />'
u'Годовой платёж - "ГД.00.0000"<br />'
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''

@ -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)

@ -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<page_num>[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<id>\d+)/edit/$' % name, getview, {'klass': klass, 'oper': 'edit',},
name='docs_%s_edit' % name),
# создать копию
url(r'^%s/(?P<id>\d+)/copy/$' % name, getview, {'klass': klass, 'oper': 'copy',},
name='docs_%s_copy' % name),
# удалить
url(r'^%s/(?P<id>\d+)/delete/$' % name, getview, {'klass': klass, 'oper': 'delete',},
name='docs_%s_delete' % name),
# сохранить в pdf
url(r'^%s/(?P<id>\d+)/pdf/$' % name, getview, {'klass': klass, 'oper': 'as_pdf',},
name='docs_%s_pdf' % name),
# сохранить в excel
url(r'^%s/(?P<id>\d+)/xls/$' % name, getview, {'klass': klass, 'oper': 'as_xls',},
name='docs_%s_xls' % name),
# отправить pdf/xls на email
url(r'^%s/(?P<id>\d+)/email/$' % name, getview, {'klass': klass, 'oper': 'email',},
name='docs_%s_email' % name),
# поля документа - AJAX
url(r'^%s/(?P<id>\d+)/get/ajax/$' % name, getview, {'klass': klass, 'oper': 'get_ajax',},
name='docs_%s_get_ajax' % name),
# отправить pdf/xls на email - AJAX
url(r'^%s/(?P<id>\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<invoice_id>\d+)/$' % 'aktrabot', getview,
{'klass': AktRabotViews, 'oper': 'add_by_invoice',}, name='docs_%s_add_by_invoice' % 'aktrabot'),
# создать по Счету -> Накладную
url(r'^%s/add/by/invoice/(?P<invoice_id>\d+)/$' % 'nakladn', getview,
{'klass': NakladnViews, 'oper': 'add_by_invoice',}, name='docs_%s_add_by_invoice' % 'nakladn'),
# # создать по Счету -> Счёт-фактуру
# url(r'^%s/add/by/invoice/(?P<invoice_id>\d+)/$' % 'sfv', getview, {'klass': SfvViews, 'oper': 'add_by_invoice',},
# name='docs_%s_add_by_invoice' % 'sfv'),
)

@ -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)

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -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,
}

@ -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'

@ -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()

@ -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

@ -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

@ -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,)

@ -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)

@ -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<key>[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<key>[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'),
)

@ -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,})

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -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)

@ -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)

Binary file not shown.

Binary file not shown.

@ -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

@ -0,0 +1 @@
.aligned label { width: 15em; }

@ -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; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save