@ -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 @@ |
||||
* |
||||
@ -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,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 @@ |
||||
# 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,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,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,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,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 @@ |
||||
|
||||
@ -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) |
||||
@ -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; } |
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 418 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 205 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 207 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 278 B |
|
After Width: | Height: | Size: 328 B |