Остатки не сходятся с движениями: как найти месяц, где сломались итоги регистра
База данных - Инструменты администратора БД
Остатки по одной позиции не бьются с движениями. Отчёт по движениям показывает одно число, запрос к остаткам другое. Документы перепровели, ничего не изменилось. Дальше звучит совет пересчитать итоги, а на большой базе полный пересчёт это ночное окно, и заранее никто не скажет, поможет ли он.
Ниже способ найти регистр, позицию и месяц, где итоги разошлись с движениями, одним запросом, и починить это пересчётом за один месяц вместо всей истории. Всё на встроенном языке, внешних обработок не нужно. Пример на типовом регистре ТоварыНаСкладах из УТ 11.5, проверен на тестовой копии с намеренно испорченной строкой итогов.
Почему перепроведение не лечит
Проверили на тестовой копии. При испорченной строке итогов за 1 августа прочитали и записали заново наборы записей всех трёх документов с движениями в июле, 1 800 записей:
Набор = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей(); Набор.Отбор.Регистратор.Установить(ДокументСсылка); Набор.Прочитать(); // Те же движения: итоги не изменились, испорченная строка осталась. Набор.Записать();
При записи итоги меняются на разницу между старым и новым набором. Движения те же, разница нулевая, и ошибка, которая уже сидит в строке итогов, остаётся на месте. Отсюда знакомая картина: по каждому документу движения верные, а остаток всё равно не бьётся.
Что хранится в строке итогов
У регистра остатков с итогами на каждый месяц хранится строка: остаток на начало месяца по каждой комбинации измерений. Плюс отдельная строка текущих итогов, остаток на сейчас. На тестовой УТ по регистру ТоварыНаСкладах это выглядит так (ресурс ВНаличии, сумма по всем позициям):
движения за месяц строка итогов
июнь +87 081 01.07 87 081
июль -4 424 01.08 82 657
август -819 01.09 81 838
текущие 81 838
Строка с периодом 1 августа хранит остаток на начало августа, то есть всё, что накопилось по конец июля. Запрос Остатки на первое число месяца берёт ровно эту строку и движения не читает. Испорченная строка вернётся из запроса как есть, и никакая проверка документов её не увидит.
Запрос: где итоги разошлись с движениями
Идея простая. Складываем активные движения раньше границы месяца: приход с плюсом, расход с минусом. Это остаток, который должен быть. Рядом ставим Остатки на ту же границу, это остаток, который хранится. Разница по позиции и есть ошибка строки итогов.
ВЫБРАТЬ Т.Номенклатура, Т.Характеристика, Т.Назначение, Т.Склад, Т.Помещение, Т.Серия, СУММА(ВЫБОР КОГДА Т.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход) ТОГДА Т.ВНаличии ИНАЧЕ -Т.ВНаличии КОНЕЦ) КАК ВНаличии ПОМЕСТИТЬ ВТ_Движения ИЗ РегистрНакопления.ТоварыНаСкладах КАК Т ГДЕ Т.Активность И Т.Период < &ДвиженияДо СГРУППИРОВАТЬ ПО Т.Номенклатура, Т.Характеристика, Т.Назначение, Т.Склад, Т.Помещение, Т.Серия ; ВЫБРАТЬ О.Номенклатура, О.Характеристика, О.Назначение, О.Склад, О.Помещение, О.Серия, О.ВНаличииОстаток КАК ВНаличии ПОМЕСТИТЬ ВТ_Итоги ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(&Граница) КАК О ; ВЫБРАТЬ ЕСТЬNULL(Д.Номенклатура, Ит.Номенклатура) КАК Номенклатура, ЕСТЬNULL(Д.Склад, Ит.Склад) КАК Склад, ЕСТЬNULL(Д.ВНаличии, 0) КАК ПоДвижениям, ЕСТЬNULL(Ит.ВНаличии, 0) КАК ПоИтогам, ЕСТЬNULL(Д.ВНаличии, 0) - ЕСТЬNULL(Ит.ВНаличии, 0) КАК Разница ИЗ ВТ_Движения КАК Д ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Итоги КАК Ит ПО Д.Номенклатура = Ит.Номенклатура И Д.Характеристика = Ит.Характеристика И Д.Назначение = Ит.Назначение И Д.Склад = Ит.Склад И Д.Помещение = Ит.Помещение И Д.Серия = Ит.Серия ГДЕ ЕСТЬNULL(Д.ВНаличии, 0) - ЕСТЬNULL(Ит.ВНаличии, 0) <> 0
Почему так, а не проще:
- Полное соединение, потому что испорченная строка бывает у позиции, которой по движениям уже нет, и наоборот.
- Только активные движения: неактивные в итоги не попадают, и сравнивать надо с тем же.
- Строго меньше границы:
Остатки(&Граница)возвращает остаток на начало этой даты, движения ровно в 00:00:00 первого числа в него не входят. - Все измерения регистра в группировке и соединении. Если какое-то пропустить, разница сложится по нему, и две встречные ошибки погасят друг друга.
- Два параметра вместо одного: для месячной границы оба равны первому числу месяца, а для текущих итогов
Границапустая, иДвиженияДоберёт все движения.
Для своего регистра меняется имя регистра (два раза в запросе и один в коде обхода ниже), список измерений в четырёх местах и ресурс. Ресурсов несколько - на каждый своя колонка суммы и своё условие в последнем ГДЕ через ИЛИ. На большом регистре первая часть читает всю историю движений, поэтому добавьте одинаковый отбор по складу или номенклатуре в обе части: в первую условием ГДЕ, в Остатки вторым параметром. В выборку для краткости выведены номенклатура и склад; если у вас расходится по характеристике или серии, выведите и их, иначе две строки на экране будут выглядеть одинаково.
Запрос проверяет одну границу. Обходим их от ранней к поздней:
Менеджер = РегистрыНакопления.ТоварыНаСкладах; // Итоги рассчитаны по 31.08 - последняя хранимая строка месяца имеет период 01.09. МаксПериод = Менеджер.ПолучитьМаксимальныйПериодРассчитанныхИтогов(); // Пустая дата: итоги по месяцам не рассчитаны, остаётся только полный ПересчитатьИтоги(). Последняя = КонецМесяца(МаксПериод) + 1; Запрос = Новый Запрос(ТекстЗапроса); // текст запроса выше ПерваяСломанная = Неопределено; Граница = ДобавитьМесяц(Последняя, -12); МинПериод = Менеджер.ПолучитьМинимальныйПериодРассчитанныхИтогов(); Если ЗначениеЗаполнено(МинПериод) И Граница < НачалоМесяца(МинПериод) Тогда Граница = НачалоМесяца(МинПериод); // раньше минимального периода строк итогов нет КонецЕсли; Пока Граница <= Последняя Цикл Запрос.УстановитьПараметр("Граница", Граница); Запрос.УстановитьПараметр("ДвиженияДо", Граница); Расхождения = Запрос.Выполнить().Выгрузить(); Если Расхождения.Количество() > 0 И ПерваяСломанная = Неопределено Тогда ПерваяСломанная = Граница; КонецЕсли; Для Каждого Стр Из Расхождения Цикл Сообщить(Формат(Граница, "ДФ=dd.MM.yyyy") + " " + Стр.Номенклатура + ", " + Стр.Склад + ": движения " + Стр.ПоДвижениям + ", итоги " + Стр.ПоИтогам + ", разница " + Стр.Разница); КонецЦикла; Граница = ДобавитьМесяц(Граница, 1); КонецЦикла; // Текущие итоги: Остатки без даты против всех движений. Запрос.УстановитьПараметр("Граница", Неопределено); Запрос.УстановитьПараметр("ДвиженияДо", Дата(3999, 12, 31)); СломаныТекущие = Не Запрос.Выполнить().Пустой();
Дальше последней границы цикл не идёт: хранимых строк по месяцам там нет, проверять нечего. Глубина в 12 месяцев здесь для примера.
Какой месяц пересчитывать
Здесь мы сами ошиблись. Логика казалась очевидной: строка 1 августа хранит итог июля, значит, чинить её надо пересчётом за июль. Прогон на тестовой копии сказал другое:
| Что пересчитали при испорченной строке 01.08 | Время | Строка 01.08 |
|---|---|---|
ПересчитатьИтогиЗаПериод за июнь |
33 мс | осталась испорченной |
| за июль | 108 мс | осталась испорченной |
| за август | 116 и 85 мс (два раза) | починилась оба раза |
С последней строкой то же самое. Итоги рассчитаны по 31.08, но хранится и строка 01.09: это остаток на конец августа, а период у неё сентябрьский. Испорченную строку 01.09 не чинит ни пересчёт за август, ни за июнь-август. Чинит пересчёт за сентябрь, и максимальный период рассчитанных итогов от этого не сдвигается.
Правило получилось такое: ПересчитатьИтогиЗаПериод(Начало, Конец) перестраивает строки, чей период попадает в интервал. Строку на 1-е число месяца чинит пересчёт за этот же месяц. Если первая сломанная граница 01.08, пересчитываем с 01.08 по конец месяца последней границы: так заодно перестраиваются все более поздние строки, вдруг сломаны и они.
Второе, что показал тот же прогон: текущие итоги строятся от последней хранимой строки. При испорченной строке 01.09 вызов ПересчитатьТекущиеИтоги честно перенёс ту же ошибку и в текущие. Поэтому порядок жёсткий: сначала строки по месяцам, потом текущие.
Если ПерваяСломанная <> Неопределено Тогда // Строку 01.08 перестраивает пересчёт за август. Конец - месяц последней границы. Менеджер.ПересчитатьИтогиЗаПериод(ПерваяСломанная, КонецМесяца(Последняя)); КонецЕсли; Если СломаныТекущие Тогда // Только после строк по месяцам: текущие считаются от последней из них. Менеджер.ПересчитатьТекущиеИтоги(); КонецЕсли; // Контроль: тот же цикл по границам ещё раз. Расхождений быть не должно.
Пересчёт идёт по всему регистру за период, с отбором по складу или товару платформа его не умеет: ПересчитатьИтоги, ПересчитатьИтогиЗаПериод и ПересчитатьТекущиеИтоги отбора не принимают. Выигрыш метода только во времени: вместо всей истории перестраиваются один-два месяца. Запускать всё равно лучше вне рабочего времени. Права нужны "Управление итогами" на регистр.
Пример целиком
Тестовая копия УТ 11.5.22.67, платформа 8.3.27.1606, MS SQL 2022. Регистр ТоварыНаСкладах: 5 545 движений с июня по начало августа 2026, итоги рассчитаны по 31.08, разделения итогов нет, текущие итоги включены.
- На чистой базе цикл по четырём границам с 01.06 по 01.09 плюс текущие: расхождений ноль, весь проход 0,2 секунды.
- На тестовой копии изменили строку итогов 01.08 у одной позиции: плюс 7 к ВНаличии.
- Тот же цикл: 01.06 и 01.07 пусто, на 01.08 одна строка "движения 52, итоги 59, разница -7", 01.09 и текущие пусто. Первая сломанная граница 01.08, текущие целы.
ПересчитатьИтогиЗаПериод(01.08.2026, 30.09.2026): 153, 152 и 230 мс в трёх повторах. Перед каждым повтором строку портили заново, и каждый раз после пересчёта она возвращалась к 82 657, как до порчи. В первом повторе это показал контрольный цикл: ни одного расхождения.- Для сравнения при той же порче полный
ПересчитатьИтоги(): 309, 306 и 308 мс. Строка тоже починилась.
Разница вдвое, и на таком маленьком регистре она ничего не доказывает: пять с половиной тысяч движений за три месяца пересчитываются целиком за треть секунды. Как соотносятся времена на регистре с годами истории и миллионами строк, наш полигон не показывает. Если у вас есть такой замер, пишите в комментариях.
Где метод не работает
- Оборотные регистры. Таблицы
Остаткиу них нет. Сравнивать нужно виртуальную таблицуОборотыс периодичностью Месяц и движения, сгруппированные по началу месяца. Логика та же, запрос другой. В нашей УТ 11.5 из 32 оборотных регистров у 10 таблицы итогов нет совсем, им ломаться нечему. - Итоги выключены. Тогда
Остаткисам считает по движениям, и разница всегда ноль. Проверять нечего, сначала надо разобраться, почему итоги выключены. - Регистры бухгалтерии. Принцип тот же, но запрос сильно сложнее: движения раскладываются на дебет и кредит, у
Остаткиотдельные параметры для счетов и измерений, небалансовые ресурсы учитываются только по счетам с признаком учёта. Здесь этого запроса нет. - Разделение итогов. Запрос не меняется. При разделении платформа хранит итог одной позиции несколькими строками, чтобы параллельные проведения не ждали друг друга, а
Остаткискладывает их сам. Порчу на регистре с разделением итогов мы не проверяли. - Ошибка в самих движениях. Если движения документа неправильные, итоги им честно соответствуют, и запрос покажет ноль. Это другой класс проблем, его ловят проверкой движений, а не итогов.
Вопрос к тем, кто видел порчу итогов на живой базе: откуда она взялась? Мы меняли строку сами на тестовой копии, а сценария, при котором итоги расходятся с движениями сами, у нас нет.
Этот же обход границ и контрольная сверка собраны в обработку Чек-ап итогов и остатков 1С.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.