from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from core.models.directories import directory_default class InternetProvider(models.Model): name = models.CharField('Название', max_length=200, unique=True) phone = models.CharField('Телефон', max_length=50, blank=True) email = models.EmailField('Email', blank=True) website = models.URLField('Сайт', blank=True) support_phone = models.CharField('Телефон техподдержки', max_length=50, blank=True) support_email = models.EmailField('Email техподдержки', blank=True) notes = models.TextField('Примечания', blank=True) is_active = models.BooleanField('Активен', default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = 'Провайдер' verbose_name_plural = 'Провайдеры' ordering = ['name'] def __str__(self): return self.name @property def active_contracts_count(self): return self.contracts.filter(status__code='active').count() class InternetContract(models.Model): provider = models.ForeignKey( InternetProvider, on_delete=models.PROTECT, related_name='contracts', verbose_name='Провайдер', ) location = models.ForeignKey( 'locations.Location', on_delete=models.PROTECT, related_name='internet_contracts', verbose_name='Локация', null=True, blank=True, ) contract_number = models.CharField('Номер договора', max_length=100) service_type = models.ForeignKey( 'core.Directory', on_delete=models.PROTECT, limit_choices_to={'directory_type__code': 'contract_service_type', 'is_active': True}, related_name='contracts_by_service_type', verbose_name='Тип услуги', default=directory_default('contract_service_type', 'internet'), ) connection_type = models.ForeignKey( 'core.Directory', on_delete=models.SET_NULL, limit_choices_to={'directory_type__code': 'contract_connection_type', 'is_active': True}, related_name='contracts_by_connection_type', verbose_name='Технология подключения', null=True, blank=True, ) status = models.ForeignKey( 'core.Directory', on_delete=models.PROTECT, limit_choices_to={'directory_type__code': 'contract_status', 'is_active': True}, related_name='contracts_by_status', verbose_name='Статус', default=directory_default('contract_status', 'active'), ) start_date = models.DateField('Дата начала') end_date = models.DateField('Дата окончания', null=True, blank=True) monthly_cost = models.DecimalField('Стоимость в месяц', max_digits=10, decimal_places=2, null=True, blank=True) bandwidth_down = models.PositiveIntegerField('Скорость вниз (Мбит/с)', null=True, blank=True) bandwidth_up = models.PositiveIntegerField('Скорость вверх (Мбит/с)', null=True, blank=True) static_ips = models.TextField('Статические IP', blank=True, help_text='По одному на строку') account_number = models.CharField('Лицевой счёт', max_length=100, blank=True) notes = models.TextField('Примечания', blank=True) is_framework = models.BooleanField( 'Общий (рамочный) договор', default=False, help_text='Групповой договор провайдера; к нему привязываются дочерние.', ) parent_contract = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.PROTECT, related_name='child_contracts', verbose_name='Общий договор', ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Договор с провайдером' verbose_name_plural = 'Договоры с провайдерами' ordering = ['provider__name', 'contract_number'] unique_together = [('provider', 'contract_number')] def __str__(self): return f'{self.provider} — {self.location_display} (№{self.contract_number})' @property def location_display(self) -> str: if self.location_id: return str(self.location) if self.is_framework: return '— общий договор —' return '—' @property def is_expiring_soon(self): if not self.end_date or self.status.code != 'active': return False return (self.end_date - timezone.now().date()).days <= 30 @property def bandwidth_display(self): if self.bandwidth_down and self.bandwidth_up: return f'{self.bandwidth_down}↓ / {self.bandwidth_up}↑ Мбит/с' if self.bandwidth_down: return f'{self.bandwidth_down} Мбит/с' return '—' @property def static_ip_list(self): return [ip.strip() for ip in self.static_ips.splitlines() if ip.strip()] def clean(self): super().clean() if self.parent_contract_id: if self.parent_contract_id == self.pk: raise ValidationError({'parent_contract': 'Договор не может ссылаться сам на себя.'}) if self.parent_contract.provider_id != self.provider_id: raise ValidationError({'parent_contract': 'Общий договор должен быть у того же провайдера.'}) if self.parent_contract.parent_contract_id: raise ValidationError({'parent_contract': 'Дочерний договор можно привязать только к рамочному.'}) if self.is_framework and self.parent_contract_id: raise ValidationError({ 'is_framework': 'Рамочный договор не может иметь родителя.', 'parent_contract': 'Рамочный договор не может иметь родителя.', }) if self.is_framework: if self.location_id: raise ValidationError({'location': 'У рамочного договора локация не указывается.'}) elif not self.location_id: raise ValidationError({'location': 'Укажите локацию для договора.'}) class InternetContractPayment(models.Model): """Оплата по договору за календарный месяц (учёт в бухгалтерии).""" contract = models.ForeignKey( InternetContract, on_delete=models.CASCADE, related_name='payments', verbose_name='Договор', ) period = models.DateField('Период (месяц)', help_text='Первое число месяца') amount = models.DecimalField( 'Сумма', max_digits=10, decimal_places=2, null=True, blank=True, ) is_paid = models.BooleanField('Оплачено', default=False) paid_at = models.DateField('Дата оплаты', null=True, blank=True) invoice_number = models.CharField('Номер счёта/акта', max_length=100, blank=True) notes = models.TextField('Примечания', blank=True) recorded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='internet_payments_recorded', verbose_name='Кто отметил', ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Оплата по договору' verbose_name_plural = 'Оплаты по договорам' ordering = ['-period', 'contract'] constraints = [ models.UniqueConstraint( fields=['contract', 'period'], name='unique_internet_payment_per_contract_period', ), ] def __str__(self): status = 'оплачено' if self.is_paid else 'не оплачено' return f'{self.contract.contract_number} · {self.period:%m.%Y} · {status}' def clean(self): super().clean() if self.period and self.period.day != 1: raise ValidationError({'period': 'Укажите первое число месяца.'}) if self.is_paid and not self.paid_at: self.paid_at = timezone.localdate() if not self.is_paid: self.paid_at = None if self.amount is None and self.contract_id: self.amount = self.contract.monthly_cost