030bbc98fd
Шаблоны/ручное создание, access_projects, сид «Открытие аптеки». Плюс SILO/S3 для Django media. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
"""Создание проектов из шаблона / вручную и активация (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
|