Разработчики, работающие со сложными периодическими расчетами в платформе «1С:Предприятие» (будь то расчет заработной платы в ЗУП/ERP или начисление коммунальных услуг в отраслевых решениях), часто сталкиваются с неочевидным поведением виртуальных таблиц регистров расчета. Одной из наиболее труднодиагностируемых проблем является задвоение или некорректный расчет базы при повторном сторнировании записей.
В этой статье мы подробно разберем механизм возникновения этой ошибки, погрузимся в математику расчетов платформы, выясним влияние структуры измерений регистра расчета на получение базовых данных и выработаем пошаговый план решения и предотвращения подобных ситуаций.
Рассмотрим конкретный пример расчета коммунальных начислений, в котором участвуют три услуги:
Проследим за цепочкой расчетов и перерасчетов за март 2019 года, выполняемых в последующих периодах регистрации:
1. Первичный расчет в марте 2019 года:
85,166713,1667 (Расчет верный).2. Первый перерасчет за март, выполненный в апреле 2019 года:
8-85,1667-5,166713,1667-13,1667Все записи перекрывают друг друга, итоговый результат перерасчета равен нулю, что также абсолютно верно.
3. Второй перерасчет за март, выполненный в мае 2019 года:
Возникла необходимость разбить период действия начислений на два интервала: с 1 по 21 марта и с 22 по 31 марта (параметры расчета при этом не изменились).
5,79312,2069-8 (Сумма новых: 5,7931 + 2,2069 = 8. Сторно: -8. Итог верный).3,74141,4253-5,1667 (Сумма новых: 3,7414 + 1,4253 = 5,1667. Сторно: -5,1667. Итог верный).18,4539 (Ошибка!)7,8795 (Ошибка!)-13,1667При сложении новых записей ВО мы получаем: 18,4539 + 7,8795 = 26,3334, что ровно в два раза превышает корректное значение базового начисления (13,1667 * 2 = 26,3334). Произошло явное задвоение базы. При этом, если запустить расчет «с чистого листа» (без проведения цепочки сторнирований), платформа рассчитывает базу абсолютно верно.
Проанализируем, как именно платформа получила ошибочные цифры для ВО за майские периоды перерасчета. Путем несложных вычислений определим формулу, по которой СУБД рассчитала базу для периода с 22 по 31 марта (10 дней из общего 31 дня марта):
База_ВО = (Новая_База_ХВС_ГВС_за_период) + (Старая_База_за_март * 10 / 31)
Подставим фактические значения:
(2,2069 + 1,4253) + (13,1667 * 10 / 31) = 3,6322 + 4,2473 = 7,8795
Аналогично для периода с 01 по 21 марта (21 день из 31):
(5,7931 + 3,7414) + (13,1667 * 21 / 31) = 9,5345 + 8,9193 = 18,4538 (с учетом округлений платформы — 18,4539).
Этот математический расклад наглядно показывает: платформа при расчете базы проигнорировала сторнирующие записи за предыдущие периоды. Из-за этого старая мартовская база в размере 13,1667 осталась «активной» и была пропорционально добавлена к новым начислениям мая.
Для того чтобы понять, почему сторно-запись игнорируется при расчете виртуальной таблицы базы, обратимся к механизму сопоставления записей.
Когда платформа рассчитывает виртуальную таблицу БазаНачисленияУслуг (или любую другую таблицу вида База[ИмяРегистра]), она выполняет левое соединение таблиц начислений и таблиц сторно. Главное правило этого сопоставления (клиринга) заключается в следующем: оригинальная запись и ее сторно-запись сопоставляются по абсолютно всем измерениям регистра расчета.
Если в процессе развития конфигурации в регистр расчета добавляется новое измерение (например, аналитика ЛицевойСчет или иное пользовательское измерение), возникает опасная коллизия данных:
NULL или пустую ссылку).При выполнении запроса к виртуальной таблице базы СУБД пытается связать исходную запись с пустой аналитикой и сторно-запись с заполненной аналитикой. Так как значения в полях измерений не совпадают, платформа считает, что эта сторно-запись относится к какому-то другому начислению. В итоге:
Разработчиков часто вводит в заблуждение тот факт, что новое измерение может вообще не передаваться в параметрах виртуальной таблицы базы. Рассмотрим пример запроса, формирующего базу:
Запрос.Текст = "ВЫБРАТЬ
| ЕСТЬNULL(База.ПотреблениеФактБаза, 0) КАК ПотреблениеФактБаза,
| База.РегистраторРазрез КАК РегистраторРазрез,
| База.ПериодРегистрацииРазрез КАК ПериодРегистрациРазрез,
| База.ВидНачисленияРазрез КАК БазовыйВидНачисления,
| База.НомерСтроки КАК НомерСтроки
|ИЗ
| РегистрРасчета.НачисленияУслуг.БазаНачисленияУслуг(
| &парамИзмеренияОсновного,
| &парамИзмеренияБазового,
| &РазрезБазы,
| ПериодРегистрации = &ПериодРегистрации
| И Строение = &Строение
| И Регистратор = &Регистратор
| И НЕ Сторно) КАК База";
Даже если новое измерение не указано в массивах &парамИзмеренияОсновного или &парамИзмеренияБазового, платформа на уровне ядра СУБД все равно осуществляет связывание таблиц по физическим полям всех измерений регистра. Несовпадение значений в любом из них гарантированно ломает логику свертки.
Если вы столкнулись с подобным задвоением базы, необходимо локализовать расхождения в измерениях и устранить их. Разберем шаги для исправления ситуации.
Для начала необходимо проанализировать физическую таблицу регистра расчета. Сделать это можно с помощью консоли запросов. Нам нужно найти записи за один и тот же период регистрации и период действия, у которых различаются измерения.
Особое внимание обратите на новые или измененные измерения. Убедитесь, что у сторнируемой и сторнирующей записи все без исключения измерения заполнены идентично (вплоть до типов данных: пустая ссылка конкретного справочника и значение Неопределено для платформы являются разными значениями).
Если расхождение найдено (например, в старых записях измерение пустое, а в новых — заполнено), необходимо выровнять исторические данные. Напишем код внешней обработки, которая заполнит новое измерение в старых записях регистра расчета.
// Пример кода для заполнения пропущенного измерения в регистре расчета
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Начисления.Регистратор КАК Регистратор,
| Начисления.НомерСтроки КАК НомерСтроки,
| Начисления.ЛицевойСчет КАК ЛицевойСчет
|ИЗ
| РегистрРасчета.НачисленияУслуг КАК Начисления
|ГДЕ
| Начисления.НовоеИзмерениеАналитики = ЗНАЧЕНИЕ(Справочник.ДополнительнаяАналитика.ПустаяСсылка)";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
ПредыдущийРегистратор = Неопределено;
НаборЗаписей = Неопределено;
Пока Выборка.Следующий() Цикл
Если ПредыдущийРегистратор <> Выборка.Регистратор Тогда
// Записываем предыдущий набор, если он был модифицирован
Если НаборЗаписей <> Неопределено Тогда
НаборЗаписей.Записать(Истина);
КонецЕсли;
// Создаем новый набор записей по регистратору
НаборЗаписей = РегистрыРасчета.НачисленияУслуг.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
НаборЗаписей.Прочитать();
ПредыдущийРегистратор = Выборка.Регистратор;
КонецЕсли;
// Находим нужную строку в наборе и заполняем измерение
Для Каждого Запись Из НаборЗаписей Цикл
Если Запись.НомерСтроки = Выборка.НомерСтроки Тогда
// Определяем корректное значение на основании связанных данных
Запись.НовоеИзмерениеАналитики = ПолучитьЗначениеАналитики(Выборка.ЛицевойСчет);
КонецЕсли;
КонецЦикла;
КонецЦикла;
// Не забываем записать последний набор
Если НаборЗаписей <> Неопределено Тогда
НаборЗаписей.Записать(Истина);
КонецЕсли;
Чтобы исключить возникновение подобных проблем в будущем, формирование сторно-записей должно происходить стандартизированными платформенными методами. Наиболее безопасным является использование метода ПолучитьДополнение() регистра расчета.
Этот метод анализирует вытесняющие записи и возвращает готовую таблицу записей «дополнения» (сторно-записей). При этом платформа автоматически копирует все измерения из оригинальных записей один в один, что гарантирует их последующее корректное сопоставление.
Рассмотрим пример правильного формирования сторно-записей при проведении расчетного документа:
// Программное получение сторно-записей через ПолучитьДополнение()
ТаблицаСторно = РегистрыРасчета.НачисленияУслуг.ПолучитьДополнение(
Новый Структура("Регистратор", СсылкаНаДокументКорректировки)
);
НаборЗаписей = РегистрыРасчета.НачисленияУслуг.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаДокументКорректировки);
Для Каждого СтрокаСторно Из ТаблицаСторно Цикл
НоваяЗапись = НаборЗаписей.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаСторно);
// Принудительно устанавливаем признак Сторно
НоваяЗапись.Сторно = Истина;
// Важно: измерения перенеслись автоматически из исходной записи!
// При необходимости заполняем дополнительные реквизиты
НоваяЗапись.ПериодРегистрации = РабочийПериодРегистрации;
КонецДляЕж;
НаборЗаписей.Записать();
При проектировании и доработке регистров расчета всегда придерживайтесь следующих правил:
NULL или Неопределено в полях измерений.ПолучитьДополнение() для точного копирования структуры измерений исходных записей.Соблюдение этих рекомендаций сбережет ваше время при локализации ошибок и обеспечит абсолютно точный расчет базы в любых сложных схемах перерасчетов.