Разберем типичную ситуацию, с которой сталкиваются многие разработчики в 1С. Представьте: вы доработали форму документа, добавили новый реквизит Прибыльность в табличную часть Товары и теперь вам нужно массово пересчитать этот показатель для всех старых документов в базе. Вы создаете внешнюю обработку, пишете код, нажимаете кнопку — и ничего не происходит. Ошибок нет, но и результат нулевой. Проанализируем ситуацию и выясним причину такого поведения системы.
Для начала посмотрим на пример кода, который часто пишут разработчики для решения этой задачи:
Выборка = Документы.ЗаказКлиента.Выбрать;
Пока Выборка.Следующий Цикл
ВыборкаОбъект = Выборка.ПолучитьОбъект;
Для каждого Товар Из ВыборкаОбъект.Товары Цикл
Если ЗначениеЗаполнено(Товар.СуммаЗакупки) И ЗначениеЗаполнено(Товар.СуммаПродажи) Тогда
СуммаЗТ = Товар.СуммаЗакупки;
СуммаПТ = Товар.СуммаПродажи;
Товар.Прибыльность = СуммаЗТ/СуммаПТ*100;
ВыборкаОбъект.Записать;
КонецЕсли;
КонецЦикла;
КонецЦикла;
На первый взгляд, алгоритм кажется логичным. Однако он содержит комплекс ошибок, связанных как с архитектурой платформы, так и с логикой обработки данных. Разберем по шагам каждую из них.
Основная причина того, что при нажатии кнопки "ничего не происходит", заключается в архитектуре современного приложения 1С. Если вы работаете в конфигурациях вроде "Управление торговлей", "ERP" или "Комплексная автоматизация", вы используете Управляемые формы.
По умолчанию обработчик нажатия кнопки на форме создается с директивой компиляции &НаКлиенте. На клиенте нельзя обращаться напрямую к базе данных. Любые вызовы менеджеров объектов, такие как Документы.ЗаказКлиента, на клиенте недоступны. Платформа часто молча игнорирует такие конструкции или обрывает выполнение, если не настроен явный перехват исключений. Мы должны разделить логику: на клиенте мы нажимаем кнопку, а тяжелую работу с базой данных выполняем на сервере.
Создадим правильную структуру вызовов:
&НаКлиенте
Процедура ПересчитатьПрибыльность(Команда)
ПересчитатьПрибыльностьНаСервере();
ПоказатьОповещениеПользователя("Готово",, "Пересчет завершен!");
КонецПроцедуры
&НаСервереБезКонтекста
Процедура ПересчитатьПрибыльностьНаСервере()
// Здесь будет наш основной код
КонецПроцедуры
Посмотрим на первую строку исходного кода: Выборка = Документы.ЗаказКлиента.Выбрать;. В платформе 1С для вызова методов и функций обязательно использование круглых скобок. Правильный вариант записи выглядит так: Выборка = Документы.ЗаказКлиента.Выбрать();. Без скобок система не формирует выборку, переменная не инициализируется должным образом, и программа просто перепрыгивает цикл.
Рассмотрим внутренний цикл кода. Автор поместил команду ВыборкаОбъект.Записать() внутрь цикла перебора строк табличной части. Если в документе 100 товаров, документ будет перезаписан в базу данных 100 раз подряд. Это колоссальная и бессмысленная нагрузка на сервер.
Метод записи должен вызываться только один раз для каждого документа — после того, как все строки табличной части пересчитаны. Вынесем эту команду из внутреннего цикла во внешний.
Проанализируем формулу из примера: СуммаЗТ/СуммаПТ*100. С точки зрения экономики, это расчет доли себестоимости в конечной цене, а не прибыльности (рентабельности). Чтобы рассчитать реальную прибыльность продаж (маржинальность), формула должна вычислять долю прибыли в выручке.
Корректная формула выглядит так: (СуммаПТ - СуммаЗТ) / СуммаПТ * 100.
При этом стоит отметить грамотный подход к проверке: ЗначениеЗаполнено(Товар.СуммаПродажи). Эта функция вернет Ложь, если сумма равна нулю. Это отличная практика, которая защищает наш алгоритм от критической ошибки «Деление на ноль».
Обычный вызов ВыборкаОбъект.Записать() просто сохраняет данные в базу. Однако, если добавленный реквизит Прибыльность как-то влияет на движения документа (например, данные должны попасть в регистры накопления для построения отчетов), документ необходимо перепровести.
Для этого укажем режим записи явно: ВыборкаОбъект.Записать(РежимЗаписиДокумента.Проведение);.
Использовать метод Документы.ЗаказКлиента.Выбрать() для массового изменения — это плохая практика. Платформа начнет поднимать из базы абсолютно все документы за всю историю ведения учета, даже те, которые закрыты периодом или уже имеют заполненную прибыльность.
Применим более профессиональный подход — используем объект Запрос. Запросом мы выберем только те документы, которые действительно нужно пересчитать.
При массовом обновлении данных велик шанс наткнуться на документ, который прямо сейчас открыт менеджером. Метод ПолучитьОбъект() в таком случае выдаст ошибку блокировки, и наша обработка полностью прервется. Обернем получение объекта и запись в конструкцию Попытка ... Исключение, чтобы обработка просто пропускала занятые документы.
Соберем все наши улучшения и напишем качественный, безопасный и оптимизированный код. Рассмотрим итоговый вариант процедур:
&НаКлиенте
Процедура ПересчитатьПрибыльность(Команда)
ПересчитатьПрибыльностьНаСервере();
ПоказатьОповещениеПользователя("Обработка завершена",, "Пересчет документов выполнен.");
КонецПроцедуры
&НаСервереБезКонтекста
Процедура ПересчитатьПрибыльностьНаСервере()
// Используем запрос для получения нужных документов (например, за текущий год)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказКлиента.Ссылка КАК ДокументСсылка
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента
|ГДЕ
| ЗаказКлиента.Дата >= &НачалоПериода
| И ЗаказКлиента.Проведен = ИСТИНА";
Запрос.УстановитьПараметр("НачалоПериода", НачалоГода(ТекущаяДата()));
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Попытка
// Получаем объект документа
ДокументОбъект = ВыборкаДетальныеЗаписи.ДокументСсылка.ПолучитьОбъект();
ДокументИзменен = Ложь;
// Перебираем строки табличной части
Для каждого ТекущаяСтрока Из ДокументОбъект.Товары Цикл
Если ЗначениеЗаполнено(ТекущаяСтрока.СуммаПродажи) И ЗначениеЗаполнено(ТекущаяСтрока.СуммаЗакупки) Тогда
НоваяПрибыльность = (ТекущаяСтрока.СуммаПродажи - ТекущаяСтрока.СуммаЗакупки) / ТекущаяСтрока.СуммаПродажи * 100;
// Проверяем, изменилось ли значение, чтобы лишний раз не перезаписывать
Если ТекущаяСтрока.Прибыльность <> НоваяПрибыльность Тогда
ТекущаяСтрока.Прибыльность = НоваяПрибыльность;
ДокументИзменен = Истина;
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Записываем документ и проводим его, ТОЛЬКО если были изменения
Если ДокументИзменен Тогда
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
Исключение
// Если документ заблокирован другим пользователем, пропускаем его
Сообщить("Не удалось обработать документ: " + ВыборкаДетальныеЗаписи.ДокументСсылка + ". Причина: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Таким образом, мы решили исходную проблему с неработающей кнопкой, перенеся код на сервер. Мы исправили синтаксис, оптимизировали обращение к базе данных с помощью запроса, вынесли запись объекта из внутреннего цикла и добавили защиту от блокировок. Этот алгоритм будет работать быстро, надежно и не приведет к зависанию базы данных.