О модуле

Sub-package плагина Inventory (не отдельный плагин) — двусторонняя синхронизация остатков материалов монтажников с 1С.

Направление Когда Что происходит

1С → ERP

Вручную (для HS — по кнопке, для File — загрузкой файла; см. Импорт остатков)

Импорт остатков склада монтажника → pending → подтверждение хозяином склада → баланс

ERP → 1С

Для HS — автоматически, при переходе процесса в статус СОГЛАСОВАНО; для File — вручную, скачиванием CSV и подтверждением (см. Экспорт списания при СОГЛАСОВАНО)

Экспорт списания материалов, зарезервированных по процессу

Резервирование материалов — только внутренняя операция ERP, в 1С не передаётся ни в каком виде. 1С видит только финальное списание после согласования.

Протоколы обмена

Способ доставки данных туда-обратно настраивается per-instance (поле protocol у inventory_sync1c_connector) — так решается проблема "у клиента нет своего 1С-программиста": основная бизнес-логика (очереди inventory_sync1c_import_pending/inventory_sync1c_export_queue, append-only движения) одна и та же для всех протоколов, отличается только то, как остатки/списания физически попадают туда-обратно.

Протокол Требует от 1С Когда использовать

hs (по умолчанию)

Кастомный HTTP-сервис (/hs/gtd_numbers/*), пишет 1С-программист под конкретную базу

Есть свой 1С-программист — полностью автоматический обмен в обе стороны

file

Ничего — только штатный отчёт 1С в CSV и ручное проведение

Нет 1С-программиста (большинство небольших клиентов) — см. [setup-protocol-file]

epf (зарезервировано, не реализовано)

Ничего программного — готовая внешняя обработка (.epf), один раз собирается BGERP/подрядчиком под типовую конфигурацию и раздаётся клиентам

Планируется как промежуточный вариант — автоматизация без необходимости публиковать веб-сервис на стороне 1С

Настройка

Инстанс 1С

Администрирование → Инвентарь → Синхронизация с 1С → Инстанции. Один инстанс = одно подключение к одной базе 1С:

Поле Назначение

title

Название для админки

protocol

hs или file — см. Протоколы обмена

api_url

Базовый URL HTTP-сервиса, обычно http://<host>/<база>/hs/gtd_numbers — только для hs

username/password

HTTP Basic Auth — только для hs

item_ext_id_param_id

id text-параметра номенклатуры, где хранится product_id (внешний ID из 1С)

store_ext_id_param_id

id text-параметра склада, где хранится warehouse_id (внешний ID из 1С)

Кнопка Проверить соединение дёргает /stocks с пустыми параметрами и смотрит на HTTP-статус — доступна только для протокола hs.

Маппинг монтажник ↔ склад в 1С

Таблица inventory_sync1c_engineer_warehouse (админка → Маппинг): instance_id, user_id (монтажник в ERP), warehouse_id/warehouse_name (UUID и название склада в 1С), location_name (локация/объект), store_id (склад в ERP — создаётся автоматически при первом импорте, если ещё не существует).

Для протокола file заводить эту запись заранее не нужно — она создаётся автоматически при первой загрузке CSV на склад, см. [setup-protocol-file]. Раздел «Маппинг» здесь актуален прежде всего для протокола hs (там нужен реальный warehouse_id, известный заранее).

Конфигурация плагина

Тот же config_global, что и общий inventory:* (см. Inventory):

sync1c:enable=1

# Типы процессов, для которых включена интеграция (через запятую)
sync1c:processTypeIds=45

# Для каждого типа — id статуса, при входе в который отправляется списание
sync1c:processType.45.statusWriteoff=14

# Максимум попыток отправки при ошибке (после — status=failed, видно в админке)
sync1c:export.maxAttempts=3

Планировщик

Только экспорт для протокола hs работает по расписанию — импорт остатков запускается вручную всегда (см. Импорт остатков), а экспорт для протокола file подтверждается вручную (см. Экспорт списания при СОГЛАСОВАНО). ExportPoller при выборке pending-записей джойнится на inventory_sync1c_connector и берёт только записи инстансов с protocol='hs'file-инстансы поллер не трогает:

scheduler.task.sync1c_export.class=org.bgerp.plugin.inventory.sync1c.exec.ExportPoller
scheduler.task.sync1c_export.minutes=*/1

Импорт остатков

Протокол hsАдминистрирование → Синхронизация с 1С → Инстанции → Импортировать — вручную, по конкретному маппингу монтажник↔склад (ewId). Запускает ImportService.syncEngineerWarehouse(): запрашивает /stocks, при первом обращении создаёт склад и недостающие позиции номенклатуры (по product_id/warehouse_id), пишет ВСЕ позиции в inventory_sync1c_import_pending — баланс не трогается до явного подтверждения (см. Импорт остатков (Фаза 1 — с подтверждением)).

Протокол file — путь короче, чем у hs, и не требует заранее заведённого маппинга монтажник↔склад: файл → склад → превью → подтверждение.

Точки входа (любая ведёт на одну и ту же форму importFileForm):

  • Карточка склада (Инвентарь → Склады → карточка склада, раздел «Остатки на складе» → кнопка Загрузить CSV) — склад уже предвыбран.

  • Инвентарь → Движения ТМЦ → кнопка Загрузить CSV — склад предвыбран, если он выбран в фильтре.

  • Администрирование → Инвентарь → Синхронизация с 1С — раздел для настройки самой интеграции (инстансы, маппинг, логи); кнопки загрузки файла здесь намеренно нет — см. ниже.

Дальше — три шага:

  1. Выбор склада и файла (importFileFormimport_file_upload.jsp) — инстанция 1С автовыбирается, если она одна (при нескольких — выпадающий список); список складов отфильтрован по доступу вызывающего (см. доступ к складам) — обычный пользователь видит только свои склады, полный админ — все.

  2. Превью (importFilePreview, read-only, ничего не пишет в БД) — парсит CSV, резолвит каждую строку по product_id (внешний ID номенклатуры), показывает таблицу: чекбокс принять/отклонить строку, найденное наименование (или «новая позиция: …​», если товар с таким product_id в ERP ещё не заведён), редактируемое поле «Количество».

  3. Подтверждение (importFileConfirm) — принимает только отмеченные строки, резолвит или создаёт EngineerWarehouse-маппинг «на лету» (resolveEngineerWarehouse() — без реального warehouse_id от 1С, это ручная загрузка, а не HS-синхронизация; подтверждающий пользователь по умолчанию = владелец склада, см. Импорт остатков (Фаза 1 — с подтверждением)), пишет в inventory_sync1c_import_pending — дальше всё как у протокола hs (см. Импорт остатков (Фаза 1 — с подтверждением)).

Формат CSV — заголовок обязателен, порядок колонок не важен (ищутся по имени), лишние колонки игнорируются:

product_id,product_name,quantity
d776ab8b-ca60-11ee-bba6-f8f082350659,Кабель ВОК 8,15.000

Пример файла — example_stocks.csv рядом с этим документом.

Количество — это ИТОГОВЫЙ (абсолютный) остаток на складе на момент выгрузки, а не добавка к тому, что уже есть в ERP. Как и у протокола hs (см. POST /hs/gtd_numbers/stocks — получение остатков) — 1С/файл всегда описывает полный снимок, а не дельту. Если в файле 15.000, а в ERP уже было 10 — после подтверждения станет 15 (не 25). Колонка «Разница» в «Ожидающих поступлениях» (см. Импорт остатков (Фаза 1 — с подтверждением)) как раз показывает фактическое изменение, чтобы не перепутать итог с добавкой — то же предупреждение показано прямо на форме загрузки.

Кладовщик формирует такой файл штатным отчётом 1С (или выгрузкой из консоли/обработки) — сам ERP не диктует, как именно 1С сформирует данные, только формат файла. Число — с точкой в качестве разделителя (запятая внутри поля трактуется как разделитель CSV, если поле не заковычено — при заковыченном поле "15,000" парсер тоже примет запятую как десятичный разделитель); строго проверяется на клиенте перед отправкой — нечисловое значение (например, опечатка 15,0a) блокирует подтверждение целиком, а не тихо обрезается до числовой части. Файл кодируется в UTF-8 (с BOM или без — оба варианта читаются).

ImportService.importFromRows() — то же самое ядро (upsert item/store, запись в inventory_sync1c_import_pending), что и у протокола hs, только источник строк другой — не HTTP, а разобранный CsvUtil.parse() файл. Дальнейшее подтверждение (см. Импорт остатков (Фаза 1 — с подтверждением)) не отличается от протокола hs вообще.

Кто может загружать CSV — доступ на уровне склада, не только полный админ

Загрузка CSV и последующее подтверждение в «Ожидающих поступлениях» — это ДВЕ отдельные точки, обе проверяют доступ к конкретному складу так же, как остальные действия плагина (store_access ∪ владелец склада ∪ «группа редактирования», см. доступ к складам), не только «есть ли право на действие вообще»:

  • AdminSyncAction.importFileForm/importFilePreview/importFileConfirm — фильтруют список складов и проверяют storeId из запроса на сервере (не только скрытием в UI).

  • UserImportPendingAction (страница «Ожидающие поступления») — видит и может провести/отклонить запись любой, у кого есть доступ к складу этой записи, а не только исходный владелец склада (EngineerWarehouse.userId). Это позволяет нескольким «кладовщикам» делить обязанности по одному складу — см. доступ к складам про группу редактирования.

Права на сами три действия загрузки (importFileForm/importFilePreview/importFileConfirm) выдаются в наборах прав независимо от остальной админки sync1c (instance:null и т.д.) — можно выдать кладовщику ТОЛЬКО загрузку файла, не открывая ему весь раздел «Синхронизация с 1С».

API-контракт (HTTP-сервис на стороне 1С)

Только для протокола hs (см. Протоколы обмена) — для file этот раздел не применяется, там формат другой (см. [setup-protocol-file]).

Кастомный HTTP-сервис (/hs/…​), реализуется 1С-разработчиками под конкретную базу. BGERP не знает и не зависит от версии/конфигурации 1С — только от этого контракта. Если несколько баз/версий 1С реализуют один и тот же контракт — код ERP менять не нужно, достаточно завести новый инстанс.

Общее

  • Базовый URL: http://<host>:<port>/<база>/hs/gtd_numbers

  • Аутентификация: Authorization: Basic <base64(login:password)>

  • Content-Type: application/json, кодировка UTF-8

  • Формат ответа:

    { "data": [...], "error": false, "errorText": "" }

    При ошибке: "error": true, "errorText": "описание".

POST /hs/gtd_numbers/stocks — получение остатков

Запрос:

{ "warehouse_name": "Лопатин Владимир Владимирович", "location_name": "ст-ца Брюховецкая - ОМИПЛАТ" }

Ответ — массив data, поля на элемент: product_id, product_name, organization_id, organization_name, gtd_id, gtd_name, warehouse_id, warehouse_name, location, engineer (bool), stocks (число), stocks_rntp (число).

Контракт по обнулившимся позициям (решено): если остаток по позиции стал равен нулю, она полностью пропадает из массива data — 1С не присылает её с stocks=0. Ответ на /stocks трактуется как ПОЛНЫЙ снимок остатков склада на момент запроса (не дельта), поэтому ERP сама считает исчезновение позиции из ответа сигналом "остаток обнулился" (см. Обнуление остатков — позиция пропала из ответа). Это верно для всех протоколов (hs/file/будущий epf) — источник строк (HTTP-ответ, CSV, JSON push) не важен, обнуление считается по одному и тому же правилу.

POST /hs/gtd_numbers/writeoff — списание материалов

Вызывается ERP автоматически при переходе процесса в статус СОГЛАСОВАНО (sync1c:processType.<id>.statusWriteoff).

Запрос:

{
  "process_id": 12345,
  "date": "2026-02-26",
  "warehouse_id": "228a5e7e-c13c-11f0-b829-005056ba50ab",
  "items": [
    { "product_id": "d776ab8b-ca60-11ee-bba6-f8f082350659", "gtd_id": "07cd97c7-019a-11f1-b829-005056ba50ab", "quantity": 3.0 },
    { "product_id": "a1b2c3d4-0000-0000-0000-000000000001", "quantity": 10.0 }
  ]
}

gtd_id — необязателен. Ответ — тот же общий формат, "data": null при успехе.

Требуется идемпотентность на стороне 1С: повторный вызов с тем же process_id должен возвращать успех («уже списано»), не ошибку — ERP автоматически повторяет отправку при сбоях (см. Экспорт списания при СОГЛАСОВАНО).

Использование

Импорт остатков (Фаза 1 — с подтверждением)

1С присылает АБСОЛЮТНЫЕ остатки (не дельты). Импорт (см. Импорт остатков) пишет их не сразу в баланс, а в inventory_sync1c_import_pending со статусом pending — кто-то с доступом к складу должен явно подтвердить или отклонить каждую позицию:

  • Пункт меню «Поступления» (пользователь, /user/plugin/inventory/import_pending) — список pending-позиций для складов, к которым у текущего пользователя есть доступ: он владелец склада (store.user_id), у него есть индивидуальная запись inventory_store_access, или он состоит в группе, указанной у склада как «группа редактирования» (edit_group_id) — см. доступ к складам. Так несколько «кладовщиков» могут делить обязанности по одному складу — не обязательно быть его формальным владельцем, чтобы увидеть и провести чужую загрузку.

  • Провести — записывает абсолютное значение в inventory_balance.quantity (не трогая reserved), плюс пишет строку в inventory_movement (type=adjustment, количество = разница «стало минус было», автор — кто нажал «Провести») — см. Паттерн append-only + ref_movement_id и Движения ТМЦ в основном документе. Так изменение видно в уже привычной ленте движений склада, а не только мгновение в списке pending.

  • Провести всё — массово, то же самое для каждой позиции.

  • Отклонить — помечает rejected, баланс не трогается, движение не пишется.

И apply, и reject дополнительно проверяют на сервере, что конкретная pending-запись действительно относится к складу, доступному вызывающему (не только видна в списке) — id записи недостаточно, чтобы провести/отклонить чужую.

Повторный импорт той же позиции (уникальный ключ ew_id + item_id) сбрасывает её обратно в pending, независимо от прежнего статуса (applied/rejected) — сброс идёт при каждом новом импорте, даже если предыдущая загрузка ещё не была проведена (новое количество полностью затирает предыдущее ожидающее значение, они не складываются). Это верно для обоих протоколов — источник строк (HTTP-ответ или CSV-файл) не влияет на дальнейшую обработку.

Админка (Синхронизация с 1С → Ожидающие поступления) даёт тот же функционал для любого инстанса (без ограничения по доступу — админ видит всё), плюс Лог импортов — история запусков со статусом/количеством позиций/текстом ошибки (но без разбивки по товарам и без указания, кто загрузил — за подробностями по конкретному товару и правил движения смотреть саму ленту «Движения ТМЦ» склада, см. выше).

Обнуление остатков — позиция пропала из ответа

Раз 1С не присылает обнулившиеся позиции явно (см. POST /hs/gtd_numbers/stocks — получение остатков), ERP сама детектирует исчезновение: ImportService.importFromRows() после обработки всех полученных строк вычисляет ImportPendingDAO.getKnownItemIds(ewId, storeId) — объединение (a) позиций с quantity > 0 в inventory_balance для этого склада и (b) позиций, всё ещё ожидающих подтверждения (status='pending') по этому маппингу — и для каждой, отсутствующей в текущем ответе, вызывает тот же upsert(…​) с quantity=0. Дальше — обычный флоу подтверждения: обнулённая позиция появляется в «Поступлениях» как pending-запись с количеством 0, хозяин склада жмёт «Провести» (обнуляет inventory_balance.quantity) или «Отклонить» (считает, что 1С ошиблась/позиция ещё физически есть).

Работает и для полностью пустого ответа (data: []) — в этом случае обнуляются вообще все позиции, когда-либо известные для этого склада. Единственный случай, когда обнуление не считается: самый первый импорт склада пришёл пустым (склад ещё не создан, ew.storeId=0) — тут обнулять нечего.

Правило одинаково для всех протоколов (hs/file/будущий epf) — реализовано в протокол-агностичном importFromRows(), а не в HTTP-специфичном коде.

Экспорт списания при СОГЛАСОВАНО

  1. Процесс переходит в статус, указанный в sync1c:processType.<id>.statusWriteoffSync1cExportListener ставит запись в очередь inventory_sync1c_export_queue (upsert по process_id + instance_id — повторный вход в статус сбрасывает существующую запись обратно в pending). Это общий шаг для обоих протоколов.

  2. Протокол hsExportPoller (раз в минуту) забирает pending-записи инстансов с protocol='hs', отправляет /writeoff со всеми активными резервами процесса, при успехе создаёт writeoff-движения (уменьшают quantity и reserved) и помечает запись success. При ошибке — failed (до sync1c:export.maxAttempts попыток, видно в админке с текстом ошибки, кнопка Повторить). Перед финальной записью результата поллер перепроверяет, что запись всё ещё pending — если её успели отменить конкурентно (см. ниже), отправка/запись результата пропускается.

  3. Протокол fileExportPoller эти записи не трогает вообще (см. Планировщик). В Очередь экспорта доступна кнопка Скачать CSV (queue_id,process_id,warehouse_id,product_id,gtd_id,quantity, по строке на позицию — один queue_id может дать несколько строк) — кладовщик проводит списание в 1С вручную по этому файлу, затем в ERP жмёт Подтвердить (по одной записи) или Подтвердить всё (все pending записи инстанса разом). Подтверждение делает то же самое, что успешный ExportPoller: создаёт writeoff-движения и переводит запись в success — retry/attempts не применяются, т.к. нет HTTP-вызова, который мог бы упасть.

Каждая попытка отправки/подтверждения (оба протокола) пишет строку в inventory_sync1c_export_log (Синхронизация с 1С → Лог экспорта) — instance/process/queue id, время начала/завершения, количество позиций, статус (running/ok/error) и текст ошибки. Аналог import_log, но для экспорта; полезно для диагностики "почему списание не ушло" без необходимости лезть в bgerp.log.

Матрица статусов очереди экспорта

Статус Значение

pending

Ждёт отправки (или переотправки после сброса)

success

Списание успешно отправлено в 1С, ERP-движения writeoff созданы

failed

Ошибка при отправке (текст — last_error), будет повторено поллером до лимита попыток

cancelled

Процесс вернули «на доработку» до фактической отправки — см. Сценарий «На доработку» — отмена ещё не отправленного экспорта

reversed

Списание было отправлено (success), но процесс потом вернули из СОГЛАСОВАНО и списание реверснуто в ERP — см. Сценарий «Возврат из Согласовано» — реверс уже отправленного списания

Сценарий «На доработку» — отмена ещё не отправленного экспорта

Процесс вернули на доработку до того, как ExportPoller успел забрать pending-запись (окно — до минуты). Монтажник отменяет резерв (unreserve) — ExportQueueDAO.cancelPendingByProcessId() переводит запись pending → cancelled (записи success/failed не трогает). При повторном переходе в СОГЛАСОВАНО запись пересоздаётся штатным upsert, экспорт уходит заново.

Сценарий «Возврат из Согласовано» — реверс уже отправленного списания

Решение по продукту: количество материалов ведёт ERP, состояние 1С после реверса — не забота ERP («монтажник может делать что хочет со своим рюкзаком, дальнейшая сверка с 1С — на стороне 1С»).

Если процесс покидает статус СОГЛАСОВАНО (в любую сторону, не обязательно «Новый» — конкретно у типа 45 из статусной матрицы явно разрешён переход 14→1), а по нему уже есть success-запись экспорта — Sync1cExportListener вызывает ExportService.reverseWriteoffMovements():

  1. Для каждого writeoff-движения процесса с ext_ref='sync1c' создаётся receipt-движение на ту же номенклатуру/склад/количество (НЕ return — тот дополнительно уменьшает reserved, что задело бы чужие резервы на этом складе/номенклатуре, никак не связанные с данным процессом).

  2. Все success-записи очереди экспорта процесса переводятся в reversed.

  3. Вкладка «Материалы» автоматически разблокируется — блокировка по «уже экспортировано» смотрит только на status='success' (см. Inventory).

Реверс покрывает весь накопленный по процессу writeoff, а не только последнее движение — если списаний было несколько (например, экспорт срабатывал повторно после промежуточных откатов), реверснутся все ещё не реверснутые.

Без проверки на отрицательный баланс и без обращения к 1С за подтверждением — намеренно, по решению выше. receipt физически не может увести баланс в минус (только прибавляет), так что защита и не нужна.

Разработка

Структура файлов

src/org/bgerp/plugin/inventory/sync1c/
├── Config.java                    — sync1c:enable, processTypeIds, processType.<id>.statusWriteoff, export.maxAttempts
├── action/
│   ├── AdminSyncAction.java       — /admin/plugin/sync1c/instance (инстансы, маппинг, импорт/экспорт для
│   │                                обоих протоколов); importFileForm/importFilePreview/importFileConfirm
│   │                                (протокол file) + getAccessRestriction() — та же схема доступа
│   │                                (store_access ∪ владелец ∪ edit_group_id), маркер "полный админ" —
│   │                                владение /admin/plugin/sync1c/instance:null
│   └── UserImportPendingAction.java — /user/plugin/inventory/import_pending; getAccessibleStoreIds()
│                                      (владелец ∪ store_access ∪ edit_group_id) вместо жёсткой привязки
│                                      к EngineerWarehouse.userId
├── dao/
│   ├── InstanceDAO, EngineerWarehouseDAO, SyncLogDAO, ExportLogDAO
│   ├── ImportPendingDAO           — upsert/get/getListForStores/getListForInstance/apply/applyAll/reject/
│   │                                getKnownItemIds; apply() дополнительно пишет audit-строку в
│   │                                inventory_movement (type=adjustment), см. <<usage-import>>
│   └── ExportQueueDAO             — add/get/getPendingByInstance/getPendingByProtocol/isPending/finalizePendingStatus/hasSuccessExport/cancelPendingByProcessId/markReversed
├── event/
│   └── Sync1cExportListener.java  — ProcessChangedEvent → enqueue() при входе в writeoff-статус, reverseWriteoffMovements() при выходе с success-экспортом
├── exec/
│   ├── ImportPoller.java          — не используется по расписанию сейчас, импорт триггерится вручную
│   └── ExportPoller.java          — scheduler task, раз в минуту, только protocol='hs' (getPendingByProtocol)
├── service/
│   ├── Api1cClient.java           — HTTP-клиент (/stocks, /writeoff), Basic Auth — только протокол hs
│   ├── ImportService.java         — syncEngineerWarehouse() (hs, вызывает Api1cClient) и importFromRows() (протокол-агностичное ядро: upsert item/store, import_pending) — оба протокола делегируют сюда
│   ├── ExportService.java         — enqueue/sendExportQueue/buildWriteoffItems (переиспользуется CSV-экспортом)/createWriteoffMovements/reverseWriteoffMovements
│   └── CsvUtil.java               — минимальный RFC4180 parse/write (UTF-8 + BOM), протокол file
└── model/
    ├── Sync1cConnector             — + protocol (PROTOCOL_HS/PROTOCOL_FILE, "epf" зарезервирован), EngineerWarehouse, ImportLog
    ├── ImportPending, ExportQueue (STATUS_PENDING/SUCCESS/FAILED/CANCELLED/REVERSED), ExportLog
    └── Api1cStock                 — @JsonIgnoreProperties(ignoreUnknown=true), маппинг ответа /stocks (и CSV-строк для протокола file)

webapps/WEB-INF/jspf/user/plugin/inventory/
├── import_pending.jsp             — «Ожидающие поступления» (доступ по store_access/владелец/edit_group_id)
└── store_card.jsp                 — кнопка "Загрузить CSV" в разделе "Остатки на складе" (лежит в
                                      основном плагине, не в sync1c, ссылается на форму ниже с storeId+userTier)

webapps/WEB-INF/jspf/admin/plugin/sync1c/
├── instance_list.jsp, instance_edit.jsp (+ поле protocol)
├── mapping_list.jsp (импорт-пункт меню зависит от protocol), mapping_edit.jsp — кнопки загрузки CSV
│   здесь больше нет, см. <<setup-protocol-file>> про новые точки входа
├── import_file_upload.jsp         — файл→склад→превью→подтверждение (протокол file); параметр
│                                     storeId — предвыбор склада, userTier — куда вести после
│                                     подтверждения (user-tier "Ожидающие поступления" или admin)
├── import_pending.jsp, import_log.jsp
├── export_queue.jsp               — + "Скачать CSV"/"Подтвердить"/"Подтвердить всё" для протокола file
└── export_log.jsp                 — история попыток экспорта (оба протокола), см. <<dev-db>>

Кнопка "Загрузить CSV" продублирована также на admin/plugin/inventory/store/balance.jsp и
admin/plugin/inventory/movement/list.jsp (основной плагин, не sync1c) — три равнозначные точки
входа на одну и ту же форму, см. <<setup-protocol-file>>.

Таблицы БД

Таблица Назначение

inventory_sync1c_connector

Подключения к базам 1С (url, credentials, ext_id param ids)

inventory_sync1c_engineer_warehouse

Маппинг монтажник (user_id) ↔ склад в 1С ↔ склад в ERP

inventory_sync1c_import_pending

Остатки от 1С, ждущие подтверждения (UNIQUE ew_id, item_id)

inventory_sync1c_import_log

История запусков импорта

inventory_sync1c_export_queue

Очередь экспорта списаний (UNIQUE process_id, instance_id), status — VARCHAR(20), см. Экспорт списания при СОГЛАСОВАНО

inventory_sync1c_export_log

История попыток отправки списания — одна запись на каждый прогон ExportPoller (протокол hs) или ручное "Подтвердить" (протокол file), см. Экспорт списания при СОГЛАСОВАНО

Паттерн append-only + ref_movement_id

Как и весь inventory_movement — записи только добавляются. Связь «это движение — следствие того движения» — через ref_movement_id:

«Активная» reserve-строка — та, на которую не ссылается ни один unreserve/writeoff (см. MovementDAO.getListByProcess). Отмена уже списанного резерва отдельно защищена — MovementDAO.isActiveReserve() отклоняет повторный/устаревший unreserve.

Отладка

grep "Sync1cExportListener\|ExportService\|ExportPoller" log/bgerp.log
grep "ImportService" log/bgerp.log
-- Очередь экспорта конкретного процесса
SELECT * FROM inventory_sync1c_export_queue WHERE process_id=?;

-- Pending-остатки монтажника
SELECT * FROM inventory_sync1c_import_pending WHERE ew_id=? AND status='pending';

-- Проверить, что интеграция включена и куда смотрит writeoff-статус
SELECT data FROM config_global WHERE data LIKE '%sync1c:%';
Проблема Причина Решение

Списание не уходит в 1С

ExportPoller не запущен или запись не pending

Проверить scheduler.task.sync1c_export.* в bgerp.properties, статус записи в inventory_sync1c_export_queue

Вкладка «Материалы» не блокируется после экспорта

Нет success-записи, либо она уже reversed

SELECT status FROM inventory_sync1c_export_queue WHERE process_id=?

Форма «Материалы» не разблокируется после возврата из Согласовано

Sync1cExportListener не сработал (например, статус меняли напрямую в БД, минуя ProcessChangedEvent)

Менять статус только через штатный /user/process:processStatusUpdate, не прямым UPDATE

Импорт создаёт дубли номенклатуры/складов

item_ext_id_param_id/store_ext_id_param_id не заданы или указывают на несуществующий параметр

Проверить настройки инстанса, что параметр реально существует и заполняется