# -*- coding: utf-8 -*- import os import logging from datetime import datetime, timedelta from PIL import Image from django.db.models.signals import post_save from django.dispatch import receiver from pytils import numeral from dateutil.relativedelta import relativedelta from django.db import models from django.db.models import Max from django.core.urlresolvers import reverse from django.utils import timezone from django.utils.deconstruct import deconstructible from django.conf import settings from customer import consts, managers, utils from customer.utils import create_bonus_license, get_robokassa_url from myauth.models import DokUser from commons.utils import only_numerics from robokassa.signals import result_received, success_page_visited from . import emails log = logging.getLogger(__name__) # куда сохранять загруженные изображения PROFILE_IMAGES_UPLOAD_DIR = 'customer/profile/' BOSS_SIGN_IMG_SIZE = (159, 65) GLAVBUH_SIGN_IMG_SIZE = (159, 65) STAMP_IMG_SIZE = (180, 180) LOGO_SIZE = (351, 121) def get_profile(user): """Возвращает профиль пользователя или None.""" try: return user.profile except: return None @deconstructible class UploadAndRename(object): def __init__(self, path, filename): self.path = path self.filename = filename def __call__(self, instance, filename): filename = self.filename or filename try: profile_dir = instance.get_first_user().username except: profile_dir = 'NoUser' return os.path.join(self.path, profile_dir, filename) class UserProfile(models.Model): """Профиль пользователя.""" profile_type = models.PositiveSmallIntegerField('Тип профиля', choices=consts.PROFILE_TYPES) # общие поля boss_surname = models.CharField('Фамилия', max_length=30, default='', help_text='Используется для строки "подпись" в документах.') boss_name = models.CharField('Имя', max_length=30, default='') boss_midname = models.CharField('Отчество', max_length=30, default='') # длина: 10 для организаций, 12 для ИП inn = models.CharField('ИНН', max_length=12, default='') # длина: 13 для организаций, 15 для ИП ogrn = models.CharField('ОГРН/ОГРНИП', max_length=15, default='') # длина: 8 для организаций, 8 или 10 для ИП okpo = models.CharField('ОКПО', max_length=10, blank=True, default='') glavbuh_surname = models.CharField('Фамилия', max_length=30, blank=True, default='', help_text='Используется для строки "подпись" в документах.') glavbuh_name = models.CharField('Имя', max_length=30, blank=True, default='') glavbuh_midname = models.CharField('Отчество', max_length=30, blank=True, default='') address = models.CharField( 'Фактический адрес', max_length=256, default='', help_text='Будет подставляться в создаваемые счета, акты и накладные.' ) jur_address = models.CharField('Юридический адрес', max_length=256, blank=True, default='', help_text='Как в учредительных документах.') real_address = models.CharField('Почтовый адрес', max_length=256, blank=True, default='', help_text='Используется только для карточки компании.') phone_code = models.CharField('Код города', max_length=10, blank=True, default='') phone = models.CharField('Номер телефона', max_length=20, blank=True, default='') fax_code = models.CharField('Код города', max_length=10, blank=True, default='') fax = models.CharField('Номер телефона', max_length=20, blank=True, default='') email = models.EmailField( 'Электронная почта', max_length=75, blank=True, default='' ) site = models.CharField( 'Сайт', max_length=256, blank=True, default='' ) # поля, только для ИП svid_gos_reg = models.CharField( 'Свид-во о гос. регистрации', max_length=256, blank=True, default='', help_text='Требуется для счет-фактуры.' ) ip_reg_date = models.DateField('Дата регистрации ИП', blank=True, null=True) # поля, только для Организации name = models.CharField( 'Краткое название организации', max_length=256, default='', help_text='Будет подставляться в создаваемые документы.' ) full_name = models.CharField( 'Полное название организации', max_length=256, blank=True, default='', help_text='Как в учредительных документах.' ) kpp = models.CharField('КПП', max_length=9, default='') boss_title = models.CharField( 'Должность руководителя', max_length=256, blank=True, default='' ) na_osnovanii = models.CharField( 'Действует на основании', max_length=256, blank=True, default='' ) # подписи, печать и логотип boss_sign = models.ImageField( 'Подпись руководителя', blank=True, default='', upload_to=UploadAndRename(PROFILE_IMAGES_UPLOAD_DIR, 'boss_sign.png') ) glavbuh_sign = models.ImageField( 'Подпись бухгалтера', blank=True, default='', upload_to=UploadAndRename(PROFILE_IMAGES_UPLOAD_DIR, 'glavbuh_sign.png') ) stamp = models.ImageField( 'Печать', blank=True, default='', upload_to=UploadAndRename(PROFILE_IMAGES_UPLOAD_DIR, 'stamp.png') ) logo = models.ImageField( 'Логотип', blank=True, default='', upload_to=UploadAndRename(PROFILE_IMAGES_UPLOAD_DIR, 'logo.png') ) created_at = models.DateTimeField('Создан', auto_now_add=True) updated_at = models.DateTimeField('Изменен', auto_now=True) active = models.BooleanField('Активен', default=False) confirmed = models.BooleanField('Подтверждён', default=False) user_session_key = models.CharField( 'Ключ сессии (служебная информация)', max_length=256, blank=True, default='', help_text='Руками не трогать...' ) objects = managers.UserProfileManager() class Meta: verbose_name = 'Реквизиты (профиль)' verbose_name_plural = 'Реквизиты (профили)' def __str__(self): return '%s, ИНН %s' % (self.get_company_name()[0:30], self.inn or 'не указан') def save(self, *args, **kwargs): self.inn = only_numerics(self.inn) self.ogrn = only_numerics(self.ogrn) self.okpo = only_numerics(self.okpo) self.kpp = only_numerics(self.kpp) def process_img(orig_img, size): # TODO http://stackoverflow.com/questions/9166400/convert-rgba-png-to-rgb-with-pil 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) img.thumbnail(size, Image.ANTIALIAS) img.save(filename, 'PNG') 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) if self.logo: process_img(self.logo, size=LOGO_SIZE) def is_ip(self): return self.profile_type == consts.IP_PROFILE def is_org(self): return self.profile_type == consts.ORG_PROFILE def check_name_not_filled(self): """`ИП ФИО` или `Название Организации`.""" if self.is_ip(): return self.get_boss_full_fio().strip() == '' elif self.is_org(): return self.name.strip() == '' return False def get_main_bank_account(self): try: bank_accounts = BankAccount.objects.filter(company=self, is_main=True).first() return bank_accounts except BankAccount.DoesNotExist: return None def get_first_user(self): try: first_user = DokUser.objects.filter(profile=self)[0] return first_user except DokUser.DoesNotExist: return None def check_main_reqs_not_filled(self): result = self.check_name_not_filled() or self.inn == '' or self.address == '' or \ self.get_boss_fio() == '' or self.get_main_bank_account() == '' # noqa if result: return True if self.is_ip(): return self.ogrn == '' elif self.is_org(): return self.kpp == '' def get_company_name(self): """ `ИП ФИО` или `Название Организации`. """ if self.profile_type == consts.IP_PROFILE: return 'ИП %s' % self.get_boss_full_fio() elif self.profile_type == consts.ORG_PROFILE: return self.name.strip() return '' def get_inn_and_kpp(self): """Возвращает пару ИНН/КПП или только ИНН, если это ИП или КПП не заполнен.""" if self.profile_type == consts.ORG_PROFILE: kpp = self.kpp.strip() if kpp: return '%s/%s' % (self.inn, kpp,) return self.inn def get_boss_title(self): """Текст 'Индивидуальный предприниматель' или 'Руководитель организации'.""" if self.profile_type == consts.IP_PROFILE: return 'Индивидуальный предприниматель' elif self.profile_type == consts.ORG_PROFILE: return 'Руководитель организации' return '' def get_boss_fio(self): """Фамилия и инициалы руководителя ИП/организации.""" if self.boss_surname and self.boss_name and self.boss_midname: return '%s %s.%s.' % (self.boss_surname, self.boss_name[0], self.boss_midname[0],) return '' def get_boss_full_fio(self): """Полное ФИО руководителя ИП/организации.""" return ('%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 f'{self.glavbuh_surname} {self.glavbuh_name[0]}. {self.glavbuh_midname[0]}.' return '' def get_glavbuh_full_fio(self): """Полное ФИО главного бухгалтера.""" return f'{self.glavbuh_surname} {self.glavbuh_name} {self.glavbuh_midname}' def get_full_phone(self): """(Код города) Номер телефона.""" if self.phone_code: phone_code = self.phone_code.strip('() ') phone_code = f'({phone_code})' if self.phone and self.phone_code: return f'{phone_code} {self.phone}' def get_email(self): try: return self.get_first_user().email except: return None def get_full_fax(self): """(Код города) Номер факса.""" fax_code = self.fax_code.strip('() ') fax_code = '(%s)' % fax_code if fax_code else fax_code return ('%s %s' % (fax_code, self.fax,)).strip() def validate_has_profile_account(self): """ Check there account from this profile :return: True or False """ if self.bank_accounts.all(): return True else: return False class BankAccount(models.Model): """Расчетные счета.""" company = models.ForeignKey(UserProfile, related_name='bank_accounts') bik = models.CharField('БИК', max_length=10) name = models.CharField('Наименование банка', max_length=256) short_name = models.CharField( 'Сокращенное название банка', max_length=100, blank=True, default='' ) korr_account = models.CharField('Корр. счет', max_length=20) account = models.CharField('Расчетный счет', max_length=20) is_main = models.BooleanField('Основной счет', default=False) address = models.CharField('Местонахождение', max_length=256, blank=True, default='') # TODO delete field? created_at = models.DateTimeField('Создан', auto_now_add=True) updated_at = models.DateTimeField('Изменен', auto_now=True) objects = managers.BankAccountManager() class Meta: verbose_name = 'Расчётный счет' verbose_name_plural = 'Расчётные счета' ordering = ['-created_at'] def __str__(self): return f'{self.account}, {self.short_name[0:30] or self.name[0:30]}' def save(self, *args, **kwargs): self.bik = only_numerics(self.bik) self.korr_account = only_numerics(self.korr_account) self.account = only_numerics(self.account) super(BankAccount, self).save(*args, **kwargs) if self.is_main: # если задано, что это будет основной счет, то # сбросить у остальных счетов пользователя этот признак BankAccount.objects.filter( company=self.company, is_main=True).exclude(pk=self.pk).update(is_main=False) else: # если нет основного счета, то установить его принудительно BankAccount.objects.force_main(company=self.company) def delete(self, *args, **kwargs): super(BankAccount, self).delete(*args, **kwargs) # если нет основного счета, то установить его принудительно BankAccount.objects.force_main(company=self.company) class Client(models.Model): """Контрагенты.""" company = models.ForeignKey(UserProfile, related_name='clients') name = models.CharField('Наименование', max_length=256, db_index=True) name_short_self = models.CharField( 'Короткое наименование', max_length=256, null=True, blank=True ) name_short_dadata = models.CharField( 'Наименование из Dadata', max_length=256, null=True, blank=True ) inn = models.CharField('ИНН', max_length=12) kpp = models.CharField('КПП', max_length=9, blank=True, default='') # Организация ogrn = models.CharField('ОГРН', max_length=15, default='') okpo = models.CharField('ОКПО', max_length=10, blank=True, default='') # ИП address = models.CharField('Юр. адрес', max_length=256) # банковские реквизиты bank_bik = models.CharField('БИК', max_length=10, blank=True, default='') bank_name = models.CharField('Наименование банка', max_length=256, blank=True, default='') bank_short_name = models.CharField( 'Сокращенное наименование банка', max_length=256, blank=True, default='' ) bank_korr_account = models.CharField('Корр. счет', max_length=20, blank=True, default='') bank_account = models.CharField('Расчетный счет', max_length=20, blank=True, default='') bank_address = models.CharField('Местонахождение', max_length=256, blank=True, default='') # TODO delete field? # контакты contact_name = models.CharField('Имя', max_length=50, blank=True, default='') contact_email = models.EmailField('E-mail', max_length=50, blank=True, default='') contact_phone = models.CharField('Телефон', max_length=50, blank=True, default='') contact_skype = models.CharField('Skype', max_length=20, blank=True, default='') contact_other = models.CharField('Другое', max_length=256, blank=True, default='') created_at = models.DateTimeField('Создан', auto_now_add=True) updated_at = models.DateTimeField('Изменен', auto_now=True) objects = managers.ClientManager() class Meta: verbose_name = 'Контрагент' verbose_name_plural = 'Контрагенты' ordering = ['name', '-created_at'] def __str__(self): if self.name_short_self: return f'{self.name_short_dadata}, {self.name_short_self}' else: return f'{self.name}, ИНН {self.inn or "не указан"}' def save(self, *args, **kwargs): self.inn = only_numerics(self.inn) self.kpp = only_numerics(self.kpp) self.ogrn = only_numerics(self.ogrn) self.okpo = only_numerics(self.okpo) self.bank_bik = only_numerics(self.bank_bik) self.bank_korr_account = only_numerics(self.bank_korr_account) self.bank_account = only_numerics(self.bank_account) super(Client, self).save(*args, **kwargs) def get_inn_and_kpp(self): """Возвращает пару ИНН/КПП или только ИНН, если КПП не заполнен.""" kpp = self.kpp.strip() if kpp: return f'{self.inn}/{kpp}' return self.inn @property def short_name(self): if self.name_short_dadata: return self.name_short_dadata else: return self.name class UserProfileFilters(models.Model): """ Фильтрация реквизитов: какие данные показывать/скрывать при генерации карточки компании. """ company = models.OneToOneField(UserProfile, related_name='profile_filters', primary_key=True) # общие фильтры show_profile_type = models.BooleanField('Тип профиля', default=True) show_inn = models.BooleanField('ИНН', default=True) show_ogrn = models.BooleanField('ОГРН/ОГРНИП', default=True) show_okpo = models.BooleanField('ОКПО', default=True) show_glavbuh = models.BooleanField('Главный бухгалтер', default=True) show_bank_account = models.BooleanField('Банковские реквизиты', default=True) bank_account = models.ForeignKey( BankAccount, related_name='+', verbose_name='Расчетный счет', blank=True, null=True, default=None ) show_contact_info = models.BooleanField('Контактная информация', default=True) show_address = models.BooleanField('Фактический адрес', default=True) show_jur_address = models.BooleanField('Юридический адрес', default=True) show_real_address = models.BooleanField('Почтовый адрес', default=True) show_phone = models.BooleanField('Телефон', default=True) show_fax = models.BooleanField('Факс', default=True) show_email = models.BooleanField('Электронная почта', default=True) show_site = models.BooleanField('Сайт', default=True) show_logo = models.BooleanField('Логотип', default=True) # только для ИП show_ip_boss_fio = models.BooleanField('Фамилия, Имя, Отчество', default=True) show_svid_gos_reg = models.BooleanField('Свид-во о гос. регистрации', default=True) show_ip_reg_date = models.BooleanField('Дата регистрации ИП', default=True) # только для Организации show_name = models.BooleanField('Краткое название организации', default=True) show_full_name = models.BooleanField('Полное название организации', default=True) show_kpp = models.BooleanField('КПП', default=True) show_org_boss_title_and_fio = models.BooleanField( 'Руководитель (Должность, ФИО)', default=True) show_na_osnovanii = models.BooleanField('Действует на основании', default=True) objects = managers.UserProfileFiltersManager() class Meta: verbose_name = 'Фильтры реквизитов' verbose_name_plural = 'Фильтры реквизитов' def __str__(self): # TODO fix name return f'{self.company.email}' def save(self, *args, **kwargs): # всегда включены self.show_ip_boss_fio = True self.show_name = True super(UserProfileFilters, self).save(*args, **kwargs) class License(models.Model): company = models.ForeignKey( UserProfile, related_name='licenses', verbose_name='пользователь' ) term = models.IntegerField(verbose_name='срок лицензии') date_from = models.DateField('дата начала', null=True, blank=True) date_to = models.DateField('дата окончания', null=True, blank=True) payform = models.IntegerField(verbose_name='форма оплаты', choices=consts.PAYFORMS, default=0) status = models.IntegerField(verbose_name='статус лицензии', choices=consts.LICENSE_STATUSES, default=0) order_date = models.DateField(verbose_name='дата заказа', auto_now_add=True) paid_date = models.DateField(verbose_name='дата оплаты', null=True, blank=True) pay_sum = models.IntegerField(verbose_name='сумма оплаты') deleted = models.BooleanField('удалено', default=False) class Meta: verbose_name = 'Лицензии' verbose_name_plural = 'Лицензии' def __init__(self, *args, **kwargs): super(License, self).__init__(*args, **kwargs) self.__prev_date = self.paid_date def __str__(self): return '%s - %s %s (%d %s)' % ( self.company.get_company_name(), self.term, numeral.choose_plural(self.term, "месяц, месяца, месяцев"), self.pay_sum, numeral.choose_plural(self.pay_sum, "рубль, рубля, рублей"), ) def get_payment_id(self): if self.payform == 1: payments = Payment.objects.filter(order_number=self.id) if payments: return payments.first().id def get_payment(self): if self.payform == 1: payments = Payment.objects.filter(order_number=self.id) if payments: return payments.first() # TODO: test def save(self, *args, **kwargs): if not self.__prev_date and self.paid_date: max_date_license = License.objects.\ filter(company=self.company).aggregate(Max('date_to'))['date_to__max'] today = datetime.now().date() if max_date_license < today: max_date_license = today - timedelta(1) self.date_from = max_date_license + relativedelta(days=1) self.date_to = self.date_from + relativedelta(months=self.term, days=-1) self.company.active = True self.company.save() self.status = 1 utils.check_one_profile(self.company, datetime.now(), manual=True) super(License, self).save(*args, **kwargs) def get_company(self): return self.company.get_company_name() def get_action_link(self): if self.status == 0: if self.payform == 0: url = reverse( 'get_payment_account', kwargs={'order_num': self.id} ) return f'Оплата безналичным платежом' elif self.payform == 1: if self.get_payment(): url = get_robokassa_url(self.get_payment()) else: url = '#' return f'Оплата картой, электронными деньгами, наличными' elif self.status in [1, 2]: url = reverse( 'get_certificate_of_completion', kwargs={'order_num': self.id} ) cost_str = f'{self.pay_sum} ' \ f'{numeral.choose_plural(self.pay_sum, "рубль, рубля, рублей")}' return f'Скачать акт № {self.id} на {cost_str}' elif self.status == 4: if self.payform == 0: return f'Оплата безналичным платежом' elif self.payform == 1: return f'Оплата картой, ' \ f'электронными деньгами, наличными' else: return '' def get_term(self): if self.term == 0: return '45 дней' else: return f'{self.term} {numeral.choose_plural(self.term, "месяц, месяца, месяцев")}' def get_paid_status(self): if self.status == 1: return 'Лицензия выдана, ещё не активирована' elif self.status in [2, -1]: left = relativedelta(self.date_to, timezone.now().date()) if left.months: left_str = f'{left.months} ' \ f'{numeral.choose_plural(left.months, "месяц, месяца, месяцев")} ' \ f'{left.days} {numeral.choose_plural(left.days, "день, дня, дней")}' else: left_str = f'{left.days} {numeral.choose_plural(left.days, "день, дня, дней")}' remain_str = numeral.choose_plural(left.days, "остался, осталось, осталось") return f'Лицензия активна, {remain_str} {left_str}' elif self.status == 3: return 'Время истекло' else: return None @property def account_status(self): if self.status in [0, 4]: return 'Счет не оплачен' else: return 'Счет оплачен' @property def account_sub_status(self): if self.status == 4: freeze_date = self.order_date + timezone.timedelta(5) return f'Счет заморожен {freeze_date.strftime("%d.%m.%Y")}' if self.status == 0: remain_day = relativedelta(self.order_date + timezone.timedelta(5), timezone.now().date()) remain_day_str = f'{remain_day.days} ' \ f'{numeral.choose_plural(remain_day.days, "день, дня, дней")}' remain_str = numeral.choose_plural(remain_day.days, "Остался, Осталось, Осталось") if remain_day.days == 0: return 'Завтра счет будет заморожен' else: return f'{remain_str} {remain_day_str}' @property def is_status_need_to_change(self): if self.status == 0: remain_day = relativedelta(self.order_date + timezone.timedelta(5), timezone.now().date()) if remain_day.days <= 0: return True else: return False def set_freeze_status(self): try: self.status = 4 self.save() return True except Exception as e: log.info(f"don't change status.Error {e}") return False class LicensePrice(models.Model): term = models.IntegerField(verbose_name='срок лицензии', choices=consts.TERMS) price = models.IntegerField(verbose_name='сумма оплаты') class Meta: verbose_name = 'Прайс на лицензии' verbose_name_plural = 'Прайсы на лицензии' def __str__(self): return f'{self.term} {numeral.choose_plural(self.term, "месяц, месяца, месяцев")} ' \ f'({self.price} {numeral.choose_plural(self.price, "рубль, рубля, рублей")})' class Payment(models.Model): PROCESSED = 0 SUCCESS = 1 FAIL = 2 CHOICES = ( (PROCESSED, 'Ожидает оплаты'), (SUCCESS, 'Оплачен'), (FAIL, 'Отклонен'), ) BANK = 0 CARD = 1 CHOICES_TYPE = ( (BANK, 'Безналичный расчёт'), (CARD, 'Банковская карта'), ) order_amount = models.DecimalField('Сумма заказа', max_digits=15, decimal_places=2) order_number = models.IntegerField('Номер заказа') user = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='payment_user', verbose_name='Пользователь' ) created = models.DateTimeField('Время создания', auto_now_add=True) status = models.PositiveSmallIntegerField( verbose_name='Статус', choices=CHOICES, default=PROCESSED ) type = models.PositiveSmallIntegerField( verbose_name='Тип платежа', choices=CHOICES_TYPE, default=BANK ) date = models.DateField(verbose_name='Дата', null=True, blank=True) class Meta: verbose_name = 'Платеж' verbose_name_plural = 'Платежи' def __str__(self): return f'{self.user}-{self.order_number}-{self.get_status_display()}' def save(self, *args, **kwargs): # if manual setup if self.status == self.SUCCESS: if self.date is None: self.date = datetime.now().date() payment = super(Payment, self).save(*args, **kwargs) return payment def confirmed_purchase(**kwargs): try: payment = Payment.objects.get( pk=kwargs['InvId'], order_amount=kwargs['OutSum'] ) payment.status = Payment.SUCCESS payment.date = datetime.now().date() payment.save() except Payment.DoesNotExist: log.info(f"payment with id={kwargs['InvId']} not found") # Robokassa @receiver(result_received) def order_completed(sender, **kwargs): confirmed_purchase(**kwargs) # Robokassa for local debugging @receiver(success_page_visited) def success_page_visited_completed(sender, **kwargs): if settings.DEBUG: confirmed_purchase(**kwargs) @receiver(post_save, sender=Payment) def check_license_dependence(sender, **kwargs): if not kwargs.get('created'): payment = kwargs.get('instance') if payment.status == Payment.SUCCESS and payment.date: try: lic = License.objects.get(pk=payment.order_number) lic.status = consts.STATUS_PAID lic.paid_date = payment.date lic.save() emails.send_license_successful_purchased.delay( payment.user.email, lic.pk, lic.get_term(), lic.date_from.strftime("%d.%m.%Y") ) payment.user.profile.active = True payment.user.profile.save() bonus_license = create_bonus_license(lic) if bonus_license: emails.send_bonus_license_issued.delay( payment.user.email, lic.pk, lic.get_term(), bonus_license.get_term(), bonus_license.date_from.strftime("%d.%m.%Y") ) except License.DoesNotExist: log.info(f"payment with id={kwargs['InvId']} not found")