Рассмотрим ситуацию, с которой часто сталкиваются начинающие разработчики или специалисты, работающие в условиях горящих сроков: написан код для расчета скидок перед записью документа, но он работает неправильно, содержит синтаксические ошибки и неоптимален с точки зрения архитектуры. Проанализируем типичные ошибки на конкретном примере, разберем по шагам каждую проблему и выясним причину, по которой код требует глубокого рефакторинга.
Посмотрим на пример процедуры расчета, которая вызывается в модуле формы документа перед его записью. Задача кода — определить сумму закупки, рассчитать размер скидки (либо по жестким порогам, либо из регистра сведений) и применить эту скидку к строкам табличной части.
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Если Контрагент <> Справочники.Контрагенты.ПустаяСсылка() Тогда
ЦенаСУчетомСкидки=0;
Если ОбъемЗакупки.Количество() > 0 Тогда
СуммаЗакупки = ОбъемЗакупки[0].Сумма;
Если СуммаЗакупки >= 20000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(2, 20000);
ИначеЕсли СуммаЗакупки >= 10000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(1, 10000);
ИначеЕсли СуммаЗакупки >= 5000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(2, 5000);
ИначеЕсли СуммаЗакупки >= 3000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(3, 3000);
ИначеЕсли СуммаЗакупки >= 1000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(1, 1000);
Иначе
РазмерСкидки = ПолучитьРазмерСкидкиПоРС(Контрагент);
ЦенаСУчетомСкидки = РазмерСкидки*СуммаЗакупки;
КонецЕсли;
КонецЕсли;
Если ЦенаСУчетомСкидки > 0 Тогда
НачислитьСкидку(ЦенаСУчетомСкидки);
Сообщить("У клиента с учетом скидки цена " + ЦенаСУчетомСкидки + " руб.");
КонецЕсли;
Иначе
Сообщить("Перед записью выберите контрагента.");
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура НачислитьСкидку(РазмерСкидки)
Для Каждого СтрокаТЧ Из Товары
СтрокаДок.Цена = РазмерСкидки;
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьРазмерСкидкиПоКоэффициенту(Вариант,СуммаЗакупки)
Если Вариант=1 Тогда
Коэффициент = 1;
ИначеЕсли Вариант=2 Тогда
Коэффициент = 0.9;
КонецЕсли;
Возврат Коэффициент*СуммаЗакупки;
КонецФункции
На первый взгляд логика понятна, но дьявол кроется в деталях. Разберем по шагам, что именно здесь не так и как это исправить.
Проанализируем первую строку условия: Если Контрагент <> Справочники.Контрагенты.ПустаяСсылка() Тогда. Вызов менеджера Справочники... в процедуре &НаКлиенте приводит к неявному и совершенно лишнему обращению к серверу. Платформе приходится отправлять запрос на сервер только ради того, чтобы получить пустую ссылку. Это замедляет работу формы, особенно при медленном интернет-соединении.
Как правильно: Используйте глобальную функцию ЗначениеЗаполнено(Контрагент). Она отрабатывает локально на клиенте и не требует серверного вызова. Если же вам действительно нужно получить пустую ссылку на клиенте, используйте ПредопределенноеЗначение("Справочник.Контрагенты.ПустаяСсылка").
Посмотрим на то, как определяется сумма закупки: СуммаЗакупки = ОбъемЗакупки[0].Сумма;. Разработчик берет сумму только из первой строки (с индексом 0) табличной части. Это грубая логическая ошибка, так как скидка на заказ должна рассчитываться от общего объема закупки (итоговой суммы всего документа), а не от стоимости одного первого товара.
Как правильно: Если нужна общая сумма по всей табличной части, следует использовать специализированный метод коллекции Итог(). Оптимальный код выглядит так: СуммаЗакупки = ОбъемЗакупки.Итог("Сумма");. Это штатный и самый быстрый способ получить общую сумму колонки без написания циклов.
Рассмотрим структуру Если ... ИначеЕсли. Мы видим использование конкретных чисел: 20000, 10000, 5000, а также коэффициентов 1 и 0.9 внутри функции. Использование таких значений прямо в коде — это антипаттерн "магические числа". При любом изменении маркетинговой политики компании (например, если порог скидки изменится с 10000 на 15000 или добавятся новые уровни скидок) придется привлекать программиста, снимать базу с поддержки и править программный код.
Кроме того, в коде допущена синтаксическая ошибка при вызове:
ИначеЕсли СуммаЗакупки >= 3000 Тогда ЦенаСУчетомСкидки = ПолучитьРазмерСкидкиПоКоэффициенту(3, 3000);
Вы передаете вариант "3", но в функции ПолучитьРазмерСкидкиПоКоэффициенту этот вариант вообще не описан. Из-за отсутствия блока Иначе для третьего варианта переменная Коэффициент останется неопределенной, и при попытке выполнить умножение Возврат Коэффициент*СуммаЗакупки; платформа выдаст ошибку выполнения.
Как правильно: Подобные пороги скидок и условия их предоставления должны храниться в базе данных. Для этого нужно создать специальный регистр сведений (например, "УсловияПредоставленияСкидок"). Это позволит пользователям, маркетологам или руководителям самостоятельно настраивать и менять скидки в пользовательском режиме без изменения кода.
Выясним причину главной математической ошибки. Функция ПолучитьРазмерСкидкиПоКоэффициенту возвращает итоговую сумму заказа с учетом скидки. Затем эта итоговая сумма заказа передается в процедуру НачислитьСкидку(РазмерСкидки), где происходит следующее:
Для Каждого СтрокаТЧ Из Товары
СтрокаДок.Цена = РазмерСкидки;
КонецЦикла;
Во-первых, переменная СтрокаДок нигде не объявлена. В цикле используется переменная СтрокаТЧ, к ней и нужно обращаться (СтрокаТЧ.Цена). Из-за этого код просто упадет с ошибкой.
Во-вторых, общая сумма всего заказа (пусть это будет 18000 рублей) присваивается в реквизит Цена каждой отдельной строки товаров. Цена одной единицы товара не может быть равна общей сумме всего документа!
Как правильно: Скидка должна рассчитываться построчно. Либо мы вычисляем единый процент (коэффициент) скидки для всего контрагента, а затем в цикле обходим строки табличной части и умножаем базовую цену товара на этот коэффициент:
Для Каждого СтрокаТЧ Из Товары
СтрокаТЧ.Цена = СтрокаТЧ.Цена * КоэффициентСкидки;
СтрокаТЧ.Сумма = СтрокаТЧ.Цена * СтрокаТЧ.Количество;
КонецЦикла;
Проанализируем серверную функцию, которая пытается получить данные из регистра:
ВЫБРАТЬ ПЕРВЫЕ 1
КоэффициентыКлиентовСрезПоследних.Коэффициент
ИЗ
РегистрСведений.КоэффициентыКлиентов.СрезПоследних(&ДатаКон, Контрагент = &Контрагент) КАК КоэффициентыКлиентовСрезПоследних
Если в регистре для одного контрагента срез последних всегда возвращает одну запись, то конструкция ПЕРВЫЕ 1 просто не имеет смысла. Но есть и другая, более серьезная проблема. При наличии нескольких записей в регистре сведений с одной и той же секундой (периодом) и одинаковым набором измерений, платформа не гарантирует, какая именно из них будет считаться "последней", если не задан дополнительный параметр сортировки.
Использование ВЫБРАТЬ ПЕРВЫЕ 1 без секции УПОРЯДОЧИТЬ ПО всегда является грубой ошибкой построения запросов, так как база данных (СУБД) может вернуть любую строку из подходящих в случайном порядке. Вы можете получать разные коэффициенты при каждом вызове кода.
Процедура НачислитьСкидку выполняется &НаСервере и вызывается из клиентского контекста. При каждом таком вызове платформа сериализует весь контекст формы (все реквизиты, большие табличные части) и отправляет их с клиента на сервер, а затем возвращает обратно. Если в документе 1000 строк, форма будет ощутимо "зависать".
Как правильно: Если бизнес-логика позволяет, простые математические пересчеты строк, которые уже загружены в форму, можно делать прямо &НаКлиенте. Если же логика сложная и требует сервера, лучше передавать на сервер "без контекста" только массив необходимых структур, там их обрабатывать и возвращать на клиент, либо работать напрямую с серверным объектом через РеквизитФормыВЗначение.
Размещение важной расчетной бизнес-логики (цены и скидки) в обработчике формы ПередЗаписью — это фундаментальная архитектурная ошибка. Представьте, что документ будет создан и записан программно (например, через загрузку XML, REST API, внешнюю обработку или регламентное задание). В этом случае код в модуле формы вообще не отработает, и документ запишется в базу с неверными суммами без скидок!
Как правильно:
ПередЗаписью или ОбработкаЗаполнения).ПриИзменении колонки "Сумма" или "Номенклатура", либо при изменении самого реквизита "Контрагент"). Так пользователь сразу увидит актуальные цены и скидки на экране, а не узнает о них постфактум, нажав кнопку "Записать".Мы подробно разобрали основные ошибки в представленном коде. Важно помнить, что написание качественного кода на платформе 1С требует не только знания синтаксических конструкций, но и глубокого понимания архитектуры системы: минимизации серверных вызовов, правильного разделения контекста на клиентский и серверный, отказа от жесткого кодирования бизнес-логики (хардкода) и грамотной работы с табличными частями и запросами. Применяйте описанные подходы в своих разработках, тестируйте все ветвления условий, и ваши системы будут работать быстро, стабильно и предсказуемо.