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

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

Обновлено: 20.08.2025

Назначение

Важно

Компонента позволяет кастомизировать логику работы с данными:

  • Предполагает понимание языка C# и структуры данных Timetta
  • Предназначена для прикладных разработчиков
  • Находится в стадии активного развития

Если вам нужно переопределение логики, рекомендуем обратиться в поддержку support@timetta.com для платной консультации.

Задания по расписанию (также называемые Запланированные задания или 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
    • Статус таймшита меняется на «Согласовано»
  • Если таймшит в статусе «На согласовании», статус меняется на «Согласовано»

Примечание

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

Внимание

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

Логирование заданий по расписанию

В карточке задания доступна вкладка Лог, на которой отображается история выполнения выбранного задания.

  • В списке выводятся записи связанные с текущим заданием.
  • Каждая запись обычно содержит:
    • дату и время события;
    • уровень (Info или Error);
    • сообщение лога;
    • при наличии — детали ошибки (стек вызовов).
  • Лог позволяет быстро понять, когда и как выполнялось задание, и увидеть сообщения, записанные как платформой, так и вашим кодом через context.Log(...).

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

Что логируется автоматически

  • Начало выполнения задания — сообщение вида ScheduledJobInvoker started.
  • Успешное завершение задания — сообщение вида ScheduledJobInvoker completed.
  • Необработанные исключения — с уровнем Error и сохранением стека вызовов.

Запись в лог из кода обработчика

Для упрощения отладки и анализа работы запланированных заданий в обработчике доступен метод логирования Log. Он позволяет записывать события выполнения задания в сущность ScheduledJobLog с привязкой к конкретному заданию.

Примеры использования:

await context.Log(
    "Проект c кодом 'DOWNTIME' не найден, недостающие часы не будут проставлены.",
    DynamicCodeLogLevel.Error
);

Рекомендуется использовать context.Log для осмысленных бизнес-событий (количество обработанных записей, принятые решения и т. п.), не дублируя уже существующие технические логи.

Важно

  • Всегда используйте await при вызове context.Log(...), так как метод асинхронный.
  • Подбирайте уровень DynamicCodeLogLevel по смыслу: Info или Error
  • Логируйте только действительно важные шаги и решения бизнес-логики, чтобы логи оставались читаемыми и полезными.

Содержание

Назначение Создание задания по расписанию Настройка задания по расписанию Пример кода обработчика для задания по расписанию Логирование заданий по расписанию Что логируется автоматически Запись в лог из кода обработчика
Ничего не найдено

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