Как получить список дел из сделок
Scope:
crm, user_briefКто может выполнять метод: пользователь с доступом на чтение сделок в CRM
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
Список дел позволяет отслеживать текущие задачи и звонки по сделкам, сроки выполнения дел и ответственных. Чтобы сформировать таблицу дел, последовательно выполним методы:
-
user.current — найдем
IDтекущего пользователя, -
crm.item.list — получим
IDвсех сделок, в которых сотрудник является ответственным, -
crm.activity.list — сформируем список дел по сделкам,
-
user.get — получим информацию об ответственных за дела.
1. Получим ID текущего пользователя
Чтобы получить идентификатор текущего пользователя, используем метод user.current.
Как использовать примеры в документации
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
const result = await $b24.actions.v2.call.make({
method: 'user.current',
params: {}
});
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');
$currentUser = $sb->getUserScope()->user()->current()->user();
from b24pysdk import BitrixWebhook, Client
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
result = client.user.current().response.result
res, err := core.Call(ctx, "user.current", nil, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.current: %w", err)
}
// Идентификатор приходит СТРОКОЙ ("29"): b24.ID разбирает и число, и строку
// с числом, обычный int здесь падает.
var me struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &me); err != nil {
return fmt.Errorf("разбор текущего пользователя: %w", err)
}
В результате получим идентификатор пользователя "ID": "29".
{
"result": {
"ID": "29",
"ACTIVE": true,
"NAME": "Иван",
"LAST_NAME": "Иванов",
...
}
}
2. Получим список ID сделок сотрудника
Чтобы получить идентификаторы сделок, закрепленных за сотрудником, вызовем метод crm.item.list. Передаем параметры:
-
entityTypeId— идентификатор типа объекта CRM. Получить идентификаторы можно методом crm.enum.ownertype. Укажем значение —2, то есть сделка. -
select— массив полей, которые нужно выбрать. Укажемselect: ['id','title'], чтобы получить идентификаторы и названия сделок. -
filter— фильтр выборки. Чтобы выбрать сделки поIDответственного сотрудника, укажем идентификатор пользователя, который получили в прошлом запросеassignedById: 29.
Чтобы запрос работал быстрее и возвращал только актуальные данные, добавьте фильтр по стадиям stageId. Например можно выбрать сделки на стадии В работе.
const result = await $b24.actions.v2.call.make({
method: 'crm.item.list',
params: {
entityTypeId: 2,
select: ['id', 'title'],
filter: {
assignedById: 29
}
}
});
$items = $sb->getCRMScope()->item()->list(
2,
[],
['assignedById' => 29],
['id', 'title']
)->getItems();
result = client.crm.item.list(
entity_type_id=2,
select=["id", "title"],
filter={"assignedById": 29},
).response.result
// Списочный метод отдаёт максимум 50 записей за запрос. Pages идёт по
// курсору сам: ОТСУТСТВИЕ next заканчивает список, потому что next: 0 —
// это законное первое смещение, а не признак конца.
pager, err := core.Pages("crm.item.list", b24.Params{
"entityTypeId": entityTypeDeal,
"select": []string{"id", "title"},
"filter": b24.Params{"assignedById": me.ID},
}, b24.WithCallOptions(b24.WithIdempotent()))
if err != nil {
return fmt.Errorf("crm.item.list: %w", err)
}
var deals []struct {
ID int `json:"id"`
Title string `json:"title"`
}
for pager.Next(ctx) {
for _, row := range pager.Rows() {
var d struct {
ID int `json:"id"`
Title string `json:"title"`
}
if err := json.Unmarshal(row, &d); err != nil {
return fmt.Errorf("разбор сделки: %w", err)
}
deals = append(deals, d)
}
if len(deals) >= maxDeals {
break
}
}
// Ошибка всплывает ЗДЕСЬ, после цикла: Next возвращает false и в конце
// списка, и при ошибке.
if err := pager.Err(); err != nil {
return fmt.Errorf("обход сделок: %w", err)
}
В результате получим массив items c идентификаторами сделок вида "id": 5111.
{
"result": {
"items": [
{ "id": 5111, "title": "Сделка №1" },
{ "id": 5199, "title": "Сделка №2" },
{ "id": 5257, "title": "Сделка №3" }
]
},
"total": 3
}
3. Получим список дел по найденным сделкам
Для получения списка дел используем метод crm.activity.list.
Чтобы выбрать дела из нескольких сделок, в фильтре filter используем ключ привязки к элементам CRM BINDINGS. Передадим в нем массив объектов. Каждый объект содержит:
-
OWNER_TYPE_ID— идентификатор типа объекта CRM. Получить идентификаторы можно методом crm.enum.ownertype. Укажем значение —2, то есть сделка. -
OWNER_ID— идентификатор сделки из результата прошлого запроса.
Также отфильтруем только активные дела COMPLETED: 'N'.
Выведем в результате select поля:
-
ID— идентификатор дела, -
OWNER_ID— идентификатор сделки, -
SUBJECT— описание дела, -
DEADLINE— дата и время срока выполнения, -
RESPONSIBLE_ID— идентификатор пользователя, ответственного за дело.
const result = await $b24.actions.v2.call.make({
method: 'crm.activity.list',
params: {
filter: {
BINDINGS: [
{ OWNER_TYPE_ID: 2, OWNER_ID: 5111 },
{ OWNER_TYPE_ID: 2, OWNER_ID: 5199 },
{ OWNER_TYPE_ID: 2, OWNER_ID: 5257 }
],
COMPLETED: 'N'
},
select: ['ID', 'OWNER_ID', 'SUBJECT', 'DEADLINE', 'RESPONSIBLE_ID']
}
});
$activities = $sb->getCRMScope()->activity()->list(
[],
[
'BINDINGS' => [
['OWNER_TYPE_ID' => 2, 'OWNER_ID' => 5111],
['OWNER_TYPE_ID' => 2, 'OWNER_ID' => 5199],
['OWNER_TYPE_ID' => 2, 'OWNER_ID' => 5257]
],
'COMPLETED' => 'N'
],
['ID', 'OWNER_ID', 'SUBJECT', 'DEADLINE', 'RESPONSIBLE_ID'],
0
)->getActivities();
result = client.crm.activity.list(
filter={
"BINDINGS": [
{"OWNER_TYPE_ID": 2, "OWNER_ID": 5111},
{"OWNER_TYPE_ID": 2, "OWNER_ID": 5199},
{"OWNER_TYPE_ID": 2, "OWNER_ID": 5257},
],
"COMPLETED": "N",
},
select=["ID", "OWNER_ID", "SUBJECT", "DEADLINE", "RESPONSIBLE_ID"],
).response.result
// BINDINGS — массив привязок: по объекту на каждую сделку.
bindings := make([]b24.Params, 0, len(deals))
for _, d := range deals {
bindings = append(bindings, b24.Params{"OWNER_TYPE_ID": entityTypeDeal, "OWNER_ID": d.ID})
}
pager, err = core.Pages("crm.activity.list", b24.Params{
"filter": b24.Params{"BINDINGS": bindings, "COMPLETED": "N"},
"select": []string{"ID", "OWNER_ID", "SUBJECT", "DEADLINE", "RESPONSIBLE_ID"},
}, b24.WithCallOptions(b24.WithIdempotent()))
if err != nil {
return fmt.Errorf("crm.activity.list: %w", err)
}
// Поля дел — в ВЕРХНЕМ РЕГИСТРЕ, тогда как crm.item.list отдаёт camelCase.
var activities []activity
for pager.Next(ctx) {
for _, row := range pager.Rows() {
var a activity
if err := json.Unmarshal(row, &a); err != nil {
return fmt.Errorf("разбор дела: %w", err)
}
activities = append(activities, a)
}
}
if err := pager.Err(); err != nil {
return fmt.Errorf("обход дел: %w", err)
}
В результате получим список дел с описанием каждого дела.
{
"result": [
{
"ID": "10120",
"OWNER_ID": "5111",
"SUBJECT": "Позвонить клиенту",
"DEADLINE": "2025-08-21T16:00:00+03:00",
"RESPONSIBLE_ID": "29"
},
{
"ID": "10131",
"OWNER_ID": "5199",
"SUBJECT": "Проверить договор",
"DEADLINE": "2025-08-29T16:00:00+03:00",
"RESPONSIBLE_ID": "47"
},
...
],
"total": 5
}
4. Получим данные пользователей по RESPONSIBLE_ID
Ответственным за дело в сделке может быть любой пользователь, не только ответственный за сделку. Чтобы увидеть в таблице имя и фамилию ответственного за дело, используем метод user.get.
В фильтре filter передадим идентификаторы ответственных ID: [29, 47, ...].
const result = await $b24.actions.v2.call.make({
method: 'user.get',
params: {
filter: {
ID: [29, 47]
}
}
});
$users = $sb->getUserScope()->user()->get(
[],
['ID' => [29, 47]]
)->getUsers();
result = client.user.get(
filter={
"ID": [29, 47],
}
).response.result
res, err := core.Call(ctx, "user.get", b24.Params{
"filter": b24.Params{"ID": ids},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.get: %w", err)
}
var rows []struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &rows); err != nil {
return fmt.Errorf("разбор сотрудников: %w", err)
}
for _, u := range rows {
users[u.ID] = u.Name + " " + u.LastName
}
В результате получим информацию о пользователях.
{
"result": [
{
"ID": "29",
"XML_ID": "23699770",
"ACTIVE": true,
"NAME": "Иван",
"LAST_NAME": "Иванов"
},
{
"ID": "47",
"XML_ID": "63726962",
"ACTIVE": true,
"NAME": "Петр",
"LAST_NAME": "Петров"
},
...
],
"total": 3,
}
Пример кода
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
// Функция для формирования массива привязок к сделкам
// тип объекта CRM OWNER_TYPE_ID — 2, то есть сделка
function buildBindingsFromDealIds(dealIds) {
return dealIds.map((id) => ({ OWNER_TYPE_ID: 2, OWNER_ID: id }));
}
// Функция для получения всех элементов с помощью постраничной навигации
// Нужна для списочных методов, так как один запрос получает максимум 50 записей
async function fetchAllItems(method, params) {
let allResults = [];
let start = 0;
const batchSize = 50;
while (true) {
const result = await $b24.actions.v2.call.make({
method,
params: { ...params, start }
});
if (!result.isSuccess) {
throw new Error(`Ошибка получения данных из ${method}: ${result.getErrorMessages().join('; ')}`);
}
const data = result.getData().result;
// Обработка результатов в зависимости от метода
let pageItems;
if (method === 'crm.item.list') {
pageItems = data.items || [];
} else if (Array.isArray(data)) {
pageItems = data;
} else {
pageItems = data.result || [];
}
allResults = allResults.concat(pageItems);
// Проверяем, есть ли ещё данные
if (pageItems.length < batchSize) {
break;
}
start += batchSize;
}
return allResults;
}
// Шаг 1: Получаем информацию о текущем пользователе
const userResult = await $b24.actions.v2.call.make({ method: 'user.current', params: {} });
if (!userResult.isSuccess) {
throw new Error('Ошибка получения пользователя: ' + userResult.getErrorMessages().join('; '));
}
const userId = Number(userResult.getData().result.ID);
console.log('Текущий пользователь ID:', userId);
// Шаг 2: Получаем список всех сделок
const allItems = await fetchAllItems('crm.item.list', {
entityTypeId: 2,
select: ['id', 'title'],
filter: { assignedById: userId }
});
const dealIds = allItems.map(it => it.id);
const dealMap = allItems.reduce((map, deal) => {
map[deal.id] = deal.title;
return map;
}, {});
console.log('Сделки:', dealMap);
if (dealIds.length === 0) {
console.log('У сотрудника нет сделок');
} else {
// Формируем привязки для поиска дел по сделкам
const bindings = buildBindingsFromDealIds(dealIds);
// Шаг 3: Получаем все дела, привязанные к этим сделкам
const allActivities = await fetchAllItems('crm.activity.list', {
filter: { BINDINGS: bindings, COMPLETED: 'N' },
select: ['ID', 'OWNER_ID', 'SUBJECT', 'DEADLINE', 'RESPONSIBLE_ID']
});
const userIds = [...new Set(allActivities.map(a => a.RESPONSIBLE_ID))];
if (userIds.length === 0) {
console.log('Нет незавершенных дел по сделкам.');
console.table([]);
} else {
// Шаг 4: Получаем данные пользователей
const usersResult = await $b24.actions.v2.call.make({
method: 'user.get',
params: { filter: { ID: userIds } }
});
let userMap = {};
if (usersResult.isSuccess) {
userMap = usersResult.getData().result.reduce((map, user) => {
map[user.ID] = `${user.NAME || ''} ${user.LAST_NAME || ''}`.trim() || user.LOGIN;
return map;
}, {});
} else {
console.error('Ошибка получения пользователей:', usersResult.getErrorMessages().join('; '));
}
const table = allActivities.map(a => ({
activityId: a.ID,
dealTitle: dealMap[a.OWNER_ID] || `Сделка #${a.OWNER_ID}`,
subject: a.SUBJECT,
deadline: a.DEADLINE,
responsibleId: a.RESPONSIBLE_ID,
responsibleName: userMap[a.RESPONSIBLE_ID] || `Пользователь ${a.RESPONSIBLE_ID}`
}));
console.table(table);
}
}
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');
// Функция для формирования массива привязок к сделкам
// OWNER_TYPE_ID: 2 — тип объекта CRM — сделка
function buildBindingsFromDealIds($dealIds) {
$bindings = [];
foreach ($dealIds as $id) {
$bindings[] = [
'OWNER_TYPE_ID' => 2,
'OWNER_ID' => (int)$id,
];
}
return $bindings;
}
// Функция для получения всех элементов с помощью постраничной навигации
// Нужна для списочных методов, так как один запрос получает максимум 50 записей
// $fetchPage($start) возвращает массив элементов одной страницы
function fetchAllItems(callable $fetchPage) {
$allResults = [];
$start = 0;
$batchSize = 50;
do {
$pageItems = $fetchPage($start);
$allResults = array_merge($allResults, $pageItems);
if (count($pageItems) < $batchSize) {
break;
}
$start += $batchSize;
} while (true);
return $allResults;
}
$crm = $sb->getCRMScope();
// Шаг 1: Получаем информацию о текущем пользователе
$userId = $sb->getUserScope()->user()->current()->user()->ID;
echo "Текущий пользователь ID: $userId\n";
// Шаг 2: Получаем список всех сделок
$allItems = fetchAllItems(fn($start) => $crm->item()->list(
2,
[],
['assignedById' => $userId],
['id', 'title'],
$start
)->getItems());
$dealMap = [];
$dealIds = [];
foreach ($allItems as $item) {
$id = $item->id;
$dealIds[] = $id;
$dealMap[$id] = $item->title;
}
echo "Найдено сделок: " . count($dealIds) . "\n";
if (empty($dealIds)) {
echo "У сотрудника нет сделок\n";
exit;
}
// Формируем привязки для поиска дел по сделкам
$bindings = buildBindingsFromDealIds($dealIds);
// Шаг 3: Получаем все дела, привязанные к этим сделкам
$allActivities = fetchAllItems(fn($start) => $crm->activity()->list(
[],
[
'BINDINGS' => $bindings,
'COMPLETED' => 'N',
],
['ID', 'OWNER_ID', 'SUBJECT', 'DEADLINE', 'RESPONSIBLE_ID'],
$start
)->getActivities());
if (empty($allActivities)) {
echo "Нет незавершённых дел по сделкам.\n";
echo implode("\t", ['ID дела', 'Сделка', 'Тема', 'Дедлайн', 'Ответственный']) . "\n";
exit;
}
// Собираем уникальные ID ответственных
$responsibleIds = [];
foreach ($allActivities as $a) {
$responsibleIds[$a->RESPONSIBLE_ID] = true;
}
$responsibleIds = array_keys($responsibleIds);
$userMap = [];
if (!empty($responsibleIds)) {
// Шаг 4: Получаем данные пользователей
$users = $sb->getUserScope()->user()->get([], ['ID' => $responsibleIds])->getUsers();
foreach ($users as $user) {
$fullName = trim(($user->NAME ?? '') . ' ' . ($user->LAST_NAME ?? ''));
$userMap[$user->ID] = $fullName ?: ($user->LOGIN ?? "Пользователь {$user->ID}");
}
}
// Формируем и выводим таблицу
$header = ['ID дела', 'Сделка', 'Тема', 'Дедлайн', 'Ответственный'];
echo implode("\t", $header) . "\n";
foreach ($allActivities as $a) {
$activityId = $a->ID;
$ownerId = (int)$a->OWNER_ID;
$dealTitle = $dealMap[$ownerId] ?? "Сделка #{$ownerId}";
$subject = $a->SUBJECT ?? '';
$deadline = $a->DEADLINE;
$responsibleId = $a->RESPONSIBLE_ID;
$responsibleName = $userMap[$responsibleId] ?? "Пользователь {$responsibleId} (не найден)";
echo implode("\t", [
$activityId,
$dealTitle,
$subject,
$deadline,
$responsibleName
]) . "\n";
}
from b24pysdk import BitrixWebhook, Client
def build_bindings_from_deal_ids(deal_ids):
return [{"OWNER_TYPE_ID": 2, "OWNER_ID": deal_id} for deal_id in deal_ids]
def fetch_all_items(fetch_page, data_key=None):
all_results = []
start = 0
batch_size = 50
while True:
response = fetch_page(start)
if data_key is None:
page_items = response.result or []
else:
page_items = response.result.get(data_key, [])
all_results.extend(page_items)
if len(page_items) < batch_size:
break
start += batch_size
return all_results
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
current = client.user.current().response.result
user_id = int(current["ID"])
print(f"Текущий пользователь ID: {user_id}")
all_items = fetch_all_items(
lambda start: client.crm.item.list(
entity_type_id=2,
select=["id", "title"],
filter={"assignedById": user_id},
start=start,
).response,
data_key="items",
)
deal_ids = [int(item["id"]) for item in all_items]
deal_map = {int(item["id"]): item["title"] for item in all_items}
print(f"Найдено сделок: {len(deal_ids)}")
if not deal_ids:
print("У сотрудника нет сделок")
else:
bindings = build_bindings_from_deal_ids(deal_ids)
all_activities = fetch_all_items(
lambda start: client.crm.activity.list(
filter={
"BINDINGS": bindings,
"COMPLETED": "N",
},
select=["ID", "OWNER_ID", "SUBJECT", "DEADLINE", "RESPONSIBLE_ID"],
start=start,
).response
)
if not all_activities:
print("Нет незавершенных дел по сделкам.")
print("\t".join(["ID дела", "Сделка", "Тема", "Дедлайн", "Ответственный"]))
else:
responsible_ids = sorted(
{
int(item["RESPONSIBLE_ID"])
for item in all_activities
if item.get("RESPONSIBLE_ID")
}
)
user_map = {}
if responsible_ids:
users = fetch_all_items(
lambda start: client.user.get(
filter={"ID": responsible_ids},
start=start,
).response
)
for user in users:
full_name = f"{user.get('NAME', '')} {user.get('LAST_NAME', '')}".strip()
user_map[str(user["ID"])] = full_name or user.get("LOGIN", f"Пользователь {user['ID']}")
print("\t".join(["ID дела", "Сделка", "Тема", "Дедлайн", "Ответственный"]))
for activity in all_activities:
activity_id = activity.get("ID", "")
owner_id = int(activity.get("OWNER_ID", 0))
deal_title = deal_map.get(owner_id, f"Сделка #{owner_id}")
subject = activity.get("SUBJECT", "")
deadline = activity.get("DEADLINE", "")
responsible_id = activity.get("RESPONSIBLE_ID", "")
responsible_name = user_map.get(str(responsible_id), f"Пользователь {responsible_id} (не найден)")
print(
"\t".join(
[
str(activity_id),
str(deal_title),
str(subject),
str(deadline),
str(responsible_name),
]
)
)
// Подготовка в пустом каталоге — go get без go mod init не сработает:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Запуск:
//
// export B24_WEBHOOK_URL='https://ваш-портал.bitrix24.ru/rest/1/токен/' && go run .
//
// Пример самодостаточный: он создаёт две свои сделки с делами, собирает по ним
// таблицу дел с ответственными и убирает за собой. Запускается на любом
// портале, ничего править не нужно.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"sort"
"time"
b24 "github.com/bitrix24/b24gosdk"
)
// entityTypeDeal — идентификатор типа объекта «сделка» из crm.enum.ownertype.
const entityTypeDeal = 2
// maxDeals ограничивает выборку сделок. BINDINGS — это ФИЛЬТР, а не батч:
// массив из тысячи привязок уедет в одном запросе и утяжелит его. На боевом
// портале сделки стоит сузить ещё и фильтром по стадии.
const maxDeals = 50
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// Путь вебхука — это секрет, поэтому он приходит из окружения, а не из кода.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- шаг 1: идентификатор текущего пользователя
res, err := core.Call(ctx, "user.current", nil, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.current: %w", err)
}
// Идентификатор приходит СТРОКОЙ ("29"): b24.ID разбирает и число, и строку
// с числом, обычный int здесь падает.
var me struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &me); err != nil {
return fmt.Errorf("разбор текущего пользователя: %w", err)
}
fmt.Printf("текущий пользователь %d: %s %s\n", me.ID, me.Name, me.LastName)
// --- подготовка: свои сделки с делами, чтобы таблице было что показать
cleanup, err := createDealsWithActivities(ctx, core, me.ID)
defer cleanup()
if err != nil {
return err
}
// --- шаг 2: сделки, за которые отвечает сотрудник
// Списочный метод отдаёт максимум 50 записей за запрос. Pages идёт по
// курсору сам: ОТСУТСТВИЕ next заканчивает список, потому что next: 0 —
// это законное первое смещение, а не признак конца.
pager, err := core.Pages("crm.item.list", b24.Params{
"entityTypeId": entityTypeDeal,
"select": []string{"id", "title"},
"filter": b24.Params{"assignedById": me.ID},
}, b24.WithCallOptions(b24.WithIdempotent()))
if err != nil {
return fmt.Errorf("crm.item.list: %w", err)
}
var deals []struct {
ID int `json:"id"`
Title string `json:"title"`
}
for pager.Next(ctx) {
for _, row := range pager.Rows() {
var d struct {
ID int `json:"id"`
Title string `json:"title"`
}
if err := json.Unmarshal(row, &d); err != nil {
return fmt.Errorf("разбор сделки: %w", err)
}
deals = append(deals, d)
}
if len(deals) >= maxDeals {
break
}
}
// Ошибка всплывает ЗДЕСЬ, после цикла: Next возвращает false и в конце
// списка, и при ошибке.
if err := pager.Err(); err != nil {
return fmt.Errorf("обход сделок: %w", err)
}
fmt.Printf("сделок сотрудника взято: %d\n", len(deals))
if len(deals) == 0 {
return nil
}
// --- шаг 3: дела по этим сделкам
// BINDINGS — массив привязок: по объекту на каждую сделку.
bindings := make([]b24.Params, 0, len(deals))
for _, d := range deals {
bindings = append(bindings, b24.Params{"OWNER_TYPE_ID": entityTypeDeal, "OWNER_ID": d.ID})
}
pager, err = core.Pages("crm.activity.list", b24.Params{
"filter": b24.Params{"BINDINGS": bindings, "COMPLETED": "N"},
"select": []string{"ID", "OWNER_ID", "SUBJECT", "DEADLINE", "RESPONSIBLE_ID"},
}, b24.WithCallOptions(b24.WithIdempotent()))
if err != nil {
return fmt.Errorf("crm.activity.list: %w", err)
}
// Поля дел — в ВЕРХНЕМ РЕГИСТРЕ, тогда как crm.item.list отдаёт camelCase.
var activities []activity
for pager.Next(ctx) {
for _, row := range pager.Rows() {
var a activity
if err := json.Unmarshal(row, &a); err != nil {
return fmt.Errorf("разбор дела: %w", err)
}
activities = append(activities, a)
}
}
if err := pager.Err(); err != nil {
return fmt.Errorf("обход дел: %w", err)
}
fmt.Printf("активных дел: %d\n", len(activities))
// --- шаг 4: ответственные за дела
// Ответственным за дело может быть не тот, кто отвечает за сделку, поэтому
// идентификаторы берутся из самих дел.
ids := uniqueIDs(activities)
users := map[b24.ID]string{}
if len(ids) > 0 {
res, err := core.Call(ctx, "user.get", b24.Params{
"filter": b24.Params{"ID": ids},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.get: %w", err)
}
var rows []struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &rows); err != nil {
return fmt.Errorf("разбор сотрудников: %w", err)
}
for _, u := range rows {
users[u.ID] = u.Name + " " + u.LastName
}
}
titles := map[int]string{}
for _, d := range deals {
titles[d.ID] = d.Title
}
fmt.Println("Дело\tСделка\tОписание\tСрок\tОтветственный")
for _, a := range activities {
who := users[a.ResponsibleID]
if who == "" {
who = "Неизвестно"
}
fmt.Printf("%d\t%s\t%s\t%s\t%s\n", a.ID, titles[int(a.OwnerID)], a.Subject, a.Deadline, who)
}
return nil
}
// activity — одна строка ответа crm.activity.list.
type activity struct {
ID b24.ID `json:"ID"`
OwnerID b24.ID `json:"OWNER_ID"`
Subject string `json:"SUBJECT"`
Deadline string `json:"DEADLINE"`
ResponsibleID b24.ID `json:"RESPONSIBLE_ID"`
}
func uniqueIDs(activities []activity) []b24.ID {
seen := map[b24.ID]bool{}
out := make([]b24.ID, 0, len(activities))
for _, a := range activities {
if a.ResponsibleID > 0 && !seen[a.ResponsibleID] {
seen[a.ResponsibleID] = true
out = append(out, a.ResponsibleID)
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
// --- вспомогательное: подготовка данных и уборка
// createDealsWithActivities создаёт две сделки и по делу в каждой. Возвращает
// функцию уборки — она вызывается даже если подготовка оборвалась на середине.
func createDealsWithActivities(ctx context.Context, core *b24.Core, userID b24.ID) (func(), error) {
var dealIDs []b24.ID
cleanup := func() {
// Удаление сделки уносит и её дела.
for _, id := range dealIDs {
del(ctx, core, "crm.item.delete", b24.Params{"entityTypeId": entityTypeDeal, "id": id})
}
}
for i, spec := range []struct {
title string
task string
in time.Duration
}{
{"Закупка печей (пример b24gosdk)", "Позвонить клиенту", 24 * time.Hour},
{"Закупка блендеров (пример b24gosdk)", "Отправить счёт", 48 * time.Hour},
} {
res, err := core.Call(ctx, "crm.item.add", b24.Params{
"entityTypeId": entityTypeDeal,
"fields": b24.Params{
"title": spec.title,
"assignedById": userID,
"opportunity": (i + 1) * 100,
},
})
if err != nil {
return cleanup, fmt.Errorf("crm.item.add: %w", err)
}
raw, ok := b24.Unwrap(res.Result, "item", "id")
if !ok {
return cleanup, fmt.Errorf("нет item.id в %s", res.Result)
}
var dealID b24.ID
if err := json.Unmarshal(raw, &dealID); err != nil {
return cleanup, err
}
dealIDs = append(dealIDs, dealID)
if _, err := core.Call(ctx, "crm.activity.todo.add", b24.Params{
"ownerTypeId": entityTypeDeal,
"ownerId": dealID,
"title": spec.task,
"description": "Дело создано примером b24gosdk",
"deadline": time.Now().Add(spec.in).Format(time.RFC3339),
"responsibleId": userID,
}); err != nil {
return cleanup, fmt.Errorf("crm.activity.todo.add: %w", err)
}
}
return cleanup, nil
}
// del убирает созданное. Ошибку уборки печатаем, но не возвращаем: она не
// должна подменить собой настоящую ошибку сценария.
func del(ctx context.Context, core *b24.Core, method string, params b24.Params) {
if _, err := core.Call(ctx, method, params); err != nil {
fmt.Fprintf(os.Stderr, "уборка, %s: %v\n", method, err)
}
}