Механизм обработчиков — часть системы кастомизации 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;
}
}
Перейти на русскую версию?