Задания по расписанию (scheduled jobs) предназначены для автоматического выполнения задач в определённое время или с заданной периодичностью.
Права доступа
Задания по расписанию доступны в области Настройки, если пользователю назначен набор прав для роли Администратор
с активными гранулами «Задания по расписанию».
Примечание
Минимальный период выполнения задания по расписанию — 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», где в команду проекта добавлены все пользователи (через добавление подразделений) выполняется задание, в котором:
Сравниваются фактические часы (Actual) и часы по расписанию (Duration).
Если Actual >= Duration --> ничего не предпринимается.
Если Actual < Duration выполняются следующие операции:
Меняется статус таймшита на «Согласовано».
Примечание
Все активные задания по расписанию начинают работать сразу после их создания.
Внимание
Если задание по расписанию было удалено в момент выполненя, то выполнение задания будет закончено, а затем удалено. При этом на странице записей заданий по расписанию, запись задания будет удалена сразу.
Перейти на русскую версию?