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

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

Обновлено: 12.08.2025

Назначение

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

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

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

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

Чтобы создать запланированное задание, следует:

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

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

Карточка задания по расписанию:
Задания по расписанию

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

Примечание

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

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

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

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.
    • Меняется статус таймшита на «Согласовано».
  • Если таймшит в статусе «На согласовании», статус меняется на «Согласовано».

Примечание

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

Внимание

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

Содержание

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

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