Общие сведения
Надёжность и безопасность
Покупка лицензии
Начало работы
Роли в системе
Проекты
Концепции
Компоненты
Инструкции
Задачи
Финансы
Ресурсы
Таймшиты
Клиенты
Вики
Затраты
Отчёты и аналитика
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-интеграции Типичные ошибки при написании обработчиков Изменение сущности в неправильном хуке Отсутствие проверки типа операции Загрузка лишних данных Отсутствие логирования Слишком сложная логика
Ничего не найдено

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