Files
ACORE/apps/it_manager/models/providers.py
T
lokses 0da9c13192 it_manager+reports: иерархия договоров, биллинг и месячные отчёты
Иерархия рамочных/дочерних договоров и платежей, интернет-биллинг,
месячные IT-отчёты (импорт, DOCX/PDF, шаблоны выводов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:44 +03:00

194 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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