Почему 1С:Комплексная автоматизация 2 и ERP неправильно рассчитывают вес упаковок и как это исправить?

Программист 1С v8.3 (Управляемые формы) 1С:Комплексная автоматизация Управленческий учет Торговля и дистрибуция
← На главную

В практической работе крупных торговых и производственных предприятий точность весогабаритных характеристик имеет колоссальное значение. Ошибки в расчете веса отгружаемой продукции ведут к путанице в сопроводительных документах, штрафам от логистических компаний и простоям на складе. В версиях конфигураций 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. Переменная ВесЕдиницыИзмерения для базовой единицы "кг" равна 1.
  2. Переменная ТекущаяСтрока.ВесУпаковки равна 46,8.
  3. Вычисляется отношение единицы измерения к весу упаковки: 1 / 46,8 = 0,0213675213675213... (получаем бесконечную периодическую дробь).
  4. Программа выполняет принудительное округление полученного значения до 6 знаков после запятой: Окр(0,02136752136..., 6). В результате округления мы получаем жестко зафиксированное число 0,021368.
  5. Далее вычисляется обратная величина от округленного коэффициента: 1 / 0,021368 ≈ 46,7989517. Как мы видим, вес одной упаковки в памяти системы уже исказился и превратился из оригинальных 46,8 кг в 46,7989517 кг.
  6. Это искаженное значение умножается на количество упаковок (3 шт.): 3 * 46,7989517 = 140,396855...
  7. При записи в документ число округляется до стандартной точности хранения веса в базе данных (3 знака после запятой), что дает нам итоговые 140,397 кг. Потеря составила 3 грамма на ровном месте.

Для сравнения, давайте посмотрим, как эта же процедура выглядела в более ранних, стабильных релизах (например, в 2.4.6.175):


ВесЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияВеса, КэшированныеЗначения).Коэффициент;
Если ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0 Тогда
    ТекущаяСтрока.Вес = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] *
                        ТекущаяСтрока.ВесУпаковки /
                        ВесЕдиницыИзмерения;
КонецЕсли;

Здесь используется простая и логичная формула: Количество * ВесУпаковки / ВесЕдиницы. В этой формуле нет никаких промежуточных округлений делителей, поэтому расчет всегда оставался точным.

Зачем разработчики 1С усложнили формулу?

Логичный вопрос, который возникает у любого разработчика и пользователя: зачем нужно было отказываться от простой работающей формулы и внедрять сомнительную конструкцию с принудительным округлением делителя?

Подобные изменения обычно связаны с попытками обойти ограничения платформы при работе с дробными величинами. На практике встречаются ситуации, когда товар отпускается не целыми, а дробными долями упаковок (например, 1/3 или 0,15 от коробки). При делении без округления в памяти компьютера возникают бесконечные периодические дроби во float-представлении. При последующем суммировании и свертке строк таких документов копейки и граммы начинали "плыть", из-за чего документы могли не проводиться по регистрам или вызывать ошибки контроля остатков.

Разработчики попытались привести вес к единому "нормализованному" коэффициенту доли веса упаковки в базовой единице измерения. Однако они допустили серьезную архитектурную и математическую ошибку. Округлять промежуточный делитель перед выполнением деления (операция 1 / Окр(X, 6)) математически недопустимо. Функция обратной величины крайне чувствительна к малейшим изменениям аргумента, особенно когда сам аргумент мал. В результате округление исказило исходные данные уже во втором знаке после запятой.

Дополнительно в измененном коде возникла еще одна уязвимость. Конструкция содержит деление на вес упаковки: ВесЕдиницыИзмерения / ТекущаяСтрока.ВесУпаковки. Хотя в условии есть проверка ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0, в некоторых документах значение веса упаковки может быть не заполнено или случайно очищено пользователем, что потенциально способно приводить к ошибкам деления на ноль при определенных сценариях динамического расчета на клиенте.

Как решить проблему?

Для исправления этой ошибки у нас есть два основных пути в зависимости от политики обновлений вашей информационной базы.

Способ 1. Официальное обновление конфигурации

В последующих релизах линеек 2.4.7+, а также в актуальных ветках 2.5+ разработчики признали эту логику ошибочной и вернули расчет к классическому виду, отказавшись от промежуточного округления делителей. Округлению теперь подвергается только финальный результат вычислений всей строки. Если у вас есть возможность обновить конфигурацию до актуального релиза, это будет самым системным решением.

Способ 2. Исправление через расширение конфигурации (без снятия с поддержки)

Если ваша база находится на замке, а оперативно обновиться на новый релиз нет возможности (например, из-за обилия доработок), проблему можно решить локально. Мы можем перехватить выполнение проблемной процедуры с помощью механизма расширений.

Проанализируем и выполним по шагам процесс создания исправления:

  1. Откройте конфигуратор вашей базы данных и перейдите в меню Конфигурация — Расширения конфигурации.
  2. Добавьте новое расширение, укажите для него назначение "Адаптация" или "Исправление", снимите флажки "Безопасный режим" и "Защита от опасных действий" (если это необходимо для работы вашего кода).
  3. Найдите в основной конфигурации общий модуль ОбработкаТабличнойЧастиКлиентСервер. Кликните по нему правой кнопкой мыши и выберите команду Добавить в расширение.
  4. В модуле расширения создайте процедуру-перехватчик с директивой &Вместо для замены стандартного поведения процедуры ПересчитатьВесОбъемВСтрокеТЧ.

Ниже представлен безопасный и корректный вариант кода для модуля расширения, который возвращает проверенную временем точную математическую формулу расчета:


&Вместо("ПересчитатьВесОбъемВСтрокеТЧ")
Процедура Расш_ПересчитатьВесОбъемВСтрокеТЧ(ТекущаяСтрока, ИмяТЧ = "Товары", КэшированныеЗначения = Неопределено)
    
    // Определяем суффикс для работы с реквизитами ТЧ (актуально для различных документов)
    Суффикс = ?(ИмяТЧ = "Товары" Или ИмяТЧ = "ПорчаТоваров", "Упаковок", "");
    
    // Проверим наличие необходимых колонок в строке, чтобы избежать ошибок выполнения
    Если Не ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "КоличествоУпаковок" + Суффикс) Тогда
        Возврат;
    КонецЕсли;
    
    // Получаем коэффициент базовой единицы веса
    ВесЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияВеса, КэшированныеЗначения).Коэффициент;
    
    // Возвращаем классический точный расчет без промежуточного округления делителей
    Если ВесЕдиницыИзмерения * ТекущаяСтрока.ВесУпаковки <> 0 Тогда
        ТекущаяСтрока.Вес = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] * 
                            ТекущаяСтрока.ВесУпаковки / 
                            ВесЕдиницыИзмерения;
    Иначе
        ТекущаяСтрока.Вес = 0;
    КонецЕсли;
    
    // Аналогичный блок пересчета объема (если он присутствует в вашей версии конфигурации)
    Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "ЕдиницаИзмеренияОбъема") Тогда
        ОбъемЕдиницыИзмерения = ПолучитьКоэффициентУпаковки(ТекущаяСтрока.ЕдиницаИзмеренияОбъема, КэшированныеЗначения).Коэффициент;
        Если ОбъемЕдиницыИзмерения * ТекущаяСтрока.ОбъемУпаковки <> 0 Тогда
            ТекущаяСтрока.Объем = ТекущаяСтрока["КоличествоУпаковок" + Суффикс] * 
                                  ТекущаяСтрока.ОбъемУпаковки / 
                                  ОбъемЕдиницыИзмерения;
        Иначе
            ТекущаяСтрока.Объем = 0;
        КонецЕсли;
    КонецЕсли;

КонецПроцедуры

Применив данное расширение, мы полностью нейтрализуем проблему накопления погрешности. При изменении количества в строке документа вес будет рассчитываться строго по формуле: 3 * 46,8 / 1 = 140,4. Округление конечного результата произойдет уже при стандартной записи в реквизит табличной части до 3 знаков, что исключит появление паразитных тысячных долей килограмма (граммов) и гарантирует идеальное совпадение итогов как в пределах одного документа, так и при отгрузке гигантских партий товара.

← На главную