it_manager: справочник типовых работ в журнале WorkLog.
Категория/работа/required_details вместо свободного work_type; seed Directory, отчёт и тесты. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+106
-49
@@ -15,6 +15,13 @@ from apps.it_manager.models import (
|
||||
InternetContract, InternetProvider,
|
||||
ServiceCall, Ticket, TicketComment, WorkLog,
|
||||
)
|
||||
from apps.it_manager.work_directory import (
|
||||
catalog_payload_for_ui,
|
||||
detail_labels_from_db,
|
||||
get_catalog_work,
|
||||
parse_details_from_post,
|
||||
validate_worklog_details,
|
||||
)
|
||||
from apps.it_manager.monthly_report import (
|
||||
MONTHS_RU,
|
||||
user_can_manage_it_monthly_report,
|
||||
@@ -306,7 +313,9 @@ def _it_manager_context(request):
|
||||
'comments__author',
|
||||
models.Prefetch(
|
||||
'work_logs',
|
||||
queryset=scoped_queryset(request.user, WorkLog.objects.all()).select_related('technician', 'work_type').order_by('-date', '-created_at'),
|
||||
queryset=scoped_queryset(request.user, WorkLog.objects.all()).select_related(
|
||||
'technician', 'work_type', 'catalog_work', 'catalog_work__parent',
|
||||
).order_by('-date', '-created_at'),
|
||||
),
|
||||
)
|
||||
.annotate(worklogs_count=models.Count('work_logs', distinct=True))
|
||||
@@ -330,6 +339,8 @@ def _it_manager_context(request):
|
||||
month_start = timezone.localdate().replace(day=1)
|
||||
worklog_technician_raw = (request.GET.get('worklog_technician') or '').strip()
|
||||
worklog_technician_id = int(worklog_technician_raw) if worklog_technician_raw.isdigit() else None
|
||||
worklog_category_raw = (request.GET.get('worklog_category') or '').strip()
|
||||
worklog_category_id = int(worklog_category_raw) if worklog_category_raw.isdigit() else None
|
||||
|
||||
worklog_month_raw = (request.GET.get('worklog_month') or '').strip()
|
||||
worklog_month_start = None
|
||||
@@ -346,6 +357,7 @@ def _it_manager_context(request):
|
||||
worklogs_qs = (
|
||||
scoped_queryset(request.user, WorkLog.objects.all()).select_related(
|
||||
'technician', 'location', 'equipment', 'ticket', 'work_type',
|
||||
'catalog_work', 'catalog_work__parent',
|
||||
)
|
||||
.prefetch_related(
|
||||
models.Prefetch(
|
||||
@@ -357,11 +369,15 @@ def _it_manager_context(request):
|
||||
)
|
||||
if worklog_technician_id:
|
||||
worklogs_qs = worklogs_qs.filter(technician_id=worklog_technician_id)
|
||||
if worklog_category_id:
|
||||
worklogs_qs = worklogs_qs.filter(catalog_work__parent_id=worklog_category_id)
|
||||
if worklog_month_start and worklog_month_end:
|
||||
worklogs_qs = worklogs_qs.filter(
|
||||
date__gte=worklog_month_start, date__lte=worklog_month_end,
|
||||
)
|
||||
worklogs_page = paginate(request, worklogs_qs, page_param='worklogs_page', size_param='worklogs_per_page')
|
||||
it_work_catalog = catalog_payload_for_ui()
|
||||
worklog_detail_labels = it_work_catalog.get('detail_labels') or detail_labels_from_db()
|
||||
|
||||
# Статистика: выбранный месяц или текущий календарный
|
||||
if worklog_month_start and worklog_month_end:
|
||||
@@ -468,10 +484,13 @@ def _it_manager_context(request):
|
||||
'worklog_types': _directory_choices('worklog_type'),
|
||||
'worklog_technicians': worklog_technicians,
|
||||
'worklog_technician_id': worklog_technician_id,
|
||||
'worklog_category_id': worklog_category_id,
|
||||
'worklog_month_id': worklog_month_id,
|
||||
'worklog_month_choices': worklog_month_choices,
|
||||
'worklog_stats_label': worklog_stats_label,
|
||||
'it_can_edit_worklogs': can_edit_worklogs,
|
||||
'it_work_catalog': it_work_catalog,
|
||||
'worklog_detail_labels': worklog_detail_labels,
|
||||
'month_hours': month_hours,
|
||||
'month_count': month_count,
|
||||
|
||||
@@ -519,6 +538,7 @@ def it_manager_view(request):
|
||||
elif (
|
||||
'worklog_technician' in request.GET
|
||||
or 'worklog_month' in request.GET
|
||||
or 'worklog_category' in request.GET
|
||||
or request.GET.get('worklogs_page')
|
||||
):
|
||||
context['it_active_tab'] = 'worklog'
|
||||
@@ -887,49 +907,53 @@ def worklog_create_view(request):
|
||||
worklog_error = ''
|
||||
worklog_message = ''
|
||||
if request.method == 'POST':
|
||||
catalog_work = get_catalog_work(request.POST.get('catalog_work_id'))
|
||||
description = request.POST.get('description', '').strip()
|
||||
work_type_raw = request.POST.get('work_type', '')
|
||||
work_type_id = _resolve_directory('worklog_type', work_type_raw)
|
||||
details = parse_details_from_post(request.POST, catalog_work)
|
||||
|
||||
if not description:
|
||||
worklog_error = 'Укажите описание работ.'
|
||||
elif not work_type_id or work_type_raw == '__new__':
|
||||
worklog_error = 'Выберите вид работ.'
|
||||
if not catalog_work:
|
||||
worklog_error = 'Выберите типовую работу из справочника.'
|
||||
else:
|
||||
try:
|
||||
log_date = datetime.strptime(request.POST.get('date', ''), '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
log_date = timezone.localdate()
|
||||
try:
|
||||
hours_spent = float(request.POST.get('hours_spent', '').strip() or 0) or None
|
||||
except ValueError:
|
||||
hours_spent = None
|
||||
|
||||
location_id = request.POST.get('location_id') or None
|
||||
if not location_id and not has_all_locations(request.user):
|
||||
return HttpResponseForbidden('Укажите доступную локацию.')
|
||||
equipment_id = request.POST.get('equipment_id') or None
|
||||
ticket_id = request.POST.get('ticket_id') or None
|
||||
|
||||
try:
|
||||
WorkLog.objects.create(
|
||||
date=log_date,
|
||||
technician=request.user,
|
||||
work_type_id=work_type_id,
|
||||
location_id=location_id,
|
||||
equipment_id=equipment_id,
|
||||
ticket_id=ticket_id,
|
||||
description=description,
|
||||
result=request.POST.get('result', '').strip(),
|
||||
hours_spent=hours_spent,
|
||||
)
|
||||
except Exception:
|
||||
worklog_error = 'Не удалось сохранить запись. Проверьте локацию, заявку и оборудование.'
|
||||
details_err = validate_worklog_details(catalog_work, details)
|
||||
if details_err:
|
||||
worklog_error = details_err
|
||||
else:
|
||||
if ticket_id:
|
||||
worklog_message = 'Работа привязана к заявке и сохранена.'
|
||||
try:
|
||||
log_date = datetime.strptime(request.POST.get('date', ''), '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
log_date = timezone.localdate()
|
||||
try:
|
||||
hours_spent = float(request.POST.get('hours_spent', '').strip() or 0) or None
|
||||
except ValueError:
|
||||
hours_spent = None
|
||||
|
||||
location_id = request.POST.get('location_id') or None
|
||||
if not location_id and not has_all_locations(request.user):
|
||||
return HttpResponseForbidden('Укажите доступную локацию.')
|
||||
equipment_id = request.POST.get('equipment_id') or None
|
||||
ticket_id = request.POST.get('ticket_id') or None
|
||||
|
||||
try:
|
||||
WorkLog.objects.create(
|
||||
date=log_date,
|
||||
technician=request.user,
|
||||
catalog_work=catalog_work,
|
||||
details=details,
|
||||
work_type_id=None,
|
||||
location_id=location_id,
|
||||
equipment_id=equipment_id,
|
||||
ticket_id=ticket_id,
|
||||
description=description,
|
||||
result=request.POST.get('result', '').strip(),
|
||||
hours_spent=hours_spent,
|
||||
)
|
||||
except Exception:
|
||||
worklog_error = 'Не удалось сохранить запись. Проверьте локацию, заявку и оборудование.'
|
||||
else:
|
||||
worklog_message = 'Запись о выполненной работе сохранена.'
|
||||
if ticket_id:
|
||||
worklog_message = 'Работа привязана к заявке и сохранена.'
|
||||
else:
|
||||
worklog_message = 'Запись о выполненной работе сохранена.'
|
||||
|
||||
context = _it_manager_context(request)
|
||||
context['it_active_tab'] = 'worklog'
|
||||
@@ -953,19 +977,47 @@ def worklog_update_view(request, pk):
|
||||
|
||||
log = get_object_or_404(scoped_queryset(request.user, WorkLog.objects.all()), pk=pk)
|
||||
description = request.POST.get('description', '').strip()
|
||||
work_type_raw = request.POST.get('work_type', '')
|
||||
work_type_id = _resolve_directory('worklog_type', work_type_raw)
|
||||
catalog_raw = (request.POST.get('catalog_work_id') or '').strip()
|
||||
catalog_work = get_catalog_work(catalog_raw) if catalog_raw else None
|
||||
# Если запись уже из справочника — сохраняем FK даже без смены в форме
|
||||
if not catalog_work and log.catalog_work_id and not catalog_raw:
|
||||
catalog_work = log.catalog_work
|
||||
|
||||
if not description:
|
||||
work_type_raw = request.POST.get('work_type', '')
|
||||
work_type_id = _resolve_directory('worklog_type', work_type_raw) if work_type_raw else None
|
||||
|
||||
if catalog_work:
|
||||
details = parse_details_from_post(request.POST, catalog_work)
|
||||
# если detail_* не пришли — оставить прежние значения для ключей без POST
|
||||
if log.catalog_work_id == catalog_work.pk and log.details:
|
||||
merged = dict(log.details)
|
||||
for key in details:
|
||||
if f'detail_{key}' in request.POST:
|
||||
merged[key] = details[key]
|
||||
details = merged
|
||||
details_err = validate_worklog_details(catalog_work, details)
|
||||
if details_err:
|
||||
return HttpResponse(
|
||||
f'<span style="color:var(--color-destructive); font-size:0.75rem;">{details_err}</span>',
|
||||
status=400,
|
||||
)
|
||||
elif log.catalog_work_id:
|
||||
return HttpResponse(
|
||||
'<span style="color:var(--color-destructive); font-size:0.75rem;">Нужно описание</span>',
|
||||
status=400,
|
||||
)
|
||||
if not work_type_id or work_type_raw == '__new__':
|
||||
return HttpResponse(
|
||||
'<span style="color:var(--color-destructive); font-size:0.75rem;">Укажите вид работ</span>',
|
||||
'<span style="color:var(--color-destructive); font-size:0.75rem;">Укажите типовую работу</span>',
|
||||
status=400,
|
||||
)
|
||||
else:
|
||||
# Историческая запись без справочника
|
||||
if not description:
|
||||
return HttpResponse(
|
||||
'<span style="color:var(--color-destructive); font-size:0.75rem;">Нужно описание</span>',
|
||||
status=400,
|
||||
)
|
||||
if not work_type_id or work_type_raw == '__new__':
|
||||
return HttpResponse(
|
||||
'<span style="color:var(--color-destructive); font-size:0.75rem;">Укажите вид работ</span>',
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
log_date = datetime.strptime(request.POST.get('date', ''), '%Y-%m-%d').date()
|
||||
@@ -991,7 +1043,12 @@ def worklog_update_view(request, pk):
|
||||
return HttpResponseForbidden('Укажите доступную локацию.')
|
||||
log.date = log_date
|
||||
log.technician_id = technician_id
|
||||
log.work_type_id = work_type_id
|
||||
if catalog_work:
|
||||
log.catalog_work = catalog_work
|
||||
log.details = details
|
||||
log.work_type_id = None
|
||||
else:
|
||||
log.work_type_id = work_type_id
|
||||
log.description = description
|
||||
log.result = request.POST.get('result', '').strip()
|
||||
log.hours_spent = hours_spent
|
||||
|
||||
Reference in New Issue
Block a user