#Синхронизация с 1С/ERP
🔄 Сложность: advanced | Скоупы: crm | Стек: Node.js, node-cron
#Что делаем
Реализуем двустороннюю синхронизацию контактов между Bitrix24 CRM и внешней ERP-системой (1С или аналог). Скрипт сравнивает данные по ключевому полю (ИНН или email), создаёт недостающие записи в обоих системах и обновляет изменённые поля. Синхронизация запускается по расписанию и использует Entity API для оптимальной работы. Это обеспечивает единую базу клиентов без ручного ввода.
#Необходимо
- API-ключ Вайбкод с правами
crm - Node.js 18+
- Пакет:
node-cron - Доступ к API внешней ERP-системы (в примере используется mock-заглушка)
#Полный код
// erp-sync.js
// Двусторонняя синхронизация контактов Bitrix24 <-> 1С/ERP
import cron from 'node-cron';
const API_KEY = process.env.VIBE_API_KEY;
const BASE_URL = process.env.VIBE_BASE_URL;
const HEADERS = {
'X-Api-Key': API_KEY,
'Content-Type': 'application/json',
};
// ============================================================
// Mock: имитация данных из 1С/ERP
// В реальном проекте замените на запросы к API вашей ERP-системы
// ============================================================
function fetchErpContacts() {
return [
{
id: 'erp-001',
name: 'Иван',
lastName: 'Петров',
email: 'petrov@example.com',
phone: '+79001234567',
inn: '7701234567',
company: 'ООО Ромашка',
updatedAt: new Date('2026-03-15'),
},
{
id: 'erp-002',
name: 'Мария',
lastName: 'Сидорова',
email: 'sidorova@example.com',
phone: '+79009876543',
inn: '7709876543',
company: 'ИП Сидорова',
updatedAt: new Date('2026-03-16'),
},
{
id: 'erp-003',
name: 'Алексей',
lastName: 'Козлов',
email: 'kozlov@example.com',
phone: '+79005551234',
inn: '7705551234',
company: 'АО Берёзка',
updatedAt: new Date('2026-03-17'),
},
];
}
function createErpContact(contact) {
console.log(` [1С] Создан контакт: ${contact.name} ${contact.lastName}`);
return { id: `erp-${Date.now()}` };
}
function updateErpContact(erpId, fields) {
console.log(` [1С] Обновлён контакт ${erpId}: ${JSON.stringify(fields)}`);
}
// ============================================================
// Загружаем все контакты из Bitrix24 через Entity API
async function fetchBitrixContacts() {
const allContacts = [];
let offset = 0;
const limit = 200;
while (true) {
const response = await fetch(
`${BASE_URL}/v1/contacts?limit=${limit}&offset=${offset}&select=id,name,lastName,email,phone,ufCrmInn,companyTitle,updatedAt`,
{ headers: { 'X-Api-Key': API_KEY } }
);
const { data } = await response.json();
allContacts.push(...(data || []));
if (!data || data.length < limit) break;
offset += limit;
}
return allContacts;
}
// Сопоставляем контакты по ИНН (основной ключ) или email (запасной)
function matchContacts(bitrixContacts, erpContacts) {
const matched = []; // совпавшие пары
const onlyInBitrix = []; // есть только в Bitrix24
const onlyInErp = []; // есть только в 1С
const bitrixByInn = new Map();
const bitrixByEmail = new Map();
for (const bc of bitrixContacts) {
const inn = bc.ufCrmInn;
const email = bc.email?.[0]?.VALUE;
if (inn) bitrixByInn.set(inn, bc);
if (email) bitrixByEmail.set(email.toLowerCase(), bc);
}
const matchedBitrixIds = new Set();
for (const erp of erpContacts) {
const bitrixMatch = bitrixByInn.get(erp.inn) || bitrixByEmail.get(erp.email?.toLowerCase());
if (bitrixMatch) {
matched.push({ bitrix: bitrixMatch, erp });
matchedBitrixIds.add(bitrixMatch.id);
} else {
onlyInErp.push(erp);
}
}
for (const bc of bitrixContacts) {
if (!matchedBitrixIds.has(bc.id)) {
onlyInBitrix.push(bc);
}
}
return { matched, onlyInBitrix, onlyInErp };
}
// Создаём контакт в Bitrix24 через Entity API
async function createBitrixContact(erpContact) {
const response = await fetch(`${BASE_URL}/v1/contacts`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
name: erpContact.name,
lastName: erpContact.lastName,
email: [{ VALUE: erpContact.email, VALUE_TYPE: 'WORK' }],
phone: [{ VALUE: erpContact.phone, VALUE_TYPE: 'WORK' }],
ufCrmInn: erpContact.inn,
companyTitle: erpContact.company,
sourceId: 'OTHER',
sourceDescription: 'Импорт из 1С/ERP',
}),
});
const { data } = await response.json();
console.log(` [B24] Создан контакт: ${erpContact.name} ${erpContact.lastName} → ID: ${data.id}`);
return data;
}
// Обновляем изменённые поля через Entity API
async function syncMatchedContact(bitrix, erp) {
const updates = {};
let hasChanges = false;
if (bitrix.name !== erp.name) {
updates.name = erp.name;
hasChanges = true;
}
if (bitrix.lastName !== erp.lastName) {
updates.lastName = erp.lastName;
hasChanges = true;
}
if (hasChanges) {
await fetch(`${BASE_URL}/v1/contacts/${bitrix.id}`, {
method: 'PATCH',
headers: HEADERS,
body: JSON.stringify(updates),
});
console.log(` [B24] Обновлён контакт #${bitrix.id}: ${JSON.stringify(updates)}`);
}
return hasChanges;
}
// Основная функция синхронизации
async function synchronize() {
const startTime = Date.now();
console.log(`\n${'='.repeat(60)}`);
console.log(`🔄 Синхронизация: ${new Date().toLocaleString('ru-RU')}`);
console.log('='.repeat(60));
// Загружаем данные из обоих источников
console.log('\n📥 Загрузка данных...');
const [bitrixContacts, erpContacts] = await Promise.all([
fetchBitrixContacts(),
Promise.resolve(fetchErpContacts()),
]);
console.log(` Bitrix24: ${bitrixContacts.length} контактов`);
console.log(` 1С/ERP: ${erpContacts.length} контактов`);
// Сопоставляем
console.log('\n🔍 Сопоставление...');
const { matched, onlyInBitrix, onlyInErp } = matchContacts(bitrixContacts, erpContacts);
console.log(` Совпадений: ${matched.length}`);
console.log(` Только в B24: ${onlyInBitrix.length}`);
console.log(` Только в 1С: ${onlyInErp.length}`);
let created = 0;
let updated = 0;
// Создаём недостающие контакты в Bitrix24
if (onlyInErp.length > 0) {
console.log('\n➕ Создание в Bitrix24...');
for (const erp of onlyInErp) {
await createBitrixContact(erp);
created++;
}
}
// Создаём недостающие контакты в 1С
if (onlyInBitrix.length > 0) {
console.log('\n➕ Создание в 1С/ERP...');
for (const bc of onlyInBitrix) {
createErpContact(bc);
created++;
}
}
// Обновляем совпавшие записи
if (matched.length > 0) {
console.log('\n🔄 Обновление совпавших...');
for (const { bitrix, erp } of matched) {
const changed = await syncMatchedContact(bitrix, erp);
if (changed) updated++;
}
}
// Итоги
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\n${'─'.repeat(60)}`);
console.log(`📊 Итоги: создано ${created}, обновлено ${updated}, время ${duration}с`);
console.log('─'.repeat(60));
}
// Запуск по расписанию: каждый час
console.log('🔄 Сервис синхронизации с 1С/ERP запущен');
console.log(' Расписание: каждый час');
// Первый запуск сразу
synchronize().catch(console.error);
// Далее по крону
cron.schedule('0 * * * *', () => {
synchronize().catch(console.error);
});
#Как это работает
- Сервис параллельно загружает контакты из Bitrix24 через Entity API (
GET /v1/contactsс авто-пагинацией через limit/offset) и из внешней ERP-системы. - Контакты сопоставляются по ИНН как основному ключу и по email как запасному. В результате формируются три группы: совпавшие, только в Bitrix24, только в ERP.
- Контакты, которые есть только в ERP, создаются в Bitrix24 через Entity API (
POST /v1/contacts) с пометкой об источнике. - Контакты, которые есть только в Bitrix24, создаются во внешней системе (в примере — mock-функция).
- Для совпавших записей сравниваются поля, и при расхождении обновляется Bitrix24 через Entity API (
PATCH /v1/contacts/:id). - Синхронизация запускается каждый час через node-cron и при старте сервиса.
#Что можно улучшить
- Заменить mock-функции ERP на реальные запросы к API 1С (например, через OData)
- Добавить журнал синхронизации в базу данных для отслеживания истории изменений
- Реализовать conflict resolution: кто главнее при расхождении — Bitrix24 или ERP
- Использовать
POST /v1/contacts/batchдля создания/обновления контактов пачками по 500 штук