Mazmunga o'tish
Tez Base

Остатки в 1С не сходятся с движениями: как найти сломанный месяц итогов и не пересчитывать всю историю

· Chop etilgan:

Запросы и скорость

Картина знакомая. Ведомость по движениям показывает по товару 52 штуки, запрос к остаткам на то же число возвращает 59. Документы перепроводят, число не меняется. Кто-то предлагает пересчитать итоги, кто-то возражает, что на этой базе полный пересчёт идёт всю ночь и никто не обещает, что он поможет.

Помочь он может, но почти всегда это стрельба из пушки по воробьям. Поломка итогов обычно сидит в одной строке: у одной позиции, на одну дату. Найти её можно запросом за секунды, а починить пересчётом за один-два месяца. Ниже весь путь на типовом регистре ТоварыНаСкладах из УТ 11.5, с кодом и цифрами прогона на тестовой копии, где мы сами испортили строку итогов.

Сначала понять, что сломано

Расхождение между отчётом по движениям и остатками бывает по трём разным причинам, и лечатся они по-разному:

Что видимВероятная причинаЧем проверить
остаток неверный, но совпадает с суммой движенийошибка в самих движениях документапроверка движений: регистраторы, ссылки, непроведённые документы
сумма движений с неактивными записями не равна остаткунеактивные движения в итоги не входят, и это правильносравнивать только с активными
остаток не равен сумме активных движенийиспорчена строка итоговзапрос ниже

Третий случай и есть наш. Перепроведение его не лечит по простой причине. Когда платформа записывает набор движений, она меняет итоги на разницу между старым и новым набором. Если движения после перепроведения те же, разница равна нулю, и испорченная строка остаётся как была. На копии мы это проверили напрямую: прочитали и записали заново наборы всех трёх документов, у которых были движения в июле, всего 1 800 записей.

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

// Те же движения: итоги не изменились, испорченная строка осталась.
Набор.Записать();

Строка итогов после этого осталась испорченной.

Как выглядит строка итогов изнутри

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

Посмотреть на эти строки можно из 1С: имя таблицы итогов и её поля даёт ПолучитьСтруктуруХраненияБазыДанных. Код для серверного контекста или консоли кода:

Объекты = Новый Массив;
Объекты.Добавить(Метаданные.РегистрыНакопления.ТоварыНаСкладах);
Для Каждого Таблица Из ПолучитьСтруктуруХраненияБазыДанных(Объекты, Истина) Цикл
	Если Таблица.Назначение = "Итоги" Тогда
		Для Каждого Поле Из Таблица.Поля Цикл
			Сообщить(Таблица.ИмяТаблицыХранения + "." + Поле.ИмяПоляХранения + " - " + Поле.ИмяПоля);
		КонецЦикла;
	КонецЕсли;
КонецЦикла;

А дальше на MS SQL один запрос, только чтение. Имена таблицы и поля ниже условные, подставьте свои из вывода кода выше:

-- Строки итогов регистра по периодам: сколько строк и какая сумма ресурса в каждой.
-- Только чтение. _AccumRgT31415 и _Fld31416 замените своими именами.
DECLARE @off int = ISNULL((SELECT TOP 1 Offset FROM dbo._YearOffset), 0);   -- смещение лет базы
DECLARE @cur datetime = DATEADD(year, @off, CONVERT(datetime, '3999-11-01', 120)); -- период текущих итогов

SELECT
    CASE WHEN _Period >= @cur THEN N'текущие итоги'
         ELSE CONVERT(nchar(10), DATEADD(year, -@off, _Period), 104) END AS [Период (дата 1С)],
    COUNT_BIG(*)   AS [Строк],
    SUM(_Fld31416) AS [Сумма ресурса]
FROM dbo._AccumRgT31415
GROUP BY _Period
ORDER BY _Period;

Текущие итоги платформа хранит с периодом 01.11.3999, к нему прибавляется смещение лет из _YearOffset, если оно в базе есть. На нашей тестовой УТ по ресурсу ВНаличии вышло так:

Период строкиЧто в ней хранитсяСуммаДвижения месяца перед ней
01.07остаток на конец июня87 081июнь, +87 081
01.08остаток на конец июля82 657июль, -4 424
01.09остаток на конец августа81 838август, -819
текущиеостаток на сейчас81 838

Главное, что надо запомнить из этой таблицы: строка с периодом 1 августа хранит итог июля. Дальше это сыграет.

Смотреть сюда SQL-ом полезно, менять нельзя. Испорченную строку на копии мы получили именно прямым UPDATE, и это ровно то, чего на рабочей базе делать не следует.

Запрос, который находит сломанную позицию

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

ТекстЗапроса =
"ВЫБРАТЬ
|	Т.Номенклатура, Т.Характеристика, Т.Назначение, Т.Склад, Т.Помещение, Т.Серия,
|	СУММА(ВЫБОР КОГДА Т.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
|		ТОГДА Т.ВНаличии ИНАЧЕ -Т.ВНаличии КОНЕЦ) КАК ВНаличии
|ПОМЕСТИТЬ ВТ_Движения
|ИЗ
|	РегистрНакопления.ТоварыНаСкладах КАК Т
|ГДЕ
|	Т.Активность
|	И Т.Период < &ДвиженияДо
|СГРУППИРОВАТЬ ПО
|	Т.Номенклатура, Т.Характеристика, Т.Назначение, Т.Склад, Т.Помещение, Т.Серия
|;
|ВЫБРАТЬ
|	О.Номенклатура, О.Характеристика, О.Назначение, О.Склад, О.Помещение, О.Серия,
|	О.ВНаличииОстаток КАК ВНаличии
|ПОМЕСТИТЬ ВТ_Итоги
|ИЗ
|	РегистрНакопления.ТоварыНаСкладах.Остатки(&Граница) КАК О
|;
|ВЫБРАТЬ
|	ЕСТЬNULL(Д.Номенклатура, Ит.Номенклатура) КАК Номенклатура,
|	ЕСТЬNULL(Д.Склад, Ит.Склад) КАК Склад,
|	ЕСТЬNULL(Д.ВНаличии, 0) КАК ПоДвижениям,
|	ЕСТЬNULL(Ит.ВНаличии, 0) КАК ПоИтогам,
|	ЕСТЬNULL(Д.ВНаличии, 0) - ЕСТЬNULL(Ит.ВНаличии, 0) КАК Разница
|ИЗ
|	ВТ_Движения КАК Д
|		ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Итоги КАК Ит
|		ПО Д.Номенклатура = Ит.Номенклатура
|			И Д.Характеристика = Ит.Характеристика
|			И Д.Назначение = Ит.Назначение
|			И Д.Склад = Ит.Склад
|			И Д.Помещение = Ит.Помещение
|			И Д.Серия = Ит.Серия
|ГДЕ
|	ЕСТЬNULL(Д.ВНаличии, 0) - ЕСТЬNULL(Ит.ВНаличии, 0) <> 0";

Пять мест, где проще написать неправильно:

  1. Соединение полное. Испорченная строка бывает у позиции, движений по которой уже нет, и наоборот. Левое соединение одну из сторон потеряет.
  2. Условие Т.Активность. Неактивные движения в итоги не попадают, значит и в эталон их брать нельзя.
  3. Строгое “меньше”. Остатки(&Граница) даёт остаток на начало указанного момента, движения ровно в полночь первого числа в него не входят.
  4. Все измерения. Пропустите хотя бы одно, и разница сложится по нему: две встречные ошибки по разным сериям дадут ноль.
  5. Два параметра. Для месячной границы оба равны первому числу. Для текущих итогов Граница пустая, а ДвиженияДо дальше любого движения.

Под свой регистр меняются имя регистра, список измерений (в группировке, в обеих выборках и в условии соединения) и ресурс. На каждый дополнительный ресурс своя колонка суммы и своё условие через ИЛИ в последнем ГДЕ. На большом регистре первая часть читает всю историю движений, поэтому добавьте одинаковый отбор по складу или номенклатуре в обе части: условием в первую, вторым параметром в Остатки. И если расхождение может быть по характеристике или серии, выведите их в выборку, иначе две строки результата будут выглядеть одинаково.

Обход границ месяца

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

Менеджер = РегистрыНакопления.ТоварыНаСкладах;
// Итоги рассчитаны по 31.08 - последняя хранимая строка месяца имеет период 01.09.
МаксПериод = Менеджер.ПолучитьМаксимальныйПериодРассчитанныхИтогов();
// Пустая дата: итоги по месяцам не рассчитаны, остаётся только полный ПересчитатьИтоги().
Последняя = КонецМесяца(МаксПериод) + 1;

Запрос = Новый Запрос(ТекстЗапроса);
ПерваяСломанная = Неопределено;

Граница = ДобавитьМесяц(Последняя, -12);
МинПериод = Менеджер.ПолучитьМинимальныйПериодРассчитанныхИтогов();
Если ЗначениеЗаполнено(МинПериод) И Граница < НачалоМесяца(МинПериод) Тогда
	Граница = НачалоМесяца(МинПериод);   // раньше минимального периода строк итогов нет
КонецЕсли;
Пока Граница <= Последняя Цикл
	Запрос.УстановитьПараметр("Граница", Граница);
	Запрос.УстановитьПараметр("ДвиженияДо", Граница);
	Расхождения = Запрос.Выполнить().Выгрузить();
	Если Расхождения.Количество() > 0 И ПерваяСломанная = Неопределено Тогда
		ПерваяСломанная = Граница;
	КонецЕсли;
	Для Каждого Стр Из Расхождения Цикл
		Сообщить(Формат(Граница, "ДФ=dd.MM.yyyy") + " " + Стр.Номенклатура + ", " + Стр.Склад
			+ ": движения " + Стр.ПоДвижениям + ", итоги " + Стр.ПоИтогам + ", разница " + Стр.Разница);
	КонецЦикла;
	Граница = ДобавитьМесяц(Граница, 1);
КонецЦикла;

// Текущие итоги: Остатки без даты против всех движений.
Запрос.УстановитьПараметр("Граница", Неопределено);
Запрос.УстановитьПараметр("ДвиженияДо", Дата(3999, 12, 31));
СломаныТекущие = Не Запрос.Выполнить().Пустой();

Глубина в двенадцать месяцев взята для примера. Дальше последней границы идти незачем: строк по месяцам там нет.

Какой месяц пересчитывать

Интуиция подсказывает: строка 1 августа хранит итог июля, значит, чинить её пересчётом за июль. Мы тоже так думали, пока не проверили. Испортили строку 01.08 (плюс 7 у одной позиции) и пересчитывали разные месяцы:

Сломана строкаПересчиталиВремяРезультат
01.08июнь33 мсне починилась
01.08июль108 мсне починилась
01.08август116 и 85 мспочинилась, оба раза
01.09 (последняя хранимая)август84 мсне починилась
01.09июнь-август157 мсне починилась
01.09сентябрь79 мспочинилась

Вывод простой и проверяемый: ПересчитатьИтогиЗаПериод(Начало, Конец) перестраивает строки, у которых период попадает между Начало и Конец. Строку на 1-е число месяца чинит пересчёт за этот самый месяц. Последняя строка тоже не исключение: при итогах, рассчитанных по 31.08, строку 01.09 чинит пересчёт за сентябрь, и максимальный период рассчитанных итогов при этом остаётся 31.08.

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

Текущие итоги строго последними

Текущие итоги платформа строит от последней хранимой строки по месяцам. Мы проверили это неприятным способом: при испорченной строке 01.09 вызвали ПересчитатьТекущиеИтоги(), и ошибка аккуратно переехала в текущие. Отсюда жёсткий порядок:

Если ПерваяСломанная <> Неопределено Тогда
	// Строку 01.08 перестраивает пересчёт за август. Конец - месяц последней границы.
	Менеджер.ПересчитатьИтогиЗаПериод(ПерваяСломанная, КонецМесяца(Последняя));
КонецЕсли;
Если СломаныТекущие Тогда
	// Только после строк по месяцам: текущие считаются от последней из них.
	Менеджер.ПересчитатьТекущиеИтоги();
КонецЕсли;

// Контроль: тот же цикл по границам ещё раз. Расхождений быть не должно.

Ни один из трёх методов пересчёта не принимает отбор по складу или товару, они всегда работают по регистру целиком. Выигрыш только во времени: месяц-два вместо всей истории. Права нужны “Управление итогами”, запускать лучше вне рабочего времени.

Цифры прогона

Тестовая копия УТ 11.5.22.67, платформа 8.3.27.1606, MS SQL 2022. В регистре 5 545 движений с июня по начало августа, итоги рассчитаны по 31.08, разделения итогов нет, текущие итоги включены.

ШагРезультат
обход четырёх границ и текущих на чистой базерасхождений ноль, весь проход около 0,2 с
порча строки 01.08 у одной позицииплюс 7 к ВНаличии
обход после порчиодна строка на 01.08: движения 52, итоги 59, разница -7; остальные границы и текущие чистые
ПересчитатьИтогиЗаПериод(01.08.2026, 30.09.2026)153, 152 и 230 мс в трёх повторах, строка вернулась к 82 657
полный ПересчитатьИтоги() при той же порче309, 306 и 308 мс, строка тоже починилась

Разница вдвое на таком регистре ничего не доказывает: пять с половиной тысяч движений за три месяца пересчитываются целиком за треть секунды. На регистре с многолетней историей соотношение будет другим, но замера на таком объёме у нас нет. Если у вас есть, поделитесь, нам интересно.

Где этот способ не подходит

СлучайЧто не такЧто делать
оборотный регистрвиртуальной таблицы Остатки нетсравнивать Обороты с периодичностью Месяц и движения, сгруппированные по началу месяца
итоги выключеныОстатки считается по движениям, разница всегда нольсначала выяснить, почему итоги выключены
регистр бухгалтериидебет и кредит, отдельные параметры для счетов, небалансовые ресурсыпринцип тот же, запрос заметно сложнее, здесь его нет
разделение итоговитог одной позиции хранится несколькими строкамизапрос не меняется, Остатки складывает строки сам; порчу при разделении мы не проверяли
ошибка в движенияхитоги честно соответствуют движениямзапрос покажет ноль, проверять надо движения

У оборотных регистров есть своя тонкость: в УТ 11.5 из 32 оборотных регистров у 10 таблицы итогов нет вовсе, ломаться там нечему.

То же самое кнопкой

Обход границ, план пересчёта, сам пересчёт и контрольная сверка собраны в обработку Чек-ап итогов и остатков 1С. Она умеет то, что в статье осталось за кадром: оборотные регистры и бухгалтерию по счетам, отбор по измерениям и оценку объёма до запуска, обзор настроек итогов по всем регистрам базы, проверку самих движений, снимки закрытых периодов и SQL-скрипты для администратора СУБД. Если итоги ломаются вместе с дедлоками при проведении, загляните в разбор конфликтов на таблице итогов.

А если ночного окна под пересчёт нет и разбираться в регистрах некогда, напишите нам в WhatsApp: найдём сломанные строки и пересчитаем только то, что нужно.

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

Найдём регистр, позицию и месяц поломки, пересчитаем только нужный период и покажем сверку до и после

Narxi
450 000 ₸ danbir martalik sozlash; kuzatib borish - oyiga 150 000 ₸ dan; soliqlarsiz
Muddati
от 1 дня

Yozishga erta? O'zingiz o'lchang

Чек-ап итогов и остатков 1С: месяц, позиция и короткий пересчёт

Ishlov berishni ochish

INFOSTART TECH EVENT 2026: конференция для 1с-специалистов. если собираетесь, регистрируйтесь по нашей ссылке. Регистрация →

Buni ham o'qing

База 1С не открывается после обновления конфигурации: порядок действий, когда копии нет

Конфигуратор пишет "При обновлении данных, после последней реструктуризации, произошла критическая ошибка", пользователи в базу не попадают, копии до обновления нет. Разбираем по шагам: что сохранить первым, как по журналу регистрации найти этап, на котором упало, какие действия только добавляют ошибок, когда бросать починку старой базы и как через ibcmd выгрузить .dt из базы, которая не открывается, собрать новую и тихо подменить её в кластере. В конце - задания резервного копирования, после которых такая авария превращается в откат на минуту до обновления.

CHECKDB нашёл ошибки в базе 1С: порядок действий до REPAIR и после

CHECKDB советует repair_allow_data_loss и на битый индекс, и на потерянные документы. Разбираем по шагам, что делать в первый час: что сохранить до исправления, как по таблице и номеру индекса понять, что задето в 1С, как перестроить битый индекс без потерь и как получить список пропавших документов, пока REPAIR не удалил их молча. Проверено на копии УТ для Казахстана с испорченными страницами.

Скрипт нейросети за ночь пометил 5 700 документов в рабочей базе 1С: где были двери и как их закрыть

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