Проблема «лишних копеек» — это классическая ситуация в программировании на платформе 1С и в бухгалтерском учете в целом. Она возникает из-за того, что математическое округление суммы не всегда равно сумме округлений ее составляющих. Рассмотрим ситуацию: когда мы рассчитываем общую сумму с высокой точностью, а затем записываем ее в регистр с округлением до двух знаков, результат может отличаться от суммы строк, каждая из которых также была округлена до двух знаков.
В этой статье мы подробно разберем, почему возникают такие расхождения, проанализируем математические аспекты проблемы и рассмотрим несколько эффективных способов программного решения задачи, чтобы данные в разных регистрах всегда сходились.
Давайте проанализируем пример, приведенный в задаче. Существует два способа расчета:
Разница в 4 копейки возникла из-за того, что при округлении каждого слагаемого до двух знаков мы теряем или приобретаем крошечные доли, которые при большом количестве строк превращаются в ощутимую погрешность. Выясним, как с этим бороться в рамках разработки на 1С.
Это самый популярный и надежный способ в среде 1С. Суть его заключается в том, что мы сначала вычисляем «идеальный» итог всей операции, а затем при заполнении детальных записей (например, по дням или по позициям номенклатуры) вычисляем сумму для последней строки как разницу между общим итогом и суммой всех предыдущих строк.
Рассмотрим, как это реализовать программно. Допустим, у нас есть общая сумма ВсегоКРаспределению и массив строк, по которым нужно разнести эту сумму.
// Пример распределения остатка на последнюю запись
ОбщаяСумма = 3854.00;
КоличествоСтрок = РезультатЗапроса.Количество();
ТекущаяСуммаСтрок = 0;
Для НомерСтроки = 1 По КоличествоСтрок Цикл
Если НомерСтроки < КоличествоСтрок Тогда
// Для всех строк, кроме последней, применяем обычное округление
ЗначениеСтроки = Окр(РассчитанноеЗначение, 2);
ТекущаяСуммаСтрок = ТекущаяСуммаСтрок + ЗначениеСтроки;
Иначе
// В последней строке «схлопываем» погрешность
ЗначениеСтроки = ОбщаяСумма - ТекущаяСуммаСтрок;
КонецЕсли;
// Запись в регистр
ЗаписатьВРегистр(ЗначениеСтроки);
КонецЦикла;
Этот метод гарантирует, что итоговая сумма во втором регистре будет в точности равна 3854.00, даже если одна из строк будет отличаться на несколько копеек от своего «математически чистого» значения.
Если распределение на последнюю строку кажется вам грубым (например, если погрешность в последней строке может составить не 1-2 копейки, а больше), можно использовать более изящный алгоритм. Он пересчитывает среднее значение на каждом шаге цикла, исходя из оставшейся суммы и оставшегося количества итераций.
Посмотрим на пример кода, который позволяет равномерно распределить копейки:
Процедура РаспределитьСумму(ОбщаяСумма, ОбщееКоличество)
СуммаОсталось = ОбщаяСумма;
КолвоОсталось = ОбщееКоличество;
Пока КолвоОсталось > 0 Цикл
// Вычисляем сумму для текущей позиции с учетом остатка
ТекСуммаПозиции = Окр(СуммаОсталось / КолвоОсталось, 2);
// Выводим результат или записываем в регистр
Сообщить("Сумма на текущий шаг: " + ТекСуммаПозиции);
// Уменьшаем общий котел на уже распределенную величину
СуммаОсталось = СуммаОсталось - ТекСуммаПозиции;
КолвоОсталось = КолвоОсталось - 1;
КонецЦикла;
КонецПроцедуры
Этот алгоритм хорош тем, что он распределяет погрешность максимально равномерно по всем строкам, не создавая резкого перекоса в самом конце расчета.
Для сложных случаев, когда сумму нужно распределить по множеству аналитик, программисты 1С иногда адаптируют алгоритм Брезенхема (изначально предназначенный для построения линий на растровом экране). В нашем контексте мы заводим переменную-аккумулятор для накопления «ошибки округления».
Проанализируем ситуацию: на каждой итерации мы вычисляем разницу между точным значением (с 10+ знаками) и округленным значением. Эта разница прибавляется к переменной НакопленнаяОшибка. Как только эта ошибка превышает 0.005, мы добавляем копейку к текущей строке и вычитаем 0.01 из аккумулятора. Это позволяет «размазать» лишние копейки по всему периоду так, что визуально расчет выглядит очень естественным.
Важно помнить, что функция Окр() в 1С имеет необязательный параметр РежимОкругления. По умолчанию используется РежимОкругления.Окр15как20 (арифметическое округление). Однако в финансовом секторе иногда применяется «банковское округление» — до ближайшего четного.
Рассмотрим разницу:
РежимОкругления.Окр15как10) 1.5 станет 2, но и 2.5 станет 2. На больших массивах данных это позволяет ошибкам взаимозачитываться, и итоговая сумма получается точнее.Нужно учитывать, как платформа 1С взаимодействует с базой данных. Если в конфигураторе для ресурса регистра указана точность 2, то при записи значения 224.133333 SQL Server физически обрежет его до 224.13. Никакие программные расчеты внутри 1С не помогут сохранить точность, если само «хранилище» (ресурс регистра) не готово принимать больше знаков.
Если бизнес-логика требует идеальной точности, рекомендуем рассмотреть следующие шаги:
Подведем итог: Невозможно заставить математику работать иначе — сумма округленных частей почти никогда не будет равна округленной сумме целого. Задача разработчика 1С — выбрать алгоритм (распределение остатка или итерационный пересчет), который приведет эти данные к общему знаменателю согласно правилам учета, принятым в конкретной организации.