"""Создание проектов из шаблона / вручную и активация (Task).""" from __future__ import annotations from datetime import timedelta from django.db import transaction from django.utils import timezone from apps.projects.models import ( Project, ProjectStep, ProjectStepItem, ProjectTemplate, ) from apps.tasks.models import Task def _department_head(department_id): try: from apps.reports.it_monthly.builder import department_head return department_head(department_id) except Exception: # noqa: BLE001 return None def _add_days(base, days: int): if base is None: return None return base + timedelta(days=int(days or 0)) @transaction.atomic def create_project_from_template( *, template: ProjectTemplate, title: str, owner, created_by=None, start_date=None, due_date=None, pharmacy=None, description: str = '', ) -> Project: start = start_date or timezone.localdate() if due_date is None and template.default_duration_days: due_date = _add_days(start, template.default_duration_days) project = Project.objects.create( title=(title or template.name).strip() or template.name, description=(description or template.description or '').strip(), status=Project.STATUS_DRAFT, start_date=start, due_date=due_date, pharmacy=pharmacy, template=template, owner=owner, created_by=created_by or owner, ) for t_step in template.steps.prefetch_related('items').order_by('order', 'id'): step = ProjectStep.objects.create( project=project, title=t_step.title, order=t_step.order, department=t_step.department, status=ProjectStep.STATUS_PENDING, planned_start=_add_days(start, t_step.offset_days), planned_due=_add_days(start, t_step.offset_days + t_step.duration_days), ) for t_item in t_step.items.order_by('order', 'id'): ProjectStepItem.objects.create( step=step, title=t_item.title, description=t_item.description, priority=t_item.priority, order=t_item.order, ) return project @transaction.atomic def create_manual_project( *, title: str, owner, created_by=None, description: str = '', start_date=None, due_date=None, pharmacy=None, steps: list[dict] | None = None, ) -> Project: """steps: [{title, department_id, order, items: [{title, description, priority}]}]""" start = start_date or timezone.localdate() project = Project.objects.create( title=title.strip(), description=(description or '').strip(), status=Project.STATUS_DRAFT, start_date=start, due_date=due_date, pharmacy=pharmacy, owner=owner, created_by=created_by or owner, ) for idx, raw in enumerate(steps or []): step = ProjectStep.objects.create( project=project, title=(raw.get('title') or f'Шаг {idx + 1}').strip(), order=int(raw.get('order', idx)), department_id=raw.get('department_id') or None, status=ProjectStep.STATUS_PENDING, planned_start=raw.get('planned_start') or start, planned_due=raw.get('planned_due') or due_date, ) for j, item in enumerate(raw.get('items') or []): ProjectStepItem.objects.create( step=step, title=(item.get('title') or f'Задача {j + 1}').strip(), description=(item.get('description') or '').strip(), priority=item.get('priority') or 'medium', order=int(item.get('order', j)), ) return project def _ensure_task_for_item(item: ProjectStepItem, *, owner) -> Task | None: if item.task_id: return item.task step = item.step head = _department_head(step.department_id) task = Task.objects.create( title=item.title, description=item.description or '', status='pending', priority=item.priority or 'medium', due_date=step.planned_due, creator=owner, ) responsible = head or owner if responsible is not None: task.responsible.add(responsible) item.task = task item.save(update_fields=['task', 'updated_at']) return task @transaction.atomic def activate_project(project: Project) -> Project: """draft → active: создать Task для всех пунктов без задачи.""" if project.status == Project.STATUS_CANCELLED: raise ValueError('Отменённый проект нельзя активировать.') owner = project.owner or project.created_by for step in project.steps.prefetch_related('items').order_by('order', 'id'): for item in step.items.all(): _ensure_task_for_item(item, owner=owner) step.refresh_status(save=True) if project.status == Project.STATUS_DRAFT: project.status = Project.STATUS_ACTIVE if not project.start_date: project.start_date = timezone.localdate() project.save(update_fields=['status', 'start_date', 'updated_at']) refresh_project_status(project) return project @transaction.atomic def activate_step(step: ProjectStep) -> ProjectStep: project = step.project owner = project.owner or project.created_by for item in step.items.all(): _ensure_task_for_item(item, owner=owner) step.refresh_status(save=True) if project.status == Project.STATUS_DRAFT: project.status = Project.STATUS_ACTIVE if not project.start_date: project.start_date = timezone.localdate() project.save(update_fields=['status', 'start_date', 'updated_at']) refresh_project_status(project) return step def refresh_project_status(project: Project) -> str: """Авто-done, если все шаги done и есть хотя бы один шаг с задачами.""" steps = list(project.steps.all()) if not steps: return project.status for step in steps: step.refresh_status(save=True) steps = list(project.steps.all()) if project.status == Project.STATUS_ACTIVE and steps and all( s.status == ProjectStep.STATUS_DONE for s in steps ): project.status = Project.STATUS_DONE project.save(update_fields=['status', 'updated_at']) return project.status