projects: модуль проектов с шагами по отделам и Task.
Шаблоны/ручное создание, access_projects, сид «Открытие аптеки». Плюс SILO/S3 для Django media. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""Создание проектов из шаблона / вручную и активация (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
|
||||
Reference in New Issue
Block a user