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

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

Обновлено: 15.07.2025

Механизм обработчиков — часть системы кастомизации Timetta. Подробнее о принципах работы и структуре — Обработчик событий сущности.

Ниже рассмотрены примеры кода, которые показывают, как можно настроить логику работы данных с помощью обработчиков.

Важно

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

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

Один из популярных сценариев работы — использование читаемых и структурированных номеров проектов, сделок и других сущностей. Например:

  • IT-2025-FIN-007
  • PP-2024-TRANS-105
  • НИОКР-2024-ХИМ-028
  • ГОСТ-2024-ЭКО-112

В Timetta можно автоматически генерировать такие номера с помощью хуков сущностей.

Ниже приведён пример обработчика, который сформирует для сущности Project номер в формате PR-[BiilingType]-[Index]. Для хранения порядкового номера необходимо создать дополнительное поле с типом «целое число» (например, IntegerValue1). Это поле будет использоваться в качестве индекса в итоговом коде.

public class CustomHooks : EntityTypeCustomHooks<Project>
{
    public override async Task AfterUpsert(
        CustomHooksContext context,
        Project contextEntity,
        Project detachedEntity
    )
    {
        // Обновляем если Код пустой.
        if (string.IsNullOrWhiteSpace(contextEntity.Code))
        {
            var billingTypeService = context.GetEntityService<ProjectBillingType>();
            var billingType = await billingTypeService
                .Get(bt => bt.Id == contextEntity.BillingTypeId)
                .FirstOrDefaultAsync();

            var projectService = context.GetEntityService<Project>();
            var maxRowNumber = await projectService
                .Get(project => true, checkRights: false)
                .MaxAsync(p => p.IntegerValue1);

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

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

            // Шаблон: PROJECT-[BillingType]-[Index]
            contextEntity.Code = $"PROJECT-{billingTypeCode}-{rowNumber}";
        }
    }
}

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

  • Чтобы формировать более сложные шаблоны, можно использовать несколько полей (IntegerValue1, StringValue1 и т. д.).
  • Реализовать автогенерацию номера можно и для других сущностей, например для Program и Invoice.

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

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

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

Соотношение Код группы -> Имя продукта задаётся в обработчике списком в виде константы.

public class CustomHooks : EntityTypeCustomHooks<UserGroup>
{
   // Настройка Группа (наименование) - какие лицензии назначать.
   // LicenseProduct.Projects, LicenseProduct.TimeTracking, LicenseProduct.Expenses,
   // LicenseProduct.Resources, LicenseProduct.Finance, LicenseProduct.Billing,
   // Clients = 1.  
    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 groupLicense = await GetGroupLicense(context, contextEntity);
        if (groupLicense == null)
            return;

        var userProductService = context.GetEntityService<UserProduct>();
        var alreadyHasLicense = await userProductService
            .Get(up => up.UserId == contextEntity.UserId)
            .Where(up => up.Product == groupLicense)
            .AnyAsync();
        if (alreadyHasLicense)
            return;

        var limitReached = await IsLimitReached(context, contextEntity, groupLicense);
        if (limitReached)
            return;

        var userProduct = new UserProduct { UserId = contextEntity.UserId, Product = groupLicense };
        await userProductService.InsertAsync(userProduct);
    }

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

        var userProductService = context.GetEntityService<UserProduct>();
        var userProduct = await userProductService
            .Get(up => up.Product == groupLicense)
            .FirstOrDefaultAsync(up => up.UserId == contextEntity.UserId);
        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 groupLicense);
        return groupLicense;
    }

    private async Task<bool> IsLimitReached(
        CustomHooksContext context,
        UserGroup userGroup,
        LicenseProduct license
    )
    {
        var licenseLimits = await context.GetLicenseProductLimitsAsync();
        if (!licenseLimits.TryGetValue(license, out var licenseLimit))
            return false;

        var userGroupService = context.GetEntityService<UserGroup>();
        var groupUserIds = await userGroupService
            .Get(ug => ug.GroupId == userGroup.GroupId)
            .Select(ug => ug.UserId)
            .ToListAsync();

        var userProductService = context.GetEntityService<UserProduct>();
		await userProductService.Get(up => up.Product == license).LockAsync();
        var licenseCount = await userProductService
            .Get(up => up.Product == license)
            .CountAsync();
        return licenseCount >= licenseLimit;
    }
}

Содержание

Присвоение кода проекту Автоматическое назначение лицензии
Ничего не найдено

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