Files
ACORE/apps/projects/services.py
T
lokses 030bbc98fd projects: модуль проектов с шагами по отделам и Task.
Шаблоны/ручное создание, access_projects, сид «Открытие аптеки». Плюс SILO/S3 для Django media.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 16:26:45 +03:00

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