KNOTTA research & development

Наряды и бригады

Модель ServiceVisit, статусная машина, состав бригад и автоматическая синхронизация с отчётом.

Наряды и бригады

ServiceVisit

Префикс: sv_ Таблица: service_visit Файл: garden/modules/service_visit/models.py

Один выезд бригады на объект клиента — центральная сущность платформы.

ПолеТипОписание
idKSUID, sv_*
objectFK → ClientObjectНа какой территории работаем (PROTECT)
brigadierFK → UserНазначенный ответственный за выезд. Ограничено role=brigadier. Назначает сотрудник или администратор при создании наряда.
report_templateFK → ReportSurveyTemplateКакой шаблон отчёта использовать
statusServiceVisitStatusСм. статусную машину ниже
planned_forDateTimeField(nullable)Когда запланировано
started_atDateTimeField(nullable)Авто-выставляется при → started
ended_atDateTimeField(nullable)Авто-выставляется при → completed или → cancelled
display_team_nameCharField(255)Опциональное название команды для UI

При создании, если report_template не указан явно, в save() подставляется текущий default-шаблон (ReportSurveyTemplate.is_default=True).

Подключает simple_history.HistoricalRecords().

Статусная машина

Загрузка диаграммы…
СтатусЧто значит
plannedНаряд создан, бригада, дата и шаблон назначены. Стартовая позиция.
startedБригада приступила к работе. Срабатывает NotificationRouter().notify(WORK_STARTED, …) → admin/staff + контактные лица.
completedРаботы завершены. Срабатывает WORK_COMPLETED → admin/staff + контактные лица.
cancelledНаряд отменён. Допустимо из planned или started. Не допустимо из completed (HTTP 400).

Свойство is_active:

@property
def is_active(self):
    return self.status in {ServiceVisitStatus.PLANNED, ServiceVisitStatus.STARTED}

Используется в фильтрах списков нарядов.

ServiceVisitMember

Префикс: svm_ Таблица: service_visit_member

Участники бригады. Привязка User → ServiceVisit с ролью.

ПолеТипОписание
idKSUID, svm_*
service_visitFK → ServiceVisitКакой наряд
userFK → UserКто. Ограничено role IN (brigadier, worker)
roleServiceVisitMemberRolelead или worker (default: worker)

Constraint uq_service_visit_user_once: один пользователь не может быть в одном наряде дважды.

class ServiceVisitMemberRole(models.TextChoices):
    LEAD   = "lead",   _("Lead")
    WORKER = "worker", _("Worker")

lead — старший на объекте (обычно бригадир, но может быть и другой опытный работник). worker — рядовой исполнитель.

Автоматическая синхронизация бригады из отчёта

С апреля 2026 года работники добавляются в наряд автоматически — по факту работ из отчёта, а не по плану. Логика:

  1. Бригадир заполняет отчёт: указывает работников и часы в survey_data.routine-care-workers и survey_data.topiary-cutting-workers.
  2. Подаёт на проверку (Report.status → submitted).
  3. Сотрудник утверждает (Report.status → approved).
  4. В этот момент срабатывает сервис sync_visit_members_from_survey(report) в modules/service_visit/services.py.

Сервис идемпотентен:

def sync_visit_members_from_survey(report: Report) -> None:
    desired = set()
    for key in ("routine-care-workers", "topiary-cutting-workers"):
        for entry in report.survey_data.get(key, []):
            if wid := entry.get("worker_id"):
                desired.add(wid)

    with transaction.atomic():
        existing = set(
            ServiceVisitMember.objects
            .filter(service_visit=report.service_visit, role="worker")
            .values_list("user_id", flat=True)
        )

        # удалить лишних
        ServiceVisitMember.objects.filter(
            service_visit=report.service_visit,
            user_id__in=(existing - desired),
            role="worker",
        ).delete()

        # добавить новых
        ServiceVisitMember.objects.bulk_create([
            ServiceVisitMember(
                service_visit=report.service_visit,
                user_id=wid,
                role="worker",
            )
            for wid in (desired - existing)
        ])

Гарантии:

  • Атомарность (transaction.atomic) — нет «полу-обновлённого» состояния.
  • Только role=worker — лидер и бригадир не затрагиваются.
  • No-op при повторе — повторное утверждение с тем же survey_data ничего не меняет.
  • Корректная коррекция — если бригадир переподал отчёт с другим составом, при повторном approved старые удалятся, новые добавятся.

Списки нарядов и фильтрация

Доступ:

КтоЧто видит
admin, staff (is_staff=True)Все наряды
brigadierСвои наряды (где он brigadier_id) или где он участник (members.user_id)
workerТолько наряды, где он members.user_id
clientНе использует этот эндпоинт; видит только отчёты на публичной странице

Фильтры (ServiceVisitFilter):

  • status — точное совпадение
  • object — по ClientObject.id
  • brigadier — по User.id
  • planned_from, planned_to — диапазон планирования
  • active — алиас для status IN (planned, started)

Поиск: display_team_name, object__name, object__address, brigadier__first_name|last_name|email.

Сортировка: planned_for, created_at, status, started_at, ended_at.

Эндпоинты

МетодПутьНазначение
GET/api/service-visits/Список нарядов
POST/api/service-visits/Создать наряд
GET/api/service-visits/{id}/Детали
PATCH/api/service-visits/{id}/Изменить (objet, дата, шаблон, бригадир, команда)
DELETE/api/service-visits/{id}/Удалить (только при особом разрешении)
POST/api/service-visits/{id}/started/planned → started + WORK_STARTED
POST/api/service-visits/{id}/completed/started → completed + WORK_COMPLETED
POST/api/service-visits/{id}/cancelled/→ cancelled
GET POST/api/service-visits/{id}/members/Список / добавление участника
DELETE/api/service-visits/{id}/members/{member_id}/Удалить участника
GET/api/service-visit-members/Список членств (фильтрация по visit, user, role)
На странице