В практической работе крупных торговых и производственных предприятий точность весогабаритных характеристик имеет колоссальное значение. Ошибки в расчете веса отгружаемой продукции ведут к путанице в сопроводительных документах, штрафам от логистических компаний и простоям на складе. В версиях конфигураций 1С:Комплексная автоматизация 2 и 1С:ERP Управление предприятием 2 (в частности, на релизе 2.4.6.207) пользователи столкнулись со странным поведением системы: при изменении количества упаковок в документах отгрузки итоговый вес начинал рассчитываться некорректно, выдавая дробные значения с погрешностью.
В этой статье мы подробно разберем причины этой проблемы, проанализируем математическую модель ошибочной формулы, выясним мотивы разработчиков фирмы "1С" и предоставим готовое техническое решение для исправления ситуации с помощью механизма расширений.
Рассмотрим конкретную практическую ситуацию, с которой столкнулись пользователи на складе. Имеются следующие исходные данные:
При первоначальном открытии или автоматическом заполнении ордера система выполняет верный расчет: 3 * 46,8 = 140,4 кг. Однако стоит кладовщику или оператору вручную перевыбрать или просто перезаписать количество в строке (даже указав те же самые 3 упаковки), программа мгновенно пересчитывает вес и выдает поразительный результат: 140,397 кг!
На трех упаковках потеря составляет всего 3 грамма, что кажется незначительным. Но если отгрузка идет контейнерами или крупными партиями, погрешность приобретает лавинообразный характер. При отправке тысяч упаковок округление приводит к виртуальному "недовесу" в сотни килограммов и даже тонны, ввергая сотрудников склада в ступор.
Давайте выясним, почему система "разучилась" умножать два числа.
Проанализируем программный код общего модуля, отвечающего за расчет весогабаритных характеристик. Проблемный алгоритм сосредоточен в процедуре ПересчитатьВесОбъемВСтрокеТЧ общего модуля ОбработкаТабличнойЧастиКлиентСервер.
В проблемном релизе 2.4.6.207 код выглядит следующим образом:
ВесЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияВеса, КэшированныеЗначения).Коэффициент;
Если ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0 Тогда
ТекущаяСтрока.Вес = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] *
1/Окр(ВесЕдиницыИзмерения/ТекущаяСтрока.ВесУпаковки , 6, РежимОкругления.Окр15как20);
КонецЕсли;
Давайте пошагово разберем математику этого вычисления, подставив наши данные:
ВесЕдиницыИзмерения для базовой единицы "кг" равна 1.ТекущаяСтрока.ВесУпаковки равна 46,8.1 / 46,8 = 0,0213675213675213... (получаем бесконечную периодическую дробь).Окр(0,02136752136..., 6). В результате округления мы получаем жестко зафиксированное число 0,021368.1 / 0,021368 ≈ 46,7989517. Как мы видим, вес одной упаковки в памяти системы уже исказился и превратился из оригинальных 46,8 кг в 46,7989517 кг.3 * 46,7989517 = 140,396855...Для сравнения, давайте посмотрим, как эта же процедура выглядела в более ранних, стабильных релизах (например, в 2.4.6.175):
ВесЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияВеса, КэшированныеЗначения).Коэффициент;
Если ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0 Тогда
ТекущаяСтрока.Вес = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] *
ТекущаяСтрока.ВесУпаковки /
ВесЕдиницыИзмерения;
КонецЕсли;
Здесь используется простая и логичная формула: Количество * ВесУпаковки / ВесЕдиницы. В этой формуле нет никаких промежуточных округлений делителей, поэтому расчет всегда оставался точным.
Логичный вопрос, который возникает у любого разработчика и пользователя: зачем нужно было отказываться от простой работающей формулы и внедрять сомнительную конструкцию с принудительным округлением делителя?
Подобные изменения обычно связаны с попытками обойти ограничения платформы при работе с дробными величинами. На практике встречаются ситуации, когда товар отпускается не целыми, а дробными долями упаковок (например, 1/3 или 0,15 от коробки). При делении без округления в памяти компьютера возникают бесконечные периодические дроби во float-представлении. При последующем суммировании и свертке строк таких документов копейки и граммы начинали "плыть", из-за чего документы могли не проводиться по регистрам или вызывать ошибки контроля остатков.
Разработчики попытались привести вес к единому "нормализованному" коэффициенту доли веса упаковки в базовой единице измерения. Однако они допустили серьезную архитектурную и математическую ошибку. Округлять промежуточный делитель перед выполнением деления (операция 1 / Окр(X, 6)) математически недопустимо. Функция обратной величины крайне чувствительна к малейшим изменениям аргумента, особенно когда сам аргумент мал. В результате округление исказило исходные данные уже во втором знаке после запятой.
Дополнительно в измененном коде возникла еще одна уязвимость. Конструкция содержит деление на вес упаковки: ВесЕдиницыИзмерения / ТекущаяСтрока.ВесУпаковки. Хотя в условии есть проверка ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0, в некоторых документах значение веса упаковки может быть не заполнено или случайно очищено пользователем, что потенциально способно приводить к ошибкам деления на ноль при определенных сценариях динамического расчета на клиенте.
Для исправления этой ошибки у нас есть два основных пути в зависимости от политики обновлений вашей информационной базы.
В последующих релизах линеек 2.4.7+, а также в актуальных ветках 2.5+ разработчики признали эту логику ошибочной и вернули расчет к классическому виду, отказавшись от промежуточного округления делителей. Округлению теперь подвергается только финальный результат вычислений всей строки. Если у вас есть возможность обновить конфигурацию до актуального релиза, это будет самым системным решением.
Если ваша база находится на замке, а оперативно обновиться на новый релиз нет возможности (например, из-за обилия доработок), проблему можно решить локально. Мы можем перехватить выполнение проблемной процедуры с помощью механизма расширений.
Проанализируем и выполним по шагам процесс создания исправления:
ОбработкаТабличнойЧастиКлиентСервер. Кликните по нему правой кнопкой мыши и выберите команду Добавить в расширение.&Вместо для замены стандартного поведения процедуры ПересчитатьВесОбъемВСтрокеТЧ.Ниже представлен безопасный и корректный вариант кода для модуля расширения, который возвращает проверенную временем точную математическую формулу расчета:
&Вместо("ПересчитатьВесОбъемВСтрокеТЧ")
Процедура Расш_ПересчитатьВесОбъемВСтрокеТЧ(ТекущаяСтрока, ИмяТЧ = "Товары", КэшированныеЗначения = Неопределено)
// Определяем суффикс для работы с реквизитами ТЧ (актуально для различных документов)
Суффикс = ?(ИмяТЧ = "Товары" Или ИмяТЧ = "ПорчаТоваров", "Упаковок", "");
// Проверим наличие необходимых колонок в строке, чтобы избежать ошибок выполнения
Если Не ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "КоличествоУпаковок" + Суффикс) Тогда
Возврат;
КонецЕсли;
// Получаем коэффициент базовой единицы веса
ВесЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияВеса, КэшированныеЗначения).Коэффициент;
// Возвращаем классический точный расчет без промежуточного округления делителей
Если ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0 Тогда
ТекущаяСтрока.Вес = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] *
ТекущаяСтрока.ВесУпаковки /
ВесЕдиницыИзмерения;
Иначе
ТекущаяСтрока.Вес = 0;
КонецЕсли;
// Аналогичный блок пересчета объема (если он присутствует в вашей версии конфигурации)
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "ЕдиницаИзмеренияОбъема") Тогда
ОбъемЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияОбъема, КэшированныеЗначения).Коэффициент;
Если ОбъемЕдиницыИзмерения * ТекущаяСтрока.ОбъемУпаковки <> 0 Тогда
ТекущаяСтрока.Объем = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] *
ТекущаяСтрока.ОбъемУпаковки /
ОбъемЕдиницыИзмерения;
Иначе
ТекущаяСтрока.Объем = 0;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Применив данное расширение, мы полностью нейтрализуем проблему накопления погрешности. При изменении количества в строке документа вес будет рассчитываться строго по формуле: 3 * 46,8 / 1 = 140,4. Округление конечного результата произойдет уже при стандартной записи в реквизит табличной части до 3 знаков, что исключит появление паразитных тысячных долей килограмма (граммов) и гарантирует идеальное совпадение итогов как в пределах одного документа, так и при отгрузке гигантских партий товара.