Почему задваивается база регистра расчета при сторнировании в 1С и как это исправить?

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

Разработчики, работающие со сложными периодическими расчетами в платформе «1С:Предприятие» (будь то расчет заработной платы в ЗУП/ERP или начисление коммунальных услуг в отраслевых решениях), часто сталкиваются с неочевидным поведением виртуальных таблиц регистров расчета. Одной из наиболее труднодиагностируемых проблем является задвоение или некорректный расчет базы при повторном сторнировании записей.

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

Суть проблемы и практический сценарий

Рассмотрим конкретный пример расчета коммунальных начислений, в котором участвуют три услуги:

  1. Холодное водоснабжение (ХВС) — прямое начисление.
  2. Горячее водоснабжение (ГВС) — прямое начисление.
  3. Водоотведение (ВО) — расчетная услуга, базой для которой является сумма объемов ХВС и ГВС.

Проследим за цепочкой расчетов и перерасчетов за март 2019 года, выполняемых в последующих периодах регистрации:

1. Первичный расчет в марте 2019 года:

2. Первый перерасчет за март, выполненный в апреле 2019 года:

Все записи перекрывают друг друга, итоговый результат перерасчета равен нулю, что также абсолютно верно.

3. Второй перерасчет за март, выполненный в мае 2019 года:
Возникла необходимость разбить период действия начислений на два интервала: с 1 по 21 марта и с 22 по 31 марта (параметры расчета при этом не изменились).

При сложении новых записей ВО мы получаем: 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 осталась «активной» и была пропорционально добавлена к новым начислениям мая.

Внутренние механизмы платформы 1С: почему ломается сторно?

Для того чтобы понять, почему сторно-запись игнорируется при расчете виртуальной таблицы базы, обратимся к механизму сопоставления записей.

Когда платформа рассчитывает виртуальную таблицу БазаНачисленияУслуг (или любую другую таблицу вида База[ИмяРегистра]), она выполняет левое соединение таблиц начислений и таблиц сторно. Главное правило этого сопоставления (клиринга) заключается в следующем: оригинальная запись и ее сторно-запись сопоставляются по абсолютно всем измерениям регистра расчета.

Если в процессе развития конфигурации в регистр расчета добавляется новое измерение (например, аналитика ЛицевойСчет или иное пользовательское измерение), возникает опасная коллизия данных:

  1. Исторические записи (созданные до добавления измерения или до изменения логики проведения документов) содержат в новом измерении пустое значение (NULL или пустую ссылку).
  2. Новые записи сторно (формируемые уже обновленным кодом системы) записываются в ИБ с заполненным новым измерением.

При выполнении запроса к виртуальной таблице базы СУБД пытается связать исходную запись с пустой аналитикой и сторно-запись с заполненной аналитикой. Так как значения в полях измерений не совпадают, платформа считает, что эта сторно-запись относится к какому-то другому начислению. В итоге:

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


Запрос.Текст = "ВЫБРАТЬ
|   ЕСТЬNULL(База.ПотреблениеФактБаза, 0) КАК ПотреблениеФактБаза,
|   База.РегистраторРазрез КАК РегистраторРазрез,
|   База.ПериодРегистрацииРазрез КАК ПериодРегистрациРазрез,
|   База.ВидНачисленияРазрез КАК БазовыйВидНачисления,
|   База.НомерСтроки КАК НомерСтроки
|ИЗ
|   РегистрРасчета.НачисленияУслуг.БазаНачисленияУслуг(
|       &парамИзмеренияОсновного,
|       &парамИзмеренияБазового,
|       &РазрезБазы,
|       ПериодРегистрации = &ПериодРегистрации
|       И Строение = &Строение
|       И Регистратор = &Регистратор
|       И НЕ Сторно) КАК База";

Даже если новое измерение не указано в массивах &парамИзмеренияОсновного или &парамИзмеренияБазового, платформа на уровне ядра СУБД все равно осуществляет связывание таблиц по физическим полям всех измерений регистра. Несовпадение значений в любом из них гарантированно ломает логику свертки.

Пошаговый план решения проблемы

Если вы столкнулись с подобным задвоением базы, необходимо локализовать расхождения в измерениях и устранить их. Разберем шаги для исправления ситуации.

Шаг 1. Локализация расхождений

Для начала необходимо проанализировать физическую таблицу регистра расчета. Сделать это можно с помощью консоли запросов. Нам нужно найти записи за один и тот же период регистрации и период действия, у которых различаются измерения.

Особое внимание обратите на новые или измененные измерения. Убедитесь, что у сторнируемой и сторнирующей записи все без исключения измерения заполнены идентично (вплоть до типов данных: пустая ссылка конкретного справочника и значение Неопределено для платформы являются разными значениями).

Шаг 2. Написание обработки восстановления исторических данных

Если расхождение найдено (например, в старых записях измерение пустое, а в новых — заполнено), необходимо выровнять исторические данные. Напишем код внешней обработки, которая заполнит новое измерение в старых записях регистра расчета.


// Пример кода для заполнения пропущенного измерения в регистре расчета
Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	Начисления.Регистратор КАК Регистратор,
|	Начисления.НомерСтроки КАК НомерСтроки,
|	Начисления.ЛицевойСчет КАК ЛицевойСчет
|ИЗ
|	РегистрРасчета.НачисленияУслуг КАК Начисления
|ГДЕ
|	Начисления.НовоеИзмерениеАналитики = ЗНАЧЕНИЕ(Справочник.ДополнительнаяАналитика.ПустаяСсылка)";

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

ПредыдущийРегистратор = Неопределено;
НаборЗаписей = Неопределено;

Пока Выборка.Следующий() Цикл
	
	Если ПредыдущийРегистратор <> Выборка.Регистратор Тогда
		
		// Записываем предыдущий набор, если он был модифицирован
		Если НаборЗаписей <> Неопределено Тогда
			НаборЗаписей.Записать(Истина);
		КонецЕсли;
		
		// Создаем новый набор записей по регистратору
		НаборЗаписей = РегистрыРасчета.НачисленияУслуг.СоздатьНаборЗаписей();
		НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
		НаборЗаписей.Прочитать();
		
		ПредыдущийРегистратор = Выборка.Регистратор;
		
	КонецЕсли;
	
	// Находим нужную строку в наборе и заполняем измерение
	Для Каждого Запись Из НаборЗаписей Цикл
		Если Запись.НомерСтроки = Выборка.НомерСтроки Тогда
			// Определяем корректное значение на основании связанных данных
			Запись.НовоеИзмерениеАналитики = ПолучитьЗначениеАналитики(Выборка.ЛицевойСчет);
		КонецЕсли;
	КонецЦикла;
	
КонецЦикла;

// Не забываем записать последний набор
Если НаборЗаписей <> Неопределено Тогда
	НаборЗаписей.Записать(Истина);
КонецЕсли;

Шаг 3. Использование правильных методов создания сторно-записей

Чтобы исключить возникновение подобных проблем в будущем, формирование сторно-записей должно происходить стандартизированными платформенными методами. Наиболее безопасным является использование метода ПолучитьДополнение() регистра расчета.

Этот метод анализирует вытесняющие записи и возвращает готовую таблицу записей «дополнения» (сторно-записей). При этом платформа автоматически копирует все измерения из оригинальных записей один в один, что гарантирует их последующее корректное сопоставление.

Рассмотрим пример правильного формирования сторно-записей при проведении расчетного документа:


// Программное получение сторно-записей через ПолучитьДополнение()
ТаблицаСторно = РегистрыРасчета.НачисленияУслуг.ПолучитьДополнение(
	Новый Структура("Регистратор", СсылкаНаДокументКорректировки)
);

НаборЗаписей = РегистрыРасчета.НачисленияУслуг.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаДокументКорректировки);

Для Каждого СтрокаСторно Из ТаблицаСторно Цикл
	
	НоваяЗапись = НаборЗаписей.Добавить();
	ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаСторно);
	
	// Принудительно устанавливаем признак Сторно
	НоваяЗапись.Сторно = Истина;
	
	// Важно: измерения перенеслись автоматически из исходной записи!
	// При необходимости заполняем дополнительные реквизиты
	НоваяЗапись.ПериодРегистрации = РабочийПериодРегистрации;
	
КонецДляЕж;

НаборЗаписей.Записать();

Резюме и рекомендации

При проектировании и доработке регистров расчета всегда придерживайтесь следующих правил:

Соблюдение этих рекомендаций сбережет ваше время при локализации ошибок и обеспечит абсолютно точный расчет базы в любых сложных схемах перерасчетов.

← На главную