Мы часто сталкиваемся с задачами, где необходимо рассчитать какую-либо величину путем деления. Один из наших коллег столкнулся со следующей ситуацией: при попытке прямого вычисления себестоимости по формуле Себестоимость = ВыборкаНоменклатура.СуммаОстаток / ВыборкаНоменклатура.КоличествоОстаток, результат оказывался "Неопределено". Однако, если сначала присвоить значения в промежуточные переменные, например, ВыбСум = ВыборкаНоменклатура.СуммаОстаток; ВыбКол = ВыборкаНоменклатура.КоличествоОстаток; Себестоимость = ВыбСум / ВыбКол;, то все начинало работать корректно. Давайте вместе разберемся, почему так происходит и как правильно решать подобные задачи.
Основная причина, по которой мы можем получить значение "Неопределено" при попытке деления, кроется в двух аспектах:
ВыборкаНоменклатура.КоличествоОстаток равно нулю, то результатом деления будет ошибка, которая в контексте 1С может быть интерпретирована как "Неопределено".
СуммаОстатокДт или КоличествоОстатокДт) содержит значение NULL (что часто происходит при использовании левых соединений, когда нет соответствующих записей), то любая арифметическая операция с NULL также вернет NULL. В коде 1С NULL преобразуется в "Неопределено".
Почему же использование промежуточных переменных ВыбКол и ВыбСум "решает" проблему? На самом деле, это не решение проблемы, а скорее способ сделать значения более наглядными для отладки. Промежуточные переменные позволяют нам увидеть, какие именно значения (включая 0 или "Неопределено") присваиваются перед выполнением операции деления. Если проблема заключалась в делении на ноль, то присвоение значения в переменную не изменит того факта, что делитель равен нулю, и ошибка все равно возникнет при делении. Вероятнее всего, в вашем случае, при использовании промежуточных переменных, вы могли интуитивно или явно проверять значения, что и помогало избежать ошибки.
Для того чтобы избежать подобных ситуаций и писать надежный код, мы рекомендуем придерживаться следующих шагов:
ЕСТЬNULL в запросе.
Прежде всего, давайте убедимся, что поля, участвующие в расчете, не содержат NULL значений. Для этого в тексте запроса необходимо использовать функцию ЕСТЬNULL(Поле, ЗначениеПоУмолчанию). Эта функция заменяет NULL на указанное нами значение, чаще всего на 0, если речь идет о числовых полях.
Рассмотрим пример запроса, где мы получаем остатки по регистру бухгалтерии:
ВЫБРАТЬ
врТаблица.Номенклатура КАК Номенклатура,
ПРЕДСТАВЛЕНИЕ(врТаблица.Номенклатура) КАК НоменклатураП,
УправленческийОстатки.Субконто2 КАК Склад,
врТаблица.Проект КАК Проект,
врТаблица.Количество КАК Количество,
врТаблица.Цена КАК Цена,
ЕСТЬNULL(УправленческийОстатки.СуммаОстатокДт, 0) КАК СуммаОстаток,
ЕСТЬNULL(УправленческийОстатки.КоличествоОстатокДт, 0) КАК КоличествоОстаток
ИЗ
врТаблица КАК врТаблица
ЛЕВОЕ СОЕДИНЕНИЕ РегистрБухгалтерии.Управленческий.Остатки(
&Период,
Счет = &Счет,
,
Субконто1 В
(ВЫБРАТЬ
врТаблица.Номенклатура КАК Номенклатура
ИЗ
врТаблица КАК врТаблица)
И Субконто2 = &Склад) КАК УправленческийОстатки
ПО врТаблица.Номенклатура = УправленческийОстатки.Субконто1
В этом запросе мы видим, что поля СуммаОстатокДт и КоличествоОстатокДт обернуты в функцию ЕСТЬNULL(..., 0). Это гарантирует, что в результате запроса эти поля будут содержать числовые значения (0, если остатков нет), а не NULL.
Даже если мы использовали ЕСТЬNULL в запросе, это не исключает ситуации, когда КоличествоОстаток может быть равно нулю (например, если товара нет на остатках). Поэтому в коде 1С всегда необходимо явно проверять делитель перед выполнением деления.
Рассмотрим ваш код с учетом этой проверки:
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
// Проверяем, что КоличествоОстаток не равно нулю, чтобы избежать деления на ноль
Если ВыборкаНоменклатура.КоличествоОстаток <> 0 Тогда
// Теперь мы уверены, что КоличествоОстаток - это число и оно не равно нулю
Себестоимость = ВыборкаНоменклатура.СуммаОстаток / ВыборкаНоменклатура.КоличествоОстаток;
Иначе
// Если КоличествоОстаток равно нулю, себестоимость может быть 0
// или иное значение по вашей бизнес-логике
Себестоимость = 0;
КонецЕсли;
// ... дальнейшая обработка выборки
// Например, расчет движений документа
Выборка = ВыборкаНоменклатура.Выбрать();
Пока Выборка.Следующий() Цикл
// ... ваш код для движений
Если Не Отказ Тогда
Движение = Движения.Управленческий.Добавить();
Движение.СчетДт = ПланыСчетов.Управленческий.ПрибылиУбытки;
Движение.СчетКт = ПланыСчетов.Управленческий.Товары;
Движение.Период = Дата;
Движение.Сумма = Выборка.Количество * Себестоимость; // Используем рассчитанную себестоимость
Движение.КоличествоКт = Выборка.Количество;
// ... остальные поля движения
КонецЕсли;
КонецЦикла;
КонецЦикла;
Таким образом, мы сначала гарантируем, что поля в выборке не содержат NULL, а затем явно проверяем делитель на равенство нулю, предотвращая ошибку деления. Это делает наш код более надежным и предсказуемым.
В ходе обсуждения на форуме были затронуты также важные темы, связанные с контролем отрицательных остатков и блокировкой данных. Давайте разберем их подробнее.
В 1С существуют две основные методики контроля отрицательных остатков, которые мы можем применять:
ДЛЯ ИЗМЕНЕНИЯ в запросе) проверяем остатки. Если обнаруживаются отрицательные остатки, проведение документа отменяется.
В современных версиях 1С (начиная с 8.2/8.3) в клиент-серверном варианте работы мы предпочтительно используем механизм управляемых блокировок платформы. Эти блокировки позволяют нам повысить параллельность работы пользователей за счет использования более низкого уровня изоляции транзакций СУБД (Read Committed).
Для установки управляемых блокировок мы используем объект БлокировкаДанных. Давайте посмотрим на фрагмент вашего кода, где уже применяются управляемые блокировки:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрБухгалтерии.Управленческий");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Счет", ПланыСчетов.Управленческий.Товары);
ЭлементБлокировки.УстановитьЗначение("Субконто2", Склад);
ЭлементБлокировки.ИсточникДанных = СписокНоменклатуры;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Субконто1", "Номенклатура");
Блокировка.Заблокировать();
Здесь мы создаем объект БлокировкаДанных, добавляем элемент блокировки для регистра бухгалтерии Управленческий, указываем Исключительный режим блокировки (чтобы другие пользователи не могли изменить эти данные), и привязываем блокировку к данным из СписокНоменклатуры. Затем вызываем метод Блокировка.Заблокировать().
Важный момент: При использовании управляемых блокировок для наборов записей регистров накопления и бухгалтерии, конструкция ДЛЯ ИЗМЕНЕНИЯ в запросе становится избыточной и не требуется. Блокировка данных для изменения будет осуществляться платформой автоматически при записи набора записей регистра.
ДЛЯ ИЗМЕНЕНИЯ в запросе (когда она актуальна)Конструкция ДЛЯ ИЗМЕНЕНИЯ в запросах SQL используется для блокировки считываемых данных, чтобы предотвратить их изменение другими транзакциями до завершения текущей. В 1С 8.1 и более ранних версиях это был стандартный способ блокировки данных регистра при контроле остатков.
Мы можем использовать ДЛЯ ИЗМЕНЕНИЯ в запросе, если:
Пример использования ДЛЯ ИЗМЕНЕНИЯ в запросе:
ВЫБРАТЬ
РегистрНакопления.ОстаткиТоваров.Номенклатура КАК Номенклатура,
РегистрНакопления.ОстаткиТоваров.КоличествоОстаток КАК Количество
ИЗ
РегистрНакопления.ОстаткиТоваров.Остатки КАК РегистрНакопления.ОстаткиТоваров
ГДЕ
РегистрНакопления.ОстаткиТоваров.Номенклатура В (&СписокНоменклатуры)
ДЛЯ ИЗМЕНЕНИЯ
В вашем случае, с учетом использования управляемых блокировок, добавление ДЛЯ ИЗМЕНЕНИЯ в запрос, который читает остатки регистра бухгалтерии, не имеет смысла. Управляемые блокировки, которые вы уже используете, более предпочтительны и эффективны в клиент-серверной архитектуре.
Мы выяснили, что "странное решение" с промежуточными переменными, вероятно, помогало избежать ошибки деления на ноль, которая возникала из-за неподготовленных данных. Для корректного и надежного расчета себестоимости и других величин, требующих деления, мы должны всегда использовать функцию ЕСТЬNULL в запросах для обработки потенциальных NULL-значений и обязательно проверять делитель на ноль в коде 1С перед выполнением операции. Кроме того, для обеспечения целостности данных и параллельной работы пользователей, мы рекомендуем использовать современные механизмы управляемых блокировок платформы 1С.