В процессе разработки конфигураций на платформе 1С:Предприятие часто возникает задача синхронизации данных между связанными объектами. Типичный пример: в документе есть табличная часть, где пользователь выбирает изделие (или номенклатуру), и система должна автоматически подтянуть актуальную цену этого изделия из другого документа или справочника. Но что делать, если цена в объекте-источнике изменилась после того, как документ-приемник был заполнен или открыт? Как правильно отследить это изменение и обновить данные?
В этой статье мы подробно разберем, почему прямая привязка реквизитов документов друг к другу является архитектурной ошибкой, и рассмотрим методически верные паттерны проектирования, которые используются в профессиональной разработке на платформе 1С:Предприятие 8.
Попытка «на лету» отслеживать изменения в одном документе и сразу же перезаписывать данные в других связанных документах приводит к ряду серьезных проблем:
Выясним, как решить эту задачу правильно с точки зрения архитектуры 1С.
---
Связывать цену в документе напрямую с реквизитом другого документа или справочника методически неверно. Для хранения цен, тарифов и других изменяющихся во времени характеристик в 1С всегда используются периодические регистры сведений (например, ЦеныНоменклатуры).
Рассмотрим по шагам, как должна работать эта схема:
Посмотрим на пример реализации получения цены на клиенте при изменении реквизита табличной части:
&НаКлиенте
Процедура ТоварыИзделиеПриИзменении(Элемент)
СтрокаТЧ = Элементы.Товары.ТекущиеДанные;
Если СтрокаТЧ <> Неопределено Тогда
// Получаем актуальную цену неконтекстным серверным вызовом
СтрокаТЧ.Цена = ПолучитьЦенуИзделияНаСервере(СтрокаТЧ.Изделие, Объект.Дата);
// Пересчитываем сумму строки
ПересчитатьСуммуСтроки(СтрокаТЧ);
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьЦенуИзделияНаСервере(Знач СсылкаНаИзделие, Знач ДатаДокумента)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныИзделийСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныИзделий.СрезПоследних(&Период, Изделие = &Изделие) КАК ЦеныИзделийСрезПоследних";
Запрос.УстановитьПараметр("Период", ДатаДокумента);
Запрос.УстановитьПараметр("Изделие", СсылкаНаИзделие);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Цена;
КонецЕсли;
Возврат 0;
КонецФункции
---
Проанализируем ситуацию: пользователь открыл документ в 9:00, заполнил его по актуальным ценам, но оставил открытым и ушел на обед. В 12:00 другой менеджер утвердил новые прайс-листы. В 14:00 первый пользователь вернулся и нажал кнопку «Провести». На этот момент цены в документе уже неактуальны.
Чтобы избежать коллизий, методически верно выполнять финальную проверку цен непосредственно в транзакции записи/проведения документа на сервере. Если цены в табличной части не совпадают с актуальными ценами в базе данных, транзакцию необходимо отменить и выдать пользователю информативное сообщение.
Запрограммируем этот контроль в модуле объекта документа:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Выполняем проверку только при проведении документа
Если РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
Для Каждого СтрокаТЧ Из Товары Цикл
// Получаем актуальную цену на момент проведения
АктуальнаяЦена = ПолучитьЦенуИзделияНаСервере(СтрокаТЧ.Изделие, Дата);
Если СтрокаТЧ.Цена <> АктуальнаяЦена Тогда
// Выводим сообщение пользователю и отменяем проведение
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Внимание! Цена по изделию """ + СтрокаТЧ.Изделие + """ изменилась в базе данных. " +
"В документе: " + СтрокаТЧ.Цена + ", Актуальная цена: " + АктуальнаяЦена + ". " +
"Пожалуйста, обновите цены в документе.";
Сообщение.Поле = "Товары[" + (Товары.Индекс(СтрокаТЧ)) + "].Цена";
Сообщение.УстановитьДанные(ЭтотОбъект);
Сообщение.Сообщить();
Отказ = Истина; // Отмена транзакции
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
В этом случае мы гарантируем, что в систему не попадут некорректные или устаревшие данные, а пользователь получит четкое указание, в какой именно строке возникло расхождение.
---
Если вам все же требуется предоставить пользователю возможность интерактивно реагировать на изменения в реальном времени (например, если документы редактируются параллельно в рамках одного сеанса), мы можем использовать встроенный в платформу 1С механизм оповещений.
Для реализации этого сценария применим связку методов Оповестить() и ОбработкаОповещения():
Шаг 1. В форме документа-источника (где меняется цена) в обработчике события ПослеЗаписи отправляем глобальное оповещение по всей системе:
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
// Оповещаем систему, что цена конкретного изделия изменилась
Оповестить("ИзменениеЦеныИзделия", Объект.Ссылка);
КонецПроцедуры
Шаг 2. В форме нашего документа-приемника настраиваем обработчик события формы ОбработкаОповещения. Данное событие срабатывает на клиенте при вызове метода Оповестить в любой другой форме:
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
Если ИмяСобытия = "ИзменениеЦеныИзделия" Тогда
// Параметр содержит ссылку на измененное изделие
СсылкаНаИзделие = Параметр;
// Проверяем, есть ли это изделие в нашей табличной части
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
Если СтрокаТЧ.Изделие = СсылкаНаИзделие Тогда
// Запрашиваем новую цену с сервера и обновляем форму
СтрокаТЧ.Цена = ПолучитьЦенуИзделияНаСервере(СсылкаНаИзделие, Объект.Target);
ПересчитатьСуммуСтроки(СтрокаТЧ);
ПоказатьОповещениеПользователя("Обновление данных", , "Цена изделия была успешно обновлена!");
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Этот метод отлично работает в рамках одного клиентского приложения (одного сеанса пользователя). Если же пользователи работают в разных сеансах (на разных компьютерах), для аналогичного интерактивного взаимодействия рекомендуется использовать современный механизм платформы — Систему взаимодействия.
---
Подводя итоги, сформулируем основные правила работы с зависимыми реквизитами в 1С:
ОповеститьОбИзменении() или обработку оповещений, избегая жестких автоматических перезаписей данных в фоне.