Важно
Примеры предназначены для разработчиков и администраторов. Предполагают знание языка 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);Лицензии можно назначать автоматически в зависимости от того, к какой группе принадлежит пользователь.
Например:
| Группа | Назначаемая лицензия |
|---|---|
| Проектная команда | 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}");
}
}
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}");
}
}
Для вызова внешних 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("Начало обработки проекта");
В обработчиках должна быть только прикладная бизнес-логика. Избегайте:
Обработчик должен быть коротким и понятным.
Перейти на русскую версию?