Наряды и бригады
Модель ServiceVisit, статусная машина, состав бригад и автоматическая синхронизация с отчётом.
Наряды и бригады
ServiceVisit
Префикс: sv_
Таблица: service_visit
Файл: garden/modules/service_visit/models.py
Один выезд бригады на объект клиента — центральная сущность платформы.
| Поле | Тип | Описание |
|---|---|---|
id | KSUID, sv_* | |
object | FK → ClientObject | На какой территории работаем (PROTECT) |
brigadier | FK → User | Назначенный ответственный за выезд. Ограничено role=brigadier. Назначает сотрудник или администратор при создании наряда. |
report_template | FK → ReportSurveyTemplate | Какой шаблон отчёта использовать |
status | ServiceVisitStatus | См. статусную машину ниже |
planned_for | DateTimeField(nullable) | Когда запланировано |
started_at | DateTimeField(nullable) | Авто-выставляется при → started |
ended_at | DateTimeField(nullable) | Авто-выставляется при → completed или → cancelled |
display_team_name | CharField(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). |
Цикл рецензирования живёт на стороне отчёта (Report.status). Наряд может быть completed, даже если связанный отчёт ещё в submitted или rejected — эти два жизненных цикла независимы.
Свойство is_active:
@property
def is_active(self):
return self.status in {ServiceVisitStatus.PLANNED, ServiceVisitStatus.STARTED}Используется в фильтрах списков нарядов.
ServiceVisitMember
Префикс: svm_
Таблица: service_visit_member
Участники бригады. Привязка User → ServiceVisit с ролью.
| Поле | Тип | Описание |
|---|---|---|
id | KSUID, svm_* | |
service_visit | FK → ServiceVisit | Какой наряд |
user | FK → User | Кто. Ограничено role IN (brigadier, worker) |
role | ServiceVisitMemberRole | lead или worker (default: worker) |
Constraint uq_service_visit_user_once: один пользователь не может быть в одном наряде дважды.
class ServiceVisitMemberRole(models.TextChoices):
LEAD = "lead", _("Lead")
WORKER = "worker", _("Worker")lead — старший на объекте (обычно бригадир, но может быть и другой опытный работник).
worker — рядовой исполнитель.
Автоматическая синхронизация бригады из отчёта
С апреля 2026 года работники добавляются в наряд автоматически — по факту работ из отчёта, а не по плану. Логика:
- Бригадир заполняет отчёт: указывает работников и часы в
survey_data.routine-care-workersиsurvey_data.topiary-cutting-workers. - Подаёт на проверку (
Report.status → submitted). - Сотрудник утверждает (
Report.status → approved). - В этот момент срабатывает сервис
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.idbrigadier— поUser.idplanned_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) |