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

Запланированное задание

Обновлено: 22.05.2025

Назначение

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

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

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

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

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

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

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

  • Наименование — наименование задания по расписанию. Должно быть уникальным в рамках одной сущности
  • Cron-выражение — это значение, которое определяет расписание выполнения задачи в формате, понятном для системных планировщиков (примеры cron-выражений)
  • Описание — свободное описание или заметка для задания по расписанию
  • Код обработчика — это программный компонент, написанный на языке программирования C#, который автоматически выполняет задачи в заданное время или с определённой периодичностью

Примечание

Минимальный период выполнения задания по расписанию — 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.
    • Меняется статус таймшита на «Согласовано».

  • Если таймшит в статусе «На согласовании» меняется статус на «Согласовано».

Примечание

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

Внимание

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

Содержание

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

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