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

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

Один из распространённых сценариев в процессе продаж — переход сделки из одного типа в другой. Так, предпродажные активности в случае успеха становятся полноценным тёплым лидом. Для сохранения и передачи всей информации, полученной при первом контакте с клиентом, при переходе от одного типа продаж к другому можно настроить специальный обработчик. Его принцип работы:

  1. Сделка одного типа ЖЦ сделки попадает в системное состояние Выиграно.
  2. Автоматически в ЖЦ другого типа сделки, в начальном состоянии создаётся новая сделка, которая подтягивает всю информацию с той, что улетела в Выиграно.
public class CustomHooks : EntityTypeCustomHooks<Deal>
{
    private const string WarmupDealTypeCode = "WARM";
    private const string SalesDealTypeCode = "SALES";
    private const string SalesDealStateCode = "NEW";

    public override async Task AfterSetState(
        CustomHooksContext context,
        Deal entity,
        Guid oldStateId,
        Guid newStateId
    )
    {
        // NOTE: Handle only warmups
        var directoryEntryService = context.GetEntityService<DirectoryEntry>();
        var dealType = await directoryEntryService.GetAsync(entity.TypeId, checkRights: false);
        if (dealType.Code != WarmupDealTypeCode)
            return;

        // NOTE: If warmup was won - proceed
        if (newStateId != Deal.WonState.Id)
            return;

        await CreateSalesDeal(context, entity);
    }

    private async Task CreateSalesDeal(CustomHooksContext context, Deal from)
    {
        var directoryEntryService = context.GetEntityService<DirectoryEntry>();
        var salesDealType = await directoryEntryService
            .Get(de => de.Code == SalesDealTypeCode)
            .FirstAsync();

        var lifecycleService = context.GetEntityService<Lifecycle>();
        var dealLifecycle = await lifecycleService
            .Get(lc => lc.EntityType == nameof(Deal))
            .Include(lc => lc.States)
            .FirstAsync();
        var salesDealState = dealLifecycle.States.First(s => s.Code == SalesDealStateCode);

        // NOTE: Create "Sales" deal
        var salesDeal = new Deal
        {
            TypeId = salesDealType.Id,
            StateId = salesDealState.Id,

            ProjectId = from.ProjectId,
            ManagerId = from.ManagerId,
            OrganizationId = from.OrganizationId,
            SourceId = from.SourceId,

            Name = from.Name,
            Code = from.Code,
            Description = from.Description,

            Amount = from.Amount,
            Probability = from.Probability,
            CheckList = from.CheckList,
        };
        salesDeal.CopyCustomFields(from);
        var dealService = context.GetEntityService<Deal>();
        await dealService.InsertAsync(salesDeal);

        // NOTE: Copy DealContacts
        var dealContactService = context.GetEntityService<DealContact>();
        var dealContacts = await dealContactService.Get(dc => dc.DealId == from.Id).ToListAsync();
        foreach (var dealContact in dealContacts)
        {
            var salesDealContact = new DealContact
            {
                DealId = salesDeal.Id,
                ContactId = dealContact.ContactId,
                Description = dealContact.Description,
            };
            await dealContactService.InsertAsync(salesDealContact);
        }

        // NOTE: Copy interactions
        var interactionService = context.GetEntityService<Interaction>();
        var dealInteractions = await interactionService
            .Get(dc => dc.DealId == from.Id)
            .ToListAsync();
        foreach (var dealInteraction in dealInteractions)
        {
            var salesDealInteraction = new Interaction
            {
                DealId = salesDeal.Id,
                Description = dealInteraction.Description,
                TypeId = dealInteraction.TypeId,
                Date = dealInteraction.Date,
                Email = dealInteraction.Email,
                IsPlanned = dealInteraction.IsPlanned,
                IsSent = dealInteraction.IsSent,
                OrganizationId = dealInteraction.OrganizationId,
                PerformerId = dealInteraction.PerformerId,
                SendAutomatically = dealInteraction.SendAutomatically,
                Subject = dealInteraction.Subject,
            };
            await interactionService.InsertAsync(salesDealInteraction);

            // NOTE: Copy interaction contacts
            var contactInteractionService = context.GetEntityService<ContactInteraction>();
            var contactInteractions = await contactInteractionService
                .Get(ci => ci.InteractionId == dealInteraction.Id)
                .ToListAsync();
            foreach (var contactInteraction in contactInteractions)
            {
                var salesContactInteraction = new ContactInteraction
                {
                    InteractionId = salesDealInteraction.Id,
                    ContactId = contactInteraction.ContactId,
                };
                await contactInteractionService.InsertAsync(salesContactInteraction);
            }
        }
    }
}
Ничего не найдено

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