При проектировании бизнес-логики в системе «1С:Предприятие 8» часто возникает задача контроля заполнения связанных документов. Типичный пример: подчиненный документ создается на основании базового документа, и система должна гарантировать, что пользователь внес в него хотя бы какие-то изменения перед проведением. Если подчиненный документ остался точной копией исходного, его проведение бессмысленно и может привести к задвоению данных или логическим ошибкам в учете.
В этой статье мы подробно разберем, как реализовать такую проверку. Мы рассмотрим различные подходы: от самого простого ручного сравнения до продвинутых автоматизированных методов с использованием метаданных, механизмов БСП (Библиотеки стандартных подсистем) и сериализации данных.
Самый очевидный и простой в реализации способ — это явное прописание проверки для каждого значимого реквизита в модуле документа. Этот подход отлично подходит для небольших документов, содержащих всего несколько полей шапки.
Давайте реализуем проверку в обработчике ПередЗаписью модуля документа. Нам необходимо убедиться, что документ записывается в режиме проведения, проверить наличие заполненного документа-основания и выполнить сравнение полей.
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Проверку выполняем только при проведении документа
Если РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
// Проверяем, заполнено ли поле документа-основания
Если Не ЗначениеЗаполнено(ДокументОснование) Тогда
Возврат;
КонецЕсли;
// Выполняем сравнение ключевых реквизитов
Если Организация = ДокументОснование.Организация
И Контрагент = ДокументОснование.Контрагент
И СуммаДокумента = ДокументОснование.СуммаДокумента Тогда
// Если все ключевые параметры равны, отменяем проведение и информируем пользователя
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Не удалось провести документ! Параметры документа полностью совпадают с документом-основанием. Пожалуйста, внесите изменения.";
Сообщение.Сообщить();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Преимущества метода: простота написания и высокая скорость работы на уровне платформы.
Недостатки метода: жесткая привязка к конкретным именам полей. Если в будущем в конфигурацию будут добавлены новые реквизиты, разработчику придется вручную дописывать их проверку в код. Также данный метод неэффективен, если документ содержит табличные части.
Для того чтобы сделать наш код универсальным и устойчивым к будущим изменениям структуры данных, применим динамический обход метаданных. Платформа «1С:Предприятие 8» позволяет программно получить список всех реквизитов объекта и сравнить их значения в цикле.
Давайте напишем вспомогательную функцию, которая будет автоматически сопоставлять значения реквизитов текущего документа с реквизитами документа-основания:
Функция ДокументСовпадаетСОснованием(ТекущийОбъект) Экспорт
СсылкаОснования = ТекущийОбъект.ДокументОснование;
МетаданныеДокумента = ТекущийОбъект.Метаданные();
МетаданныеОснования = СсылкаОснования.Метаданные();
// Список системных реквизитов, которые мы исключаем из сравнения
ИсключаемыеСвойства = Новый Массив;
ИсключаемыеСвойства.Добавить("Ссылка");
ИсключаемыеСвойства.Добавить("ВерсияДанных");
ИсключаемыеСвойства.Добавить("Дата");
ИсключаемыеСвойства.Добавить("Номер");
ИсключаемыеСвойства.Добавить("Проведен");
ИсключаемыеСвойства.Добавить("ПометкаУдаления");
ИсключаемыеСвойства.Добавить("ДокументОснование");
// Шаг 1. Проверяем стандартные реквизиты
Для Каждого Реквизит Из МетаданныеДокумента.СтандартныеРеквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
Если ИсключаемыеСвойства.Найти(ИмяРеквизита) <> Неопределено Тогда
Продолжить;
КонецЕсли;
// Проверяем, есть ли аналогичный стандартный реквизит у основания
Попытка
ЗначениеТекущее = ТекущийОбъект[ИмяРеквизита];
ЗначениеОснования = СсылкаОснования[ИмяРеквизита];
Если ЗначениеТекущее <> ЗначениеОснования Тогда
Возврат Ложь; // Найдено различие
КонецЕсли;
Исключение
// Если у основания нет такого реквизита, просто пропускаем проверку по нему
КонецПопытки;
КонецЦикла;
// Шаг 2. Проверяем пользовательские реквизиты шапки
Для Каждого Реквизит Из МетаданныеДокумента.Реквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
Если ИсключаемыеСвойства.Найти(ИмяРеквизита) <> Неопределено Тогда
Продолжить;
КонецЕсли;
// Ищем реквизит в метаданных документа-основания
РеквизитОснования = МетаданныеОснования.Реквизиты.Найти(ИмяРеквизита);
Если РеквизитОснования <> Неопределено Тогда
ЗначениеТекущее = ТекущийОбъект[ИмяРеквизита];
ЗначениеОснования = СсылкаОснования[ИмяРеквизита];
Если ЗначениеТекущее <> ЗначениеОснования Тогда
Возврат Ложь; // Найдено различие
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Истина; // Все проверяемые реквизиты идентичны
КонецФункции
Этот алгоритм автоматически адаптируется к любым изменениям состава реквизитов. Если вы добавите новое поле в конфигураторе, оно сразу же начнет участвовать в проверке без изменения программного кода.
Если ваша конфигурация построена на базе современных типовых решений фирмы «1С» (таких как 1С:ERP, УТ 11, КА 2, Бухгалтерия предприятия 3.0 и др.), то в вашем распоряжении имеется мощная Библиотека стандартных подсистем (БСП).
В составе БСП есть специализированный общий модуль, предоставляющий оптимизированный инструмент для сравнения объектов. Мы можем использовать готовую функцию ОбменДаннымиСобытия.ДанныеРазличаются. Этот метод берет на себя всю черновую работу, включая корректное сопоставление типов данных и проверку ссылочной целостности.
Пример вызова функции БСП в модуле объекта:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если РежимЗаписи = РежимЗаписиДокумента.Проведение
И ЗначениеЗаполнено(ДокументОснование) Тогда
СписокСвойств = ""; // Сравниваем все свойства
ИсключаяСвойства = "Ссылка, Номер, Дата, Проведен, ПометкаУдаления, ДокументОснование";
// Метод возвращает Истина, если объекты имеют различия
ДанныеРазличаются = ОбменДаннымиСобытия.ДанныеРазличаются(
ЭтотОбъект,
ДокументОснование,
СписокСвойств,
ИсключаяСвойства
);
Если Не ДанныеРазличаются Тогда
Отказ = Истина;
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
"Документ ничем не отличается от документа-основания. Проведение невозможно."
);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Использование БСП значительно сокращает объем написанного кода и гарантирует стабильную работу системы, так как эти механизмы тщательно протестированы разработчиками фирмы «1С» на миллионах информационных баз.
Очень часто документы содержат табличные части (например, список номенклатуры, услуг, цен и количеств). Обычное сравнение через операторы равенства в данном случае неприменимо, поскольку порядок строк в табличных частях подчиненного и базового документов может различаться, хотя состав строк может быть абсолютно одинаковым.
Рассмотрим надежный способ сравнения табличных частей путем выгрузки их в объекты типа ТаблицаЗначений, последующей сортировки и построчного сопоставления.
Функция ТабличныеЧастиИдентичны(ТабличнаяЧасть1, ТабличнаяЧасть2, СписокКолонок)
// Шаг 1. Выгружаем данные табличных частей во временные таблицы значений
Таблица1 = ТабличнаяЧасть1.Выгрузить(, СписокКолонок);
Таблица2 = ТабличнаяЧасть2.Выгрузить(, СписокКолонок);
// Шаг 2. Если количество строк разное, таблицы гарантированно различаются
Если Таблица1.Количество() <> Таблица2.Количество() Тогда
Возврат Ложь;
КонецЕсли;
// Шаг 3. Сортируем обе таблицы по ключевым колонкам для приведения к общему виду
Таблица1.Сортировать(СписокКолонок);
Таблица2.Сортировать(СписокКолонок);
// Шаг 4. Построчно сравниваем значения ячеек
Для Индекс = 0 По Таблица1.Количество() - 1 Цикл
Строка1 = Таблица1[Индекс];
Строка2 = Таблица2[Индекс];
Для Каждого ИмяКолонки Из СтрРазделить(СписокКолонок, ",") Цикл
ИмяКолонкиОчищенное = СокрЛП(ИмяКолонки);
Если Строка1[ИмяКолонкиОчищенное] <> Строка2[ИмяКолонкиОчищенное] Тогда
Возврат Ложь; // Найдено несовпадение в конкретной ячейке
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат Истина; // Таблицы полностью совпадают
КонецФункции
Для вызова этой функции мы передаем в нее табличные части и перечень колонок, которые важны для бизнес-логики (например, "Номенклатура, Количество, Цена, Сумма").
Если вам нужно быстро сравнить два объекта целиком (включая все их табличные части), но при этом вы не хотите писать громоздкие циклы обхода метаданных, можно прибегнуть к сериализации данных. Мы можем преобразовать структуры обоих документов в текстовый формат (например, JSON) и сравнить полученные строки.
Для корректного сравнения нам потребуется исключить уникальные системные идентификаторы, даты и номера, которые гарантированно будут отличаться у нового документа. Сделать это можно путем формирования структуры значимых данных.
Функция ПолучитьСериализованныеДанные(ДокументОбъект)
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
// Формируем структуру, содержащую только бизнес-данные
ДанныеДляСравнения = Новый Структура;
ДанныеДляСравнения.Вставить("Организация", ДокументОбъект.Организация);
ДанныеДляСравнения.Вставить("Контрагент", ДокументОбъект.Контрагент);
ДанныеДляСравнения.Вставить("СуммаДокумента", ДокументОбъект.СуммаДокумента);
// Выгружаем табличную часть "Товары" в массив структур
МассивТоваров = Новый Массив;
Для Каждого СтрокаТЧ Из ДокументОбъект.Товары Цикл
СтруктураСтроки = Новый Структура("Номенклатура, Количество, Цена",
СтрокаТЧ.Номенклатура, СтрокаТЧ.Количество, СтрокаТЧ.Цена);
МассивТоваров.Добавить(СтруктураСтроки);
КонецЦикла;
ДанныеДляСравнения.Вставить("Товары", МассивТоваров);
// Сериализуем подготовленную структуру в JSON-строку
ЗаписатьJSON(ЗаписьJSON, ДанныеДляСравнения);
Возврат ЗаписьJSON.Закрыть();
КонецФункции
После получения сериализованных строк для обоих документов сравнение сводится к одной простой строчке кода: Если СтрокаОбъекта = СтрокаОснования Тогда .... Это изящное и современное решение для комплексных задач контроля уникальности данных.