Общие сведения
Надёжность и безопасность
Начало работы
Обзор системы
Проекты
Компоненты
Инструкции
Часто задаваемые вопросы
Ресурсы
Таймшиты
Финансы
Клиенты
Биллинг
Затраты
Отчёты и аналитика
FAQ
Типы отчётов
Использование отчётов
Группировка и суммирование данных источника
Группировка данных в отчёте
Типы виджетов
Общие отчёты и шаблоны
Настройка отчёта
Экспорт отчётов
Пользовательские настройки отчёта
Вычисляемые поля
Выражения вычисляемых полей
Особенности работы с колонками «Расчёт времени за период»
Использование панелей мониторинга
Публикация панелей
Фильтры источников данных
Настройка и администрирование
Типовой порядок настройки системы
Жизненные циклы и воркфлоу
On-premises
API
История изменений
Термины и определения

Задание по расписанию

Обновлено: 20.08.2025

Назначение

Задания по расписанию (также называемые Запланированные задания или Scheduled jobs) используются для автоматического выполнения задач в заданное время или с определённой периодичностью.

Права доступа

Доступ к заданиям по расписанию имеют пользователи с ролью Администратор, у которых активирована гранула прав «Задания по расписанию».

Создание задания по расписанию

Чтобы создать новое задание:

  1. Перейдите в компонент Задания по расписанию.
  2. На панели действий нажмите кнопку Создать задание.
  3. Заполните поле Наименование.
  4. Нажмите кнопку Создать.

Настройка задания по расписанию

После создания задания откроется его карточка для настройки:
Задания по расписанию

В карточке задания доступны следующие параметры:

  1. Наименование — уникальное название задания в рамках системы. Обязательное поле.
  2. Cron-выражение — значение, определяющее расписание выполнения задачи в формате, понятном для системных планировщиков. Примеры cron-выражений.
  3. Описание — дополнительные заметки или описание задания.
  4. Код обработчика — программный код на языке C#, который автоматически выполняется согласно расписанию.
  5. Запустить — кнопка для немедленного ручного выполнения задачи. Позволяет проверить работу обработчика без ожидания автоматического запуска по расписанию.

Примечание

Минимальный интервал между выполнениями задания составляет 30 минут.

Пример кода обработчика для задания по расписанию

По умолчанию в коде обработчика содержится шаблон класса на C# для обработки запланированных задач:

public class ScheduledJobHandler : IScheduledJobHandler {

    public async Task Execute(ScheduledJobContext context)
    {

        await Task.CompletedTask;
    }
}

Пример кода обработчика задания по расписанию для таймшитов:

public class ScheduledJobHandler : IScheduledJobHandler
{
    private static readonly Guid SubmittedId = new("4dc757b3-0f7a-4c5e-8111-e669e14172b0");

    public async Task Execute(ScheduledJobContext context)
    {
        var timesheetService = context.GetEntityService<TimeSheet>();
        var timesheets = await timesheetService
            .Get(ts => ts.DateFrom.Month == DateTime.Now.Month)
            .Include(ts => ts.TimeAllocations)
            .ToListAsync();
        foreach (var timesheet in timesheets)
            await HandleTimesheet(context, timesheet);
    }

    private async Task HandleTimesheet(ScheduledJobContext context, TimeSheet timesheet)
    {
        var handledStateIds = new Guid[] { TimeSheet.DraftId, SubmittedId };
        if (!handledStateIds.Contains(timesheet.StateId))
            return;

        if (timesheet.StateId == TimeSheet.DraftId)
            await HandleDraft(context, timesheet);

        timesheet.StateId = TimeSheet.ApprovedId;
        return;
    }

    private async Task HandleDraft(ScheduledJobContext context, TimeSheet timesheet)
    {
        var userScheduleService = context.GetEntityService<UserSchedule>();
        var userSchedule = await userScheduleService
            .Get(us => us.UserId == timesheet.UserId)
            .Include(us => us.Schedule)
            .ThenInclude(s => s.PatternDays)
            .GetEffectiveOnDateAsync(timesheet.DateFrom);
        var schedule = userSchedule.Schedule;

        var (start, end) = (timesheet.DateFrom, timesheet.DateTo);
        var scheduleHours = Enumerable
            .Range(0, 1 + end.Subtract(start).Days)
            .Select(offset => start.AddDays(offset))
            .Sum(date => DayLengthFrom(schedule, date));
        var actualHours = timesheet.TimeAllocations.Sum(ta => ta.Hours);
        if (actualHours >= scheduleHours)
            return;

        var projectService = context.GetEntityService<Project>();
        var downtimeProject = await projectService
            .Get(p => p.Code == "DOWNTIME")
            .Include(p => p.ProjectTasks)
            .FirstOrDefaultAsync();
        if (downtimeProject is null)
            return;

        var timesheetLineService = context.GetEntityService<TimeSheetLine>();
        var downtimeLine = await timesheetLineService
            .Get(tsl => tsl.TimeSheetId == timesheet.Id)
            .Include(tsl => tsl.TimeAllocations)
            .FirstOrDefaultAsync(tsl => tsl.ProjectId == downtimeProject.Id);
        if (downtimeLine is null)
        {
            var mainTask = downtimeProject.ProjectTasks
                .First(pt => !pt.LeadTaskId.HasValue);
            downtimeLine = new TimeSheetLine
            {
                TimeSheetId = timesheet.Id,
                ProjectId = downtimeProject.Id,
                ProjectTaskId = mainTask.Id
            };
            downtimeLine = await timesheetLineService.InsertAsync(downtimeLine);
        }

        var timeAllocationService = context.GetEntityService<TimeAllocation>();
        var hoursDiff = scheduleHours - actualHours;
        var downtimeAllocation = new TimeAllocation
        {
            TimeSheetId = timesheet.Id,
            TimeSheetLineId = downtimeLine.Id,
            UserId = timesheet.UserId,
            Date = timesheet.DateTo,
            Hours = hoursDiff
        };
        await timeAllocationService.InsertAsync(downtimeAllocation);
    }

    private int DayNumberFrom(Schedule schedule, DateTime date)
    {
        var isWeek = schedule.PatternDays.Count == 7;
        if (isWeek)
            return DayOfWeekNumberIso(date);

        var isSingleDay = schedule.PatternDays.Count == 1;
        if (isSingleDay)
            return 1;

        var firstDayInPast = schedule.FirstDay <= date;
        var daysDiff = Math.Abs(date.Subtract(schedule.FirstDay!.Value).Days);
        var daysCount = schedule.PatternDays.Count;
        if (firstDayInPast)
        {
            var futureDayNumber = (daysDiff + 1) % daysCount;
            if (futureDayNumber == decimal.Zero)
                futureDayNumber = daysCount;
            return futureDayNumber;
        }

        var pastDayNumber = daysDiff % daysCount;
        if (pastDayNumber == decimal.Zero)
            pastDayNumber = daysCount;
        int InvertDayNumber(int number) => daysCount + 1 - number;
        return InvertDayNumber(pastDayNumber);
    }

    private decimal DayLengthFrom(Schedule schedule, DateTime date)
    {
        var exceptionDays = schedule.ScheduleException?.ExceptionDays;
        var exceptionDay = exceptionDays?.FirstOrDefault(ed => ed.Date == date);
        if (exceptionDay != null)
            return exceptionDay.DayLength;

        var dayNumber = DayNumberFrom(schedule, date);
        var orderedPatternDays = schedule.PatternDays.OrderBy(pd => pd.DayNumber);
        var patternDay = orderedPatternDays.ElementAtOrDefault(dayNumber - 1);
        return patternDay?.DayLength ?? decimal.Zero;
    }

    private int DayOfWeekNumberIso(DateTime date) =>
        date.DayOfWeek switch
        {
            DayOfWeek.Monday => 1,
            DayOfWeek.Tuesday => 2,
            DayOfWeek.Wednesday => 3,
            DayOfWeek.Thursday => 4,
            DayOfWeek.Friday => 5,
            DayOfWeek.Saturday => 6,
            DayOfWeek.Sunday => 7,
        };
}

Данный пример работает с проектом в статусе «В работе», имеющим код "DOWNTIME" и название «Простой». В команду проекта добавлены все пользователи (через подразделение).

Алгоритм работы задания:

  • Выбираются все таймшиты, у которых dateFrom попадает в текущий месяц
  • Для каждого таймшита проверяется его статус
  • Если таймшит в статусе «Черновик»:
    • Сравниваются фактические часы (Actual) и часы по расписанию (Duration)
    • Если Actual >= Duration, изменения не требуются
    • Если Actual < Duration:
      • Проверяется наличие строки по проекту "DOWNTIME"
      • Если строки нет, она добавляется
      • Добавляются недостающие часы на проект, чтобы Actual = Duration
    • Статус таймшита меняется на «Согласовано»
  • Если таймшит в статусе «На согласовании», статус меняется на «Согласовано»

Примечание

Все активные задания по расписанию начинают выполняться сразу после создания.

Внимание

Если задание по расписанию было удалено в момент его выполнения, то выполнение сначала будет завершено, и только затем задание удалится. При этом на странице заданий соответствующая запись исчезнет сразу.

Содержание

Назначение Создание задания по расписанию Настройка задания по расписанию Пример кода обработчика для задания по расписанию
Ничего не найдено

Перейти на русскую версию?