Сотрудник выбирает договоры в CRM и создаёт по ним счёт в 1С. Номер счёта возвращается к выбранным договорам. Позже по ним можно подготовить акт, а банковские поступления из 1С загрузить в CRM для сверки оплат.
В основе этой цепочки — договор в CRM: кто заказчик, какие услуги ему оказывают и сколько они стоят. Команда связала CRM с 1С через OData, чтобы использовать эти данные при подготовке документов. Сотрудник запускает операции, интеграция собирает реквизиты, отправляет запросы и сохраняет ответы.
Что связывает договор, счёт и акт
Для разбора возьму два договора из примера ниже: по первому нужно подготовить макет за 15 000 рублей, по второму — дважды настроить форму по 5 000. По ним нужно выставить общий счёт на 25 000 рублей и сохранить его связь с обоими договорами.
CRM передаёт в 1С организацию-исполнителя, заказчика, договор контрагента в 1С и строки услуг. В ответ получает номер счёта и GUID — уникальный идентификатор документа в 1С. Номер нужен сотруднику, а GUID используется в следующих запросах: получить документ, подготовить печатное представление или связать с ним акт.
Перед созданием CRM проверяет контрагента, организацию, номенклатуру и ссылку на договор контрагента в 1С. Если выбранные договоры CRM ссылаются на разные договоры в 1С, общий документ не создаётся. Если к договору CRM уже привязан счёт, форма останавливает повторное создание.
Ниже разберу запросы этой цепочки на PHP: поиск контрагента, создание счёта, подготовку акта и получение поступлений. Все файлы используют одну функцию odata() и лежат в одной папке. Общий HTTP-клиент и настройка доступа приведены в разделе «Подключение к 1С для примеров». Если собираете примеры для запуска, начните с него. Запросы записи выполняйте на копии базы: они создают документы и записи справочников.
Сначала найти контрагента и выбрать его договор в 1С
Для счёта нужен идентификатор контрагента в 1С. Интеграция сначала берёт его из записи компании в CRM. Если связи ещё нет, ищет контрагента по ИНН; при отсутствии совпадения создаёт запись по реквизитам и сохраняет её GUID. Этот идентификатор затем попадёт в счёт.
В company.php поиск юридического лица уточняется по КПП. Если найдено несколько записей, программа останавливается и требует выбрать контрагента. В проекте исходный поиск выполняется по ИНН.
<?php
require __DIR__ . '/odata.php';
$inn = $argv[1] ?? '';
$kpp = $argv[2] ?? '';
$name = $argv[3] ?? '';
$countryId = $argv[4] ?? '';
if (!preg_match('/^\d{10}$/', $inn) || !preg_match('/^\d{9}$/', $kpp) || !$name || !$countryId) {
throw new InvalidArgumentException('Нужны ИНН юрлица, КПП, название и GUID страны');
}
$result = odata('GET', 'Catalog_Контрагенты', [
'$filter' => "ИНН eq '$inn' and КПП eq '$kpp'",
'$select' => 'Ref_Key,Description',
'$top' => 2,
]);
$companies = $result['value'];
if (count($companies) > 1) {
throw new RuntimeException('Найдено несколько контрагентов: требуется выбор');
}
$company = $companies[0] ?? odata('POST', 'Catalog_Контрагенты', [], [
'Description' => $name,
'НаименованиеПолное' => $name,
'ЮридическоеФизическоеЛицо' => 'ЮридическоеЛицо',
'СтранаРегистрации_Key' => $countryId,
'ИНН' => $inn,
'КПП' => $kpp,
]);
echo $company['Ref_Key'], PHP_EOL;
Запуск: php company.php ИНН КПП 'Название компании' GUID_страны. Передайте реквизиты тестового контрагента и идентификатор страны из справочника вашей базы. Программа напечатает GUID найденной или созданной записи — его нужно сохранить для следующих обращений к этому контрагенту.
Поиск и создание — два отдельных запроса. Если два процесса одновременно ищут одну компанию, оба могут получить пустой результат и затем создать по записи. При встраивании такого кода в приложение нужно учитывать параллельные запросы.
После контрагента определяется его договор в 1С — запись справочника, ссылка на которую нужна в счёте и акте. Она подбирается для пары «контрагент — наша организация». Сотрудник может выбрать этот договор явно; иначе интеграция ищет подходящий в 1С, а при отсутствии создаёт основной.
Список договоров контрагента запрашивается через Catalog_ДоговорыКонтрагентов с фильтром по Owner_Key и Организация_Key. В примере создания счёта ниже договор контрагента в 1С уже выбран: его GUID передаётся вместе с остальными реквизитами документа.
Создать счёт и вернуть его номер к договорам
Теперь у нас есть реквизиты контрагента и его договора в 1С. CRM собирает из выбранных договоров наименования услуг, номенклатуру, количество и стоимость. После ответа 1С номер и GUID документа записываются у всех выбранных договоров.
Чтобы повторить запрос без хранилища CRM, передадим те же виды данных в JSON. header содержит реквизиты документа, contracts — два договора CRM с собственными идентификаторами. Пример рассчитан на услуги без НДС и целое количество; деньги до отправки считаются в копейках.
Сохраните invoice-input.json. GUID замените идентификаторами объектов своей базы. Для контрагента подойдёт результат company.php; для валюты документа выберите рубли.
{
"header": {
"Date": "2026-09-01T00:00:00",
"Организация_Key": "11111111-1111-4111-8111-111111111111",
"ОрганизацияПолучатель_Key": "11111111-1111-4111-8111-111111111111",
"Контрагент_Key": "22222222-2222-4222-8222-222222222222",
"ДоговорКонтрагента_Key": "33333333-3333-4333-8333-333333333333",
"ВалютаДокумента_Key": "44444444-4444-4444-8444-444444444444"
},
"contracts": [
{"id": "contract-1", "name": "Подготовка макета", "quantity": 1,
"unit_kopecks": 1500000, "item_id": "55555555-5555-4555-8555-555555555555"},
{"id": "contract-2", "name": "Настройка формы", "quantity": 2,
"unit_kopecks": 500000, "item_id": "55555555-5555-4555-8555-555555555555"}
]
}
Обязательные реквизиты зависят от конфигурации и настроек учёта. Если ваша база требует склад, ответственного или другие поля, добавьте их в header. Имена и типы полей доступны в $metadata, а дополнительные требования к записи определяет логика конфигурации.
invoice.php переводит договоры в строки документа, считает общую сумму и отправляет счёт в 1С:
<?php
require __DIR__ . '/odata.php';
$input = json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR);
$rows = [];
$total = 0;
foreach ($input['contracts'] as $index => $contract) {
if (!is_int($contract['unit_kopecks']) || $contract['unit_kopecks'] < 0
|| !is_int($contract['quantity']) || $contract['quantity'] < 1) {
throw new InvalidArgumentException('Цена — целые копейки; количество — положительное целое');
}
$amount = $contract['unit_kopecks'] * $contract['quantity'];
$total += $amount;
$rows[] = [
'LineNumber' => (string) ($index + 1),
'Номенклатура_Key' => $contract['item_id'],
'Содержание' => $contract['name'],
'Количество' => $contract['quantity'],
'Цена' => $contract['unit_kopecks'] / 100,
'Сумма' => $amount / 100,
'СтавкаНДС' => 'БезНДС',
];
}
if (!$rows) {
throw new InvalidArgumentException('Не выбраны договоры');
}
$invoice = odata('POST', 'Document_СчетНаОплатуПокупателю', [], array_replace(
$input['header'],
['Posted' => false, 'ДокументБезНДС' => true,
'СуммаДокумента' => $total / 100, 'Товары' => $rows]
));
echo json_encode([
'invoice_id' => $invoice['Ref_Key'],
'number' => $invoice['Number'],
'contract_ids' => array_column($input['contracts'], 'id'),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), PHP_EOL;
Запустите php invoice.php invoice-input.json. Сумма составит 25 000 рублей: 15 000 за макет и 10 000 за две настройки формы. Программа вернёт invoice_id, number и contract_ids со значениями contract-1 и contract-2. По этому ответу можно записать номер и идентификатор счёта к обоим договорам — именно такую связь сохраняет интеграция в CRM.
Счёт создаётся непроведённым (Posted: false), чтобы реквизиты и услуги можно было проверить до проведения. Повторный запуск создаст ещё один документ. Перед включением примера в приложение нужна проверка уже сохранённого счёта, как в форме проекта.
При тайм-ауте 1С могла уже создать счёт, хотя CRM ещё не получила его GUID. Перед повторной отправкой нужно проверить результат первого запроса.
Подготовить акт по связанным договорам
Для акта CRM находит счёт выбранного договора, затем собирает все договоры CRM, связанные с этим счётом. Здесь и используется связь, сохранённая на предыдущем шаге. Договор контрагента в 1С берётся из явного выбора сотрудника, затем из счёта; если его там нет, подбирается по контрагенту и организации. После создания номер и GUID акта записываются у договоров, затем предусмотрен вызов проведения.
В act.php источником строк служит счёт в 1С. Такой пример можно запустить без таблиц CRM: получить счёт по GUID, перенести услуги и реквизиты в документ реализации, сохранить ссылку на счёт в документе и его строках. Он рассчитан на счёт из предыдущего примера — только услуги без НДС.
<?php
require __DIR__ . '/odata.php';
$invoiceId = $argv[1] ?? '';
if (!preg_match('/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i', $invoiceId)) {
throw new InvalidArgumentException('Нужен GUID счёта');
}
$invoice = odata('GET', "Document_СчетНаОплатуПокупателю(guid'$invoiceId')");
$services = [];
foreach ($invoice['Товары'] as $index => $row) {
$services[] = [
'LineNumber' => (string) ($index + 1),
'Номенклатура_Key' => $row['Номенклатура_Key'],
'Содержание' => $row['Содержание'],
'Количество' => $row['Количество'],
'Цена' => $row['Цена'],
'Сумма' => $row['Сумма'],
'СтавкаНДС' => 'БезНДС',
'СчетНаОплатуПокупателю_Key' => $invoiceId,
];
}
if (!$services) {
throw new RuntimeException('В счёте нет строк услуг');
}
$act = odata('POST', 'Document_РеализацияТоваровУслуг', [], [
'Date' => date('Y-m-d\TH:i:s'),
'Posted' => false,
'ВидОперации' => 'Услуги',
'Организация_Key' => $invoice['Организация_Key'],
'Контрагент_Key' => $invoice['Контрагент_Key'],
'ДоговорКонтрагента_Key' => $invoice['ДоговорКонтрагента_Key'],
'ВалютаДокумента_Key' => $invoice['ВалютаДокумента_Key'],
'ДокументБезНДС' => true,
'СчетНаОплатуПокупателю_Key' => $invoiceId,
'СуммаДокумента' => $invoice['СуммаДокумента'],
'Услуги' => $services,
]);
echo $act['Ref_Key'], ' ', $act['Number'], PHP_EOL;
Запуск: php act.php GUID_счёта. На выходе — GUID и номер акта. Контрагент, организация, договор контрагента в 1С, валюта и строки услуг взяты из счёта, поэтому повторно вводить их не требуется.
В примере акт остаётся непроведённым. Для проведения могут понадобиться счета учёта и аналитика строк услуг; их состав зависит от конфигурации. В CRM проекта эти реквизиты задаются при подготовке документа. После их заполнения проведение вызывается отдельным POST к Document_РеализацияТоваровУслуг(guid'GUID_акта')/Post(). Успешность этой операции проверяется по её ответу.
Когда нужны печатные представления счёта и акта, CRM запрашивает документ из 1С и подставляет данные вместе с реквизитами сторон в шаблон. Состав услуг берётся из документа.
Загрузить банковские поступления для сверки
После подготовки документов остаётся проверить оплату. Для этого данные идут в обратную сторону: сотрудник выбирает в CRM месяц и загружает из 1С поступления на расчётный счёт. В форме доступны дата, сумма, назначение платежа и реквизиты плательщика; сотрудник выбирает поступление для дальнейшей сверки с договором CRM.
payments.php получает проведённые поступления за месяц и возвращает JSON. В выборку входят идентификатор, дата, сумма и назначение платежа:
<?php
require __DIR__ . '/odata.php';
$month = $argv[1] ?? '';
if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
throw new InvalidArgumentException('Месяц должен иметь формат YYYY-MM');
}
$start = new DateTimeImmutable($month . '-01T00:00:00');
$end = $start->modify('+1 month');
$from = $start->format('Y-m-d\TH:i:s');
$to = $end->format('Y-m-d\TH:i:s');
$payments = [];
for ($skip = 0; ; $skip += count($page)) {
$page = odata('GET', 'Document_ПоступлениеНаРасчетныйСчет', [
'$filter' => "Date ge datetime'$from' and Date lt datetime'$to'"
. ' and Posted eq true and DeletionMark eq false',
'$select' => 'Ref_Key,Date,СуммаДокумента,НазначениеПлатежа',
'$orderby' => 'Date asc,Ref_Key asc',
'$top' => 100,
'$skip' => $skip,
])['value'];
if (!$page) {
break;
}
foreach ($page as $payment) {
$payments[$payment['Ref_Key']] = $payment;
}
}
echo json_encode(array_values($payments),
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), PHP_EOL;
Запуск: php payments.php 2026-09. Начало периода включено, начало следующего месяца исключено условием Date lt. Запрос охватывает сентябрь целиком, без подбора последнего дня и времени месяца.
В исходной форме CRM запрашивается до 200 поступлений за выбранный месяц. В примере выгрузка расширена: запросы идут страницами по 100 записей до пустого ответа, а фильтры оставляют проведённые документы без пометки удаления. Эти дополнения относятся к примеру.
Сортировка по дате и GUID задаёт однозначный порядок записей. Если во время обхода через $skip документы меняются, состав страниц может измениться. Для регулярной синхронизации потребуется повторная сверка по идентификаторам.
Подключение к 1С для примеров
Примерам нужны PHP 8 с расширением cURL, адрес публикации OData и пользователь с правами на соответствующие справочники и документы. Имена объектов и полей сверьте запросом GET к $metadata вашей публикации. Учётные данные передаются через переменные окружения; в Bash пароль можно ввести без отображения:
export ODATA_URL='https://example.com/base/odata/standard.odata/'
read -r -p 'Логин 1С: ' ODATA_USER
read -r -s -p 'Пароль: ' ODATA_PASSWORD
export ODATA_USER ODATA_PASSWORD
Замените адрес своим. Функцию ниже сохраните в odata.php рядом с четырьмя файлами сценариев. Она собирает адрес и параметры, отправляет JSON и возвращает разобранный ответ 1С.
<?php
function odata(string $method, string $resource, array $query = [], ?array $body = null): array
{
$base = getenv('ODATA_URL');
$user = getenv('ODATA_USER');
$password = getenv('ODATA_PASSWORD');
if (!$base || !$user || $password === false) {
throw new RuntimeException('Задайте ODATA_URL, ODATA_USER и ODATA_PASSWORD');
}
$path = implode('/', array_map('rawurlencode', explode('/', $resource)));
$url = rtrim($base, '/') . '/' . $path . '?' . http_build_query(
$query + ['$format' => 'json'], '', '&', PHP_QUERY_RFC3986
);
$headers = ['Accept: application/json'];
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user . ':' . $password,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
];
if ($body !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($body, JSON_THROW_ON_ERROR);
$headers[] = 'Content-Type: application/json; charset=utf-8';
}
$options[CURLOPT_HTTPHEADER] = $headers;
$curl = curl_init($url);
curl_setopt_array($curl, $options);
$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($response === false) {
throw new RuntimeException('Ошибка связи: ' . $error);
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException('1С вернула HTTP ' . $status . ': ' . $response);
}
return $response === '' ? [] : json_decode($response, true, 512, JSON_THROW_ON_ERROR);
}
При ошибке HTTP функция прерывает выполнение. После тайм-аута она также возвращает ошибку; проверка того, успела ли 1С создать объект, остаётся вызывающему коду. Автоматического повторения запроса здесь нет.
Что остаётся сотруднику
Создание документов запускает сотрудник. Он же подтверждает связь банковского поступления с договором CRM. Интеграция берёт на себя подготовку данных и обмен с 1С, а результат сохраняет в карточках договоров CRM: можно открыть связанный документ, подготовить акт или печатное представление без повторного набора услуг.
Примеры написаны специально для кейса. Они показывают операции OData и обработку ответа; отличия от реализации CRM описаны рядом с соответствующими сценариями.