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

Примеры кода обработчика

Обновлено: 15.07.2025

Важно

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

Присвоение кода проекту

Часто используется присвоение структурированных кодов проектам, сделкам и другим сущностям. Например:

  • IT-2025-FIN-007;
  • PR-DEV-00025;
  • CONSULT-00012.

Такие коды можно создавать автоматически с помощью обработчика.

В примере ниже код проекта формируется по шаблону:

PROJECT-[BillingType]-[Index]

где:

  • BillingType — код типа биллинга проекта;
  • Index — порядковый номер.

Для хранения индекса используется дополнительное числовое поле, например IntegerValue1.

public class CustomHooks : EntityTypeCustomHooks<Project>
{
    public override async Task AfterUpsert(
        CustomHooksContext context,
        Project contextEntity,
        Project detachedEntity)
    {
        if (!string.IsNullOrWhiteSpace(contextEntity.Code))
            return;

        var billingTypeService = context.GetEntityService<ProjectBillingType>();
        var billingType = await billingTypeService
            .Get(bt => bt.Id == contextEntity.BillingTypeId)
            .FirstOrDefaultAsync();

        var projectService = context.GetEntityService<Project>();

        var maxIndex = await projectService
            .Get(p => true, checkRights: false)
            .MaxAsync(p => p.IntegerValue1);

        if (context.OperationType == EntityServiceOperation.Insert)
            contextEntity.IntegerValue1 = (maxIndex ?? 0) + 1;

        var billingTypeCode = billingType?.Code?.ToUpper() ?? "NONE";
        var index = contextEntity.IntegerValue1?.ToString("D5") ?? "00000";

        contextEntity.Code = $"PROJECT-{billingTypeCode}-{index}";
    }
}

Рекомендации:

  • для сложных шаблонов используйте дополнительные поля (IntegerValue, StringValue);
  • аналогичную логику можно применять к другим сущностям, например Program, Invoice и так далее.

Автоматическое назначение лицензии

Лицензии можно назначать автоматически в зависимости от того, к какой группе принадлежит пользователь.

Например:

Группа Назначаемая лицензия
Проектная команда Projects
Финансы Finance

Ниже приведён обработчик, который назначает лицензию при добавлении пользователя в группу и удаляет её при исключении из группы.

public class CustomHooks : EntityTypeCustomHooks<UserGroup>
{
    private static readonly Dictionary<string, LicenseProduct> GroupToLicense = new()
    {
        { "Группа 1", LicenseProduct.Projects }
    };

    public override async Task AfterUpsert(
        CustomHooksContext context,
        UserGroup contextEntity,
        UserGroup detachedEntity)
    {
        if (context.OperationType != EntityServiceOperation.Insert)
            return;

        var license = await GetGroupLicense(context, contextEntity);
        if (license == null)
            return;

        var userProductService = context.GetEntityService<UserProduct>();

        var alreadyExists = await userProductService
            .Get(x => x.UserId == contextEntity.UserId && x.Product == license)
            .AnyAsync();

        if (alreadyExists)
            return;

        var userProduct = new UserProduct
        {
            UserId = contextEntity.UserId,
            Product = license
        };

        await userProductService.InsertAsync(userProduct);
    }

    public override async Task AfterDelete(CustomHooksContext context, UserGroup contextEntity)
    {
        var license = await GetGroupLicense(context, contextEntity);
        if (license == null)
            return;

        var userProductService = context.GetEntityService<UserProduct>();

        var userProduct = await userProductService
            .Get(x => x.UserId == contextEntity.UserId && x.Product == license)
            .FirstOrDefaultAsync();

        if (userProduct != null)
            await userProductService.DeleteAsync(userProduct.Id);
    }

    private async Task<LicenseProduct> GetGroupLicense(
        CustomHooksContext context,
        UserGroup userGroup)
    {
        var groupService = context.GetEntityService<Group>();
        var group = await groupService.Get(g => g.Id == userGroup.GroupId).FirstAsync();

        GroupToLicense.TryGetValue(group.Name, out var license);

        return license;
    }
}

Создание сделки нового типа

Иногда нужно автоматически создавать новую сделку после успешного завершения другой. Например, предпродажная активность может превратиться в полноценную продажу.

public class CustomHooks : EntityTypeCustomHooks<Deal>
{
    public override async Task AfterSetState(
        CustomHooksContext context,
        Deal entity,
        Guid oldStateId,
        Guid newStateId)
    {
        if (newStateId != Deal.WonState.Id)
            return;

        var dealService = context.GetEntityService<Deal>();

        var newDeal = new Deal
        {
            Name = entity.Name,
            OrganizationId = entity.OrganizationId,
            ManagerId = entity.ManagerId,
            Amount = entity.Amount
        };

        await dealService.InsertAsync(newDeal);

        await context.Log($"Создана новая сделка на основе {entity.Id}");
    }
}

Пример CRUD-хуков

public class CustomHooks : EntityTypeCustomHooks<Activity>
{
    public override async Task BeforeUpsert(
        CustomHooksContext context,
        Activity detachedEntity)
    {
        if (string.IsNullOrWhiteSpace(detachedEntity.Name))
            await context.Log("Имя активности не задано", DynamicCodeLogLevel.Error);
    }

    public override async Task AfterUpsert(
        CustomHooksContext context,
        Activity contextEntity,
        Activity detachedEntity)
    {
        await context.Log($"Активность сохранена: {contextEntity.Id}");
    }

    public override async Task AfterDelete(
        CustomHooksContext context,
        Activity contextEntity)
    {
        await context.Log($"Активность удалена: {contextEntity.Id}");
    }
}

HTTP-интеграции

Для вызова внешних API используйте безопасный HTTP-клиент, который предоставляет платформа.

public override async Task AfterUpsert(
    CustomHooksContext context,
    Activity contextEntity,
    Activity detachedEntity)
{
    var http = context.GetHttpClient();

    var response = await http.GetStringAsync(
        "https://api.example.com/activities"
    );

    await context.Log("Ответ получен от внешнего API");
}

Типичные ошибки при написании обработчиков

Изменение сущности в неправильном хуке

Изменения данных лучше делать в BeforeUpsert, а не в AfterUpsert.

Неправильно:

public override async Task AfterUpsert(
    CustomHooksContext context,
    Project contextEntity,
    Project detachedEntity)
{
    contextEntity.Code = "PR-001";
}

Правильно:

public override async Task BeforeUpsert(
    CustomHooksContext context,
    Project detachedEntity)
{
    detachedEntity.Code = "PR-001";
}

Отсутствие проверки типа операции

Хук Upsert срабатывает и при создании, и при обновлении сущности. Добавьте проверку типа операции:

if (context.OperationType != EntityServiceOperation.Insert)
    return;

Загрузка лишних данных

Неправильно:

var projects = await projectService.Get(p => true).ToListAsync();
var count = projects.Count;

Правильно:

var count = await projectService
    .Get(p => true)
    .CountAsync();

Отсутствие логирования

Если обработчик выполняет сложную логику, добавляйте логирование:

await context.Log("Начало обработки проекта");

Слишком сложная логика

В обработчиках должна быть только прикладная бизнес-логика. Избегайте:

  • сложных вычислений;
  • длинных методов;
  • длительных операций.

Обработчик должен быть коротким и понятным.

Содержание

Присвоение кода проекту Автоматическое назначение лицензии Создание сделки нового типа Пример CRUD-хуков HTTP-интеграции Типичные ошибки при написании обработчиков Изменение сущности в неправильном хуке Отсутствие проверки типа операции Загрузка лишних данных Отсутствие логирования Слишком сложная логика
Ничего не найдено

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