it_manager+reports: иерархия договоров, биллинг и месячные отчёты

Иерархия рамочных/дочерних договоров и платежей, интернет-биллинг,
месячные IT-отчёты (импорт, DOCX/PDF, шаблоны выводов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 18:03:44 +03:00
parent 897ffc99c9
commit 0da9c13192
59 changed files with 6861 additions and 267 deletions
+31 -13
View File
@@ -1,5 +1,6 @@
from django import forms
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@@ -9,7 +10,7 @@ from unfold.admin import ModelAdmin, TabularInline
from .models import (
Equipment, EquipmentAssignment, EquipmentModel,
Ticket, TicketComment,
InternetProvider, InternetContract,
InternetProvider, InternetContract, InternetContractPayment,
WorkLog, ServiceCall,
Consumable,
)
@@ -17,23 +18,28 @@ from .models import (
class LocationChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
type_label = obj.get_location_type_display()
addr = obj.get_full_address() or obj.address or ''
if obj.location_type == 'pharmacy':
try:
num = obj.pharmacy.pharmacy_number
return f'Аптека №{num} — {addr}'
except Exception:
pass
return f'{type_label} — {addr}' if addr else type_label
return str(obj)
class InternetContractForm(forms.ModelForm):
location = LocationChoiceField(
queryset=None,
required=False,
label=_('Локация'),
)
def clean(self):
cleaned = super().clean()
is_framework = cleaned.get('is_framework')
location = cleaned.get('location')
if is_framework and location:
raise ValidationError({'location': _('У рамочного договора локация не указывается.')})
if not is_framework and not location:
raise ValidationError({'location': _('Укажите локацию для договора.')})
if is_framework:
cleaned['location'] = None
return cleaned
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from apps.locations.models import Location
@@ -400,12 +406,20 @@ class TicketAdmin(ModelAdmin):
class InternetContractInline(TabularInline):
model = InternetContract
form = InternetContractForm
extra = 0
fields = ('location', 'contract_number', 'service_type', 'status',
'bandwidth_down', 'bandwidth_up', 'monthly_cost', 'end_date')
fields = ('location', 'contract_number', 'is_framework', 'parent_contract', 'service_type', 'status',
'start_date', 'bandwidth_down', 'bandwidth_up', 'monthly_cost', 'end_date')
show_change_link = True
class InternetContractPaymentInline(TabularInline):
model = InternetContractPayment
extra = 0
fields = ('period', 'amount', 'is_paid', 'paid_at', 'invoice_number', 'recorded_by')
readonly_fields = ('recorded_by',)
@admin.register(InternetProvider)
class InternetProviderAdmin(ModelAdmin):
list_display = ['name', 'phone', 'support_phone', 'email',
@@ -443,8 +457,10 @@ class InternetProviderAdmin(ModelAdmin):
@admin.register(InternetContract)
class InternetContractAdmin(ModelAdmin):
form = InternetContractForm
inlines = [InternetContractPaymentInline]
list_display = [
'provider', 'location', 'contract_number', 'service_type_display',
'provider', 'location', 'contract_number', 'is_framework', 'parent_contract',
'service_type_display',
'connection_type_display', 'status_display', 'bandwidth_display',
'monthly_cost', 'end_date', 'expiry_display',
]
@@ -459,10 +475,12 @@ class InternetContractAdmin(ModelAdmin):
fieldsets = (
(_('Договор'), {
'fields': ('provider', 'contract_number', 'account_number',
'is_framework', 'parent_contract',
'service_type', 'connection_type', 'status'),
}),
(_('Локация'), {
'fields': ('location',),
'description': _('Для рамочного договора локация не указывается.'),
}),
(_('Параметры'), {
'fields': ('bandwidth_down', 'bandwidth_up', 'monthly_cost'),